julewire-core 1.0.1 → 1.1.0

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 (137) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/docs/configuration.md +14 -7
  4. data/docs/context-and-propagation.md +20 -0
  5. data/docs/contracts.md +23 -49
  6. data/docs/extensions-and-api.md +78 -82
  7. data/docs/health-schema.md +12 -2
  8. data/docs/internals.md +21 -20
  9. data/docs/outputs-and-lifecycle.md +10 -6
  10. data/docs/records-and-data-policy.md +36 -5
  11. data/lib/julewire/core/cli/doctor.rb +20 -16
  12. data/lib/julewire/core/cli/line_helpers.rb +23 -12
  13. data/lib/julewire/core/cli/log_formats/core_json_decoder.rb +3 -3
  14. data/lib/julewire/core/cli/log_formats.rb +9 -9
  15. data/lib/julewire/core/cli/tail.rb +19 -14
  16. data/lib/julewire/core/cli/transcode.rb +4 -5
  17. data/lib/julewire/core/cli.rb +1 -1
  18. data/lib/julewire/core/configuration.rb +1 -1
  19. data/lib/julewire/core/context_store.rb +51 -87
  20. data/lib/julewire/core/destinations/chaos_output.rb +2 -2
  21. data/lib/julewire/core/destinations/collection.rb +8 -16
  22. data/lib/julewire/core/destinations/definition.rb +1 -9
  23. data/lib/julewire/core/destinations/destination.rb +13 -63
  24. data/lib/julewire/core/destinations/processor_handling.rb +78 -0
  25. data/lib/julewire/core/destinations/registry.rb +1 -1
  26. data/lib/julewire/core/destinations/sink.rb +0 -1
  27. data/lib/julewire/core/destinations/synchronized_output.rb +14 -13
  28. data/lib/julewire/core/destinations/tail_sampling.rb +18 -27
  29. data/lib/julewire/core/destinations/write_step.rb +5 -5
  30. data/lib/julewire/core/destinations.rb +0 -7
  31. data/lib/julewire/core/diagnostics/callback_notifier.rb +4 -2
  32. data/lib/julewire/core/diagnostics/doctor.rb +16 -9
  33. data/lib/julewire/core/diagnostics/failure_snapshot.rb +6 -2
  34. data/lib/julewire/core/diagnostics/health.rb +41 -57
  35. data/lib/julewire/core/diagnostics/integration_health_store.rb +11 -24
  36. data/lib/julewire/core/diagnostics/internal_records.rb +8 -12
  37. data/lib/julewire/core/diagnostics/invalid_severity_reporter.rb +16 -55
  38. data/lib/julewire/core/diagnostics/meta_observer.rb +37 -44
  39. data/lib/julewire/core/diagnostics/process_integration_health.rb +0 -5
  40. data/lib/julewire/core/diagnostics/tail/renderer.rb +1 -1
  41. data/lib/julewire/core/diagnostics/tail.rb +18 -22
  42. data/lib/julewire/core/execution/boundary.rb +2 -8
  43. data/lib/julewire/core/execution/handle.rb +12 -17
  44. data/lib/julewire/core/execution/lineage.rb +16 -56
  45. data/lib/julewire/core/execution/measurement_handle.rb +6 -10
  46. data/lib/julewire/core/execution/scope.rb +15 -20
  47. data/lib/julewire/core/execution/scope_fields.rb +3 -12
  48. data/lib/julewire/core/execution/scope_identity.rb +1 -9
  49. data/lib/julewire/core/execution/scope_snapshot.rb +18 -39
  50. data/lib/julewire/core/execution/summary_state.rb +16 -19
  51. data/lib/julewire/core/execution/view.rb +8 -17
  52. data/lib/julewire/core/facade_methods.rb +67 -65
  53. data/lib/julewire/core/fields/attribute_keys.rb +0 -2
  54. data/lib/julewire/core/fields/bags.rb +23 -27
  55. data/lib/julewire/core/fields/field_set.rb +29 -19
  56. data/lib/julewire/core/fields/field_stack.rb +68 -134
  57. data/lib/julewire/core/fields/internal/deletion.rb +6 -9
  58. data/lib/julewire/core/fields/internal.rb +19 -38
  59. data/lib/julewire/core/fields/lookup.rb +9 -13
  60. data/lib/julewire/core/fields/section_proxy.rb +1 -9
  61. data/lib/julewire/core/fields/stack_set.rb +7 -12
  62. data/lib/julewire/core/fields/static_labels.rb +1 -1
  63. data/lib/julewire/core/integration/configurable.rb +4 -3
  64. data/lib/julewire/core/integration/destination_health.rb +17 -7
  65. data/lib/julewire/core/integration/event_subscriber.rb +1 -1
  66. data/lib/julewire/core/integration/facade.rb +12 -15
  67. data/lib/julewire/core/integration/fork_hooks.rb +12 -36
  68. data/lib/julewire/core/integration/health.rb +0 -3
  69. data/lib/julewire/core/integration/ivar_state.rb +0 -5
  70. data/lib/julewire/core/integration/protocol.rb +20 -0
  71. data/lib/julewire/core/integration/settings.rb +4 -4
  72. data/lib/julewire/core/integration/subscriber_install.rb +2 -2
  73. data/lib/julewire/core/integration/subscription.rb +3 -2
  74. data/lib/julewire/core/integration/values.rb +0 -17
  75. data/lib/julewire/core/local_storage.rb +3 -19
  76. data/lib/julewire/core/processing/invalid_result_failure.rb +23 -0
  77. data/lib/julewire/core/processing/level_threshold.rb +7 -14
  78. data/lib/julewire/core/processing/match.rb +6 -4
  79. data/lib/julewire/core/processing/pipeline.rb +46 -40
  80. data/lib/julewire/core/processing/processor_chain.rb +22 -11
  81. data/lib/julewire/core/processing/processor_registry.rb +3 -4
  82. data/lib/julewire/core/processing/record_field_transform.rb +0 -4
  83. data/lib/julewire/core/processing/sampling.rb +1 -3
  84. data/lib/julewire/core/processing.rb +2 -2
  85. data/lib/julewire/core/propagation/carrier.rb +14 -14
  86. data/lib/julewire/core/propagation.rb +15 -2
  87. data/lib/julewire/core/records/build_input.rb +75 -0
  88. data/lib/julewire/core/records/console_formatter.rb +6 -6
  89. data/lib/julewire/core/records/deconstruct.rb +1 -1
  90. data/lib/julewire/core/records/display_message.rb +2 -4
  91. data/lib/julewire/core/records/draft.rb +125 -262
  92. data/lib/julewire/core/records/lazy_emit_input.rb +2 -12
  93. data/lib/julewire/core/records/metadata.rb +4 -3
  94. data/lib/julewire/core/records/public_projection.rb +2 -2
  95. data/lib/julewire/core/records/raw_input.rb +1 -1
  96. data/lib/julewire/core/records/record.rb +19 -37
  97. data/lib/julewire/core/records/severity.rb +7 -15
  98. data/lib/julewire/core/runtime.rb +67 -71
  99. data/lib/julewire/core/runtime_registry.rb +20 -15
  100. data/lib/julewire/core/runtime_state.rb +5 -3
  101. data/lib/julewire/core/scheduling/shared_scheduler.rb +28 -22
  102. data/lib/julewire/core/serialization/backtrace_limiter.rb +13 -9
  103. data/lib/julewire/core/serialization/bounded_transform.rb +2 -12
  104. data/lib/julewire/core/serialization/bounded_traversal.rb +49 -66
  105. data/lib/julewire/core/serialization/deep_compact_empty.rb +9 -11
  106. data/lib/julewire/core/serialization/deep_freeze.rb +45 -6
  107. data/lib/julewire/core/serialization/encoding_sanitizer.rb +2 -3
  108. data/lib/julewire/core/serialization/exception_shape.rb +7 -6
  109. data/lib/julewire/core/serialization/json_encoder.rb +10 -13
  110. data/lib/julewire/core/serialization/serializer.rb +23 -50
  111. data/lib/julewire/core/serialization/serializer_pool.rb +9 -0
  112. data/lib/julewire/core/serialization/text_encoder.rb +5 -5
  113. data/lib/julewire/core/serialization/truncation_metadata.rb +63 -48
  114. data/lib/julewire/core/serialization/value_copy.rb +60 -227
  115. data/lib/julewire/core/serialization/value_traversal.rb +10 -127
  116. data/lib/julewire/core/testing.rb +6 -56
  117. data/lib/julewire/core/validation.rb +2 -3
  118. data/lib/julewire/core/version.rb +1 -1
  119. data/lib/julewire/core.rb +2 -7
  120. metadata +6 -19
  121. data/lib/julewire/core/scheduling/deadline_scheduler.rb +0 -207
  122. data/lib/julewire/core/testing/chaos/catalog.rb +0 -72
  123. data/lib/julewire/core/testing/chaos/core_runtime.rb +0 -120
  124. data/lib/julewire/core/testing/chaos/destination.rb +0 -55
  125. data/lib/julewire/core/testing/chaos/emitter.rb +0 -20
  126. data/lib/julewire/core/testing/chaos/raising_output.rb +0 -42
  127. data/lib/julewire/core/testing/chaos.rb +0 -80
  128. data/lib/julewire/core/testing/contracts/component.rb +0 -162
  129. data/lib/julewire/core/testing/contracts/deadline_scheduler.rb +0 -59
  130. data/lib/julewire/core/testing/contracts/integration.rb +0 -166
  131. data/lib/julewire/core/testing/contracts/integration_fields.rb +0 -36
  132. data/lib/julewire/core/testing/contracts/record_draft.rb +0 -37
  133. data/lib/julewire/core/testing/contracts/runtime.rb +0 -178
  134. data/lib/julewire/core/testing/contracts/wire.rb +0 -60
  135. data/lib/julewire/core/testing/contracts.rb +0 -24
  136. data/lib/julewire/core/testing/coverage.rb +0 -58
  137. data/lib/julewire/core/testing/test_reports.rb +0 -78
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 438690176aa21990767656cfc25cc35ebc89563611eb14f05875c463ef6e54f5
4
- data.tar.gz: 6ce7ec058019b2ef04a84ec10f9f5fa22d73014b1449e272b8727fc1cbd9d45a
3
+ metadata.gz: 8dcf4adb34508c1956a296d585981b88af27d8be7d0e63e9a3fe130b2c835f3b
4
+ data.tar.gz: 66cf51de3387a96f280c23badc8d847b07c53f069bd1aa5bcc7d15f72bfb4cff
5
5
  SHA512:
