@tracecode/harness 0.4.0 → 0.5.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 (43) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/browser.cjs +16 -4
  3. package/dist/browser.cjs.map +1 -1
  4. package/dist/browser.d.cts +2 -2
  5. package/dist/browser.d.ts +2 -2
  6. package/dist/browser.js +16 -4
  7. package/dist/browser.js.map +1 -1
  8. package/dist/cli.js +0 -0
  9. package/dist/core.cjs +16 -4
  10. package/dist/core.cjs.map +1 -1
  11. package/dist/core.d.cts +9 -6
  12. package/dist/core.d.ts +9 -6
  13. package/dist/core.js +16 -4
  14. package/dist/core.js.map +1 -1
  15. package/dist/index.cjs +305 -13
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +2 -2
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.js +305 -13
  20. package/dist/index.js.map +1 -1
  21. package/dist/internal/browser.d.cts +1 -1
  22. package/dist/internal/browser.d.ts +1 -1
  23. package/dist/javascript.cjs +227 -9
  24. package/dist/javascript.cjs.map +1 -1
  25. package/dist/javascript.d.cts +4 -4
  26. package/dist/javascript.d.ts +4 -4
  27. package/dist/javascript.js +227 -9
  28. package/dist/javascript.js.map +1 -1
  29. package/dist/python.cjs +62 -0
  30. package/dist/python.cjs.map +1 -1
  31. package/dist/python.d.cts +2 -2
  32. package/dist/python.d.ts +2 -2
  33. package/dist/python.js +62 -0
  34. package/dist/python.js.map +1 -1
  35. package/dist/{runtime-types-Dvgn07z9.d.cts → runtime-types--lBQ6rYu.d.cts} +1 -1
  36. package/dist/{runtime-types-C7d1LFbx.d.ts → runtime-types-DtaaAhHL.d.ts} +1 -1
  37. package/dist/{types-Bzr1Ohcf.d.cts → types-DwIYM3Ku.d.cts} +5 -2
  38. package/dist/{types-Bzr1Ohcf.d.ts → types-DwIYM3Ku.d.ts} +5 -2
  39. package/package.json +12 -10
  40. package/workers/javascript/javascript-worker.js +455 -31
  41. package/workers/python/generated-python-harness-snippets.js +1 -1
  42. package/workers/python/pyodide-worker.js +31 -0
  43. package/workers/python/runtime-core.js +235 -8
