qubit-value 0.13.2

Type-safe containers for single, multi-valued, and named runtime values
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
# qubit-value Architecture and Wire Design

[中文版](design.zh_CN.md) · [README](../README.md) · [User guide](user_guide.md) · [API documentation](https://docs.rs/qubit-value)

This document records the architectural boundaries and compatibility rules for
`qubit-value` 0.13. It is aimed at maintainers of this crate and authors of
protocols or downstream crates that build on its value model.

<a id="scope"></a>
## Scope

`qubit-value` is the typed runtime value layer shared by higher-level key-value
systems. It owns four responsibilities:

- representing one typed scalar, one homogeneous typed collection, and the
  explicit distinction between those shapes;
- providing strict reads and opt-in policy-based conversion;
- defining semantic equality and hashing for every supported runtime type;
- adapting runtime values to lossy Natural JSON or the type-preserving Wire V1
  contract.

The crate does not own property-name validation, schemas, source loading,
configuration precedence, persistence, transport framing, or distributed
identity. Those belong to downstream systems. `NamedValue` and
`NamedMultiValues` are lightweight name wrappers, not a general key-value
store.

The public containers and adapters are stable API surfaces. Their private
storage enums and serialization helpers are implementation details and may be
reorganized without changing observable behavior.

<a id="type-model"></a>
## Type Model

The model has two independent dimensions: runtime `DataType` and value shape.
Keeping them independent prevents collection length or JSON syntax from
silently changing a caller's contract.

| Public type | Role | Shape |
| --- | --- | --- |
| `Value` | Owns one concrete typed payload or `Unset(DataType)` | Scalar |
| `MultiValues` | Owns one homogeneous vector or an unset element type | Collection |
| `ValueContainer` | Wraps `Scalar(Value)` or `Collection(MultiValues)` | Explicit union |
| `NamedValue` | Associates a name with `Value` | Named scalar |
| `NamedMultiValues` | Associates a name with `MultiValues` | Named collection |

The closed internal value table is the single mapping between the 25 supported
`DataType` variants, Rust storage types, feature gates, Natural JSON classes,
and stable Wire V1 tags. It drives the scalar and collection representations,
constructors, borrowed projections, identity logic, and Wire DTOs. This keeps
the two container families aligned without making the generated storage enums
public.

`ValueContainer` never infers shape. In particular, a scalar `Int32(42)`, a
one-item `Int32` collection, an empty `Int32` collection, and an unset `Int32`
collection are four distinct states.

<a id="ownership-and-borrowed-views"></a>
## Ownership and Borrowed Views

`Value`, `MultiValues`, and `ValueContainer` own their payloads and expose
stable methods rather than their private representations. Cloning one of these
types clones owned payloads such as strings, maps, JSON trees, and collection
vectors.

`Value::view()` and `MultiValues::view()` return `ValueRef<'_>` and
`MultiValuesRef<'_>`. These non-exhaustive enums expose semantic variants while
borrowing non-copy payloads from the source. Copy-sized scalar values are
projected by value; strings, large values, maps, JSON trees, and collection
slices remain borrowed. Downstream matches must retain a wildcard arm because
the borrowed-view enums may grow.

The closed value table generates both views and their owned-to-borrowed
dispatch. `MultiValuesRef` exposes `data_type`, `len`, `is_empty`, and indexed
`get`, returning a `ValueRef` without building a temporary scalar container.
With `converter`, `DataConverter` accepts `ValueRef` directly. This lets
configuration Serde visitors convert original numeric types and borrow native
collections without first materializing a Natural JSON tree.

Wire ownership is also explicit:

| Adapter | Ownership | Intended use |
| --- | --- | --- |
| `ValueWireV1` | Owns a standalone versioned envelope | Store or transfer a complete Wire value |
| `ValueWirePayloadV1` | Owns an unversioned typed shape | Embed under an outer protocol version |
| `ValueWireRefV1<'a>` | Borrows a standalone envelope payload | Encode without cloning the runtime value |
| `ValueWirePayloadRefV1<'a>` | Borrows an unversioned payload | Embed and encode without cloning |

Borrowed Wire constructors are fallible. Before exposing a serializable view,
they validate invariants that JSON cannot represent, including non-finite
floats and an out-of-range `BigDecimal` scale. Their lifetime cannot outlive
the source runtime value.

<a id="value-semantics"></a>
## Value Semantics

Unset is a typed absence, not a typeless null. `Value::new_unset(T)` and
`MultiValues::new_unset(T)` preserve `T` so schema-aware callers can retain the
declared type before a concrete payload exists.

The following states are deliberately different:

| State | Meaning |
| --- | --- |
| `Value::new_unset(DataType::Json)` | No scalar value; declared type is `Json` |
| `MultiValues::new_unset(DataType::String)` | No collection; declared element type is `String` |
| `MultiValues::String(Vec::new())` | A concrete, homogeneous, empty collection |
| `Value::Json(serde_json::Value::Null)` | A concrete JSON scalar whose payload is JSON `null` |

Strict getters require the stored variant to match the requested Rust type.
They report a structured missing state or `TypeMismatch`; they never perform an
implicit conversion. Conversion methods are available only with `converter`
and delegate policy and resource accounting to `qubit-datatype`.

Defaulting is narrow. Strict `get_or` methods default an unset value, while
conversion defaults may also apply when the selected conversion policy reports
the source as missing. A concrete empty collection, type mismatch, or ordinary
invalid conversion is not silently replaced unless the relevant API explicitly
defines that behavior.

`set` replaces the current scalar or complete collection. `MultiValues::add`
is fallible because appended values must have the same element type. Shape is
preserved independently of item count throughout access, conversion, and Wire
serialization.

<a id="identity-and-hashing"></a>
## Identity and Hashing

Equality and hashing form one semantic contract across `Value`,
`MultiValues`, and `ValueContainer`:

- the runtime variant and scalar-versus-collection shape participate in
  identity;
- collection order and length participate in identity;
- `+0.0` and `-0.0` are one floating-point identity, and all NaN bit patterns
  are canonicalized to one identity so `Eq` and `Hash` remain lawful;
- `BigDecimal` hashing normalizes coefficient trailing zeroes and effective
  scale to agree with numeric equality;
- `StringMap` and JSON object identity are independent of map iteration order;
- JSON array order remains significant.

`Value::numeric_cmp` compares mathematical values across numeric runtime
variants without changing `PartialEq` identity: for example, `Int32(1)` and
`Int64(1)` compare equal under `NumericComparisonPolicy::Exact` while remaining
distinct `Value`s. This comparison is for an explicit operation such as a
metadata filter, not a replacement for identity or hash semantics. Callers
must restrict operands to concrete numeric values and handle
`NumericComparisonError`; unset values are not zero. `Approximate` may project
finite mixed representations to `f64`, making its result pair-dependent and
non-transitive, so it must not define ordering, sorting, grouping, or ordered
collection keys.

Numeric comparison is independent of conversion: `ConversionLimits` and
`ConversionSession` do not bound its work. Fixed-width comparisons do not
require heap allocation, while some paths involving large numbers may clone
coefficients or construct exact rational or decimal values. Cost depends on
the operand representations. Bound untrusted coefficient sizes and scales at
the input boundary, and account for cumulative comparison cost in batch
metadata filters. See the [lower-level comparison contract](https://docs.rs/qubit-datatype/latest/qubit_datatype/numeric/struct.NumberRef.html#method.compare).

JSON equality and hashing use iterative traversal instead of recursive calls.
The budget-aware hashing APIs preflight the complete JSON payload in a staged
transaction. A budget error leaves both the caller's hasher and the committed
budget unchanged.

This iterative traversal applies to the stated equality and hashing operations,
not to the complete value lifecycle. The containers directly hold
`serde_json::Value` payloads, whose cloning, ordinary `Debug` formatting, and
destruction retain recursive paths. Decode budgets constrain the operations
that use them; they are not a general stack-safety guarantee for later
lifecycle operations.

The resulting `Hash` output is for in-memory Rust collections. It is not a
persistent fingerprint, canonical protocol digest, or distributed-cache key;
it may change with the hasher, target, dependency versions, features, or crate
implementation.

<a id="natural-json-and-wire-json"></a>
## Natural JSON and Wire JSON

Natural JSON and Wire JSON solve different boundary problems.

| Property | Natural JSON | Wire V1 JSON |
| --- | --- | --- |
| Primary goal | Interoperate with ordinary JSON consumers | Reconstruct runtime type and shape |
| Type tags | Omitted | Preserved |
| Scalar/collection shape | Expressed only by JSON scalar/array syntax | Explicit `scalar` or `collection` tag |
| Unset | Projects to JSON `null` | Preserves `Unset(DataType)` |
| Round trip | Intentionally lossy | Type- and shape-preserving within V1 support |
| Entry point | `to_json_value*` | `ValueWire*V1` plus bounded encode/decode helpers |

Natural JSON is a projection into `serde_json::Value`. It emits string-map and
JSON object keys in dictionary order, rejects non-finite floats, and applies
one conversion budget across a complete projection. JSON object projection
stores completed children in each frame's final map and sorts that map when
the frame finishes. StringMap projection sorts borrowed entries before it
populates the output map because sorting the populated map regressed the
unified `preserve_order` benchmark. It cannot recover
the original integer width, distinguish unset from concrete JSON null, or
recover a declared type from JSON alone.

Projection preparation borrows source text and admits it before allocating the
final JSON string. Measuring integers and Float64 does not allocate a temporary
heap string. Float32 currently creates a temporary decimal String to preserve
its existing projection semantics. Formatted rich values are cached for the final
projection, so conversion is charged once. Preparation failure produces no usable
partial result. These changes preserve the existing Natural JSON categories and
resource limits.

Wire JSON uses explicit version, shape, and type tags. The DTOs implement
`Serialize`, but intentionally do not implement generic `Deserialize`.
Complete untrusted JSON documents must use bounded decode helpers; embedded
payloads use `ValueWireV1Seed` or `ValueWirePayloadV1Seed` under the outer
decoder's shared session.

Runtime JSON numbers are accepted by Wire V1 only when the current strict
`qubit-json` number model can reconstruct the same `serde_json::Number`: a
negative integer must fit `i64`, a non-negative integer must fit `u64`, and a
fractional or exponential value must become a finite `f64` without changing
Number equality. This preserves JSON identity as well as numeric value. With
`serde_json/arbitrary_precision`, values such as `1.2500`, `1e1`, or a decimal
with more precision than `f64` can preserve are rejected at construction or
serialization. The error does not include the number text. Wire V1 does not
preserve source JSON number spelling; values already normalized by the decoder
remain eligible when they satisfy the same rule.

This narrows the accepted input for values that could previously serialize
successfully and then decode to a different number identity. It does not change
the bytes or identity of accepted values, the V1 tags, or the schema. Callers
that need exact wider integers or decimals should use the explicit
`BigInteger`/`BigDecimal` runtime types, or use an application schema that
stores nested arbitrary JSON numbers as strings. An application that accepts
rounding may convert to a standard finite `f64` before constructing Wire data;
the library never performs that lossy conversion implicitly. Previously
rounded data cannot be recovered from its Wire representation and must be
reloaded from its original source.

When dependency feature unification enables `serde_json/arbitrary_precision`,
bounded helpers and `JsonDecoder::decode_unique_*` preserve native numeric
events; generic Serde readers can expose numbers as private maps. The unique
path constructs an intermediate tree, treats object keys literally, and uses
the configured depth budget without a separate fixed 127-container cap. This
adds memory overhead, and target-deserialization errors have no original source
coordinates. `NamedValue` and `NamedMultiValues` retain generic `Deserialize`
for embedding; only the public Wire DTOs omit that trait.

<a id="wire-v1-compatibility"></a>
## Wire V1 Compatibility

<a id="wire-v1-rich-payloads"></a>
### Rich payload formats

The following strings and objects are **payloads** under a type tag. A
standalone document still wraps the typed value in the versioned envelope;
collection shape uses an array of payloads under that same tag.

| Type / tag | Format and boundary | Representative payload |
| --- | --- | --- |
| `Int128` / `int128`, `UInt128` / `uint128`, `BigInteger` / `biginteger` | Canonical base-10 integer text inside a JSON string; no plus sign, redundant leading zeroes, or negative zero. Native-width values must also fit their range. | `"123"` |
| `BigDecimal` / `bigdecimal` | Strict object; `coefficient` is a canonical base-10 integer string, `scale` is an `i64`, and the value is coefficient × 10^(-scale). `abs(scale) ≤ 150000`. | `{"coefficient":"1234500","scale":4}` means `123.4500`. |
| `Duration` / `duration` | Strict object with `secs: u64` and `nanos < 1000000000`. | `{"secs":1,"nanos":2}` |
| `Date` / `date` | Chrono `%F` canonical output, checked by formatting the parsed date again; no additional range restriction. | `"2026-07-14"` |
| `Time` / `time` | `%H:%M:%S%.f`; fractional precision follows the current formatter's canonical output. | `"01:02:03"` |
| `DateTime` / `datetime` | `%Y-%m-%dT%H:%M:%S%.f`, without a timezone; retains current year rules. | `"2026-07-14T01:02:03"` |
| `Instant` / `instant` | UTC RFC3339, AutoSi fractional precision, `Z` suffix; UTC year `0..=9999`. | `"2026-07-14T01:02:03Z"` |
| `Url` / `url` | Canonicalized text from `Url::as_str()`; parseable input is rejected if its text changes during normalization. | `"https://example.com/path"` |

Collection adapters deserialize one typed payload at a time and immediately
validate or convert it before requesting the next element. Float collections
also check each decoded value for finiteness before requesting the next typed
element. This removes the whole-sequence intermediate payload vector and stops
typed tail processing after the first semantic validation error. It does not
change whole-document syntax preflight, input budgets, or which collections are
accepted. When one element is semantically invalid and a later element also
has a structural error, the earlier semantic error can now take precedence. A streaming
deserializer may also consume fewer tail elements on failure; budget already
charged for earlier decoding is not rolled back.

The decimal payload preserves its coefficient and scale representation.
`BigDecimal` identity/hash normalization of trailing zeroes does not rewrite
the Wire payload.

A standalone V1 document has a numeric `version` field and one typed `value`.
An embedded payload omits only the outer version field. Representative values
are:

```json
{"version":1,"value":{"scalar":{"int32":42}}}
{"version":1,"value":{"scalar":{"unset":"string"}}}
{"version":1,"value":{"collection":{"int32":[1,2]}}}
```

V1 rejects legacy bare enum representations such as `{"Int32":42}`. The
standalone envelope above is a complete V1 document; an embedded payload such
as `{"scalar":{"int32":42}}` is valid only with the payload seed/DTO, not as
a standalone document. A standalone document without the `version` field is
invalid.

JSON record positions in the envelope, named wrappers, and rich `Duration` and
`BigDecimal` payloads require objects with named fields. Array-shaped records
that may have been accepted by earlier builds are now rejected. JSON arrays
remain valid collection payloads and valid values under the `Json` type tag.

V1 is a closed contract. The following are compatibility invariants:

- the version is the JSON number `1`;
- the shape is exactly one `scalar` or `collection` variant;
- the payload is exactly one known lower-case V1 type tag;
- unknown fields, shapes, tags, and unsupported feature-gated concrete
  payloads are rejected;
- `Int128`, `UInt128`, and `BigInteger` use canonical decimal text;
- `BigDecimal` uses a bounded coefficient/scale representation;
- `Duration` uses seconds and sub-second nanoseconds;
- concrete `Instant` payloads use UTC years `0..=9999`, matching the four-digit
  year accepted by the V1 RFC3339 decoder;
- floats must be finite;
- string-map keys and nested JSON object keys are emitted in lexicographic
  order under the supported canonical JSON configuration.

The nested `scalar`/`collection` shape and lowercase type tag are part of the
V1 schema. Existing V1 tags, shapes, or payload encodings must not be
repurposed or extended in place. A new runtime type or incompatible
representation requires a new Wire version.

The byte-stability statement applies to canonical JSON emitted with the
supported `serde_json` configuration. Other Serde formats may carry the DTOs,
but their byte representation is not part of the V1 JSON stability contract.

This Instant rule tightens encoding acceptance: negative or five-digit years
could previously be emitted but could not be decoded as V1. Such values remain
valid runtime values; callers must keep them in memory or choose an explicitly
versioned application representation before crossing a V1 boundary. Normal
four-digit-year encodings are unchanged.

<a id="budget-and-preflight"></a>
## Budget and Preflight

Wire processing separates early rejection from authoritative accounting.

`ValueWireEncodePreflight` performs conservative lower-bound checks before a
downstream protocol formats payloads or allocates its final output. The
BigDecimal coefficient is counted as string payload using a no-allocation
lower bound, which may undercount its exact decimal text. In canonical JSON
serialization, an object is admitted before its sorting index is built, so a
failure of that object's own structural limit avoids the index allocation.
Successful `check_value`, `check_values`, and `check_container` calls
accumulate into one checker, allowing an outer object to charge several
embedded values to a shared budget. Each call is atomic: when it fails, the
node, payload-byte, and output-byte counters return to their pre-call state.

Preflight does not serialize, retain a sorting index, or prove that final
encoding will fit. Its estimates intentionally omit or undercount some final
syntax and formatting costs. The final `JsonEncodeSession` remains
authoritative and enforces structural measurements plus actual output bytes
during encoding. These checks can avoid some work before rejection, but do not
make every encoding preparation allocation subject to the output budget.

The explicit-limit JSON vector and writer helpers on all four Wire DTOs and
both named wrappers run a fresh conservative payload preflight before
constructing the final encoding session. The final session remains
authoritative for the complete payload or envelope, so the preflight may reject
from a lower-bound estimate but a passing preflight does not guarantee success
or bound every preparation allocation.

The three preflight counters have distinct units: `nodes` counts JSON value
nodes, `payload_bytes` counts decoded string/key/number text (null and boolean
payloads count as zero), and `output_bytes` counts the lower-bound encoded
syntax plus text. Strings and keys use UTF-8 bytes; string quotes and the
minimum object/array punctuation are included in the output lower bound.
Each key contributes its UTF-8 text to cumulative payload bytes and its text
plus two quote bytes to the output lower bound, but does not add a JSON value
node.
Escaping, canonical key sorting, outer envelope fields, and exact rich-type
formatting may add bytes later. A successful preflight therefore admits a
candidate for final encoding but never replaces that final check.

`new_value_limits` is for an outer protocol that owns the output budget.
`new_u64_limits` adapts a downstream `u64` profile to native `usize`; limits
larger than `usize::MAX` saturate rather than truncate on narrower targets.

Standalone defaults limit both raw input or output to 1 MiB and apply the
shared value profile: depth 64, 100,000 nodes, 4,096 sequence items, 4,096 map
entries, 256 KiB keys, 256 KiB strings, 4,096-byte number text, and 1 MiB value
payload. Applications may pass stricter profiles. For embedded data, the outer
protocol must own one session so envelope and payload resources are charged
together.

<a id="feature-matrix"></a>
## Feature Matrix

The default feature set is empty. Features affect which concrete payloads and
APIs can be materialized, but every `DataType` can still appear in an unset
declaration.

| Feature | Architectural effect |
| --- | --- |
| `converter` | Enables policy-based scalar and collection conversion |
| `chrono` | Materializes `Date`, `Time`, `DateTime`, and `Instant` |
| `big-integer` | Materializes `BigInteger` |
| `big-decimal` | Materializes `BigDecimal` and its bounded Wire form |
| `big-number` | Compatibility alias for both big-number features |
| `url` | Materializes `Url` |
| `json` | Materializes `Json` and enables bounded JSON Wire helpers and preflight |
| `natural-json` | Convenience alias for `converter` plus `json`; enables Natural JSON projection |
| `redact` | Enables policy-aware redacted views |
| `all` | Enables every public feature family |

A producer and consumer exchanging a concrete optional payload must enable the
same required type feature. A decoder without that feature rejects the payload
instead of coercing it to another type. Feature-gated APIs must not become an
implicit part of a minimal-feature build.

<a id="error-model"></a>
## Error Model

Errors are structured by boundary:

| Error type | Boundary |
| --- | --- |
| `ValueError` | Strict access, conversion, and Natural JSON projection |
| `ValueMissing` | Typed reason carried by `ValueError::Missing` |
| `ValueWireEncodeError` | V1 validation, budget, JSON syntax/serialization, and writer I/O |
| `ValueWireDecodeError` | Bounded input, JSON syntax/data, unsupported version, and resource failures |
| `MeasuredBudgetError<JsonResource, usize>` | Direct preflight rejection before downstream encoding |

Missing storage, a concrete empty collection, a conversion classified as
missing, type mismatch, and invalid conversion remain distinguishable.
Collection conversion errors retain the failing source index. Natural JSON
projection limit errors retain the data type, optional collection index, and
the measured resource facts. Duration formatting budget failures at the
projection boundary are reported as `ValueError::JsonProjectionLimit`, with
their original resource facts and collection index; ordinary conversion
failures retain their conversion error classification.

Fallback behavior follows this truth table:

| Source state | Strict `get_or*` | Conversion `to_*_or*` |
| --- | --- | --- |
| Unset scalar or collection | Default | Default |
| Concrete empty collection, first-item read | Error | Error |
| Policy-missing scalar | Not applicable | Default |
| Policy-missing collection item, including index 0 | Error | Error |
| Ordinary conversion or type error | Error | Error |

`ValueMissing` is a private-field fact object, classified by
`ValueMissingReason`. It records source type, requested target type, optional
source index, and the original conversion error when present. Storage
classification and conversion provenance are independent: an unset read may
still carry its original conversion failure. Callers use
`is_defaultable_for_strict_read()` or `is_defaultable_for_conversion()` rather
than assuming every missing result permits a fallback. A missing collection
item and a first-item read from a concrete empty collection never default.

Wire decoding has distinct diagnostic boundaries:

| Entry point | Diagnostic behavior |
| --- | --- |
| Bounded JSON decode helpers | Target-deserialization failures become `ValueWireDecodeError::InvalidJson`, containing only the Serde category and location. Target failures through the unique-key intermediate tree have no source coordinates and report zero for line and column. |
| Full envelope (`WireEnvelopeOwned::Deserialize` through `ValueWireV1Seed`) | Envelope decoding failures are replaced at this crate boundary with a fixed diagnostic; unsupported versions report only the constrained version number. |
| Payload `Deserialize` / `ValueWirePayloadV1Seed` | Preserves the supplied deserializer's error semantics; diagnostics may contain input-derived text. |
| Outer decoder, protocol wrapper, or reader/writer I/O | Outside the envelope replacement guarantee; follows that layer's diagnostic behavior and application logging policy. |

Resource budgets constrain input and decoded work independently of diagnostic
sanitization; the `redact` feature creates policy-aware views of values and is
not a sanitizer for arbitrary errors. Ordinary `Debug` formatting is not
implicitly redacted. Both `ValueError` and Wire
error enums are non-exhaustive where downstream code must tolerate future
diagnostic variants; downstream matches therefore need a fallback arm.

`ValueWireDecodeError::InvalidJson` now contains `ValueWireJsonError`, which
retains only Serde category, line, and column—not the original error or input.
This guarantee applies to bounded helpers' `InvalidJson` mapping, not to
generic deserializer errors or I/O sources. Target-deserialization failures
without source coordinates use zero values.
This changes the payload type from `serde_json::Error`; callers that construct
the variant or downcast its source must migrate to the safe metadata accessors.

<a id="downstream-integration"></a>
## Downstream Integration

`rs-config` uses `ValueContainer` as its property payload so configuration
sources and readers preserve scalar-versus-collection shape. Strict reads use
`StrictValueRead` at the value boundary; `Config::get` remains a converting
read. Standalone property values use `ValueWireV1` or
`ValueWireRefV1`. Before encoding a complete configuration, one
`ValueWireEncodePreflight` accumulates conservative charges for all properties
using the configuration's `u64` limit profile.

`rs-metadata` stores scalar `Value` instances and embeds
`ValueWirePayloadV1`/`ValueWirePayloadRefV1` inside its own versioned metadata
and filter protocols. Its decoder uses `ValueWirePayloadV1Seed` so the outer
document owns one decode session. Metadata and filter encoders likewise reuse
one preflight checker across contained values.

Metadata's `get` family uses strict reads, while its `convert` family requests
conversion explicitly. Both downstream crates preserve `ValueError` sources
and reuse `IntoValueDefault`; callers must not infer identical read semantics
from the shared method name `get`.

These integrations define the intended layering:

1. the downstream crate owns names, domain rules, outer versioning, and the
   complete request budget;
2. `qubit-value` owns typed payload validation, scalar/collection semantics,
   and V1 payload encoding;
3. `qubit-budget` and `qubit-json` perform shared bounded JSON accounting and
   I/O.

`qubit-value` CI verifies this crate's public contract. Direct consumer
integration belongs to each downstream crate's own CI; this crate's required
checks do not depend on pinned consumer revisions.

<a id="evolution-rules"></a>
## Evolution Rules

Maintainers should apply the following rules to future changes:

1. Preserve public method signatures and semantics within a compatible release;
   use semver checks and focused contract tests in this crate to detect
   accidental breaks. Consumer-specific integration coverage belongs in the
   downstream repository's own CI.
2. Add a runtime type through the central value table and update both owned
   containers, borrowed views, conversion behavior, identity, Natural JSON,
   Wire handling, feature tests, and documentation as one change.
3. Preserve `Eq`/`Hash` consistency. Treat changes to semantic equality as
   observable API changes even though concrete hash output is not stable.
4. Do not change V1 tags, shapes, or payload representations in place. Introduce
   a new version for incompatible protocol changes.
5. Keep generic `Deserialize` off the public Wire DTOs. New decode paths must
   preserve bounded complete-document or shared-session accounting.
6. Keep preflight conservative, cumulative, and per-call atomic. The final
   encoder remains the source of truth for resource admission.
7. Keep Natural JSON explicitly lossy and separate from Wire compatibility.
   Do not infer runtime type or shape during a Natural JSON round trip.
8. Keep the default feature set minimal. Optional concrete types require an
   explicit producer/consumer feature agreement.
9. Treat changes to default resource profiles and canonical JSON ordering as
   compatibility-sensitive behavior, with focused tests and release notes.
10. Keep English and Simplified Chinese README, user guide, and design
    documents semantically aligned whenever a contract changes.
11. Validate Natural JSON projection with a shared bounded property checker,
    corpus-backed deterministic regressions, and baseline/unified scheduled
    fuzzing. Check determinism, source immutability, limit monotonicity,
    collection shape, and recovery after failures.
12. Preserve the tag and payload write-order regression for JSON root-leaf
    identity hashing. Measure root leaves, shallow containers, and nested
    containers together, recording repeated ranges and environmental noise.
    Report allocation gains only when allocation evidence is stable; do not
    claim a speedup from noisy timing results.