cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
json_heal.c
Go to the documentation of this file.
1#define _POSIX_C_SOURCE 200809L
4#include <cjson/cJSON.h>
5#include <stdio.h>
6#include <stdlib.h>
7#include <string.h>
8#include <strings.h>
9#include <ctype.h>
10
16/* ==========================================================================
17 * Internal dynamic string buffer (heap-backed, cwist_alloc / cwist_realloc)
18 * ======================================================================== */
19
20typedef struct {
21 char *data;
22 size_t len;
23 size_t cap;
24} strbuf_t;
25
33static bool strbuf_init(strbuf_t *b, size_t init_cap) {
34 b->data = (char *)cwist_alloc(init_cap + 1);
35 if (!b->data) return false;
36 b->len = 0;
37 b->cap = init_cap;
38 b->data[0] = '\0';
39 return true;
40}
41
49static bool strbuf_push(strbuf_t *b, char c) {
50 if (b->len >= b->cap) {
51 size_t new_cap = b->cap * 2 + 64;
52 char *nd = (char *)cwist_realloc(b->data, new_cap + 1);
53 if (!nd) return false;
54 b->data = nd;
55 b->cap = new_cap;
56 }
57 b->data[b->len++] = c;
58 b->data[b->len] = '\0';
59 return true;
60}
61
66static void strbuf_free(strbuf_t *b) {
67 if (b->data) {
68 cwist_free(b->data);
69 b->data = NULL;
70 }
71 b->len = b->cap = 0;
72}
73
74/* ==========================================================================
75 * Log helper
76 * ======================================================================== */
77
84static void log_append(char *log, size_t log_sz, const char *msg) {
85 if (!log || log_sz == 0) return;
86 size_t cur = strlen(log);
87 if (cur >= log_sz - 1) return;
88 strncat(log, msg, log_sz - cur - 1);
89}
90
91/* ==========================================================================
92 * L1 – Syntax healing
93 *
94 * Steps applied in order:
95 * 1. Strip UTF-8 BOM.
96 * 2. Remove JavaScript-style // line-comments (not valid JSON but common).
97 * 3. Fix trailing commas before ] or }.
98 * 4. Balance unmatched { } [ ] by appending missing closers at the end.
99 * 5. Attempt cJSON_Parse; return NULL if it still fails.
100 * ======================================================================== */
101
109static void remove_trailing_commas(strbuf_t *b) {
110 char *d = b->data;
111 size_t len = b->len;
112 bool in_str = false;
113 bool esc = false;
114 for (size_t i = 0; i < len; i++) {
115 char c = d[i];
116 if (esc) {
117 esc = false;
118 continue;
119 }
120 if (c == '\\' && in_str) {
121 esc = true;
122 continue;
123 }
124 if (c == '"') {
125 in_str = !in_str;
126 continue;
127 }
128 if (in_str) continue;
129
130 if (c == ']' || c == '}') {
131 /* find last non-whitespace before i */
132 size_t j = i;
133 while (j > 0 &&
134 (d[j - 1] == ' ' || d[j - 1] == '\t' || d[j - 1] == '\r' || d[j - 1] == '\n')) {
135 j--;
136 }
137 if (j > 0 && d[j - 1] == ',') {
138 memmove(&d[j - 1], &d[j], len - j + 1);
139 len--;
140 b->len = len;
141 i--; /* re-examine the same position */
142 }
143 }
144 }
145}
146
157static void balance_brackets(strbuf_t *b, char *log, size_t log_sz) {
159 char *stack = (char *)cwist_scratch_alloc(&stack_s, b->len + 1);
160 if (!stack) {
161 log_append(log, log_sz, "[L1] bracket-balance skipped (alloc failure); ");
162 return;
163 }
164 int top = 0;
165 bool in_string = false;
166 bool escaped = false;
167
168 const char *d = b->data;
169 size_t len = b->len;
170
171 for (size_t i = 0; i < len; i++) {
172 char c = d[i];
173 if (escaped) {
174 escaped = false;
175 continue;
176 }
177 if (c == '\\' && in_string) {
178 escaped = true;
179 continue;
180 }
181 if (c == '"') {
183 continue;
184 }
185 if (in_string) continue;
186
187 if (c == '{')
188 stack[top++] = '}';
189 else if (c == '[')
190 stack[top++] = ']';
191 else if (c == '}' || c == ']') {
192 if (top > 0 && stack[top - 1] == c) top--;
193 /* mismatched closer - ignore; cJSON will flag remaining issues */
194 }
195 }
196
197 if (top > 0) {
198 char msg[64];
199 snprintf(msg, sizeof(msg), "[L1] appended %d missing closer(s); ", top);
200 log_append(log, log_sz, msg);
201 }
202
203 while (top > 0) strbuf_push(b, stack[--top]);
204}
205
219static char *l1_heal(const char *input, char *log, size_t log_sz) {
220 if (!input) return NULL;
221 size_t in_len = strlen(input);
222
223 strbuf_t b;
224 if (!strbuf_init(&b, in_len + 64)) return NULL;
225
226 const char *p = input;
227
228 /* 1. Strip UTF-8 BOM */
229 if (in_len >= 3 && (unsigned char)p[0] == 0xEF && (unsigned char)p[1] == 0xBB &&
230 (unsigned char)p[2] == 0xBF) {
231 p += 3;
232 log_append(log, log_sz, "[L1] stripped BOM; ");
233 }
234
235 /* 2. Copy, stripping // line-comments outside strings */
236 bool in_str = false;
237 bool esc = false;
238 bool stripped = false;
239
240 while (*p) {
241 char c = *p;
242 if (esc) {
243 esc = false;
244 strbuf_push(&b, c);
245 p++;
246 continue;
247 }
248 if (c == '\\' && in_str) {
249 esc = true;
250 strbuf_push(&b, c);
251 p++;
252 continue;
253 }
254 if (c == '"') {
255 in_str = !in_str;
256 strbuf_push(&b, c);
257 p++;
258 continue;
259 }
260 if (!in_str && c == '/' && p[1] == '/') {
261 while (*p && *p != '\n') p++;
262 stripped = true;
263 continue;
264 }
265 strbuf_push(&b, c);
266 p++;
267 }
268 if (stripped) log_append(log, log_sz, "[L1] removed line comment(s); ");
269
270 /* 3. Balance brackets */
271 balance_brackets(&b, log, log_sz);
272
273 /* 4. Fix trailing commas (including before newly balanced closers) */
274 size_t old_len = b.len;
275 remove_trailing_commas(&b);
276 if (b.len != old_len) log_append(log, log_sz, "[L1] removed trailing comma(s); ");
277
278 /* 5. Try to parse */
279 cJSON *test = cJSON_Parse(b.data);
280 if (!test) {
281 strbuf_free(&b);
282 return NULL;
283 }
285 return b.data; /* caller owns (cwist_alloc backing) */
286}
287
288/* ==========================================================================
289 * L2 – Schema alignment (field renaming + type coercion)
290 * ======================================================================== */
291
301static void normalise_name(const char *src, char *dst, size_t dst_sz) {
302 size_t j = 0;
303 for (size_t i = 0; src[i] && j < dst_sz - 1; i++) {
304 char c = src[i];
305 if (c == '_' || c == '-') continue;
306 dst[j++] = (char)tolower((unsigned char)c);
307 }
308 dst[j] = '\0';
309}
310
318static bool names_fuzzy_match(const char *a, const char *b) {
319 char na[64], nb[64];
320 normalise_name(a, na, sizeof(na));
321 normalise_name(b, nb, sizeof(nb));
322 return strcmp(na, nb) == 0;
323}
324
332static cJSON *find_fuzzy(cJSON *obj, const char *name, const char **found_key) {
333 cJSON *child = obj->child;
334 while (child) {
335 if (names_fuzzy_match(child->string, name)) {
336 if (found_key) *found_key = child->string;
337 return child;
338 }
339 child = child->next;
340 }
341 return NULL;
342}
343
358int cwist_json_schema_align(cJSON *obj, const cwist_schema_t *schema, char *log, size_t log_sz) {
359 if (!obj || !schema || !cJSON_IsObject(obj)) return -1;
360
361 int changes = 0;
362
363 for (int fi = 0; fi < schema->field_count; fi++) {
364 const cwist_schema_field_t *fd = &schema->fields[fi];
365
366 /* ---- Locate the field in the JSON object ---- */
367 cJSON *item = NULL;
368 const char *found_as = NULL;
369
370 /* Try canonical name first */
372 if (item) {
373 found_as = fd->name;
374 }
375
376 /* Try explicit aliases */
377 if (!item) {
378 for (int ai = 0; ai < CWIST_SCHEMA_MAX_ALIASES; ai++) {
379 if (!fd->aliases[ai]) break;
381 if (item) {
382 found_as = fd->aliases[ai];
383 break;
384 }
385 /* fuzzy alias match */
386 const char *fk = NULL;
387 item = find_fuzzy(obj, fd->aliases[ai], &fk);
388 if (item) {
389 found_as = fk;
390 break;
391 }
392 }
393 }
394
395 /* Fuzzy match on canonical name */
396 if (!item) {
397 const char *fk = NULL;
398 item = find_fuzzy(obj, fd->name, &fk);
399 if (item) found_as = fk;
400 }
401
402 if (!item) continue;
403
404 /* ---- Rename to canonical name if needed ---- */
405 if (strcmp(found_as, fd->name) != 0) {
407 if (clone) {
410 item = clone;
411 char msg[128];
412 snprintf(msg, sizeof(msg), "[L2] '%s'→'%s'; ", found_as, fd->name);
413 log_append(log, log_sz, msg);
414 changes++;
415 }
416 }
417
418 /* ---- Type coercion ---- */
419 bool type_ok;
420 switch (fd->type) {
422 case CWIST_FIELD_INT:
427 default: type_ok = true; break;
428 }
429
430 if (type_ok) continue;
431
432 char msg[128];
433
434 /* String → Number */
435 if ((fd->type == CWIST_FIELD_INT || fd->type == CWIST_FIELD_FLOAT) &&
436 cJSON_IsString(item) && item->valuestring) {
437 char *end = NULL;
438 double v = strtod(item->valuestring, &end);
439 if (end && *end == '\0') {
441 if (num) {
443 snprintf(msg, sizeof(msg), "[L2] '%s': str→num; ", fd->name);
444 log_append(log, log_sz, msg);
445 changes++;
446 }
447 }
448 }
449 /* Number → String */
450 else if (fd->type == CWIST_FIELD_STRING && cJSON_IsNumber(item)) {
451 char numstr[64];
452 snprintf(numstr, sizeof(numstr), "%g", item->valuedouble);
454 if (str) {
456 snprintf(msg, sizeof(msg), "[L2] '%s': num→str; ", fd->name);
457 log_append(log, log_sz, msg);
458 changes++;
459 }
460 }
461 /* String "true"/"false"/"1"/"0" → Bool */
462 else if (fd->type == CWIST_FIELD_BOOL && cJSON_IsString(item) && item->valuestring) {
463 int bval = -1;
464 if (strcasecmp(item->valuestring, "true") == 0 || strcmp(item->valuestring, "1") == 0)
465 bval = 1;
466 if (strcasecmp(item->valuestring, "false") == 0 || strcmp(item->valuestring, "0") == 0)
467 bval = 0;
468 if (bval >= 0) {
470 if (bj) {
472 snprintf(msg, sizeof(msg), "[L2] '%s': str→bool; ", fd->name);
473 log_append(log, log_sz, msg);
474 changes++;
475 }
476 }
477 }
478 /* Number 0/1 → Bool */
479 else if (fd->type == CWIST_FIELD_BOOL && cJSON_IsNumber(item)) {
480 cJSON *bj = (item->valuedouble != 0.0) ? cJSON_CreateTrue() : cJSON_CreateFalse();
481 if (bj) {
483 snprintf(msg, sizeof(msg), "[L2] '%s': num→bool; ", fd->name);
484 log_append(log, log_sz, msg);
485 changes++;
486 }
487 }
488 }
489
490 return changes;
491}
492
493/* ==========================================================================
494 * Public API
495 * ======================================================================== */
496
510 cwist_heal_result_t result;
511 memset(&result, 0, sizeof(result));
512
513 if (!input) return result;
514
515 double threshold = (cfg && cfg->threshold > 0.0) ? cfg->threshold : 0.8;
516
517 /* ---- Try direct parse ---- */
519 if (parsed) {
520 if (cfg && cfg->schema) {
521 int changes =
523 if (changes > 0) {
524 result.healed = true;
525 result.level = 2;
526 result.confidence = 1.0;
527 }
528 }
530 if (!result.healed) result.confidence = 1.0;
532 return result;
533 }
534
535 /* ---- L1: Syntax fix ---- */
536 char *fixed = l1_heal(input, result.log, CWIST_HEAL_LOG_MAX);
537 if (fixed) {
540 if (parsed) {
541 result.level = 1;
542 result.healed = true;
543 result.confidence = 0.9;
544
545 if (cfg && cfg->schema) {
546 int ch =
548 if (ch > 0) result.level = 2;
549 }
550
551 if (result.confidence >= threshold) result.json = cJSON_PrintUnformatted(parsed);
552
554 return result;
555 }
556 }
557
558 /* ---- L3: SLLM callback ---- */
559 if (cfg && cfg->sllm_fn) {
560 char *recovered = cfg->sllm_fn(input, cfg->schema, cfg->sllm_userdata);
561 if (recovered) {
563 free(recovered); /* SLLM callback uses plain malloc per contract */
564 if (parsed) {
565 result.level = 3;
566 result.healed = true;
567 result.confidence = 0.7; /* heuristic */
568 log_append(result.log, CWIST_HEAL_LOG_MAX, "[L3] SLLM recovery; ");
569
570 if (result.confidence >= threshold) result.json = cJSON_PrintUnformatted(parsed);
571
573 }
574 }
575 }
576
577 return result;
578}
579
585 if (!r) return;
586 /* result.json comes from cJSON_PrintUnformatted → plain malloc */
587 if (r->json) {
588 free(r->json);
589 r->json = NULL;
590 }
591}
Memory allocation wrappers backed by libttak.
void cwist_free(void *ptr)
Free memory obtained via cwist_alloc/cwist_strdup/etc.
Definition alloc.c:52
void * cwist_realloc(void *ptr, size_t new_size)
Resize an existing allocation (libttak tracked).
Definition alloc.c:27
void * cwist_alloc(size_t size)
Allocate zeroed memory tracked by libttak.
Definition alloc.c:17
#define CWIST_SCRATCH_DEFER
Attach to a cwist_scratch_t variable's declaration so its allocation (arena- or heap-backed,...
Definition alloc.h:190
int cwist_json_schema_align(cJSON *obj, const cwist_schema_t *schema, char *log, size_t log_sz)
Align a parsed JSON object with a schema in place (L2 stage).
Definition json_heal.c:358
void cwist_heal_result_free(cwist_heal_result_t *r)
Free heap storage owned by a healing result.
Definition json_heal.c:584
cwist_heal_result_t cwist_json_heal(const char *input, const cwist_heal_config_t *cfg)
Recover malformed JSON using syntax repair, schema alignment, and an optional callback.
Definition json_heal.c:509
Self-Healing JSON Recovery Layer (CWIST)
@ CWIST_FIELD_INT
Definition json_heal.h:42
@ CWIST_FIELD_BOOL
Definition json_heal.h:44
@ CWIST_FIELD_FLOAT
Definition json_heal.h:43
@ CWIST_FIELD_OBJECT
Definition json_heal.h:45
@ CWIST_FIELD_ARRAY
Definition json_heal.h:46
@ CWIST_FIELD_STRING
Definition json_heal.h:41
#define CWIST_HEAL_LOG_MAX
Definition json_heal.h:108
#define CWIST_SCHEMA_MAX_ALIASES
Definition json_heal.h:49
size_t cap
Definition otel.c:56
Configuration passed to cwist_json_heal().
Definition json_heal.h:97
void * sllm_userdata
Passed verbatim to sllm_fn.
Definition json_heal.h:101
const cwist_schema_t * schema
Schema for L2 alignment (may be NULL).
Definition json_heal.h:99
double threshold
Min confidence (0.0–1.0) to accept recovery. Default: 0.8.
Definition json_heal.h:98
cwist_sllm_heal_fn sllm_fn
Optional L3 deep-recovery callback.
Definition json_heal.h:100
Result of a healing attempt.
Definition json_heal.h:111
char * json
Healed JSON string (free with cwist_heal_result_free). NULL on failure.
Definition json_heal.h:112
bool healed
True if any modification was made.
Definition json_heal.h:113
double confidence
Estimated confidence in the recovery (0.0–1.0).
Definition json_heal.h:115
int level
Recovery level reached: 0=none, 1=L1, 2=L2, 3=L3.
Definition json_heal.h:114
char log[CWIST_HEAL_LOG_MAX]
Human-readable description of changes made.
Definition json_heal.h:116
Single field descriptor used by both the healer and the Zod validator.
Definition json_heal.h:58
const char * name
Canonical field name.
Definition json_heal.h:59
const char * aliases[CWIST_SCHEMA_MAX_ALIASES]
Alternative names (NULL-terminated).
Definition json_heal.h:60
cwist_field_type_t type
Expected value type.
Definition json_heal.h:61
A complete schema: array of field descriptors + count.
Definition json_heal.h:66
int field_count
Definition json_heal.h:68
const cwist_schema_field_t * fields
Definition json_heal.h:67
Backing state for cwist_scratch_alloc()/CWIST_SCRATCH_DEFER: a single block-scoped allocation that pr...
Definition alloc.h:131