julewire-core 1.0.0 → 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 (139) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/docs/advanced-configuration.md +2 -1
  4. data/docs/configuration.md +14 -7
  5. data/docs/context-and-propagation.md +26 -4
  6. data/docs/contracts.md +25 -50
  7. data/docs/extensions-and-api.md +87 -88
  8. data/docs/health-schema.md +12 -2
  9. data/docs/internals.md +24 -23
  10. data/docs/outputs-and-lifecycle.md +15 -8
  11. data/docs/records-and-data-policy.md +38 -7
  12. data/lib/julewire/core/cli/doctor.rb +20 -16
  13. data/lib/julewire/core/cli/line_helpers.rb +23 -12
  14. data/lib/julewire/core/cli/log_formats/core_json_decoder.rb +3 -3
  15. data/lib/julewire/core/cli/log_formats/record_decoder.rb +1 -1
  16. data/lib/julewire/core/cli/log_formats.rb +9 -9
  17. data/lib/julewire/core/cli/tail.rb +19 -14
  18. data/lib/julewire/core/cli/transcode.rb +4 -5
  19. data/lib/julewire/core/cli.rb +1 -1
  20. data/lib/julewire/core/configuration.rb +1 -1
  21. data/lib/julewire/core/context_store.rb +56 -87
  22. data/lib/julewire/core/destinations/chaos_output.rb +2 -2
  23. data/lib/julewire/core/destinations/collection.rb +8 -16
  24. data/lib/julewire/core/destinations/definition.rb +1 -9
  25. data/lib/julewire/core/destinations/destination.rb +16 -66
  26. data/lib/julewire/core/destinations/processor_handling.rb +78 -0
  27. data/lib/julewire/core/destinations/registry.rb +1 -1
  28. data/lib/julewire/core/destinations/sink.rb +0 -1
  29. data/lib/julewire/core/destinations/synchronized_output.rb +54 -17
  30. data/lib/julewire/core/destinations/tail_sampling.rb +18 -27
  31. data/lib/julewire/core/destinations/write_step.rb +5 -5
  32. data/lib/julewire/core/destinations.rb +0 -7
  33. data/lib/julewire/core/diagnostics/callback_notifier.rb +4 -2
  34. data/lib/julewire/core/diagnostics/doctor.rb +22 -15
  35. data/lib/julewire/core/diagnostics/failure_snapshot.rb +9 -3
  36. data/lib/julewire/core/diagnostics/health.rb +41 -57
  37. data/lib/julewire/core/diagnostics/integration_health_store.rb +11 -24
  38. data/lib/julewire/core/diagnostics/internal_records.rb +8 -12
  39. data/lib/julewire/core/diagnostics/invalid_severity_reporter.rb +16 -55
  40. data/lib/julewire/core/diagnostics/meta_observer.rb +37 -44
  41. data/lib/julewire/core/diagnostics/process_integration_health.rb +0 -5
  42. data/lib/julewire/core/diagnostics/tail/renderer.rb +1 -1
  43. data/lib/julewire/core/diagnostics/tail.rb +18 -22
  44. data/lib/julewire/core/execution/boundary.rb +2 -8
  45. data/lib/julewire/core/execution/handle.rb +14 -17
  46. data/lib/julewire/core/execution/lineage.rb +26 -62
  47. data/lib/julewire/core/execution/measurement_handle.rb +6 -10
  48. data/lib/julewire/core/execution/scope.rb +17 -22
  49. data/lib/julewire/core/execution/scope_fields.rb +4 -13
  50. data/lib/julewire/core/execution/scope_identity.rb +1 -9
  51. data/lib/julewire/core/execution/scope_snapshot.rb +18 -39
  52. data/lib/julewire/core/execution/summary_state.rb +16 -19
  53. data/lib/julewire/core/execution/view.rb +8 -17
  54. data/lib/julewire/core/facade_methods.rb +67 -65
  55. data/lib/julewire/core/fields/attribute_keys.rb +0 -2
  56. data/lib/julewire/core/fields/bags.rb +23 -27
  57. data/lib/julewire/core/fields/field_set.rb +52 -16
  58. data/lib/julewire/core/fields/field_stack.rb +69 -109
  59. data/lib/julewire/core/fields/internal/deletion.rb +6 -9
  60. data/lib/julewire/core/fields/internal.rb +35 -26
  61. data/lib/julewire/core/fields/lookup.rb +9 -13
  62. data/lib/julewire/core/fields/section_proxy.rb +1 -9
  63. data/lib/julewire/core/fields/stack_set.rb +9 -14
  64. data/lib/julewire/core/fields/static_labels.rb +1 -1
  65. data/lib/julewire/core/integration/configurable.rb +4 -3
  66. data/lib/julewire/core/integration/destination_health.rb +28 -7
  67. data/lib/julewire/core/integration/event_subscriber.rb +1 -1
  68. data/lib/julewire/core/integration/facade.rb +12 -15
  69. data/lib/julewire/core/integration/fork_hooks.rb +12 -36
  70. data/lib/julewire/core/integration/health.rb +0 -3
  71. data/lib/julewire/core/integration/ivar_state.rb +0 -5
  72. data/lib/julewire/core/integration/protocol.rb +20 -0
  73. data/lib/julewire/core/integration/settings.rb +4 -4
  74. data/lib/julewire/core/integration/subscriber_install.rb +2 -2
  75. data/lib/julewire/core/integration/subscription.rb +3 -2
  76. data/lib/julewire/core/integration/values.rb +0 -17
  77. data/lib/julewire/core/local_storage.rb +3 -19
  78. data/lib/julewire/core/processing/invalid_result_failure.rb +23 -0
  79. data/lib/julewire/core/processing/level_threshold.rb +7 -14
  80. data/lib/julewire/core/processing/match.rb +6 -4
  81. data/lib/julewire/core/processing/pipeline.rb +46 -40
  82. data/lib/julewire/core/processing/processor_chain.rb +22 -11
  83. data/lib/julewire/core/processing/processor_registry.rb +3 -4
  84. data/lib/julewire/core/processing/record_field_transform.rb +0 -4
  85. data/lib/julewire/core/processing/sampling.rb +1 -3
  86. data/lib/julewire/core/processing.rb +2 -2
  87. data/lib/julewire/core/propagation/carrier.rb +73 -19
  88. data/lib/julewire/core/propagation.rb +18 -4
  89. data/lib/julewire/core/records/build_input.rb +75 -0
  90. data/lib/julewire/core/records/console_formatter.rb +6 -6
  91. data/lib/julewire/core/records/deconstruct.rb +1 -1
  92. data/lib/julewire/core/records/display_message.rb +2 -4
  93. data/lib/julewire/core/records/draft.rb +127 -258
  94. data/lib/julewire/core/records/lazy_emit_input.rb +2 -12
  95. data/lib/julewire/core/records/metadata.rb +4 -3
  96. data/lib/julewire/core/records/public_projection.rb +2 -2
  97. data/lib/julewire/core/records/raw_input.rb +1 -1
  98. data/lib/julewire/core/records/record.rb +30 -18
  99. data/lib/julewire/core/records/severity.rb +7 -15
  100. data/lib/julewire/core/runtime.rb +87 -72
  101. data/lib/julewire/core/runtime_registry.rb +20 -15
  102. data/lib/julewire/core/runtime_state.rb +5 -3
  103. data/lib/julewire/core/scheduling/shared_scheduler.rb +28 -22
  104. data/lib/julewire/core/serialization/backtrace_limiter.rb +13 -9
  105. data/lib/julewire/core/serialization/bounded_transform.rb +3 -2
  106. data/lib/julewire/core/serialization/bounded_traversal.rb +72 -83
  107. data/lib/julewire/core/serialization/deep_compact_empty.rb +9 -11
  108. data/lib/julewire/core/serialization/deep_freeze.rb +45 -6
  109. data/lib/julewire/core/serialization/encoding_sanitizer.rb +2 -3
  110. data/lib/julewire/core/serialization/exception_shape.rb +7 -6
  111. data/lib/julewire/core/serialization/json_encoder.rb +10 -13
  112. data/lib/julewire/core/serialization/serializer.rb +22 -49
  113. data/lib/julewire/core/serialization/serializer_pool.rb +9 -0
  114. data/lib/julewire/core/serialization/text_encoder.rb +5 -5
  115. data/lib/julewire/core/serialization/truncation_metadata.rb +157 -0
  116. data/lib/julewire/core/serialization/value_copy.rb +183 -109
  117. data/lib/julewire/core/serialization/value_traversal.rb +10 -127
  118. data/lib/julewire/core/testing.rb +6 -56
  119. data/lib/julewire/core/validation.rb +2 -3
  120. data/lib/julewire/core/version.rb +1 -1
  121. data/lib/julewire/core.rb +2 -7
  122. metadata +7 -19
  123. data/lib/julewire/core/scheduling/deadline_scheduler.rb +0 -207
  124. data/lib/julewire/core/testing/chaos/catalog.rb +0 -72
  125. data/lib/julewire/core/testing/chaos/core_runtime.rb +0 -120
  126. data/lib/julewire/core/testing/chaos/destination.rb +0 -55
  127. data/lib/julewire/core/testing/chaos/emitter.rb +0 -20
  128. data/lib/julewire/core/testing/chaos/raising_output.rb +0 -42
  129. data/lib/julewire/core/testing/chaos.rb +0 -80
  130. data/lib/julewire/core/testing/contracts/component.rb +0 -162
  131. data/lib/julewire/core/testing/contracts/deadline_scheduler.rb +0 -59
  132. data/lib/julewire/core/testing/contracts/integration.rb +0 -166
  133. data/lib/julewire/core/testing/contracts/integration_fields.rb +0 -36
  134. data/lib/julewire/core/testing/contracts/record_draft.rb +0 -37
  135. data/lib/julewire/core/testing/contracts/runtime.rb +0 -178
  136. data/lib/julewire/core/testing/contracts/wire.rb +0 -60
  137. data/lib/julewire/core/testing/contracts.rb +0 -24
  138. data/lib/julewire/core/testing/coverage.rb +0 -58
  139. 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: e20df02ec73ceafd282ab52015e46a28f9aa7b1d69461e2622afc301f9e98d40
