tranfi 0.1.2 → 0.2.1

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.
Files changed (132) hide show
  1. package/LICENSE +177 -0
  2. package/NOTICE +8 -0
  3. package/README.md +443 -51
  4. package/app/assets/{index-pDFMluyz.js → index-BIAIKnrp.js} +1 -1
  5. package/app/index.html +1 -1
  6. package/binding.gyp +55 -3
  7. package/csrc/arena.c +7 -5
  8. package/csrc/batch.c +818 -71
  9. package/csrc/buffer.c +84 -8
  10. package/csrc/cJSON.c +262 -19
  11. package/csrc/cJSON.h +17 -1
  12. package/csrc/codec_csv.c +1074 -181
  13. package/csrc/codec_jsonl.c +830 -118
  14. package/csrc/codec_table.c +108 -78
  15. package/csrc/codec_text.c +286 -68
  16. package/csrc/compiler.c +31 -3
  17. package/csrc/config.h +21 -0
  18. package/csrc/dsl.c +4722 -485
  19. package/csrc/expr.c +363 -55
  20. package/csrc/expr.h +2 -0
  21. package/csrc/internal.h +316 -27
  22. package/csrc/ir.c +65 -18
  23. package/csrc/ir.h +41 -0
  24. package/csrc/ir_schema.c +20 -5
  25. package/csrc/ir_serialize.c +68 -6
  26. package/csrc/ir_sql.c +796 -185
  27. package/csrc/ir_validate.c +462 -6
  28. package/csrc/json_path.c +210 -0
  29. package/csrc/main.c +879 -30
  30. package/csrc/memory_estimate.c +477 -0
  31. package/csrc/op_acf.c +171 -21
  32. package/csrc/op_across.c +477 -0
  33. package/csrc/op_anomaly.c +167 -32
  34. package/csrc/op_assert.c +761 -0
  35. package/csrc/op_bin.c +168 -29
  36. package/csrc/op_cast.c +383 -55
  37. package/csrc/op_clip.c +30 -19
  38. package/csrc/op_date_trunc.c +208 -34
  39. package/csrc/op_datetime.c +259 -77
  40. package/csrc/op_derive.c +65 -97
  41. package/csrc/op_diff.c +146 -30
  42. package/csrc/op_ewma.c +149 -30
  43. package/csrc/op_explode.c +124 -26
  44. package/csrc/op_fill_down.c +125 -53
  45. package/csrc/op_fill_null.c +176 -31
  46. package/csrc/op_filter.c +89 -40
  47. package/csrc/op_frequency.c +571 -43
  48. package/csrc/op_grep.c +36 -18
  49. package/csrc/op_group_agg.c +1790 -119
  50. package/csrc/op_hash.c +48 -15
  51. package/csrc/op_head.c +21 -86
  52. package/csrc/op_interpolate.c +268 -62
  53. package/csrc/op_join.c +2700 -182
  54. package/csrc/op_json_extract.c +227 -0
  55. package/csrc/op_json_filter.c +384 -0
  56. package/csrc/op_json_flatten.c +293 -0
  57. package/csrc/op_json_schema.c +503 -0
  58. package/csrc/op_label_encode.c +328 -53
  59. package/csrc/op_lag.c +181 -0
  60. package/csrc/op_lead.c +141 -89
  61. package/csrc/op_normalize.c +363 -79
  62. package/csrc/op_onehot.c +345 -73
  63. package/csrc/op_pivot.c +1546 -162
  64. package/csrc/op_quarantine.c +189 -0
  65. package/csrc/op_registry.c +2062 -166
  66. package/csrc/op_rename.c +41 -50
  67. package/csrc/op_replace.c +270 -118
  68. package/csrc/op_rleid.c +297 -0
  69. package/csrc/op_rowid.c +559 -0
  70. package/csrc/op_sample.c +80 -23
  71. package/csrc/op_schema.c +1341 -0
  72. package/csrc/op_schema_infer.c +252 -0
  73. package/csrc/op_select.c +265 -65
  74. package/csrc/op_set.c +3449 -0
  75. package/csrc/op_skip.c +30 -87
  76. package/csrc/op_sort.c +670 -124
  77. package/csrc/op_source_name.c +120 -0
  78. package/csrc/op_split.c +65 -28
  79. package/csrc/op_split_data.c +41 -9
  80. package/csrc/op_stack.c +178 -222
  81. package/csrc/op_stats.c +206 -110
  82. package/csrc/op_step.c +217 -55
  83. package/csrc/op_tail.c +21 -12
  84. package/csrc/op_tee.c +338 -0
  85. package/csrc/op_top.c +260 -53
  86. package/csrc/op_trim.c +48 -19
  87. package/csrc/op_unique.c +1193 -150
  88. package/csrc/op_unpivot.c +100 -66
  89. package/csrc/op_validate.c +601 -24
  90. package/csrc/op_window.c +492 -51
  91. package/csrc/path_policy.c +85 -0
  92. package/csrc/pipeline.c +872 -99
  93. package/csrc/recipes.c +3 -1
  94. package/csrc/report.c +73 -30
  95. package/csrc/selector.c +1097 -0
  96. package/csrc/size_utils.c +352 -0
  97. package/csrc/spill.c +317 -0
  98. package/csrc/spill.h +21 -0
  99. package/csrc/tranfi.h +169 -1
  100. package/csrc/transform.h +209 -0
  101. package/csrc/transform_api.c +2237 -0
  102. package/csrc/transform_categorical.c +923 -0
  103. package/csrc/transform_internal.h +472 -0
  104. package/csrc/transform_json.c +3812 -0
  105. package/csrc/transform_numeric.c +1966 -0
  106. package/csrc/transform_sha256.c +154 -0
  107. package/csrc/transform_wasm.h +162 -0
  108. package/csrc/transform_wasm_api.c +1373 -0
  109. package/csrc/wasm_api.c +70 -9
  110. package/napi_api.c +219 -11
  111. package/napi_transform.c +1648 -0
  112. package/napi_transform.h +8 -0
  113. package/package.json +27 -11
  114. package/scripts/install-native.js +76 -0
  115. package/scripts/prepack.js +64 -0
  116. package/scripts/sync-csrc.js +23 -0
  117. package/src/cli.js +81 -41
  118. package/src/engines/duckdb.js +45 -12
  119. package/src/index.js +661 -42
  120. package/src/memory_policy.js +411 -0
  121. package/src/native.js +1 -5
  122. package/src/pipeline.js +454 -31
  123. package/src/recipe_json.js +80 -0
  124. package/src/server.js +10 -8
  125. package/src/transform.js +403 -0
  126. package/src/transform_error.js +10 -0
  127. package/src/wasm.js +6 -4
  128. package/wasm/index.js +498 -10
  129. package/wasm/tranfi_core.js +0 -0
  130. package/wasm/transform.js +1156 -0
  131. package/wasm/worker.js +786 -0
  132. package/csrc/plan.c +0 -206
