@probelabs/probe 0.6.0-rc149 → 0.6.0-rc151

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.
@@ -5,6 +5,65 @@
5
5
 
6
6
  import { createMessagePreview } from '../tools/common.js';
7
7
  import { validate, fixText, extractMermaidBlocks } from '@probelabs/maid';
8
+ import Ajv from 'ajv';
9
+
10
+ /**
11
+ * Recursively apply additionalProperties: false to all object schemas
12
+ * This ensures strict validation at all nesting levels
13
+ * @param {Object} schema - JSON schema object
14
+ * @returns {Object} - Modified schema with additionalProperties enforced
15
+ */
16
+ function enforceNoAdditionalProperties(schema) {
17
+ if (!schema || typeof schema !== 'object') {
18
+ return schema;
19
+ }
20
+
21
+ // Create a deep clone to avoid modifying the original
22
+ const cloned = JSON.parse(JSON.stringify(schema));
23
+
24
+ function applyRecursively(obj) {
25
+ if (!obj || typeof obj !== 'object') {
26
+ return;
27
+ }
28
+
29
+ // If this is an object type schema and doesn't have additionalProperties set
30
+ if (obj.type === 'object' && obj.additionalProperties === undefined) {
31
+ obj.additionalProperties = false;
32
+ }
33
+
34
+ // Recursively process properties
35
+ if (obj.properties && typeof obj.properties === 'object') {
36
+ Object.values(obj.properties).forEach(applyRecursively);
37
+ }
38
+
39
+ // Process items for arrays
40
+ if (obj.items) {
41
+ if (Array.isArray(obj.items)) {
42
+ obj.items.forEach(applyRecursively);
43
+ } else {
44
+ applyRecursively(obj.items);
45
+ }
46
+ }
47
+
48
+ // Process nested schemas in oneOf, anyOf, allOf
49
+ ['oneOf', 'anyOf', 'allOf'].forEach(key => {
50
+ if (Array.isArray(obj[key])) {
51
+ obj[key].forEach(applyRecursively);
52
+ }
53
+ });
54
+
55
+ // Process definitions/defs
56
+ if (obj.definitions && typeof obj.definitions === 'object') {
57
+ Object.values(obj.definitions).forEach(applyRecursively);
58
+ }
59
+ if (obj.$defs && typeof obj.$defs === 'object') {
60
+ Object.values(obj.$defs).forEach(applyRecursively);
61
+ }
62
+ }
63
+
64
+ applyRecursively(cloned);
65
+ return cloned;
66
+ }
8
67
 
9
68
  /**
10
69
  * HTML entity decoder map for common entities that might appear in mermaid diagrams
@@ -141,12 +200,15 @@ export function cleanSchemaResponse(response) {
141
200
  * @returns {Object} - {isValid: boolean, parsed?: Object, error?: string}
142
201
  */