4
- data.tar.gz: 4e12bb12f787534aa5318084f6fa69fc1888590d85874d253c12dbfc6d6701f7
3
+ metadata.gz: 8dcf4adb34508c1956a296d585981b88af27d8be7d0e63e9a3fe130b2c835f3b
4
+ data.tar.gz: 66cf51de3387a96f280c23badc8d847b07c53f069bd1aa5bcc7d15f72bfb4cff
5
5
  SHA512:
6
- metadata.gz: 222e5041e3d984b1e3d50a989c461522cf7622257d1698e7e9fdee9f10f2837537a883fbfe16466ac31abf868558807e1a07642e52c894ac020bf71c628a1896
7
- data.tar.gz: 75f19494c48c6d46ae6bdf997db3605abc93323180589804d0e6338e1c7e592cd58150fbef9be4ccb5bfba577fb1086745ee8b779b6588aa7996572a55a5563f
6
+ metadata.gz: e5f73d78908ae3a654c69f91eb05aa97d804e5ad9d087bb709044b7b8f0273c0babc5b4094096d9136378e7cc43a2ad850766cb8307e7883747408551be87424
7
+ data.tar.gz: eef32910880411f8760142726eb4453b938f073124688ff992bbf3d59a0c6f9a3f3d70a95a020946015d970e7694ba551785436d9755b0faf215bf599769b47e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
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
+
21
+ ## 1.0.1 - 2026-06-25
22
+
23
+ - Harden bounded traversal, ingress copying, and carrier extraction against
24
+ noisy or hostile record shapes.
25
+ - Keep normalized record constructors strict: symbol-key contracts validate
26
+ instead of quietly normalizing pipeline-owned data.
27
+ - Default carrier extraction to the byte cap, report extraction status to
28
+ integrations, reserve truncation metadata keys at field ingress, and keep
29
+ custom normalization limits out of the thread-local copier pool.
30
+ - Keep output writes independent from flush/close lifecycle locking.
31
+
3
32
  ## 1.0.0 - 2026-06-21
