@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.
- package/IMPLEMENTATION_SUMMARY.json +150 -0
- package/PLUGIN_SYSTEM_SUMMARY.json +149 -0
- package/README.md +98 -0
- package/TRANSACTION_IMPLEMENTATION.json +119 -0
- package/capability-map.md +93 -0
- package/docs/api-stability.md +269 -0
- package/docs/extensions/plugin-development.md +382 -0
- package/package.json +40 -0
- package/plugins/registry.json +35 -0
- package/src/admission-gate.mjs +414 -0
- package/src/api-version.mjs +373 -0
- package/src/atomic-admission.mjs +310 -0
- package/src/bounds.mjs +289 -0
- package/src/bulkhead-manager.mjs +280 -0
- package/src/capsule.mjs +524 -0
- package/src/crdt.mjs +361 -0
- package/src/enhanced-bounds.mjs +614 -0
- package/src/executor.mjs +73 -0
- package/src/freeze-restore.mjs +521 -0
- package/src/index.mjs +62 -0
- package/src/materialized-views.mjs +371 -0
- package/src/merge.mjs +472 -0
- package/src/plugin-isolation.mjs +392 -0
- package/src/plugin-manager.mjs +441 -0
- package/src/projections-api.mjs +336 -0
- package/src/projections-cli.mjs +238 -0
- package/src/projections-docs.mjs +300 -0
- package/src/projections-ide.mjs +278 -0
- package/src/receipt.mjs +340 -0
- package/src/rollback.mjs +258 -0
- package/src/saga-orchestrator.mjs +355 -0
- package/src/schemas.mjs +1330 -0
- package/src/storage-optimization.mjs +359 -0
- package/src/tool-registry.mjs +272 -0
- package/src/transaction.mjs +466 -0
- package/src/validators.mjs +485 -0
- package/src/work-item.mjs +449 -0
- package/templates/plugin-template/README.md +58 -0
- package/templates/plugin-template/index.mjs +162 -0
- package/templates/plugin-template/plugin.json +19 -0
- package/test/admission-gate.test.mjs +583 -0
- package/test/api-version.test.mjs +74 -0
- package/test/atomic-admission.test.mjs +155 -0
- package/test/bounds.test.mjs +341 -0
- package/test/bulkhead-manager.test.mjs +236 -0
- package/test/capsule.test.mjs +625 -0
- package/test/crdt.test.mjs +215 -0
- package/test/enhanced-bounds.test.mjs +487 -0
- package/test/freeze-restore.test.mjs +472 -0
- package/test/materialized-views.test.mjs +243 -0
- package/test/merge.test.mjs +665 -0
- package/test/plugin-isolation.test.mjs +109 -0
- package/test/plugin-manager.test.mjs +208 -0
- package/test/projections-api.test.mjs +293 -0
- package/test/projections-cli.test.mjs +204 -0
- package/test/projections-docs.test.mjs +173 -0
- package/test/projections-ide.test.mjs +230 -0
- package/test/receipt.test.mjs +295 -0
- package/test/rollback.test.mjs +132 -0
- package/test/saga-orchestrator.test.mjs +279 -0
- package/test/schemas.test.mjs +716 -0
- package/test/storage-optimization.test.mjs +503 -0
- package/test/tool-registry.test.mjs +341 -0
- package/test/transaction.test.mjs +189 -0
- package/test/validators.test.mjs +463 -0
- package/test/work-item.test.mjs +548 -0
- package/test/work-item.test.mjs.bak +548 -0
- package/var/kgc/test-atomic-log.json +519 -0
- package/var/kgc/test-cascading-log.json +145 -0
- 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
|
+
}
|