@mcp-web/decompose-zod-schema 0.1.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/LICENSE +201 -0
- package/README.md +358 -0
- package/dist/apply.d.ts +2 -0
- package/dist/apply.d.ts.map +1 -0
- package/dist/apply.js +65 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +164 -0
- package/dist/decompose.d.ts +41 -0
- package/dist/decompose.d.ts.map +1 -0
- package/dist/decompose.js +383 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/plan-builder.d.ts +81 -0
- package/dist/plan-builder.d.ts.map +1 -0
- package/dist/plan-builder.js +109 -0
- package/dist/schemas.d.ts +14 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +22 -0
- package/dist/split-suggestions.d.ts +42 -0
- package/dist/split-suggestions.d.ts.map +1 -0
- package/dist/split-suggestions.js +179 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +20 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +161 -0
- package/dist/validate.d.ts +9 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +134 -0
- package/package.json +46 -0
package/dist/utils.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { ZodEnum, ZodObject, z } from 'zod';
|
|
2
|
+
// Path utilities - simple and dependency-free
|
|
3
|
+
export const setNestedValue = (obj, path, value) => {
|
|
4
|
+
const keys = path.split('.');
|
|
5
|
+
let current = obj;
|
|
6
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
7
|
+
const key = keys[i];
|
|
8
|
+
if (!(key in current) ||
|
|
9
|
+
typeof current[key] !== 'object' ||
|
|
10
|
+
current[key] === null) {
|
|
11
|
+
current[key] = {};
|
|
12
|
+
}
|
|
13
|
+
current = current[key];
|
|
14
|
+
}
|
|
15
|
+
current[keys[keys.length - 1]] = value;
|
|
16
|
+
};
|
|
17
|
+
// Custom even chunking function for optimal distribution
|
|
18
|
+
export const evenChunk = (array, maxChunkSize) => {
|
|
19
|
+
if (array.length <= maxChunkSize)
|
|
20
|
+
return [array];
|
|
21
|
+
const numChunks = Math.ceil(array.length / maxChunkSize);
|
|
22
|
+
const evenChunkSize = Math.ceil(array.length / numChunks);
|
|
23
|
+
const chunks = [];
|
|
24
|
+
for (let i = 0; i < array.length; i += evenChunkSize) {
|
|
25
|
+
chunks.push(array.slice(i, i + evenChunkSize));
|
|
26
|
+
}
|
|
27
|
+
return chunks;
|
|
28
|
+
};
|
|
29
|
+
// Parse split notation including enum splits, array splits, and record splits
|
|
30
|
+
export const parseEnumSplit = (item) => {
|
|
31
|
+
// Handle nested array split notation: 'views[].tracks.top[]'
|
|
32
|
+
// This matches patterns where we have array splits within array splits
|
|
33
|
+
const nestedArrayMatch = item.match(/^(.+)\[\]\.(.+)\[\]$/);
|
|
34
|
+
if (nestedArrayMatch) {
|
|
35
|
+
const [, basePath, subPath] = nestedArrayMatch;
|
|
36
|
+
// For validation, we need to check if the base path exists as an array
|
|
37
|
+
// and if the subPath exists within the array element schema
|
|
38
|
+
return { path: `${basePath}.${subPath}`, isArraySplit: true };
|
|
39
|
+
}
|
|
40
|
+
// Handle simple array split notation: 'views[]'
|
|
41
|
+
const arrayMatch = item.match(/^(.+)\[\]$/);
|
|
42
|
+
if (arrayMatch) {
|
|
43
|
+
const [, path] = arrayMatch;
|
|
44
|
+
return { path, isArraySplit: true };
|
|
45
|
+
}
|
|
46
|
+
// Handle record split notation: 'configs{}'
|
|
47
|
+
const recordMatch = item.match(/^(.+)\{\}$/);
|
|
48
|
+
if (recordMatch) {
|
|
49
|
+
const [, path] = recordMatch;
|
|
50
|
+
return { path, isRecordSplit: true };
|
|
51
|
+
}
|
|
52
|
+
// Handle chunk size notation: 'category[50]'
|
|
53
|
+
const chunkMatch = item.match(/^(.+)\[(\d+)\]$/);
|
|
54
|
+
if (chunkMatch) {
|
|
55
|
+
const [, path, chunkSizeStr] = chunkMatch;
|
|
56
|
+
return { path, chunkSize: parseInt(chunkSizeStr, 10) };
|
|
57
|
+
}
|
|
58
|
+
// Handle full slice notation: 'category[10:20]' or 'category[-5:10]'
|
|
59
|
+
const fullSliceMatch = item.match(/^(.+)\[(-?\d+):(-?\d+)\]$/);
|
|
60
|
+
if (fullSliceMatch) {
|
|
61
|
+
const [, path, startStr, endStr] = fullSliceMatch;
|
|
62
|
+
return { path, start: parseInt(startStr, 10), end: parseInt(endStr, 10) };
|
|
63
|
+
}
|
|
64
|
+
// Handle slice from index to end: 'category[10:]' or 'category[-5:]'
|
|
65
|
+
const fromIndexMatch = item.match(/^(.+)\[(-?\d+):\]$/);
|
|
66
|
+
if (fromIndexMatch) {
|
|
67
|
+
const [, path, startStr] = fromIndexMatch;
|
|
68
|
+
return { path, start: parseInt(startStr, 10), end: undefined };
|
|
69
|
+
}
|
|
70
|
+
// Handle slice from start to index: 'category[:20]' or 'category[:-5]'
|
|
71
|
+
const toIndexMatch = item.match(/^(.+)\[:(-?\d+)\]$/);
|
|
72
|
+
if (toIndexMatch) {
|
|
73
|
+
const [, path, endStr] = toIndexMatch;
|
|
74
|
+
return { path, start: 0, end: parseInt(endStr, 10) };
|
|
75
|
+
}
|
|
76
|
+
// Handle entire enum slice: 'category[:]'
|
|
77
|
+
const entireMatch = item.match(/^(.+)\[:\]$/);
|
|
78
|
+
if (entireMatch) {
|
|
79
|
+
const [, path] = entireMatch;
|
|
80
|
+
return { path, start: 0, end: undefined };
|
|
81
|
+
}
|
|
82
|
+
// Regular path
|
|
83
|
+
return { path: item };
|
|
84
|
+
};
|
|
85
|
+
// Extract schema at a given path
|
|
86
|
+
export const getSchemaAtPath = (schema, path) => {
|
|
87
|
+
const parts = path.split('.');
|
|
88
|
+
let current = schema;
|
|
89
|
+
for (const part of parts) {
|
|
90
|
+
if (current instanceof ZodObject) {
|
|
91
|
+
const shape = current.shape;
|
|
92
|
+
current = shape[part];
|
|
93
|
+
if (!current)
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return current;
|
|
101
|
+
};
|
|
102
|
+
// Create a schema with only specified paths
|
|
103
|
+
export const extractSchemaForPaths = (originalSchema, paths) => {
|
|
104
|
+
const shape = {};
|
|
105
|
+
for (const path of paths) {
|
|
106
|
+
const parsedPath = parseEnumSplit(path);
|
|
107
|
+
const schema = getSchemaAtPath(originalSchema, parsedPath.path);
|
|
108
|
+
if (!schema)
|
|
109
|
+
continue;
|
|
110
|
+
// Handle enum splitting
|
|
111
|
+
if (schema instanceof ZodEnum &&
|
|
112
|
+
(parsedPath.chunkSize || parsedPath.start !== undefined)) {
|
|
113
|
+
const enumOptions = schema.options;
|
|
114
|
+
if (parsedPath.chunkSize) {
|
|
115
|
+
// For chunk size, we don't handle it in extractSchemaForPaths - that's done in manual-decompose
|
|
116
|
+
// This should not happen in normal usage as chunks generate multiple schemas
|
|
117
|
+
// Skip this path and continue with regular schema extraction
|
|
118
|
+
}
|
|
119
|
+
else if (parsedPath.start !== undefined) {
|
|
120
|
+
// Handle slice notation with optional end
|
|
121
|
+
const end = parsedPath.end !== undefined ? parsedPath.end : enumOptions.length;
|
|
122
|
+
const slicedOptions = enumOptions.slice(parsedPath.start, end);
|
|
123
|
+
if (slicedOptions.length > 0) {
|
|
124
|
+
setNestedValue(shape, parsedPath.path, z.enum(slicedOptions));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
// Handle regular schema extraction
|
|
130
|
+
setNestedValue(shape, parsedPath.path, schema);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return z.object(shape);
|
|
134
|
+
};
|
|
135
|
+
// Estimate tokens in a schema (rough approximation)
|
|
136
|
+
export const estimateTokensByJsonSchema = (schema) => {
|
|
137
|
+
try {
|
|
138
|
+
const jsonSchema = z.toJSONSchema(schema);
|
|
139
|
+
const schemaString = JSON.stringify(jsonSchema);
|
|
140
|
+
return Math.ceil(schemaString.length / 3.5);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// Fallback for schemas that can't be converted
|
|
144
|
+
return 100;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Helper function to create conditional enum splits based on enum size
|
|
149
|
+
*/
|
|
150
|
+
export function conditionalEnumSplit(path, maxSize, schema) {
|
|
151
|
+
const enumSchema = getSchemaAtPath(schema, path);
|
|
152
|
+
if (!(enumSchema instanceof ZodEnum)) {
|
|
153
|
+
return [path]; // Not an enum, return as regular path
|
|
154
|
+
}
|
|
155
|
+
const enumOptions = enumSchema.options;
|
|
156
|
+
if (enumOptions.length <= maxSize) {
|
|
157
|
+
return [path]; // Small enough, no split needed
|
|
158
|
+
}
|
|
159
|
+
// Return chunk notation
|
|
160
|
+
return [`${path}[${maxSize}]`];
|
|
161
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ZodObject, type ZodType } from 'zod';
|
|
2
|
+
import type { SplitPlan } from './types.js';
|
|
3
|
+
export declare const validatePlan: (plan: SplitPlan, schema: ZodObject<Record<string, ZodType>>) => string[];
|
|
4
|
+
export declare const validateSliceCompleteness: (enumPath: string, slices: Array<{
|
|
5
|
+
start: number;
|
|
6
|
+
end: number;
|
|
7
|
+
item: string;
|
|
8
|
+
}>, enumLength: number) => string[];
|
|
9
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,SAAS,EAAE,KAAK,OAAO,EAAE,MAAM,KAAK,CAAC;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAI5C,eAAO,MAAM,YAAY,GACvB,MAAM,SAAS,EACf,QAAQ,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,KACzC,MAAM,EAyHR,CAAC;AAGF,eAAO,MAAM,yBAAyB,GACpC,UAAU,MAAM,EAChB,QAAQ,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EAC3D,YAAY,MAAM,KACjB,MAAM,EA4CR,CAAC"}
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { ZodEnum } from 'zod';
|
|
2
|
+
import { getSchemaAtPath, parseEnumSplit } from './utils.js';
|
|
3
|
+
// Validate that a split plan is structurally correct
|
|
4
|
+
export const validatePlan = (plan, schema) => {
|
|
5
|
+
const errors = [];
|
|
6
|
+
const enumSlices = new Map();
|
|
7
|
+
for (const item of plan) {
|
|
8
|
+
const parsed = parseEnumSplit(item);
|
|
9
|
+
// Handle nested array validation (e.g., 'views[].tracks.top[]')
|
|
10
|
+
if (parsed.isArraySplit && item.includes('[].')) {
|
|
11
|
+
const nestedMatch = item.match(/^(.+)\[\]\.(.+)\[\]$/);
|
|
12
|
+
if (nestedMatch) {
|
|
13
|
+
const [, basePath, subPath] = nestedMatch;
|
|
14
|
+
// Check if the base path exists and is an array
|
|
15
|
+
const baseSchema = getSchemaAtPath(schema, basePath);
|
|
16
|
+
if (!baseSchema) {
|
|
17
|
+
errors.push(`Base path '${basePath}' does not exist in schema`);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
// For array schemas, we need to check the element schema
|
|
21
|
+
if ('element' in baseSchema && baseSchema.element) {
|
|
22
|
+
const elementSchema = baseSchema.element;
|
|
23
|
+
if (elementSchema && typeof elementSchema === 'object' && 'shape' in elementSchema) {
|
|
24
|
+
const subSchemaAtPath = getSchemaAtPath(elementSchema, subPath);
|
|
25
|
+
if (!subSchemaAtPath) {
|
|
26
|
+
errors.push(`Sub-path '${subPath}' does not exist in array element schema at '${basePath}'`);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
// Unwrap optional schemas to get to the inner array
|
|
30
|
+
let actualSubSchema = subSchemaAtPath;
|
|
31
|
+
if (subSchemaAtPath && typeof subSchemaAtPath === 'object' && 'unwrap' in subSchemaAtPath) {
|
|
32
|
+
actualSubSchema = subSchemaAtPath.unwrap();
|
|
33
|
+
}
|
|
34
|
+
// Check if the sub-path is also an array (for the final [])
|
|
35
|
+
if (!actualSubSchema || typeof actualSubSchema !== 'object' || !('element' in actualSubSchema) || !actualSubSchema.element) {
|
|
36
|
+
errors.push(`Sub-path '${subPath}' at '${basePath}' is not an array schema`);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
errors.push(`Array element at '${basePath}' is not an object schema`);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
errors.push(`Path '${basePath}' is not an array schema`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Get schema at path for all non-nested array items
|
|
52
|
+
let schemaAtPath;
|
|
53
|
+
if (!(parsed.isArraySplit && item.includes('[].'))) {
|
|
54
|
+
schemaAtPath = getSchemaAtPath(schema, parsed.path);
|
|
55
|
+
if (!schemaAtPath) {
|
|
56
|
+
errors.push(`Path '${parsed.path}' does not exist in schema`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Validate enum split notation
|
|
61
|
+
if (parsed.chunkSize || parsed.start !== undefined) {
|
|
62
|
+
if (!schemaAtPath || !(schemaAtPath instanceof ZodEnum)) {
|
|
63
|
+
errors.push(`Path '${parsed.path}' is not an enum but has enum split notation`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const enumOptions = schemaAtPath.options;
|
|
67
|
+
if (parsed.chunkSize) {
|
|
68
|
+
if (parsed.chunkSize <= 0) {
|
|
69
|
+
errors.push(`Chunk size must be positive for '${parsed.path}[${parsed.chunkSize}]'`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else if (parsed.start !== undefined) {
|
|
73
|
+
const end = parsed.end !== undefined ? parsed.end : enumOptions.length;
|
|
74
|
+
// Validate slice bounds
|
|
75
|
+
if (parsed.start < 0) {
|
|
76
|
+
errors.push(`Start index cannot be negative in '${item}'`);
|
|
77
|
+
}
|
|
78
|
+
if (end > enumOptions.length) {
|
|
79
|
+
errors.push(`End index ${end} exceeds enum length ${enumOptions.length} in '${item}'`);
|
|
80
|
+
}
|
|
81
|
+
if (parsed.start >= end) {
|
|
82
|
+
errors.push(`Start index must be less than end index in '${item}'`);
|
|
83
|
+
}
|
|
84
|
+
// Track slices for completeness validation
|
|
85
|
+
if (!enumSlices.has(parsed.path)) {
|
|
86
|
+
enumSlices.set(parsed.path, []);
|
|
87
|
+
}
|
|
88
|
+
const slicesArray = enumSlices.get(parsed.path);
|
|
89
|
+
if (slicesArray) {
|
|
90
|
+
slicesArray.push({ start: parsed.start, end, item });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Validate slice completeness for each enum path
|
|
96
|
+
for (const [enumPath, slices] of enumSlices) {
|
|
97
|
+
const schemaAtPath = getSchemaAtPath(schema, enumPath);
|
|
98
|
+
if (!(schemaAtPath instanceof ZodEnum))
|
|
99
|
+
continue;
|
|
100
|
+
const enumLength = schemaAtPath.options.length;
|
|
101
|
+
const sliceErrors = validateSliceCompleteness(enumPath, slices, enumLength);
|
|
102
|
+
errors.push(...sliceErrors);
|
|
103
|
+
}
|
|
104
|
+
return errors;
|
|
105
|
+
};
|
|
106
|
+
// Validate that slices for an enum are complete (no gaps, no overlaps)
|
|
107
|
+
export const validateSliceCompleteness = (enumPath, slices, enumLength) => {
|
|
108
|
+
const errors = [];
|
|
109
|
+
// Sort slices by start index
|
|
110
|
+
const sortedSlices = [...slices].sort((a, b) => a.start - b.start);
|
|
111
|
+
// Check for overlaps and gaps
|
|
112
|
+
let expectedStart = 0;
|
|
113
|
+
for (let i = 0; i < sortedSlices.length; i++) {
|
|
114
|
+
const slice = sortedSlices[i];
|
|
115
|
+
// Check for gap
|
|
116
|
+
if (slice.start > expectedStart) {
|
|
117
|
+
errors.push(`Gap in enum slices for '${enumPath}': missing range [${expectedStart}:${slice.start}]`);
|
|
118
|
+
}
|
|
119
|
+
// Check for overlap with previous slice
|
|
120
|
+
if (slice.start < expectedStart) {
|
|
121
|
+
errors.push(`Overlapping enum slices for '${enumPath}': '${slice.item}' overlaps with previous slice`);
|
|
122
|
+
}
|
|
123
|
+
expectedStart = slice.end;
|
|
124
|
+
}
|
|
125
|
+
// Check if we covered the entire enum
|
|
126
|
+
if (expectedStart < enumLength) {
|
|
127
|
+
errors.push(`Incomplete enum slices for '${enumPath}': missing range [${expectedStart}:${enumLength}]`);
|
|
128
|
+
}
|
|
129
|
+
// Check if we went beyond the enum length
|
|
130
|
+
if (expectedStart > enumLength) {
|
|
131
|
+
errors.push(`Enum slices for '${enumPath}' extend beyond enum length ${enumLength}`);
|
|
132
|
+
}
|
|
133
|
+
return errors;
|
|
134
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mcp-web/decompose-zod-schema",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Utility for decomposing large Zod schemas into smaller, manageable sub-schemas with partial update support",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"bin": {
|
|
18
|
+
"@mcp-web/decompose-zod-schema": "./dist/cli.js"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"commander": "^14.0.0",
|
|
22
|
+
"immer": "^10.1.1",
|
|
23
|
+
"zod": "~4.1.12"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^25.0.9",
|
|
27
|
+
"tsx": "^4.16.0",
|
|
28
|
+
"typescript": "~5.9.3"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"zod",
|
|
32
|
+
"schema",
|
|
33
|
+
"decomposition",
|
|
34
|
+
"validation",
|
|
35
|
+
"typescript",
|
|
36
|
+
"partial-updates"
|
|
37
|
+
],
|
|
38
|
+
"author": "MCP Frontend Integration System",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc",
|
|
42
|
+
"dev": "tsc --watch",
|
|
43
|
+
"cli": "tsx src/cli.ts",
|
|
44
|
+
"example": "tsx src/cli.ts -i examples/example-schema.ts -s userSchema -v"
|
|
45
|
+
}
|
|
46
|
+
}
|