4
33
 
5
34
  - Initial release: execution-scoped structured logging, propagation, bounded
@@ -13,7 +13,8 @@ is unsupported. Call `Julewire.configure` again.
13
13
  User-supplied destination formatter, encoder, output, and processor instances
14
14
  are shared by reference. Processors, formatters, and encoders must be stateless
15
15
  or otherwise reentrant; core does not synchronize them. Direct outputs are
16
- wrapped with a per-destination mutex.
16
+ wrapped with a per-destination write mutex; `flush` may overlap writes, while
17
+ terminal `close` is serialized with writes.
17
18
 
18
19
  When a reconfigure reuses the same output or destination object, core keeps that
19
20
  resource open for the new active pipeline and skips teardown through the old
@@ -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
 
@@ -241,9 +254,9 @@ Julewire::Core::Propagation.restore(envelope, link_executions: true) do
241
254
  end
242
255
  ```
243
256
 
244
- Restoring an envelope is an explicit trust decision. Core validates shape, and
245
- outbound injection can enforce a byte limit, but core does not authenticate who
246
- wrote the carrier. At untrusted boundaries, extract only the carrier fields you
257
+ Restoring an envelope is an explicit trust decision. Core validates shape and
258
+ caps inbound carrier bytes by default, but core does not authenticate who wrote
259
+ the carrier. At untrusted boundaries, extract only the carrier fields you
247
260
  intentionally accept, apply any application spoofing/filter policy first, then
248
261
  call `Carrier.restore` on that filtered carrier map.
249
262
 
@@ -269,13 +282,20 @@ Julewire::Core::Propagation::Carrier.restore(trusted) do
269
282
  end
270
283
  ```
