tigrcorn-contract 0.3.16.dev12__tar.gz → 0.3.17.dev1__tar.gz

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 (21) hide show
  1. {tigrcorn_contract-0.3.16.dev12 → tigrcorn_contract-0.3.17.dev1}/PKG-INFO +3 -3
  2. {tigrcorn_contract-0.3.16.dev12 → tigrcorn_contract-0.3.17.dev1}/pyproject.toml +3 -3
  3. {tigrcorn_contract-0.3.16.dev12 → tigrcorn_contract-0.3.17.dev1}/src/tigrcorn_contract/__init__.py +42 -0
  4. {tigrcorn_contract-0.3.16.dev12 → tigrcorn_contract-0.3.17.dev1}/src/tigrcorn_contract/events.py +8 -1
  5. tigrcorn_contract-0.3.17.dev1/src/tigrcorn_contract/registry.py +248 -0
  6. tigrcorn_contract-0.3.17.dev1/src/tigrcorn_contract/semantics.py +348 -0
  7. {tigrcorn_contract-0.3.16.dev12 → tigrcorn_contract-0.3.17.dev1}/src/tigrcorn_contract.egg-info/PKG-INFO +3 -3
  8. {tigrcorn_contract-0.3.16.dev12 → tigrcorn_contract-0.3.17.dev1}/src/tigrcorn_contract.egg-info/SOURCES.txt +2 -0
  9. tigrcorn_contract-0.3.17.dev1/src/tigrcorn_contract.egg-info/requires.txt +3 -0
  10. tigrcorn_contract-0.3.16.dev12/src/tigrcorn_contract.egg-info/requires.txt +0 -3
  11. {tigrcorn_contract-0.3.16.dev12 → tigrcorn_contract-0.3.17.dev1}/LICENSE +0 -0
  12. {tigrcorn_contract-0.3.16.dev12 → tigrcorn_contract-0.3.17.dev1}/README.md +0 -0
  13. {tigrcorn_contract-0.3.16.dev12 → tigrcorn_contract-0.3.17.dev1}/setup.cfg +0 -0
  14. {tigrcorn_contract-0.3.16.dev12 → tigrcorn_contract-0.3.17.dev1}/src/tigrcorn_contract/classification.py +0 -0
  15. {tigrcorn_contract-0.3.16.dev12 → tigrcorn_contract-0.3.17.dev1}/src/tigrcorn_contract/compat.py +0 -0
  16. {tigrcorn_contract-0.3.16.dev12 → tigrcorn_contract-0.3.17.dev1}/src/tigrcorn_contract/metadata.py +0 -0
  17. {tigrcorn_contract-0.3.16.dev12 → tigrcorn_contract-0.3.17.dev1}/src/tigrcorn_contract/projection.py +0 -0
  18. {tigrcorn_contract-0.3.16.dev12 → tigrcorn_contract-0.3.17.dev1}/src/tigrcorn_contract/py.typed +0 -0
  19. {tigrcorn_contract-0.3.16.dev12 → tigrcorn_contract-0.3.17.dev1}/src/tigrcorn_contract/scopes.py +0 -0
  20. {tigrcorn_contract-0.3.16.dev12 → tigrcorn_contract-0.3.17.dev1}/src/tigrcorn_contract.egg-info/dependency_links.txt +0 -0
  21. {tigrcorn_contract-0.3.16.dev12 → tigrcorn_contract-0.3.17.dev1}/src/tigrcorn_contract.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tigrcorn-contract
3
- Version: 0.3.16.dev12
3
+ Version: 0.3.17.dev1
4
4
  Summary: tigr-asgi-contract adapters and runtime-boundary classification for Tigrcorn ASGI3, WebSocket, and WebTransport surfaces.
5
5
  Author-email: Jacob Stewart <jacob@swarmauri.com>
6
6
  License: Apache License
@@ -186,8 +186,8 @@ Classifier: Typing :: Typed
186
186
  Requires-Python: <3.15,>=3.10
187
187
  Description-Content-Type: text/markdown
188
188
  License-File: LICENSE
189
- Requires-Dist: tigrcorn-core==0.3.16.dev12
190
- Requires-Dist: tigrcorn-asgi==0.3.16.dev12
189
+ Requires-Dist: tigrcorn-core==0.3.17.dev1
190
+ Requires-Dist: tigrcorn-asgi==0.3.17.dev1
191
191
  Requires-Dist: tigr-asgi-contract>=0.3.2
192
192
  Dynamic: license-file
193
193
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tigrcorn-contract"
7
- version = "0.3.16.dev12"
7
+ version = "0.3.17.dev1"
8
8
  description = "tigr-asgi-contract adapters and runtime-boundary classification for Tigrcorn ASGI3, WebSocket, and WebTransport surfaces."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10,<3.15"
@@ -29,8 +29,8 @@ classifiers = [
29
29
  "Typing :: Typed",
30
30
  ]
31
31
  dependencies = [
32
- "tigrcorn-core==0.3.16.dev12",
33
- "tigrcorn-asgi==0.3.16.dev12",
32
+ "tigrcorn-core==0.3.17.dev1",
33
+ "tigrcorn-asgi==0.3.17.dev1",
34
34
  "tigr-asgi-contract>=0.3.2",
35
35
  ]
