tranfi 0.0.2 → 0.2.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.
Files changed (151) hide show
  1. package/LICENSE +177 -21
  2. package/NOTICE +8 -0
  3. package/README.md +627 -0
  4. package/app/assets/index-6quYZ5Ap.css +5 -0
  5. package/app/assets/index-BIAIKnrp.js +160 -0
  6. package/app/assets/materialdesignicons-webfont-B7mPwVP_.ttf +0 -0
  7. package/app/assets/materialdesignicons-webfont-CSr8KVlo.eot +0 -0
  8. package/app/assets/materialdesignicons-webfont-Dp5v-WZN.woff2 +0 -0
  9. package/app/assets/materialdesignicons-webfont-PXm3-2wK.woff +0 -0
  10. package/app/index.html +13 -0
  11. package/binding.gyp +121 -0
  12. package/csrc/arena.c +93 -0
  13. package/csrc/batch.c +976 -0
  14. package/csrc/buffer.c +154 -0
  15. package/csrc/cJSON.c +3386 -0
  16. package/csrc/cJSON.h +316 -0
  17. package/csrc/codec_csv.c +1951 -0
  18. package/csrc/codec_jsonl.c +1086 -0
  19. package/csrc/codec_table.c +248 -0
  20. package/csrc/codec_text.c +447 -0
  21. package/csrc/compiler.c +130 -0
  22. package/csrc/config.h +21 -0
  23. package/csrc/date_utils.h +94 -0
  24. package/csrc/dsl.c +5417 -0
  25. package/csrc/dsl.h +22 -0
  26. package/csrc/expr.c +1553 -0
  27. package/csrc/expr.h +58 -0
  28. package/csrc/internal.h +539 -0
  29. package/csrc/ir.c +166 -0
  30. package/csrc/ir.h +208 -0
  31. package/csrc/ir_schema.c +75 -0
  32. package/csrc/ir_serialize.c +166 -0
  33. package/csrc/ir_sql.c +1822 -0
  34. package/csrc/ir_validate.c +576 -0
  35. package/csrc/json_path.c +210 -0
  36. package/csrc/main.c +1241 -0
  37. package/csrc/memory_estimate.c +477 -0
  38. package/csrc/op_acf.c +283 -0
  39. package/csrc/op_across.c +477 -0
  40. package/csrc/op_anomaly.c +255 -0
  41. package/csrc/op_assert.c +761 -0
  42. package/csrc/op_bin.c +248 -0
  43. package/csrc/op_cast.c +523 -0
  44. package/csrc/op_clip.c +99 -0
  45. package/csrc/op_date_trunc.c +355 -0
  46. package/csrc/op_datetime.c +394 -0
  47. package/csrc/op_derive.c +216 -0
  48. package/csrc/op_diff.c +250 -0
  49. package/csrc/op_ewma.c +222 -0
  50. package/csrc/op_explode.c +206 -0
  51. package/csrc/op_fill_down.c +235 -0
  52. package/csrc/op_fill_null.c +268 -0
  53. package/csrc/op_filter.c +181 -0
  54. package/csrc/op_frequency.c +721 -0
  55. package/csrc/op_grep.c +181 -0
  56. package/csrc/op_group_agg.c +1956 -0
  57. package/csrc/op_hash.c +159 -0
  58. package/csrc/op_head.c +84 -0
  59. package/csrc/op_interpolate.c +445 -0
  60. package/csrc/op_join.c +2902 -0
  61. package/csrc/op_json_extract.c +227 -0
  62. package/csrc/op_json_filter.c +384 -0
  63. package/csrc/op_json_flatten.c +293 -0
  64. package/csrc/op_json_schema.c +503 -0
  65. package/csrc/op_label_encode.c +419 -0
  66. package/csrc/op_lag.c +181 -0
  67. package/csrc/op_lead.c +242 -0
  68. package/csrc/op_normalize.c +510 -0
  69. package/csrc/op_onehot.c +457 -0
  70. package/csrc/op_pivot.c +1754 -0
  71. package/csrc/op_quarantine.c +189 -0
  72. package/csrc/op_registry.c +3044 -0
  73. package/csrc/op_rename.c +129 -0
  74. package/csrc/op_replace.c +354 -0
  75. package/csrc/op_rleid.c +297 -0
  76. package/csrc/op_rowid.c +559 -0
  77. package/csrc/op_sample.c +158 -0
  78. package/csrc/op_schema.c +1341 -0
  79. package/csrc/op_schema_infer.c +252 -0
  80. package/csrc/op_select.c +340 -0
  81. package/csrc/op_set.c +3449 -0
  82. package/csrc/op_skip.c +95 -0
  83. package/csrc/op_sort.c +819 -0
  84. package/csrc/op_source_name.c +120 -0
  85. package/csrc/op_split.c +151 -0
  86. package/csrc/op_split_data.c +119 -0
  87. package/csrc/op_stack.c +271 -0
  88. package/csrc/op_stats.c +875 -0
  89. package/csrc/op_step.c +333 -0
  90. package/csrc/op_tail.c +105 -0
  91. package/csrc/op_tee.c +338 -0
  92. package/csrc/op_top.c +357 -0
  93. package/csrc/op_trim.c +138 -0
  94. package/csrc/op_unique.c +1343 -0
  95. package/csrc/op_unpivot.c +193 -0
  96. package/csrc/op_validate.c +648 -0
  97. package/csrc/op_window.c +591 -0
  98. package/csrc/path_policy.c +85 -0
  99. package/csrc/pipeline.c +1088 -0
  100. package/csrc/recipes.c +104 -0
  101. package/csrc/recipes.h +27 -0
  102. package/csrc/report.c +506 -0
  103. package/csrc/report.h +22 -0
  104. package/csrc/selector.c +1097 -0
  105. package/csrc/size_utils.c +348 -0
  106. package/csrc/spill.c +317 -0
  107. package/csrc/spill.h +21 -0
  108. package/csrc/tranfi.h +291 -0
  109. package/csrc/transform.h +209 -0
  110. package/csrc/transform_api.c +2237 -0
  111. package/csrc/transform_categorical.c +923 -0
  112. package/csrc/transform_internal.h +472 -0
  113. package/csrc/transform_json.c +3812 -0
  114. package/csrc/transform_numeric.c +1966 -0
  115. package/csrc/transform_sha256.c +154 -0
  116. package/csrc/transform_wasm.h +162 -0
  117. package/csrc/transform_wasm_api.c +1373 -0
  118. package/csrc/wasm_api.c +218 -0
  119. package/napi_api.c +534 -0
  120. package/napi_transform.c +1648 -0
  121. package/napi_transform.h +8 -0
  122. package/package.json +64 -59
  123. package/scripts/install-native.js +76 -0
  124. package/scripts/prepack.js +64 -0
  125. package/scripts/sync-csrc.js +23 -0
  126. package/src/cli.js +190 -0
  127. package/src/engines/duckdb.js +142 -0
  128. package/src/index.js +925 -0
  129. package/src/memory_policy.js +411 -0
  130. package/src/native.js +18 -0
  131. package/src/pipeline.js +709 -0
  132. package/src/recipe_json.js +80 -0
  133. package/src/server.js +279 -0
  134. package/src/transform.js +403 -0
  135. package/src/transform_error.js +10 -0
  136. package/src/wasm.js +21 -0
  137. package/wasm/index.js +732 -0
  138. package/wasm/package.json +1 -0
  139. package/wasm/tranfi_core.js +0 -0
  140. package/wasm/transform.js +1156 -0
  141. package/wasm/worker.js +786 -0
  142. package/dist/bundle.js +0 -1
  143. package/index.html +0 -18
  144. package/src/app.css +0 -169
  145. package/src/app.js +0 -203
  146. package/src/app.vue +0 -250
  147. package/src/bulma-input.vue +0 -110
  148. package/src/common-inputs.js +0 -28
  149. package/src/main.js +0 -20
  150. package/src/transforms.js +0 -166
  151. package/webpack.config.js +0 -108