6
- metadata.gz: b965bae8da19cd50a0acf2df7d2005bb421c4b256fc286c7cfb89d2c58b034eefcdaafded7bfacf02243948f63977e4d390cde723f4f1c3f34f3b646dc9ed96d
7
- data.tar.gz: a9623120a2bb1c1af5a5c2c877bfcdc74a71225f6826781348fa479c223dc167ff838fa60b8b0759d829307c1f4a180497746a132b7ab65de94d2f7ceb2f2bb1
6
+ metadata.gz: e5f73d78908ae3a654c69f91eb05aa97d804e5ad9d087bb709044b7b8f0273c0babc5b4094096d9136378e7cc43a2ad850766cb8307e7883747408551be87424
7
+ data.tar.gz: eef32910880411f8760142726eb4453b938f073124688ff992bbf3d59a0c6f9a3f3d70a95a020946015d970e7694ba551785436d9755b0faf215bf599769b47e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  ## Unreleased
2
2
 
3
+ ## 1.1.0 - 2026-07-20
4
+
5
+ - Make carrier extraction status public via `Carrier::Extracted` and rename the
6
+ convenience reader to `extract_envelope`.
7
+ - Enforce recursive Symbol keys for owned records and protocols; reject invalid
8
+ processor results and malformed propagation sections instead of normalizing
9
+ or ignoring them.
10
+ - Require canonical lowercase severity Symbols while retaining case-insensitive
11
+ String and standard-library Logger integer ingress.
12
+ - Harden bounded serialization, `RecordDraft` isolation/finalization, and
13
+ processor, summary, and lifecycle failure health; remove reusable bounded
14
+ transforms and the undocumented `freeze_sections` mode.
15
+ - Remove `Julewire::Testing::Contracts` and `Julewire::Testing::Chaos`; the
16
+ testing API now contains only capture and null-output observation fixtures.
17
+ - Remove the generic deadline-scheduler SPI; main-process integrations now use
18
+ only the process-owned shared scheduler, leaving Ractor-safe timeout ownership
19
+ to the Ractor integration.
20
+
3
21
  ## 1.0.1 - 2026-06-25
