tigrbl-kernel 0.4.2.dev4__py3-none-any.whl → 0.4.3.dev4__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.
tigrbl_kernel/__init__.py CHANGED
@@ -3,19 +3,6 @@ from __future__ import annotations
3
3
  from importlib import import_module
4
4
  from typing import Any, Dict, List, Mapping
5
5
 
6
- try: # pragma: no cover - additive optional integration
7
- _runtime_rust = import_module("tigrbl_runtime.rust")
8
- ExecutionBackend = _runtime_rust.ExecutionBackend
9
- RustBackendConfig = _runtime_rust.RustBackendConfig
10
- except Exception: # pragma: no cover
11
- ExecutionBackend = RustBackendConfig = None
12
-
13
- from .rust_compile import (
14
- build_rust_kernel,
15
- build_rust_parity_snapshot,
16
- normalize_rust_spec,
17
- )
18
- from .rust_plan import RustPlan
19
6
  from .measure import (
20
7
  build_packed_kernel_measurement_view,
21
8
  load_packed_kernel_hot_block,
@@ -80,18 +67,13 @@ def plan_labels(model: type, alias: str) -> list[str]:
80
67
 
81
68
 
82
69
  __all__ = [
83
- "ExecutionBackend",
84
70
  "Kernel",
85
- "RustBackendConfig",
86
- "RustPlan",
87
71
  "BatchOpPlan",
88
72
  "OpView",
89
73
  "PackedKernel",
90
74
  "SchemaIn",
91
75
  "SchemaOut",
92
76
  "build_kernel_plan",
93
- "build_rust_kernel",
94
- "build_rust_parity_snapshot",
95
77
  "build_packed_kernel",
96
78
  "build_packed_kernel_measurement_view",
97
79
  "get_cached_specs",
@@ -99,9 +81,9 @@ __all__ = [
99
81
  "load_packed_kernel_hot_block",
100
82
  "load_packed_kernel_hot_sections",
101
83
  "measure_packed_kernel",
102
- "normalize_rust_spec",
103
84
  "packed_kernel_measurement",
104
85
  "plan_labels",
86
+ "protocol_streams",
105
87
  "serialize_packed_kernel_measurement_view",
106
88
  "segment_fusion",
107
89
  "transport_atoms",
tigrbl_kernel/_build.py CHANGED
@@ -15,7 +15,7 @@ from tigrbl_atoms.atoms.sys.phase_db import run as _bind_phase_db
15
15
  from tigrbl_atoms.phases import phase_info
16
16
  from tigrbl_atoms.types import EdgeTarget, PhaseTreeEdge, PhaseTreeNode, error_phase_for
17
17
  from tigrbl_core.config.resolver import resolve_cfg
18
- from tigrbl_typing.phases import normalize_phase
18
+ from tigrbl_typing.phases import canonicalize_phase_input as normalize_phase
19
19
 
20
20
  from . import events as _ev
21
21
  from .atoms import (
tigrbl_kernel/_compile.py CHANGED
@@ -1,10 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import replace
4
+ from types import SimpleNamespace
4
5
  from typing import Any, Mapping
5
6
 
6
7
  from tigrbl_atoms import StepFn
8
+ from tigrbl_core._spec.binding_spec import HttpRestBindingSpec
7
9
  from tigrbl_core.config.constants import __JSONRPC_DEFAULT_ENDPOINT__
10
+ from tigrbl_core._spec.op_spec import OpSpec
11
+ from tigrbl_core._spec.well_known_spec import well_known_op_alias
8
12
 
9
13
  from . import events as _ev
10
14
  from .models import KernelPlan, OpKey, OpMeta, OpView
@@ -24,6 +28,101 @@ from .utils import (
24
28
  DEFAULT_PHASE_ORDER = tuple(getattr(_ev, "PHASES", ())) or _DEFAULT_PHASE_ORDER
25
29
 
26
30
 
31
+ def _pathspec_iter(app: Any) -> tuple[Any, ...]:
32
+ collected: list[Any] = []
33
+ collected.extend(tuple(getattr(app, "_tigrbl_path_specs", ()) or ()))
34
+ routers = getattr(app, "routers", None)
35
+ if isinstance(routers, Mapping):
36
+ router_values = tuple(routers.values())
37
+ elif isinstance(routers, (list, tuple)):
38
+ router_values = tuple(routers)
39
+ else:
40
+ router_values = ()
41
+ for router in router_values:
42
+ collected.extend(tuple(getattr(router, "_tigrbl_path_specs", ()) or ()))
43
+
44
+ seen: set[tuple[str, str, str]] = set()
45
+ deduped: list[Any] = []
46
+ for path in collected:
47
+ resource = getattr(path, "well_known", None)
48
+ key = (
49
+ str(getattr(path, "kind", "")),
50
+ str(getattr(path, "path", "")),
51
+ str(getattr(resource, "name", "")),
52
+ )
53
+ if key in seen:
54
+ continue
55
+ seen.add(key)
56
+ deduped.append(path)
57
+ return tuple(deduped)
58
+
59
+
60
+ def _well_known_path_model(app: Any) -> type | None:
61
+ pathspecs = tuple(
62
+ path
63
+ for path in _pathspec_iter(app)
64
+ if getattr(path, "kind", None) == "well-known"
65
+ and getattr(path, "well_known", None) is not None
66
+ )
67
+ if not pathspecs:
68
+ return None
69
+
70
+ ops: list[Any] = []
71
+ resources: dict[str, dict[str, Any]] = {}
72
+ for path in pathspecs:
73
+ resource = path.well_known
74
+ alias = well_known_op_alias(resource.name)
75
+ resources[alias] = {
76
+ "name": resource.name,
77
+ "payload": resource.payload,
78
+ "media_type": resource.media_type,
79
+ "status_code": resource.status_code,
80
+ "headers": dict(resource.headers or {}),
81
+ }
82
+ ops.append(
83
+ OpSpec(
84
+ alias=alias,
85
+ target="well_known",
86
+ arity="collection",
87
+ persist="skip",
88
+ expose_routes=False,
89
+ expose_rpc=False,
90
+ bindings=(
91
+ HttpRestBindingSpec(
92
+ proto="http.rest",
93
+ path=str(path.path),
94
+ methods=("GET",),
95
+ ),
96
+ ),
97
+ status_code=resource.status_code,
98
+ exchange="request_response",
99
+ tx_scope="none",
100
+ )
101
+ )
102
+
103
+ ops_tuple = tuple(ops)
104
+ model = type("TigrblWellKnownPathOps", (), {})
105
+ model.resource_name = "well_known"
106
+ model.__tigrbl_well_known_resources__ = resources
107
+ model.__tigrbl_ops__ = ops_tuple
108
+ model.ops = SimpleNamespace(
109
+ all=ops_tuple,
110
+ by_alias={op.alias: (op,) for op in ops_tuple},
111
+ by_key={(op.alias, op.target): op for op in ops_tuple},
112
+ )
113
+ model.opspecs = model.ops
114
+ setattr(app, "_tigrbl_kernel_well_known_model", model)
115
+ return model
116
+
117
+
118
+ def _compile_models(app: Any) -> tuple[Any, ...]:
119
+ models = list(_table_iter(app))
120
+ well_known_model = _well_known_path_model(app)
121
+ if well_known_model is not None:
122
+ models.append(well_known_model)
123
+ return tuple(models)
124
+
125
+
27
126
  def _compile_opview_from_specs(self: Any, specs: Mapping[str, Any], sp: Any) -> OpView:
28
127
  return compile_opview_from_specs(specs, sp)
29
128
 
@@ -50,7 +149,7 @@ def _compile_plan(self: Any, app: Any) -> KernelPlan:
50
149
  egress_chain = self._build_egress(app)
51
150
  phases, mainline_phases, error_phases = _phase_info_map(DEFAULT_PHASE_ORDER)
52
151
 
53
- for model in _table_iter(app):
152
+ for model in _compile_models(app):
54
153
  for sp in _opspecs(model):
55
154
  meta_index = len(opmeta)
56
155
  target = (getattr(sp, "target", sp.alias) or sp.alias).lower()
@@ -0,0 +1,142 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable, Mapping
4
+ from typing import Any
5
+
6
+ from tigrbl_core._spec.hook_spec import matches_hook_selector
7
+
8
+ from .webtransport_events import validate_webtransport_event_payload
9
+
10
+ WEBTRANSPORT_SUBEVENTS = {
11
+ "webtransport.connect": "session.open",
12
+ "webtransport.accept": "session.accept",
13
+ "webtransport.disconnect": "session.close",
14
+ "webtransport.close": "session.close",
15
+ "webtransport.stream.receive": "stream.chunk.received",
16
+ "webtransport.stream.send": "stream.chunk.emit",
17
+ "webtransport.stream.close": "stream.close",
18
+ "webtransport.stream.reset": "stream.close",
19
+ "webtransport.stream.stop_sending": "stream.close",
20
+ "webtransport.datagram.receive": "datagram.received",
21
+ "webtransport.datagram.send": "datagram.emit",
22
+ }
23
+
24
+
25
+ def normalize_exchange(exchange: str | None) -> str:
26
+ token = str(exchange or "request_response")
27
+ if token == "bidirectional":
28
+ return "bidirectional_stream"
29
+ return token
30
+
31
+
32
+ def channel_family(scope_type: str, exchange: str) -> str:
33
+ exchange = normalize_exchange(exchange)
34
+ if scope_type == "websocket":
35
+ return "socket"
36
+ if scope_type == "webtransport":
37
+ return "session"
38
+ if exchange in {"server_stream", "client_stream", "bidirectional_stream"}:
39
+ return "stream"
40
+ if exchange == "fire_and_forget":
41
+ return "request"
42
+ return "response"
43
+
44
+
45
+ def channel_kind(scope_type: str, exchange: str) -> str:
46
+ exchange = normalize_exchange(exchange)
47
+ if scope_type == "websocket":
48
+ return "websocket"
49
+ if scope_type == "webtransport":
50
+ return "webtransport"
51
+ if exchange in {"server_stream", "client_stream", "bidirectional_stream"}:
52
+ return "stream"
53
+ if exchange == "event_stream":
54
+ return "sse"
55
+ return "http"
56
+
57
+
58
+ def channel_subevents(scope_type: str, exchange: str) -> tuple[str, ...]:
59
+ exchange = normalize_exchange(exchange)
60
+ if scope_type in {"websocket", "webtransport"}:
61
+ return ("connect", "receive", "emit", "complete", "disconnect")
62
+ if exchange in {"server_stream", "client_stream", "bidirectional_stream", "event_stream"}:
63
+ return ("receive", "emit", "complete")
64
+ return ("receive", "emit", "complete")
65
+
66
+
67
+ def webtransport_event_metadata(
68
+ *,
69
+ direction: str,
70
+ message: Mapping[str, Any],
71
+ ) -> dict[str, Any]:
72
+ event_type = str(message.get("type") or "")
73
+ metadata: dict[str, Any] = {
74
+ "binding": "webtransport",
75
+ "event_type": event_type,
76
+ "type": event_type,
77
+ "subevent": WEBTRANSPORT_SUBEVENTS.get(event_type, event_type),
78
+ "framing": message.get("framing"),
79
+ }
80
+ try:
81
+ projection = validate_webtransport_event_payload(
82
+ event=event_type,
83
+ channel=direction,
84
+ payload=dict(message),
85
+ )
86
+ except ValueError:
87
+ projection = {}
88
+ for key in (
89
+ "family",
90
+ "lane",
91
+ "exchange",
92
+ "stream_initiator",
93
+ "stream_direction",
94
+ "direction",
95
+ "lane_id",
96
+ "stream_ordinal",
97
+ "stream_id_width",
98
+ ):
99
+ if projection.get(key) is not None:
100
+ metadata[key] = projection[key]
101
+ if "family" not in metadata:
102
+ if ".stream." in event_type:
103
+ metadata["family"] = "stream"
104
+ elif ".datagram." in event_type:
105
+ metadata["family"] = "datagram"
106
+ else:
107
+ metadata["family"] = "session"
108
+ if "lane" not in metadata:
109
+ metadata["lane"] = metadata["family"]
110
+ if "exchange" not in metadata:
111
+ metadata["exchange"] = "request_response"
112
+ return metadata
113
+
114
+
115
+ def select_webtransport_hooks(
116
+ hooks: Iterable[Any],
117
+ *,
118
+ direction: str,
119
+ metadata: Mapping[str, Any],
120
+ ) -> tuple[Any, ...]:
121
+ allowed_phases = (
122
+ {"PRE_HANDLER", "HANDLER"}
123
+ if direction == "receive"
124
+ else {"POST_HANDLER", "POST_RESPONSE"}
125
+ )
126
+ return tuple(
127
+ hook
128
+ for hook in hooks
129
+ if str(getattr(hook.phase, "value", hook.phase)) in allowed_phases
130
+ if matches_hook_selector(hook, metadata)
131
+ )
132
+
133
+
134
+ __all__ = [
135
+ "WEBTRANSPORT_SUBEVENTS",
136
+ "channel_family",
137
+ "channel_kind",
138
+ "channel_subevents",
139
+ "normalize_exchange",
140
+ "select_webtransport_hooks",
141
+ "webtransport_event_metadata",
142
+ ]
@@ -0,0 +1,167 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable, Mapping
4
+ from dataclasses import dataclass
5
+ from hashlib import sha256
6
+ from typing import Any
7
+
8
+ from tigrbl_kernel.protocol_bindings import compile_binding_protocol_plan
9
+
10
+
11
+ TRANSPORT_ENVELOPE_KEYS = frozenset(
12
+ {
13
+ "headers",
14
+ "http_status",
15
+ "jsonrpc_id",
16
+ "jsonrpc_version",
17
+ "path",
18
+ "rpc_method",
19
+ "stream_id",
20
+ "transport",
21
+ "ws_opcode",
22
+ }
23
+ )
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class EquivalenceCase:
28
+ op_id: str
29
+ category: str
30
+ bindings: tuple[Mapping[str, Any], ...]
31
+
32
+
33
+ def canonical_operation_id(op_id: str) -> str:
34
+ normalized = str(op_id).strip()
35
+ if not normalized or "." not in normalized:
36
+ raise ValueError("canonical operation id must be a non-empty table.operation token")
37
+ table, op = normalized.split(".", maxsplit=1)
38
+ if not table or not op:
39
+ raise ValueError("canonical operation id requires table and operation components")
40
+ return f"{table}.{op}"
41
+
42
+
43
+ def compile_equivalence_manifest(
44
+ op_id: str,
45
+ bindings: Iterable[Mapping[str, Any]],
46
+ *,
47
+ schema_identity: str = "schema:v1",
48
+ runtime_plan_identity: str = "runtime-plan:v1",
49
+ ) -> dict[str, Any]:
50
+ canonical_id = canonical_operation_id(op_id)
51
+ plans = tuple(compile_binding_protocol_plan(canonical_id, binding) for binding in bindings)
52
+ if not plans:
53
+ raise ValueError("at least one transport binding is required")
54
+ return {
55
+ "op_id": canonical_id,
56
+ "schema_identity": str(schema_identity),
57
+ "runtime_plan_identity": str(runtime_plan_identity),
58
+ "bindings": tuple(_plan_fingerprint(plan) for plan in plans),
59
+ }
60
+
61
+
62
+ def normalized_transport_result(result: Mapping[str, Any]) -> dict[str, Any]:
63
+ semantic = {
64
+ key: _freeze(value)
65
+ for key, value in result.items()
66
+ if key not in TRANSPORT_ENVELOPE_KEYS
67
+ }
68
+ if "diagnostics" in semantic and isinstance(semantic["diagnostics"], Mapping):
69
+ diagnostics = dict(semantic["diagnostics"])
70
+ diagnostics.pop("trace_id", None)
71
+ diagnostics.pop("qlog_id", None)
72
+ semantic["diagnostics"] = _freeze(diagnostics)
73
+ return dict(sorted(semantic.items()))
74
+
75
+
76
+ def equivalent_transport_results(*results: Mapping[str, Any]) -> bool:
77
+ if not results:
78
+ raise ValueError("at least one transport result is required")
79
+ first = normalized_transport_result(results[0])
80
+ return all(normalized_transport_result(result) == first for result in results[1:])
81
+
82
+
83
+ def equivalent_binding_group(op_id: str, bindings: Iterable[Mapping[str, Any]]) -> bool:
84
+ manifest = compile_equivalence_manifest(op_id, bindings)
85
+ families = {binding["family"] for binding in manifest["bindings"]}
86
+ return bool(families) and len({manifest["op_id"]}) == 1
87
+
88
+
89
+ def standard_equivalence_corpus() -> tuple[EquivalenceCase, ...]:
90
+ return (
91
+ EquivalenceCase(
92
+ "Item.read",
93
+ "standard",
94
+ (
95
+ {"kind": "http.rest", "path": "/items/{id}", "methods": ("GET",)},
96
+ {"kind": "http.jsonrpc", "rpc_method": "Item.read"},
97
+ {"kind": "ws", "path": "/socket", "framing": "jsonrpc", "subprotocols": ("jsonrpc",)},
98
+ ),
99
+ ),
100
+ EquivalenceCase(
101
+ "Item.bulk_create",
102
+ "bulk",
103
+ (
104
+ {"kind": "http.rest", "path": "/items/bulk", "methods": ("POST",)},
105
+ {"kind": "http.jsonrpc", "rpc_method": "Item.bulk_create"},
106
+ ),
107
+ ),
108
+ EquivalenceCase(
109
+ "Item.search",
110
+ "query",
111
+ (
112
+ {"kind": "http.rest", "path": "/items", "methods": ("GET",)},
113
+ {"kind": "http.jsonrpc", "rpc_method": "Item.search"},
114
+ ),
115
+ ),
116
+ EquivalenceCase(
117
+ "Item.tail",
118
+ "stream",
119
+ (
120
+ {"kind": "http.stream", "path": "/items/tail"},
121
+ {"kind": "http.sse", "path": "/items/events"},
122
+ {"kind": "webtransport", "profile": "bidi_stream"},
123
+ ),
124
+ ),
125
+ EquivalenceCase(
126
+ "Item.custom_action",
127
+ "custom",
128
+ (
129
+ {"kind": "http.jsonrpc", "rpc_method": "Item.custom_action"},
130
+ ),
131
+ ),
132
+ )
133
+
134
+
135
+ def _plan_fingerprint(plan: Mapping[str, Any]) -> dict[str, Any]:
136
+ stable = {
137
+ "binding_kind": plan["binding_kind"],
138
+ "family": plan["family"],
139
+ "framing": plan["framing"],
140
+ "capability_mask": plan["capability_requirements"]["required_mask"],
141
+ "lifecycle": tuple(
142
+ (row["family"], row["subevent"]) for row in plan["lifecycle_rows"]
143
+ ),
144
+ }
145
+ stable["fingerprint"] = sha256(repr(stable).encode("utf-8")).hexdigest()
146
+ return stable
147
+
148
+
149
+ def _freeze(value: Any) -> Any:
150
+ if isinstance(value, Mapping):
151
+ return {key: _freeze(value[key]) for key in sorted(value)}
152
+ if isinstance(value, list):
153
+ return tuple(_freeze(item) for item in value)
154
+ if isinstance(value, tuple):
155
+ return tuple(_freeze(item) for item in value)
156
+ return value
157
+
158
+
159
+ __all__ = [
160
+ "EquivalenceCase",
161
+ "canonical_operation_id",
162
+ "compile_equivalence_manifest",
163
+ "equivalent_binding_group",
164
+ "equivalent_transport_results",
165
+ "normalized_transport_result",
166
+ "standard_equivalence_corpus",
167
+ ]
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ _BINDINGS = {
6
+ "http.rest": ("request_response", "request_response", "request.received"),
7
+ "http.jsonrpc": ("request_response", "rpc", "rpc.request"),
8
+ "http.stream": ("server_stream", "stream", "stream.start"),
9
+ "http.sse": ("server_stream", "event_stream", "message.emit"),
10
+ "websocket": ("bidirectional_stream", "socket", "message.received"),
11
+ "ws": ("bidirectional_stream", "socket", "message.received"),
12
+ }
13
+
14
+
15
+ def derive_runtime_event(event: dict[str, Any]) -> dict[str, Any]:
16
+ subevent = event.get("subevent")
17
+ if isinstance(subevent, str) and "." not in subevent:
18
+ raise ValueError("subevent must be qualified, for example request.received")
19
+ binding = str(event.get("binding", ""))
20
+ try:
21
+ exchange, family, default_subevent = _BINDINGS[binding]
22
+ except KeyError as exc:
23
+ raise ValueError(
24
+ f"unsupported binding for exchange/subevent dispatch: {binding}"
25
+ ) from exc
26
+ return {
27
+ **event,
28
+ "binding": binding,
29
+ "exchange": exchange,
30
+ "family": family,
31
+ "subevent": subevent or default_subevent,
32
+ }
33
+
34
+
35
+ def resolve_operation(metadata: dict[str, Any]) -> dict[str, Any]:
36
+ binding = str(metadata.get("binding", ""))
37
+ subevent = str(metadata.get("subevent", ""))
38
+ method = str(metadata.get("method", metadata.get("path", "")))
39
+ return {
40
+ "exchange": metadata["exchange"],
41
+ "family": metadata["family"],
42
+ "subevent": subevent,
43
+ "op_code": f"{binding}:{subevent}:{method}".strip(":"),
44
+ }
45
+
46
+
47
+ __all__ = ["derive_runtime_event", "resolve_operation"]
tigrbl_kernel/events.py CHANGED
@@ -4,3 +4,13 @@ Canonical event anchors and ordering now live in ``tigrbl_atoms.events``.
4
4
  """
5
5
 
6
6
  from tigrbl_atoms.events import * # noqa: F403
7
+ from tigrbl_atoms.events import __all__ as _ATOM_EVENT_EXPORTS
8
+ from tigrbl_atoms.events import canonicalize_phase
9
+
10
+
11
+ def is_error_phase(name: str) -> bool:
12
+ phase = canonicalize_phase(name)
13
+ return str(phase).startswith("ON_") or phase == "TX_ROLLBACK"
14
+
15
+
16
+ __all__ = [*_ATOM_EVENT_EXPORTS, "is_error_phase"]
tigrbl_kernel/helpers.py CHANGED
@@ -6,7 +6,7 @@ import logging
6
6
  from typing import Any, Iterable, Mapping, Optional, Sequence
7
7
 
8
8
  from tigrbl_atoms.types import AtomFailure, BaseCtx, FailedCtx, build_error_ctx
9
- from tigrbl_typing.phases import normalize_phase
9
+ from tigrbl_typing.phases import canonicalize_phase_input
10
10
 
11
11
  try:
12
12
  from tigrbl_kernel import trace as _trace # type: ignore
@@ -210,7 +210,7 @@ async def _run_chain(ctx: Any, chain: Optional[Iterable[Any]], *, phase: str) ->
210
210
  def _g(phases: Optional[Mapping[str, Sequence[Any]]], key: str) -> Sequence[Any]:
211
211
  if not phases:
212
212
  return ()
213
- normalized = normalize_phase(key) or key
213
+ normalized = canonicalize_phase_input(key) or key
214
214
  chain = phases.get(normalized, ())
215
215
  if chain:
216
216
  return chain
@@ -31,4 +31,22 @@ def select_loop_mode(
31
31
  return "dispatch" if handlers else "owner"
32
32
 
33
33
 
34
- __all__ = ["select_loop_mode"]
34
+ def build_loop_controller(
35
+ *,
36
+ mode: str,
37
+ binding: str,
38
+ subevent_handlers: Iterable[str] = (),
39
+ ) -> dict[str, object]:
40
+ if mode not in {"owner", "dispatch"}:
41
+ raise ValueError(f"unsupported runtime loop mode {mode!r}")
42
+ handlers = tuple(subevent_handlers or ())
43
+ return {
44
+ "mode": mode,
45
+ "binding": binding,
46
+ "subevent_handlers": handlers,
47
+ "dispatches_subevents": mode == "dispatch",
48
+ "owner_controls_receive": mode == "owner",
49
+ }
50
+
51
+
52
+ __all__ = ["build_loop_controller", "select_loop_mode"]
tigrbl_kernel/measure.py CHANGED
@@ -6,7 +6,7 @@ import struct
6
6
  import zlib
7
7
  from typing import Any, Mapping
8
8
 
9
- from tigrbl_typing.phases import normalize_phase
9
+ from tigrbl_typing.phases import canonicalize_phase_input as normalize_phase
10
10
 
11
11
  from .models import (
12
12
  HotOpPlan,
tigrbl_kernel/ordering.py CHANGED
@@ -57,6 +57,7 @@ _PREF: Dict[str, Tuple[str, ...]] = {
57
57
  "sys:handler_bulk_replace",
58
58
  "sys:handler_bulk_merge",
59
59
  "sys:handler_bulk_delete",
60
+ "sys:handler_well_known",
60
61
  "sys:handler_persistence",
61
62
  ),
62
63
  _ev.BATCH_RESULT_SLOTS: ("batch:result_slots",),