271
284
 
272
- Use `max_bytes:` when the carrier target has practical size constraints:
285
+ Inbound extraction and restore default to `Carrier::DEFAULT_MAX_BYTES`; pass
286
+ `max_bytes: nil` only for a trusted unbounded carrier. Use `max_bytes:` on
287
+ injection when the carrier target has stricter size constraints:
273
288
 
274
289
  ```ruby
275
290
  headers = Julewire::Core::Propagation::Carrier.inject({}, max_bytes: 8 * 1024)
276
291
  # => nil when the serialized carrier is too large; the carrier key is removed
277
292
  ```
278
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
+
279
299
  The default carrier key is `"julewire"`. Carriers are intentionally
280
300
  provider-neutral: core stores only the Julewire propagation envelope. External
281
301
  header conventions stay outside core.
@@ -341,6 +361,8 @@ counters.
341
361
  - current fiber context store
342
362
  - the old active pipeline/output lifecycle
343
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
344
366
 
345
367
  `health[:counts]` is different: it is monotonic for the current runtime object
346
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,13 +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
- scope snapshot, and an `enforce_level:` flag.
166
+ scope snapshot, `enforce_level:`, and `owned:`. Bridges pass `owned: true`
167
+ only for envelopes that came from Julewire's own wire format.
194
168
 
195
- Bridge runtimes may expose `emit_without_level` when integration code inside
196
- the bridge has already applied its own level gate. Parent runtime labels,
197
- 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.
198
173
 
199
174
  ## Internal implementation
200
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
 
