@claudetools/tools 0.8.11 → 0.9.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/codedna/generators/astro.d.ts +18 -0
- package/dist/codedna/generators/astro.js +91 -0
- package/dist/codedna/generators/authjs.d.ts +18 -0
- package/dist/codedna/generators/authjs.js +68 -0
- package/dist/codedna/generators/better-auth.d.ts +18 -0
- package/dist/codedna/generators/better-auth.js +62 -0
- package/dist/codedna/generators/drizzle-orm.d.ts +18 -0
- package/dist/codedna/generators/drizzle-orm.js +65 -0
- package/dist/codedna/generators/elysia-api.d.ts +12 -0
- package/dist/codedna/generators/elysia-api.js +64 -0
- package/dist/codedna/generators/hono-api.d.ts +12 -0
- package/dist/codedna/generators/hono-api.js +64 -0
- package/dist/codedna/generators/lucia-auth.d.ts +18 -0
- package/dist/codedna/generators/lucia-auth.js +69 -0
- package/dist/codedna/generators/prisma.d.ts +18 -0
- package/dist/codedna/generators/prisma.js +64 -0
- package/dist/codedna/generators/react-router-v7.d.ts +18 -0
- package/dist/codedna/generators/react-router-v7.js +77 -0
- package/dist/codedna/generators/react19-shadcn.d.ts +21 -0
- package/dist/codedna/generators/react19-shadcn.js +367 -0
- package/dist/codedna/generators/sveltekit.d.ts +18 -0
- package/dist/codedna/generators/sveltekit.js +73 -0
- package/dist/codedna/generators/tanstack-start-drizzle.d.ts +92 -0
- package/dist/codedna/generators/tanstack-start-drizzle.js +824 -0
- package/dist/codedna/generators/trpc-api.d.ts +12 -0
- package/dist/codedna/generators/trpc-api.js +64 -0
- package/dist/codedna/index.d.ts +31 -0
- package/dist/codedna/index.js +39 -0
- package/dist/codedna/kappa-api-generator.d.ts +89 -0
- package/dist/codedna/kappa-api-generator.js +493 -0
- package/dist/codedna/kappa-ast.d.ts +552 -0
- package/dist/codedna/kappa-ast.js +141 -0
- package/dist/codedna/kappa-cli.d.ts +2 -0
- package/dist/codedna/kappa-cli.js +302 -0
- package/dist/codedna/kappa-component-generator.d.ts +47 -0
- package/dist/codedna/kappa-component-generator.js +295 -0
- package/dist/codedna/kappa-design-generator.d.ts +52 -0
- package/dist/codedna/kappa-design-generator.js +365 -0
- package/dist/codedna/kappa-drizzle-generator.d.ts +45 -0
- package/dist/codedna/kappa-drizzle-generator.js +355 -0
- package/dist/codedna/kappa-form-generator.d.ts +51 -0
- package/dist/codedna/kappa-form-generator.js +319 -0
- package/dist/codedna/kappa-lexer.d.ts +268 -0
- package/dist/codedna/kappa-lexer.js +757 -0
- package/dist/codedna/kappa-page-generator.d.ts +57 -0
- package/dist/codedna/kappa-page-generator.js +338 -0
- package/dist/codedna/kappa-parser.d.ts +261 -0
- package/dist/codedna/kappa-parser.js +2547 -0
- package/dist/codedna/kappa-provenance.d.ts +101 -0
- package/dist/codedna/kappa-provenance.js +199 -0
- package/dist/codedna/kappa-types-generator.d.ts +37 -0
- package/dist/codedna/kappa-types-generator.js +159 -0
- package/dist/codedna/kappa-validator.d.ts +86 -0
- package/dist/codedna/kappa-validator.js +638 -0
- package/dist/codedna/kappa-zod-generator.d.ts +32 -0
- package/dist/codedna/kappa-zod-generator.js +216 -0
- package/dist/handlers/kappa-handlers.d.ts +116 -0
- package/dist/handlers/kappa-handlers.js +465 -0
- package/dist/handlers/tool-handlers.js +121 -0
- package/dist/templates/claude-md.d.ts +1 -1
- package/dist/templates/claude-md.js +166 -9
- package/dist/tools.js +199 -0
- package/docs/research/2026-01-02-codedna-il-specification.md +639 -0
- package/docs/research/2026-01-02-codedna-v2-research.md +943 -0
- package/docs/research/2026-01-02-computation-foundations.md +564 -0
- package/docs/research/2026-01-02-hardware-description.md +814 -0
- package/docs/research/2026-01-02-kappa-specification.md +697 -0
- package/docs/research/2026-01-02-kappa-tanstack-example.md +527 -0
- package/docs/research/2026-01-02-kappa-v2-synthesis.md +406 -0
- package/docs/research/2026-01-02-kappa-v2.5-specification.md +1218 -0
- package/docs/research/2026-01-02-kappa-v3-specification.md +1864 -0
- package/docs/research/2026-01-02-kappa-whitepaper.md +662 -0
- package/docs/research/2026-01-02-logic-constraint.md +731 -0
- package/docs/research/2026-01-02-quantum-computation.md +635 -0
- package/package.json +4 -2
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Kappa v2.5 Semantic Validator
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Validates Kappa AST for semantic correctness:
|
|
6
|
+
// - Entity references point to defined entities
|
|
7
|
+
// - Field types are valid
|
|
8
|
+
// - Relationship targets exist
|
|
9
|
+
// - API operations reference valid entities
|
|
10
|
+
//
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// Validator Class
|
|
13
|
+
// =============================================================================
|
|
14
|
+
export class KappaValidator {
|
|
15
|
+
errors = [];
|
|
16
|
+
warnings = [];
|
|
17
|
+
/** Set of all defined entity names */
|
|
18
|
+
entityNames = new Set();
|
|
19
|
+
/** Set of all defined API names */
|
|
20
|
+
apiNames = new Set();
|
|
21
|
+
/** Set of all defined page names */
|
|
22
|
+
pageNames = new Set();
|
|
23
|
+
/** Set of all defined form names */
|
|
24
|
+
formNames = new Set();
|
|
25
|
+
/** Set of all defined journey names */
|
|
26
|
+
journeyNames = new Set();
|
|
27
|
+
/**
|
|
28
|
+
* Validate a Kappa specification AST
|
|
29
|
+
*/
|
|
30
|
+
validate(spec) {
|
|
31
|
+
this.reset();
|
|
32
|
+
// First pass: collect all defined names
|
|
33
|
+
this.collectDefinedNames(spec);
|
|
34
|
+
// Second pass: validate references
|
|
35
|
+
this.validateEntities(spec.entities);
|
|
36
|
+
this.validateAPIs(spec.apis);
|
|
37
|
+
this.validateJourneys(spec.journeys);
|
|
38
|
+
this.validatePages(spec.pages);
|
|
39
|
+
this.validateForms(spec.forms);
|
|
40
|
+
return {
|
|
41
|
+
valid: this.errors.length === 0,
|
|
42
|
+
errors: this.errors,
|
|
43
|
+
warnings: this.warnings,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
reset() {
|
|
47
|
+
this.errors = [];
|
|
48
|
+
this.warnings = [];
|
|
49
|
+
this.entityNames.clear();
|
|
50
|
+
this.apiNames.clear();
|
|
51
|
+
this.pageNames.clear();
|
|
52
|
+
this.formNames.clear();
|
|
53
|
+
this.journeyNames.clear();
|
|
54
|
+
}
|
|
55
|
+
// ===========================================================================
|
|
56
|
+
// Name Collection (First Pass)
|
|
57
|
+
// ===========================================================================
|
|
58
|
+
collectDefinedNames(spec) {
|
|
59
|
+
// Collect entity names
|
|
60
|
+
for (const entity of spec.entities) {
|
|
61
|
+
if (this.entityNames.has(entity.name)) {
|
|
62
|
+
this.addError({
|
|
63
|
+
code: 'DUPLICATE_ENTITY_NAME',
|
|
64
|
+
message: `Duplicate entity name: '${entity.name}'`,
|
|
65
|
+
location: entity.loc,
|
|
66
|
+
context: { entityName: entity.name },
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
this.entityNames.add(entity.name);
|
|
70
|
+
}
|
|
71
|
+
// Collect API names
|
|
72
|
+
for (const api of spec.apis) {
|
|
73
|
+
if (this.apiNames.has(api.name)) {
|
|
74
|
+
this.addError({
|
|
75
|
+
code: 'DUPLICATE_API_NAME',
|
|
76
|
+
message: `Duplicate API name: '${api.name}'`,
|
|
77
|
+
location: api.loc,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
this.apiNames.add(api.name);
|
|
81
|
+
}
|
|
82
|
+
// Collect page names
|
|
83
|
+
for (const page of spec.pages) {
|
|
84
|
+
this.pageNames.add(page.name);
|
|
85
|
+
}
|
|
86
|
+
// Collect form names
|
|
87
|
+
for (const form of spec.forms) {
|
|
88
|
+
this.formNames.add(form.name);
|
|
89
|
+
}
|
|
90
|
+
// Collect journey names
|
|
91
|
+
for (const journey of spec.journeys) {
|
|
92
|
+
this.journeyNames.add(journey.name);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// ===========================================================================
|
|
96
|
+
// Entity Validation
|
|
97
|
+
// ===========================================================================
|
|
98
|
+
validateEntities(entities) {
|
|
99
|
+
for (const entity of entities) {
|
|
100
|
+
this.validateEntity(entity);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
validateEntity(entity) {
|
|
104
|
+
const fieldNames = new Set();
|
|
105
|
+
let hasPrimaryKey = false;
|
|
106
|
+
// Validate fields
|
|
107
|
+
for (const field of entity.fields) {
|
|
108
|
+
// Check for duplicate field names
|
|
109
|
+
if (fieldNames.has(field.name)) {
|
|
110
|
+
this.addError({
|
|
111
|
+
code: 'DUPLICATE_FIELD_NAME',
|
|
112
|
+
message: `Duplicate field name '${field.name}' in entity '${entity.name}'`,
|
|
113
|
+
location: field.loc,
|
|
114
|
+
context: { entityName: entity.name, fieldName: field.name },
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
fieldNames.add(field.name);
|
|
118
|
+
// Check for multiple primary keys
|
|
119
|
+
if (field.modifiers.includes('primary')) {
|
|
120
|
+
if (hasPrimaryKey) {
|
|
121
|
+
this.addError({
|
|
122
|
+
code: 'DUPLICATE_PRIMARY_KEY',
|
|
123
|
+
message: `Entity '${entity.name}' has multiple primary keys`,
|
|
124
|
+
location: field.loc,
|
|
125
|
+
context: { entityName: entity.name, fieldName: field.name },
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
hasPrimaryKey = true;
|
|
129
|
+
}
|
|
130
|
+
// Validate field type references
|
|
131
|
+
this.validateFieldType(field.type, field.loc, entity.name, field.name);
|
|
132
|
+
// Validate modifiers for type
|
|
133
|
+
this.validateModifiersForType(field, entity.name);
|
|
134
|
+
}
|
|
135
|
+
// Validate relationships
|
|
136
|
+
for (const relationship of entity.relationships) {
|
|
137
|
+
this.validateRelationship(relationship, entity.name);
|
|
138
|
+
}
|
|
139
|
+
// Validate capabilities
|
|
140
|
+
for (const capability of entity.capabilities) {
|
|
141
|
+
this.validateCapability(capability, entity.name);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
validateFieldType(type, location, entityName, fieldName) {
|
|
145
|
+
switch (type.kind) {
|
|
146
|
+
case 'reference':
|
|
147
|
+
if (!this.entityNames.has(type.entity)) {
|
|
148
|
+
this.addError({
|
|
149
|
+
code: 'UNDEFINED_ENTITY_REFERENCE',
|
|
150
|
+
message: `Field '${fieldName}' references undefined entity '${type.entity}'`,
|
|
151
|
+
location,
|
|
152
|
+
context: { entityName, fieldName, referencedEntity: type.entity },
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
case 'enum':
|
|
157
|
+
// Check for empty enum
|
|
158
|
+
if (type.values.length === 0) {
|
|
159
|
+
this.addError({
|
|
160
|
+
code: 'EMPTY_ENUM',
|
|
161
|
+
message: `Enum field '${fieldName}' has no values`,
|
|
162
|
+
location,
|
|
163
|
+
context: { entityName, fieldName },
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
// Check for duplicate enum values
|
|
167
|
+
const enumValues = new Set();
|
|
168
|
+
for (const value of type.values) {
|
|
169
|
+
if (enumValues.has(value)) {
|
|
170
|
+
this.addError({
|
|
171
|
+
code: 'DUPLICATE_ENUM_VALUE',
|
|
172
|
+
message: `Enum field '${fieldName}' has duplicate value '${value}'`,
|
|
173
|
+
location,
|
|
174
|
+
context: { entityName, fieldName },
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
enumValues.add(value);
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
case 'primitive':
|
|
181
|
+
// Validate range constraints
|
|
182
|
+
if (type.range) {
|
|
183
|
+
const { min, max } = type.range;
|
|
184
|
+
if (min !== undefined && max !== undefined && min > max) {
|
|
185
|
+
this.addError({
|
|
186
|
+
code: 'INVALID_RANGE',
|
|
187
|
+
message: `Field '${fieldName}' has invalid range: min (${min}) > max (${max})`,
|
|
188
|
+
location,
|
|
189
|
+
context: { entityName, fieldName },
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
if (min !== undefined && min < 0 && type.type === 'string') {
|
|
193
|
+
this.addError({
|
|
194
|
+
code: 'INVALID_RANGE',
|
|
195
|
+
message: `String field '${fieldName}' cannot have negative min length (${min})`,
|
|
196
|
+
location,
|
|
197
|
+
context: { entityName, fieldName },
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
case 'array':
|
|
203
|
+
// Recursively validate array item type
|
|
204
|
+
this.validateFieldType(type.itemType, location, entityName, fieldName);
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
validateModifiersForType(field, entityName) {
|
|
209
|
+
const { type, modifiers, loc, name } = field;
|
|
210
|
+
// 'hashed' is only valid on string types
|
|
211
|
+
if (modifiers.includes('hashed')) {
|
|
212
|
+
if (type.kind !== 'primitive' || type.type !== 'string') {
|
|
213
|
+
this.addError({
|
|
214
|
+
code: 'INVALID_MODIFIER_FOR_TYPE',
|
|
215
|
+
message: `Modifier 'hashed' is only valid on string fields, but '${name}' is not a string`,
|
|
216
|
+
location: loc,
|
|
217
|
+
context: { entityName, fieldName: name },
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// 'auto' is only valid on uuid, timestamp, or int (for auto-increment)
|
|
222
|
+
if (modifiers.includes('auto')) {
|
|
223
|
+
const validAutoTypes = ['uuid', 'timestamp', 'int'];
|
|
224
|
+
if (type.kind !== 'primitive' || !validAutoTypes.includes(type.type)) {
|
|
225
|
+
this.addWarning({
|
|
226
|
+
code: 'INVALID_MODIFIER_FOR_TYPE',
|
|
227
|
+
message: `Modifier 'auto' on '${name}' may not work as expected for type '${type.kind === 'primitive' ? type.type : type.kind}'`,
|
|
228
|
+
location: loc,
|
|
229
|
+
context: { entityName, fieldName: name },
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// 'primary' and 'optional' are conflicting
|
|
234
|
+
if (modifiers.includes('primary') && modifiers.includes('optional')) {
|
|
235
|
+
this.addError({
|
|
236
|
+
code: 'CONFLICTING_MODIFIERS',
|
|
237
|
+
message: `Field '${name}' cannot be both 'primary' and 'optional'`,
|
|
238
|
+
location: loc,
|
|
239
|
+
context: { entityName, fieldName: name },
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
// 'unique' on reference types is usually a warning (might be intentional)
|
|
243
|
+
if (modifiers.includes('unique') && type.kind === 'reference') {
|
|
244
|
+
this.addWarning({
|
|
245
|
+
code: 'INVALID_MODIFIER_FOR_TYPE',
|
|
246
|
+
message: `Modifier 'unique' on reference field '${name}' creates a one-to-one relationship`,
|
|
247
|
+
location: loc,
|
|
248
|
+
context: { entityName, fieldName: name },
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
// 'immutable' and 'auto' with 'on_update' is conflicting
|
|
252
|
+
if (modifiers.includes('immutable') && field.updateTrigger) {
|
|
253
|
+
this.addError({
|
|
254
|
+
code: 'CONFLICTING_MODIFIERS',
|
|
255
|
+
message: `Field '${name}' cannot be 'immutable' with update trigger`,
|
|
256
|
+
location: loc,
|
|
257
|
+
context: { entityName, fieldName: name },
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
// 'internal' is only useful on fields that would otherwise be exposed
|
|
261
|
+
if (modifiers.includes('internal') && modifiers.includes('hashed')) {
|
|
262
|
+
// This is fine - password fields are commonly internal + hashed
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
validateRelationship(relationship, entityName) {
|
|
266
|
+
// Check if target entity exists
|
|
267
|
+
if (!this.entityNames.has(relationship.entity)) {
|
|
268
|
+
this.addError({
|
|
269
|
+
code: 'UNDEFINED_ENTITY_REFERENCE',
|
|
270
|
+
message: `Relationship references undefined entity '${relationship.entity}'`,
|
|
271
|
+
location: relationship.loc,
|
|
272
|
+
context: { entityName, referencedEntity: relationship.entity },
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
// Check if through entity exists (for many-to-many)
|
|
276
|
+
if (relationship.through && !this.entityNames.has(relationship.through)) {
|
|
277
|
+
this.addError({
|
|
278
|
+
code: 'UNDEFINED_THROUGH_ENTITY',
|
|
279
|
+
message: `Relationship 'through' references undefined entity '${relationship.through}'`,
|
|
280
|
+
location: relationship.loc,
|
|
281
|
+
context: { entityName, referencedEntity: relationship.through },
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
validateCapability(capability, entityName) {
|
|
286
|
+
// Check if target entity exists (when specified)
|
|
287
|
+
if (capability.target && !this.entityNames.has(capability.target)) {
|
|
288
|
+
this.addError({
|
|
289
|
+
code: 'UNDEFINED_CAPABILITY_TARGET',
|
|
290
|
+
message: `Capability references undefined entity '${capability.target}'`,
|
|
291
|
+
location: capability.loc,
|
|
292
|
+
context: { entityName, referencedEntity: capability.target },
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// ===========================================================================
|
|
297
|
+
// API Validation
|
|
298
|
+
// ===========================================================================
|
|
299
|
+
validateAPIs(apis) {
|
|
300
|
+
for (const api of apis) {
|
|
301
|
+
this.validateAPI(api);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
validateAPI(api) {
|
|
305
|
+
// Validate CRUD shorthands
|
|
306
|
+
for (const crud of api.crud) {
|
|
307
|
+
this.validateCRUD(crud, api.name);
|
|
308
|
+
}
|
|
309
|
+
// Validate operations
|
|
310
|
+
for (const operation of api.operations) {
|
|
311
|
+
this.validateAPIOperation(operation, api.name);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
validateCRUD(crud, apiName) {
|
|
315
|
+
// Check if CRUD target entity exists
|
|
316
|
+
if (!this.entityNames.has(crud.entity)) {
|
|
317
|
+
this.addError({
|
|
318
|
+
code: 'UNDEFINED_CRUD_ENTITY',
|
|
319
|
+
message: `CRUD shorthand references undefined entity '${crud.entity}'`,
|
|
320
|
+
location: crud.loc,
|
|
321
|
+
context: { referencedEntity: crud.entity },
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
validateAPIOperation(operation, apiName) {
|
|
326
|
+
const operationContext = `${apiName}.${operation.name}`;
|
|
327
|
+
// Validate return type entity reference
|
|
328
|
+
const returnType = operation.returnType.type;
|
|
329
|
+
// Skip validation for void and object types
|
|
330
|
+
if (returnType !== 'void' && !returnType.startsWith('{')) {
|
|
331
|
+
// Check if it's an entity reference
|
|
332
|
+
if (!this.entityNames.has(returnType)) {
|
|
333
|
+
// Could be a primitive or inline type - just warn for now
|
|
334
|
+
this.addWarning({
|
|
335
|
+
code: 'UNDEFINED_RETURN_TYPE_ENTITY',
|
|
336
|
+
message: `API operation '${operation.name}' returns unknown type '${returnType}'`,
|
|
337
|
+
location: operation.loc,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// Validate joined entities in return type
|
|
342
|
+
if (operation.returnType.joined) {
|
|
343
|
+
for (const join of operation.returnType.joined) {
|
|
344
|
+
if (!this.entityNames.has(join.entity)) {
|
|
345
|
+
this.addError({
|
|
346
|
+
code: 'UNDEFINED_JOINED_ENTITY',
|
|
347
|
+
message: `API operation '${operation.name}' joins undefined entity '${join.entity}'`,
|
|
348
|
+
location: operation.returnType.loc,
|
|
349
|
+
context: { referencedEntity: join.entity },
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// Validate effect annotations
|
|
355
|
+
const validEffects = new Set(['DB', 'Auth', 'Email', 'Audit', 'Queue', 'Cache', 'External']);
|
|
356
|
+
for (const effect of operation.effects) {
|
|
357
|
+
if (!validEffects.has(effect)) {
|
|
358
|
+
this.addError({
|
|
359
|
+
code: 'INVALID_EFFECT_TYPE',
|
|
360
|
+
message: `API operation '${operation.name}' has invalid effect '${effect}'`,
|
|
361
|
+
location: operation.loc,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// Validate parameter types and check for duplicates
|
|
366
|
+
const paramNames = new Set();
|
|
367
|
+
for (const param of operation.parameters) {
|
|
368
|
+
// Check for duplicate parameter names
|
|
369
|
+
if (paramNames.has(param.name)) {
|
|
370
|
+
this.addError({
|
|
371
|
+
code: 'DUPLICATE_PARAMETER_NAME',
|
|
372
|
+
message: `API operation '${operation.name}' has duplicate parameter '${param.name}'`,
|
|
373
|
+
location: param.loc,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
paramNames.add(param.name);
|
|
377
|
+
// Validate parameter type
|
|
378
|
+
this.validateFieldType(param.type, param.loc, operationContext, param.name);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// ===========================================================================
|
|
382
|
+
// Journey Validation
|
|
383
|
+
// ===========================================================================
|
|
384
|
+
validateJourneys(journeys) {
|
|
385
|
+
for (const journey of journeys) {
|
|
386
|
+
this.validateJourney(journey);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
validateJourney(journey) {
|
|
390
|
+
const stepNames = new Set();
|
|
391
|
+
for (const step of journey.steps) {
|
|
392
|
+
stepNames.add(step.name);
|
|
393
|
+
}
|
|
394
|
+
// Validate step references
|
|
395
|
+
for (const step of journey.steps) {
|
|
396
|
+
// Check if 'next' step exists
|
|
397
|
+
if (step.next && !stepNames.has(step.next)) {
|
|
398
|
+
this.addError({
|
|
399
|
+
code: 'UNDEFINED_PAGE_REFERENCE',
|
|
400
|
+
message: `Journey step '${step.name}' references undefined next step '${step.next}'`,
|
|
401
|
+
location: step.loc,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
// Check if page exists
|
|
405
|
+
if (step.page && !this.pageNames.has(step.page) && !this.formNames.has(step.page)) {
|
|
406
|
+
this.addWarning({
|
|
407
|
+
code: 'UNDEFINED_PAGE_REFERENCE',
|
|
408
|
+
message: `Journey step '${step.name}' references undefined page '${step.page}'`,
|
|
409
|
+
location: step.loc,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// Validate recovery handlers
|
|
414
|
+
for (const recovery of journey.recovery) {
|
|
415
|
+
if (!stepNames.has(recovery.step)) {
|
|
416
|
+
this.addError({
|
|
417
|
+
code: 'UNDEFINED_PAGE_REFERENCE',
|
|
418
|
+
message: `Recovery handler references undefined step '${recovery.step}'`,
|
|
419
|
+
location: recovery.loc,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// ===========================================================================
|
|
425
|
+
// Page Validation
|
|
426
|
+
// ===========================================================================
|
|
427
|
+
validatePages(pages) {
|
|
428
|
+
for (const page of pages) {
|
|
429
|
+
this.validatePage(page);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
validatePage(page) {
|
|
433
|
+
// Validate component reference
|
|
434
|
+
if (page.component && !this.formNames.has(page.component)) {
|
|
435
|
+
// Component might be an external component - just warn
|
|
436
|
+
this.addWarning({
|
|
437
|
+
code: 'UNDEFINED_PAGE_REFERENCE',
|
|
438
|
+
message: `Page '${page.name}' references component '${page.component}' which is not defined in the spec`,
|
|
439
|
+
location: page.loc,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// ===========================================================================
|
|
444
|
+
// Form Validation
|
|
445
|
+
// ===========================================================================
|
|
446
|
+
validateForms(forms) {
|
|
447
|
+
for (const form of forms) {
|
|
448
|
+
this.validateForm(form);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
validateForm(form) {
|
|
452
|
+
const fieldNames = new Set();
|
|
453
|
+
for (const field of form.fields) {
|
|
454
|
+
// Check for duplicate field names
|
|
455
|
+
if (fieldNames.has(field.name)) {
|
|
456
|
+
this.addError({
|
|
457
|
+
code: 'DUPLICATE_FIELD_NAME',
|
|
458
|
+
message: `Duplicate field name '${field.name}' in form '${form.name}'`,
|
|
459
|
+
location: field.loc,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
fieldNames.add(field.name);
|
|
463
|
+
}
|
|
464
|
+
// Validate layout references
|
|
465
|
+
for (const row of form.layout) {
|
|
466
|
+
for (const fieldRef of row.fields) {
|
|
467
|
+
if (!fieldNames.has(fieldRef)) {
|
|
468
|
+
this.addError({
|
|
469
|
+
code: 'UNDEFINED_PAGE_REFERENCE',
|
|
470
|
+
message: `Form layout references undefined field '${fieldRef}'`,
|
|
471
|
+
location: row.loc,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
// ===========================================================================
|
|
478
|
+
// Helper Methods
|
|
479
|
+
// ===========================================================================
|
|
480
|
+
addError(error) {
|
|
481
|
+
this.errors.push(error);
|
|
482
|
+
}
|
|
483
|
+
addWarning(warning) {
|
|
484
|
+
this.warnings.push(warning);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
// =============================================================================
|
|
488
|
+
// Convenience Function
|
|
489
|
+
// =============================================================================
|
|
490
|
+
/**
|
|
491
|
+
* Validate a Kappa specification AST
|
|
492
|
+
*/
|
|
493
|
+
export function validateKappaSpec(spec) {
|
|
494
|
+
const validator = new KappaValidator();
|
|
495
|
+
return validator.validate(spec);
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Format a validation error for display (simple format)
|
|
499
|
+
*/
|
|
500
|
+
export function formatValidationError(error) {
|
|
501
|
+
const { code, message, location, context } = error;
|
|
502
|
+
const locStr = `${location.startLine}:${location.startColumn}`;
|
|
503
|
+
let contextStr = '';
|
|
504
|
+
if (context) {
|
|
505
|
+
const parts = [];
|
|
506
|
+
if (context.entityName)
|
|
507
|
+
parts.push(`entity: ${context.entityName}`);
|
|
508
|
+
if (context.fieldName)
|
|
509
|
+
parts.push(`field: ${context.fieldName}`);
|
|
510
|
+
if (context.referencedEntity)
|
|
511
|
+
parts.push(`references: ${context.referencedEntity}`);
|
|
512
|
+
if (parts.length > 0) {
|
|
513
|
+
contextStr = ` (${parts.join(', ')})`;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return `[${code}] ${locStr}: ${message}${contextStr}`;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Format a validation error with code snippet and visual indicators
|
|
520
|
+
*/
|
|
521
|
+
export function formatValidationErrorRich(error, options = {}) {
|
|
522
|
+
const { code, message, location, context } = error;
|
|
523
|
+
const { source, colors = false, contextLines = 1 } = options;
|
|
524
|
+
const lines = [];
|
|
525
|
+
// Error header with location
|
|
526
|
+
const locStr = `${location.startLine}:${location.startColumn}`;
|
|
527
|
+
const header = colors
|
|
528
|
+
? `\x1b[31merror\x1b[0m[\x1b[33m${code}\x1b[0m]: ${message}`
|
|
529
|
+
: `error[${code}]: ${message}`;
|
|
530
|
+
lines.push(header);
|
|
531
|
+
// Location pointer
|
|
532
|
+
const locationLine = colors
|
|
533
|
+
? ` \x1b[36m-->\x1b[0m ${locStr}`
|
|
534
|
+
: ` --> ${locStr}`;
|
|
535
|
+
lines.push(locationLine);
|
|
536
|
+
// Code snippet if source is provided
|
|
537
|
+
if (source) {
|
|
538
|
+
const sourceLines = source.split('\n');
|
|
539
|
+
const errorLine = location.startLine;
|
|
540
|
+
const startLine = Math.max(1, errorLine - contextLines);
|
|
541
|
+
const endLine = Math.min(sourceLines.length, errorLine + contextLines);
|
|
542
|
+
// Calculate gutter width based on line numbers
|
|
543
|
+
const gutterWidth = String(endLine).length;
|
|
544
|
+
lines.push(' |');
|
|
545
|
+
for (let i = startLine; i <= endLine; i++) {
|
|
546
|
+
const lineContent = sourceLines[i - 1] || '';
|
|
547
|
+
const lineNum = String(i).padStart(gutterWidth, ' ');
|
|
548
|
+
if (i === errorLine) {
|
|
549
|
+
// Error line - highlight it
|
|
550
|
+
const prefix = colors ? `\x1b[31m${lineNum}\x1b[0m |` : `${lineNum} |`;
|
|
551
|
+
lines.push(`${prefix} ${lineContent}`);
|
|
552
|
+
// Add caret pointer
|
|
553
|
+
const caretPadding = ' '.repeat(location.startColumn - 1);
|
|
554
|
+
const caretLength = Math.max(1, Math.min(location.endColumn - location.startColumn, lineContent.length - location.startColumn + 1));
|
|
555
|
+
const caret = '^'.repeat(caretLength);
|
|
556
|
+
const caretLine = colors
|
|
557
|
+
? `${''.padStart(gutterWidth, ' ')} | ${caretPadding}\x1b[31m${caret}\x1b[0m`
|
|
558
|
+
: `${''.padStart(gutterWidth, ' ')} | ${caretPadding}${caret}`;
|
|
559
|
+
lines.push(caretLine);
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
// Context line
|
|
563
|
+
const prefix = colors
|
|
564
|
+
? `\x1b[90m${lineNum}\x1b[0m |`
|
|
565
|
+
: `${lineNum} |`;
|
|
566
|
+
const content = colors ? `\x1b[90m${lineContent}\x1b[0m` : lineContent;
|
|
567
|
+
lines.push(`${prefix} ${content}`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
lines.push(' |');
|
|
571
|
+
}
|
|
572
|
+
// Add context info if available
|
|
573
|
+
if (context) {
|
|
574
|
+
const parts = [];
|
|
575
|
+
if (context.entityName)
|
|
576
|
+
parts.push(`entity: ${context.entityName}`);
|
|
577
|
+
if (context.fieldName)
|
|
578
|
+
parts.push(`field: ${context.fieldName}`);
|
|
579
|
+
if (context.referencedEntity)
|
|
580
|
+
parts.push(`references: ${context.referencedEntity}`);
|
|
581
|
+
if (parts.length > 0) {
|
|
582
|
+
const contextLine = colors
|
|
583
|
+
? ` \x1b[90m= note: ${parts.join(', ')}\x1b[0m`
|
|
584
|
+
: ` = note: ${parts.join(', ')}`;
|
|
585
|
+
lines.push(contextLine);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return lines.join('\n');
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Format all validation results for display
|
|
592
|
+
*/
|
|
593
|
+
export function formatValidationResults(result, options = {}) {
|
|
594
|
+
const sections = [];
|
|
595
|
+
// Format errors
|
|
596
|
+
if (result.errors.length > 0) {
|
|
597
|
+
for (const error of result.errors) {
|
|
598
|
+
sections.push(formatValidationErrorRich(error, options));
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
// Format warnings
|
|
602
|
+
if (result.warnings.length > 0) {
|
|
603
|
+
const warningOptions = { ...options };
|
|
604
|
+
for (const warning of result.warnings) {
|
|
605
|
+
// Modify message to indicate it's a warning
|
|
606
|
+
const warningError = {
|
|
607
|
+
...warning,
|
|
608
|
+
message: `(warning) ${warning.message}`,
|
|
609
|
+
};
|
|
610
|
+
sections.push(formatValidationErrorRich(warningError, warningOptions));
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
// Summary
|
|
614
|
+
const errorCount = result.errors.length;
|
|
615
|
+
const warningCount = result.warnings.length;
|
|
616
|
+
const summaryParts = [];
|
|
617
|
+
if (errorCount > 0) {
|
|
618
|
+
const errorText = errorCount === 1 ? 'error' : 'errors';
|
|
619
|
+
summaryParts.push(options.colors
|
|
620
|
+
? `\x1b[31m${errorCount} ${errorText}\x1b[0m`
|
|
621
|
+
: `${errorCount} ${errorText}`);
|
|
622
|
+
}
|
|
623
|
+
if (warningCount > 0) {
|
|
624
|
+
const warningText = warningCount === 1 ? 'warning' : 'warnings';
|
|
625
|
+
summaryParts.push(options.colors
|
|
626
|
+
? `\x1b[33m${warningCount} ${warningText}\x1b[0m`
|
|
627
|
+
: `${warningCount} ${warningText}`);
|
|
628
|
+
}
|
|
629
|
+
if (summaryParts.length > 0) {
|
|
630
|
+
sections.push(`\nFound ${summaryParts.join(' and ')}`);
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
sections.push(options.colors
|
|
634
|
+
? `\n\x1b[32m✓ Validation passed\x1b[0m`
|
|
635
|
+
: '\n✓ Validation passed');
|
|
636
|
+
}
|
|
637
|
+
return sections.join('\n\n');
|
|
638
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { EntityBlock } from './kappa-ast.js';
|
|
2
|
+
export interface ZodGeneratorOptions {
|
|
3
|
+
/** Add provenance comments (default: true) */
|
|
4
|
+
provenance?: boolean;
|
|
5
|
+
/** Generate update schemas with partial fields (default: true) */
|
|
6
|
+
updateSchemas?: boolean;
|
|
7
|
+
/** Generate form schemas (default: false) */
|
|
8
|
+
formSchemas?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface GeneratedZodSchemas {
|
|
11
|
+
/** Main schemas file content */
|
|
12
|
+
schemas: string;
|
|
13
|
+
}
|
|
14
|
+
export declare class KappaZodGenerator {
|
|
15
|
+
private provenance;
|
|
16
|
+
private updateSchemas;
|
|
17
|
+
private formSchemas;
|
|
18
|
+
constructor(options?: ZodGeneratorOptions);
|
|
19
|
+
/**
|
|
20
|
+
* Generate Zod schemas from entity blocks
|
|
21
|
+
*/
|
|
22
|
+
generate(entities: EntityBlock[]): GeneratedZodSchemas;
|
|
23
|
+
private generateSchemasFile;
|
|
24
|
+
private generateEntitySchemas;
|
|
25
|
+
private generateFieldSchema;
|
|
26
|
+
private typeToZodSchema;
|
|
27
|
+
private primitiveToZod;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Generate Zod schemas from Kappa entities
|
|
31
|
+
*/
|
|
32
|
+
export declare function generateZodSchemas(entities: EntityBlock[], options?: ZodGeneratorOptions): GeneratedZodSchemas;
|