143
202
  export function validateJsonResponse(response, options = {}) {
144
- const { debug = false } = options;
203
+ const { debug = false, schema = null, strictSchema = true } = options;
145
204
 
146
205
  if (debug) {
147
206
  console.log(`[DEBUG] JSON validation: Starting validation for response (${response.length} chars)`);
148
207
  const preview = createMessagePreview(response);
149
208
  console.log(`[DEBUG] JSON validation: Preview: ${preview}`);
209
+ if (schema) {
210
+ console.log(`[DEBUG] JSON validation: Schema validation enabled`);
211
+ }
150
212
  }
151
213
 
152
214
  try {
@@ -159,6 +221,140 @@ export function validateJsonResponse(response, options = {}) {
159
221
  console.log(`[DEBUG] JSON validation: Object type: ${typeof parsed}, keys: ${Object.keys(parsed || {}).length}`);
160
222
  }
161
223
 
224
+ // If schema provided, validate against it
225
+ if (schema) {
226
+ const schemaStart = Date.now();
227
+ let schemaObj;
228
+
229
+ try {
230
+ // Parse schema if it's a string
231
+ schemaObj = typeof schema === 'string' ? JSON.parse(schema) : schema;
232
+
233
+ // Apply strict mode: enforce additionalProperties: false on all nested objects
234
+ // unless explicitly disabled via strictSchema: false option
235
+ if (strictSchema) {
236
+ schemaObj = enforceNoAdditionalProperties(schemaObj);
237
+ if (debug) {
238
+ console.log(`[DEBUG] JSON validation: Applied strict mode - additionalProperties: false enforced on all levels`);
239
+ }
240
+ }
241
+ } catch (schemaParseError) {
242
+ if (debug) {
243
+ console.log(`[DEBUG] JSON validation: Failed to parse schema: ${schemaParseError.message}`);
244
+ }
245
+ return {
246
+ isValid: false,
247
+ error: 'Invalid schema provided',
248
+ schemaError: schemaParseError.message
249
+ };
250
+ }
251
+
252
+ // Create AJV validator with strict mode
253
+ const ajv = new Ajv({
254
+ strict: false, // Don't fail on unknown schema keywords
255
+ allErrors: true, // Return all errors, not just the first
256
+ verbose: true // Include schema and data in errors
257
+ });
258
+
259
+ let validate;
260
+ try {
261
+ validate = ajv.compile(schemaObj);
262
+ } catch (compileError) {
263
+ if (debug) {
264
+ console.log(`[DEBUG] JSON validation: Schema compilation failed: ${compileError.message}`);
265
+ }
266
+ return {
267
+ isValid: false,
268
+ error: 'Schema compilation failed',
269
+ schemaError: compileError.message
270
+ };
271
+ }
272
+
273
+ const valid = validate(parsed);
274
+ const schemaTime = Date.now() - schemaStart;
275
+
276
+ if (debug) {
277
+ console.log(`[DEBUG] JSON validation: Schema validation completed in ${schemaTime}ms, valid: ${valid}`);
278
+ }
279
+
280
+ if (!valid) {
281
+ // Format schema errors for better readability and actionability
282
+ const formattedErrors = validate.errors.map(err => {
283
+ // Convert JSON Pointer path to dot notation for readability
284
+ // /user/profile/age -> user.profile.age
285
+ const path = err.instancePath
286
+ ? err.instancePath.substring(1).replace(/\//g, '.')
287
+ : '<root>';
288
+
289
+ let message = '';
290
+ let suggestion = '';
291
+
292
+ // Create crisp, actionable error messages based on error type
293
+ if (err.keyword === 'additionalProperties') {
294
+ const extraField = err.params.additionalProperty;
295
+ message = `Extra field '${extraField}' is not allowed`;
296
+ suggestion = `Remove '${extraField}' or add it to the schema`;
297
+ } else if (err.keyword === 'required') {
298
+ const missingField = err.params.missingProperty;
299
+ message = `Missing required field '${missingField}'`;
300
+ suggestion = `Add '${missingField}' to this object`;
301
+ } else if (err.keyword === 'type') {
302
+ const expected = err.params.type;
303
+ const actual = typeof err.data;
304
+ const value = JSON.stringify(err.data);
305
+ message = `Wrong type: expected ${expected}, got ${actual} (value: ${value})`;
306
+ suggestion = `Change value to ${expected} type`;
307
+ } else if (err.keyword === 'enum') {
308
+ const allowed = err.params.allowedValues;
309
+ const value = JSON.stringify(err.data);
310
+ message = `Invalid value ${value}. Allowed: ${allowed.map(v => JSON.stringify(v)).join(', ')}`;
311
+ suggestion = `Use one of the allowed values`;
312
+ } else if (err.keyword === 'minimum' || err.keyword === 'maximum') {
313
+ const limit = err.params.limit;
314
+ const comparison = err.params.comparison;
315
+ message = `Value ${err.data} ${err.message} (${comparison} ${limit})`;
316
+ suggestion = `Adjust value to meet constraint`;
317
+ } else if (err.keyword === 'minLength' || err.keyword === 'maxLength') {
318
+ const limit = err.params.limit;
319
+ message = `String length ${err.message} (current: ${err.data.length}, ${err.keyword}: ${limit})`;
320
+ suggestion = `Adjust string length`;
321
+ } else if (err.keyword === 'pattern') {
322
+ message = `Value doesn't match required pattern: ${err.params.pattern}`;
323
+ suggestion = `Update value to match the pattern`;
324
+ } else {
325
+ // Fallback for other error types
326
+ message = err.message;
327
+ suggestion = '';
328
+ }
329
+
330
+ // Format: "path: message | suggestion"
331
+ const location = path ? `at '${path}'` : 'at root';
332
+ return suggestion
333
+ ? `${location}: ${message} → ${suggestion}`
334
+ : `${location}: ${message}`;
335
+ });
336
+
337
+ const errorSummary = formattedErrors.join('\n ');
338
+
339
+ if (debug) {
340
+ console.log(`[DEBUG] JSON validation: Schema validation errors:\n ${errorSummary}`);
341
+ }
342
+
343
+ return {
344
+ isValid: false,
345
+ error: 'Schema validation failed',
346
+ schemaErrors: validate.errors,
347
+ formattedErrors: formattedErrors,
348
+ errorSummary: `Schema validation failed:\n ${errorSummary}`,
349
+ parsed: parsed // Still return parsed data for debugging
350
+ };
351
+ }
352
+
353
+ if (debug) {
354
+ console.log(`[DEBUG] JSON validation: Schema validation passed`);
355
+ }
356
+ }
357
+
162
358
  return { isValid: true, parsed };
163
359
  } catch (error) {
164
360
  // Extract error position from error message if available
@@ -886,9 +1082,21 @@ When presented with broken JSON, analyze it thoroughly and provide the corrected
886
1082
  errorContext = validationResult.enhancedError;
887
1083
  }
888
1084
 
1085
+ // Add schema validation errors if present
1086
+ let schemaErrorDetails = '';
1087
+ if (validationResult.errorSummary) {
1088
+ schemaErrorDetails = `\n\nSchema Validation Errors:\n${validationResult.errorSummary}`;
1089
+ } else if (validationResult.schemaErrors && validationResult.schemaErrors.length > 0) {
1090
+ const errors = validationResult.schemaErrors.map(err => {
1091
+ const path = err.instancePath || '(root)';
1092
+ return ` ${path}: ${err.message}`;
1093
+ }).join('\n');
1094
+ schemaErrorDetails = `\n\nSchema Validation Errors:\n${errors}`;
1095
+ }
1096
+
889
1097
  const prompt = `Fix the following invalid JSON.
890
1098
 
891
- Error: ${errorContext}
1099
+ Error: ${errorContext}${schemaErrorDetails}
892
1100
 
893
1101
  Invalid JSON:
894
1102
  ${invalidJson}
@@ -896,7 +1104,7 @@ ${invalidJson}
896
1104
  Expected schema structure:
897
1105
  ${schema}
898
1106
 
899
- Provide only the corrected JSON without any markdown formatting or explanations.`;
1107
+ ${schemaErrorDetails ? 'CRITICAL: Pay special attention to the schema validation errors above. The JSON may be syntactically valid but does not conform to the required schema. Make sure to:\n- Include all required fields\n- Use correct data types\n- Remove any additional properties not defined in the schema (if additionalProperties is false)\n- Ensure all values match their schema constraints\n\n' : ''}Provide only the corrected JSON without any markdown formatting or explanations.`;
900
1108
 
901
1109
  try {
902
1110
  if (this.options.debug) {