openframe-core 2.0.0__tar.gz → 3.0.1__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 (172) hide show
  1. openframe_core-3.0.1/.github/CHANGELOG.md +222 -0
  2. {openframe_core-2.0.0 → openframe_core-3.0.1}/.github/requirements/docs.txt +1 -0
  3. {openframe_core-2.0.0 → openframe_core-3.0.1}/.github/workflows/app-test.yml +2 -2
  4. {openframe_core-2.0.0 → openframe_core-3.0.1}/.github/workflows/auto-docs.yml +4 -4
  5. {openframe_core-2.0.0 → openframe_core-3.0.1}/.github/workflows/python-build.yml +6 -6
  6. openframe_core-3.0.1/.vscode/settings.json +0 -0
  7. {openframe_core-2.0.0 → openframe_core-3.0.1}/PKG-INFO +142 -24
  8. openframe_core-3.0.1/README.md +367 -0
  9. openframe_core-3.0.1/docs/assets/images/banner.png +0 -0
  10. openframe_core-3.0.1/docs/code/cicd/pipeline.md +100 -0
  11. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/code/getting-started/index.md +11 -3
  12. openframe_core-3.0.1/docs/code/modules/contracts.md +219 -0
  13. openframe_core-3.0.1/docs/code/modules/exceptions.md +268 -0
  14. openframe_core-3.0.1/docs/code/modules/health.md +41 -0
  15. openframe_core-3.0.1/docs/code/modules/inbound.md +153 -0
  16. openframe_core-3.0.1/docs/code/modules/index.md +20 -0
  17. openframe_core-3.0.1/docs/code/modules/plugins.md +165 -0
  18. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/code/modules/ports.md +66 -5
  19. openframe_core-3.0.1/docs/code/modules/runtime.md +122 -0
  20. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/code/modules/telemetry.md +40 -0
  21. openframe_core-3.0.1/docs/code/modules/testing.md +194 -0
  22. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/code/modules/tracing.md +2 -2
  23. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/developer-guide/debugging.md +41 -4
  24. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/developer-guide/explore.md +61 -10
  25. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/developer-guide/first-change.md +12 -5
  26. openframe_core-3.0.1/docs/developer-guide/how-it-works.md +113 -0
  27. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/developer-guide/local-dev.md +1 -1
  28. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/developer-guide/quick-start/index.md +45 -25
  29. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/index.md +11 -7
  30. openframe_core-3.0.1/docs/technical/architecture/adrs/adr-006-unified-port-lifecycle.md +108 -0
  31. openframe_core-3.0.1/docs/technical/architecture/adrs/index.md +22 -0
  32. openframe_core-3.0.1/docs/technical/architecture/capability-taxonomy.md +78 -0
  33. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/architecture/component-lifecycle.md +26 -5
  34. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/architecture/design-decisions.md +68 -1
  35. openframe_core-3.0.1/docs/technical/architecture/error-taxonomy.md +142 -0
  36. openframe_core-3.0.1/docs/technical/architecture/package-journey/adapter-lifecycle.md +70 -0
  37. openframe_core-3.0.1/docs/technical/architecture/package-journey/error-propagation.md +101 -0
  38. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/architecture/package-journey/index.md +6 -6
  39. openframe_core-3.0.1/docs/technical/architecture/package-journey/template-wiring.md +60 -0
  40. openframe_core-3.0.1/docs/technical/architecture/system-architecture.md +194 -0
  41. openframe_core-3.0.1/docs/technical/deployment/cicd.md +98 -0
  42. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/infrastructure/networking.md +1 -1
  43. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/operations/index.md +2 -2
  44. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/operations/runbooks/adapter-connection-failure.md +18 -7
  45. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/operations/runbooks/postgres-pool-exhausted.md +1 -1
  46. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/operations/runbooks/queue-consumer-stuck.md +7 -3
  47. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/operations/runbooks/redis-connection-lost.md +1 -1
  48. openframe_core-3.0.1/docs/technical/overview/glossary.md +117 -0
  49. openframe_core-3.0.1/docs/technical/overview/roadmap.md +89 -0
  50. openframe_core-3.0.1/docs/technical/overview/system-overview.md +111 -0
  51. {openframe_core-2.0.0 → openframe_core-3.0.1}/mkdocs.yml +49 -12
  52. openframe_core-3.0.1/openframe/__init__.py +4 -0
  53. openframe_core-3.0.1/openframe/core/contracts/__init__.py +61 -0
  54. openframe_core-3.0.1/openframe/core/contracts/capability.py +61 -0
  55. openframe_core-3.0.1/openframe/core/contracts/context.py +64 -0
  56. openframe_core-3.0.1/openframe/core/contracts/health.py +96 -0
  57. openframe_core-3.0.1/openframe/core/contracts/identity.py +47 -0
  58. openframe_core-3.0.1/openframe/core/contracts/lifecycle.py +81 -0
  59. openframe_core-3.0.1/openframe/core/contracts/port.py +47 -0
  60. openframe_core-3.0.1/openframe/core/exceptions/__init__.py +72 -0
  61. openframe_core-2.0.0/openframe/core/exceptions/errors.py → openframe_core-3.0.1/openframe/core/exceptions/adapter.py +55 -14
  62. openframe_core-3.0.1/openframe/core/exceptions/base.py +120 -0
  63. openframe_core-3.0.1/openframe/core/exceptions/codes.py +79 -0
  64. openframe_core-3.0.1/openframe/core/exceptions/plugin.py +142 -0
  65. openframe_core-3.0.1/openframe/core/inbound/__init__.py +32 -0
  66. openframe_core-3.0.1/openframe/core/inbound/command.py +59 -0
  67. openframe_core-3.0.1/openframe/core/inbound/context.py +48 -0
  68. openframe_core-3.0.1/openframe/core/inbound/query.py +60 -0
  69. openframe_core-3.0.1/openframe/core/inbound/usecase.py +72 -0
  70. {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/middleware/telemetry.py +32 -4
  71. openframe_core-3.0.1/openframe/core/plugins/__init__.py +35 -0
  72. openframe_core-3.0.1/openframe/core/plugins/registry.py +364 -0
  73. {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/ports/__init__.py +7 -4
  74. {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/ports/consumer.py +21 -10
  75. {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/ports/producer.py +22 -11
  76. {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/ports/repository.py +26 -11
  77. openframe_core-3.0.1/openframe/core/runtime/__init__.py +21 -0
  78. openframe_core-3.0.1/openframe/core/runtime/bootstrap.py +196 -0
  79. {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/telemetry/__init__.py +10 -1
  80. openframe_core-3.0.1/openframe/core/telemetry/setup.py +400 -0
  81. {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/testing/__init__.py +7 -4
  82. {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/testing/contracts/__init__.py +5 -1
  83. {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/testing/contracts/consumer.py +25 -5
  84. openframe_core-3.0.1/openframe/core/testing/contracts/lifecycle.py +118 -0
  85. openframe_core-3.0.1/openframe/core/testing/contracts/port.py +85 -0
  86. {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/testing/contracts/producer.py +25 -5
  87. {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/testing/contracts/repository.py +31 -51
  88. {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/testing/fakes/__init__.py +3 -2
  89. {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/testing/fakes/consumer.py +51 -7
  90. {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/testing/fakes/producer.py +46 -6
  91. {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/testing/fakes/repository.py +67 -33
  92. openframe_core-3.0.1/openframe/core/tracing/__init__.py +43 -0
  93. openframe_core-3.0.1/openframe/core/tracing/propagation.py +77 -0
  94. {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/tracing/proxy.py +9 -3
  95. openframe_core-3.0.1/openframe/py.typed +0 -0
  96. {openframe_core-2.0.0 → openframe_core-3.0.1}/pyproject.toml +1 -1
  97. openframe_core-3.0.1/tests/test_contracts.py +245 -0
  98. openframe_core-3.0.1/tests/test_exceptions.py +399 -0
  99. openframe_core-3.0.1/tests/test_inbound.py +158 -0
  100. {openframe_core-2.0.0 → openframe_core-3.0.1}/tests/test_plugins.py +86 -38
  101. {openframe_core-2.0.0 → openframe_core-3.0.1}/tests/test_ports.py +59 -10
  102. openframe_core-3.0.1/tests/test_record_error.py +143 -0
  103. {openframe_core-2.0.0 → openframe_core-3.0.1}/tests/test_runtime.py +30 -16
  104. openframe_core-3.0.1/tests/test_shutdown_telemetry.py +144 -0
  105. openframe_core-3.0.1/tests/test_telemetry_sidecar.py +181 -0
  106. {openframe_core-2.0.0 → openframe_core-3.0.1}/tests/test_testing_fakes.py +58 -15
  107. openframe_core-3.0.1/tests/test_tracing_propagation.py +148 -0
  108. openframe_core-2.0.0/.github/CHANGELOG.md +0 -91
  109. openframe_core-2.0.0/README.md +0 -249
  110. openframe_core-2.0.0/docs/assets/images/banner.png +0 -0
  111. openframe_core-2.0.0/docs/code/cicd/pipeline.md +0 -57
  112. openframe_core-2.0.0/docs/code/modules/exceptions.md +0 -97
  113. openframe_core-2.0.0/docs/code/modules/health.md +0 -55
  114. openframe_core-2.0.0/docs/code/modules/index.md +0 -13
  115. openframe_core-2.0.0/docs/developer-guide/how-it-works.md +0 -81
  116. openframe_core-2.0.0/docs/technical/architecture/package-journey/adapter-lifecycle.md +0 -58
  117. openframe_core-2.0.0/docs/technical/architecture/package-journey/error-propagation.md +0 -66
  118. openframe_core-2.0.0/docs/technical/architecture/package-journey/template-wiring.md +0 -54
  119. openframe_core-2.0.0/docs/technical/architecture/system-architecture.md +0 -100
  120. openframe_core-2.0.0/docs/technical/deployment/cicd.md +0 -46
  121. openframe_core-2.0.0/docs/technical/overview/glossary.md +0 -71
  122. openframe_core-2.0.0/docs/technical/overview/roadmap.md +0 -42
  123. openframe_core-2.0.0/docs/technical/overview/system-overview.md +0 -77
  124. openframe_core-2.0.0/openframe/core/errors/__init__.py +0 -25
  125. openframe_core-2.0.0/openframe/core/errors/plugin.py +0 -83
  126. openframe_core-2.0.0/openframe/core/exceptions/__init__.py +0 -39
  127. openframe_core-2.0.0/openframe/core/health/__init__.py +0 -19
  128. openframe_core-2.0.0/openframe/core/health/protocol.py +0 -94
  129. openframe_core-2.0.0/openframe/core/plugins/__init__.py +0 -27
  130. openframe_core-2.0.0/openframe/core/plugins/contracts.py +0 -148
  131. openframe_core-2.0.0/openframe/core/plugins/registry.py +0 -266
  132. openframe_core-2.0.0/openframe/core/runtime/__init__.py +0 -14
  133. openframe_core-2.0.0/openframe/core/runtime/bootstrap.py +0 -148
  134. openframe_core-2.0.0/openframe/core/telemetry/setup.py +0 -230
  135. openframe_core-2.0.0/openframe/core/tracing/__init__.py +0 -24
  136. openframe_core-2.0.0/tests/test_exceptions.py +0 -218
  137. openframe_core-2.0.0/tests/test_health.py +0 -118
  138. {openframe_core-2.0.0 → openframe_core-3.0.1}/.github/requirements/test.txt +0 -0
  139. {openframe_core-2.0.0 → openframe_core-3.0.1}/.gitignore +0 -0
  140. {openframe_core-2.0.0 → openframe_core-3.0.1}/LICENSE +0 -0
  141. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/assets/images/favicon.png +0 -0
  142. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/assets/images/logo-dark.png +0 -0
  143. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/assets/images/logo-light.png +0 -0
  144. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/assets/javascripts/extra.js +0 -0
  145. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/assets/stylesheets/extra.css +0 -0
  146. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/code/modules/config.md +0 -0
  147. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/code/modules/middleware.md +0 -0
  148. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/architecture/adrs/adr-001-namespace.md +0 -0
  149. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/architecture/adrs/adr-002-hexagonal.md +0 -0
  150. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/architecture/adrs/adr-003-async.md +0 -0
  151. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/architecture/adrs/adr-004-pydantic.md +0 -0
  152. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/architecture/adrs/adr-005-middleware.md +0 -0
  153. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/architecture/data-flows.md +0 -0
  154. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/architecture/package-journey/tracing-flow.md +0 -0
  155. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/deployment/environments.md +0 -0
  156. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/deployment/secrets-and-config.md +0 -0
  157. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/infrastructure/platform.md +0 -0
  158. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/infrastructure/storage.md +0 -0
  159. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/operations/monitoring.md +0 -0
  160. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/operations/runbooks/modal-cold-start-spike.md +0 -0
  161. {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/operations/runbooks/telemetry-export-failing.md +0 -0
  162. {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/__init__.py +0 -0
  163. {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/config/__init__.py +0 -0
  164. {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/config/settings.py +0 -0
  165. {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/middleware/__init__.py +0 -0
  166. {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/middleware/types.py +0 -0
  167. {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/testing/fixtures.py +0 -0
  168. {openframe_core-2.0.0 → openframe_core-3.0.1}/tests/conftest.py +0 -0
  169. {openframe_core-2.0.0 → openframe_core-3.0.1}/tests/test_config.py +0 -0
  170. {openframe_core-2.0.0 → openframe_core-3.0.1}/tests/test_middleware.py +0 -0
  171. {openframe_core-2.0.0 → openframe_core-3.0.1}/tests/test_middleware_types.py +0 -0
  172. {openframe_core-2.0.0 → openframe_core-3.0.1}/tests/test_tracing.py +0 -0
@@ -0,0 +1,222 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ---
9
+
10
+ ## [Unreleased]
11
+
12
+ ### Watch items (architectural)
13
+
14
+ - **Three application wiring options** — `deps.py` + `lru_cache`,
15
+ `PluginRegistry` direct, and `ApplicationBootstrap` now coexist without a
16
+ documented hierarchy. The ambiguity is better-named than before but still
17
+ unresolved. The next documentation pass must establish `ApplicationBootstrap`
18
+ as the default starting point and frame the other two as explicit alternatives
19
+ with stated conditions. Tracked in the roadmap.
20
+
21
+ - **`contracts/` five-module depth** — the `capability → context → health →
22
+ identity → lifecycle → port` chain is clean today. The risk is a sixth or
23
+ seventh module that doesn't stay within the single-concept-per-file rule or
24
+ that imports from non-adjacent layers. Consolidate before expanding if either
25
+ signal appears.
26
+
27
+ ### Added
28
+
29
+ - **`openframe.core.telemetry.shutdown_telemetry()`** — flushes and shuts
30
+ down the OTel SDK, ensuring the `BatchSpanProcessor` exports all buffered
31
+ spans before process exit. Idempotent, never raises, resets `_INITIALISED`
32
+ so `setup_telemetry()` can run again (useful in tests).
33
+ `ApplicationBootstrap.stop()` calls it automatically after port shutdown.
34
+ Templates that do not use `ApplicationBootstrap` should call it in the
35
+ `lifespan` teardown path.
36
+
37
+ - **`openframe.core.tracing.propagation`** — W3C TraceContext inject/extract
38
+ helpers for message-broker adapters: `inject(carrier)` writes
39
+ `traceparent` into a header dict; `extract(carrier)` reads it back and
40
+ returns an OTel `Context`. Centralises what was previously private inside
41
+ the Kafka adapter so every broker adapter shares one implementation.
42
+ Re-exported as `openframe.core.tracing.propagation`.
43
+
44
+ ### Fixed
45
+
46
+ #### CI/CD
47
+
48
+ - Upgraded all GitHub Actions to Node.js 24-compatible versions, resolving
49
+ Node.js 20 deprecation warnings across all three workflows:
50
+ - `actions/checkout` v4 → **v5**
51
+ - `actions/setup-python` v5 → **v6**
52
+ - `actions/upload-artifact` v4 → **v6**
53
+ - `actions/download-artifact` v4 → **v7**
54
+ - Removed invalid `fetch-depth` input from the `actions/upload-artifact`
55
+ step in `python-build.yml` — this parameter belongs on `actions/checkout`,
56
+ not on the artifact upload action.
57
+ - Renamed `skip_existing` → `skip-existing` in `pypa/gh-action-pypi-publish`
58
+ to comply with the action's current kebab-case input convention.
59
+ - Added `attestations: false` to the PyPI publish step; token-based
60
+ authentication disables Trusted Publishing, which also disables provenance
61
+ attestation generation — the flag must be set explicitly to suppress the
62
+ spurious "attestations input ignored" warning. Remove this flag (or set
63
+ `attestations: true`) once Trusted Publishing (OIDC) is configured on PyPI.
64
+
65
+ ---
66
+
67
+ ## [3.0.0] — 2026-07-07
68
+
69
+ ### Architecture scorecard
70
+
71
+ | Dimension | Score | Notes |
72
+ |---|---|---|
73
+ | Contract design | **9.5 / 10** | `Capability` enum closes the open-string gap; `BasePort` unification removes the dual-mechanism ambiguity. |
74
+ | Error model | **9.5 / 10** | `StrEnum` + decentralised `domain.kind` convention is the correct extensibility model for a multi-package ecosystem. |
75
+ | Testing infrastructure | **9.0 / 10** | `testing/contracts/` + `testing/fakes/` centralise what every adapter was duplicating — highest-leverage addition for the ecosystem. |
76
+ | Composition / wiring | **7.0 / 10** | `ApplicationBootstrap` names the third option, but `shutdown_telemetry()` is still missing — lifecycle is not complete. |
77
+ | Telemetry | **8.0 / 10** | Sidecar architecture, bounded-queue export, `record_error` seam — no shutdown flush hook yet.
78
+
79
+ ### BREAKING CHANGE
80
+
81
+ Full clean-break redesign of the port/health/plugin contract layer. There
82
+ are no deprecated aliases and no backward-compatible shims for anything
83
+ removed below — see
84
+ [ADR-006](../docs/technical/architecture/adrs/adr-006-unified-port-lifecycle.md)
85
+ for the complete rationale.
86
+
87
+ #### Added
88
+
89
+ - **`openframe.core.contracts`** (new, apex module) — the single, unified port + lifecycle contract layer:
90
+ - `Identity` — `runtime_checkable` Protocol: `name`, `version`, `capability`.
91
+ - `Lifecycle` — `runtime_checkable` Protocol: `async initialize(context)`, `async shutdown()`, `async health()`.
92
+ - `BasePort(Identity, Lifecycle, Protocol)` — `runtime_checkable`. The single base every outbound port extends.
93
+ - `Capability(str, Enum)` — closed taxonomy: `PERSISTENCE`, `CACHE`, `QUEUE`, `SECRETS`, `FLAGS`, `STORAGE`, `TRANSPORT`, `INFERENCE`, `EMBEDDING`, `SCHEDULE`, `SEARCH`.
94
+ - `PluginStatus`, `PluginHealth`, `PluginContext` — moved here from the deleted `plugins/contracts.py`; the one canonical home.
95
+ - `PrincipalContext`, `TenantContext` — new frozen dataclasses threaded through `PluginContext` and (on the inbound side) `RequestContext`.
96
+ - **`openframe.core.inbound`** (new module) — the driving side of the hexagon:
97
+ - `UseCase[TInput, TOutput]`, `CommandHandler[TInput]`, `QueryHandler[TInput, TOutput]` — `runtime_checkable` Protocols.
98
+ - `RequestContext` — frozen dataclass: correlation id + optional `PrincipalContext`/`TenantContext`.
99
+ - **`openframe.core.exceptions`** — unified error hierarchy (consolidated; see Changed/Removed below):
100
+ - `OpenFrameError` — the single root every ecosystem error derives from. Carries `code`, `message`, `severity`, `retryable`, `correlation_id`, `context`, and `cause`. A single `except OpenFrameError` is a catch point for the whole platform.
101
+ - `ErrorCode`, `Severity` — `StrEnum` taxonomy. Codes follow a decentralised `domain.kind` convention; downstream packages declare their own codes rather than extending the enum.
102
+ - `AmbiguousCapabilityError` — raised by `PluginRegistry.get()` when more than one port shares a capability.
103
+ - **`openframe.core.telemetry.record_error`** — the single seam through which errors flow into telemetry: sets `error.code`/`error.severity`/`error.retryable` span attributes, records the exception, stamps `correlation_id` back onto the error, and increments the `openframe.error.count` metric exactly once per error (deduped across seams). Called at the `TracingProxy`, `TelemetryMiddleware`, and `PluginRegistry` lifecycle boundaries. `exceptions` never imports telemetry — telemetry reads the error's data.
104
+ - **`docs/technical/architecture/adrs/adr-006-unified-port-lifecycle.md`** — binding design record for this release; supersedes the port/health/plugin portions of ADR-002. Includes the unified-error-hierarchy addendum.
105
+ - **`docs/technical/architecture/capability-taxonomy.md`** — human-readable reference for the `Capability` enum.
106
+ - **`docs/technical/architecture/error-taxonomy.md`** — human-readable reference for the `OpenFrameError` hierarchy and the `domain.kind` code convention.
107
+
108
+ #### Changed
109
+
110
+ - **`openframe.core.ports`** — `BaseRepository[T]`, `BaseProducer[T]`, `BaseConsumer[T]` are rebuilt in place on `BasePort`. Every port is now lifecycle-aware by definition (`name`/`version`/`capability`/`initialize`/`shutdown`/`health` in addition to its domain methods). There is no `Managed*` twin and no lifecycle-free variant.
111
+ - **`openframe.core.plugins.registry.PluginRegistry`**
112
+ - `register()` now accepts any `BasePort` (previously `OpenFramePlugin`) and an optional `config` mapping threaded through to `PluginContext` at initialize time.
113
+ - `get()`/`get_all()` now take a `Capability` enum member instead of a raw `str`.
114
+ - `get()` is now strict — raises `AmbiguousCapabilityError` when more than one port shares the requested capability, instead of silently returning the first match. Use `get_all()` for the deliberate multi-port case.
115
+ - New `set_context(principal=..., tenant=...)` — threads a real `PrincipalContext`/`TenantContext` through every port's `PluginContext` in `initialize_all()` (previously always passed `config={}` with no identity).
116
+ - **`openframe.core.runtime.ApplicationBootstrap`** — `register()`/`get()` updated to the `BasePort`/`Capability` signatures above.
117
+ - **`openframe.core.testing.contracts`** — `RepositoryContractTests`, `ProducerContractTests`, `ConsumerContractTests` now build on the new `PortContractTests` (identity + lifecycle checks for any `BasePort`), which itself builds on `LifecycleContractTests` (initialize → health → idempotent shutdown → shutdown-before-initialize safety).
118
+ - **`openframe.core.testing.fakes`** — `InMemoryRepository`, `FakeProducer`, `FakeConsumer` now satisfy `BasePort`: each gained `name`/`version`/`capability` and `initialize`/`shutdown`/`health`.
119
+ - **`openframe.core.exceptions`** — `AdapterError` and `PluginError` are now both re-parented under the new `OpenFrameError` root. Their constructors (`AdapterError(message, adapter, operation, cause)`, `PluginError(message, plugin_name)`) and `__str__` output are unchanged, so existing raise sites and `except` clauses keep working. Each subclass now also carries a `domain.kind` `code` and a `retryable` default (`adapter.connection`/`adapter.timeout` are retryable; `adapter.not_found` is not).
120
+
121
+ #### Removed
122
+
123
+ - **`openframe.core.health`** (entire module deleted) — `HealthCheck` (`ping()`/`is_ready()`) no longer exists. Health is exclusively `Lifecycle.health() -> PluginHealth`.
124
+ - **`OpenFramePlugin`** (`openframe.core.plugins.contracts`, module deleted) — no longer exists as a separate protocol. A "plugin" is just a registered `BasePort`.
125
+ - The pre-v3 hardcoded, lifecycle-free `BaseRepository`/`BaseProducer`/`BaseConsumer` Protocols (domain methods only, no identity/lifecycle) — replaced in place, not additively.
126
+ - `InMemoryRepository.ping()`/`.is_ready()` (`ping_healthy=`/`ready_healthy=` constructor args) — replaced by `health()` (`healthy=` constructor arg).
127
+ - **`openframe.core.errors`** (entire module deleted) — the `PluginError` family (`PluginError`, `PluginInitializationError`, `PluginNotFoundError`, `DuplicatePluginError`, `AmbiguousCapabilityError`) moved into the unified `openframe.core.exceptions` package. Import from `openframe.core.exceptions` instead of `openframe.core.errors`.
128
+
129
+ ### Migration notes
130
+
131
+ `openframe-adapters` packages pinned `openframe-core>=2.0,<3` continue
132
+ resolving to 2.x and are unaffected until explicitly migrated. To migrate
133
+ an adapter to v3:
134
+
135
+ 1. Add `name: str`, `version: str`, `capability: Capability` attributes to every port implementation.
136
+ 2. Add `async initialize(self, context: PluginContext) -> None` and `async shutdown(self) -> None`.
137
+ 3. Replace `ping()`/`is_ready()` with a single `async health(self) -> PluginHealth`, encoding the liveness/readiness distinction (if you need to keep it) in `PluginStatus`/`PluginHealth.details` rather than two methods.
138
+ 4. Replace any hand-written `OpenFramePlugin` wrapper class with the port object itself — it already satisfies `BasePort` once steps 1–3 are done, so no wrapper is needed.
139
+ 5. Replace string capability keys (`"persistence"`) with the `Capability` enum (`Capability.PERSISTENCE`) everywhere they're used to register with or query a `PluginRegistry`.
140
+ 6. Update any `from openframe.core.errors import ...` to `from openframe.core.exceptions import ...` (the `errors` package was consolidated into `exceptions`). Adapter raise sites for `AdapterError` and its subclasses need no changes — the constructors are unchanged. Optionally, catch `OpenFrameError` for a single ecosystem-wide catch point, and read `err.code`/`err.retryable`/`err.severity` instead of branching on concrete exception classes.
141
+
142
+ ---
143
+
144
+ ## [1.0.0] — 2026-06-13
145
+
146
+ ### Added
147
+
148
+ #### `openframe.core.exceptions`
149
+ - `AdapterError` — base exception class for all OpenFrame adapter packages. Carries `message`, `adapter`, `operation`, and `cause` fields. Implements `__str__` to produce `[adapter.operation] message — caused by: <cause>` rather than a raw tuple repr.
150
+ - `AdapterConnectionError` — raised when an adapter cannot establish a connection to its backend.
151
+ - `AdapterQueryError` — raised when an operation fails after a connection is established.
152
+ - `AdapterNotFoundError` — raised when a requested entity does not exist in the backend.
153
+ - `AdapterConfigurationError` — raised when required configuration (env vars, settings) is missing or invalid.
154
+ - `AdapterTimeoutError` — raised when an operation exceeds its configured timeout.
155
+
156
+ #### `openframe.core.config`
157
+ - `BaseAdapterSettings` — Pydantic `BaseSettings` subclass that all adapter config classes inherit from. Reads fields from environment variables automatically (case-insensitive, no prefix). Ships with four common fields: `adapter_name`, `connection_timeout` (30 s), `operation_timeout` (10 s), and `max_retries` (3). Missing required fields raise `ValidationError` at instantiation time.
158
+
159
+ #### `openframe.core.ports`
160
+ - `BaseRepository[T]` — generic `runtime_checkable` Protocol for persistence adapters. Methods: `get`, `list`, `create`, `update`, `delete`.
161
+ - `BaseProducer[T]` — generic `runtime_checkable` Protocol for message producers. Methods: `publish`, `publish_batch`, `close`.
162
+ - `BaseConsumer[T]` — generic `runtime_checkable` Protocol for message consumers. Methods: `subscribe`, `ack`, `nack`, `close`. Uses a push-based handler model via `Callable[[T], Awaitable[None]]`.
163
+
164
+ #### `openframe.core.health`
165
+ - `HealthCheck` — `runtime_checkable` Protocol with two async methods: `ping()` (low-cost liveness, e.g. `SELECT 1`) and `is_ready()` (full readiness — schema migrations, pool health).
166
+
167
+ #### `openframe.core.telemetry`
168
+ - `setup_telemetry()` — idempotent OTel SDK bootstrap. Configures OTLP trace and metric exporters when `OTEL_EXPORTER_OTLP_ENDPOINT` is set; falls back to no-op providers otherwise. Applies `LoggingInstrumentor` for automatic trace-id injection into log records.
169
+ - `get_tracer(name)` — `@lru_cache` accessor for the globally-configured `Tracer`.
170
+ - `get_meter(name)` — `@lru_cache` accessor for the globally-configured `Meter`.
171
+ - `record_lifecycle_event(event_name, attributes)` — platform-agnostic lifecycle counter. Replaces the template's `record_cold_start()`; callers pass `event_name="cold_start"`. Core remains platform-agnostic.
172
+ - Reads environment variables: `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_HEADERS`, `OTEL_SERVICE_NAME`, `OTEL_SERVICE_VERSION`, `OTEL_METRIC_EXPORT_INTERVAL_MS`, `OPENFRAME_ENV`.
173
+
174
+ #### `openframe.core.tracing`
175
+ - `TracingProxy` — zero-code async telemetry sidecar. Wraps any object and intercepts every async method call with an OTel child span. Span name: `{prefix}.{method_name}`. Sync methods pass through unwrapped. Uses an explicit `_cache` dict for closure reuse without method snapshotting — safe for reconnecting adapters (Postgres pools, Redis clients) that replace their own methods after reconnect.
176
+
177
+ #### `openframe.core.middleware`
178
+ - `TelemetryMiddleware` — pure ASGI telemetry middleware, compatible with FastAPI, Starlette, Litestar, and bare ASGI. Records one OTel span and five HTTP metric instruments per request. Injects `x-session-id` response header. Sets `StatusCode.ERROR` for HTTP ≥ 400, `StatusCode.OK` otherwise. Emits a structured log line per request.
179
+ - Metric instruments: `http.server.request.count` (`1`), `http.server.request.duration` (`s`), `http.server.active_requests` (`1`), `http.server.error.count` (`1`), `http.server.response.size` (`By`). Duration uses unit `"s"` per OTel HTTP semantic conventions.
180
+ - Span attributes: `http.method`, `http.target`, `http.route`, `http.scheme`, `http.status_code`, `http.user_agent`, `net.host.name`, `http.client_ip`, `app.session_id`.
181
+ - Route template extraction: raw ASGI uses `scope["path"]`; Starlette/FastAPI users get template grouping automatically via `scope["route"].path`.
182
+
183
+ #### `openframe.core.middleware.types`
184
+ - `ASGIScope`, `ASGIMessage`, `Receive`, `Send`, `ASGIApp` — stdlib-only ASGI type aliases shared across the OpenFrame ecosystem. No framework imports required.
185
+
186
+ #### Tests
187
+ - 112 tests across 8 test modules: `test_exceptions`, `test_config`, `test_ports`, `test_health`, `test_tracing`, `test_middleware`, `test_middleware_types`, `test_config`.
188
+ - `conftest.py` resets all OTel SDK global state between tests (the `Once._done` guard on `set_tracer_provider` / `set_meter_provider`, `lru_cache` entries, and the `_INITIALISED` flag) so every test receives a clean, isolated tracer and meter.
189
+
190
+ #### Package
191
+ - `pyproject.toml` with `hatchling` build backend, `requires-python = ">=3.11"`.
192
+ - `pytest.ini` with `asyncio_mode = auto` for `pytest-asyncio`.
193
+ - Namespace package declaration in `openframe/__init__.py` via `pkgutil.extend_path`.
194
+
195
+ ### Design decisions
196
+
197
+ - **Exception naming** — all exception subclasses carry the `Adapter` prefix (`AdapterConnectionError`, `AdapterTimeoutError`) to avoid shadowing the Python stdlib built-ins `ConnectionError` and `TimeoutError`.
198
+ - **No Grafana-specific auth** — `_grafana_headers()` from the production template is removed. The OTel SDK handles authentication natively via `OTEL_EXPORTER_OTLP_HEADERS`.
199
+ - **`OPENFRAME_ENV` replaces `MODAL_ENV`** — the environment tag is platform-agnostic. Modal users map `MODAL_ENV → OPENFRAME_ENV` in their `configure_env_vars()`.
200
+ - **Middleware does not call `setup_telemetry()`** — it calls `get_tracer()` and `get_meter()` directly. `setup_telemetry()` is an application startup concern (called once in a `lifespan` handler). This design makes the middleware safely composable with test fixtures that install their own providers.
201
+ - **`TracingProxy` closure safety** — the original template captured the resolved method in the closure at first access (a snapshot). This implementation resolves the method fresh on every invocation inside `_traced`, making the proxy safe for adapters that replace their own methods after reconnect.
202
+ - **Pure ASGI middleware** — uses `__call__(scope, receive, send)` rather than Starlette's `BaseHTTPMiddleware`, which has documented streaming and buffering limitations.
203
+
204
+ ### Dependencies
205
+
206
+ ```
207
+ opentelemetry-api>=1.24
208
+ opentelemetry-sdk>=1.24
209
+ opentelemetry-exporter-otlp-proto-http>=1.24
210
+ opentelemetry-semantic-conventions>=0.45b0
211
+ opentelemetry-instrumentation-logging>=0.45b0
212
+ pydantic>=2.0
213
+ pydantic-settings>=2.0
214
+ ```
215
+
216
+ Dev dependencies: `pytest>=8.0`, `pytest-asyncio>=0.23`, `httpx>=0.27`.
217
+
218
+ ---
219
+
220
+ [Unreleased]: https://github.com/openframe-org/openframe-core/compare/v3.0.0...HEAD
221
+ [3.0.0]: https://github.com/openframe-org/openframe-core/compare/v1.0.0...v3.0.0
222
+ [1.0.0]: https://github.com/openframe-org/openframe-core/releases/tag/v1.0.0
@@ -1,3 +1,4 @@
1
1
  mkdocs>=1.6
2
2
  mkdocs-material>=9.5
3
3
  mkdocs-swagger-ui-tag>=0.6
4
+ mkdocs-include-markdown-plugin
@@ -22,10 +22,10 @@ jobs:
22
22
 
23
23
  steps:
24
24
  - name: Checkout
25
- uses: actions/checkout@v4
25
+ uses: actions/checkout@v5
26
26
 
27
27
  - name: Setup Python ${{ matrix.python-version }}
28
- uses: actions/setup-python@v5
28
+ uses: actions/setup-python@v6
29
29
  with:
30
30
  python-version: ${{ matrix.python-version }}
31
31
  allow-prereleases: true # required if 3.14 is still RC on the runner
@@ -20,10 +20,10 @@ jobs:
20
20
 
21
21
  steps:
22
22
  - name: Checkout
23
- uses: actions/checkout@v4
23
+ uses: actions/checkout@v5
24
24
 
25
25
  - name: Setup Python
26
- uses: actions/setup-python@v5
26
+ uses: actions/setup-python@v6
27
27
  with:
28
28
  python-version: "3.11"
29
29
  cache: pip
@@ -50,12 +50,12 @@ jobs:
50
50
 
51
51
  steps:
52
52
  - name: Checkout
53
- uses: actions/checkout@v4
53
+ uses: actions/checkout@v5
54
54
  with:
55
55
  fetch-depth: 0 # full history needed for mkdocs git-revision-date plugins
56
56
 
57
57
  - name: Setup Python
58
- uses: actions/setup-python@v5
58
+ uses: actions/setup-python@v6
59
59
  with:
60
60
  python-version: "3.11"
61
61
  cache: pip
@@ -21,10 +21,10 @@ jobs:
21
21
 
22
22
  steps:
23
23
  - name: Checkout
24
- uses: actions/checkout@v4
24
+ uses: actions/checkout@v5
25
25
 
26
26
  - name: Setup Python
27
- uses: actions/setup-python@v5
27
+ uses: actions/setup-python@v6
28
28
  with:
29
29
  python-version: "3.11"
30
30
  cache: pip
@@ -38,12 +38,11 @@ jobs:
38
38
  run: python -m build
39
39
 
40
40
  - name: Upload distribution artifacts
41
- uses: actions/upload-artifact@v4
41
+ uses: actions/upload-artifact@v6
42
42
  with:
43
43
  name: dist
44
44
  path: dist/
45
45
  retention-days: 7
46
- fetch-depth: 2 # needs at least 2 commits for HEAD~1 to exist
47
46
 
48
47
  # ── 3. Publish to PyPI ──────────────────────────────────────────────────
49
48
  publish:
@@ -71,7 +70,7 @@ jobs:
71
70
 
72
71
  steps:
73
72
  - name: Download distribution artifacts
74
- uses: actions/download-artifact@v4
73
+ uses: actions/download-artifact@v7
75
74
  with:
76
75
  name: dist
77
76
  path: dist/
@@ -84,4 +83,5 @@ jobs:
84
83
  uses: pypa/gh-action-pypi-publish@release/v1
85
84
  with:
86
85
  password: ${{ secrets.PYPI_API_TOKEN }}
87
- skip_existing: true
86
+ skip-existing: true
87
+ attestations: false
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openframe-core
3
- Version: 2.0.0
3
+ Version: 3.0.1
4
4
  Summary: OpenFrame Microservice Suite - platform kernel. Contracts, testing utilities, plugin registry, and composition root.
5
5
  Project-URL: Homepage, https://github.com/Furious-Meteors/openframe-core
6
6
  Project-URL: Documentation, https://furious-meteors.github.io/openframe-core/
@@ -55,13 +55,13 @@ Requires-Dist: ruff<1,>=0.11; extra == 'docs-dev'
55
55
  Description-Content-Type: text/markdown
56
56
 
57
57
  <p align="center">
58
- <img src="docs/assets/images/banner.png" alt="openframe-core" width="100%" />
58
+ <img src="https://raw.githubusercontent.com/Furious-Meteors/openframe-core/production/docs/assets/images/banner.png" alt="openframe-core" width="100%" />
59
59
  </p>
60
60
 
61
61
  <p align="center">
62
62
  <a href="https://pypi.org/project/openframe-core/"><img src="https://img.shields.io/pypi/v/openframe-core?color=6DB33F&labelColor=1a1a1a&label=PyPI" alt="PyPI version"/></a>
63
63
  <a href="https://pypi.org/project/openframe-core/"><img src="https://img.shields.io/pypi/pyversions/openframe-core?color=6DB33F&labelColor=1a1a1a" alt="Python versions"/></a>
64
- <a href="https://github.com/Furious-Meteors/openframe-core/actions/workflows/app-test.yml"><img src="https://img.shields.io/github/actions/workflow/status/Furious-Meteors/openframe-core/app-test.yml?branch=production&color=6DB33F&labelColor=1a1a1a&label=tests" alt="Tests"/></a>
64
+ <a href="https://github.com/Furious-Meteors/openframe-core/actions/workflows/python-build.yml"><img src="https://img.shields.io/github/actions/workflow/status/Furious-Meteors/openframe-core/python-build.yml?branch=production&color=6DB33F&labelColor=1a1a1a&label=tests" alt="Tests"/></a>
65
65
  <a href="https://github.com/Furious-Meteors/openframe-core/blob/production/LICENSE"><img src="https://img.shields.io/badge/license-MIT-6DB33F?labelColor=1a1a1a" alt="License"/></a>
66
66
  <a href="https://furious-meteors.github.io/openframe-core/"><img src="https://img.shields.io/badge/docs-live-6DB33F?labelColor=1a1a1a" alt="Docs"/></a>
67
67
  </p>
@@ -77,7 +77,9 @@ Description-Content-Type: text/markdown
77
77
 
78
78
  `openframe-core` is the foundation package of the OpenFrame Microservice Development Suite. It defines the structural contracts, telemetry primitives, and shared utilities that every other package in the ecosystem depends on.
79
79
 
80
- Every package in the OpenFrame ecosystem pins `openframe-core>=1.0,<2`. The major version is the stability contract for the entire ecosystem.
80
+ Every package in the OpenFrame ecosystem pins `openframe-core>=3.0,<4`. The major version is the stability contract for the entire ecosystem.
81
+
82
+ > **v3.0.0 is a breaking redesign.** The old hardcoded, lifecycle-free ports, the standalone `HealthCheck` protocol, and the separate `OpenFramePlugin` protocol are gone. They are replaced by a single unified contract layer built on `BasePort` (`Identity` + `Lifecycle`), and the error hierarchy is consolidated under a single `OpenFrameError` root. See [ADR-006](https://furious-meteors.github.io/openframe-core/technical/architecture/adrs/adr-006-unified-port-lifecycle/) and the [Changelog](.github/CHANGELOG.md) for the full migration guide.
81
83
 
82
84
  ---
83
85
 
@@ -85,14 +87,18 @@ Every package in the OpenFrame ecosystem pins `openframe-core>=1.0,<2`. The majo
85
87
 
86
88
  | Module | Exports |
87
89
  |---|---|
88
- | `openframe.core.exceptions` | `AdapterError` · `AdapterConnectionError` · `AdapterQueryError` · `AdapterNotFoundError` · `AdapterConfigurationError` · `AdapterTimeoutError` |
90
+ | `openframe.core.contracts` | `BasePort` · `Identity` · `Lifecycle` · `Capability` · `PluginStatus` · `PluginHealth` · `PluginContext` · `PrincipalContext` · `TenantContext` |
91
+ | `openframe.core.exceptions` | `OpenFrameError` (root) · `ErrorCode` · `Severity` · `AdapterError` (+5) · `PluginError` (+4, incl. `AmbiguousCapabilityError`) |
89
92
  | `openframe.core.config` | `BaseAdapterSettings` — Pydantic `BaseSettings` subclass all adapters inherit |
90
- | `openframe.core.ports` | `BaseRepository[T]` · `BaseProducer[T]` · `BaseConsumer[T]` — `runtime_checkable` Protocols |
91
- | `openframe.core.health` | `HealthCheck` — `ping()` and `is_ready()` Protocol |
92
- | `openframe.core.telemetry` | `setup_telemetry()` · `get_tracer()` · `get_meter()` · `record_lifecycle_event()` |
93
+ | `openframe.core.ports` | `BaseRepository[T]` · `BaseProducer[T]` · `BaseConsumer[T]` — `BasePort` + domain methods (outbound side) |
94
+ | `openframe.core.inbound` | `UseCase[TIn, TOut]` · `CommandHandler[TIn]` · `QueryHandler[TIn, TOut]` · `RequestContext` (driving side) |
95
+ | `openframe.core.plugins` | `PluginRegistry` — explicit registration, ordered init, LIFO shutdown, capability lookup |
96
+ | `openframe.core.runtime` | `ApplicationBootstrap` — optional composition-root base class |
97
+ | `openframe.core.telemetry` | `setup_telemetry()` · `get_tracer()` · `get_meter()` · `record_lifecycle_event()` · `record_error()` |
93
98
  | `openframe.core.tracing` | `TracingProxy` — zero-code async telemetry sidecar |
94
99
  | `openframe.core.middleware` | `TelemetryMiddleware` — pure ASGI middleware |
95
100
  | `openframe.core.middleware.types` | `ASGIScope` · `ASGIMessage` · `Receive` · `Send` · `ASGIApp` |
101
+ | `openframe.core.testing` | `InMemoryRepository` · `FakeProducer` · `FakeConsumer` · `PortContractTests` · `LifecycleContractTests` · `RepositoryContractTests` · `ProducerContractTests` · `ConsumerContractTests` |
96
102
 
97
103
  ---
98
104
 
@@ -111,10 +117,14 @@ pip install "openframe-core[dev]"
111
117
 
112
118
  ## Quick start
113
119
 
114
- ### Exceptions — single catch point across all adapters
120
+ ### Exceptions — one catch point for the whole ecosystem
121
+
122
+ Every error derives from `OpenFrameError`, and semantics (`code`, `retryable`, `severity`) are carried as data:
115
123
 
116
124
  ```python
117
- from openframe.core.exceptions import AdapterError, AdapterNotFoundError, AdapterQueryError
125
+ from openframe.core.exceptions import (
126
+ OpenFrameError, AdapterNotFoundError, AdapterQueryError,
127
+ )
118
128
 
119
129
  # In an adapter — wrap driver exceptions before they leave
120
130
  try:
@@ -127,16 +137,20 @@ except SomeDriverError as exc:
127
137
  cause=exc,
128
138
  ) from exc
129
139
 
130
- # In a service — one catch point regardless of which adapter is wired in
140
+ # In a service — one catch point for anything the ecosystem raises
131
141
  try:
132
142
  entity = await repo.get(entity_id)
133
143
  except AdapterNotFoundError:
134
144
  return None
135
- except AdapterError as exc:
136
- logger.error("Adapter failure: %s", exc) # [postgres.get] message — caused by: ...
145
+ except OpenFrameError as exc:
146
+ if exc.retryable: # act on data, not an isinstance ladder
147
+ await backoff_and_retry()
148
+ logger.error("failure code=%s: %s", exc.code, exc) # [postgres.get] ... — caused by: ...
137
149
  raise
138
150
  ```
139
151
 
152
+ Error codes follow a decentralised `domain.kind` convention (`adapter.connection`, `plugin.duplicate`); downstream packages declare their own codes and still catch cleanly under `OpenFrameError`.
153
+
140
154
  ### Config — env vars validated at startup, not first request
141
155
 
142
156
  ```python
@@ -150,12 +164,27 @@ class PostgresSettings(BaseAdapterSettings):
150
164
  settings = PostgresSettings()
151
165
  ```
152
166
 
153
- ### Ports — structural contracts, no inheritance required
167
+ ### Ports — one lifecycle-aware base for every adapter
168
+
169
+ Every port is a `BasePort`: `Identity` (`name`/`version`/`capability`) + `Lifecycle` (`initialize`/`shutdown`/`health`) plus its own domain methods. Adapters satisfy it structurally — no inheritance required.
154
170
 
155
171
  ```python
172
+ from openframe.core.contracts import Capability, PluginContext, PluginHealth, PluginStatus
156
173
  from openframe.core.ports import BaseRepository
157
174
 
158
175
  class PostgresItemRepository:
176
+ # Identity
177
+ name = "postgres-items"
178
+ version = "1.0.0"
179
+ capability = Capability.PERSISTENCE
180
+
181
+ # Lifecycle
182
+ async def initialize(self, context: PluginContext) -> None: ...
183
+ async def shutdown(self) -> None: ...
184
+ async def health(self) -> PluginHealth:
185
+ return PluginHealth(status=PluginStatus.READY)
186
+
187
+ # Domain methods
159
188
  async def get(self, entity_id: str) -> Item | None: ...
160
189
  async def list(self, limit: int, offset: int) -> tuple[list[Item], int]: ...
161
190
  async def create(self, entity: Item) -> Item: ...
@@ -166,6 +195,58 @@ class PostgresItemRepository:
166
195
  assert isinstance(PostgresItemRepository(), BaseRepository) # True
167
196
  ```
168
197
 
198
+ ### Plugins — register any port, managed lifecycle
199
+
200
+ A "plugin" is just a registered `BasePort`. The registry initializes in order, shuts down LIFO, and looks up by the typed `Capability` enum:
201
+
202
+ ```python
203
+ from openframe.core.contracts import Capability
204
+ from openframe.core.plugins import PluginRegistry
205
+
206
+ registry = PluginRegistry()
207
+ registry.register(PostgresItemRepository(), config={"dsn": "postgres://..."})
208
+ registry.register(RedisCache(), config={"url": "redis://..."})
209
+
210
+ await registry.initialize_all() # forward order; rolls back on failure
211
+ repo = registry.get(Capability.PERSISTENCE) # strict — raises if >1 match
212
+ # ... serve traffic ...
213
+ await registry.shutdown_all() # reverse order; never raises
214
+ ```
215
+
216
+ `get()` is strict: it raises `AmbiguousCapabilityError` when more than one port shares a capability. Use `get_all(Capability.PERSISTENCE)` for the intended multi-port case (e.g. primary + replica).
217
+
218
+ ### Runtime — optional composition root
219
+
220
+ ```python
221
+ from openframe.core.contracts import Capability
222
+ from openframe.core.runtime import ApplicationBootstrap
223
+
224
+ class MyServiceBootstrap(ApplicationBootstrap):
225
+ def configure(self) -> None:
226
+ self.register(PostgresItemRepository(), config={"dsn": "..."})
227
+
228
+ async with MyServiceBootstrap() as bootstrap: # start() on enter, stop() on exit
229
+ repo = bootstrap.get(Capability.PERSISTENCE)
230
+ await serve(ItemService(repo))
231
+ ```
232
+
233
+ ### Inbound ports — the driving side of the hexagon
234
+
235
+ ```python
236
+ from openframe.core.inbound import UseCase, RequestContext
237
+
238
+ class CreateItem:
239
+ async def execute(self, command: CreateItemCommand,
240
+ context: RequestContext | None = None) -> Item:
241
+ ...
242
+
243
+ assert isinstance(CreateItem(), UseCase) # structural — no inheritance
244
+
245
+ # An inbound adapter (HTTP route, message handler, CLI) builds the context:
246
+ ctx = RequestContext(correlation_id="trace-abc")
247
+ item = await CreateItem().execute(command, ctx)
248
+ ```
249
+
169
250
  ### Telemetry — OTel bootstrap in one call
170
251
 
171
252
  ```python
@@ -222,6 +303,37 @@ app = TelemetryMiddleware(my_asgi_app)
222
303
 
223
304
  Instruments five HTTP metrics per request: `http.server.request.count`, `http.server.request.duration` (`s`), `http.server.active_requests`, `http.server.error.count`, `http.server.response.size`. Injects `x-session-id` on every response.
224
305
 
306
+ ### Errors flow into telemetry automatically
307
+
308
+ `OpenFrameError`s are recorded into telemetry at every boundary seam — `TracingProxy` (outbound adapter calls), `TelemetryMiddleware` (inbound HTTP), and `PluginRegistry` lifecycle (startup/shutdown). Each records the exception on the active span, sets the `error.code` / `error.severity` / `error.retryable` span attributes, stamps the trace id back onto `err.correlation_id`, and increments the `openframe.error.count` metric exactly once per error. The errors layer never imports telemetry — telemetry reads the error's data — so the dependency DAG stays intact. You can also call it directly:
309
+
310
+ ```python
311
+ from openframe.core.telemetry import record_error
312
+
313
+ try:
314
+ ...
315
+ except OpenFrameError as exc:
316
+ record_error(exc) # span attributes + one metric increment (deduped)
317
+ raise
318
+ ```
319
+
320
+ ### Testing utilities — shared fakes and contract tests
321
+
322
+ `openframe-core` ships test doubles and reusable pytest base classes that every adapter package uses, so adapters get behavioral + lifecycle conformance for free:
323
+
324
+ ```python
325
+ from openframe.core.testing import InMemoryRepository, RepositoryContractTests
326
+
327
+ # In-memory BasePort implementation for fast unit tests / local dev
328
+ repo = InMemoryRepository[Item]()
329
+
330
+ # Inherit the full contract suite (CRUD + BasePort lifecycle) for your adapter
331
+ class TestPostgresRepository(RepositoryContractTests):
332
+ @pytest.fixture
333
+ def port(self):
334
+ return PostgresItemRepository()
335
+ ```
336
+
225
337
  ---
226
338
 
227
339
  ## Environment variables
@@ -244,23 +356,29 @@ pip install -e ".[dev]"
244
356
  pytest tests/ -v
245
357
  ```
246
358
 
247
- 112 tests across 8 modules. All run in under 1 second — no network calls, no external services.
359
+ 284 tests. All run in under 1 second — no network calls, no external services.
248
360
 
249
361
  ```bash
250
362
  # Smoke test
251
- python -c "from openframe.core.ports import BaseRepository; print('openframe-core OK')"
363
+ python -c "from openframe.core.contracts import BasePort; print('openframe-core OK')"
252
364
  ```
253
365
 
254
366
  ---
255
367
 
256
368
  ## Design decisions
257
369
 
370
+ **Why one `BasePort` instead of separate port / health / plugin protocols?**
371
+ Before v3 an adapter had to satisfy three unrelated protocols (a port, `HealthCheck`, and `OpenFramePlugin`) and often hand-write a wrapper class to be registrable. `BasePort` (`Identity` + `Lifecycle`) unifies them: every port is lifecycle-aware and registry-ready with zero wrapper code. See [ADR-006](https://furious-meteors.github.io/openframe-core/technical/architecture/adrs/adr-006-unified-port-lifecycle/).
372
+
373
+ **Why a single `OpenFrameError` root?**
374
+ So a service (or gateway, or middleware) can catch anything the ecosystem raises at one point, and act on `code` / `retryable` / `severity` as data rather than importing and branching on every concrete error class. Downstream packages add their own families under the same root without widening anyone's `except`.
375
+
376
+ **Why is `Capability` a closed enum?**
377
+ Registry lookups (`registry.get(Capability.PERSISTENCE)`) are type-checked, and the duplicate-capability guard and "which capabilities exist" analysis are only possible with a closed vocabulary — no adapters inventing ad hoc capability strings.
378
+
258
379
  **Why `Adapter` prefix on exceptions?**
259
380
  `ConnectionError` and `TimeoutError` are Python stdlib built-ins. Using the same names would shadow them silently. `AdapterConnectionError` and `AdapterTimeoutError` are unambiguous.
260
381
 
261
- **Why `super().__init__(message)` only?**
262
- `Exception.__str__` returns a tuple repr when multiple args are passed. Passing only the message keeps `str(exc)` readable: `[postgres.get] entity not found`.
263
-
264
382
  **Why does `TelemetryMiddleware` not call `setup_telemetry()`?**
265
383
  The OTel SDK's `set_tracer_provider()` is protected by a once-guard — subsequent calls are silently rejected. Calling `setup_telemetry()` inside the middleware overwrites the provider set at startup (including test fixture providers), causing spans to be dropped silently.
266
384
 
@@ -279,7 +397,7 @@ The closure resolves the wrapped method via `getattr(wrapped, name)` on every in
279
397
  | [`openframe-ai`](https://github.com/Furious-Meteors/openframe-ai) | `pip install openframe-ai[serving]` | LangChain · LlamaIndex · CrewAI · model serving · training |
280
398
  | [`openframe-suite`](https://github.com/Furious-Meteors/openframe-suite) | `pip install openframe-suite[all]` | Full platform — installs everything |
281
399
 
282
- All packages pin `openframe-core>=1.0,<2`.
400
+ All packages pin `openframe-core>=3.0,<4`.
283
401
 
284
402
  ---
285
403
 
@@ -291,11 +409,11 @@ Full documentation — architecture, module reference, runbooks, developer guide
291
409
 
292
410
  | Section | Contents |
293
411
  |---|---|
294
- | [System Overview](https://furious-meteors.github.io/openframe-core/technical/overview/system-overview/) | Seven-module model, dependency order, key design properties |
295
- | [Package Journey](https://furious-meteors.github.io/openframe-core/technical/architecture/package-journey/) | How a request flows from template wiring through adapter to response |
296
- | [ADRs](https://furious-meteors.github.io/openframe-core/technical/architecture/adrs/adr-001-namespace/) | Five architectural decision records |
412
+ | [System Architecture](https://furious-meteors.github.io/openframe-core/technical/architecture/system-architecture/) | Unified contract layer, module dependency DAG, the hexagon's two sides |
413
+ | [ADR-006](https://furious-meteors.github.io/openframe-core/technical/architecture/adrs/adr-006-unified-port-lifecycle/) | The unified port + lifecycle contract, and the unified error hierarchy |
414
+ | [Capability Taxonomy](https://furious-meteors.github.io/openframe-core/technical/architecture/capability-taxonomy/) | The `Capability` enum reference |
415
+ | [Error Taxonomy](https://furious-meteors.github.io/openframe-core/technical/architecture/error-taxonomy/) | The `OpenFrameError` hierarchy and `domain.kind` code convention |
297
416
  | [Module Reference](https://furious-meteors.github.io/openframe-core/code/modules/) | Every public class, method, parameter, return type, and raises |
298
- | [Runbooks](https://furious-meteors.github.io/openframe-core/technical/operations/) | Six operational failure scenarios with recovery steps |
299
417
  | [Developer Guide](https://furious-meteors.github.io/openframe-core/developer-guide/quick-start/) | Quick start, how it works, first code change, debugging |
300
418
 
301
419
  ---