diff --git a/AyCode.Core/docs/BINARY/BINARY_ISSUES.md b/AyCode.Core/docs/BINARY/BINARY_ISSUES.md index 0b87804..776c264 100644 --- a/AyCode.Core/docs/BINARY/BINARY_ISSUES.md +++ b/AyCode.Core/docs/BINARY/BINARY_ISSUES.md @@ -219,6 +219,27 @@ With `ThrowOnCircularReference=false` + reference handling enabled, **only `IId` - **Universal cycle detection** — track all reference types for cycle detection regardless of `IId`-ness when `ThrowOnCircularReference=false` (deduplication remains `IId`-only — cycle detection becomes universal). - **Diagnostic event** — surface "non-`IId` cycle dropped at depth N" as an `Action?` on options, opt-in. +### ACCORE-BIN-I-D9Y2: Default-value omission relies on type-level default consistency across writer/reader + +**Status:** Open +**Affects:** All property writes — `BinaryTypeCode.PropertySkip` (102) marker + +The serializer writes a 1-byte `PropertySkip` marker for any property whose value equals `default(T)` instead of the full encoded value. The deserializer interprets this marker as "leave the target property at its CLR default" — which assumes the consumer-side type definition has the **same default value** as the producer-side. + +**Impact:** Latent silent corruption across version-mismatched readers/writers. Three concrete failure modes: + +1. **Default-value change in type definition**: `bool IsActive = true` refactored to `bool IsActive = false` → wire produced with the old default decodes to wrong value on the new reader. +2. **Property added with non-zero default in v2**: e.g., v2 adds `int RetryCount = 3`. Deserializing a v1 wire on the v2 reader → `RetryCount = 0` (CLR default) instead of the expected v2 default 3. +3. **Cross-language consumers**: a non-.NET reader following the wire spec must implement the same default-resolution rules per type — not portable, not self-describing on the wire. + +**Wire-size impact (production-realistic):** Significant for DTOs with many optional/default-valued fields (status flags, nullable refs, default ints/decimals/Guids/DateTimes). Benchmark contribution: estimated **~3-8% of AcBinary wire-size advantage** vs MemoryPack on the current test data; real-world DTOs may see 10-30% depending on the default-value share. + +**Possible fix directions** (decision deferred — see `BINARY_TODO.md#accore-bin-t-w7n5`): + +- **Doc-only**: position as a deliberate protobuf-style feature, consumer responsibility to keep type definitions stable across versions. +- **Option flag**: `AcBinarySerializerOptions.OmitDefaults` (default `true` for back-compat); `false` writes every property's full value regardless. Lets consumers opt out for fragile-class-evolution scenarios. +- **Hybrid**: ship doc + flag, default `true`. + ## Cross-cutting (canonical home: `../XCUT/`) ### ACCORE-XCUT-I-X8Q1: JSON-in-Binary request parameters — cross-ref diff --git a/AyCode.Core/docs/BINARY/BINARY_TODO.md b/AyCode.Core/docs/BINARY/BINARY_TODO.md index 78a908f..be806e9 100644 --- a/AyCode.Core/docs/BINARY/BINARY_TODO.md +++ b/AyCode.Core/docs/BINARY/BINARY_TODO.md @@ -797,3 +797,20 @@ Replace `Encoding.UTF8.GetBytes` calls in `WriteStringUtf8` / `WriteStringUtf8In - Wire format unchanged (custom encoder produces same bytes as `Encoding.UTF8`) - Round-trip tests pass +## ACCORE-BIN-T-W7N5: Default-value omission policy — doc + optional opt-out +**Priority:** P2 · **Type:** Refactor + Documentation · **Related:** `BINARY_ISSUES.md#accore-bin-i-d9y2` (canonical issue) + +The serializer's `PropertySkip` (102) optimization saves 1 byte per default-valued property by omitting the full value from the wire — relying on the consumer-side type definition to have the same `default(T)`. This is a **latent correctness risk** documented in `ACCORE-BIN-I-D9Y2`. This entry tracks the mitigation plan; full failure-mode analysis lives in the issue. + +### Decision tree (TBD when implementing) + +1. **Doc-only**: position as a deliberate protobuf-style feature; consumer keeps type defaults stable across versions. Lowest cost, maximum benchmark wire-size advantage retained. +2. **Option flag**: `AcBinarySerializerOptions.OmitDefaults` boolean. Default `true` (preserves current behavior + benchmark numbers). `false` writes every property in full — opt-out for fragile-class-evolution scenarios. +3. **Both**: ship doc + flag. Default behavior unchanged; consumers who hit silent-corruption have an explicit opt-out. + +### Acceptance (when implementing) + +- `BINARY_FEATURES.md` adds a "Default-Value Omission" section documenting the semantic and the tradeoff (with cross-ref to `ACCORE-BIN-I-D9Y2`) +- If flag added: round-trip tests covering both `true` and `false`; benchmark comparison table showing wire-size delta on ASCII / Hungarian / DTO-heavy workloads +- Decision rationale recorded in `LLM_PROTOCOL_DECISIONS.md` (or a `### Resolution` block on the issue) once implemented +