@json-layout/core 2.4.0 → 2.5.1

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.
Files changed (49) hide show
  1. package/package.json +8 -1
  2. package/src/i18n/de.js +0 -19
  3. package/src/i18n/en.js +0 -19
  4. package/src/i18n/fr.js +0 -19
  5. package/src/i18n/nl.js +0 -19
  6. package/src/i18n/types.ts +0 -19
  7. package/src/state/index.js +12 -11
  8. package/src/state/state-node.js +19 -13
  9. package/src/state/state-tree.js +13 -1
  10. package/src/state/types.ts +5 -2
  11. package/src/webmcp/index.js +283 -0
  12. package/src/webmcp/project.js +148 -0
  13. package/src/webmcp/resolve.js +25 -0
  14. package/src/webmcp/tools/describe-state.js +74 -0
  15. package/src/webmcp/tools/fill-form-skill.js +67 -0
  16. package/src/webmcp/tools/get-data.js +36 -0
  17. package/src/webmcp/tools/get-field-suggestions.js +84 -0
  18. package/src/webmcp/tools/set-data.js +56 -0
  19. package/src/webmcp/tools/set-field-value.js +71 -0
  20. package/types/i18n/de.d.ts +0 -19
  21. package/types/i18n/en.d.ts +0 -19
  22. package/types/i18n/fr.d.ts +0 -19
  23. package/types/i18n/nl.d.ts +0 -19
  24. package/types/i18n/types.d.ts +0 -19
  25. package/types/i18n/types.d.ts.map +1 -1
  26. package/types/state/index.d.ts.map +1 -1
  27. package/types/state/state-node.d.ts +2 -1
  28. package/types/state/state-node.d.ts.map +1 -1
  29. package/types/state/state-tree.d.ts.map +1 -1
  30. package/types/state/types.d.ts +5 -2
  31. package/types/state/types.d.ts.map +1 -1
  32. package/types/webmcp/index.d.ts +78 -0
  33. package/types/webmcp/index.d.ts.map +1 -0
  34. package/types/webmcp/project.d.ts +59 -0
  35. package/types/webmcp/project.d.ts.map +1 -0
  36. package/types/webmcp/resolve.d.ts +11 -0
  37. package/types/webmcp/resolve.d.ts.map +1 -0
  38. package/types/webmcp/tools/describe-state.d.ts +70 -0
  39. package/types/webmcp/tools/describe-state.d.ts.map +1 -0
  40. package/types/webmcp/tools/fill-form-skill.d.ts +24 -0
  41. package/types/webmcp/tools/fill-form-skill.d.ts.map +1 -0
  42. package/types/webmcp/tools/get-data.d.ts +31 -0
  43. package/types/webmcp/tools/get-data.d.ts.map +1 -0
  44. package/types/webmcp/tools/get-field-suggestions.d.ts +66 -0
  45. package/types/webmcp/tools/get-field-suggestions.d.ts.map +1 -0
  46. package/types/webmcp/tools/set-data.d.ts +65 -0
  47. package/types/webmcp/tools/set-data.d.ts.map +1 -0
  48. package/types/webmcp/tools/set-field-value.d.ts +73 -0
  49. package/types/webmcp/tools/set-field-value.d.ts.map +1 -0
