onec-interactive-runtime-core 0.1.19__py3-none-any.whl

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 (87) hide show
  1. onec_interactive_runtime_core-0.1.19.dist-info/METADATA +12 -0
  2. onec_interactive_runtime_core-0.1.19.dist-info/RECORD +87 -0
  3. onec_interactive_runtime_core-0.1.19.dist-info/WHEEL +4 -0
  4. onec_interactive_runtime_core-0.1.19.dist-info/licenses/COPYRIGHT +9 -0
  5. onec_interactive_runtime_core-0.1.19.dist-info/licenses/LICENSE +232 -0
  6. onec_interactive_runtime_core-0.1.19.dist-info/licenses/THIRD_PARTY_NOTICES.txt +32 -0
  7. onec_runtime/AGENTS.md +7 -0
  8. onec_runtime/__init__.py +3 -0
  9. onec_runtime/artifacts.py +59 -0
  10. onec_runtime/bootstrap.py +310 -0
  11. onec_runtime/breakpoint_workspace.py +249 -0
  12. onec_runtime/bsl/__init__.py +139 -0
  13. onec_runtime/bsl/diagnostics.py +1046 -0
  14. onec_runtime/bsl/full_ast_worker_projection.py +522 -0
  15. onec_runtime/bsl/generated_semantic_parser.py +6322 -0
  16. onec_runtime/bsl/lexer.py +164 -0
  17. onec_runtime/bsl/module_catalog.py +420 -0
  18. onec_runtime/bsl/module_delta.py +1079 -0
  19. onec_runtime/bsl/module_universe.py +1877 -0
  20. onec_runtime/bsl/notebook_cells.py +232 -0
  21. onec_runtime/bsl/notebook_method_globals.py +74 -0
  22. onec_runtime/bsl/notebook_methods.py +175 -0
  23. onec_runtime/bsl/parser_artifact_identity.py +119 -0
  24. onec_runtime/bsl/parser_target.py +388 -0
  25. onec_runtime/bsl/preprocessor.py +16 -0
  26. onec_runtime/bsl/semantic_lowering.py +1539 -0
  27. onec_runtime/bsl/source_maps.py +2411 -0
  28. onec_runtime/bsl/worker_dependency_resolver.py +289 -0
  29. onec_runtime/bsl/worker_preprocessor.py +247 -0
  30. onec_runtime/bsl/worker_projection_model.py +137 -0
  31. onec_runtime/bsl/worker_reload_source_map.py +1697 -0
  32. onec_runtime/capture.py +113 -0
  33. onec_runtime/capture_source.py +453 -0
  34. onec_runtime/compact_table.py +376 -0
  35. onec_runtime/compact_table_backend.py +500 -0
  36. onec_runtime/config.py +198 -0
  37. onec_runtime/configurator_agent.py +520 -0
  38. onec_runtime/controller_worker.py +400 -0
  39. onec_runtime/credentials.py +18 -0
  40. onec_runtime/epf_container.py +196 -0
  41. onec_runtime/errors.py +205 -0
  42. onec_runtime/experiment.py +130 -0
  43. onec_runtime/extension_bundle.py +944 -0
  44. onec_runtime/extension_lifecycle.py +402 -0
  45. onec_runtime/extension_state.py +222 -0
  46. onec_runtime/fault_injection.py +30 -0
  47. onec_runtime/kernel.py +179 -0
  48. onec_runtime/lease.py +123 -0
  49. onec_runtime/observation.py +287 -0
  50. onec_runtime/performance_profile.py +245 -0
  51. onec_runtime/privacy.py +263 -0
  52. onec_runtime/processes.py +265 -0
  53. onec_runtime/prototype_runtime.py +2644 -0
  54. onec_runtime/rdbg/__init__.py +1 -0
  55. onec_runtime/rdbg/models.py +130 -0
  56. onec_runtime/rdbg/reconnect.py +59 -0
  57. onec_runtime/rdbg/session.py +913 -0
  58. onec_runtime/rdbg/transport.py +138 -0
  59. onec_runtime/rdbg/xml_codec.py +980 -0
  60. onec_runtime/recovery.py +106 -0
  61. onec_runtime/recovery_journal.py +79 -0
  62. onec_runtime/resources/extension/OnecInteractiveRuntime.cfe +0 -0
  63. onec_runtime/resources/extension/extension-manifest.json +108 -0
  64. onec_runtime/runtime_api.py +5272 -0
  65. onec_runtime/runtime_contracts.py +196 -0
  66. onec_runtime/server_worker.py +2034 -0
  67. onec_runtime/session.py +2123 -0
  68. onec_runtime/startup_diagnostics.py +45 -0
  69. onec_runtime/stop_routing.py +65 -0
  70. onec_runtime/supervised_processes.py +232 -0
  71. onec_runtime/supervised_runtime.py +512 -0
  72. onec_runtime/supervisor.py +363 -0
  73. onec_runtime/supervisor_model.py +148 -0
  74. onec_runtime/supervisor_protocol.py +77 -0
  75. onec_runtime/table_materialization.py +471 -0
  76. onec_runtime/table_transfer_backend.py +203 -0
  77. onec_runtime/table_value.py +311 -0
  78. onec_runtime/toolchain.py +730 -0
  79. onec_runtime/value_materialization.py +432 -0
  80. onec_runtime/value_transfer_backend.py +199 -0
  81. onec_runtime/worker_breakpoints.py +1431 -0
  82. onec_runtime/worker_epf.py +313 -0
  83. onec_runtime/worker_epf_template.py +53 -0
  84. onec_runtime/worker_stage_protocol.py +516 -0
  85. onec_runtime/worker_universe.py +3772 -0
  86. onec_runtime_build/__init__.py +1 -0
  87. onec_runtime_build/parser_target_development.py +116 -0