4
22
 
5
23
  - Harden bounded traversal, ingress copying, and carrier extraction against
@@ -38,6 +38,10 @@ end
38
38
  | `on_failure` | Best-effort callback for contained core failures. |
39
39
  | `on_drop` | Best-effort callback for operational drops after a record reaches destination/output handling. |
40
40
 
41
+ Severity Symbols must use canonical lowercase values. String configuration
42
+ values are case-insensitive, and standard-library Logger severity integers are
43
+ accepted.
44
+
41
45
  Timeout values must be `nil` or non-negative finite numerics.
42
46
  `error_backtrace_lines` must be a non-negative integer.
43
47
 
@@ -117,10 +121,11 @@ processors. Output-specific shape belongs in destination formatters. Processors
117
121
  are the mutation stage.
118
122
 
119
123
  Processors receive the current `Julewire::RecordDraft`. Mutate the draft
120
- directly. Return `:drop` to stop delivery or a different `Julewire::RecordDraft` to
121
- replace the current draft; any other return value is ignored. Processors own
122
- the draft until the final record boundary, so direct mutation is the normal hot
123
- path:
124
+ directly and return `nil` to keep it. Return `:drop` to stop delivery or a
125
+ different `Julewire::RecordDraft` to replace the current draft. Unsupported
126
+ non-`nil` return values keep the current draft but increment `processor_invalid`
127
+ and record last-failure metadata. Processors own the draft until the final
128
+ record boundary, so direct mutation is the normal hot path:
124
129
 
125
130
  ```ruby
126
131
  class AddTenantLabel
@@ -155,11 +160,13 @@ Processor exceptions use the registration policy:
155
160
  | `on_error` | Behavior |
156
161
  | --- | --- |
157
162
  | `:fail_closed` | Default. Emit a `julewire.processor_error` replacement record and stop the chain. |
158
- | `:fail_open` | Record the failure, keep the current draft, and continue with later processors. |
163
+ | `:fail_open` | Record the failure and continue only when the current draft still satisfies the owned record contract. |
159
164
  | `:drop` | Record the failure and suppress the record. |
160
165
 
161
- Public emit input remains defensive. The final immutable record boundary
162
- validates the Julewire record shape before destinations see it.
166
+ Public emit input remains defensive. Core validates the draft after every
167
+ processor, so a malformed mutation is attributed and dropped before a later
168
+ processor or destination sees it. The final immutable record boundary still
169
+ validates the Julewire record shape.
163
170
 
164
171
  Destination-local processors run after the global processor chain and before
165
172
  one destination formats the record. Use them for sink-specific policy such as
@@ -225,9 +225,22 @@ Execution metadata is restored as ordinary execution fields. Use
225
225
  `Julewire.with_execution(..., fields: { trace_id: "..." })` for custom
226
226
  execution fields at the public API boundary.
227
227
 
228
+ Direct `Propagation.restore` treats its envelope as public/unowned input:
229
+ String keys are normalized, but `_julewire_truncation` remains reserved and is
230
+ rejected. `Carrier.restore` is the owned decoded-wire path; it preserves
231
+ Julewire-generated truncation metadata while restoring the carrier envelope.
232
+ Bridge code may also call `Propagation.restore(..., owned: true)` for a local
233
+ Julewire-owned envelope. That strict path requires a recursively Symbol-keyed
234
+ Hash and requires every present propagation section to be a Hash; it does not
235
+ replace malformed owned sections with empty hashes.
236
+
228
237
  Propagation serializes values through the core serializer. That means values
229
238
  cross a log-safe boundary, not an object-identity boundary.
230
239
 
240
+ Carrier JSON decode is a key-normalization boundary: decoded String keys become
241
+ Symbols before restore. Once restored, context and carry are owned Symbol-key
242
+ data; integrations must not add String-key overlays.
243
+
231
244
  Core does not redact propagation envelopes. Do not put secrets in context or
232
245
  carry unless your app or processor policy handles them.
233
246
 
@@ -278,6 +291,11 @@ headers = Julewire::Core::Propagation::Carrier.inject({}, max_bytes: 8 * 1024)
278
291
  # => nil when the serialized carrier is too large; the carrier key is removed
279
292
  ```
280
293
 
294
+ Integrations that need failure status should call `Carrier.extract_result` and
295
+ inspect its public `Carrier::Extracted` fields: `status`, `reason`, `error`,
296
+ and `envelope`. Use `Carrier.extract_envelope` for convenience-only restore
297
+ paths.
298
+
281
299
  The default carrier key is `"julewire"`. Carriers are intentionally
282
300
  provider-neutral: core stores only the Julewire propagation envelope. External
283
301
  header conventions stay outside core.
@@ -343,6 +361,8 @@ counters.
343
361
  - current fiber context store
344
362
  - the old active pipeline/output lifecycle
345
363
  - live runtime post-close drop and callback-failure state
364
+ - registered named runtimes and their pipelines when called through the main
365
+ `Julewire` facade; a later named lookup creates a fresh runtime
346
366
 
347
367
  `health[:counts]` is different: it is monotonic for the current runtime object