@@ -300,11 +317,11 @@ health
300
317
  ```
301
318
 
302
319
  Output lifecycle hooks are sync and local. Runtime timeouts provide one carried
303
- deadline while core walks destinations, but plain outputs do not receive the
304
- timeout value and core cannot interrupt blocking raw outputs. After the first
305
- attempted resource, exhausted deadlines skip later resources. Custom
306
- destinations own async drain, retry, reopen targets, rotation, and
307
- timeout-aware shutdown.
320
+ deadline while core walks destinations. Plain outputs receive the timeout only
321
+ when their lifecycle method accepts `timeout:` or `**kwargs`; core still cannot
322
+ interrupt blocking raw output code. After the first attempted resource,
323
+ exhausted deadlines skip later resources. Custom destinations own async drain,
324
+ retry, reopen targets, rotation, and timeout-aware shutdown.
308
325
 
309
326
  Plain output writes are successful when `write` returns without raising and does
310
327
  not return `false`. A plain `false` is treated as a rejected write. A rejection
@@ -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
@@ -363,6 +387,12 @@ an immutable `Record` for formatters and destinations. `Record` is not a raw
363
387
  input builder; it is the read-only destination boundary. Use
364
388
  `Record.from_normalized_hash` only when an extension already owns a complete
365
389
  symbol-key normalized record hash and needs the immutable destination shape.
390
+ That path validates the strict internal contract; it does not clean up
391
+ JSON-style or user-input hashes. Use `RecordDraft.build` at raw boundaries.
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.
366
396
 
367
397
  ## Public Facade
368
398
 
@@ -374,12 +404,15 @@ Integrations that keep process-local state can register a reset hook with
374
404
  component: :component_name) { ... }`. The hook runs after core has refreshed its
375
405
  own process-local state and after the active pipeline has forwarded
376
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.
377
409
 
378
410
  `Julewire.observe_self!(runtime_name = :default, target: :meta)` starts a
379
411
  `Julewire::Core::Diagnostics::MetaObserver`. The observer samples one runtime's
380
412
  health and emits health-change records into another named runtime. Pass
381
413
  `start: false` and call `sample!` manually when deterministic polling is
382
- preferred.
414
+ preferred. Call `observer.stop!` to cancel scheduled sampling when the
415
+ observer is no longer needed.
383
416
 
384
417
  Framework and provider adapters may also use the core integration SPI. The
385
418
  `Julewire::Core::Integration` namespace is split by concern:
@@ -416,13 +449,25 @@ procs; return a normalized value or raise.
416
449
 
417
450
  Runtime access:
418
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.
419
461
  - `Integration::Facade.with_execution` for framework integrations that
420
462
  build fresh execution attributes and want the same execution boundary as
421
463
  `Julewire.with_execution` without copying already-owned attribute hashes.
422
464
  - `Integration::Facade.emit` for framework/provider integrations that
423
465
  emit already-normalized, adapter-owned record hashes. This is not the
424
466
  app-facing `Julewire.emit` input path; integrations should pass explicit
425
- 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.
426
471
 
427
472
  Owned field overlays:
428
473
 
@@ -431,7 +476,10 @@ Owned field overlays:
431
476
  hashes around callback, request, or message processing.
432
477
  - `Integration::Facade.add_context`, `add_carry`, `add_attributes`, and
433
478
  `add_neutral` for already-normalized, adapter-owned field hashes added to the
434
- 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.
435
483
  `add_carry` and `add_neutral` are deliberate symmetry points for integrations
436
484
  that need ambient propagation or formatter-coordination fields outside a
437
485
  scoped callback.
@@ -459,9 +507,9 @@ methods or indexed access.
459
507
 
460
508
  Bounded transforms:
461
509
 
462
- - `Julewire::Core::Serialization::BoundedTransform` when a processor or adapter needs a bounded
463
- walk with core-compatible depth, array, hash, string, cycle, and truncation
464
- 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.
465
513
  It can insert `_julewire_truncation` metadata before the final encoder sees
466
514
  the payload.
467
515
  - `Julewire::Serializer.truncation_metadata` and serializer truncation
@@ -472,67 +520,16 @@ This SPI is documented support for integration gems, but not a compatibility
472
520
  freeze. It may change when the ecosystem gets cleaner. `contracts.md` is the
473
521
  source of truth for the current tier inventory.
474
522
 
