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,576 @@
1
+ /*
2
+ * ir_validate.c — Validation pass over an IR plan.
3
+ *
4
+ * Checks:
5
+ * 1. At least one node
6
+ * 2. First node must be a decoder
7
+ * 3. Last node must be an encoder
8
+ * 4. All op names exist in the registry
9
+ * 5. Required args are present
10
+ * 6. No multiple decoders or encoders
11
+ */
12
+
13
+ #include "internal.h"
14
+ #include "cJSON.h"
15
+ #include <stdlib.h>
16
+ #include <string.h>
17
+ #include <stdio.h>
18
+
19
+ static void set_plan_error(tf_ir_plan *plan, const char *msg) {
20
+ free(plan->error);
21
+ plan->error = strdup(msg);
22
+ }
23
+
24
+ static void set_plan_errorf(tf_ir_plan *plan, const char *fmt, const char *detail) {
25
+ free(plan->error);
26
+ char buf[256];
27
+ snprintf(buf, sizeof(buf), fmt, detail);
28
+ plan->error = strdup(buf);
29
+ }
30
+
31
+ static int node_bool_arg_true(const tf_ir_node *node, const char *name) {
32
+ cJSON *item = node && node->args ? cJSON_GetObjectItemCaseSensitive(node->args, name) : NULL;
33
+ return cJSON_IsTrue(item);
34
+ }
35
+
36
+ static int node_array_arg_nonempty(const tf_ir_node *node, const char *name) {
37
+ cJSON *item = node && node->args ? cJSON_GetObjectItemCaseSensitive(node->args, name) : NULL;
38
+ return cJSON_IsArray(item) && cJSON_GetArraySize(item) > 0;
39
+ }
40
+
41
+
42
+ #define TF_POLICY_PATH_MAX 4096
43
+
44
+ static int path_is_absolute(const char *path) {
45
+ if (!path || !path[0]) return 0;
46
+ if (path[0] == '/' || path[0] == '\\') return 1;
47
+ return ((path[0] >= 'A' && path[0] <= 'Z') || (path[0] >= 'a' && path[0] <= 'z')) &&
48
+ path[1] == ':';
49
+ }
50
+
51
+ static int append_char(char *out, size_t out_sz, size_t *len, char ch) {
52
+ if (*len + 1 >= out_sz) return TF_ERROR;
53
+ out[(*len)++] = ch;
54
+ out[*len] = '\0';
55
+ return TF_OK;
56
+ }
57
+
58
+ static int append_segment(char *out, size_t out_sz, size_t *len,
59
+ const char *seg, size_t seg_len, size_t prefix_len) {
60
+ if (seg_len == 0) return TF_OK;
61
+ if (*len > 0 && out[*len - 1] != '/' &&
62
+ !(*len == prefix_len && prefix_len > 0 && out[prefix_len - 1] == '/')) {
63
+ if (append_char(out, out_sz, len, '/') != TF_OK) return TF_ERROR;
64
+ }
65
+ if (*len + seg_len >= out_sz) return TF_ERROR;
66
+ memcpy(out + *len, seg, seg_len);
67
+ *len += seg_len;
68
+ out[*len] = '\0';
69
+ return TF_OK;
70
+ }
71
+
72
+ static int pop_segment(char *out, size_t *len, size_t prefix_len) {
73
+ if (*len <= prefix_len) return TF_ERROR;
74
+ while (*len > prefix_len && out[*len - 1] != '/') (*len)--;
75
+ if (*len > prefix_len && out[*len - 1] == '/') (*len)--;
76
+ if (*len < prefix_len) *len = prefix_len;
77
+ out[*len] = '\0';
78
+ return TF_OK;
79
+ }
80
+
81
+ static int normalize_path_lexical(const char *in, char *out, size_t out_sz) {
82
+ if (!in || !in[0] || !out || out_sz == 0) return TF_ERROR;
83
+ size_t len = 0;
84
+ size_t prefix_len = 0;
85
+ const char *p = in;
86
+
87
+ if (((p[0] >= 'A' && p[0] <= 'Z') || (p[0] >= 'a' && p[0] <= 'z')) && p[1] == ':') {
88
+ if (out_sz < 3) return TF_ERROR;
89
+ out[len++] = p[0];
90
+ out[len++] = ':';
91
+ out[len] = '\0';
92
+ p += 2;
93
+ if (*p == '/' || *p == '\\') {
94
+ if (append_char(out, out_sz, &len, '/') != TF_OK) return TF_ERROR;
95
+ while (*p == '/' || *p == '\\') p++;
96
+ }
97
+ prefix_len = len;
98
+ } else if (*p == '/' || *p == '\\') {
99
+ if (append_char(out, out_sz, &len, '/') != TF_OK) return TF_ERROR;
100
+ while (*p == '/' || *p == '\\') p++;
101
+ prefix_len = len;
102
+ }
103
+
104
+ while (*p) {
105
+ while (*p == '/' || *p == '\\') p++;
106
+ const char *seg = p;
107
+ while (*p && *p != '/' && *p != '\\') p++;
108
+ size_t seg_len = (size_t)(p - seg);
109
+ if (seg_len == 0 || (seg_len == 1 && seg[0] == '.')) continue;
110
+ if (seg_len == 2 && seg[0] == '.' && seg[1] == '.') {
111
+ if (pop_segment(out, &len, prefix_len) != TF_OK) return TF_ERROR;
112
+ continue;
113
+ }
114
+ if (append_segment(out, out_sz, &len, seg, seg_len, prefix_len) != TF_OK) return TF_ERROR;
115
+ }
116
+
117
+ if (len == 0) {
118
+ if (out_sz < 2) return TF_ERROR;
119
+ out[0] = '.';
120
+ out[1] = '\0';
121
+ }
122
+ return TF_OK;
123
+ }
124
+
125
+ static int path_is_under_root(const char *root, const char *path) {
126
+ size_t n = strlen(root);
127
+ if (strcmp(root, "/") == 0) return path[0] == '/';
128
+ return strcmp(root, path) == 0 || (strncmp(root, path, n) == 0 && path[n] == '/');
129
+ }
130
+
131
+ static int copy_path_checked(const char *src, char *out, size_t out_sz) {
132
+ if (!src || !out || out_sz == 0) return TF_ERROR;
133
+ size_t n = strlen(src);
134
+ if (n + 1 > out_sz) return TF_ERROR;
135
+ memcpy(out, src, n + 1);
136
+ return TF_OK;
137
+ }
138
+
139
+ static int canonicalize_existing_path(const char *path, char *out, size_t out_sz) {
140
+ #ifdef _WIN32
141
+ return normalize_path_lexical(path, out, out_sz);
142
+ #else
143
+ char *resolved = realpath(path, NULL);
144
+ if (!resolved) return TF_ERROR;
145
+ int rc = copy_path_checked(resolved, out, out_sz);
146
+ free(resolved);
147
+ return rc;
148
+ #endif
149
+ }
150
+
151
+ static int resolve_workspace_path(const char *workspace_root, const char *logical,
152
+ char *out, size_t out_sz) {
153
+ char root[TF_POLICY_PATH_MAX];
154
+ char joined[TF_POLICY_PATH_MAX];
155
+ char candidate[TF_POLICY_PATH_MAX];
156
+ char resolved[TF_POLICY_PATH_MAX];
157
+ if (canonicalize_existing_path(workspace_root, root, sizeof(root)) != TF_OK) return TF_ERROR;
158
+ if (path_is_absolute(logical)) {
159
+ if (normalize_path_lexical(logical, candidate, sizeof(candidate)) != TF_OK) return TF_ERROR;
160
+ } else {
161
+ int n = snprintf(joined, sizeof(joined), "%s/%s", root, logical);
162
+ if (n < 0 || (size_t)n >= sizeof(joined)) return TF_ERROR;
163
+ if (normalize_path_lexical(joined, candidate, sizeof(candidate)) != TF_OK) return TF_ERROR;
164
+ }
165
+ if (canonicalize_existing_path(candidate, resolved, sizeof(resolved)) != TF_OK) return TF_ERROR;
166
+ if (!path_is_under_root(root, resolved)) return TF_ERROR;
167
+ if (copy_path_checked(resolved, out, out_sz) != TF_OK) return TF_ERROR;
168
+ return TF_OK;
169
+ }
170
+
171
+ static void set_policy_error(tf_ir_plan *plan, const tf_ir_node *node,
172
+ const char *arg, const char *reason) {
173
+ free(plan->error);
174
+ char buf[384];
175
+ snprintf(buf, sizeof(buf), "capability denied: op '%s' arg '%s' %s",
176
+ node && node->op ? node->op : "unknown", arg ? arg : "?", reason ? reason : "rejected");
177
+ plan->error = strdup(buf);
178
+ }
179
+
180
+ static int policy_rewrite_path_arg(tf_ir_plan *plan, tf_ir_node *node,
181
+ const tf_host_policy *policy,
182
+ const char *arg, int is_spill, int is_rules_file) {
183
+ cJSON *item = node && node->args ? cJSON_GetObjectItemCaseSensitive(node->args, arg) : NULL;
184
+ if (!cJSON_IsString(item) || !item->valuestring || item->valuestring[0] == '\0') return TF_OK;
185
+ if (!policy) return TF_OK;
186
+
187
+ if (!policy->allow_fs) {
188
+ set_policy_error(plan, node, arg, "requires filesystem access (allow_fs=false)");
189
+ return TF_ERROR;
190
+ }
191
+ if (is_spill && !policy->allow_spill) {
192
+ set_policy_error(plan, node, arg, "requires spill access (allow_spill=false)");
193
+ return TF_ERROR;
194
+ }
195
+ if (is_rules_file && !policy->allow_rules_file) {
196
+ set_policy_error(plan, node, arg, "requires rules_file access (allow_rules_file=false)");
197
+ return TF_ERROR;
198
+ }
199
+
200
+ if (policy->resolve_path || (policy->workspace_root && policy->workspace_root[0])) {
201
+ char resolved[TF_POLICY_PATH_MAX];
202
+ int rc = policy->resolve_path
203
+ ? policy->resolve_path(item->valuestring, resolved, sizeof(resolved), policy->user)
204
+ : resolve_workspace_path(policy->workspace_root, item->valuestring, resolved, sizeof(resolved));
205
+ if (rc != TF_OK || resolved[0] == '\0') {
206
+ set_policy_error(plan, node, arg, "was rejected by host path resolver");
207
+ return TF_ERROR;
208
+ }
209
+ if (!cJSON_SetValuestring(item, resolved)) {
210
+ set_plan_error(plan, "out of memory while resolving host path");
211
+ return TF_ERROR;
212
+ }
213
+ if (!policy->resolve_path && !is_spill) {
214
+ const char *hidden = NULL;
215
+ if (strcmp(arg, "file") == 0) {
216
+ hidden = TF_POLICY_VALIDATED_FILE_PATH_ARG;
217
+ } else if (strcmp(arg, "rules_file") == 0 || strcmp(arg, "rulesFile") == 0) {
218
+ hidden = TF_POLICY_VALIDATED_RULES_FILE_PATH_ARG;
219
+ }
220
+ if (hidden) {
221
+ cJSON_DeleteItemFromObjectCaseSensitive(node->args, hidden);
222
+ if (tf_json_add_string(node->args, hidden, resolved) != TF_OK) {
223
+ set_plan_error(plan, "out of memory while recording host path");
224
+ return TF_ERROR;
225
+ }
226
+ }
227
+ }
228
+ }
229
+ return TF_OK;
230
+ }
231
+
232
+ static int enforce_host_policy(tf_ir_plan *plan, tf_ir_node *node, const tf_host_policy *policy) {
233
+ if (!policy) return TF_OK;
234
+ if (!policy->allow_net && (node->caps & TF_CAP_NET)) {
235
+ set_policy_error(plan, node, "network", "requires network access (allow_net=false)");
236
+ return TF_ERROR;
237
+ }
238
+ if (!policy->allow_blocking && node->memory_class == TF_MEM_BLOCKING) {
239
+ set_policy_error(plan, node, "memory", "is blocking (allow_blocking=false)");
240
+ return TF_ERROR;
241
+ }
242
+ if (policy_rewrite_path_arg(plan, node, policy, "file", 0, 0) != TF_OK) return TF_ERROR;
243
+ if (policy_rewrite_path_arg(plan, node, policy, "spill_dir", 1, 0) != TF_OK) return TF_ERROR;
244
+ if (policy_rewrite_path_arg(plan, node, policy, "rules_file", 0, 1) != TF_OK) return TF_ERROR;
245
+ if (policy_rewrite_path_arg(plan, node, policy, "rulesFile", 0, 1) != TF_OK) return TF_ERROR;
246
+ return TF_OK;
247
+ }
248
+
249
+ static int node_string_arg_nonempty(const tf_ir_node *node, const char *name) {
250
+ cJSON *item = node && node->args ? cJSON_GetObjectItemCaseSensitive(node->args, name) : NULL;
251
+ return cJSON_IsString(item) && item->valuestring && item->valuestring[0] != '\0';
252
+ }
253
+
254
+ static int node_string_arg_equals(const tf_ir_node *node, const char *name, const char *value) {
255
+ cJSON *item = node && node->args ? cJSON_GetObjectItemCaseSensitive(node->args, name) : NULL;
256
+ return cJSON_IsString(item) && item->valuestring && value &&
257
+ strcmp(item->valuestring, value) == 0;
258
+ }
259
+
260
+ static int node_op_is_unique(const tf_ir_node *node) {
261
+ return node && node->op &&
262
+ (strcmp(node->op, "unique") == 0 || strcmp(node->op, "dedup") == 0);
263
+ }
264
+
265
+ static int node_op_is_group_agg(const tf_ir_node *node) {
266
+ return node && node->op && strcmp(node->op, "group-agg") == 0;
267
+ }
268
+
269
+ static int node_array_arg_missing_or_empty(const tf_ir_node *node, const char *name) {
270
+ cJSON *item = node && node->args ? cJSON_GetObjectItemCaseSensitive(node->args, name) : NULL;
271
+ if (!item) return 1;
272
+ if (!cJSON_IsArray(item)) return 0;
273
+ return cJSON_GetArraySize(item) <= 0;
274
+ }
275
+
276
+ static int node_number_arg_positive(const tf_ir_node *node, const char *name) {
277
+ cJSON *item = node && node->args ? cJSON_GetObjectItemCaseSensitive(node->args, name) : NULL;
278
+ return cJSON_IsNumber(item) && item->valuedouble > 0.0;
279
+ }
280
+
281
+ static int node_op_is_row_set_spillable(const tf_ir_node *node) {
282
+ return node && node->op &&
283
+ (strcmp(node->op, "intersect") == 0 || strcmp(node->op, "setdiff") == 0 ||
284
+ strcmp(node->op, "intersect-all") == 0 || strcmp(node->op, "setdiff-all") == 0 ||
285
+ strcmp(node->op, "union") == 0);
286
+ }
287
+
288
+ static int node_op_is_sorted_set_bounded(const tf_ir_node *node) {
289
+ return node && node->op && node_bool_arg_true(node, "sorted") &&
290
+ (strcmp(node->op, "intersect") == 0 || strcmp(node->op, "setdiff") == 0 ||
291
+ strcmp(node->op, "intersect-all") == 0 || strcmp(node->op, "setdiff-all") == 0 ||
292
+ strcmp(node->op, "union") == 0);
293
+ }
294
+
295
+ static int node_op_is_filtering_join(const tf_ir_node *node) {
296
+ if (!node || !node->op) return 0;
297
+ if (strcmp(node->op, "semi-join") == 0 || strcmp(node->op, "anti-join") == 0) return 1;
298
+ if (strcmp(node->op, "join") != 0 || !node->args) return 0;
299
+ cJSON *how = cJSON_GetObjectItemCaseSensitive(node->args, "how");
300
+ return cJSON_IsString(how) && how->valuestring &&
301
+ (strcmp(how->valuestring, "semi") == 0 || strcmp(how->valuestring, "anti") == 0);
302
+ }
303
+
304
+ static int node_op_is_join_spillable(const tf_ir_node *node) {
305
+ if (!node || !node->op) return 0;
306
+ if (strcmp(node->op, "semi-join") == 0 || strcmp(node->op, "anti-join") == 0) return 1;
307
+ if (strcmp(node->op, "join") != 0) return 0;
308
+ if (!node->args) return 1;
309
+ cJSON *how = cJSON_GetObjectItemCaseSensitive(node->args, "how");
310
+ if (!cJSON_IsString(how) || !how->valuestring) return 1;
311
+ return strcmp(how->valuestring, "inner") == 0 || strcmp(how->valuestring, "left") == 0 ||
312
+ strcmp(how->valuestring, "semi") == 0 || strcmp(how->valuestring, "anti") == 0;
313
+ }
314
+
315
+ static void apply_dynamic_contract(tf_ir_node *node) {
316
+ if (!node || !node->op) return;
317
+ if (strcmp(node->op, "validate") == 0 &&
318
+ (node_string_arg_nonempty(node, "rules_file") || node_string_arg_nonempty(node, "rulesFile"))) {
319
+ node->caps |= TF_CAP_FS;
320
+ node->caps &= ~TF_CAP_BROWSER_SAFE;
321
+ }
322
+ if (strcmp(node->op, "pivot") == 0) {
323
+ if (node_string_arg_nonempty(node, "spill_dir")) {
324
+ node->caps |= TF_CAP_STREAMING | TF_CAP_BOUNDED_MEMORY | TF_CAP_FS | TF_CAP_DETERMINISTIC;
325
+ node->caps &= ~TF_CAP_BROWSER_SAFE;
326
+ node->memory_class = TF_MEM_EXTERNAL;
327
+ node->emit_class = TF_EMIT_ON_FLUSH;
328
+ node->schema_class = node_array_arg_nonempty(node, "categories") ?
329
+ TF_SCHEMA_PARAMETRIC : TF_SCHEMA_DATA_DEPENDENT;
330
+ node->state_estimate = "O(spill_run_rows * columns + categories) RAM + O(input_rows + output_rows) spill";
331
+ } else if (node_bool_arg_true(node, "sorted") && node_array_arg_nonempty(node, "categories")) {
332
+ node->caps |= TF_CAP_STREAMING | TF_CAP_BOUNDED_MEMORY | TF_CAP_BROWSER_SAFE | TF_CAP_DETERMINISTIC;
333
+ node->memory_class = TF_MEM_BOUNDED_STATE;
334
+ node->emit_class = TF_EMIT_MIXED;
335
+ node->schema_class = TF_SCHEMA_PARAMETRIC;
336
+ node->state_estimate = "O(current_group + categories)";
337
+ } else if (node_array_arg_nonempty(node, "categories")) {
338
+ node->schema_class = TF_SCHEMA_PARAMETRIC;
339
+ }
340
+ }
341
+ if (strcmp(node->op, "rowid") == 0 &&
342
+ (node_array_arg_missing_or_empty(node, "columns") || node_bool_arg_true(node, "sorted"))) {
343
+ node->caps |= TF_CAP_STREAMING | TF_CAP_BOUNDED_MEMORY | TF_CAP_BROWSER_SAFE | TF_CAP_DETERMINISTIC;
344
+ node->memory_class = TF_MEM_BOUNDED_STATE;
345
+ node->emit_class = TF_EMIT_PER_BATCH;
346
+ node->schema_class = TF_SCHEMA_PARAMETRIC;
347
+ node->state_estimate = node_array_arg_missing_or_empty(node, "columns") ?
348
+ "O(1)" : "O(previous_key + counter)";
349
+ }
350
+ if (node_op_is_unique(node)) {
351
+ if (node_string_arg_nonempty(node, "spill_dir")) {
352
+ node->caps |= TF_CAP_STREAMING | TF_CAP_BOUNDED_MEMORY | TF_CAP_FS | TF_CAP_DETERMINISTIC;
353
+ node->caps &= ~TF_CAP_BROWSER_SAFE;
354
+ node->memory_class = TF_MEM_EXTERNAL;
355
+ node->emit_class = TF_EMIT_ON_FLUSH;
356
+ node->schema_class = TF_SCHEMA_STABLE;
357
+ node->state_estimate = "O(spill_run_rows * columns) RAM + O(input_rows) spill";
358
+ } else if (node_string_arg_equals(node, "mode", "approx") || node_bool_arg_true(node, "approx")) {
359
+ node->caps |= TF_CAP_STREAMING | TF_CAP_BOUNDED_MEMORY | TF_CAP_BROWSER_SAFE | TF_CAP_DETERMINISTIC;
360
+ node->memory_class = TF_MEM_BOUNDED_STATE;
361
+ node->emit_class = TF_EMIT_PER_BATCH;
362
+ node->schema_class = TF_SCHEMA_STABLE;
363
+ node->state_estimate = "O(bloom_bytes)";
364
+ } else if (node_bool_arg_true(node, "sorted")) {
365
+ node->caps |= TF_CAP_STREAMING | TF_CAP_BOUNDED_MEMORY | TF_CAP_BROWSER_SAFE | TF_CAP_DETERMINISTIC;
366
+ node->memory_class = TF_MEM_BOUNDED_STATE;
367
+ node->emit_class = TF_EMIT_PER_BATCH;
368
+ node->schema_class = TF_SCHEMA_STABLE;
369
+ node->state_estimate = "O(previous_key)";
370
+ }
371
+ }
372
+ if (node_op_is_group_agg(node)) {
373
+ if (node_string_arg_nonempty(node, "spill_dir")) {
374
+ node->caps |= TF_CAP_STREAMING | TF_CAP_BOUNDED_MEMORY | TF_CAP_FS | TF_CAP_DETERMINISTIC;
375
+ node->caps &= ~TF_CAP_BROWSER_SAFE;
376
+ node->memory_class = TF_MEM_EXTERNAL;
377
+ node->emit_class = TF_EMIT_ON_FLUSH;
378
+ node->schema_class = TF_SCHEMA_PARAMETRIC;
379
+ node->state_estimate = "O(spill_run_rows * columns) RAM + O(input_rows) spill";
380
+ } else if (node_bool_arg_true(node, "sorted")) {
381
+ node->caps |= TF_CAP_STREAMING | TF_CAP_BOUNDED_MEMORY | TF_CAP_BROWSER_SAFE | TF_CAP_DETERMINISTIC;
382
+ node->memory_class = TF_MEM_BOUNDED_STATE;
383
+ node->emit_class = TF_EMIT_MIXED;
384
+ node->schema_class = TF_SCHEMA_PARAMETRIC;
385
+ node->state_estimate = "O(current_group + aggregate_accumulators)";
386
+ }
387
+ }
388
+ if (node_op_is_sorted_set_bounded(node)) {
389
+ node->caps |= TF_CAP_STREAMING | TF_CAP_BOUNDED_MEMORY | TF_CAP_DETERMINISTIC;
390
+ if (node->caps & (TF_CAP_FS | TF_CAP_NET)) node->caps &= ~TF_CAP_BROWSER_SAFE;
391
+ else node->caps |= TF_CAP_BROWSER_SAFE;
392
+ node->memory_class = TF_MEM_BOUNDED_STATE;
393
+ node->emit_class = strcmp(node->op, "union") == 0 ? TF_EMIT_MIXED : TF_EMIT_PER_BATCH;
394
+ node->schema_class = TF_SCHEMA_STABLE;
395
+ node->state_estimate = strcmp(node->op, "union") == 0
396
+ ? "O(previous_left_key + current_file_key)"
397
+ : ((strcmp(node->op, "intersect-all") == 0 || strcmp(node->op, "setdiff-all") == 0)
398
+ ? "O(current_lookup_run + current_left_run)"
399
+ : "O(previous_left_key + current_lookup_key)");
400
+ }
401
+ if (node_op_is_join_spillable(node) && node_bool_arg_true(node, "sorted") &&
402
+ !node_string_arg_nonempty(node, "spill_dir")) {
403
+ int filtering = node_op_is_filtering_join(node);
404
+ node->caps |= TF_CAP_STREAMING | TF_CAP_BOUNDED_MEMORY | TF_CAP_DETERMINISTIC;
405
+ if (node->caps & (TF_CAP_FS | TF_CAP_NET)) node->caps &= ~TF_CAP_BROWSER_SAFE;
406
+ else node->caps |= TF_CAP_BROWSER_SAFE;
407
+ node->memory_class = TF_MEM_BOUNDED_STATE;
408
+ node->emit_class = TF_EMIT_PER_BATCH;
409
+ node->schema_class = filtering ? TF_SCHEMA_STABLE : TF_SCHEMA_DATA_DEPENDENT;
410
+ node->state_estimate = filtering
411
+ ? "O(current_lookup_run)"
412
+ : "O(current_lookup_run + max_matches_per_row)";
413
+ }
414
+ if (node_op_is_join_spillable(node) && node_string_arg_nonempty(node, "spill_dir")) {
415
+ int filtering = node_op_is_filtering_join(node);
416
+ node->caps |= TF_CAP_STREAMING | TF_CAP_BOUNDED_MEMORY | TF_CAP_FS | TF_CAP_DETERMINISTIC;
417
+ node->caps &= ~TF_CAP_BROWSER_SAFE;
418
+ node->memory_class = TF_MEM_EXTERNAL;
419
+ node->emit_class = TF_EMIT_ON_FLUSH;
420
+ node->schema_class = filtering ? TF_SCHEMA_STABLE : TF_SCHEMA_DATA_DEPENDENT;
421
+ node->state_estimate = filtering
422
+ ? "O(spill_run_rows * columns) RAM + O(left_rows + lookup_keys) spill"
423
+ : "O(spill_run_rows * columns + max_matches_per_row) RAM + O(left_rows + lookup_rows + output_rows) spill";
424
+ }
425
+
426
+ if (strcmp(node->op, "assert") == 0 && node_string_arg_nonempty(node, "aggregate")) {
427
+ node->memory_class = TF_MEM_BOUNDED_STATE;
428
+ node->emit_class = TF_EMIT_MIXED;
429
+ node->schema_class = TF_SCHEMA_STABLE;
430
+ node->state_estimate = "O(1) aggregate counters";
431
+ }
432
+ if (node_op_is_row_set_spillable(node) && node_string_arg_nonempty(node, "spill_dir")) {
433
+ node->caps |= TF_CAP_STREAMING | TF_CAP_BOUNDED_MEMORY | TF_CAP_FS | TF_CAP_DETERMINISTIC;
434
+ node->caps &= ~TF_CAP_BROWSER_SAFE;
435
+ node->memory_class = TF_MEM_EXTERNAL;
436
+ node->emit_class = TF_EMIT_ON_FLUSH;
437
+ node->schema_class = TF_SCHEMA_STABLE;
438
+ node->state_estimate = strcmp(node->op, "union") == 0
439
+ ? "O(spill_run_rows * columns) RAM + O(left_rows + file_rows) spill"
440
+ : ((strcmp(node->op, "intersect-all") == 0 || strcmp(node->op, "setdiff-all") == 0)
441
+ ? "O(spill_run_rows * columns) RAM + O(left_rows + lookup_rows) spill"
442
+ : "O(spill_run_rows * columns) RAM + O(left_rows + lookup_keys) spill");
443
+ }
444
+ }
445
+
446
+ static int tf_ir_validate_impl(tf_ir_plan *plan, const tf_host_policy *policy) {
447
+ plan->validated = false;
448
+ free(plan->error);
449
+ plan->error = NULL;
450
+
451
+ /* 1. At least one node */
452
+ if (plan->n_nodes == 0) {
453
+ set_plan_error(plan, "plan has no steps");
454
+ return TF_ERROR;
455
+ }
456
+
457
+ bool has_decoder = false;
458
+ bool has_encoder = false;
459
+
460
+ for (size_t i = 0; i < plan->n_nodes; i++) {
461
+ tf_ir_node *node = &plan->nodes[i];
462
+
463
+ /* 4. Op must exist in registry */
464
+ const tf_op_entry *entry = tf_op_registry_find(node->op);
465
+ if (!entry) {
466
+ set_plan_errorf(plan, "unknown op: '%s'", node->op);
467
+ return TF_ERROR;
468
+ }
469
+
470
+ /* Populate execution contract metadata from registry. */
471
+ node->caps = entry->caps;
472
+ node->memory_class = entry->memory_class;
473
+ node->emit_class = entry->emit_class;
474
+ node->schema_class = entry->schema_class;
475
+ node->state_estimate = entry->state_estimate ? entry->state_estimate : "unknown";
476
+ apply_dynamic_contract(node);
477
+
478
+ /* Check decoder/encoder placement */
479
+ if (entry->kind == TF_OP_DECODER) {
480
+ if (has_decoder) {
481
+ set_plan_error(plan, "multiple decoders not supported");
482
+ return TF_ERROR;
483
+ }
484
+ /* 2. Decoder must be the first node */
485
+ if (i != 0) {
486
+ set_plan_errorf(plan, "decoder '%s' must be the first step", node->op);
487
+ return TF_ERROR;
488
+ }
489
+ has_decoder = true;
490
+ } else if (entry->kind == TF_OP_ENCODER) {
491
+ if (has_encoder) {
492
+ set_plan_error(plan, "multiple encoders not supported");
493
+ return TF_ERROR;
494
+ }
495
+ /* 3. Encoder must be the last node */
496
+ if (i != plan->n_nodes - 1) {
497
+ set_plan_errorf(plan, "encoder '%s' must be the last step", node->op);
498
+ return TF_ERROR;
499
+ }
500
+ has_encoder = true;
501
+ } else {
502
+ /* Transform must not be first or last if we expect decoder/encoder */
503
+ }
504
+
505
+ /* 5. Required args present */
506
+ for (size_t a = 0; a < entry->n_args; a++) {
507
+ if (!entry->args[a].required) continue;
508
+ cJSON *val = cJSON_GetObjectItemCaseSensitive(node->args,
509
+ entry->args[a].name);
510
+ if (!val) {
511
+ char buf[256];
512
+ snprintf(buf, sizeof(buf), "op '%s' missing required arg '%s'",
513
+ node->op, entry->args[a].name);
514
+ set_plan_error(plan, buf);
515
+ return TF_ERROR;
516
+ }
517
+ }
518
+ if (strcmp(node->op, "validate") == 0) {
519
+ cJSON *expr = cJSON_GetObjectItemCaseSensitive(node->args, "expr");
520
+ cJSON *rules = cJSON_GetObjectItemCaseSensitive(node->args, "rules");
521
+ cJSON *rules_file = cJSON_GetObjectItemCaseSensitive(node->args, "rules_file");
522
+ if (!rules_file) rules_file = cJSON_GetObjectItemCaseSensitive(node->args, "rulesFile");
523
+ if (!expr && !rules && !rules_file) {
524
+ set_plan_error(plan, "op 'validate' requires expr, rules, or rules_file");
525
+ return TF_ERROR;
526
+ }
527
+ }
528
+ if (strcmp(node->op, "assert") == 0) {
529
+ cJSON *expr = cJSON_GetObjectItemCaseSensitive(node->args, "expr");
530
+ cJSON *agg = cJSON_GetObjectItemCaseSensitive(node->args, "aggregate");
531
+ if (!agg) agg = cJSON_GetObjectItemCaseSensitive(node->args, "agg");
532
+ if (!expr && !agg) {
533
+ set_plan_error(plan, "op 'assert' requires expr or aggregate");
534
+ return TF_ERROR;
535
+ }
536
+ }
537
+ if (node_op_is_join_spillable(node) && !node_op_is_filtering_join(node) &&
538
+ (node_bool_arg_true(node, "sorted") || node_string_arg_nonempty(node, "spill_dir")) &&
539
+ !node_number_arg_positive(node, "max_matches_per_row")) {
540
+ set_plan_error(plan,
541
+ "op 'join' sorted=true or spill_dir for inner/left joins requires max_matches_per_row");
542
+ return TF_ERROR;
543
+ }
544
+ if (enforce_host_policy(plan, node, policy) != TF_OK) {
545
+ return TF_ERROR;
546
+ }
547
+ }
548
+
549
+ if (!has_decoder) {
550
+ set_plan_error(plan, "plan has no decoder (need a codec.*.decode step)");
551
+ return TF_ERROR;
552
+ }
553
+ if (!has_encoder) {
554
+ set_plan_error(plan, "plan has no encoder (need a codec.*.encode step)");
555
+ return TF_ERROR;
556
+ }
557
+
558
+ /* Compute plan-level caps (intersection of all node caps) */
559
+ plan->plan_caps = ~(uint32_t)0;
560
+ for (size_t i = 0; i < plan->n_nodes; i++) {
561
+ plan->plan_caps &= plan->nodes[i].caps;
562
+ }
563
+
564
+ plan->validated = true;
565
+ return TF_OK;
566
+ }
567
+
568
+
569
+ int tf_ir_validate_with_host_policy(tf_ir_plan *plan, const tf_host_policy *policy) {
570
+ if (!plan) return TF_ERROR;
571
+ return tf_ir_validate_impl(plan, policy);
572
+ }
573
+
574
+ int tf_ir_validate(tf_ir_plan *plan) {
575
+ return tf_ir_validate_with_host_policy(plan, NULL);
576
+ }