@@ -1,36 +1,272 @@
1
1
  /*
2
- * op_validate.c — Like filter but adds _valid bool column, keeps all rows.
2
+ * op_validate.c - Row-local validation rule sets.
3
3
  *
4
- * Config: {"expr": "col('age') > 0"}
4
+ * Config:
5
+ * {"expr": "col('age') > 0", "audit": true, "audit_limit": 1000}
6
+ * {"rules": [{"name":"age_ok", "expr":"col('age') > 0"}], "audit": true}
5
7
  */
6
8
 
7
9
  #include "internal.h"
8
10
  #include "cJSON.h"
11
+ #include <math.h>
9
12
  #include <stdlib.h>
10
13
  #include <string.h>
14
+ #include <stdio.h>
11
15
 
16
+ #define TF_VALIDATE_RULES_FILE_MAX_BYTES (1024 * 1024)
17
+
18
+ /* Validate is row-local. Rule sets are evaluated independently for each row;
19
+ * per-rule counts are O(number_of_rules), and audit records are opt-in/capped. */
12
20
  typedef struct {
13
21
  tf_expr *expr;
22
+ char *expr_text;
23
+ char *name;
24
+ char *message;
25
+ size_t checked_rows;
26
+ size_t passed_rows;
27
+ size_t failed_rows;
28
+ size_t audit_emitted;
29
+ } validate_rule;
30
+
31
+ typedef struct {
32
+ validate_rule *rules;
33
+ size_t n_rules;
34
+ char *name;
35
+ char *message;
36
+ size_t row_index;
37
+ size_t checked_rows;
38
+ size_t passed_rows;
39
+ size_t failed_rows;
40
+ size_t audit_limit;
41
+ size_t audit_emitted;
42
+ size_t max_failures;
43
+ int has_max_failures;
44
+ double max_failure_rate;
45
+ int has_max_failure_rate;
46
+ double warn_failure_rate;
47
+ int has_warn_failure_rate;
48
+ int audit;
49
+ tf_audit_options audit_opts;
14
50
  } validate_state;
15
51
 