@@ -11,7 +11,7 @@
11
11
  scope.__TRACECODE_PYTHON_HARNESS__ = Object.freeze({
12
12
  PYTHON_CLASS_DEFINITIONS: "\nclass TreeNode:\n def __init__(self, val=0, left=None, right=None):\n self.val = val\n self.value = val\n self.left = left\n self.right = right\n def __getitem__(self, key):\n if key == 'val': return getattr(self, 'val', getattr(self, 'value', None))\n if key == 'value': return getattr(self, 'value', getattr(self, 'val', None))\n if key == 'left': return self.left\n if key == 'right': return self.right\n raise KeyError(key)\n def get(self, key, default=None):\n if key == 'val': return getattr(self, 'val', getattr(self, 'value', default))\n if key == 'value': return getattr(self, 'value', getattr(self, 'val', default))\n if key == 'left': return self.left\n if key == 'right': return self.right\n return default\n def __repr__(self):\n return f\"TreeNode({getattr(self, 'val', getattr(self, 'value', None))})\"\n\nclass ListNode:\n def __init__(self, val=0, next=None):\n self.val = val\n self.value = val\n self.next = next\n def __getitem__(self, key):\n if key == 'val': return getattr(self, 'val', getattr(self, 'value', None))\n if key == 'value': return getattr(self, 'value', getattr(self, 'val', None))\n if key == 'next': return self.next\n raise KeyError(key)\n def get(self, key, default=None):\n if key == 'val': return getattr(self, 'val', getattr(self, 'value', default))\n if key == 'value': return getattr(self, 'value', getattr(self, 'val', default))\n if key == 'next': return self.next\n return default\n def __repr__(self):\n return f\"ListNode({getattr(self, 'val', getattr(self, 'value', None))})\"\n",
13
13
  PYTHON_CONVERSION_HELPERS: "\ndef _ensure_node_value_aliases(node):\n if node is None:\n return node\n try:\n has_val = hasattr(node, 'val')\n has_value = hasattr(node, 'value')\n if has_value and not has_val:\n try:\n setattr(node, 'val', getattr(node, 'value'))\n except Exception:\n pass\n elif has_val and not has_value:\n try:\n setattr(node, 'value', getattr(node, 'val'))\n except Exception:\n pass\n except Exception:\n pass\n return node\n\ndef _dict_to_tree(d):\n if d is None:\n return None\n if not isinstance(d, dict):\n return d\n if 'val' not in d and 'value' not in d:\n return d\n node = TreeNode(d.get('val', d.get('value', 0)))\n _ensure_node_value_aliases(node)\n node.left = _dict_to_tree(d.get('left'))\n node.right = _dict_to_tree(d.get('right'))\n return node\n\ndef _dict_to_list(d, _refs=None):\n if _refs is None:\n _refs = {}\n if d is None:\n return None\n if not isinstance(d, dict):\n return d\n if '__ref__' in d:\n return _refs.get(d.get('__ref__'))\n if 'val' not in d and 'value' not in d:\n return d\n node = ListNode(d.get('val', d.get('value', 0)))\n _ensure_node_value_aliases(node)\n node_id = d.get('__id__')\n if isinstance(node_id, str) and node_id:\n _refs[node_id] = node\n node.next = _dict_to_list(d.get('next'), _refs)\n return node\n",
14
- PYTHON_TRACE_SERIALIZE_FUNCTION: "\n# Sentinel to mark skipped values (functions, etc.) - distinct from None\n_SKIP_SENTINEL = \"__TRACECODE_SKIP__\"\n_MAX_SERIALIZE_DEPTH = 48\n\ndef _serialize(obj, depth=0, node_refs=None):\n if node_refs is None:\n node_refs = {}\n if isinstance(obj, (bool, int, str, type(None))):\n return obj\n elif isinstance(obj, float):\n if not math.isfinite(obj):\n if math.isnan(obj):\n return \"NaN\"\n return \"Infinity\" if obj > 0 else \"-Infinity\"\n return obj\n if depth > _MAX_SERIALIZE_DEPTH:\n return \"<max depth>\"\n elif isinstance(obj, (list, tuple)):\n return [_serialize(x, depth + 1, node_refs) for x in obj]\n elif getattr(obj, '__class__', None) and getattr(obj.__class__, '__name__', '') == 'deque':\n return [_serialize(x, depth + 1, node_refs) for x in obj]\n elif isinstance(obj, dict):\n return {str(k): _serialize(v, depth + 1, node_refs) for k, v in obj.items()}\n elif isinstance(obj, set):\n # Use try/except for sorting to handle heterogeneous sets\n try:\n sorted_vals = sorted([_serialize(x, depth + 1, node_refs) for x in obj])\n except TypeError:\n sorted_vals = [_serialize(x, depth + 1, node_refs) for x in obj]\n return {\"__type__\": \"set\", \"values\": sorted_vals}\n elif (hasattr(obj, 'val') or hasattr(obj, 'value')) and (hasattr(obj, 'left') or hasattr(obj, 'right')):\n obj_ref = id(obj)\n if obj_ref in node_refs:\n return {\"__ref__\": node_refs[obj_ref]}\n node_id = f\"tree-{obj_ref}\"\n node_refs[obj_ref] = node_id\n result = {\n \"__type__\": \"TreeNode\",\n \"__id__\": node_id,\n \"val\": _serialize(getattr(obj, 'val', getattr(obj, 'value', None)), depth + 1, node_refs),\n }\n if hasattr(obj, 'left'):\n result[\"left\"] = _serialize(obj.left, depth + 1, node_refs)\n if hasattr(obj, 'right'):\n result[\"right\"] = _serialize(obj.right, depth + 1, node_refs)\n return result\n elif (hasattr(obj, 'val') or hasattr(obj, 'value')) and hasattr(obj, 'next'):\n obj_ref = id(obj)\n if obj_ref in node_refs:\n return {\"__ref__\": node_refs[obj_ref]}\n node_id = f\"list-{obj_ref}\"\n node_refs[obj_ref] = node_id\n result = {\n \"__type__\": \"ListNode\",\n \"__id__\": node_id,\n \"val\": _serialize(getattr(obj, 'val', getattr(obj, 'value', None)), depth + 1, node_refs),\n }\n result[\"next\"] = _serialize(obj.next, depth + 1, node_refs)\n return result\n elif callable(obj):\n # Skip functions entirely - return sentinel\n return _SKIP_SENTINEL\n else:\n repr_str = repr(obj)\n # Filter out function-like representations (e.g., <function foo at 0x...>)\n if repr_str.startswith('<') and repr_str.endswith('>'):\n return _SKIP_SENTINEL\n return repr_str\n",
14
+ PYTHON_TRACE_SERIALIZE_FUNCTION: "\n# Sentinel to mark skipped values (functions, etc.) - distinct from None\n_SKIP_SENTINEL = \"__TRACECODE_SKIP__\"\n_MAX_SERIALIZE_DEPTH = 48\n_MAX_OBJECT_FIELDS = 32\n\ndef _serialize(obj, depth=0, node_refs=None):\n if node_refs is None:\n node_refs = {}\n if isinstance(obj, (bool, int, str, type(None))):\n return obj\n elif isinstance(obj, float):\n if not math.isfinite(obj):\n if math.isnan(obj):\n return \"NaN\"\n return \"Infinity\" if obj > 0 else \"-Infinity\"\n return obj\n if depth > _MAX_SERIALIZE_DEPTH:\n return \"<max depth>\"\n elif isinstance(obj, (list, tuple)):\n return [_serialize(x, depth + 1, node_refs) for x in obj]\n elif getattr(obj, '__class__', None) and getattr(obj.__class__, '__name__', '') == 'deque':\n return [_serialize(x, depth + 1, node_refs) for x in obj]\n elif isinstance(obj, dict):\n return {str(k): _serialize(v, depth + 1, node_refs) for k, v in obj.items()}\n elif isinstance(obj, set):\n # Use try/except for sorting to handle heterogeneous sets\n try:\n sorted_vals = sorted([_serialize(x, depth + 1, node_refs) for x in obj])\n except TypeError:\n sorted_vals = [_serialize(x, depth + 1, node_refs) for x in obj]\n return {\"__type__\": \"set\", \"values\": sorted_vals}\n elif (hasattr(obj, 'val') or hasattr(obj, 'value')) and (hasattr(obj, 'left') or hasattr(obj, 'right')):\n obj_ref = id(obj)\n if obj_ref in node_refs:\n return {\"__ref__\": node_refs[obj_ref]}\n node_id = f\"tree-{obj_ref}\"\n node_refs[obj_ref] = node_id\n result = {\n \"__type__\": \"TreeNode\",\n \"__id__\": node_id,\n \"val\": _serialize(getattr(obj, 'val', getattr(obj, 'value', None)), depth + 1, node_refs),\n }\n if hasattr(obj, 'left'):\n result[\"left\"] = _serialize(obj.left, depth + 1, node_refs)\n if hasattr(obj, 'right'):\n result[\"right\"] = _serialize(obj.right, depth + 1, node_refs)\n return result\n elif (hasattr(obj, 'val') or hasattr(obj, 'value')) and hasattr(obj, 'next'):\n obj_ref = id(obj)\n if obj_ref in node_refs:\n return {\"__ref__\": node_refs[obj_ref]}\n node_id = f\"list-{obj_ref}\"\n node_refs[obj_ref] = node_id\n result = {\n \"__type__\": \"ListNode\",\n \"__id__\": node_id,\n \"val\": _serialize(getattr(obj, 'val', getattr(obj, 'value', None)), depth + 1, node_refs),\n }\n result[\"next\"] = _serialize(obj.next, depth + 1, node_refs)\n return result\n elif hasattr(obj, '__dict__'):\n obj_ref = id(obj)\n if obj_ref in node_refs:\n return {\"__ref__\": node_refs[obj_ref]}\n node_id = f\"object-{obj_ref}\"\n node_refs[obj_ref] = node_id\n class_name = getattr(getattr(obj, '__class__', None), '__name__', 'object')\n result = {\n \"__type__\": \"object\",\n \"__class__\": class_name,\n \"__id__\": node_id,\n }\n try:\n raw_fields = getattr(obj, '__dict__', None)\n except Exception:\n raw_fields = None\n if isinstance(raw_fields, dict):\n added = 0\n for key, value in raw_fields.items():\n key_str = str(key)\n if key_str.startswith('_'):\n continue\n if callable(value):\n continue\n result[key_str] = _serialize(value, depth + 1, node_refs)\n added += 1\n if added >= _MAX_OBJECT_FIELDS:\n result[\"__truncated__\"] = True\n break\n return result\n elif callable(obj):\n # Skip functions entirely - return sentinel\n return _SKIP_SENTINEL\n else:\n repr_str = repr(obj)\n # Filter out function-like representations (e.g., <function foo at 0x...>)\n if repr_str.startswith('<') and repr_str.endswith('>'):\n return _SKIP_SENTINEL\n return repr_str\n",
15
15
  PYTHON_EXECUTE_SERIALIZE_FUNCTION: "\n_MAX_SERIALIZE_DEPTH = 48\n\ndef _serialize(obj, depth=0):\n if isinstance(obj, (bool, int, str, type(None))):\n return obj\n elif isinstance(obj, float):\n if not math.isfinite(obj):\n if math.isnan(obj):\n return \"NaN\"\n return \"Infinity\" if obj > 0 else \"-Infinity\"\n return obj\n if depth > _MAX_SERIALIZE_DEPTH:\n return \"<max depth>\"\n elif isinstance(obj, (list, tuple)):\n return [_serialize(x, depth + 1) for x in obj]\n elif getattr(obj, '__class__', None) and getattr(obj.__class__, '__name__', '') == 'deque':\n return [_serialize(x, depth + 1) for x in obj]\n elif isinstance(obj, dict):\n return {str(k): _serialize(v, depth + 1) for k, v in obj.items()}\n elif isinstance(obj, set):\n try:\n return {\"__type__\": \"set\", \"values\": sorted([_serialize(x, depth + 1) for x in obj])}\n except TypeError:\n return {\"__type__\": \"set\", \"values\": [_serialize(x, depth + 1) for x in obj]}\n elif (hasattr(obj, 'val') or hasattr(obj, 'value')) and (hasattr(obj, 'left') or hasattr(obj, 'right')):\n result = {\"__type__\": \"TreeNode\", \"val\": _serialize(getattr(obj, 'val', getattr(obj, 'value', None)), depth + 1)}\n if hasattr(obj, 'left'):\n result[\"left\"] = _serialize(obj.left, depth + 1)\n if hasattr(obj, 'right'):\n result[\"right\"] = _serialize(obj.right, depth + 1)\n return result\n elif (hasattr(obj, 'val') or hasattr(obj, 'value')) and hasattr(obj, 'next'):\n result = {\"__type__\": \"ListNode\", \"val\": _serialize(getattr(obj, 'val', getattr(obj, 'value', None)), depth + 1)}\n result[\"next\"] = _serialize(obj.next, depth + 1)\n return result\n elif callable(obj):\n return None\n else:\n repr_str = repr(obj)\n if repr_str.startswith('<') and repr_str.endswith('>'):\n return None\n return repr_str\n",
16
16
  PYTHON_SERIALIZE_FUNCTION: "\n_MAX_SERIALIZE_DEPTH = 48\n\ndef _serialize(obj, depth=0):\n if isinstance(obj, (bool, int, str, type(None))):\n return obj\n elif isinstance(obj, float):\n if not math.isfinite(obj):\n if math.isnan(obj):\n return \"NaN\"\n return \"Infinity\" if obj > 0 else \"-Infinity\"\n return obj\n if depth > _MAX_SERIALIZE_DEPTH:\n return \"<max depth>\"\n elif isinstance(obj, (list, tuple)):\n return [_serialize(x, depth + 1) for x in obj]\n elif getattr(obj, '__class__', None) and getattr(obj.__class__, '__name__', '') == 'deque':\n return [_serialize(x, depth + 1) for x in obj]\n elif isinstance(obj, dict):\n return {str(k): _serialize(v, depth + 1) for k, v in obj.items()}\n elif isinstance(obj, set):\n try:\n return {\"__type__\": \"set\", \"values\": sorted([_serialize(x, depth + 1) for x in obj])}\n except TypeError:\n return {\"__type__\": \"set\", \"values\": [_serialize(x, depth + 1) for x in obj]}\n elif (hasattr(obj, 'val') or hasattr(obj, 'value')) and (hasattr(obj, 'left') or hasattr(obj, 'right')):\n result = {\"__type__\": \"TreeNode\", \"val\": _serialize(getattr(obj, 'val', getattr(obj, 'value', None)), depth + 1)}\n if hasattr(obj, 'left'):\n result[\"left\"] = _serialize(obj.left, depth + 1)\n if hasattr(obj, 'right'):\n result[\"right\"] = _serialize(obj.right, depth + 1)\n return result\n elif (hasattr(obj, 'val') or hasattr(obj, 'value')) and hasattr(obj, 'next'):\n result = {\"__type__\": \"ListNode\", \"val\": _serialize(getattr(obj, 'val', getattr(obj, 'value', None)), depth + 1)}\n result[\"next\"] = _serialize(obj.next, depth + 1)\n return result\n elif callable(obj):\n return None\n else:\n repr_str = repr(obj)\n if repr_str.startswith('<') and repr_str.endswith('>'):\n return None\n return repr_str\n",
17
17
  });