348
368
  and survives `reset!`. Use it for process-lifetime runtime lifecycle scrapes.
data/docs/contracts.md CHANGED
@@ -60,53 +60,18 @@ general application surface:
60
60
  - `Julewire::TailSampling` as a destination wrapper for execution-level tail
61
61
  sampling.
62
62
  - `Julewire::Testing::CaptureDestination`, `Julewire::Testing::NullOutput`,
63
- `Julewire::Testing.capture`, `Julewire::Testing.configure_capture_destination`,
64
- `Julewire::Testing::Contracts`, `Julewire::Testing::Chaos`, and
65
- `Julewire::Testing::Coverage` as extension test support.
63
+ `Julewire::Testing.capture`, and
64
+ `Julewire::Testing.configure_capture_destination` as small extension test
65
+ fixtures.
66
66
  - `RuntimeLocator.current.emit_without_level` for host-process integrations
67
67
  that already apply their framework's level gate.
68
68
 
69
69
  Extensions should consume these contracts rather than reaching into pipeline,
70
70
  runtime, storage, or destination internals.
71
71
 
72
- Testing support is shipped for integration authors, but helper names may still
73
- change when the ecosystem cleanup demands it.
74
-
75
- `Julewire::Testing::Contracts` currently ships these shared assertions for
76
- extension and integration gems:
77
-
78
- - `assert_julewire_bounded_transform_spi_contract`
79
- - `assert_julewire_deadline_scheduler_spi_contract`
80
- - `assert_julewire_destination_contract`
81
- - `assert_julewire_execution_boundary_contract`
82
- - `assert_julewire_failure_containment_contract`
83
- - `assert_julewire_formatter_contract`
84
- - `assert_julewire_integration_failure_contract`
85
- - `assert_julewire_integration_health_contract`
86
- - `assert_julewire_integration_ivar_state_contract`
87
- - `assert_julewire_integration_payload_contract`
88
- - `assert_julewire_integration_spi_contract`
89
- - `assert_julewire_integration_timestamp_contract`
90
- - `assert_julewire_integration_value_contract`
91
- - `assert_julewire_processor_contract`
92
- - `assert_julewire_propagation_contract`
93
- - `assert_julewire_record_draft_transform_contract`
94
- - `assert_julewire_record_shape_contract`
95
- - `assert_julewire_record_source_contract`
96
- - `assert_julewire_runtime_integration_contract`
97
- - `assert_julewire_truncation_marker_spi_contract`
98
- - `assert_julewire_validation_spi_contract`
99
-
100
- `Julewire::Testing::Chaos` currently ships containment helpers for extension
101
- test suites:
102
-
103
- - `assert_contained`
104
- - `assert_core_runtime_containment`
105
- - `assert_destination_chaos_contract`
106
- - `assert_discovered_chaos_contracts`
107
- - `assert_emitter_chaos_contract`
108
- - `catalog`
109
- - `raiser`
72
+ Core does not ship an assertion DSL or exception-corpus runner. Integration
73
+ tests exercise their production formatter, destination, processor, subscriber,
74
+ and framework boundary directly.
110
75
 
111
76
  ## Integration SPI
112
77
 
@@ -118,6 +83,8 @@ but they are not intended as general application API:
118
83
  integration health and scoped health wrappers.
119
84
  - `Julewire::Core::Integration::Facade` for integration-owned emits,
120
85
  execution boundaries, field overlays, and summary enrichment.
86
+ - `Julewire::Core::Integration::Protocol` for recursive, non-normalizing
87
+ validation of Symbol-keyed integration protocol data.
121
88
  - `Julewire::Core::Integration::Values::Read` for hash/object value reads
122
89
  including `value`, `hash_value`, `nested_value`, `path_value`,
123
90
  `first_value`, and `blank?`.
@@ -133,7 +100,10 @@ but they are not intended as general application API:
133
100
  snapshots without exposing core's internal health cells.
134
101
  - `Julewire::Core::Destinations::WriteStep` for destination-style integrations
135
102
  that reuse core's format, encode, bound, write, and counter sequence while
136
- keeping their own lifecycle and failure policy.
103
+ keeping their own lifecycle and failure policy. It returns `true` only when
104
+ the output accepted the encoded record; handled drops and transform failures
105
+ return `false` after recording health/loss metadata through the supplied
106
+ callbacks.
137
107
  - `Julewire::Core::CLI::LogFormats` for provider-owned CLI file-tail decoding
138
108
  and transcoding formats.
139
109
  - `Julewire::Core::Processing.register` for integration-owned processor kinds,
@@ -148,14 +118,13 @@ but they are not intended as general application API:
148
118
  - `Julewire::Core.sentinel(:name)` for readable, frozen identity markers when
149
119
  an integration needs a private empty or missing-value sentinel.
150
120
  - `Julewire::Core.deep_compact_empty` for core-compatible empty-field pruning.
151
- - `Julewire::Core::Scheduling::DeadlineScheduler` for integration-local timeout callbacks
152
- that need Julewire's fork-reset behavior.
153
121
  - `Julewire::Core::Scheduling::SharedScheduler` for process-wide main-ractor
154
122
  timeouts shared by integrations that should not own separate scheduler
155
123
  threads.
156
124
  - `Julewire::Core::Validation` for shared option and limit validation.
157
- - `Julewire::Core::Serialization::BoundedTransform` for processors and integrations that need
158
- core-compatible bounded traversal before core serializes the result.
125
+ - `Julewire::Core::Serialization::BoundedTransform.call` for processors and
126
+ integrations that need core-compatible bounded traversal before core
127
+ serializes the result.
159
128
  - `Julewire::Core::Diagnostics::CallbackNotifier` and
160
129
  `Julewire::Core::Diagnostics::FailureSnapshot` for destination-style
161
130
  integrations that need core-compatible callback and health failure shape.
@@ -181,6 +150,8 @@ different Ruby isolation boundary:
181
150
  - `Julewire::Core::UNSET`, `Julewire::Core.emit_input`, and
