@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/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"}
@@ -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
+ }