@unrdf/kgc-runtime 26.4.2

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 (70) hide show
  1. package/IMPLEMENTATION_SUMMARY.json +150 -0
  2. package/PLUGIN_SYSTEM_SUMMARY.json +149 -0
  3. package/README.md +98 -0
  4. package/TRANSACTION_IMPLEMENTATION.json +119 -0
  5. package/capability-map.md +93 -0
  6. package/docs/api-stability.md +269 -0
  7. package/docs/extensions/plugin-development.md +382 -0
  8. package/package.json +40 -0
  9. package/plugins/registry.json +35 -0
  10. package/src/admission-gate.mjs +414 -0
  11. package/src/api-version.mjs +373 -0
  12. package/src/atomic-admission.mjs +310 -0
  13. package/src/bounds.mjs +289 -0
  14. package/src/bulkhead-manager.mjs +280 -0
  15. package/src/capsule.mjs +524 -0
  16. package/src/crdt.mjs +361 -0
  17. package/src/enhanced-bounds.mjs +614 -0
  18. package/src/executor.mjs +73 -0
  19. package/src/freeze-restore.mjs +521 -0
  20. package/src/index.mjs +62 -0
  21. package/src/materialized-views.mjs +371 -0
  22. package/src/merge.mjs +472 -0
  23. package/src/plugin-isolation.mjs +392 -0
  24. package/src/plugin-manager.mjs +441 -0
  25. package/src/projections-api.mjs +336 -0
  26. package/src/projections-cli.mjs +238 -0
  27. package/src/projections-docs.mjs +300 -0
  28. package/src/projections-ide.mjs +278 -0
  29. package/src/receipt.mjs +340 -0
  30. package/src/rollback.mjs +258 -0
  31. package/src/saga-orchestrator.mjs +355 -0
  32. package/src/schemas.mjs +1330 -0
  33. package/src/storage-optimization.mjs +359 -0
  34. package/src/tool-registry.mjs +272 -0
  35. package/src/transaction.mjs +466 -0
  36. package/src/validators.mjs +485 -0
  37. package/src/work-item.mjs +449 -0
  38. package/templates/plugin-template/README.md +58 -0
  39. package/templates/plugin-template/index.mjs +162 -0
  40. package/templates/plugin-template/plugin.json +19 -0
  41. package/test/admission-gate.test.mjs +583 -0
  42. package/test/api-version.test.mjs +74 -0
  43. package/test/atomic-admission.test.mjs +155 -0
  44. package/test/bounds.test.mjs +341 -0
  45. package/test/bulkhead-manager.test.mjs +236 -0
  46. package/test/capsule.test.mjs +625 -0
  47. package/test/crdt.test.mjs +215 -0
  48. package/test/enhanced-bounds.test.mjs +487 -0
  49. package/test/freeze-restore.test.mjs +472 -0
  50. package/test/materialized-views.test.mjs +243 -0
  51. package/test/merge.test.mjs +665 -0
  52. package/test/plugin-isolation.test.mjs +109 -0
  53. package/test/plugin-manager.test.mjs +208 -0
  54. package/test/projections-api.test.mjs +293 -0
  55. package/test/projections-cli.test.mjs +204 -0
  56. package/test/projections-docs.test.mjs +173 -0
  57. package/test/projections-ide.test.mjs +230 -0
  58. package/test/receipt.test.mjs +295 -0
  59. package/test/rollback.test.mjs +132 -0
  60. package/test/saga-orchestrator.test.mjs +279 -0
  61. package/test/schemas.test.mjs +716 -0
  62. package/test/storage-optimization.test.mjs +503 -0
  63. package/test/tool-registry.test.mjs +341 -0
  64. package/test/transaction.test.mjs +189 -0
  65. package/test/validators.test.mjs +463 -0
  66. package/test/work-item.test.mjs +548 -0
  67. package/test/work-item.test.mjs.bak +548 -0
  68. package/var/kgc/test-atomic-log.json +519 -0
  69. package/var/kgc/test-cascading-log.json +145 -0
  70. package/vitest.config.mjs +18 -0