36
36
 
@@ -96,6 +96,29 @@ from .projection import (
96
96
  project_send_event,
97
97
  validate_projected_event,
98
98
  )
99
+ from .semantics import (
100
+ SemanticObservation,
101
+ channel_lifecycle_capabilities,
102
+ enforce_semantic_transition,
103
+ observe_backpressure as observe_contract_backpressure,
104
+ observe_channel_lifecycle,
105
+ observe_completion,
106
+ observe_disconnect,
107
+ observe_transport_cancellation,
108
+ require_channel_readable,
109
+ require_channel_writable,
110
+ )
111
+ from .registry import (
112
+ ContractRecord,
113
+ ContractStability,
114
+ ContractTraceability,
115
+ ContractTraceabilityError,
116
+ TraceabilityStatus,
117
+ contract_records,
118
+ export_contract_registry,
119
+ validate_contract_traceability,
120
+ validate_registry,
121
+ )
99
122
  from .scopes import SUPPORTED_SCOPE_TYPES, contract_scope, validate_scope
100
123
 
101
124
  __all__ = [
@@ -103,6 +126,10 @@ __all__ = [
103
126
  "CompletionLevel",
104
127
  "CompletionStatus",
105
128
  "ConnectionIdentity",
129
+ "ContractRecord",
130
+ "ContractStability",
131
+ "ContractTraceability",
132
+ "ContractTraceabilityError",
106
133
  "EndpointMetadata",
107
134
  "FamilyCapability",
108
135
  "GenericDatagramRuntime",
@@ -112,23 +139,29 @@ __all__ = [
112
139
  "HTTPFeatureContractMap",
113
140
  "SUPPORTED_SCOPE_TYPES",
114
141
  "SecurityMetadata",
142
+ "SemanticObservation",
115
143
  "EventProjection",
116
144
  "ScopeProjection",
117
145
  "StreamIdentity",
146
+ "TraceabilityStatus",
118
147
  "UnitIdentity",
119
148
  "alt_svc_contract_map",
120
149
  "asgi3_extensions",
121
150
  "asgi3_compat_scope",
122
151
  "asgi_extension_bridge",
152
+ "channel_lifecycle_capabilities",
123
153
  "classify_binding",
124
154
  "compatibility_feature_parity",
125
155
  "contract_scope",
156
+ "contract_records",
126
157
  "content_coding_contract_map",
127
158
  "datagram_identity",
128
159
  "datagram_receive",
129
160
  "datagram_send",
130
161
  "emit_complete",
131
162
  "endpoint_metadata",
163
+ "export_contract_registry",
164
+ "enforce_semantic_transition",
132
165
  "early_hints_contract_map",
133
166
  "family_capability",
134
167
  "http_disconnect",
@@ -145,6 +178,11 @@ __all__ = [
145
178
  "lifespan_startup_failed",
146
179
  "map_contract_event",
147
180
  "observability_contract_metadata",
181
+ "observe_contract_backpressure",
182
+ "observe_channel_lifecycle",
183
+ "observe_completion",
184
+ "observe_disconnect",
185
+ "observe_transport_cancellation",
148
186
  "proxy_normalization_contract_map",
149
187
  "product_surface_status",
150
188
  "product_surface_excluded",
@@ -152,6 +190,8 @@ __all__ = [
152
190
  "project_receive_event",
153
191
  "project_scope_classification",
154
192
  "project_send_event",
193
+ "require_channel_readable",
194
+ "require_channel_writable",
155
195
  "require_lossless_metadata",
156
196
  "require_product_boundary_exclusion",
157
197
  "require_product_runtime_available",
@@ -165,12 +205,14 @@ __all__ = [
165
205
  "trailers_contract_map",
166
206
  "unit_identity",
167
207
  "validate_binding_legality",
208
+ "validate_contract_traceability",
168
209
  "validate_connection_identity",
169
210
  "validate_datagram_event",
170
211
  "validate_endpoint_metadata",
171
212
  "validate_event_order",
172
213
  "validate_projected_event",
173
214
  "validate_security_metadata",
215
+ "validate_registry",
174
216
  "validate_scope",
175
217
  "validate_stream_event",
176
218
  "validate_stream_identity",
@@ -47,7 +47,11 @@ WEBTRANSPORT_EVENT_MAP = {
47
47
 
48
48
  class CompletionLevel(StrEnum):
49
49
  ACCEPTED_BY_RUNTIME = "accepted_by_runtime"
50
+ QUEUED_FOR_TRANSPORT = "queued_for_transport"
50
51
  FLUSHED_TO_TRANSPORT = "flushed_to_transport"
52
+ PEER_ACKNOWLEDGED = "peer_acknowledged"
53
+ FAILED_DURING_EMIT = "failed_during_emit"
54
+ ABORTED_BY_PEER = "aborted_by_peer"
51
55
 
52
56
 
53
57
  class CompletionStatus(StrEnum):
@@ -436,9 +440,12 @@ def _normalize_completion_level(level: str) -> str:
436
440
  aliases = {
437
441
  "buffered": CompletionLevel.ACCEPTED_BY_RUNTIME.value,
438
442
  "accepted": CompletionLevel.ACCEPTED_BY_RUNTIME.value,
443
+ "queued": CompletionLevel.QUEUED_FOR_TRANSPORT.value,
439
444
  "flushed": CompletionLevel.FLUSHED_TO_TRANSPORT.value,
440
445
  "transport": CompletionLevel.FLUSHED_TO_TRANSPORT.value,
441
- "acknowledged": CompletionLevel.FLUSHED_TO_TRANSPORT.value,
446
+ "acknowledged": CompletionLevel.PEER_ACKNOWLEDGED.value,
447
+ "failed": CompletionLevel.FAILED_DURING_EMIT.value,
448
+ "aborted": CompletionLevel.ABORTED_BY_PEER.value,
442
449
  }
443
450
  return aliases.get(level, level)
444
451
 
@@ -0,0 +1,248 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from typing import Any, Iterable
6
+
7
+
8
+ class ContractStability(str, Enum):
9
+ EXPERIMENTAL = "experimental"
10
+ PREVIEW = "preview"
11
+ STABLE = "stable"
12
+ CERTIFIED = "certified"
13
+ DEPRECATED = "deprecated"
14
+
15
+
16
+ class TraceabilityStatus(str, Enum):
17
+ COMPLETE = "complete"
18
+ PARTIAL = "partial"
19
+ PROVISIONAL = "provisional"
20
+ FAILED = "failed"
21
+
22
+
23
+ class ContractTraceabilityError(ValueError):
24
+ """Raised when a contract claims certification without required proof."""
25
+
26
+
27
+ @dataclass(frozen=True, slots=True)
28
+ class ContractTraceability:
29
+ rfcs: tuple[str, ...] = ()
30
+ spec_ids: tuple[str, ...] = ()
31
+ implementation_refs: tuple[str, ...] = ()
32
+ test_ids: tuple[str, ...] = ()
33
+ evidence_ids: tuple[str, ...] = ()
34
+ negative_test_ids: tuple[str, ...] = ()
35
+ status: TraceabilityStatus = TraceabilityStatus.PARTIAL
36
+ release_certified: bool = False
37
+
38
+ def as_dict(self) -> dict[str, Any]:
39
+ return {
40
+ "rfcs": list(self.rfcs),
41
+ "spec_ids": list(self.spec_ids),
42
+ "implementation_refs": list(self.implementation_refs),
43
+ "test_ids": list(self.test_ids),
44
+ "evidence_ids": list(self.evidence_ids),
45
+ "negative_test_ids": list(self.negative_test_ids),
46
+ "status": self.status.value,
47
+ "release_certified": self.release_certified,
48
+ }
49
+
50
+
51
+ @dataclass(frozen=True, slots=True)
52
+ class ContractRecord:
53
+ contract_id: str
54
+ version: str
55
+ title: str
56
+ owner_package: str
57
+ owner_module: str
58
+ stability: ContractStability
59
+ implemented: bool
60
+ certified: bool
61
+ traceability: ContractTraceability
62
+ replacement_contract_id: str | None = None
63
+ retirement_note: str | None = None
64
+ tags: tuple[str, ...] = field(default_factory=tuple)
65
+
66
+ def as_dict(self) -> dict[str, Any]:
67
+ payload = {
68
+ "contract_id": self.contract_id,
69
+ "version": self.version,
70
+ "title": self.title,
71
+ "owner_package": self.owner_package,
72
+ "owner_module": self.owner_module,
73
+ "stability": self.stability.value,
74
+ "implemented": self.implemented,
75
+ "certified": self.certified,
76
+ "traceability": self.traceability.as_dict(),
77
+ "tags": list(self.tags),
78
+ }
79
+ if self.replacement_contract_id is not None:
80
+ payload["replacement_contract_id"] = self.replacement_contract_id
81
+ if self.retirement_note is not None:
82
+ payload["retirement_note"] = self.retirement_note
83
+ return payload
84
+
85
+
86
+ def _traceability_status(
87
+ *,
88
+ rfcs: Iterable[str] = (),
89
+ spec_ids: Iterable[str] = (),
90
+ implementation_refs: Iterable[str] = (),
91
+ test_ids: Iterable[str] = (),
92
+ evidence_ids: Iterable[str] = (),
93
+ negative_test_ids: Iterable[str] = (),
94
+ provisional: bool = False,
95
+ ) -> ContractTraceability:
96
+ traceability = ContractTraceability(
97
+ rfcs=tuple(sorted(rfcs)),
98
+ spec_ids=tuple(sorted(spec_ids)),
99
+ implementation_refs=tuple(sorted(implementation_refs)),
100
+ test_ids=tuple(sorted(test_ids)),
101
+ evidence_ids=tuple(sorted(evidence_ids)),
102
+ negative_test_ids=tuple(sorted(negative_test_ids)),
103
+ status=TraceabilityStatus.PROVISIONAL if provisional else TraceabilityStatus.PARTIAL,
104
+ )
105
+ complete = all(
106
+ (
107
+ traceability.rfcs,
108
+ traceability.spec_ids,
109
+ traceability.implementation_refs,
110
+ traceability.test_ids,
111
+ traceability.evidence_ids,
112
+ traceability.negative_test_ids,
113
+ )
114
+ )
115
+ if complete and not provisional:
116
+ return ContractTraceability(
117
+ rfcs=traceability.rfcs,
118
+ spec_ids=traceability.spec_ids,
119
+ implementation_refs=traceability.implementation_refs,
120
+ test_ids=traceability.test_ids,
121
+ evidence_ids=traceability.evidence_ids,
122
+ negative_test_ids=traceability.negative_test_ids,
123
+ status=TraceabilityStatus.COMPLETE,
124
+ release_certified=True,
125
+ )
126
+ return traceability
127
+
128
+
129
+ _CONTRACTS: tuple[ContractRecord, ...] = (
130
+ ContractRecord(
131
+ contract_id="asgi.http.scope",
132
+ version="1.0",
133
+ title="HTTP scope contract",
134
+ owner_package="tigrcorn-contract",
135
+ owner_module="tigrcorn_contract.scopes",
136
+ stability=ContractStability.CERTIFIED,
137
+ implemented=True,
138
+ certified=True,
139
+ traceability=_traceability_status(
140
+ rfcs=("RFC 9110", "RFC 9112"),
141
+ spec_ids=("spc:1021",),
142
+ implementation_refs=("tigrcorn_contract.scopes:contract_scope",),
143
+ test_ids=("tst:contract-http-scope", "tst:contract-unsupported-scope-rejection"),
144
+ evidence_ids=("evd:contract-http-scope-pytest", "evd:contract-unsupported-scope-rejection-pytest"),
145
+ negative_test_ids=("tst:contract-unsupported-scope-rejection",),
146
+ ),
147
+ tags=("http", "scope"),
148
+ ),
149
+ ContractRecord(
150
+ contract_id="asgi.websocket.events",
151
+ version="1.0",
152
+ title="WebSocket event contract",
153
+ owner_package="tigrcorn-contract",
154
+ owner_module="tigrcorn_contract.events",
155
+ stability=ContractStability.STABLE,
156
+ implemented=True,
157
+ certified=False,
158
+ traceability=_traceability_status(
159
+ rfcs=("RFC 6455",),
160
+ spec_ids=("spc:1021",),
161
+ implementation_refs=("tigrcorn_contract.events:websocket_connect",),
162
+ test_ids=("tst:contract-websocket-event-map",),
163
+ evidence_ids=("evd:contract-websocket-event-map-pytest",),
164
+ ),
165
+ tags=("websocket", "events"),
166
+ ),
167
+ ContractRecord(
168
+ contract_id="webtransport.stream.identity",
169
+ version="0.9",
170
+ title="WebTransport stream identity contract",
171
+ owner_package="tigrcorn-contract",
172
+ owner_module="tigrcorn_contract.metadata",
173
+ stability=ContractStability.PREVIEW,
174
+ implemented=True,
175
+ certified=False,
176
+ traceability=_traceability_status(
177
+ rfcs=("RFC 9220",),
178
+ spec_ids=("spc:1011",),
179
+ implementation_refs=("tigrcorn_contract.metadata:stream_identity",),
180
+ test_ids=("tst:contract-webtransport-stream-identity",),
181
+ evidence_ids=("evd:contract-webtransport-stream-identity-pytest",),
182
+ provisional=True,
183
+ ),
184
+ tags=("webtransport", "identity"),
185
+ ),
186
+ ContractRecord(
187
+ contract_id="operator.contract.registry",
188
+ version="0.1",
189
+ title="Contract registry export contract",
190
+ owner_package="tigrcorn-contract",
191
+ owner_module="tigrcorn_contract.registry",
192
+ stability=ContractStability.EXPERIMENTAL,
193
+ implemented=True,
194
+ certified=False,
195
+ traceability=_traceability_status(
196
+ spec_ids=("spc:2058",),
197
+ implementation_refs=("tigrcorn_contract.registry:export_contract_registry",),
198
+ test_ids=("tst:contract-registry-record-shape", "tst:contract-registry-deterministic-export"),
199
+ evidence_ids=("evd:contract-registry-and-traceability-test-plan",),
200
+ ),
201
+ tags=("operator", "registry"),
202
+ ),
203
+ ContractRecord(
204
+ contract_id="asgi.http.scope.v0",
205
+ version="0.9",
206
+ title="Deprecated HTTP scope contract",
207
+ owner_package="tigrcorn-contract",
208
+ owner_module="tigrcorn_contract.scopes",
209
+ stability=ContractStability.DEPRECATED,
210
+ implemented=False,
211
+ certified=False,
212
+ traceability=_traceability_status(spec_ids=("spc:1021",)),
213
+ replacement_contract_id="asgi.http.scope",
214
+ retirement_note="Replaced by the certified 1.0 HTTP scope contract.",
215
+ tags=("http", "scope"),
216
+ ),
217
+ )
218
+
219
+
220
+ def contract_records() -> tuple[ContractRecord, ...]:
221
+ return tuple(sorted(_CONTRACTS, key=lambda item: item.contract_id))
222
+
223
+
224
+ def export_contract_registry() -> dict[str, Any]:
225
+ return {
226
+ "schema_version": "1.0",
227
+ "registry": "tigrcorn.contracts",
228
+ "contracts": [record.as_dict() for record in contract_records()],
229
+ }
230
+
231
+
232
+ def validate_contract_traceability(record: ContractRecord) -> None:
233
+ traceability = record.traceability
234
+ missing: list[str] = []
235
+ for field_name in ("rfcs", "spec_ids", "implementation_refs", "test_ids", "evidence_ids", "negative_test_ids"):
236
+ if not getattr(traceability, field_name):
237
+ missing.append(field_name)
238
+ if record.certified and (traceability.status != TraceabilityStatus.COMPLETE or missing):
239
+ raise ContractTraceabilityError(
240
+ f"certified contract {record.contract_id!r} lacks complete traceability: {', '.join(missing)}"
241
+ )
242
+ if record.certified and record.stability != ContractStability.CERTIFIED:
243
+ raise ContractTraceabilityError(f"certified contract {record.contract_id!r} must use certified stability")
244
+
245
+
246
+ def validate_registry(records: Iterable[ContractRecord] | None = None) -> None:
247
+ for record in records if records is not None else contract_records():
248
+ validate_contract_traceability(record)
@@ -0,0 +1,348 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Mapping
5
+
6
+ from tigrcorn_core.errors import ProtocolError
7
+
8
+ try: # pragma: no cover - covered when the newer contract package is installed.
9
+ from tigr_asgi_contract import semantic_domain as _contract_semantic_domain
10
+ from tigr_asgi_contract import semantic_transition_target as _contract_transition_target
11
+ from tigr_asgi_contract import validate_semantic_transition as _contract_validate_transition
12
+ except Exception: # pragma: no cover - published contract fallback.
13
+ _contract_semantic_domain = None
14
+ _contract_transition_target = None
15
+ _contract_validate_transition = None
16
+
17
+
18
+ _FALLBACK_TRANSITIONS = {
19
+ ("completion", "accepted_by_runtime", "completion.queued"): "queued_for_transport",
20
+ ("completion", "queued_for_transport", "completion.flushed"): "flushed_to_transport",
21
+ ("completion", "flushed_to_transport", "completion.peer_acknowledged"): "peer_acknowledged",
22
+ ("completion", "accepted_by_runtime", "completion.failed"): "failed_during_emit",
23
+ ("completion", "queued_for_transport", "completion.failed"): "failed_during_emit",
24
+ ("completion", "flushed_to_transport", "completion.failed"): "failed_during_emit",
25
+ ("completion", "accepted_by_runtime", "completion.peer_aborted"): "aborted_by_peer",
26
+ ("completion", "queued_for_transport", "completion.peer_aborted"): "aborted_by_peer",
27
+ ("completion", "flushed_to_transport", "completion.peer_aborted"): "aborted_by_peer",
28
+ ("backpressure", "writable", "backpressure.congested"): "congested",
29
+ ("backpressure", "congested", "backpressure.saturated"): "saturated",
30
+ ("backpressure", "saturated", "backpressure.draining"): "draining",
31
+ ("backpressure", "draining", "backpressure.resumed"): "resumed",
32
+ ("backpressure", "resumed", "backpressure.writable"): "writable",
33
+ ("backpressure", "congested", "backpressure.resumed"): "resumed",
34
+ ("cancellation", "requested", "cancellation.propagated"): "propagated",
35
+ ("cancellation", "propagated", "cancellation.acknowledged"): "acknowledged",
36
+ ("cancellation", "acknowledged", "cancellation.completed"): "completed",
37
+ ("cancellation", "requested", "cancellation.ignored"): "ignored",
38
+ ("cancellation", "propagated", "cancellation.failed"): "failed",
39
+ ("cancellation", "acknowledged", "cancellation.failed"): "failed",
40
+ ("disconnect", "graceful", "disconnect.peer_reset"): "peer_reset",
41
+ ("disconnect", "graceful", "disconnect.transport_lost"): "transport_lost",
42
+ ("disconnect", "graceful", "disconnect.timeout"): "timeout",
43
+ ("disconnect", "graceful", "disconnect.protocol_error"): "protocol_error",
44
+ ("disconnect", "graceful", "disconnect.server_shutdown"): "server_shutdown",
45
+ ("channel_lifecycle", "initialized", "channel.opening"): "opening",
46
+ ("channel_lifecycle", "opening", "channel.opened"): "open",
47
+ ("channel_lifecycle", "open", "channel.read_closed"): "read_closed",
48
+ ("channel_lifecycle", "open", "channel.write_closed"): "write_closed",
49
+ ("channel_lifecycle", "open", "channel.closing"): "closing",
50
+ ("channel_lifecycle", "open", "channel.closed"): "closed",
51
+ ("channel_lifecycle", "open", "channel.failed"): "failed",
52
+ ("channel_lifecycle", "open", "channel.lost"): "lost",
53
+ ("channel_lifecycle", "read_closed", "channel.write_closed"): "closed",
54
+ ("channel_lifecycle", "read_closed", "channel.closing"): "closing",
55
+ ("channel_lifecycle", "read_closed", "channel.failed"): "failed",
56
+ ("channel_lifecycle", "read_closed", "channel.lost"): "lost",
57
+ ("channel_lifecycle", "write_closed", "channel.read_closed"): "closed",
58
+ ("channel_lifecycle", "write_closed", "channel.closing"): "closing",
59
+ ("channel_lifecycle", "write_closed", "channel.failed"): "failed",
60
+ ("channel_lifecycle", "write_closed", "channel.lost"): "lost",
61
+ ("channel_lifecycle", "closing", "channel.closed"): "closed",
62
+ ("channel_lifecycle", "closing", "channel.failed"): "failed",
63
+ ("channel_lifecycle", "closing", "channel.lost"): "lost",
64
+ }
65
+
66
+ _FALLBACK_CHANNEL_CAPABILITIES = {
67
+ "initialized": {"can_drain": False, "can_read": False, "can_write": False, "terminal": False},
68
+ "opening": {"can_drain": False, "can_read": False, "can_write": False, "terminal": False},
69
+ "open": {"can_drain": False, "can_read": True, "can_write": True, "terminal": False},
70
+ "read_closed": {"can_drain": True, "can_read": False, "can_write": True, "terminal": False},
71
+ "write_closed": {"can_drain": True, "can_read": True, "can_write": False, "terminal": False},
72
+ "closing": {"can_drain": True, "can_read": False, "can_write": False, "terminal": False},
73
+ "closed": {"can_drain": False, "can_read": False, "can_write": False, "terminal": True},
74
+ "failed": {"can_drain": False, "can_read": False, "can_write": False, "terminal": True},
75
+ "lost": {"can_drain": False, "can_read": False, "can_write": False, "terminal": True},
76
+ }
77
+
78
+ _DISCONNECT_CAUSE_EVENTS = {
79
+ "http.disconnect": "disconnect.peer_reset",
80
+ "websocket.close": "disconnect.peer_reset",
81
+ "websocket.disconnect": "disconnect.peer_reset",
82
+ "http2.rst_stream": "disconnect.peer_reset",
83
+ "http3.stream.reset": "disconnect.peer_reset",
84
+ "quic.stop_sending": "disconnect.peer_reset",
85
+ "quic.reset_stream": "disconnect.peer_reset",
86
+ "webtransport.close": "disconnect.peer_reset",
87
+ "webtransport.disconnect": "disconnect.peer_reset",
88
+ "transport.lost": "disconnect.transport_lost",
89
+ "tls.protocol_failure": "disconnect.protocol_error",
90
+ "protocol.error": "disconnect.protocol_error",
91
+ "timeout": "disconnect.timeout",
92
+ "server.shutdown": "disconnect.server_shutdown",
93
+ "worker.draining": "disconnect.server_shutdown",
94
+ }
95
+
96
+ _CANCELLATION_CAUSE_EVENTS = {
97
+ "client_disconnected": "cancellation.propagated",
98
+ "stream_reset": "cancellation.propagated",
99
+ "server_shutdown": "cancellation.propagated",
100
+ "timeout": "cancellation.propagated",
101
+ "worker_draining": "cancellation.propagated",
102
+ }
103
+
104
+ _CHANNEL_CAUSE_EVENTS = {
105
+ "open_requested": "channel.opening",
106
+ "opened": "channel.opened",
107
+ "h2.end_stream_received": "channel.read_closed",
108
+ "h2.end_stream_sent": "channel.write_closed",
109
+ "h2.half_closed_remote": "channel.read_closed",
110
+ "h2.half_closed_local": "channel.write_closed",
111
+ "h2.rst_stream": "channel.lost",
112
+ "h2.goaway": "channel.closing",
113
+ "h3.request_receive_closed": "channel.read_closed",
114
+ "h3.request_send_closed": "channel.write_closed",
115
+ "h3.request_cancelled": "channel.lost",
116
+ "h3.request_rejected": "channel.failed",
117
+ "h3.goaway": "channel.closing",
118
+ "h3.stream_error": "channel.failed",
119
+ "quic.receive_stream_closed": "channel.read_closed",
120
+ "quic.send_stream_closed": "channel.write_closed",
121
+ "quic.connection_close": "channel.closing",
122
+ "quic.draining": "channel.closing",
123
+ "quic.idle_timeout": "channel.lost",
124
+ "websocket.closing": "channel.closing",
125
+ "websocket.closed": "channel.closed",
126
+ "websocket.http2.tunnel_ended": "channel.closing",
127
+ "websocket.http3.tunnel_ended": "channel.closing",
128
+ "tcp.reset": "channel.lost",
129
+ "transport.lost": "channel.lost",
130
+ "protocol.error": "channel.failed",
131
+ }
132
+
133
+ _DIRECTIONAL_RESET_EVENTS = {
134
+ "h3.reset_stream",
135
+ "quic.reset_stream",
136
+ }
137
+
138
+ _DIRECTIONAL_STOP_EVENTS = {
139
+ "h3.stop_sending",
140
+ "quic.stop_sending",
141
+ }
142
+
143
+
144
+ @dataclass(frozen=True, slots=True)
145
+ class SemanticObservation:
146
+ domain: str
147
+ previous_state: str
148
+ event: str
149
+ state: str
150
+ source: str
151
+ detail: str | None = None
152
+
153
+ def as_dict(self) -> dict[str, str]:
154
+ payload = {
155
+ "domain": self.domain,
156
+ "event": self.event,
157
+ "previous_state": self.previous_state,
158
+ "source": self.source,
159
+ "state": self.state,
160
+ }
161
+ if self.detail:
162
+ payload["detail"] = self.detail
163
+ return payload
164
+
165
+
166
+ def enforce_semantic_transition(
167
+ domain: str,
168
+ state: str,
169
+ event: str,
170
+ *,
171
+ source: str = "tigrcorn",
172
+ detail: str | None = None,
173
+ ) -> SemanticObservation:
174
+ next_state = _semantic_transition_target(domain, state, event)
175
+ return SemanticObservation(
176
+ domain=domain,
177
+ previous_state=state,
178
+ event=event,
179
+ state=next_state,
180
+ source=source,
181
+ detail=detail,
182
+ )
183
+
184
+
185
+ def observe_completion(
186
+ state: str,
187
+ *,
188
+ queued: bool = False,
189
+ flushed: bool = False,
190
+ peer_acknowledged: bool = False,
191
+ failed: bool = False,
192
+ peer_aborted: bool = False,
193
+ ) -> SemanticObservation:
194
+ event = _single_event(
195
+ "completion",
196
+ {
197
+ "completion.queued": queued,
198
+ "completion.flushed": flushed,
199
+ "completion.peer_acknowledged": peer_acknowledged,
200
+ "completion.failed": failed,
201
+ "completion.peer_aborted": peer_aborted,
202
+ },
203
+ )
204
+ return enforce_semantic_transition("completion", state, event)
205
+
206
+
207
+ def observe_backpressure(
208
+ state: str,
209
+ *,
210
+ queued_bytes: int,
211
+ high_watermark: int,
212
+ resume_watermark: int | None = None,
213
+ ) -> SemanticObservation:
214
+ if queued_bytes < 0 or high_watermark < 0:
215
+ raise ProtocolError("backpressure counters must be non-negative")
216
+ if resume_watermark is not None and resume_watermark < 0:
217
+ raise ProtocolError("resume watermark must be non-negative")
218
+ if state == "writable" and queued_bytes >= high_watermark:
219
+ event = "backpressure.congested"
220
+ elif state == "congested" and queued_bytes >= high_watermark:
221
+ event = "backpressure.saturated"
222
+ elif state == "saturated":
223
+ event = "backpressure.draining"
224
+ elif state in {"congested", "draining"} and resume_watermark is not None and queued_bytes <= resume_watermark:
225
+ event = "backpressure.resumed"
226
+ elif state == "resumed":
227
+ event = "backpressure.writable"
228
+ else:
229
+ raise ProtocolError(f"no backpressure semantic transition for state {state!r}")
230
+ return enforce_semantic_transition("backpressure", state, event)
231
+
232
+
233
+ def observe_disconnect(state: str, cause: str) -> SemanticObservation:
234
+ try:
235
+ event = _DISCONNECT_CAUSE_EVENTS[cause]
236
+ except KeyError as exc:
237
+ raise ProtocolError(f"unsupported disconnect cause: {cause!r}") from exc
238
+ return enforce_semantic_transition("disconnect", state, event)
239
+
240
+
241
+ def observe_transport_cancellation(state: str, cause: str) -> SemanticObservation:
242
+ try:
243
+ event = _CANCELLATION_CAUSE_EVENTS[cause]
244
+ except KeyError as exc:
245
+ raise ProtocolError(f"unsupported transport cancellation cause: {cause!r}") from exc
246
+ return enforce_semantic_transition(
247
+ "cancellation",
248
+ state,
249
+ event,
250
+ detail=f"transport:{cause}",
251
+ )
252
+
253
+
254
+ def observe_channel_lifecycle(
255
+ state: str,
256
+ cause: str,
257
+ *,
258
+ direction: str | None = None,
259
+ ) -> SemanticObservation:
260
+ event = _channel_event_for_cause(cause, direction=direction)
261
+ return enforce_semantic_transition(
262
+ "channel_lifecycle",
263
+ state,
264
+ event,
265
+ detail=_channel_detail(cause, direction),
266
+ )
267
+
268
+
269
+ def channel_lifecycle_capabilities(state: str) -> dict[str, bool]:
270
+ effects = _channel_capability_effects()
271
+ try:
272
+ return dict(effects[state])
273
+ except KeyError as exc:
274
+ raise ProtocolError(f"unknown channel lifecycle state: {state!r}") from exc
275
+
276
+
277
+ def require_channel_readable(state: str) -> None:
278
+ if not channel_lifecycle_capabilities(state)["can_read"]:
279
+ raise ProtocolError(f"channel is not readable in lifecycle state {state!r}")
280
+
281
+
282
+ def require_channel_writable(state: str) -> None:
283
+ if not channel_lifecycle_capabilities(state)["can_write"]:
284
+ raise ProtocolError(f"channel is not writable in lifecycle state {state!r}")
285
+
286
+
287
+ def _single_event(domain: str, flags: Mapping[str, bool]) -> str:
288
+ events = [event for event, active in flags.items() if active]
289
+ if len(events) != 1:
290
+ raise ProtocolError(f"{domain} observation must select exactly one semantic event")
291
+ return events[0]
292
+
293
+
294
+ def _semantic_transition_target(domain: str, state: str, event: str) -> str:
295
+ if _contract_transition_target is not None and _contract_validate_transition is not None:
296
+ try:
297
+ if _contract_validate_transition(domain, state, event):
298
+ return str(_contract_transition_target(domain, state, event))
299
+ except Exception:
300
+ pass
301
+ try:
302
+ return _FALLBACK_TRANSITIONS[(domain, state, event)]
303
+ except KeyError as exc:
304
+ raise ProtocolError(f"illegal semantic transition for {domain}: {state} + {event}") from exc
305
+
306
+
307
+ def _channel_event_for_cause(cause: str, *, direction: str | None) -> str:
308
+ if cause in _DIRECTIONAL_RESET_EVENTS or cause in _DIRECTIONAL_STOP_EVENTS:
309
+ if direction in {"inbound", "read", "remote"}:
310
+ return "channel.read_closed"
311
+ if direction in {"outbound", "write", "local"}:
312
+ return "channel.write_closed"
313
+ raise ProtocolError(f"{cause} requires inbound/read or outbound/write direction")
314
+ try:
315
+ return _CHANNEL_CAUSE_EVENTS[cause]
316
+ except KeyError as exc:
317
+ raise ProtocolError(f"unsupported channel lifecycle cause: {cause!r}") from exc
318
+
319
+
320
+ def _channel_detail(cause: str, direction: str | None) -> str:
321
+ if direction is None:
322
+ return f"transport:{cause}"
323
+ return f"transport:{cause}:{direction}"
324
+
325
+
326
+ def _channel_capability_effects() -> Mapping[str, Mapping[str, bool]]:
327
+ if _contract_semantic_domain is not None:
328
+ try:
329
+ effects = _contract_semantic_domain("channel_lifecycle").get("capability_effects", {})
330
+ if effects:
331
+ return effects
332
+ except Exception:
333
+ pass
334
+ return _FALLBACK_CHANNEL_CAPABILITIES
335
+
336
+
337
+ __all__ = [
338
+ "SemanticObservation",
339
+ "channel_lifecycle_capabilities",
340
+ "enforce_semantic_transition",
341
+ "observe_backpressure",
342
+ "observe_channel_lifecycle",
343
+ "observe_completion",
344
+ "observe_disconnect",
345
+ "observe_transport_cancellation",
346
+ "require_channel_readable",
347
+ "require_channel_writable",
348
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tigrcorn-contract
3
- Version: 0.3.16.dev12
3
+ Version: 0.3.17.dev1
4
4
  Summary: tigr-asgi-contract adapters and runtime-boundary classification for Tigrcorn ASGI3, WebSocket, and WebTransport surfaces.
5
5
  Author-email: Jacob Stewart <jacob@swarmauri.com>
6
6
  License: Apache License
@@ -186,8 +186,8 @@ Classifier: Typing :: Typed
186
186
  Requires-Python: <3.15,>=3.10
187
187
  Description-Content-Type: text/markdown
188
188
  License-File: LICENSE
189
- Requires-Dist: tigrcorn-core==0.3.16.dev12
190
- Requires-Dist: tigrcorn-asgi==0.3.16.dev12
189
+ Requires-Dist: tigrcorn-core==0.3.17.dev1
190
+ Requires-Dist: tigrcorn-asgi==0.3.17.dev1
191
191
  Requires-Dist: tigr-asgi-contract>=0.3.2
192
192
  Dynamic: license-file
193
193
 
@@ -8,7 +8,9 @@ src/tigrcorn_contract/events.py
8
8
  src/tigrcorn_contract/metadata.py
9
9
  src/tigrcorn_contract/projection.py
10
10
  src/tigrcorn_contract/py.typed
11
+ src/tigrcorn_contract/registry.py
11
12
  src/tigrcorn_contract/scopes.py
13
+ src/tigrcorn_contract/semantics.py
12
14
  src/tigrcorn_contract.egg-info/PKG-INFO
13
15
  src/tigrcorn_contract.egg-info/SOURCES.txt
14
16
  src/tigrcorn_contract.egg-info/dependency_links.txt
@@ -0,0 +1,3 @@
1
+ tigrcorn-core==0.3.17.dev1
2
+ tigrcorn-asgi==0.3.17.dev1
3
+ tigr-asgi-contract>=0.3.2
@@ -1,3 +0,0 @@
1
- tigrcorn-core==0.3.16.dev12
2
- tigrcorn-asgi==0.3.16.dev12
3
- tigr-asgi-contract>=0.3.2