182
151
  `Julewire::Core::Records::LazyEmitInput` to mirror facade emit semantics
183
152
  across an isolation boundary.
153
+ - `Julewire::Core::Records::BuildInput` to canonicalize public emit fields or
154
+ validate integration-owned record input before strict bridge serialization.
184
155
  - `Julewire::Core::Execution::Boundary` for shared execution boundary behavior.
185
156
  - `Julewire::Core::ContextStore.current`,
186
157
  `Julewire::Core::Fields::ContextProxy`,
@@ -188,14 +159,17 @@ different Ruby isolation boundary:
188
159
  `Julewire::Core::Fields::CarryProxy`, and
189
160
  `Julewire::Core::Fields::SummaryProxy` for bridge-local field bags.
190
161
  - `Julewire::Core::Execution::ScopeSnapshot` for detached execution scope transfer.
162
+ - `emit_integration(record, enforce_level:)` on bridge runtimes for strict
163
+ integration-owned emits inside the isolation boundary.
191
164
  - Parent-runtime hooks: `emit_envelope`, `emit_summary_record`, and `flush`.
192
165
  `emit_envelope` accepts detached input, context, carry, attributes, neutral,
193
166
  scope snapshot, `enforce_level:`, and `owned:`. Bridges pass `owned: true`
194
167
  only for envelopes that came from Julewire's own wire format.
195
168
 
196
- Bridge runtimes may expose `emit_without_level` when integration code inside
197
- the bridge has already applied its own level gate. Parent runtime labels,
198
- processors, destinations, and outputs remain parent-owned.
169
+ Bridge runtimes may expose `emit_without_level` for public bridge-local callers
170
+ that already applied a level gate. Integration code uses `emit_integration` so
171
+ owned data never falls back through public normalization. Parent runtime
172
+ labels, processors, destinations, and outputs remain parent-owned.
199
173
 
200
174
  ## Internal implementation
201
175
 
@@ -29,7 +29,7 @@ Allowed processor returns:
29
29
  nil # draft was mutated in place or unchanged
30
30
  draft # explicit draft return
31
31
  :drop # stop delivery
32
- anything else # ignored; current draft continues
32
+ anything else # current draft continues and health records it
33
33
  ```
34
34
 
35
35
  Mutate the draft for ordinary enrichment:
@@ -46,6 +46,12 @@ primary processor API:
46
46
  draft.fetch(:context).fetch(:account)[:id] = "changed"
47
47
  ```
48
48
 
49
+ Draft data is already normalized. Processors must use Symbol keys for every
50
+ new or replacement hash key; processors are not a String-key normalization
51
+ boundary. Core validates the current draft after every processor. Invalid data
52
+ is attributed to the processor that produced it, reported, and dropped before a
53
+ later processor can observe it.
54
+
49
55
  Use transform helpers when replacing values or sections. They invalidate cached
50
56
  records and keep execution lineage when execution identity is unchanged:
51
57
 
@@ -54,6 +60,11 @@ draft.transform_field!(:severity) { :warn }
54
60
  draft.transform_section!(:payload) { |payload| payload.merge(sampled: true) }
55
61
  ```
56
62
 
63
+ Transform identifiers are strict Symbols. `transform_field!` accepts only
64
+ known record fields, and `transform_section!` accepts only documented Hash
65
+ sections. These processor-owned helpers do not normalize String identifiers or
66
+ create ad hoc top-level fields.
67
+
57
68
  Use `transform_record!` for whole-record replacement transforms:
58
69
 
59
70
  ```ruby
@@ -95,6 +106,9 @@ config.processors.use Julewire::Match.new do
95
106
  end
96
107
  ```
97
108
 
109
+ `Match` condition keys are Symbol keys. A condition whose pattern is `nil`
110
+ matches a present field with a nil value; it does not match a missing field.
111
+
98
112
  For deterministic head sampling, use the registered `:sampling` processor:
99
113
 
100
114
  ```ruby
@@ -115,13 +129,14 @@ The default is `on_error: :fail_closed`: core attempts to emit a minimal
115
129
  `julewire.processor_error` record, the original record is not delivered, and
116
130
  later processors are not run. Use `on_error: :fail_open` for non-critical
117
131
  enrichment processors that should record the failure, keep the current draft,
118
- and continue. Use `on_error: :drop` when a failing processor should suppress
119
- the record. `on_error:` is a registry option, not a processor constructor
120
- keyword.
132
+ and continue, provided the draft still satisfies the owned record contract.
133
+ Fail-open never forwards malformed data. Use `on_error: :drop` when a failing
134
+ processor should suppress the record. `on_error:` is a registry option, not a
135
+ processor constructor keyword.
121
136
 
122
- Non-raising draft corruption is detected at the final immutable
123
- `Julewire::Record` boundary and contained as an `emit_record` failure
124
- without per-processor attribution.
137
+ Non-raising draft corruption is detected immediately after the processor and
138
+ reported as `:processor_data` or `:destination_processor_data` before the
139
+ record is dropped.
125
140
 
126
141
  Processors can inspect execution lineage before the default formatter strips it
127
142
  from public output. Promote only the pieces you want to expose:
@@ -162,10 +177,12 @@ Application emit input crosses a few small objects before it becomes a draft:
162
177
  | Facade merge | `Core.emit_input` | Combine positional input and keyword fields without normalizing app objects. |
163
178
  | Lazy block | `Records::LazyEmitInput` | Keep block-built payloads lazy until the level gate passes and preserve eager severity helpers. |
164
179
  | Threshold peek | `Records::RawInput` | Read severity, source, and event from raw input without building a record. |
165
- | Draft build | `Draft::BuildInput` | Split raw input into normalized top-level fields plus payload. |
180
+ | Draft build | `Records::BuildInput` | Split public input into known top-level fields plus payload, or validate strict owned input. |
166
181
 