@@ -232,6 +232,7 @@ const PYTHON_TRACE_SERIALIZE_FUNCTION_SNIPPET = resolveSharedPythonSnippet(
232
232
  # Sentinel to mark skipped values (functions, etc.) - distinct from None
233
233
  _SKIP_SENTINEL = "__TRACECODE_SKIP__"
234
234
  _MAX_SERIALIZE_DEPTH = 48
235
+ _MAX_OBJECT_FIELDS = 32
235
236
 
236
237
  def _serialize(obj, depth=0, node_refs=None):
237
238
  if node_refs is None:
@@ -288,6 +289,36 @@ def _serialize(obj, depth=0, node_refs=None):
288
289
  }
289
290
  result["next"] = _serialize(obj.next, depth + 1, node_refs)
290
291
  return result
292
+ elif hasattr(obj, '__dict__'):
293
+ obj_ref = id(obj)
294
+ if obj_ref in node_refs:
295
+ return {"__ref__": node_refs[obj_ref]}
296
+ node_id = f"object-{obj_ref}"
297
+ node_refs[obj_ref] = node_id
298
+ class_name = getattr(getattr(obj, '__class__', None), '__name__', 'object')
299
+ result = {
300
+ "__type__": "object",
301
+ "__class__": class_name,
302
+ "__id__": node_id,
303
+ }
304
+ try:
305
+ raw_fields = getattr(obj, '__dict__', None)
306
+ except Exception:
307
+ raw_fields = None
308
+ if isinstance(raw_fields, dict):
309
+ added = 0
310
+ for key, value in raw_fields.items():
311
+ key_str = str(key)
312
+ if key_str.startswith('_'):
313
+ continue
314
+ if callable(value):
315
+ continue
316
+ result[key_str] = _serialize(value, depth + 1, node_refs)
317
+ added += 1
318
+ if added >= _MAX_OBJECT_FIELDS:
319
+ result["__truncated__"] = True
320
+ break
321
+ return result
291
322
  elif callable(obj):