@@ -0,0 +1,336 @@
1
+ /**
2
+ * @fileoverview Π_api - REST/GraphQL API Projections
3
+ * Transforms KGC structures into API-compatible formats with pagination and filtering
4
+ */
5
+
6
+ import { z } from 'zod';
7
+
8
+ /**
9
+ * API projection schema
10
+ */
11
+ export const APIProjectionSchema = z.object({
12
+ type: z.literal('api'),
13
+ format: z.enum(['rest', 'graphql', 'json-api']),
14
+ data: z.any(),
15
+ meta: z.object({
16
+ pagination: z.object({
17
+ page: z.number(),
18
+ pageSize: z.number(),
19
+ total: z.number(),
20
+ totalPages: z.number(),
21
+ }).optional(),
22
+ filters: z.record(z.any()).optional(),
23
+ sort: z.array(z.object({
24
+ field: z.string(),
25
+ direction: z.enum(['asc', 'desc']),
26
+ })).optional(),
27
+ }).optional(),
28
+ links: z.object({
29
+ self: z.string().optional(),
30
+ first: z.string().optional(),
31
+ prev: z.string().optional(),
32
+ next: z.string().optional(),
33
+ last: z.string().optional(),
34
+ }).optional(),
35
+ });
36
+
37
+ /**
38
+ * @typedef {z.infer<typeof APIProjectionSchema>} APIProjection
39
+ */
40
+
41
+ /**
42
+ * Pagination options
43
+ * @typedef {{page: number, pageSize: number}} PaginationOptions
44
+ */
45
+
46
+ /**
47
+ * Filter options
48
+ * @typedef {Record<string, any>} FilterOptions
49
+ */
50
+
51
+ /**
52
+ * Sort options
53
+ * @typedef {Array<{field: string, direction: 'asc' | 'desc'}>} SortOptions
54
+ */
55
+
56
+ /**
57
+ * Project receipts to paginated REST API format
58
+ * @param {Array<import('./receipt.mjs').Receipt>} receipts - Receipts to project
59
+ * @param {PaginationOptions} pagination - Pagination options
60
+ * @param {FilterOptions} [filters] - Filter options
61
+ * @param {SortOptions} [sort] - Sort options
62
+ * @returns {APIProjection} API projection
63
+ */
64
+ export function projectReceiptsToREST(receipts, pagination, filters = {}, sort = []) {
65
+ // Apply filters
66
+ let filtered = receipts;
67
+ for (const [key, value] of Object.entries(filters)) {
68
+ filtered = filtered.filter(r => r[key] === value);
69
+ }
70
+
71
+ // Apply sorting
72
+ if (sort.length > 0) {
73
+ filtered = filtered.sort((a, b) => {
74
+ for (const { field, direction } of sort) {
75
+ const aVal = a[field];
76
+ const bVal = b[field];
77
+ const comparison = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
78
+ if (comparison !== 0) {
79
+ return direction === 'asc' ? comparison : -comparison;
80
+ }
81
+ }
82
+ return 0;
83
+ });
84
+ }
85
+
86
+ // Apply pagination
87
+ const total = filtered.length;
88
+ const totalPages = Math.ceil(total / pagination.pageSize);
89
+ const start = (pagination.page - 1) * pagination.pageSize;
90
+ const end = start + pagination.pageSize;
91
+ const paginated = filtered.slice(start, end);
92
+
93
+ // Serialize receipts
94
+ const data = paginated.map(r => ({
95
+ id: r.id,
96
+ type: 'receipt',
97
+ attributes: {
98
+ timestamp: r.timestamp,
99
+ operation: r.operation,
100
+ hash: r.hash,
101
+ parentHash: r.parentHash,
102
+ },
103
+ relationships: r.parentHash ? {
104
+ parent: {
105
+ data: { type: 'receipt', id: r.parentHash },
106
+ },
107
+ } : undefined,
108
+ }));
109
+
110
+ return APIProjectionSchema.parse({
111
+ type: 'api',
112
+ format: 'rest',
113
+ data,
114
+ meta: {
115
+ pagination: {
116
+ page: pagination.page,
117
+ pageSize: pagination.pageSize,
118
+ total,
119
+ totalPages,
120
+ },
121
+ filters,
122
+ sort,
123
+ },
124
+ links: {
125
+ self: `/receipts?page=${pagination.page}`,
126
+ first: '/receipts?page=1',
127
+ prev: pagination.page > 1 ? `/receipts?page=${pagination.page - 1}` : undefined,
128
+ next: pagination.page < totalPages ? `/receipts?page=${pagination.page + 1}` : undefined,
129
+ last: `/receipts?page=${totalPages}`,
130
+ },
131
+ });
132
+ }
133
+
134
+ /**
135
+ * Project work items to GraphQL format
136
+ * @param {Array<object>} workItems - Work items
137
+ * @param {object} [args] - GraphQL query arguments
138
+ * @returns {APIProjection} API projection
139
+ */
140
+ export function projectWorkItemsToGraphQL(workItems, args = {}) {
141
+ // Apply GraphQL-style filtering
142
+ let filtered = workItems;
143
+
144
+ if (args.where) {
145
+ filtered = filtered.filter(item => {
146
+ for (const [key, value] of Object.entries(args.where)) {
147
+ if (item[key] !== value) return false;
148
+ }
149
+ return true;
150
+ });
151
+ }
152
+
153
+ // Apply GraphQL-style ordering
154
+ if (args.orderBy) {
155
+ filtered = filtered.sort((a, b) => {
156
+ const field = args.orderBy.field;
157
+ const direction = args.orderBy.direction || 'ASC';
158
+ const comparison = a[field] < b[field] ? -1 : a[field] > b[field] ? 1 : 0;
159
+ return direction === 'ASC' ? comparison : -comparison;
160
+ });
161
+ }
162
+
163
+ // Apply GraphQL-style pagination (skip/take)
164
+ if (args.skip !== undefined) {
165
+ filtered = filtered.slice(args.skip);
166
+ }
167
+ if (args.take !== undefined) {
168
+ filtered = filtered.slice(0, args.take);
169
+ }
170
+
171
+ const data = {
172
+ workItems: filtered.map(item => ({
173
+ id: item.id,
174
+ goal: item.goal,
175
+ state: item.state,
176
+ priority: item.priority,
177
+ createdAt: item.createdAt,
178
+ updatedAt: item.updatedAt,
179
+ })),
180
+ totalCount: workItems.length,
181
+ filteredCount: filtered.length,
182
+ };
183
+
184
+ return APIProjectionSchema.parse({
185
+ type: 'api',
186
+ format: 'graphql',
187
+ data,
188
+ meta: {
189
+ filters: args.where,
190
+ sort: args.orderBy ? [args.orderBy] : undefined,
191
+ },
192
+ });
193
+ }
194
+
195
+ /**
196
+ * Project single resource to JSON:API format
197
+ * @param {object} resource - Resource object
198
+ * @param {string} resourceType - Resource type
199
+ * @param {Record<string, any>} [relationships] - Resource relationships
200
+ * @returns {APIProjection} API projection
201
+ */
202
+ export function projectResourceToJSONAPI(resource, resourceType, relationships = {}) {
203
+ const { id, ...attributes } = resource;
204
+
205
+ const relationshipData = {};
206
+ for (const [key, value] of Object.entries(relationships)) {
207
+ if (Array.isArray(value)) {
208
+ relationshipData[key] = {
209
+ data: value.map(v => ({ type: v.type, id: v.id })),
210
+ };
211
+ } else if (value) {
212
+ relationshipData[key] = {
213
+ data: { type: value.type, id: value.id },
214
+ };
215
+ }
216
+ }
217
+
218
+ return APIProjectionSchema.parse({
219
+ type: 'api',
220
+ format: 'json-api',
221
+ data: {
222
+ type: resourceType,
223
+ id,
224
+ attributes,
225
+ relationships: Object.keys(relationshipData).length > 0 ? relationshipData : undefined,
226
+ },
227
+ links: {
228
+ self: `/${resourceType}/${id}`,
229
+ },
230
+ });
231
+ }
232
+
233
+ /**
234
+ * Apply filtering to array of objects
235
+ * @param {Array<Record<string, any>>} items - Items to filter
236
+ * @param {FilterOptions} filters - Filter criteria
237
+ * @returns {Array<Record<string, any>>} Filtered items
238
+ */
239
+ export function applyFilters(items, filters) {
240
+ if (!filters || Object.keys(filters).length === 0) {
241
+ return items;
242
+ }
243
+
244
+ return items.filter(item => {
245
+ for (const [key, value] of Object.entries(filters)) {
246
+ // Support operators: eq, ne, gt, lt, contains, in
247
+ if (typeof value === 'object' && value !== null) {
248
+ const itemValue = item[key];
249
+
250
+ if ('eq' in value && itemValue !== value.eq) return false;
251
+ if ('ne' in value && itemValue === value.ne) return false;
252
+ if ('gt' in value && itemValue <= value.gt) return false;
253
+ if ('lt' in value && itemValue >= value.lt) return false;
254
+ if ('contains' in value && !String(itemValue).includes(value.contains)) return false;
255
+ if ('in' in value && !value.in.includes(itemValue)) return false;
256
+ } else {
257
+ if (item[key] !== value) return false;
258
+ }
259
+ }
260
+ return true;
261
+ });
262
+ }
263
+
264
+ /**
265
+ * Apply sorting to array of objects
266
+ * @param {Array<Record<string, any>>} items - Items to sort
267
+ * @param {SortOptions} sort - Sort criteria
268
+ * @returns {Array<Record<string, any>>} Sorted items
269
+ */
270
+ export function applySorting(items, sort) {
271
+ if (!sort || sort.length === 0) {
272
+ return items;
273
+ }
274
+
275
+ return [...items].sort((a, b) => {
276
+ for (const { field, direction } of sort) {
277
+ const aVal = a[field];
278
+ const bVal = b[field];
279
+
280
+ let comparison = 0;
281
+ if (aVal < bVal) comparison = -1;
282
+ else if (aVal > bVal) comparison = 1;
283
+
284
+ if (comparison !== 0) {
285
+ return direction === 'asc' ? comparison : -comparison;
286
+ }
287
+ }
288
+ return 0;
289
+ });
290
+ }
291
+
292
+ /**
293
+ * Apply pagination to array
294
+ * @param {Array<any>} items - Items to paginate
295
+ * @param {PaginationOptions} pagination - Pagination options
296
+ * @returns {{data: Array<any>, meta: object}} Paginated result
297
+ */
298
+ export function applyPagination(items, pagination) {
299
+ const total = items.length;
300
+ const totalPages = Math.ceil(total / pagination.pageSize);
301
+ const start = (pagination.page - 1) * pagination.pageSize;
302
+ const end = start + pagination.pageSize;
303
+ const data = items.slice(start, end);
304
+
305
+ return {
306
+ data,
307
+ meta: {
308
+ pagination: {
309
+ page: pagination.page,
310
+ pageSize: pagination.pageSize,
311
+ total,
312
+ totalPages,
313
+ },
314
+ },
315
+ };
316
+ }
317
+
318
+ /**
319
+ * Build HATEOAS links for resource
320
+ * @param {string} resourceType - Resource type
321
+ * @param {string} resourceId - Resource ID
322
+ * @param {Array<{rel: string, href: string}>} [additional=[]] - Additional links
323
+ * @returns {Record<string, string>} HATEOAS links
324
+ */
325
+ export function buildHATEOASLinks(resourceType, resourceId, additional = []) {
326
+ const links = {
327
+ self: `/${resourceType}/${resourceId}`,
328
+ collection: `/${resourceType}`,
329
+ };
330
+
331
+ for (const link of additional) {
332
+ links[link.rel] = link.href;
333
+ }
334
+
335
+ return links;
336
+ }
@@ -0,0 +1,238 @@
1
+ /**
2
+ * @fileoverview Π_cli - Command-line Interface Projections
3
+ * Transforms KGC data structures into CLI-friendly formats with ANSI colors and tables
4
+ */
5
+
6
+ import { z } from 'zod';
7
+
8
+ /**
9
+ * ANSI color codes for terminal output
10
+ * @constant
11
+ */
12
+ const COLORS = {
13
+ reset: '\x1b[0m',
14
+ bright: '\x1b[1m',
15
+ dim: '\x1b[2m',
16
+ red: '\x1b[31m',
17
+ green: '\x1b[32m',
18
+ yellow: '\x1b[33m',
19
+ blue: '\x1b[34m',
20
+ magenta: '\x1b[35m',
21
+ cyan: '\x1b[36m',
22
+ white: '\x1b[37m',
23
+ };
24
+
25
+ /**
26
+ * CLI projection schema
27
+ */
28
+ export const CLIProjectionSchema = z.object({
29
+ type: z.literal('cli'),
30
+ format: z.enum(['table', 'list', 'tree', 'summary']),
31
+ colored: z.boolean().default(true),
32
+ content: z.string(),
33
+ metadata: z.record(z.any()).optional(),
34
+ });
35
+
36
+ /**
37
+ * @typedef {z.infer<typeof CLIProjectionSchema>} CLIProjection
38
+ */
39
+
40
+ /**
41
+ * Colorize text with ANSI codes
42
+ * @param {string} text - Text to colorize
43
+ * @param {keyof typeof COLORS} color - Color name
44
+ * @param {boolean} colored - Whether to apply colors
45
+ * @returns {string} Colorized text
46
+ */
47
+ function colorize(text, color, colored = true) {
48
+ if (!colored) return text;
49
+ return `${COLORS[color]}${text}${COLORS.reset}`;
50
+ }
51
+
52
+ /**
53
+ * Project receipt to CLI-friendly format
54
+ * @param {import('./receipt.mjs').Receipt} receipt - Receipt to project
55
+ * @param {boolean} [colored=true] - Whether to use ANSI colors
56
+ * @returns {CLIProjection} CLI projection
57
+ */
58
+ export function projectReceiptToCLI(receipt, colored = true) {
59
+ const lines = [
60
+ colorize('━'.repeat(60), 'cyan', colored),
61
+ colorize('Receipt:', 'bright', colored) + ` ${receipt.id}`,
62
+ colorize('Operation:', 'blue', colored) + ` ${receipt.operation}`,
63
+ colorize('Timestamp:', 'dim', colored) + ` ${receipt.timestamp}`,
64
+ colorize('Hash:', 'magenta', colored) + ` ${receipt.hash.slice(0, 16)}...`,
65
+ ];
66
+
67
+ if (receipt.parentHash) {
68
+ lines.push(colorize('Parent:', 'dim', colored) + ` ${receipt.parentHash.slice(0, 16)}...`);
69
+ }
70
+
71
+ lines.push(colorize('━'.repeat(60), 'cyan', colored));
72
+
73
+ const content = lines.join('\n');
74
+
75
+ return CLIProjectionSchema.parse({
76
+ type: 'cli',
77
+ format: 'summary',
78
+ colored,
79
+ content,
80
+ metadata: { receiptId: receipt.id },
81
+ });
82
+ }
83
+
84
+ /**
85
+ * Project work item to CLI table format
86
+ * @param {Array<{id: string, goal: string, state: string, priority?: number}>} workItems - Work items
87
+ * @param {boolean} [colored=true] - Whether to use ANSI colors
88
+ * @returns {CLIProjection} CLI projection
89
+ */
90
+ export function projectWorkItemsToCLI(workItems, colored = true) {
91
+ if (!Array.isArray(workItems) || workItems.length === 0) {
92
+ return CLIProjectionSchema.parse({
93
+ type: 'cli',
94
+ format: 'table',
95
+ colored,
96
+ content: colorize('No work items', 'dim', colored),
97
+ });
98
+ }
99
+
100
+ // Table header
101
+ const header = [
102
+ colorize('ID', 'bright', colored).padEnd(colored ? 25 : 15),
103
+ colorize('Goal', 'bright', colored).padEnd(colored ? 45 : 35),
104
+ colorize('State', 'bright', colored).padEnd(colored ? 20 : 10),
105
+ colorize('Priority', 'bright', colored),
106
+ ].join(' │ ');
107
+
108
+ const separator = colorize('─'.repeat(100), 'dim', colored);
109
+
110
+ // Table rows
111
+ const rows = workItems.map((item) => {
112
+ const stateColor =
113
+ item.state === 'completed' ? 'green' :
114
+ item.state === 'failed' ? 'red' :
115
+ item.state === 'running' ? 'yellow' : 'dim';
116
+
117
+ return [
118
+ item.id.slice(0, 12).padEnd(15),
119
+ item.goal.slice(0, 32).padEnd(35),
120
+ colorize(item.state.padEnd(10), stateColor, colored),
121
+ (item.priority ?? 0).toString(),
122
+ ].join(' │ ');
123
+ });
124
+
125
+ const content = [header, separator, ...rows, separator].join('\n');
126
+
127
+ return CLIProjectionSchema.parse({
128
+ type: 'cli',
129
+ format: 'table',
130
+ colored,
131
+ content,
132
+ metadata: { count: workItems.length },
133
+ });
134
+ }
135
+
136
+ /**
137
+ * Project nested structure to tree format
138
+ * @param {Record<string, any>} data - Nested data structure
139
+ * @param {boolean} [colored=true] - Whether to use ANSI colors
140
+ * @param {string} [prefix=''] - Internal prefix for recursion
141
+ * @param {boolean} [isLast=true] - Internal flag for recursion
142
+ * @returns {string} Tree representation
143
+ */
144
+ function renderTree(data, colored = true, prefix = '', isLast = true) {
145
+ const lines = [];
146
+ const entries = Object.entries(data);
147
+
148
+ entries.forEach(([key, value], index) => {
149
+ const isLastEntry = index === entries.length - 1;
150
+ const connector = isLast ? '└── ' : '├── ';
151
+ const extension = isLast ? ' ' : '│ ';
152
+
153
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
154
+ lines.push(prefix + connector + colorize(key, 'cyan', colored));
155
+ lines.push(renderTree(value, colored, prefix + extension, isLastEntry));
156
+ } else {
157
+ const displayValue = Array.isArray(value) ? `[${value.length} items]` : String(value);
158
+ lines.push(
159
+ prefix + connector +
160
+ colorize(key, 'blue', colored) + ': ' +
161
+ colorize(displayValue, 'white', colored)
162
+ );
163
+ }
164
+ });
165
+
166
+ return lines.join('\n');
167
+ }
168
+
169
+ /**
170
+ * Project object to tree format
171
+ * @param {Record<string, any>} obj - Object to project
172
+ * @param {boolean} [colored=true] - Whether to use ANSI colors
173
+ * @returns {CLIProjection} CLI projection
174
+ */
175
+ export function projectObjectToTree(obj, colored = true) {
176
+ const content = renderTree(obj, colored);
177
+
178
+ return CLIProjectionSchema.parse({
179
+ type: 'cli',
180
+ format: 'tree',
181
+ colored,
182
+ content,
183
+ });
184
+ }
185
+
186
+ /**
187
+ * Project array to numbered list
188
+ * @param {Array<string | {label: string, description?: string}>} items - Items to project
189
+ * @param {boolean} [colored=true] - Whether to use ANSI colors
190
+ * @returns {CLIProjection} CLI projection
191
+ */
192
+ export function projectArrayToList(items, colored = true) {
193
+ const lines = items.map((item, index) => {
194
+ const num = colorize(`${index + 1}.`, 'cyan', colored);
195
+
196
+ if (typeof item === 'string') {
197
+ return `${num} ${item}`;
198
+ }
199
+
200
+ const label = colorize(item.label, 'bright', colored);
201
+ const desc = item.description ?
202
+ '\n ' + colorize(item.description, 'dim', colored) : '';
203
+
204
+ return `${num} ${label}${desc}`;
205
+ });
206
+
207
+ const content = lines.join('\n');
208
+
209
+ return CLIProjectionSchema.parse({
210
+ type: 'cli',
211
+ format: 'list',
212
+ colored,
213
+ content,
214
+ metadata: { count: items.length },
215
+ });
216
+ }
217
+
218
+ /**
219
+ * Flatten nested object for CLI display
220
+ * @param {Record<string, any>} obj - Object to flatten
221
+ * @param {string} [prefix=''] - Key prefix for recursion
222
+ * @returns {Record<string, any>} Flattened object
223
+ */
224
+ export function flattenForCLI(obj, prefix = '') {
225
+ const flattened = {};
226
+
227
+ for (const [key, value] of Object.entries(obj)) {
228
+ const fullKey = prefix ? `${prefix}.${key}` : key;
229
+
230
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
231
+ Object.assign(flattened, flattenForCLI(value, fullKey));
232
+ } else {
233
+ flattened[fullKey] = value;
234
+ }
235
+ }
236
+
237
+ return flattened;
238
+ }