@@ -0,0 +1,245 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass
4
+ from time import perf_counter_ns, process_time_ns
5
+ from typing import Callable, TypeVar
6
+
7
+
8
+ T = TypeVar("T")
9
+
10
+ PARSER_CALL_COUNTERS = (
11
+ "full_module_parses",
12
+ "delta_method_parses",
13
+ "packaging_validation_parses",
14
+ )
15
+ _INTERNAL_PARSER_CALL_COUNTERS = (*PARSER_CALL_COUNTERS, "worker_profile_parses")
16
+
17
+ _PING_PARSE_PHASES = frozenset(
18
+ {
19
+ "rdbg.ping.parse_xml",
20
+ "rdbg.ping.parse_targets",
21
+ "rdbg.ping.parse_stops",
22
+ "rdbg.ping.parse_local_variables",
23
+ "rdbg.ping.parse_evaluations",
24
+ }
25
+ )
26
+
27
+
28
+ @dataclass(frozen=True, slots=True)
29
+ class PhaseEvent:
30
+ sequence: int
31
+ phase: str
32
+ wall_ns: int
33
+ cpu_ns: int
34
+ input_bytes: int = 0
35
+ output_bytes: int = 0
36
+ item_count: int = 0
37
+ page_start: int | None = None
38
+ result_id: str = ""
39
+ error_present: bool = False
40
+
41
+ def as_dict(self) -> dict[str, object]:
42
+ return asdict(self)
43
+
44
+
45
+ @dataclass(frozen=True, slots=True)
46
+ class ParserCallCounters:
47
+ full_module_parses: int = 0
48
+ delta_method_parses: int = 0
49
+ packaging_validation_parses: int = 0
50
+ worker_profile_parses: int = 0
51
+
52
+ def as_tuple(self) -> tuple[int, int, int]:
53
+ return (
54
+ self.full_module_parses,
55
+ self.delta_method_parses,
56
+ self.packaging_validation_parses,
57
+ )
58
+
59
+ def as_dict(self) -> dict[str, int]:
60
+ return {
61
+ "full_module_parses": self.full_module_parses,
62
+ "delta_method_parses": self.delta_method_parses,
63
+ "packaging_validation_parses": self.packaging_validation_parses,
64
+ }
65
+
66
+
67
+ class PhaseRecorder:
68
+ """Records aggregate timings without retaining runtime values or payloads."""
69
+
70
+ def __init__(
71
+ self,
72
+ *,
73
+ wall_clock_ns: Callable[[], int] = perf_counter_ns,
74
+ cpu_clock_ns: Callable[[], int] = process_time_ns,
75
+ ) -> None:
76
+ self.events: list[PhaseEvent] = []
77
+ self._wall_clock_ns = wall_clock_ns
78
+ self._cpu_clock_ns = cpu_clock_ns
79
+ self._parser_calls = {name: 0 for name in _INTERNAL_PARSER_CALL_COUNTERS}
80
+
81
+ @property
82
+ def parser_calls(self) -> ParserCallCounters:
83
+ return ParserCallCounters(**self._parser_calls)
84
+
85
+ def measure_parser(
86
+ self,
87
+ counter: str,
88
+ operation: Callable[[], T],
89
+ ) -> T:
90
+ self.record_parser_call(counter)
91
+ return self.measure("semantic_parse", operation)
92
+
93
+ def record_parser_call(self, counter: str) -> None:
94
+ if counter not in self._parser_calls:
95
+ raise ValueError("parser call counter is invalid")
96
+ self._parser_calls[counter] += 1
97
+
98
+ def record_duration(
99
+ self,
100
+ phase: str,
101
+ *,
102
+ wall_ns: int,
103
+ cpu_ns: int = 0,
104
+ input_bytes: int = 0,
105
+ output_bytes: int = 0,
106
+ item_count: int = 0,
107
+ result_id: str = "",
108
+ ) -> None:
109
+ if not isinstance(phase, str) or not phase:
110
+ raise ValueError("phase must not be empty")
111
+ counters = (wall_ns, cpu_ns, input_bytes, output_bytes, item_count)
112
+ if any(type(value) is not int or value < 0 for value in counters):
113
+ raise ValueError("profile counters must be non-negative integers")
114
+ self.events.append(
115
+ PhaseEvent(
116
+ sequence=len(self.events) + 1,
117
+ phase=phase,
118
+ wall_ns=wall_ns,
119
+ cpu_ns=cpu_ns,
120
+ input_bytes=input_bytes,
121
+ output_bytes=output_bytes,
122
+ item_count=item_count,
123
+ result_id=result_id,
124
+ )
125
+ )
126
+
127
+ def measure(
128
+ self,
129
+ phase: str,
130
+ operation: Callable[[], T],
131
+ *,
132
+ input_bytes: int = 0,
133
+ page_start: int | None = None,
134
+ result_id: str = "",
135
+ output_bytes: Callable[[T], int] | None = None,
136
+ item_count: Callable[[T], int] | None = None,
137
+ ) -> T:
138
+ if not phase:
139
+ raise ValueError("phase must not be empty")
140
+ if input_bytes < 0 or (page_start is not None and page_start < 0):
141
+ raise ValueError("profile counters must be non-negative")
142
+ wall_started = self._wall_clock_ns()
143
+ cpu_started = self._cpu_clock_ns()
144
+ try:
145
+ result = operation()
146
+ except BaseException:
147
+ self._append(
148
+ phase,
149
+ wall_started,
150
+ cpu_started,
151
+ input_bytes=input_bytes,
152
+ page_start=page_start,
153
+ result_id=result_id,
154
+ error_present=True,
155
+ )
156
+ raise
157
+ measured_output = output_bytes(result) if output_bytes is not None else 0
158
+ measured_items = item_count(result) if item_count is not None else 0
159
+ if measured_output < 0 or measured_items < 0:
160
+ raise ValueError("profile counters must be non-negative")
161
+ self._append(
162
+ phase,
163
+ wall_started,
164
+ cpu_started,
165
+ input_bytes=input_bytes,
166
+ output_bytes=measured_output,
167
+ item_count=measured_items,
168
+ page_start=page_start,
169
+ result_id=result_id,
170
+ )
171
+ return result
172
+
173
+ def _append(
174
+ self,
175
+ phase: str,
176
+ wall_started: int,
177
+ cpu_started: int,
178
+ *,
179
+ input_bytes: int = 0,
180
+ output_bytes: int = 0,
181
+ item_count: int = 0,
182
+ page_start: int | None = None,
183
+ result_id: str = "",
184
+ error_present: bool = False,
185
+ ) -> None:
186
+ self.events.append(
187
+ PhaseEvent(
188
+ sequence=len(self.events) + 1,
189
+ phase=phase,
190
+ wall_ns=self._wall_clock_ns() - wall_started,
191
+ cpu_ns=self._cpu_clock_ns() - cpu_started,
192
+ input_bytes=input_bytes,
193
+ output_bytes=output_bytes,
194
+ item_count=item_count,
195
+ page_start=page_start,
196
+ result_id=result_id,
197
+ error_present=error_present,
198
+ )
199
+ )
200
+
201
+
202
+ def summarize_phases(events: list[PhaseEvent]) -> dict[str, object]:
203
+ by_phase: dict[str, dict[str, int]] = {}
204
+ for event in events:
205
+ totals = by_phase.setdefault(
206
+ event.phase,
207
+ {
208
+ "calls": 0,
209
+ "wall_ns": 0,
210
+ "cpu_ns": 0,
211
+ "input_bytes": 0,
212
+ "output_bytes": 0,
213
+ "item_count": 0,
214
+ "error_count": 0,
215
+ },
216
+ )
217
+ totals["calls"] += 1
218
+ totals["wall_ns"] += event.wall_ns
219
+ totals["cpu_ns"] += event.cpu_ns
220
+ totals["input_bytes"] += event.input_bytes
221
+ totals["output_bytes"] += event.output_bytes
222
+ totals["item_count"] += event.item_count
223
+ totals["error_count"] += int(event.error_present)
224
+ ping_response_bytes = by_phase.get("rdbg.ping.request", {}).get(
225
+ "output_bytes", 0
226
+ )
227
+ ping_parser_input_bytes = sum(
228
+ by_phase.get(phase, {}).get("input_bytes", 0)
229
+ for phase in _PING_PARSE_PHASES
230
+ )
231
+ return {
232
+ "event_count": len(events),
233
+ "by_phase": by_phase,
234
+ "derived": {
235
+ "ping_response_bytes": ping_response_bytes,
236
+ "ping_parser_input_bytes": ping_parser_input_bytes,
237
+ "ping_xml_parse_amplification": (
238
+ ping_parser_input_bytes / ping_response_bytes
239
+ if ping_response_bytes
240
+ else 0.0
241
+ ),
242
+ "profiled_wall_ns": sum(event.wall_ns for event in events),
243
+ "profiled_cpu_ns": sum(event.cpu_ns for event in events),
244
+ },
245
+ }
@@ -0,0 +1,263 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import fields, is_dataclass
4
+ from hashlib import sha256
5
+ import re
6
+ from typing import Any
7
+
8
+ from onec_runtime.runtime_contracts import sanitize_normalized_diagnostic
9
+ from onec_runtime.bsl import NormalizedDiagnostic
10
+
11
+
12
+ _PLATFORM_DIAGNOSTIC_LIMIT = 4_096
13
+ _SENSITIVE_DIAGNOSTIC_KEY = (
14
+ r"(?:"
15
+ r"[0-9A-Za-z_.-]*"
16
+ r"(?:password|passwd|passphrase|secret|token|authorization|credential(?:s)?)"
17
+ r"(?:[_.-]?(?:id|key|value|header))?|"
18
+ r"rdbg[_\-.]?(?:session|connection|client|server|process|subject|"
19
+ r"target|object|property|seance)"
20
+ r"(?:[_\-.]?(?:pid|id|key))?|"
21
+ r"rdbg[0-9A-Za-z_.-]*(?:pid|id|key)|"
22
+ r"(?:process|session|connection|client|server|subject|runtime|worker)"
23
+ r"[_\-.]?(?:pid|id|token|key|secret|credential(?:s)?)|"
24
+ r"(?:process|session|connection|client|server|subject|runtime|worker)"
25
+ r"[ \t]+(?:pid|id|token|key|secret|credential(?:s)?)|"
26
+ r"(?:access|refresh|auth|api)[_\-.]?(?:key|secret|credential|header)|"
27
+ r"pid"
28
+ r")"
29
+ )
30
+ _SENSITIVE_ASSIGNMENT_RE = re.compile(
31
+ rf"""
32
+ (?P<head>
33
+ (?:
34
+ ["']{_SENSITIVE_DIAGNOSTIC_KEY}["']
35
+ |
36
+ (?<![0-9A-Za-z_]){_SENSITIVE_DIAGNOSTIC_KEY}(?![0-9A-Za-z_])
37
+ )
38
+ (?:[ \t]*(?:=|:)[ \t]*|[ \t]+)
39
+ )
40
+ (?P<value>
41
+ "(?:\\.|[^"\\])*"
42
+ |'(?:\\.|[^'\\])*'
43
+ |[^\r\n,;}}\]]+
44
+ )
45
+ """,
46
+ re.IGNORECASE | re.VERBOSE,
47
+ )
48
+ _AUTHORIZATION_RE = re.compile(
49
+ r"\b(?:bearer|basic)\s+[^\s,;}}\]]+",
50
+ re.IGNORECASE,
51
+ )
52
+
53
+
54
+ def diagnostic_to_public_wire(value: NormalizedDiagnostic) -> dict[str, object]:
55
+ """Render one canonical visible-only diagnostic or fail closed."""
56
+ try:
57
+ safe = sanitize_normalized_diagnostic(value)
58
+ if safe is None:
59
+ return {}
60
+ visible_location = safe.visible_location
61
+ related = safe.related_visible_span
62
+ return {
63
+ "diagnostic_id": safe.diagnostic_id,
64
+ "runtime_summary": safe.runtime_summary,
65
+ "stage": safe.stage.value,
66
+ "mapping_confidence": safe.mapping_confidence.value,
67
+ "visible_location": (
68
+ None
69
+ if visible_location is None
70
+ else {
71
+ "line": visible_location.line,
72
+ "column": visible_location.column,
73
+ "span": _span_wire(visible_location.span),
74
+ }
75
+ ),
76
+ "related_visible_span": (
77
+ None if related is None else _span_wire(related)
78
+ ),
79
+ "excerpt": None,
80
+ "synthetic_region": safe.synthetic_region,
81
+ }
82
+ except BaseException:
83
+ return {}
84
+
85
+
86
+ def diagnostic_to_expert_wire(value: NormalizedDiagnostic) -> dict[str, object]:
87
+ """Render the bounded expert allowlist without source or runtime identity."""
88
+ try:
89
+ safe = sanitize_normalized_diagnostic(value)
90
+ public = diagnostic_to_public_wire(value)
91
+ if safe is None or not public:
92
+ return {}
93
+ lowered = safe.lowered_location
94
+ platform, platform_truncated, platform_redacted = (
95
+ bounded_platform_diagnostic(
96
+ safe.platform_diagnostic,
97
+ truncated=safe.platform_diagnostic_truncated,
98
+ redacted=safe.platform_diagnostic_redacted,
99
+ )
100
+ )
101
+ return {
102
+ **public,
103
+ "lowered_location": (
104
+ None
105
+ if lowered is None
106
+ else {
107
+ "line": lowered.line,
108
+ "column": lowered.column,
109
+ "offset": lowered.offset,
110
+ "span": _span_wire(lowered.span),
111
+ }
112
+ ),
113
+ "platform_diagnostic": platform,
114
+ "platform_diagnostic_sha256": safe.platform_diagnostic_sha256,
115
+ "platform_diagnostic_truncated": platform_truncated,
116
+ "platform_diagnostic_redacted": platform_redacted,
117
+ "execution_artifact_sha256": safe.execution_artifact_sha256,
118
+ "source_map_sha256": safe.source_map_sha256,
119
+ # RuntimeApi can bind these exact nullable fields from its
120
+ # pre-dispatch provenance; the diagnostic itself never invents it.
121
+ "worker_generation": None,
122
+ "worker_manifest_sha256": None,
123
+ }
124
+ except BaseException:
125
+ return {}
126
+
127
+
128
+ def bounded_platform_diagnostic(
129
+ value: str | None,
130
+ *,
131
+ truncated: bool,
132
+ redacted: bool,
133
+ ) -> tuple[str | None, bool, bool]:
134
+ """Bound and redact credential/process identities in expert-only prose."""
135
+ if type(truncated) is not bool or type(redacted) is not bool:
136
+ return None, True, True
137
+ if value is None:
138
+ return None, truncated, redacted
139
+ if type(value) is not str:
140
+ return None, True, True
141
+ bounded = value[:_PLATFORM_DIAGNOSTIC_LIMIT]
142
+ input_truncated = len(value) > _PLATFORM_DIAGNOSTIC_LIMIT
143
+ bounded, assignment_count = _SENSITIVE_ASSIGNMENT_RE.subn(
144
+ lambda match: f"{match.group('head')}<redacted>",
145
+ bounded,
146
+ )
147
+ bounded, authorization_count = _AUTHORIZATION_RE.subn(
148
+ "<redacted credential>",
149
+ bounded,
150
+ )
151
+ expansion_truncated = len(bounded) > _PLATFORM_DIAGNOSTIC_LIMIT
152
+ if len(bounded) > _PLATFORM_DIAGNOSTIC_LIMIT:
153
+ bounded = bounded[:_PLATFORM_DIAGNOSTIC_LIMIT]
154
+ return (
155
+ bounded,
156
+ truncated or input_truncated or expansion_truncated,
157
+ redacted or assignment_count > 0 or authorization_count > 0,
158
+ )
159
+
160
+
161
+ def _span_wire(value: object) -> dict[str, object]:
162
+ return {
163
+ "start": getattr(value, "start"),
164
+ "end": getattr(value, "end"),
165
+ }
166
+
167
+
168
+ def public_artifact_value(value: Any) -> Any:
169
+ """Recursively redact reversible BSL sources from automatic evidence paths."""
170
+ kind = type(value).__name__
171
+ if kind == "ModuleLocation":
172
+ return {
173
+ "module_type": value.module_type,
174
+ "line": value.line,
175
+ "extension_name": value.extension_name,
176
+ }
177
+ if kind == "WorkerGenerationDebugView":
178
+ return {
179
+ "handle": public_artifact_value(value.handle),
180
+ "module_count": len(value.modules),
181
+ }
182
+ if kind == "WorkerModuleDebugView":
183
+ return {
184
+ "source_unit": public_artifact_value(value.source_unit),
185
+ "canonical_module": value.canonical_module,
186
+ "artifact_sha256": value.artifact_sha256,
187
+ "source_map_sha256": value.source_map_sha256,
188
+ }
189
+ if kind == "WorkerBreakpointPlan":
190
+ return {
191
+ "proposal_id": value.proposal_id,
192
+ "expected_catalog_version": value.expected_catalog_version,
193
+ "result_id": value.result_id,
194
+ "desired_slot_count": len(value.desired_slots),
195
+ "removed_ids": public_artifact_value(value.removed_ids),
196
+ "next_snapshot": public_artifact_value(value.next_snapshot),
197
+ }
198
+ if kind == "WorkerBreakpointBinding":
199
+ return {
200
+ "breakpoint_id": value.breakpoint_id,
201
+ "generation": public_artifact_value(value.generation),
202
+ "location": public_artifact_value(value.location),
203
+ }
204
+ if kind == "WorkerFrameProvenance":
205
+ return "<redacted worker frame provenance>"
206
+ if kind == "WorkerArtifact":
207
+ return {
208
+ "logical_name": value.logical_name,
209
+ "source_sha256": value.source_sha256,
210
+ "source_map_sha256": value.source_map_sha256,
211
+ "artifact_sha256": value.artifact_sha256,
212
+ "source_provenance": public_artifact_value(value.source_provenance),
213
+ "exports": public_artifact_value(value.exports),
214
+ }
215
+ if kind == "OperationHandle":
216
+ return {
217
+ "operation_id": value.operation_id,
218
+ "visible_source_sha256": _source_sha256(value.visible_source),
219
+ "lowered_source_sha256": _source_sha256(value.lowered_source),
220
+ "messages_intercepted": value.messages_intercepted,
221
+ }
222
+ if kind == "CaptureCellResult":
223
+ return {
224
+ "operation_id": value.operation_id,
225
+ "visible_source_sha256": _source_sha256(value.visible_source),
226
+ "lowered_source_sha256": _source_sha256(value.lowered_source),
227
+ "result": public_artifact_value(value.result),
228
+ "messages": public_artifact_value(value.messages),
229
+ }
230
+ if kind == "NotebookCell":
231
+ return {
232
+ "worker_source_sha256": _source_sha256(value.worker_source),
233
+ "statement_source_sha256": _source_sha256(value.statement_source),
234
+ "exports": public_artifact_value(value.exports),
235
+ }
236
+ if is_dataclass(value) and not isinstance(value, type):
237
+ return {
238
+ item.name: public_artifact_value(getattr(value, item.name))
239
+ for item in fields(value)
240
+ }
241
+ if isinstance(value, dict):
242
+ return _public_mapping(value)
243
+ if isinstance(value, (tuple, list, set, frozenset)):
244
+ return [public_artifact_value(item) for item in value]
245
+ return value
246
+
247
+
248
+ def _source_sha256(source: str) -> str:
249
+ return sha256(source.encode("utf-8")).hexdigest()
250
+
251
+
252
+ def _public_mapping(value: dict[Any, Any]) -> dict[str, Any]:
253
+ result = {str(key): public_artifact_value(item) for key, item in value.items()}
254
+ for field_name in (
255
+ "visible_source",
256
+ "lowered_source",
257
+ "worker_source",
258
+ "statement_source",
259
+ ):
260
+ source = result.pop(field_name, None)
261
+ if isinstance(source, str):
262
+ result[f"{field_name}_sha256"] = _source_sha256(source)
263
+ return result