167
- This split keeps below-threshold eager input and lazy blocks cheap while the
168
- final immutable `Record` boundary still validates the full shape.
182
+ This split keeps below-threshold eager input and lazy blocks cheap. Owned
183
+ integration input is validated before processors, each processor result is
184
+ validated before the next processor, and the immutable `Record` boundary keeps
185
+ the destination contract explicit.
169
186
 
170
187
  ## Formatters
171
188
 
@@ -324,7 +341,7 @@ parts with important ownership or boundary rules.
324
341
  `EncodingSanitizer.call` repairs strings into valid UTF-8. It is intentionally
325
342
  string-only; passing other objects is a type error.
326
343
 
327
- `FieldSet` is the public helper for integration-owned field hashes. Its
344
+ `FieldSet` is the public helper for defensive field hashes. Its
328
345
  documented surface is:
329
346
 
330
347
  - `coerce`
@@ -336,8 +353,11 @@ documented surface is:
336
353
 
337
354
  `coerce`, `merge`, and `merge!` normalize string keys to symbols and
338
355
  defensive-copy values before inserting them, so later caller mutation does not
339
- mutate core field containers. Use symbol keys after that boundary. `VALUE_KEY`
340
- is the key used when non-hash field input is wrapped instead of dropped.
356
+ mutate core field containers. They are public-ingress helpers, not repairs for
357
+ integration-owned or cross-gem protocol data. Use `Integration::Protocol` to
358
+ validate owned data and keep Symbol keys after the ingress boundary.
359
+ `VALUE_KEY` is the key used when non-hash field input is wrapped instead of
360
+ dropped.
341
361
 
342
362
  Other `FieldSet` singleton helpers are core-internal implementation support and
343
363
  are not part of the extension contract.
@@ -349,13 +369,17 @@ Encoders and transport boundaries that need pure log-safe data should use
349
369
  `Serializer.call` there.
350
370
 
351
371
  `RecordFieldTransform` walks core's normalized record containers with
352
- `BoundedTransform`. It owns record-shape policy only; processors supply the
372
+ `BoundedTransform.call`. It owns record-shape policy only; processors supply the
353
373
  actual filtering or replacement policy.
354
374
 
355
375
  `Carrier` serializes propagation envelopes into flat string carriers for
356
376
  external boundaries. It is provider-neutral and does not parse or synthesize
357
377
  external headers. Use `max_bytes:` to leave a carrier unchanged and return
358
378
  `nil` when the serialized envelope is too large for the target boundary.
379
+ Use `Carrier.extract_result` from integrations that need health/status detail.
380
+ It returns a `Carrier::Extracted` value with `status`, `reason`, `error`, and
381
+ `envelope`. Use `Carrier.extract_envelope` only when a best-effort envelope hash
382
+ is enough.
359
383
 
360
384
  `Julewire::RecordDraft.build` is the raw-input construction path used by core and
361
385
  integration code. `Julewire::RecordDraft#to_record` freezes the final normalized data into
@@ -366,6 +390,10 @@ symbol-key normalized record hash and needs the immutable destination shape.
366
390
  That path validates the strict internal contract; it does not clean up
367
391
  JSON-style or user-input hashes. Use `RecordDraft.build` at raw boundaries.
368
392
 
393
+ Summary finalizer failure callbacks receive the contained error plus
394
+ `phase: :summary_finish` or `phase: :summary_emit`, so integration callbacks
395
+ should accept keyword metadata.
396
+
369
397
  ## Public Facade
370
398
 
371
399
  `contracts.md` owns the public facade inventory. This section covers usage