52
+ static double validate_failure_rate(size_t failed, size_t checked) {
53
+ return checked ? (double)failed / (double)checked : 0.0;
54
+ }
55
+
56
+ static void validate_rule_free(validate_rule *rule) {
57
+ if (!rule) return;
58
+ tf_expr_free(rule->expr);
59
+ free(rule->expr_text);
60
+ free(rule->name);
61
+ free(rule->message);
62
+ }
63
+
64
+ static int add_validate_rule(validate_state *st, const char *expr_text,
65
+ const char *name, const char *message) {
66
+ if (!st || !expr_text || !expr_text[0]) return TF_ERROR;
67
+ tf_expr *expr = tf_expr_parse(expr_text);
68
+ if (!expr) return TF_ERROR;
69
+ size_t next = 0;
70
+ if (tf_size_add(st->n_rules, 1, &next) != TF_OK) {
71
+ tf_expr_free(expr);
72
+ return TF_ERROR;
73
+ }
74
+ validate_rule *nr = tf_reallocarray_checked(st->rules, next, sizeof(validate_rule));
75
+ if (!nr) { tf_expr_free(expr); return TF_ERROR; }
76
+ st->rules = nr;
77
+ validate_rule *rule = &st->rules[st->n_rules];
78
+ st->n_rules = next;
79
+ memset(rule, 0, sizeof(*rule));
80
+ rule->expr = expr;
81
+ rule->expr_text = strdup(expr_text);
82
+ rule->name = strdup(name && name[0] ? name : "validate");
83
+ rule->message = strdup(message ? message : "");
84
+ if (!rule->expr_text || !rule->name || !rule->message) return TF_ERROR;
85
+ return TF_OK;
86
+ }
87
+
88
+ static int emit_validate_audit(validate_state *st, validate_rule *rule,
89
+ const tf_batch *b, size_t row,
90
+ tf_side_channels *side) {
91
+ if (!st->audit || st->audit_emitted >= st->audit_limit || !side || !side->stats) return TF_OK;
92
+ cJSON *obj = cJSON_CreateObject();
93
+ if (!obj) return TF_ERROR;
94
+ int rc = TF_ERROR;
95
+ if (tf_json_add_string(obj, "type", "audit") != TF_OK ||
96
+ tf_json_add_string(obj, "op", "validate") != TF_OK ||
97
+ tf_json_add_string(obj, "event", "validation_failed") != TF_OK ||
98
+ tf_json_add_string(obj, "reason", "validate_false") != TF_OK ||
99
+ tf_json_add_string(obj, "channel", "audit") != TF_OK ||
100
+ tf_json_add_string(obj, "action", "annotate") != TF_OK ||
101
+ tf_json_add_string(obj, "suite", st->name ? st->name : "validate") != TF_OK ||
102
+ tf_json_add_string(obj, "name", rule->name ? rule->name : "validate") != TF_OK ||
103
+ tf_json_add_string(obj, "expr", rule->expr_text ? rule->expr_text : "") != TF_OK ||
104
+ tf_json_add_string(obj, "result", "_valid") != TF_OK ||
105
+ tf_json_add_bool(obj, "valid", 0) != TF_OK ||
106
+ tf_json_add_number(obj, "row", (double)st->row_index) != TF_OK) {
107
+ goto done;
108
+ }
109
+ if (rule->message && rule->message[0]) {
110
+ if (tf_json_add_string(obj, "message", rule->message) != TF_OK) goto done;
111
+ } else if (st->message && st->message[0]) {
112
+ if (tf_json_add_string(obj, "message", st->message) != TF_OK) goto done;
113
+ }
114
+ cJSON *row_obj = tf_audit_row_to_json(b, row, &st->audit_opts);
115
+ if (row_obj) {
116
+ if (tf_json_add_item(obj, "data", row_obj) != TF_OK) goto done;
117
+ } else if (st->audit_opts.include_row) {
118
+ goto done;
119
+ }
120
+ rc = tf_buffer_write_json_line(side->stats, obj);
121
+ done:
122
+ cJSON_Delete(obj);
123
+ if (rc == TF_OK) {
124
+ st->audit_emitted++;
125
+ rule->audit_emitted++;
126
+ }
127
+ return rc;
128
+ }
129
+
130
+ static int emit_validate_threshold_error(validate_state *st, tf_side_channels *side) {
131
+ char msg[512];
132
+ snprintf(msg, sizeof(msg),
133
+ "validate max_failures exceeded at row %zu: failed_rows=%zu max_failures=%zu",
134
+ st->row_index, st->failed_rows, st->max_failures);
135
+ tf_set_last_error(msg);
136
+ if (!side || !side->errors) return TF_OK;
137
+ cJSON *obj = cJSON_CreateObject();
138
+ if (!obj) return TF_ERROR;
139
+ int rc = TF_ERROR;
140
+ if (tf_json_add_string(obj, "type", "validate_failure") != TF_OK ||
141
+ tf_json_add_string(obj, "op", "validate") != TF_OK ||
142
+ tf_json_add_string(obj, "event", "threshold_exceeded") != TF_OK ||
143
+ tf_json_add_string(obj, "reason", "max_failures_exceeded") != TF_OK ||
144
+ tf_json_add_string(obj, "severity", "error") != TF_OK ||
145
+ tf_json_add_string(obj, "name", st->name ? st->name : "validate") != TF_OK ||
146
+ tf_json_add_number(obj, "row", (double)st->row_index) != TF_OK ||
147
+ tf_json_add_number(obj, "checked_rows", (double)st->checked_rows) != TF_OK ||
148
+ tf_json_add_number(obj, "failed_rows", (double)st->failed_rows) != TF_OK ||
149
+ tf_json_add_number(obj, "failure_rate", validate_failure_rate(st->failed_rows, st->checked_rows)) != TF_OK ||
150
+ tf_json_add_number(obj, "max_failures", (double)st->max_failures) != TF_OK) {
151
+ goto done;
152
+ }
153
+ if (st->message && st->message[0] &&
154
+ tf_json_add_string(obj, "message", st->message) != TF_OK) {
155
+ goto done;
156
+ }
157
+ rc = tf_buffer_write_json_line(side->errors, obj);
158
+ done:
159
+ cJSON_Delete(obj);
160
+ return rc;
161
+ }
162
+
163
+ static int emit_validate_rate_event(validate_state *st, tf_side_channels *side,
164
+ const char *event, const char *reason,
165
+ const char *severity, const char *threshold_name,
166
+ double threshold, double failure_rate) {
167
+ if (!side || !side->errors) return TF_OK;
168
+ cJSON *obj = cJSON_CreateObject();
169
+ if (!obj) return TF_ERROR;
170
+ int rc = TF_ERROR;
171
+ if (tf_json_add_string(obj, "type", "validate_failure") != TF_OK ||
172
+ tf_json_add_string(obj, "op", "validate") != TF_OK ||
173
+ tf_json_add_string(obj, "event", event) != TF_OK ||
174
+ tf_json_add_string(obj, "reason", reason) != TF_OK ||
175
+ tf_json_add_string(obj, "severity", severity) != TF_OK ||
176
+ tf_json_add_string(obj, "name", st->name ? st->name : "validate") != TF_OK ||
177
+ tf_json_add_number(obj, "checked_rows", (double)st->checked_rows) != TF_OK ||
178
+ tf_json_add_number(obj, "failed_rows", (double)st->failed_rows) != TF_OK ||
179
+ tf_json_add_number(obj, "failure_rate", failure_rate) != TF_OK ||
180
+ tf_json_add_number(obj, threshold_name, threshold) != TF_OK) {
181
+ goto done;
182
+ }
183
+ if (st->message && st->message[0] &&
184
+ tf_json_add_string(obj, "message", st->message) != TF_OK) {
185
+ goto done;
186
+ }
187
+ rc = tf_buffer_write_json_line(side->errors, obj);
188
+ done:
189
+ cJSON_Delete(obj);
190
+ return rc;
191
+ }
192
+
193
+ static int emit_validate_rate_warning(validate_state *st, tf_side_channels *side, double failure_rate) {
194
+ return emit_validate_rate_event(st, side, "threshold_warning",
195
+ "warn_failure_rate_exceeded", "warning",
196
+ "warn_failure_rate", st->warn_failure_rate,
197
+ failure_rate);
198
+ }
199
+
200
+ static int emit_validate_rate_error(validate_state *st, tf_side_channels *side, double failure_rate) {
201
+ char msg[512];
202
+ snprintf(msg, sizeof(msg),
203
+ "validate max_failure_rate exceeded: failure_rate=%.17g max_failure_rate=%.17g failed_rows=%zu checked_rows=%zu",
204
+ failure_rate, st->max_failure_rate, st->failed_rows, st->checked_rows);
205
+ tf_set_last_error(msg);
206
+ return emit_validate_rate_event(st, side, "threshold_exceeded",
207
+ "max_failure_rate_exceeded", "error",
208
+ "max_failure_rate", st->max_failure_rate,
209
+ failure_rate);
210
+ }
211
+
16
212
  static int validate_process(tf_step *self, tf_batch *in, tf_batch **out,
17
213
  tf_side_channels *side) {
18
- (void)side;
19
214
  validate_state *st = self->state;
20
215
  *out = NULL;
21
216
 
22
- tf_batch *ob = tf_batch_create(in->n_cols + 1, in->n_rows);
217
+ size_t out_cols = 0;
218
+ if (tf_size_add(in->n_cols, 1, &out_cols) != TF_OK) return TF_ERROR;
219
+ tf_batch *ob = tf_batch_create(out_cols, in->n_rows > 0 ? in->n_rows : 1);
23
220
  if (!ob) return TF_ERROR;
24
- for (size_t c = 0; c < in->n_cols; c++)
25
- tf_batch_set_schema(ob, c, in->col_names[c], in->col_types[c]);
26
- tf_batch_set_schema(ob, in->n_cols, "_valid", TF_TYPE_BOOL);
221
+ const char *extra_names[] = {"_valid"};
222
+ const tf_type extra_types[] = {TF_TYPE_BOOL};
223
+ if (tf_batch_clone_with_extra_cols(ob, in, extra_names, extra_types, 1) != TF_OK) {
224
+ tf_batch_free(ob);
225
+ return TF_ERROR;
226
+ }
27
227
 
28
228
  for (size_t r = 0; r < in->n_rows; r++) {
29
- tf_batch_copy_row(ob, r, in, r);
30
- bool valid = false;
31
- tf_expr_eval(st->expr, in, r, &valid);
32
- tf_batch_set_bool(ob, r, in->n_cols, valid);
33
- ob->n_rows = r + 1;
229
+ st->row_index++;
230
+ if (tf_batch_copy_row(ob, r, in, r) != TF_OK) {
231
+ tf_batch_free(ob);
232
+ return TF_ERROR;
233
+ }
234
+ int row_valid = 1;
235
+ st->checked_rows++;
236
+ for (size_t i = 0; i < st->n_rules; i++) {
237
+ validate_rule *rule = &st->rules[i];
238
+ bool valid = false;
239
+ if (tf_expr_eval(rule->expr, in, r, &valid) != TF_OK) valid = false;
240
+ rule->checked_rows++;
241
+ if (valid) {
242
+ rule->passed_rows++;
243
+ } else {
244
+ rule->failed_rows++;
245
+ row_valid = 0;
246
+ if (emit_validate_audit(st, rule, in, r, side) != TF_OK) {
247
+ tf_batch_free(ob);
248
+ return TF_ERROR;
249
+ }
250
+ }
251
+ }
252
+ if (row_valid) st->passed_rows++;
253
+ else st->failed_rows++;
254
+ if (tf_batch_set_bool(ob, r, in->n_cols, row_valid ? true : false) != TF_OK) {
255
+ tf_batch_free(ob);
256
+ return TF_ERROR;
257
+ }
258
+ if (tf_batch_expose_row(ob, r) != TF_OK) {
259
+ tf_batch_free(ob);
260
+ return TF_ERROR;
261
+ }
262
+ if (!row_valid && st->has_max_failures && st->failed_rows > st->max_failures) {
263
+ if (emit_validate_threshold_error(st, side) != TF_OK) {
264
+ tf_batch_free(ob);
265
+ return TF_ERROR;
266
+ }
267
+ tf_batch_free(ob);
268
+ return TF_ERROR;
269
+ }
34
270
  }
35
271
 
36
272
  *out = ob;
@@ -38,33 +274,374 @@ static int validate_process(tf_step *self, tf_batch *in, tf_batch **out,
38
274
  }
39
275
 
40
276
  static int validate_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
41
- (void)self; (void)side;
277
+ if (!self || !self->state || !out) return TF_ERROR;
278
+ validate_state *st = self->state;
42
279
  *out = NULL;
280
+ double rate = validate_failure_rate(st->failed_rows, st->checked_rows);
281
+ if (st->has_warn_failure_rate && rate > st->warn_failure_rate) {
282
+ if (emit_validate_rate_warning(st, side, rate) != TF_OK) return TF_ERROR;
283
+ }
284
+ if (st->has_max_failure_rate && rate > st->max_failure_rate) {
285
+ int rc = emit_validate_rate_error(st, side, rate);
286
+ return rc == TF_OK ? TF_ERROR : rc;
287
+ }
43
288
  return TF_OK;
44
289
  }