@@ -0,0 +1,283 @@
1
+ /**
2
+ * @file WebMCP integration for JSON Layout StatefulLayout
3
+ * @description Provides MCP tool descriptors that work with browser's navigator.modelContext
4
+ */
5
+
6
+ import debug from 'debug'
7
+
8
+ import * as describeState from './tools/describe-state.js'
9
+ import * as setFieldValue from './tools/set-field-value.js'
10
+ import * as setData from './tools/set-data.js'
11
+ import * as getData from './tools/get-data.js'
12
+ import * as getFieldSuggestions from './tools/get-field-suggestions.js'
13
+ import * as fillFormSkill from './tools/fill-form-skill.js'
14
+
15
+ /** @typedef {import('@mcp-b/webmcp-types').ToolDescriptor} ToolDescriptor */
16
+
17
+ const log = debug('jl:webmcp')
18
+
19
+ /**
20
+ * @typedef {object} WebMCPOptions
21
+ * @property {string} [prefixName] - Prefix for all tool names
22
+ * @property {string} [dataTitle] - Title used in descriptions (default: 'form')
23
+ * @property {object} [schema] - The original JSON schema
24
+ */
25
+
26
+ /**
27
+ * WebMCP class that provides MCP tool descriptors for a StatefulLayout instance
28
+ */
29
+ export class WebMCP {
30
+ /**
31
+ * @readonly
32
+ * @type {import('../state/index.js').StatefulLayout}
33
+ */
34
+ _statefulLayout
35
+
36
+ /**
37
+ * @readonly
38
+ * @type {string}
39
+ */
40
+ _prefixName
41
+
42
+ /**
43
+ * @readonly
44
+ * @type {string}
45
+ */
46
+ _dataTitle
47
+
48
+ /**
49
+ * @readonly
50
+ * @type {string}
51
+ */
52
+ _skill
53
+
54
+ /**
55
+ * @readonly
56
+ * @type {object | null}
57
+ */
58
+ _schema = null
59
+
60
+ /**
61
+ * @type {string[]}
62
+ */
63
+ _registeredTools = []
64
+
65
+ /**
66
+ * @param {import('../state/index.js').StatefulLayout} statefulLayout
67
+ * @param {WebMCPOptions} [options]
68
+ */
69
+ constructor (statefulLayout, options = {}) {
70
+ this._statefulLayout = statefulLayout
71
+ this._prefixName = options.prefixName || ''
72
+ this._dataTitle = options.dataTitle || 'form'
73
+ this._schema = options.schema || null
74
+ this._skill = fillFormSkill.generateSkill(this._dataTitle, this._prefixName, !!this._schema, this._statefulLayout)
75
+ }
76
+
77
+ /**
78
+ * @param {string} name
79
+ * @returns {string}
80
+ */
81
+ _toolName (name) {
82
+ return this._prefixName + name
83
+ }
84
+
85
+ /**
86
+ * @returns {ToolDescriptor[]}
87
+ */
88
+ getTools () {
89
+ const dataTitle = this._dataTitle
90
+
91
+ /** @type {ToolDescriptor[]} */
92
+ const tools = [
93
+ {
94
+ name: this._toolName('fillFormSkill'),
95
+ description: fillFormSkill.getDescription(dataTitle),
96
+ outputSchema: { type: 'string' },
97
+ execute: async (args) => {
98
+ try {
99
+ return {
100
+ content: [{ type: 'text', text: this._skill }]
101
+ }
102
+ } catch (err) {
103
+ const message = err instanceof Error ? err.message : String(err)
104
+ return {
105
+ content: [{ type: 'text', text: `Error: ${message}` }],
106
+ isError: true
107
+ }
108
+ }
109
+ }
110
+ },
111
+ {
112
+ name: this._toolName('getData'),
113
+ description: getData.getDescription(dataTitle),
114
+ inputSchema: getData.inputSchema,
115
+ outputSchema: getData.outputSchema,
116
+ execute: async (args) => {
117
+ try {
118
+ const result = getData.execute(this._statefulLayout, args || {})
119
+ return {
120
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
121
+ }
122
+ } catch (err) {
123
+ const message = err instanceof Error ? err.message : String(err)
124
+ return {
125
+ content: [{ type: 'text', text: `Error: ${message}` }],
126
+ isError: true
127
+ }
128
+ }
129
+ }
130
+ },
131
+ {
132
+ name: this._toolName('setData'),
133
+ description: setData.getDescription(dataTitle),
134
+ inputSchema: setData.inputSchema,
135
+ outputSchema: setData.outputSchema,
136
+ execute: async (args) => {
137
+ try {
138
+ if (!args?.data) {
139
+ throw new Error('data is required')
140
+ }
141
+ const result = setData.execute(
142
+ this._statefulLayout,
143
+ /** @type {{ data: unknown }} */(args)
144
+ )
145
+ return {
146
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
147
+ }
148
+ } catch (err) {
149
+ const message = err instanceof Error ? err.message : String(err)
150
+ return {
151
+ content: [{ type: 'text', text: `Error: ${message}` }],
152
+ isError: true
153
+ }
154
+ }
155
+ }
156
+ },
157
+ {
158
+ name: this._toolName('describeState'),
159
+ description: describeState.getDescription(dataTitle),
160
+ inputSchema: describeState.inputSchema,
161
+ outputSchema: describeState.outputSchema,
162
+ execute: async (args) => {
163
+ try {
164
+ const result = describeState.execute(this._statefulLayout, args || {})
165
+ return {
166
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
167
+ }
168
+ } catch (err) {
169
+ const message = err instanceof Error ? err.message : String(err)
170
+ return {
171
+ content: [{ type: 'text', text: `Error: ${message}` }],
172
+ isError: true
173
+ }
174
+ }
175
+ }
176
+ },
177
+ {
178
+ name: this._toolName('setFieldValue'),
179
+ description: setFieldValue.getDescription(dataTitle),
180
+ inputSchema: setFieldValue.inputSchema,
181
+ outputSchema: setFieldValue.outputSchema,
182
+ execute: async (args) => {
183
+ try {
184
+ if (!args?.path) {
185
+ throw new Error('path is required')
186
+ }
187
+ const result = setFieldValue.execute(
188
+ this._statefulLayout,
189
+ /** @type {{ path: string, value: unknown }} */(args)
190
+ )
191
+ return {
192
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
193
+ }
194
+ } catch (err) {
195
+ const message = err instanceof Error ? err.message : String(err)
196
+ return {
197
+ content: [{ type: 'text', text: `Error: ${message}` }],
198
+ isError: true
199
+ }
200
+ }
201
+ }
202
+ },
203
+ {
204
+ name: this._toolName('getFieldSuggestions'),
205
+ description: getFieldSuggestions.getDescription(dataTitle),
206
+ inputSchema: getFieldSuggestions.inputSchema,
207
+ outputSchema: getFieldSuggestions.outputSchema,
208
+ execute: async (args) => {
209
+ try {
210
+ if (!args?.path) {
211
+ throw new Error('path is required')
212
+ }
213
+ const result = await getFieldSuggestions.execute(
214
+ this._statefulLayout,
215
+ /** @type {{ path: string, query?: string }} */(args)
216
+ )
217
+ return {
218
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
219
+ }
220
+ } catch (err) {
221
+ const message = err instanceof Error ? err.message : String(err)
222
+ return {
223
+ content: [{ type: 'text', text: `Error: ${message}` }],
224
+ isError: true
225
+ }
226
+ }
227
+ }
228
+ }
229
+ ]
230
+
231
+ if (this._schema) {
232
+ tools.push({
233
+ name: this._toolName('getSchema'),
234
+ description: `Get the the JSON schema that governs "${dataTitle}" form.`,
235
+ outputSchema: {
236
+ type: 'object',
237
+ properties: {
238
+ jsonSchema: {},
239
+ }
240
+ },
241
+ execute: async (args) => {
242
+ return {
243
+ jsonSchema: this._schema
244
+ }
245
+ }
246
+ })
247
+ }
248
+
249
+ return tools
250
+ }
251
+
252
+ /**
253
+ * @returns {Promise<void>}
254
+ */
255
+ async registerTools () {
256
+ if (typeof navigator === 'undefined' || !navigator.modelContext) {
257
+ throw new Error('navigator.modelContext is not available')
258
+ }
259
+
260
+ const tools = this.getTools()
261
+ for (const tool of tools) {
262
+ log('registering tool:', tool.name)
263
+ // @ts-ignore - complex generic types from webmcp-types
264
+ await navigator.modelContext.registerTool(tool)
265
+ this._registeredTools.push(tool.name)
266
+ }
267
+ }
268
+
269
+ /**
270
+ * @returns {Promise<void>}
271
+ */
272
+ async unregisterTools () {
273
+ if (typeof navigator === 'undefined' || !navigator.modelContext) {
274
+ throw new Error('navigator.modelContext is not available')
275
+ }
276
+
277
+ for (const name of this._registeredTools) {
278
+ log('unregistering tool:', name)
279
+ await navigator.modelContext.unregisterTool(name)
280
+ }
281
+ this._registeredTools = []
282
+ }
283
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * @file Projection functions for webmcp tools
3
+ */
4
+
5
+ import { isItemsLayout } from '@json-layout/vocabulary'
6
+
7
+ const constraintKeys = {
8
+ 'number-field': ['min', 'max', 'step', 'precision'],
9
+ slider: ['min', 'max', 'step'],
10
+ 'date-picker': ['min', 'max', 'format'],
11
+ 'date-time-picker': ['min', 'max'],
12
+ 'time-picker': ['min', 'max'],
13
+ combobox: ['separator'],
14
+ 'number-combobox': ['separator']
15
+ }
16
+
17
+ /**
18
+ * @param {string} comp
19
+ * @returns {string[]|undefined}
20
+ */
21
+ function getConstraintKeys (comp) {
22
+ // @ts-ignore - complex union type not fully represented
23
+ return constraintKeys[comp]
24
+ }
25
+
26
+ /**
27
+ * @param {import('../state/types.js').StateNode} node
28
+ * @param {import('../state/index.js').StatefulLayout} statefulLayout
29
+ * @returns {{
30
+ * key: string|number,
31
+ * path: string,
32
+ * comp: string,
33
+ * data: unknown,
34
+ * title?: string,
35
+ * label?: string,
36
+ * help?: string,
37
+ * error?: string,
38
+ * childError?: boolean,
39
+ * required?: boolean,
40
+ * readOnly?: boolean,
41
+ * constraints?: Record<string, unknown>,
42
+ * oneOfItems?: Array<{key: number, title: string}>,
43
+ * children?: Array<any>
44
+ * getSuffections?: boolean
45
+ * }}
46
+ */
47
+ export function projectNode (node, statefulLayout) {
48
+ /**
49
+ * @type {{
50
+ * key: string|number,
51
+ * path: string,
52
+ * comp: string,
53
+ * data: unknown,
54
+ * title?: string,
55
+ * label?: string,
56
+ * help?: string,
57
+ * error?: string,
58
+ * childError?: boolean,
59
+ * required?: boolean,
60
+ * readOnly?: boolean,
61
+ * constraints?: Record<string, unknown>,
62
+ * oneOfItems?: Array<{key: number, title: string}>,
63
+ * children?: Array<any>
64
+ * getSuggestions?: boolean
65
+ * }}
66
+ */
67
+ const out = {
68
+ key: node.key,
69
+ path: node.fullKey,
70
+ comp: node.layout.comp,
71
+ data: node.data
72
+ }
73
+
74
+ const layout = /** @type {Record<string, unknown>} */(node.layout)
75
+ if (typeof layout.title === 'string') out.title = layout.title
76
+ if (typeof layout.label === 'string') out.label = layout.label
77
+ if (node.layout.help) out.help = node.layout.help
78
+
79
+ if (node.error) out.error = node.error
80
+ if (node.childError) out.childError = true
81
+
82
+ if (node.skeleton.required) out.required = true
83
+ if (node.options.readOnly) out.readOnly = true
84
+ if (isItemsLayout(node.layout, statefulLayout.compiledLayout.components)) out.getSuggestions = true
85
+
86
+ const keys = getConstraintKeys(node.layout.comp)
87
+ if (keys) {
88
+ /** @type {Record<string, unknown>} */
89
+ const constraints = {}
90
+ for (const k of keys) {
91
+ const v = layout[k]
92
+ if (v !== undefined && v !== null) constraints[k] = v
93
+ }
94
+ if (Object.keys(constraints).length > 0) out.constraints = constraints
95
+ }
96
+
97
+ if (node.layout.comp === 'one-of-select' && Array.isArray(layout.oneOfItems)) {
98
+ out.oneOfItems = layout.oneOfItems
99
+ .filter((item) => !item.header)
100
+ .map((item) => ({ key: item.key, title: item.title }))
101
+ }
102
+
103
+ if (node.children) {
104
+ out.children = node.children
105
+ .filter((c) => c.layout.comp !== 'none')
106
+ .map(node => projectNode(node, statefulLayout))
107
+ }
108
+
109
+ return out
110
+ }
111
+
112
+ /**
113
+ * @param {import('../state/types.js').StateTree} stateTree
114
+ * @param {import('../state/index.js').StatefulLayout} statefulLayout
115
+ * @returns {{ root: ReturnType<typeof projectNode>, valid: boolean }}
116
+ */
117
+ export function projectStateTree (stateTree, statefulLayout) {
118
+ return {
119
+ root: projectNode(stateTree.root, statefulLayout),
120
+ valid: stateTree.valid
121
+ }
122
+ }
123
+
124
+ /**
125
+ * @param {import('../state/types.js').StateNode} node
126
+ * @returns {Array<{path: string, message: string}>}
127
+ */
128
+ export function collectErrors (node) {
129
+ /** @type {Array<{path: string, message: string}>} */
130
+ const errors = []
131
+ collectErrorsRecurse(node, errors)
132
+ return errors
133
+ }
134
+
135
+ /**
136
+ * @param {import('../state/types.js').StateNode} node
137
+ * @param {Array<{path: string, message: string}>} errors
138
+ */
139
+ function collectErrorsRecurse (node, errors) {
140
+ if (node.error) {
141
+ errors.push({ path: node.fullKey, message: node.error })
142
+ }
143
+ if (node.children) {
144
+ for (const child of node.children) {
145
+ collectErrorsRecurse(child, errors)
146
+ }
147
+ }
148
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @file Node resolution for webmcp tools
3
+ */
4
+
5
+ /**
6
+ * Navigate from a root StateNode to a descendant node by path.
7
+ * @param {import('../state/types.js').StateNode} root
8
+ * @param {string} path
9
+ * @returns {import('../state/types.js').StateNode|undefined}
10
+ */
11
+ export function resolveNode (root, path) {
12
+ if (!path || path === '/') return root
13
+
14
+ const segments = path.replace(/^\//, '').split('/')
15
+ /** @type {import('../state/types.js').StateNode|undefined} */
16
+ let current = root
17
+
18
+ for (const segment of segments) {
19
+ if (!current?.children) return undefined
20
+ const key = /^\d+$/.test(segment) ? parseInt(segment, 10) : segment
21
+ current = current.children.find((c) => c.key === key)
22
+ }
23
+
24
+ return current
25
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @file describeState tool
3
+ */
4
+
5
+ import { projectStateTree, projectNode, collectErrors } from '../project.js'
6
+ import { resolveNode } from '../resolve.js'
7
+
8
+ export const inputSchema = {
9
+ type: 'object',
10
+ properties: {
11
+ path: {
12
+ type: 'string',
13
+ description: 'Path to a specific node (e.g. "/address/city"). Omit for full tree.'
14
+ }
15
+ }
16
+ }
17
+
18
+ export const outputSchema = {
19
+ type: 'object',
20
+ properties: {
21
+ state: {
22
+ type: 'object',
23
+ description: 'Projected state tree or single node'
24
+ },
25
+ valid: {
26
+ type: 'boolean'
27
+ },
28
+ errors: {
29
+ type: 'array',
30
+ items: {
31
+ type: 'object',
32
+ properties: {
33
+ path: { type: 'string' },
34
+ message: { type: 'string' }
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+ /**
42
+ * @param {string} dataTitle
43
+ * @returns {string}
44
+ */
45
+ export function getDescription (dataTitle) {
46
+ return `Describe the current "${dataTitle}" state tree. Optionally focus on a subtree by path to reduce output size.`
47
+ }
48
+
49
+ /**
50
+ * @param {import('../../state/index.js').StatefulLayout} statefulLayout
51
+ * @param {{ path?: string }} args
52
+ * @returns {{state: ReturnType<typeof projectStateTree>|ReturnType<typeof projectNode>, valid: boolean, errors: Array<{path: string, message: string}>}}
53
+ */
54
+ export function execute (statefulLayout, args) {
55
+ const errors = collectErrors(statefulLayout.stateTree.root)
56
+
57
+ if (args.path) {
58
+ const node = resolveNode(statefulLayout.stateTree.root, args.path)
59
+ if (!node) {
60
+ throw new Error(`node not found at path: ${args.path}`)
61
+ }
62
+ return {
63
+ state: projectNode(node, statefulLayout),
64
+ valid: statefulLayout.valid,
65
+ errors
66
+ }
67
+ }
68
+
69
+ return {
70
+ state: projectStateTree(statefulLayout.stateTree, statefulLayout),
71
+ valid: statefulLayout.valid,
72
+ errors
73
+ }
74
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @file fillFormSkill tool
3
+ */
4
+
5
+ export const outputSchema = {
6
+ type: 'object',
7
+ properties: {
8
+ content: { type: 'string' }
9
+ }
10
+ }
11
+
12
+ /**
13
+ * @param {string} dataTitle
14
+ * @returns {string}
15
+ */
16
+ export function getDescription (dataTitle) {
17
+ return `Get guidance on how to interact with the form "${dataTitle}" using the available tools.`
18
+ }
19
+
20
+ /**
21
+ * Generate skill content with dataTitle injected
22
+ * @param {string} dataTitle
23
+ * @param {string} prefixName
24
+ * @param {boolean} hasSchema
25
+ * @param {import('../../state/index.js').StatefulLayout} statefulLayout
26
+ * @returns {string}
27
+ */
28
+ export function generateSkill (dataTitle, prefixName, hasSchema, statefulLayout) {
29
+ /** @type {"small" | "medium" | "large"} */
30
+ let complexity = 'small'
31
+ const nbNormalizedLayouts = Object.keys(statefulLayout.compiledLayout.normalizedLayouts).length
32
+ if (nbNormalizedLayouts > 15) complexity = 'medium'
33
+ if (nbNormalizedLayouts > 50) complexity = 'large'
34
+ let skill = `# JSON ${dataTitle.charAt(0).toUpperCase() + dataTitle.slice(1)} Form-Filling Guide
35
+
36
+ This guide teaches you how to use tools to fill the data of a form in the user's page.
37
+
38
+ Always start by getting the current data using ${prefixName}getData.
39
+ `
40
+
41
+ if (complexity === 'small') {
42
+ skill += `
43
+ Given the small complexity of this form you should start by reading the full schema definition using ${prefixName}${hasSchema ? 'getSchema' : 'describeState'} and attempt updating the whole data using ${prefixName}setData.
44
+ Only use ${prefixName}describeState and iterate with ${prefixName}setFieldValue if you encounter some difficulties with ${prefixName}setData.
45
+ `
46
+ }
47
+
48
+ if (complexity === 'small') {
49
+ skill += `
50
+ Given the medium complexity of this form you should start by reading the full schema definition using ${prefixName}${hasSchema ? 'getSchema' : 'describeState'}, if you have a satisfying understanding of the schema you can attempt updating the whole data using ${prefixName}setData at least once.
51
+ Then use ${prefixName}describeState and iterate with ${prefixName}setFieldValue.
52
+ `
53
+ }
54
+
55
+ if (complexity === 'large') {
56
+ skill += `
57
+ Given the large complexity of this form you should avoid reading the full schema definition using ${prefixName}${hasSchema ? 'getSchema' : 'describeState'}.
58
+ Prefer using ${prefixName}describeState and iterating with ${prefixName}setFieldValue.
59
+ `
60
+ }
61
+
62
+ skill += `
63
+ If you encounter getItems definitions in the schema or getSuggestions flags in the state. You must use ${prefixName}getFieldSuggestions to fetch the accepted values.
64
+ `
65
+
66
+ return skill
67
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @file getData tool
3
+ */
4
+
5
+ export const inputSchema = {
6
+ type: 'object',
7
+ properties: {}
8
+ }
9
+
10
+ export const outputSchema = {
11
+ type: 'object',
12
+ properties: {
13
+ data: {},
14
+ valid: { type: 'boolean' }
15
+ }
16
+ }
17
+
18
+ /**
19
+ * @param {string} dataTitle
20
+ * @returns {string}
21
+ */
22
+ export function getDescription (dataTitle) {
23
+ return `Get the current "${dataTitle}" data and validity status.`
24
+ }
25
+
26
+ /**
27
+ * @param {import('../../state/index.js').StatefulLayout} statefulLayout
28
+ * @param {{}} _args
29
+ * @returns {{ data: unknown, valid: boolean }}
30
+ */
31
+ export function execute (statefulLayout, _args) {
32
+ return {
33
+ data: statefulLayout.data,
34
+ valid: statefulLayout.valid
35
+ }
36
+ }