@@ -376,12 +404,15 @@ Integrations that keep process-local state can register a reset hook with
376
404
  component: :component_name) { ... }`. The hook runs after core has refreshed its
377
405
  own process-local state and after the active pipeline has forwarded
378
406
  `after_fork!` to destinations.
407
+ Both hook names are strict integration identifiers: pass non-empty Symbols.
408
+ String or non-Symbol names raise at registration and are never normalized.
379
409
 
380
410
  `Julewire.observe_self!(runtime_name = :default, target: :meta)` starts a
381
411
  `Julewire::Core::Diagnostics::MetaObserver`. The observer samples one runtime's
382
412
  health and emits health-change records into another named runtime. Pass
383
413
  `start: false` and call `sample!` manually when deterministic polling is
384
- preferred.
414
+ preferred. Call `observer.stop!` to cancel scheduled sampling when the
415
+ observer is no longer needed.
385
416
 
386
417
  Framework and provider adapters may also use the core integration SPI. The
387
418
  `Julewire::Core::Integration` namespace is split by concern:
@@ -418,13 +449,25 @@ procs; return a normalized value or raise.
418
449
 
419
450
  Runtime access:
420
451
 
452
+ - `Integration::Protocol.validate_symbol_keys(value)` for recursive validation
453
+ of integration-owned data before it crosses an internal gem, thread, or
454
+ Ractor protocol boundary. It returns the same value and raises `TypeError`
455
+ for every non-Symbol hash key; it never normalizes protocol data. Validation
456
+ is iterative and continues beyond the later copy/serialization depth bound.
457
+ - `Integration::Protocol.validate_symbol_hash(value)` additionally requires the
458
+ owned section itself to be a `Hash`. Direct integration SPI rejects malformed
459
+ sections immediately. Runtime emit boundaries contain, report, and drop that
460
+ failure instead of emitting a record with silently removed data.
421
461
  - `Integration::Facade.with_execution` for framework integrations that
422
462
  build fresh execution attributes and want the same execution boundary as
423
463
  `Julewire.with_execution` without copying already-owned attribute hashes.
424
464
  - `Integration::Facade.emit` for framework/provider integrations that
425
465
  emit already-normalized, adapter-owned record hashes. This is not the
426
466
  app-facing `Julewire.emit` input path; integrations should pass explicit
427
- record keys such as `:event`, `:source`, `:payload`, and `:attributes`.
467
+ Symbol record keys such as `:event`, `:source`, `:payload`, and
468
+ `:attributes`. Every nested hash key must also be a Symbol; this API does
469
+ not normalize String keys. Custom fields belong inside a documented record
470
+ section; unknown top-level fields are rejected rather than ignored.
428
471
 
429
472
  Owned field overlays:
430
473
 
@@ -433,7 +476,10 @@ Owned field overlays:
433
476
  hashes around callback, request, or message processing.
434
477
  - `Integration::Facade.add_context`, `add_carry`, `add_attributes`, and
435
478
  `add_neutral` for already-normalized, adapter-owned field hashes added to the
436
- current execution or ambient context.
479
+ current execution or ambient context. Every nested hash key must be a Symbol;
480
+ these owned overlays do not normalize String keys. Non-Hash values are
481
+ contract errors and are never silently ignored, including when no execution
482
+ scope is active.
437
483
  `add_carry` and `add_neutral` are deliberate symmetry points for integrations
438
484
  that need ambient propagation or formatter-coordination fields outside a
439
485
  scoped callback.
@@ -461,9 +507,9 @@ methods or indexed access.
461
507
 
462
508
  Bounded transforms:
463
509
 
464
- - `Julewire::Core::Serialization::BoundedTransform` when a processor or adapter needs a bounded
465
- walk with core-compatible depth, array, hash, string, cycle, and truncation
466
- behavior.
510
+ - `Julewire::Core::Serialization::BoundedTransform.call` when a processor or
511
+ adapter needs a bounded walk with core-compatible depth, array, hash, string,
512
+ cycle, and truncation behavior.
467
513
  It can insert `_julewire_truncation` metadata before the final encoder sees
468
514
  the payload.
469
515
  - `Julewire::Serializer.truncation_metadata` and serializer truncation
@@ -474,67 +520,16 @@ This SPI is documented support for integration gems, but not a compatibility
474
520
  freeze. It may change when the ecosystem gets cleaner. `contracts.md` is the
475
521
  source of truth for the current tier inventory.
476
522
 
477
- ## Extension Contract Tests
478
-
479
- Extensions can require `julewire/core/testing` for small test primitives:
480
-
481
- - `Julewire::Testing::CaptureDestination`
482
- - `Julewire::Testing::NullOutput`
483
- - `Julewire::Testing.configure_capture_destination`
484
- - `Julewire::Testing::Chaos`
485
- - `Julewire::Testing::Contracts`
486
- - `Julewire::Testing::Coverage`
487
-
488
- These helpers are shipped support for Julewire extension and integration gems,
489
- not runtime application API.
490
-
491
- `Julewire::Testing::Chaos.assert_contained(test_context) { |error| ... }`
492
- runs a small `StandardError` corpus through containment checks. Use it for
493
- extension paths that promise to absorb formatter, processor, destination, or
494
- subscriber failures.
495
- `Julewire::Testing::Chaos.assert_core_runtime_containment(test_context)` runs
496
- the same corpus through core's curated runtime containment surfaces: processors,
497
- formatters, encoders, outputs, callbacks, and lifecycle hooks.
498
- `Julewire::Testing::Chaos.assert_destination_chaos_contract(...)` runs the
499
- same corpus through a destination's formatter, encoder, output or transport,
500
- and callback containment paths using destination builders supplied by the
501
- extension test.
502
- `Julewire::Testing::Chaos.assert_emitter_chaos_contract(...)` runs the same
503
- corpus through a subscriber/listener-style entrypoint while the extension test
504
- keeps ownership of framework-shaped failing inputs.
505
- `Julewire::Testing::Chaos.catalog { ... }` builds a deterministic component
506
- catalog, and `assert_discovered_chaos_contracts(...)` runs the corpus through
507
- registered processor, formatter, encoder, destination, subscriber, and listener
508
- entries. Use it when an extension can describe its containment surfaces without
509
- reflecting over framework internals.
510
- `Julewire::Testing::Chaos.raiser(error)` builds a callable that raises the
511
- supplied error.
512
-
513
- `Julewire::Testing::Contracts` contains shared extension assertions.
514
- `contracts.md` owns the current helper inventory.
515
-
516
- Contract helper tiers:
517
-
518
- - Component contracts (`processor`, `formatter`, `destination`,
519
- `record_draft`, record shape/source) are the documented extension test surface.
520
- - Runtime, execution, propagation, integration, validation, truncation, bounded
521
- transform, and scheduler contracts are integration SPI tests.
522
- - Chaos helpers are shipped support for containment checks. They are intended
523
- for extension/integration test suites, not app runtime code.
524
-
525
- The runtime integration helper emits one point record inside an execution,
526
- adds context, carry, and summary data, flushes, and asserts that destination
527
- health is visible. Extensions provide their own output decoder and record paths,
528
- because formatters may move Julewire fields into a different output shape.
529
-
530
- `Julewire::Testing::Coverage` is shipped test support for Julewire
531
- extension gems. It only requires SimpleCov when `Coverage.start!` runs with
532
- `COVERAGE` set, so runtime users do not load coverage dependencies.
533
-
534
- The execution-boundary helper gives integration and propagation extensions the
535
- same probe data and lets the extension run it through its own unit of work. The
536
- failure-containment helper verifies that extension failures do not escape
537
- application calls and that health reports degradation.
523
+ ## Extension Tests
524
+
525
+ Extensions can require `julewire/core/testing` for four small observation
526
+ fixtures: `Julewire::Testing::CaptureDestination`,
527
+ `Julewire::Testing::NullOutput`, `Julewire::Testing.capture`, and
528
+ `Julewire::Testing.configure_capture_destination`.
529
+
530
+ Core does not ship an assertion DSL or failure-corpus harness. Integration gems
531
+ test their production formatter, destination, processor, subscriber, and
532
+ framework boundary directly.
538
533
 
539
534
  ## Internal or Advanced
540
535
 
@@ -558,7 +553,8 @@ building another internal record.
558
553
 
559
554
  `Julewire::Core::RuntimeLocator.current=` is an advanced runtime hook for bridge
560
555
  code. A bridge runtime must support the child-side facade methods it exposes and
561
- the parent-side bridge calls it forwards: `emit_envelope`,
556
+ the facade reset hook `reset_facade!`, strict `emit_integration`, plus the
557
+ parent-side bridge calls it forwards: `emit_envelope`,
562
558
  `emit_summary_record`, and `flush`. It is deliberately duck-typed; incompatible
563
559
  runtimes fail when called, so application code should not replace it casually.
564
560
 
@@ -3,6 +3,12 @@
3
3
  `Julewire.health` is an in-process operational snapshot. It is useful for
4
4
  metrics, smoke checks, and debug pages. It is not a delivery receipt.
5
5
 
6
+ Counters and diagnostic references are updated atomically. A health read is
7
+ not a transaction across those independent values: during a concurrent update,
8
+ one snapshot may combine adjacent observations (for example, a new count with
9
+ the preceding failure reference). Counters remain exact, and later reads
10
+ converge on the latest diagnostic state.
11
+
6
12
  ## Stable Fields
7
13
 
8
14
  These fields are intended for integration dashboards and contract tests:
@@ -59,9 +65,13 @@ reset on reconfigure. Top-level `generation` increments when configuration
59
65
  installs a new pipeline.
60
66
  `pipeline.counts[:processor_dropped]` counts intentional processor drops such
61
67
  as sampling decisions; processor failures are counted separately under
62
- `pipeline.counts[:processor_error]`.
68
+ `pipeline.counts[:processor_error]`. Unsupported processor return values keep
69
+ the current draft and increment `pipeline.counts[:processor_invalid]`.
70
+ Malformed processor-owned draft data also increments `processor_invalid`; core
71
+ stops the chain and counts the resulting loss under `processor_dropped`.
63
72
  Destination-local processors report the same `processor_dropped` and
64
- `processor_error` counters under that destination's `counts`.
73
+ `processor_error`/`processor_invalid` counters under that destination's
74
+ `counts`.
65
75
 
66
76
  `status` fields describe current health for the active runtime generation.
67
77
  Runtime, pipeline, and destination failure counters plus `last_failure` /
data/docs/internals.md CHANGED
@@ -11,12 +11,14 @@ The runtime stores immutable snapshots of:
11
11
  - pipeline
12
12
  - whether the active pipeline has been closed
13
13
 
14
- Writes are serialized where state transitions need serialization. Main-ractor
15
- hot-path reads use the current frozen runtime state without taking a global
16
- runtime lock; state transitions swap that reference under the runtime state
17
- mutex. Health and counters use their own synchronization outside that mutex.
18
- Non-main ractors use ractor-local runtime storage because they cannot touch the
19
- main runtime object directly.
14
+ Main-ractor hot-path reads use the current frozen runtime state without taking
15
+ a global runtime lock. State transitions replace that snapshot atomically;
16
+ configure and reset are additionally serialized with the configure mutex while
17
+ close may overlap them. The successful atomic transition owns the previous
18
+ pipeline lifecycle, so concurrent close/reset operations cannot close the same
19
+ pipeline twice. Health and counters use their own synchronization. Non-main
20
+ ractors use ractor-local runtime storage because they cannot touch the main
21
+ runtime object directly.
20
22
 
21
23
  Configuration, reconfigure, reset, and close are state transitions. They should
22
24
  stay boring and explicit.
@@ -67,10 +69,15 @@ extensions do not pay repeated equivalent-key scans.
67
69
 
68
70
  `Records::RawInput` reads app-facing emit input before record construction.
69
71
  `Integration::Values::Read` extracts best-effort values from framework objects.
70
- `Integration::Values::Shape` normalizes adapter-built hashes. `Fields::FieldSet`
71
- owns trusted field-bag copying, merging, and key normalization after values have
72
- entered core. `Fields::Lookup` is for display reads that tolerate symbol/string
73
- keys and return `nil` for unreadable inputs.
72
+ `Integration::Values::Shape` normalizes adapter-built hashes at framework
73
+ ingress. `Fields::FieldSet` owns public field-bag copying, merging, and key
74
+ normalization at application or framework ingress. Once data is owned,
75
+ `Integration::Protocol` and strict deep validation require recursively Symbol-
76
+ keyed hashes; cross-gem code does not repair String keys. `Fields::Lookup` is
77
+ for best-effort display reads with the exact requested key and returns `nil` for
78
+ unreadable inputs; it does not bridge Symbol/String key forms inside normalized
79
+ records. Presentation code that reads decoded wire payloads uses its separate
80
+ `wire_value` helper at that restore boundary.
74
81
 
75
82
  ## Context Storage
76
83
 
@@ -90,10 +97,10 @@ custom destination objects.
90
97
 
91
98
  ## Scheduling
92
99
 
93
- `Scheduling::DeadlineScheduler` is the small stdlib timer heap. Main-process
94
- diagnostics and framework integrations use `Scheduling::SharedScheduler` so
95
- they do not each keep a background timer thread. Ractor integrations keep their
96
- own schedulers because worker ractors cannot share the main scheduler object.
100
+ Main-process diagnostics and framework integrations use the process-owned
101
+ `Scheduling::SharedScheduler` so they do not each keep a background timer
102
+ thread. Ractor integrations own their stdlib timeout threads because a worker
103
+ Ractor cannot access the main scheduler's concurrent-ruby objects.
97
104
 
98
105
  ## Remote Envelope Hook
99
106
 
@@ -103,12 +110,6 @@ a normal record from input, context, attributes, carry, neutral, and that
103
110
  snapshot before emitting through the active pipeline. Bridges pass `owned: true`
104
111
  only for Julewire-owned wire data. It is not a public application API.
105
112
 
106
- ## Test Seams
107
-
108
- Some private methods are intentionally reachable through `Julewire::Testing`.
109
- They reset process-global registries or storage that normal applications should
110
- not touch directly.
111
-
112
113
  ## Emit Entrypoints
113
114
 
114
115
  | Entrypoint | Caller | Level gate | Input ownership |