475
- ## Extension Contract Tests
476
-
477
- Extensions can require `julewire/core/testing` for small test primitives:
478
-
479
- - `Julewire::Testing::CaptureDestination`
480
- - `Julewire::Testing::NullOutput`
481
- - `Julewire::Testing.configure_capture_destination`
482
- - `Julewire::Testing::Chaos`
483
- - `Julewire::Testing::Contracts`
484
- - `Julewire::Testing::Coverage`
485
-
486
- These helpers are shipped support for Julewire extension and integration gems,
487
- not runtime application API.
488
-
489
- `Julewire::Testing::Chaos.assert_contained(test_context) { |error| ... }`
490
- runs a small `StandardError` corpus through containment checks. Use it for
491
- extension paths that promise to absorb formatter, processor, destination, or
492
- subscriber failures.
493
- `Julewire::Testing::Chaos.assert_core_runtime_containment(test_context)` runs
494
- the same corpus through core's curated runtime containment surfaces: processors,
495
- formatters, encoders, outputs, callbacks, and lifecycle hooks.
496
- `Julewire::Testing::Chaos.assert_destination_chaos_contract(...)` runs the
497
- same corpus through a destination's formatter, encoder, output or transport,
498
- and callback containment paths using destination builders supplied by the
499
- extension test.
500
- `Julewire::Testing::Chaos.assert_emitter_chaos_contract(...)` runs the same
501
- corpus through a subscriber/listener-style entrypoint while the extension test
502
- keeps ownership of framework-shaped failing inputs.
503
- `Julewire::Testing::Chaos.catalog { ... }` builds a deterministic component
504
- catalog, and `assert_discovered_chaos_contracts(...)` runs the corpus through
505
- registered processor, formatter, encoder, destination, subscriber, and listener
506
- entries. Use it when an extension can describe its containment surfaces without
507
- reflecting over framework internals.
508
- `Julewire::Testing::Chaos.raiser(error)` builds a callable that raises the
509
- supplied error.
510
-
511
- `Julewire::Testing::Contracts` contains shared extension assertions.
512
- `contracts.md` owns the current helper inventory.
513
-
514
- Contract helper tiers:
515
-
516
- - Component contracts (`processor`, `formatter`, `destination`,
517
- `record_draft`, record shape/source) are the documented extension test surface.
518
- - Runtime, execution, propagation, integration, validation, truncation, bounded
519
- transform, and scheduler contracts are integration SPI tests.
520
- - Chaos helpers are shipped support for containment checks. They are intended
521
- for extension/integration test suites, not app runtime code.
522
-
523
- The runtime integration helper emits one point record inside an execution,
524
- adds context, carry, and summary data, flushes, and asserts that destination
525
- health is visible. Extensions provide their own output decoder and record paths,
526
- because formatters may move Julewire fields into a different output shape.
527
-
528
- `Julewire::Testing::Coverage` is shipped test support for Julewire
529
- extension gems. It only requires SimpleCov when `Coverage.start!` runs with
530
- `COVERAGE` set, so runtime users do not load coverage dependencies.
531
-
532
- The execution-boundary helper gives integration and propagation extensions the
533
- same probe data and lets the extension run it through its own unit of work. The
534
- failure-containment helper verifies that extension failures do not escape
535
- 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.
536
533
 
537
534
  ## Internal or Advanced
538
535
 
@@ -556,7 +553,8 @@ building another internal record.
556
553
 
557
554
  `Julewire::Core::RuntimeLocator.current=` is an advanced runtime hook for bridge
558
555
  code. A bridge runtime must support the child-side facade methods it exposes and
559
- 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`,
560
558
  `emit_summary_record`, and `flush`. It is deliberately duck-typed; incompatible
561
559
  runtimes fail when called, so application code should not replace it casually.
562
560
 
@@ -564,4 +562,5 @@ runtimes fail when called, so application code should not replace it casually.
564
562
 
565
563
  Core exposes the bridge SPI runtime envelope hook used by bridge code. The hook
566
564
  accepts detached input, context, attributes, carry, neutral data, and a scope
567
- snapshot, then routes them through the active pipeline.
565
+ snapshot, then routes them through the active pipeline. Bridge code may pass
566
+ `owned: true` only for data decoded from Julewire-owned wire formats.
@@ -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` /