@@ -0,0 +1,648 @@
1
+ /*
2
+ * op_validate.c - Row-local validation rule sets.
3
+ *
4
+ * Config:
5
+ * {"expr": "col('age') > 0", "audit": true, "audit_limit": 1000}
6
+ * {"rules": [{"name":"age_ok", "expr":"col('age') > 0"}], "audit": true}
7
+ */
8
+
9
+ #include "internal.h"
10
+ #include "cJSON.h"
11
+ #include <math.h>
12
+ #include <stdlib.h>
13
+ #include <string.h>
14
+ #include <stdio.h>
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. */
20
+ typedef struct {
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;
50
+ } validate_state;
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
+
212
+ static int validate_process(tf_step *self, tf_batch *in, tf_batch **out,
213
+ tf_side_channels *side) {
214
+ validate_state *st = self->state;
215
+ *out = NULL;
216
+
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);
220
+ if (!ob) return TF_ERROR;
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
+ }
227
+
228
+ for (size_t r = 0; r < in->n_rows; r++) {
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
+ }
270
+ }
271
+
272
+ *out = ob;
273
+ return TF_OK;
274
+ }
275
+
276
+ static int validate_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
277
+ if (!self || !self->state || !out) return TF_ERROR;
278
+ validate_state *st = self->state;
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
+ }
288
+ return TF_OK;
289
+ }
290
+
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;
326
+ validate_state *st = self->state;
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);
364
+ free(self);
365
+ }
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
+
507
+ tf_step *tf_validate_create(const cJSON *args) {
508
+ if (!args) return NULL;
509
+
510
+ validate_state *st = calloc(1, sizeof(validate_state));
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);
639
+
640
+ tf_step *step = calloc(1, sizeof(tf_step));
641
+ if (!step) { validate_state_free(st); return NULL; }
642
+ step->process = validate_process;
643
+ step->flush = validate_flush;
644
+ step->append_stats = validate_append_stats;
645
+ step->destroy = validate_destroy;
646
+ step->state = st;
647
+ return step;
648
+ }