45
290
 
46
- static void validate_destroy(tf_step *self) {
291
+ static int validate_append_rule_stats(validate_state *st, tf_buffer *out) {
292
+ cJSON *arr = cJSON_CreateArray();
293
+ if (!arr) return TF_ERROR;
294
+ for (size_t i = 0; i < st->n_rules; i++) {
295
+ validate_rule *rule = &st->rules[i];
296
+ cJSON *obj = cJSON_CreateObject();
297
+ if (!obj) { cJSON_Delete(arr); return TF_ERROR; }
298
+ if (tf_json_add_string(obj, "name", rule->name ? rule->name : "validate") != TF_OK ||
299
+ tf_json_add_string(obj, "expr", rule->expr_text ? rule->expr_text : "") != TF_OK ||
300
+ tf_json_add_number(obj, "checked_rows", (double)rule->checked_rows) != TF_OK ||
301
+ tf_json_add_number(obj, "passed_rows", (double)rule->passed_rows) != TF_OK ||
302
+ tf_json_add_number(obj, "failed_rows", (double)rule->failed_rows) != TF_OK ||
303
+ tf_json_add_number(obj, "failure_rate",
304
+ validate_failure_rate(rule->failed_rows, rule->checked_rows)) != TF_OK ||
305
+ tf_json_add_number(obj, "audit_emitted", (double)rule->audit_emitted) != TF_OK) {
306
+ cJSON_Delete(obj);
307
+ cJSON_Delete(arr);
308
+ return TF_ERROR;
309
+ }
310
+ if (tf_json_add_array_item(arr, obj) != TF_OK) {
311
+ cJSON_Delete(arr);
312
+ return TF_ERROR;
313
+ }
314
+ }
315
+ char *printed = cJSON_PrintUnformatted(arr);
316
+ cJSON_Delete(arr);
317
+ if (!printed) return TF_ERROR;
318
+ int rc = tf_buffer_write_str(out, ",\"rules\":");
319
+ if (rc == TF_OK) rc = tf_buffer_write_str(out, printed);
320
+ free(printed);
321
+ return rc;
322
+ }
323
+
324
+ static int validate_append_stats(tf_step *self, tf_buffer *out) {
325
+ if (!self || !self->state || !out) return TF_ERROR;
47
326
  validate_state *st = self->state;
48
- if (st) { tf_expr_free(st->expr); free(st); }
327
+ char buf[512];
328
+ snprintf(buf, sizeof(buf),
329
+ ",\"checked_rows\":%zu,\"passed_rows\":%zu,"
330
+ "\"failed_rows\":%zu,\"failure_rate\":%.17g,"
331
+ "\"audit_emitted\":%zu,\"rule_count\":%zu",
332
+ st->checked_rows, st->passed_rows, st->failed_rows,
333
+ validate_failure_rate(st->failed_rows, st->checked_rows),
334
+ st->audit_emitted, st->n_rules);
335
+ if (tf_buffer_write_str(out, buf) != TF_OK) return TF_ERROR;
336
+ if (st->has_max_failures) {
337
+ snprintf(buf, sizeof(buf), ",\"max_failures\":%zu", st->max_failures);
338
+ if (tf_buffer_write_str(out, buf) != TF_OK) return TF_ERROR;
339
+ }
340
+ if (st->has_max_failure_rate) {
341
+ snprintf(buf, sizeof(buf), ",\"max_failure_rate\":%.17g", st->max_failure_rate);
342
+ if (tf_buffer_write_str(out, buf) != TF_OK) return TF_ERROR;
343
+ }
344
+ if (st->has_warn_failure_rate) {
345
+ snprintf(buf, sizeof(buf), ",\"warn_failure_rate\":%.17g", st->warn_failure_rate);
346
+ if (tf_buffer_write_str(out, buf) != TF_OK) return TF_ERROR;
347
+ }
348
+ return validate_append_rule_stats(st, out);
349
+ }
350
+
351
+ static void validate_state_free(validate_state *st) {
352
+ if (!st) return;
353
+ for (size_t i = 0; i < st->n_rules; i++) validate_rule_free(&st->rules[i]);
354
+ free(st->rules);
355
+ free(st->name);
356
+ free(st->message);
357
+ tf_audit_options_free(&st->audit_opts);
358
+ free(st);
359
+ }
360
+
361
+ static void validate_destroy(tf_step *self) {
362
+ if (!self) return;
363
+ validate_state_free((validate_state *)self->state);
49
364
  free(self);
50
365
  }
51
366
 
367
+ static const cJSON *validate_object_item(const cJSON *obj, const char *key) {
368
+ return cJSON_IsObject(obj) ? cJSON_GetObjectItemCaseSensitive((cJSON *)obj, key) : NULL;
369
+ }
370
+
371
+ static const cJSON *validate_arg(const cJSON *args, const cJSON *rules_doc,
372
+ const char *snake, const char *camel) {
373
+ const cJSON *item = validate_object_item(args, snake);
374
+ if (!item && camel) item = validate_object_item(args, camel);
375
+ if (item) return item;
376
+ item = validate_object_item(rules_doc, snake);
377
+ if (!item && camel) item = validate_object_item(rules_doc, camel);
378
+ return item;
379
+ }
380
+
381
+ static int validate_doc_has_suite_metadata(const cJSON *doc) {
382
+ return validate_object_item(doc, "name") || validate_object_item(doc, "message") ||
383
+ validate_object_item(doc, "audit") || validate_object_item(doc, "audit_limit") ||
384
+ validate_object_item(doc, "auditLimit") ||
385
+ validate_object_item(doc, "max_failures") || validate_object_item(doc, "maxFailures") ||
386
+ validate_object_item(doc, "max_failure_rate") || validate_object_item(doc, "maxFailureRate") ||
387
+ validate_object_item(doc, "warn_failure_rate") || validate_object_item(doc, "warnFailureRate");
388
+ }
389
+
390
+ static cJSON *load_validate_rules_file(const cJSON *args, int *status) {
391
+ *status = 0;
392
+ const cJSON *path_json = validate_object_item(args, "rules_file");
393
+ if (!path_json) path_json = validate_object_item(args, "rulesFile");
394
+ if (!path_json) return NULL;
395
+ *status = -1;
396
+ if (!cJSON_IsString(path_json) || !path_json->valuestring || !path_json->valuestring[0]) {
397
+ tf_set_last_error("validate: rules_file must be a non-empty string");
398
+ return NULL;
399
+ }
400
+
401
+ const char *validated_path = tf_policy_validated_path_arg(args, "rules_file");
402
+ if (!validated_path) validated_path = tf_policy_validated_path_arg(args, "rulesFile");
403
+ FILE *f = tf_policy_fopen_read(path_json->valuestring, validated_path);
404
+ if (!f) {
405
+ tf_set_last_error("validate: unable to read rules_file");
406
+ return NULL;
407
+ }
408
+ if (fseek(f, 0, SEEK_END) != 0) {
409
+ fclose(f);
410
+ tf_set_last_error("validate: unable to read rules_file");
411
+ return NULL;
412
+ }
413
+ long len = ftell(f);
414
+ if (len < 0) {
415
+ fclose(f);
416
+ tf_set_last_error("validate: unable to read rules_file");
417
+ return NULL;
418
+ }
419
+ if ((size_t)len > TF_VALIDATE_RULES_FILE_MAX_BYTES) {
420
+ fclose(f);
421
+ tf_set_last_error("validate: rules_file exceeds 1048576 bytes");
422
+ return NULL;
423
+ }
424
+ if (fseek(f, 0, SEEK_SET) != 0) {
425
+ fclose(f);
426
+ tf_set_last_error("validate: unable to read rules_file");
427
+ return NULL;
428
+ }
429
+
430
+ char *buf = malloc((size_t)len + 1);
431
+ if (!buf) { fclose(f); return NULL; }
432
+ size_t n = fread(buf, 1, (size_t)len, f);
433
+ fclose(f);
434
+ if (n != (size_t)len) {
435
+ free(buf);
436
+ tf_set_last_error("validate: unable to read rules_file");
437
+ return NULL;
438
+ }
439
+ buf[n] = '\0';
440
+ cJSON *doc = cJSON_ParseWithLength(buf, n);
441
+ free(buf);
442
+ if (!doc) {
443
+ tf_set_last_error("validate: invalid rules_file JSON");
444
+ return NULL;
445
+ }
446
+ *status = 1;
447
+ return doc;
448
+ }
449
+
450
+ static const cJSON *validate_rules_from_doc(const cJSON *doc) {
451
+ if (!doc) return NULL;
452
+ if (cJSON_IsArray(doc)) return doc;
453
+ if (!cJSON_IsObject(doc)) return NULL;
454
+ const cJSON *rules = validate_object_item(doc, "rules");
455
+ if (rules) return rules;
456
+ if (validate_doc_has_suite_metadata(doc)) return NULL;
457
+ return doc;
458
+ }
459
+
460
+ static int parse_validate_rules(validate_state *st, const cJSON *rules_json) {
461
+ if (!rules_json) return TF_OK;
462
+ if (cJSON_IsArray(rules_json)) {
463
+ int n = cJSON_GetArraySize(rules_json);
464
+ for (int i = 0; i < n; i++) {
465
+ cJSON *item = cJSON_GetArrayItem(rules_json, i);
466
+ char default_name[64];
467
+ snprintf(default_name, sizeof(default_name), "rule_%d", i + 1);
468
+ if (cJSON_IsString(item)) {
469
+ if (add_validate_rule(st, item->valuestring, default_name, st->message) != TF_OK) return TF_ERROR;
470
+ } else if (cJSON_IsObject(item)) {
471
+ cJSON *expr_j = cJSON_GetObjectItemCaseSensitive(item, "expr");
472
+ cJSON *name_j = cJSON_GetObjectItemCaseSensitive(item, "name");
473
+ cJSON *message_j = cJSON_GetObjectItemCaseSensitive(item, "message");
474
+ if (!cJSON_IsString(expr_j) || !expr_j->valuestring[0]) return TF_ERROR;
475
+ const char *name = cJSON_IsString(name_j) && name_j->valuestring[0] ? name_j->valuestring : default_name;
476
+ const char *message = cJSON_IsString(message_j) ? message_j->valuestring : st->message;
477
+ if (add_validate_rule(st, expr_j->valuestring, name, message) != TF_OK) return TF_ERROR;
478
+ } else {
479
+ return TF_ERROR;
480
+ }
481
+ }
482
+ return st->n_rules > 0 ? TF_OK : TF_ERROR;
483
+ }
484
+ if (cJSON_IsObject(rules_json)) {
485
+ cJSON *entry = NULL;
486
+ cJSON_ArrayForEach(entry, rules_json) {
487
+ if (!entry->string || !entry->string[0]) return TF_ERROR;
488
+ if (cJSON_IsString(entry)) {
489
+ if (add_validate_rule(st, entry->valuestring, entry->string, st->message) != TF_OK) return TF_ERROR;
490
+ } else if (cJSON_IsObject(entry)) {
491
+ cJSON *expr_j = cJSON_GetObjectItemCaseSensitive(entry, "expr");
492
+ cJSON *name_j = cJSON_GetObjectItemCaseSensitive(entry, "name");
493
+ cJSON *message_j = cJSON_GetObjectItemCaseSensitive(entry, "message");
494
+ if (!cJSON_IsString(expr_j) || !expr_j->valuestring[0]) return TF_ERROR;
495
+ const char *name = cJSON_IsString(name_j) && name_j->valuestring[0] ? name_j->valuestring : entry->string;
496
+ const char *message = cJSON_IsString(message_j) ? message_j->valuestring : st->message;
497
+ if (add_validate_rule(st, expr_j->valuestring, name, message) != TF_OK) return TF_ERROR;
498
+ } else {
499
+ return TF_ERROR;
500
+ }
501
+ }
502
+ return st->n_rules > 0 ? TF_OK : TF_ERROR;
503
+ }
504
+ return TF_ERROR;
505
+ }
506
+
52
507
  tf_step *tf_validate_create(const cJSON *args) {
53
508
  if (!args) return NULL;
54
- cJSON *expr_json = cJSON_GetObjectItemCaseSensitive(args, "expr");
55
- if (!cJSON_IsString(expr_json)) return NULL;
56
-
57
- tf_expr *expr = tf_expr_parse(expr_json->valuestring);
58
- if (!expr) return NULL;
59
509
 
60
510
  validate_state *st = calloc(1, sizeof(validate_state));
61
- if (!st) { tf_expr_free(expr); return NULL; }
62
- st->expr = expr;
511
+ if (!st) return NULL;
512
+ st->audit_limit = 1000;
513
+ tf_audit_options_init(&st->audit_opts, 1);
514
+
515
+ int rules_file_status = 0;
516
+ cJSON *rules_doc = load_validate_rules_file(args, &rules_file_status);
517
+ if (rules_file_status < 0) { validate_state_free(st); return NULL; }
518
+
519
+ const cJSON *name_json = validate_arg(args, rules_doc, "name", NULL);
520
+ const cJSON *message_json = validate_arg(args, rules_doc, "message", NULL);
521
+ st->name = strdup(cJSON_IsString(name_json) && name_json->valuestring[0] ? name_json->valuestring : "validate");
522
+ st->message = strdup(cJSON_IsString(message_json) ? message_json->valuestring : "");
523
+ if (!st->name || !st->message) { cJSON_Delete(rules_doc); validate_state_free(st); return NULL; }
524
+
525
+ const cJSON *audit_json = validate_arg(args, rules_doc, "audit", NULL);
526
+ st->audit = cJSON_IsTrue(audit_json) ? 1 : 0;
527
+ const cJSON *audit_limit_json = validate_arg(args, rules_doc, "audit_limit", "auditLimit");
528
+ if (audit_limit_json) {
529
+ size_t parsed_limit = 0;
530
+ const cJSON *arg_limit = cJSON_GetObjectItemCaseSensitive(args, "audit_limit");
531
+ const cJSON *arg_limit_alt = cJSON_GetObjectItemCaseSensitive(args, "auditLimit");
532
+ const char *limit_name = arg_limit ? "audit_limit" :
533
+ (arg_limit_alt ? "auditLimit" : "audit_limit");
534
+ if (tf_json_size_value(audit_limit_json, limit_name,
535
+ 1, TF_MAX_AUDIT_RECORDS,
536
+ &parsed_limit, "validate") < 0) {
537
+ cJSON_Delete(rules_doc);
538
+ validate_state_free(st);
539
+ return NULL;
540
+ }
541
+ st->audit_limit = parsed_limit;
542
+ }
543
+
544
+ if (tf_audit_options_parse(&st->audit_opts, args, "validate") != TF_OK) {
545
+ cJSON_Delete(rules_doc);
546
+ validate_state_free(st);
547
+ return NULL;
548
+ }
549
+
550
+ const cJSON *max_failures_json = validate_arg(args, rules_doc, "max_failures", "maxFailures");
551
+ if (max_failures_json) {
552
+ size_t parsed_limit = 0;
553
+ const cJSON *arg_limit = cJSON_GetObjectItemCaseSensitive(args, "max_failures");
554
+ const cJSON *arg_limit_alt = cJSON_GetObjectItemCaseSensitive(args, "maxFailures");
555
+ const char *limit_name = arg_limit ? "max_failures" :
556
+ (arg_limit_alt ? "maxFailures" : "max_failures");
557
+ if (tf_json_size_value(max_failures_json, limit_name,
558
+ 0, TF_MAX_COUNT_ARG,
559
+ &parsed_limit, "validate") < 0) {
560
+ cJSON_Delete(rules_doc);
561
+ validate_state_free(st);
562
+ return NULL;
563
+ }
564
+ st->max_failures = parsed_limit;
565
+ st->has_max_failures = 1;
566
+ }
567
+
568
+ const cJSON *max_rate_json = validate_arg(args, rules_doc, "max_failure_rate", "maxFailureRate");
569
+ if (max_rate_json) {
570
+ if (!cJSON_IsNumber(max_rate_json) || !isfinite(max_rate_json->valuedouble) ||
571
+ max_rate_json->valuedouble < 0.0 || max_rate_json->valuedouble > 1.0) {
572
+ tf_set_last_error("validate: max_failure_rate must be between 0 and 1");
573
+ cJSON_Delete(rules_doc);
574
+ validate_state_free(st);
575
+ return NULL;
576
+ }
577
+ st->max_failure_rate = max_rate_json->valuedouble;
578
+ st->has_max_failure_rate = 1;
579
+ }
580
+
581
+ const cJSON *warn_rate_json = validate_arg(args, rules_doc, "warn_failure_rate", "warnFailureRate");
582
+ if (warn_rate_json) {
583
+ if (!cJSON_IsNumber(warn_rate_json) || !isfinite(warn_rate_json->valuedouble) ||
584
+ warn_rate_json->valuedouble < 0.0 || warn_rate_json->valuedouble > 1.0) {
585
+ tf_set_last_error("validate: warn_failure_rate must be between 0 and 1");
586
+ cJSON_Delete(rules_doc);
587
+ validate_state_free(st);
588
+ return NULL;
589
+ }
590
+ st->warn_failure_rate = warn_rate_json->valuedouble;
591
+ st->has_warn_failure_rate = 1;
592
+ }
593
+
594
+ const cJSON *file_rules = validate_rules_from_doc(rules_doc);
595
+ if (rules_doc && !file_rules) {
596
+ tf_set_last_error("validate: rules_file must contain a rules array/object or rule map");
597
+ cJSON_Delete(rules_doc);
598
+ validate_state_free(st);
599
+ return NULL;
600
+ }
601
+ if (file_rules && parse_validate_rules(st, file_rules) != TF_OK) {
602
+ tf_set_last_error("validate: invalid rules_file rules");
603
+ cJSON_Delete(rules_doc);
604
+ validate_state_free(st);
605
+ return NULL;
606
+ }
607
+
608
+ const cJSON *rules_json = cJSON_GetObjectItemCaseSensitive(args, "rules");
609
+ if (rules_json && parse_validate_rules(st, rules_json) != TF_OK) {
610
+ tf_set_last_error("validate: invalid rules");
611
+ cJSON_Delete(rules_doc);
612
+ validate_state_free(st);
613
+ return NULL;
614
+ }
615
+
616
+ const cJSON *expr_json = cJSON_GetObjectItemCaseSensitive(args, "expr");
617
+ if (expr_json) {
618
+ if (!cJSON_IsString(expr_json) || !expr_json->valuestring[0]) {
619
+ tf_set_last_error("validate: expr must be a non-empty string");
620
+ cJSON_Delete(rules_doc);
621
+ validate_state_free(st);
622
+ return NULL;
623
+ }
624
+ if (add_validate_rule(st, expr_json->valuestring, st->name, st->message) != TF_OK) {
625
+ tf_set_last_error("validate: invalid expr");
626
+ cJSON_Delete(rules_doc);
627
+ validate_state_free(st);
628
+ return NULL;
629
+ }
630
+ }
631
+
632
+ if (st->n_rules == 0) {
633
+ tf_set_last_error("validate: expr, rules, or rules_file is required");
634
+ cJSON_Delete(rules_doc);
635
+ validate_state_free(st);
636
+ return NULL;
637
+ }
638
+ cJSON_Delete(rules_doc);
63
639
 
64
- tf_step *step = malloc(sizeof(tf_step));
65
- if (!step) { tf_expr_free(expr); free(st); return NULL; }
640
+ tf_step *step = calloc(1, sizeof(tf_step));
641
+ if (!step) { validate_state_free(st); return NULL; }
66
642
  step->process = validate_process;
67
643
  step->flush = validate_flush;
644
+ step->append_stats = validate_append_stats;
68
645
  step->destroy = validate_destroy;
69
646
  step->state = st;
70
647
  return step;