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,232 @@
1
+ from __future__ import annotations
2
+
3
+ from bisect import bisect_left
4
+ from dataclasses import dataclass, field
5
+ from typing import Any
6
+
7
+ from onec_runtime.bsl.lexer import tokenize
8
+ from onec_runtime.bsl.parser_target import PythonParserTarget
9
+ from onec_runtime.bsl.semantic_lowering import WorkerExport
10
+ from onec_runtime.bsl.source_maps import (
11
+ MappedSource,
12
+ SourceArtifactKind,
13
+ SourceSpan,
14
+ SourceTransformBuilder,
15
+ SourceUnitKind,
16
+ SourceUnitRef,
17
+ mapped_visible_source,
18
+ source_sha256,
19
+ )
20
+ from onec_runtime.errors import ProtocolError
21
+
22
+
23
+ @dataclass(frozen=True, slots=True)
24
+ class NotebookMethodProjection:
25
+ """One complete AST method, including decorations and generated export."""
26
+
27
+ export: WorkerExport
28
+ mapped_source: MappedSource = field(repr=False)
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class NotebookCellProjection:
33
+ """Generated-AST projections of one immutable visible notebook cell."""
34
+
35
+ visible: MappedSource = field(repr=False)
36
+ worker: MappedSource | None = field(repr=False)
37
+ statements: MappedSource | None = field(repr=False)
38
+ exports: tuple[WorkerExport, ...]
39
+ methods: tuple[NotebookMethodProjection, ...] = field(default=(), repr=False)
40
+
41
+ @property
42
+ def worker_source(self) -> str:
43
+ return "" if self.worker is None else self.worker.text
44
+
45
+ @property
46
+ def statement_source(self) -> str:
47
+ return "" if self.statements is None else self.statements.text
48
+
49
+ @property
50
+ def has_methods(self) -> bool:
51
+ return self.worker is not None
52
+
53
+ @property
54
+ def has_statements(self) -> bool:
55
+ return self.statements is not None
56
+
57
+
58
+ # Preserve imports of the former plain-string result type while making mapped
59
+ # projections authoritative.
60
+ NotebookCell = NotebookCellProjection
61
+
62
+
63
+ @dataclass(frozen=True, slots=True)
64
+ class _ProjectionItem:
65
+ ordinal: int
66
+ node: Any
67
+ end: int
68
+
69
+
70
+ def split_notebook_cell(
71
+ parser_target: PythonParserTarget,
72
+ source: str,
73
+ *,
74
+ source_unit: SourceUnitRef | None = None,
75
+ ) -> NotebookCellProjection:
76
+ """Split a generated cell AST into independent mapped execution branches."""
77
+ unit = source_unit or SourceUnitRef(
78
+ SourceUnitKind.NOTEBOOK_CELL,
79
+ "anonymous-notebook-cell",
80
+ 0,
81
+ source_sha256(source),
82
+ )
83
+ visible = mapped_visible_source(source, unit)
84
+ root = parser_target.parse_ast(source, "ЯчейкаНоутбука")
85
+ tokens = tokenize(source)
86
+ token_starts = tuple(token.start for token in tokens)
87
+ elements = root.Elements
88
+ ordered: list[Any] = []
89
+ while elements is not None and elements.Item is not None:
90
+ ordered.append(elements.Item)
91
+ elements = elements.Rest
92
+
93
+ method_items: list[_ProjectionItem] = []
94
+ statement_items: list[_ProjectionItem] = []
95
+ exports: dict[str, WorkerExport] = {}
96
+ for ordinal, item in enumerate(ordered):
97
+ following_start = (
98
+ ordered[ordinal + 1].span.start
99
+ if ordinal + 1 < len(ordered)
100
+ else len(source)
101
+ )
102
+ projected = _ProjectionItem(
103
+ ordinal,
104
+ item,
105
+ _projection_end(item.span.end, following_start, tokens, token_starts),
106
+ )
107
+ if type(item).__name__ == "Method":
108
+ declaration = item.Declaration
109
+ export = WorkerExport(declaration.Name, declaration.Name)
110
+ key = export.public_path.casefold()
111
+ if key in exports:
112
+ raise ProtocolError(
113
+ f"Notebook cell declares duplicate method {declaration.Name!r}"
114
+ )
115
+ exports[key] = export
116
+ method_items.append(projected)
117
+ else:
118
+ statement_items.append(projected)
119
+
120
+ return NotebookCellProjection(
121
+ visible,
122
+ _build_projection(
123
+ visible,
124
+ method_items,
125
+ tokens,
126
+ SourceArtifactKind.WORKER_PROJECTION,
127
+ ),
128
+ _build_projection(
129
+ visible,
130
+ statement_items,
131
+ tokens,
132
+ SourceArtifactKind.STATEMENT_PROJECTION,
133
+ ),
134
+ tuple(exports.values()),
135
+ tuple(
136
+ NotebookMethodProjection(
137
+ export,
138
+ _build_method_projection(visible, item, tokens),
139
+ )
140
+ for export, item in zip(exports.values(), method_items, strict=True)
141
+ ),
142
+ )
143
+
144
+
145
+ def _build_method_projection(
146
+ visible: MappedSource,
147
+ item: _ProjectionItem,
148
+ tokens: tuple[Any, ...],
149
+ ) -> MappedSource:
150
+ builder = SourceTransformBuilder(visible)
151
+ # The optional semicolon belongs to the notebook element separator, not
152
+ # the method AST. In a composed Worker it starts the module body and makes
153
+ # every subsequently appended declaration illegal on the platform.
154
+ method = _ProjectionItem(item.ordinal, item.node, item.node.span.end)
155
+ _copy_method(builder, method, tokens)
156
+ return builder.build(SourceArtifactKind.WORKER_PROJECTION)
157
+
158
+
159
+ def _projection_end(
160
+ item_end: int,
161
+ following_start: int,
162
+ tokens: tuple[Any, ...],
163
+ token_starts: tuple[int, ...],
164
+ ) -> int:
165
+ """Include one grammar separator using its generated token span."""
166
+ token_index = bisect_left(token_starts, item_end)
167
+ following = (
168
+ tokens[token_index]
169
+ if token_index < len(tokens) and tokens[token_index].start < following_start
170
+ else None
171
+ )
172
+ return following.end if following is not None and following.type == ";" else item_end
173
+
174
+
175
+ def _build_projection(
176
+ visible: MappedSource,
177
+ items: list[_ProjectionItem],
178
+ tokens: tuple[Any, ...],
179
+ kind: SourceArtifactKind,
180
+ ) -> MappedSource | None:
181
+ if not items:
182
+ return None
183
+ builder = SourceTransformBuilder(visible)
184
+ previous: _ProjectionItem | None = None
185
+ for item in items:
186
+ if previous is not None:
187
+ if item.ordinal == previous.ordinal + 1:
188
+ separator = SourceSpan(previous.end, item.node.span.start)
189
+ if separator.start < separator.end:
190
+ builder.copy(separator)
191
+ else:
192
+ builder.synthetic(
193
+ "\n",
194
+ SourceSpan(item.node.span.start, item.node.span.start),
195
+ "notebook_projection_join",
196
+ )
197
+ if kind is SourceArtifactKind.WORKER_PROJECTION:
198
+ _copy_method(builder, item, tokens)
199
+ else:
200
+ builder.copy(SourceSpan(item.node.span.start, item.end))
201
+ previous = item
202
+ return builder.build(kind)
203
+
204
+
205
+ def _copy_method(
206
+ builder: SourceTransformBuilder,
207
+ item: _ProjectionItem,
208
+ tokens: tuple[Any, ...],
209
+ ) -> None:
210
+ method = item.node
211
+ declaration = method.Declaration
212
+ if declaration.Export is not None:
213
+ builder.copy(SourceSpan(method.span.start, item.end))
214
+ return
215
+ body_start = declaration.Body.span.start
216
+ closing = next(
217
+ (
218
+ token
219
+ for token in reversed(tokens)
220
+ if method.span.start <= token.start < body_start and token.type == ")"
221
+ ),
222
+ None,
223
+ )
224
+ if closing is None:
225
+ raise ProtocolError("Notebook method declaration has no parameter-list terminator")
226
+ builder.copy(SourceSpan(method.span.start, closing.end))
227
+ builder.synthetic(
228
+ " Экспорт",
229
+ SourceSpan(method.span.start, closing.end),
230
+ "notebook_method_export",
231
+ )
232
+ builder.copy(SourceSpan(closing.end, item.end))
@@ -0,0 +1,74 @@
1
+ """Bind free notebook-method reads to the live request-scoped BSL context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from onec_runtime.bsl.lexer import tokenize
6
+ from onec_runtime.bsl.parser_target import PythonParserTarget
7
+ from onec_runtime.bsl.semantic_lowering import SemanticNotebookLowerer, WorkerExport
8
+ from onec_runtime.bsl.source_maps import (
9
+ MappedSource,
10
+ SourceArtifactKind,
11
+ SourceSpan,
12
+ SourceTransformBuilder,
13
+ )
14
+ from onec_runtime.errors import ProtocolError
15
+
16
+
17
+ WORKER_GLOBALS = "__OnecNotebookGlobals"
18
+ _CONTEXT_PREFIX = f"{WORKER_GLOBALS}."
19
+
20
+
21
+ def bind_notebook_method_globals(
22
+ source: MappedSource,
23
+ *,
24
+ context_names: tuple[str, ...],
25
+ exports: tuple[WorkerExport, ...],
26
+ ) -> tuple[MappedSource, tuple[str, ...]]:
27
+ """Qualify persistent roots without changing method-local BSL semantics."""
28
+ if not context_names:
29
+ return source, ()
30
+ if any(
31
+ token.type == "ID" and token.text.casefold() == WORKER_GLOBALS.casefold()
32
+ for token in tokenize(source.text)
33
+ ):
34
+ raise ProtocolError("Notebook method uses a reserved Worker globals name")
35
+ binding = SemanticNotebookLowerer(
36
+ PythonParserTarget.from_generated(),
37
+ context_names=context_names,
38
+ worker_exports=exports,
39
+ platform_globals=("RuntimeContextStoreServer",),
40
+ ).bind_module(source.text)
41
+ references = sorted(
42
+ (
43
+ reference
44
+ for method in binding.method_scopes
45
+ for reference in method.references
46
+ if reference.kind == "persistent"
47
+ ),
48
+ key=lambda reference: reference.span.start,
49
+ )
50
+ if not references:
51
+ return source, ()
52
+
53
+ builder = SourceTransformBuilder(source)
54
+ builder.synthetic(
55
+ f"Перем {WORKER_GLOBALS} Экспорт;\n",
56
+ SourceSpan(0, 0),
57
+ "notebook_method_globals_declaration",
58
+ )
59
+ cursor = 0
60
+ names: dict[str, str] = {}
61
+ for reference in references:
62
+ root = reference.span
63
+ names.setdefault(reference.name.casefold(), reference.name)
64
+ if root.start < cursor:
65
+ raise ValueError("overlapping notebook method bindings")
66
+ builder.copy(SourceSpan(cursor, root.start))
67
+ builder.synthetic(
68
+ _CONTEXT_PREFIX,
69
+ SourceSpan(root.start, root.start),
70
+ "notebook_method_persistent_read",
71
+ )
72
+ cursor = root.start
73
+ builder.copy(SourceSpan(cursor, len(source.text)))
74
+ return builder.build(SourceArtifactKind.WORKER_PROJECTION), tuple(names.values())
@@ -0,0 +1,175 @@
1
+ """Immutable notebook method upserts with original visible source ownership."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ from onec_runtime.bsl.diagnostics import VisibleSourceContext
8
+ from onec_runtime.bsl.lexer import Token, tokenize
9
+ from onec_runtime.bsl.notebook_cells import NotebookCellProjection, NotebookMethodProjection
10
+ from onec_runtime.bsl.semantic_lowering import WorkerExport
11
+ from onec_runtime.bsl.source_maps import (
12
+ MappedSource,
13
+ SourceArtifactKind,
14
+ SourceSpan,
15
+ SourceTransformBuilder,
16
+ SourceUnitRef,
17
+ compose_mapped_sources,
18
+ )
19
+ from onec_runtime.errors import ProtocolError
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class _RetainedMethod:
24
+ projection: NotebookMethodProjection = field(repr=False)
25
+ visible: MappedSource = field(repr=False)
26
+
27
+
28
+ @dataclass(frozen=True, slots=True)
29
+ class NotebookMethodSet:
30
+ """A candidate value; publication and commitment belong to the runtime."""
31
+
32
+ mapped_source: MappedSource = field(repr=False)
33
+ exports: tuple[WorkerExport, ...]
34
+ intercepts_messages: bool
35
+ _methods: tuple[_RetainedMethod, ...] = field(repr=False)
36
+ _visible_sources: tuple[MappedSource, ...] = field(repr=False)
37
+ bound_globals: tuple[str, ...] = ()
38
+
39
+ @property
40
+ def visible_source_context(self) -> VisibleSourceContext:
41
+ # VisibleSourceContext is mutable. Hand out a detached index rather than
42
+ # allowing consumers to alter the retained candidate's diagnostic state.
43
+ return VisibleSourceContext(
44
+ {_visible_unit(source): source.text for source in self._visible_sources}
45
+ )
46
+
47
+
48
+ def _visible_unit(source: MappedSource) -> SourceUnitRef:
49
+ unit = source.source_map.map_offset(0).unit
50
+ if source.artifact.kind is not SourceArtifactKind.VISIBLE or unit is None:
51
+ raise ProtocolError("Notebook method origin must be a visible source unit")
52
+ return unit
53
+
54
+
55
+ def _unique_visible_sources(sources: tuple[MappedSource, ...]) -> tuple[MappedSource, ...]:
56
+ retained: dict[tuple[object, str, int], MappedSource] = {}
57
+ for source in sources:
58
+ unit = _visible_unit(source)
59
+ key = (unit.kind, unit.unit_id, unit.revision)
60
+ existing = retained.get(key)
61
+ if existing is not None and _visible_unit(existing).source_sha256 != unit.source_sha256:
62
+ raise ProtocolError("Notebook methods have conflicting source identities")
63
+ retained[key] = source
64
+ return tuple(retained.values())
65
+
66
+
67
+ _MESSAGE_SINK = "__OnecWorkerMessageSink"
68
+ _MESSAGE_HELPER = "__OnecWorkerMessage"
69
+
70
+
71
+ def _message_call_tokens(source: str) -> tuple[Token, ...]:
72
+ tokens = tokenize(source)
73
+ return tuple(
74
+ token
75
+ for index, token in enumerate(tokens)
76
+ if token.type == "ID"
77
+ and token.text.casefold() in {"сообщить", "message"}
78
+ and index + 1 < len(tokens)
79
+ and tokens[index + 1].type == "("
80
+ and (
81
+ index == 0
82
+ or tokens[index - 1].type not in {".", "ПРОЦЕДУРА", "ФУНКЦИЯ"}
83
+ )
84
+ )
85
+
86
+
87
+ def instrument_notebook_worker_messages(source: MappedSource) -> MappedSource:
88
+ """Route notebook method messages to the active cell without losing origins."""
89
+ calls = _message_call_tokens(source.text)
90
+ if not calls:
91
+ return source
92
+ reserved = {_MESSAGE_SINK.casefold(), _MESSAGE_HELPER.casefold()}
93
+ if any(
94
+ token.type == "ID" and token.text.casefold() in reserved
95
+ for token in tokenize(source.text)
96
+ ):
97
+ raise ProtocolError("Notebook method uses a reserved Worker message name")
98
+ builder = SourceTransformBuilder(source)
99
+ builder.synthetic(
100
+ f"Перем {_MESSAGE_SINK} Экспорт;\n",
101
+ SourceSpan(0, 0),
102
+ "worker_message_sink_declaration",
103
+ )
104
+ position = 0
105
+ for call in calls:
106
+ builder.copy(SourceSpan(position, call.start))
107
+ builder.derived(
108
+ _MESSAGE_HELPER,
109
+ SourceSpan(call.start, call.end),
110
+ "worker_message_call",
111
+ )
112
+ position = call.end
113
+ builder.copy(SourceSpan(position, len(source.text)))
114
+ builder.synthetic(
115
+ "\n\n"
116
+ f"Процедура {_MESSAGE_HELPER}(Текст = Неопределено, Статус = Неопределено)\n"
117
+ f" Если {_MESSAGE_SINK} = Неопределено Тогда\n"
118
+ " Если Статус = Неопределено Тогда\n"
119
+ " Сообщить(Текст);\n"
120
+ " Иначе\n"
121
+ " Сообщить(Текст, Статус);\n"
122
+ " КонецЕсли;\n"
123
+ " Иначе\n"
124
+ f" RuntimeKernelServer.ДобавитьСообщение({_MESSAGE_SINK}, Текст, Статус);\n"
125
+ " КонецЕсли;\n"
126
+ "КонецПроцедуры",
127
+ SourceSpan(len(source.text), len(source.text)),
128
+ "worker_message_helper",
129
+ )
130
+ return builder.build(SourceArtifactKind.WORKER_PROJECTION)
131
+
132
+
133
+ def merge_notebook_methods(
134
+ previous: NotebookMethodSet | None,
135
+ cell: NotebookCellProjection,
136
+ ) -> NotebookMethodSet:
137
+ """Replace declared names in place, append new names, retain omitted names."""
138
+ if previous is not None and not isinstance(previous, NotebookMethodSet):
139
+ raise ProtocolError("Previous notebook methods must be a NotebookMethodSet")
140
+ if not isinstance(cell, NotebookCellProjection):
141
+ raise ProtocolError("Notebook methods require a parsed cell projection")
142
+ if tuple(method.export for method in cell.methods) != cell.exports:
143
+ raise ProtocolError("Notebook method projections do not match their exports")
144
+ # Check before replacement: even replacing the last old method cannot reuse
145
+ # its explicit identity with a different hash.
146
+ _unique_visible_sources(
147
+ (() if previous is None else previous._visible_sources) + (cell.visible,)
148
+ )
149
+ methods = {
150
+ method.projection.export.method.casefold(): method
151
+ for method in (() if previous is None else previous._methods)
152
+ }
153
+ declared: set[str] = set()
154
+ for projection in cell.methods:
155
+ key = projection.export.method.casefold()
156
+ if key in declared:
157
+ raise ProtocolError("Notebook cell declares duplicate methods")
158
+ declared.add(key)
159
+ methods[key] = _RetainedMethod(projection, cell.visible)
160
+ retained = tuple(methods.values())
161
+ visible_sources = _unique_visible_sources(
162
+ tuple(method.visible for method in retained) + (cell.visible,)
163
+ )
164
+ mapped = compose_mapped_sources(
165
+ tuple(method.projection.mapped_source for method in retained),
166
+ visible_sources=visible_sources,
167
+ kind=SourceArtifactKind.WORKER_PROJECTION,
168
+ )
169
+ return NotebookMethodSet(
170
+ mapped,
171
+ tuple(method.projection.export for method in retained),
172
+ bool(_message_call_tokens(mapped.text)),
173
+ retained,
174
+ visible_sources,
175
+ )
@@ -0,0 +1,119 @@
1
+ """Canonical provenance for consumer-owned generated parser artifacts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from hashlib import sha256
7
+ import json
8
+ from pathlib import Path
9
+ from typing import TypeAlias
10
+
11
+
12
+ ManifestOptionValue: TypeAlias = bool | int | str
13
+ ManifestOptions: TypeAlias = tuple[tuple[str, ManifestOptionValue], ...]
14
+
15
+
16
+ def parsergen_package_sha256(parsergen_source: Path) -> str:
17
+ """Hash the normalized Python inputs for one parsergen package checkout."""
18
+ package = parsergen_source / "parsergen"
19
+ digest = sha256()
20
+ for source in sorted(package.rglob("*.py"), key=lambda path: path.as_posix()):
21
+ relative = source.relative_to(parsergen_source).as_posix().encode("utf-8")
22
+ digest.update(relative)
23
+ digest.update(b"\0")
24
+ normalized = source.read_bytes().replace(b"\r\n", b"\n").replace(b"\r", b"\n")
25
+ digest.update(normalized)
26
+ digest.update(b"\0")
27
+ return digest.hexdigest()
28
+
29
+
30
+ def _canonical_json(value: object) -> str:
31
+ return json.dumps(
32
+ value,
33
+ ensure_ascii=False,
34
+ sort_keys=True,
35
+ separators=(",", ":"),
36
+ )
37
+
38
+
39
+ @dataclass(frozen=True, slots=True)
40
+ class ParserArtifactManifest:
41
+ """Every consumer input that distinguishes one parser artifact from another."""
42
+
43
+ schema_version: int
44
+ backend_id: str
45
+ artifact_role: str
46
+ grammar_source_sha256: str
47
+ parsergen_package_sha256: str | None
48
+ entrypoints: tuple[tuple[str, str], ...]
49
+ lookahead: int
50
+ production_names: tuple[str, ...]
51
+ optimizer_options: ManifestOptions
52
+ codegen_options: ManifestOptions
53
+ identity_sha256: str
54
+
55
+ def to_json(self) -> str:
56
+ return _canonical_json(
57
+ {
58
+ "schema_version": self.schema_version,
59
+ "backend_id": self.backend_id,
60
+ "artifact_role": self.artifact_role,
61
+ "grammar_source_sha256": self.grammar_source_sha256,
62
+ "parsergen_package_sha256": self.parsergen_package_sha256,
63
+ "entrypoints": self.entrypoints,
64
+ "lookahead": self.lookahead,
65
+ "production_names": self.production_names,
66
+ "optimizer_options": self.optimizer_options,
67
+ "codegen_options": self.codegen_options,
68
+ "identity_sha256": self.identity_sha256,
69
+ }
70
+ )
71
+
72
+
73
+ def build_parser_artifact_manifest(
74
+ *,
75
+ backend_id: str,
76
+ artifact_role: str,
77
+ grammar_bytes: bytes,
78
+ parsergen_package_sha256: str | None,
79
+ entrypoints: tuple[tuple[str, str], ...],
80
+ lookahead: int,
81
+ production_names: tuple[str, ...],
82
+ optimizer_options: ManifestOptions,
83
+ codegen_options: ManifestOptions,
84
+ ) -> ParserArtifactManifest:
85
+ """Build an identity from raw inputs before any parsing normalization."""
86
+ payload = {
87
+ "schema_version": 2,
88
+ "backend_id": backend_id,
89
+ "artifact_role": artifact_role,
90
+ "grammar_source_sha256": sha256(grammar_bytes).hexdigest(),
91
+ "parsergen_package_sha256": parsergen_package_sha256,
92
+ "entrypoints": entrypoints,
93
+ "lookahead": lookahead,
94
+ "production_names": production_names,
95
+ "optimizer_options": optimizer_options,
96
+ "codegen_options": codegen_options,
97
+ }
98
+ return ParserArtifactManifest(
99
+ **payload,
100
+ identity_sha256=sha256(_canonical_json(payload).encode("utf-8")).hexdigest(),
101
+ )
102
+
103
+
104
+ def verify_parser_artifact_manifest(raw: str) -> ParserArtifactManifest:
105
+ """Decode a manifest only when its recorded consumer identity is authentic."""
106
+ decoded = json.loads(raw)
107
+ if decoded.get("schema_version") != 2:
108
+ raise ValueError("unsupported parser artifact manifest schema")
109
+ recorded_identity = decoded.pop("identity_sha256")
110
+ expected_identity = sha256(_canonical_json(decoded).encode("utf-8")).hexdigest()
111
+ if recorded_identity != expected_identity:
112
+ raise ValueError("generated parser artifact identity is invalid")
113
+ decoded["entrypoints"] = tuple(tuple(item) for item in decoded["entrypoints"])
114
+ decoded["production_names"] = tuple(decoded["production_names"])
115
+ decoded["optimizer_options"] = tuple(
116
+ tuple(item) for item in decoded["optimizer_options"]
117
+ )
118
+ decoded["codegen_options"] = tuple(tuple(item) for item in decoded["codegen_options"])
119
+ return ParserArtifactManifest(**decoded, identity_sha256=recorded_identity)