292
323
  # Skip functions entirely - return sentinel
293
324
  return _SKIP_SENTINEL
@@ -38,6 +38,8 @@ _original_print = _builtins.print
38
38
  _target_function = "${targetFunction}"
39
39
  _MIRROR_PRINT_TO_WORKER_CONSOLE = ${mirrorPrintToConsole ? 'True' : 'False'}
40
40
  _MINIMAL_TRACE = ${minimalTrace ? 'True' : 'False'}
41
+ _SCRIPT_MODE = ${functionName ? 'False' : 'True'}
42
+ _TRACE_INPUT_NAMES = set(${JSON.stringify(Object.keys(inputs))})
41
43
 
42
44
  class _InfiniteLoopDetected(Exception):
43
45
  pass
@@ -71,6 +73,7 @@ _internal_funcs = {'_serialize', '_tracer', '_custom_print', '_dict_to_tree', '_
71
73
  _internal_locals = {
72
74
  '_trace_data', '_console_output', '_original_print', '_target_function',
73
75
  '_MIRROR_PRINT_TO_WORKER_CONSOLE', '_MINIMAL_TRACE', '_SKIP_SENTINEL',
76
+ '_SCRIPT_MODE', '_TRACE_INPUT_NAMES', '_SCRIPT_PRE_USER_GLOBALS',
74
77
  '_call_stack', '_pending_accesses', '_prev_hashmap_snapshots', '_TRACE_MUTATING_METHODS', '_internal_funcs', '_internal_locals', '_max_trace_steps',
75
78
  '_trace_limit_exceeded', '_timeout_reason', '_total_line_events', '_max_line_events',
76
79
  '_line_hit_count', '_max_single_line_hits', '_infinite_loop_line',
@@ -134,18 +137,235 @@ def _snapshot_call_stack():
134
137
  return []
135
138
  return [f.copy() for f in _call_stack]
136
139
 
137
- def _snapshot_locals(frame):
140
+ def _is_serialized_ref(value):
141
+ return isinstance(value, dict) and len(value) == 1 and isinstance(value.get('__ref__'), str)
142
+
143
+ def _is_serialized_list_node(value):
144
+ return isinstance(value, dict) and value.get('__type__') == 'ListNode' and isinstance(value.get('__id__'), str)
145
+
146
+ def _serialized_list_root_id(value):
147
+ if _is_serialized_list_node(value):
148
+ return value.get('__id__')
149
+ if _is_serialized_ref(value):
150
+ return value.get('__ref__')
151
+ return None
152
+
153
+ def _collect_serialized_list_component(value, node_ids=None, ref_ids=None, seen=None):
154
+ if node_ids is None:
155
+ node_ids = set()
156
+ if ref_ids is None:
157
+ ref_ids = set()
158
+ if seen is None:
159
+ seen = set()
160
+
161
+ if _is_serialized_ref(value):
162
+ ref_ids.add(value.get('__ref__'))
163
+ return (node_ids, ref_ids)
164
+
165
+ if not _is_serialized_list_node(value):
166
+ return (node_ids, ref_ids)
167
+
168
+ marker = id(value)
169
+ if marker in seen:
170
+ return (node_ids, ref_ids)
171
+ seen.add(marker)
172
+
173
+ node_id = value.get('__id__')
174
+ if isinstance(node_id, str):
175
+ node_ids.add(node_id)
176
+
177
+ for field_name in ('next', 'prev'):
178
+ if field_name in value:
179
+ _collect_serialized_list_component(value.get(field_name), node_ids, ref_ids, seen)
180
+
181
+ return (node_ids, ref_ids)
182
+
183
+ def _clone_serialized_value(value):
184
+ if isinstance(value, dict):
185
+ return {key: _clone_serialized_value(nested) for key, nested in value.items()}
186
+ if isinstance(value, list):
187
+ return [_clone_serialized_value(item) for item in value]
188
+ return value
189
+
190
+ def _inline_component_list_refs(value, root_payloads, seen_root_ids=None):
191
+ if seen_root_ids is None:
192
+ seen_root_ids = set()
193
+
194
+ if _is_serialized_ref(value):
195
+ ref_id = value.get('__ref__')
196
+ if not isinstance(ref_id, str):
197
+ return value
198
+ target = root_payloads.get(ref_id)
199
+ if target is None or ref_id in seen_root_ids:
200
+ return value
201
+ next_seen = set(seen_root_ids)
202
+ next_seen.add(ref_id)
203
+ return _inline_component_list_refs(_clone_serialized_value(target), root_payloads, next_seen)
204
+
205
+ if isinstance(value, list):
206
+ return [_inline_component_list_refs(item, root_payloads, seen_root_ids) for item in value]
207
+
208
+ if not isinstance(value, dict):
209
+ return value
210
+
211
+ out = {}
212
+ next_seen = set(seen_root_ids)
213
+ value_id = value.get('__id__')
214
+ if isinstance(value_id, str):
215
+ next_seen.add(value_id)
216
+
217
+ for key, nested in value.items():
218
+ out[key] = _inline_component_list_refs(nested, root_payloads, next_seen)
219
+ return out
220
+
221
+ def _normalize_top_level_linked_list_locals(local_vars):
222
+ if not isinstance(local_vars, dict) or len(local_vars) < 2:
223
+ return local_vars
224
+
225
+ ordered_names = list(local_vars.keys())
226
+ candidates = []
227
+
228
+ for index, name in enumerate(ordered_names):
229
+ value = local_vars.get(name)
230
+ root_id = _serialized_list_root_id(value)
231
+ if not isinstance(root_id, str):
232
+ continue
233
+ node_ids, ref_ids = _collect_serialized_list_component(value)
234
+ all_ids = set(node_ids) | set(ref_ids)
235
+ if not all_ids:
236
+ all_ids.add(root_id)
237
+ candidates.append({
238
+ 'name': name,
239
+ 'index': index,
240
+ 'value': value,
241
+ 'root_id': root_id,
242
+ 'is_ref_only': _is_serialized_ref(value),
243
+ 'node_ids': node_ids,
244
+ 'ref_ids': ref_ids,
245
+ 'all_ids': all_ids,
246
+ 'incoming': 0,
247
+ })
248
+
249
+ if len(candidates) < 2:
250
+ return local_vars
251
+
252
+ parent = list(range(len(candidates)))
253
+
254
+ def _find(i):
255
+ while parent[i] != i:
256
+ parent[i] = parent[parent[i]]
257
+ i = parent[i]
258
+ return i
259
+
260
+ def _union(a, b):
261
+ ra = _find(a)
262
+ rb = _find(b)
263
+ if ra != rb:
264
+ parent[rb] = ra
265
+
266
+ for i in range(len(candidates)):
267
+ left = candidates[i]
268
+ for j in range(i + 1, len(candidates)):
269
+ right = candidates[j]
270
+ if left['all_ids'].intersection(right['all_ids']):
271
+ _union(i, j)
272
+ if left['root_id'] in right['all_ids'] or right['root_id'] in left['all_ids']:
273
+ _union(i, j)
274
+
275
+ for i in range(len(candidates)):
276
+ left = candidates[i]
277
+ for j in range(len(candidates)):
278
+ if i == j:
279
+ continue
280
+ right = candidates[j]
281
+ if left['root_id'] in right['all_ids'] and left['root_id'] != right['root_id']:
282
+ left['incoming'] += 1
283
+
284
+ groups = {}
285
+ for index, candidate in enumerate(candidates):
286
+ groups.setdefault(_find(index), []).append(candidate)
287
+
288
+ for group in groups.values():
289
+ if len(group) < 2:
290
+ continue
291
+
292
+ root_payloads = {}
293
+ for candidate in group:
294
+ root_id = candidate.get('root_id')
295
+ value = candidate.get('value')
296
+ if isinstance(root_id, str) and _is_serialized_list_node(value):
297
+ root_payloads[root_id] = _clone_serialized_value(value)
298
+
299
+ canonical = max(
300
+ group,
301
+ key=lambda candidate: (
302
+ 0 if candidate['is_ref_only'] else 1,
303
+ 1 if candidate['incoming'] == 0 else 0,
304
+ len(candidate['node_ids']) + len(candidate['ref_ids']),
305
+ -candidate['index'],
306
+ ),
307
+ )
308
+
309
+ if _is_serialized_list_node(canonical.get('value')):
310
+ local_vars[canonical['name']] = _inline_component_list_refs(
311
+ _clone_serialized_value(canonical['value']),
312
+ root_payloads,
313
+ set([canonical.get('root_id')]) if isinstance(canonical.get('root_id'), str) else set(),
314
+ )
315
+
316
+ for candidate in group:
317
+ if candidate is canonical:
318
+ continue
319
+ root_id = candidate.get('root_id')
320
+ if isinstance(root_id, str):
321
+ local_vars[candidate['name']] = {'__ref__': root_id}
322
+
323
+ return local_vars
324
+
325
+ _SCRIPT_PRE_USER_GLOBALS = set()
326
+
327
+ def _snapshot_local_sources(frame):
138
328
  if _MINIMAL_TRACE:
139
329
  return {}
330
+ try:
331
+ func_name = frame.f_code.co_name
332
+ sources = {}
333
+ for name in frame.f_locals.keys():
334
+ if name in _internal_locals or name == '_' or name.startswith('__'):
335
+ continue
336
+ if _SCRIPT_MODE and func_name == '<module>':
337
+ if name in _TRACE_INPUT_NAMES:
338
+ sources[name] = 'user-input'
339
+ elif name in _SCRIPT_PRE_USER_GLOBALS:
340
+ sources[name] = 'harness-prelude'
341
+ else:
342
+ sources[name] = 'user'
343
+ else:
344
+ sources[name] = 'user'
345
+ return sources
346
+ except Exception:
347
+ return {}
348
+
349
+ def _snapshot_locals(frame, with_sources=False):
350
+ if _MINIMAL_TRACE:
351
+ return ({}, {}) if with_sources else {}
140
352
  try:
141
353
  _node_refs = {}
142
- return {
354
+ _sources = _snapshot_local_sources(frame)
355
+ local_vars = {
143
356
  k: v
144
- for k, v in ((k, _serialize(v, 0, _node_refs)) for k, v in frame.f_locals.items() if k not in _internal_locals and k != '_' and not k.startswith('__'))
357
+ for k, v in (
358
+ (k, _serialize(v, 0, _node_refs))
359
+ for k, v in frame.f_locals.items()
360
+ if k not in _internal_locals and k != '_' and not k.startswith('__') and _sources.get(k) != 'harness-prelude'
361
+ )
145
362
  if v != _SKIP_SENTINEL
146
363
  }
364
+ local_vars = _normalize_top_level_linked_list_locals(local_vars)
365
+ local_sources = {name: _sources.get(name, 'user') for name in local_vars.keys()}
366
+ return (local_vars, local_sources) if with_sources else local_vars
147
367
  except Exception:
148
- return {}
368
+ return ({}, {}) if with_sources else {}
149
369
 
150
370
  def __tracecode_record_access(frame, event):
151
371
  if frame is None or not isinstance(event, dict):
@@ -681,12 +901,13 @@ def _tracer(frame, event, arg):
681
901
  _trace_limit_exceeded = True
682
902
  _timeout_reason = 'single-line-limit'
683
903
  _infinite_loop_line = frame.f_lineno
684
- local_vars = _snapshot_locals(frame)
904
+ local_vars, local_sources = _snapshot_locals(frame, with_sources=True)
685
905
  local_vars['timeoutReason'] = _timeout_reason
686
906
  _trace_data.append({
687
907
  'line': frame.f_lineno,
688
908
  'event': 'timeout',
689
909
  'variables': local_vars,
910
+ 'variableSources': local_sources,
690
911
  'function': func_name,
691
912
  'callStack': _snapshot_call_stack(),
692
913
  'stdoutLineCount': len(_console_output),
@@ -715,7 +936,7 @@ def _tracer(frame, event, arg):
715
936
  raise _InfiniteLoopDetected(f"Exceeded {_max_trace_steps} trace steps")
716
937
 
717
938
  if event == 'call':
718
- local_vars = _snapshot_locals(frame)
939
+ local_vars, local_sources = _snapshot_locals(frame, with_sources=True)
719
940
  if func_name != '<module>':
720
941
  _call_stack.append({
721
942
  'function': func_name,
@@ -728,6 +949,7 @@ def _tracer(frame, event, arg):
728
949
  'line': frame.f_lineno,
729
950
  'event': 'call',
730
951
  'variables': local_vars,
952
+ 'variableSources': local_sources,
731
953
  'function': func_name,
732
954
  'callStack': _snapshot_call_stack(),
733
955
  'stdoutLineCount': len(_console_output),
@@ -737,11 +959,12 @@ def _tracer(frame, event, arg):
737
959
  elif event == 'line':
738
960
  if _MINIMAL_TRACE:
739
961
  return _tracer
740
- local_vars = _snapshot_locals(frame)
962
+ local_vars, local_sources = _snapshot_locals(frame, with_sources=True)
741
963
  _trace_data.append({
742
964
  'line': frame.f_lineno,
743
965
  'event': event,
744
966
  'variables': local_vars,
967
+ 'variableSources': local_sources,
745
968
  'function': func_name,
746
969
  'callStack': _snapshot_call_stack(),
747
970
  'stdoutLineCount': len(_console_output),
@@ -750,11 +973,12 @@ def _tracer(frame, event, arg):
750
973
  })
751
974
  elif event == 'return':
752
975
  if not _MINIMAL_TRACE:
753
- local_vars = _snapshot_locals(frame)
976
+ local_vars, local_sources = _snapshot_locals(frame, with_sources=True)
754
977
  _trace_data.append({
755
978
  'line': frame.f_lineno,
756
979
  'event': 'return',
757
980
  'variables': local_vars,
981
+ 'variableSources': local_sources,
758
982
  'function': func_name,
759
983
  'returnValue': _serialize(arg),
760
984
  'callStack': _snapshot_call_stack(),
@@ -893,6 +1117,9 @@ ${listConversions}
893
1117
 
894
1118
  ${preloadUserDefinitions}
895
1119
 
1120
+ if _SCRIPT_MODE:
1121
+ _SCRIPT_PRE_USER_GLOBALS = set(globals().keys()) - _TRACE_INPUT_NAMES
1122
+
896
1123
  sys.settrace(_tracer)
897
1124
  _trace_failed = False
898
1125