[LOADED_DOCS: 5 files (+2 this turn: BINARY_ISSUES.md, LOGGING_ISSUES.md)]
Simplify Status field; add docs-archive skill & archiving - Reduced Status field values in issues/TODOs to Open, InProgress, Closed; updated all affected entries to new convention - Introduced docs-archive skill for rotating Closed entries into year-bucketed archive files; process is user-invoked or LLM-suggested, never automatic - Expanded docs-discovery and protocol documentation to clarify archive file handling and on-demand loading - Updated session setup: only reactive skills pre-loaded, user-gated skills now lazy-loaded for token efficiency - Clarified and documented Status update workflow, archive eligibility, and lifecycle - Updated all relevant issue/TODO files to match new Status conventions and archival process
This commit is contained in:
parent
37559b6dc4
commit
9a53aa1d73
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -85,6 +85,39 @@ LLMP-DEC-4 # LLM-protocol decision 4 (Decision Log entry)
|
||||||
- **Code comments**: `// See LOG-I-5` — bare ID acceptable since it's globally unique.
|
- **Code comments**: `// See LOG-I-5` — bare ID acceptable since it's globally unique.
|
||||||
- **DB natural key** (future migration): `(topic, type, seq)` tuple; or the full string `LOG-I-5` as a single column.
|
- **DB natural key** (future migration): `(topic, type, seq)` tuple; or the full string `LOG-I-5` as a single column.
|
||||||
|
|
||||||
|
## Status field conventions
|
||||||
|
|
||||||
|
Every entry in `_ISSUES.md`, `_TODO.md`, and `LLM_PROTOCOL_DECISIONS.md` SHOULD carry an explicit `Status` field. **3 allowed values**:
|
||||||
|
|
||||||
|
| Status | Meaning | Archive eligible? |
|
||||||
|
|---|---|---|
|
||||||
|
| `Open` | Active / unresolved (default for new entries); also used for documented-current-behaviour entries that must remain visible | No |
|
||||||
|
| `InProgress` | Partial work in flight; some scope addressed but more remains | No |
|
||||||
|
| `Closed` | Done — bug fixed, decision made (won't fix / superseded by another entry / accepted), TODO completed. The body of the entry explains *what happened* (date, ref, rationale). | Yes |
|
||||||
|
|
||||||
|
### Defaults
|
||||||
|
|
||||||
|
- New entries default to `Status: Open`.
|
||||||
|
- For documented current-behaviour entries (accepted limitations / "by design" / "this is how it works"), use `Status: Open` with an optional body callout: `> **Note:** This entry documents accepted current behaviour — not scheduled for change.` These never archive (Open status).
|
||||||
|
|
||||||
|
### Update workflow
|
||||||
|
|
||||||
|
When status changes, update the `Status` line in-place. **This is the ONE exception to append-only** — the Status field is mutable; entry body / ID / Description remain immutable.
|
||||||
|
|
||||||
|
When marking `Closed`:
|
||||||
|
1. **Format the Status line as** `Status: Closed (YYYY-MM-DD)` — the inline date is what `docs-archive` uses to determine the destination year-bucket.
|
||||||
|
2. **Add a `### Resolution` sub-section** documenting the closure. **Strongly recommended** — without it, future readers (and the `docs-archive` skill on lookup) have no context for "what changed, why, where". Suggested fields:
|
||||||
|
- **What:** one-line summary of the change.
|
||||||
|
- **Where:** code reference (file/class/commit hash) or doc reference (ADR / PR).
|
||||||
|
- **Why:** the rationale (fix / "won't fix because X" / "superseded by LOG-I-Y" / "accepted as-is").
|
||||||
|
- Optional: scope, date if different from Status line, related entries.
|
||||||
|
|
||||||
|
The body carries the **nuance**; the Status field only signals archive-eligibility.
|
||||||
|
|
||||||
|
### Lifecycle: archive
|
||||||
|
|
||||||
|
`Closed` entries are eligible for rotation into year-bucketed archive files (`<file>_<year>.md`) via the `docs-archive` skill. Year derived from a date in the entry body. Archive operation is user-invoked — closed entries don't disappear automatically. See `AyCode.Core/.github/skills/docs-archive/SKILL.md`.
|
||||||
|
|
||||||
## Change history
|
## Change history
|
||||||
|
|
||||||
See the Decision Log (`../../../LLM_PROTOCOL_DECISIONS.md`) for the introduction of this registry and future topic-code additions.
|
See the Decision Log (`../../../LLM_PROTOCOL_DECISIONS.md`) for the introduction of this registry and future topic-code additions.
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,45 @@ If any `{DOMAIN}.md` is loaded (e.g., `LOGGING.md`), ALSO glob and load its comp
|
||||||
|
|
||||||
These are **paired docs** and must be loaded as a set. Skipping ISSUES/TODO risks reintroducing fixed bugs or conflicting with ongoing refactors.
|
These are **paired docs** and must be loaded as a set. Skipping ISSUES/TODO risks reintroducing fixed bugs or conflicting with ongoing refactors.
|
||||||
|
|
||||||
|
## Archive files (`*_<year>.md`)
|
||||||
|
|
||||||
|
Closed entries from `_ISSUES.md` / `_TODO.md` / `LLM_PROTOCOL_DECISIONS.md` may be rotated into year-bucketed archive files by the `docs-archive` skill. Examples:
|
||||||
|
- `LOGGING_ISSUES_2025.md`
|
||||||
|
- `BINARY_TODO_2026.md`
|
||||||
|
- `LLM_PROTOCOL_DECISIONS_2026.md`
|
||||||
|
|
||||||
|
### Default behaviour: NOT auto-loaded
|
||||||
|
|
||||||
|
The Step 2 glob patterns target **active** companions only — unsuffixed names. Year-suffixed variants are excluded by default. Practically:
|
||||||
|
|
||||||
|
- `**/docs/{TOPIC}/{TOPIC}_ISSUES.md` matches; `**/docs/{TOPIC}/{TOPIC}_ISSUES_2025.md` does NOT.
|
||||||
|
- If a generic `{TOPIC}_*.md` pattern inadvertently matches year-suffixed files, filter them out before passing to Step 4 (Load).
|
||||||
|
|
||||||
|
### On-demand read (no user-confirm needed — read-only operation)
|
||||||
|
|
||||||
|
Read an archive file when ANY of these signals appears:
|
||||||
|
- A loaded entry references an archived ID (e.g., `Superseded by LOG-I-X` where X resolves only to `_<year>.md`)
|
||||||
|
- A code comment or other doc references an ID resolving only to an archive file
|
||||||
|
- The user's request describes a behaviour pattern matching an archived `Fixed` entry's Description (regression suspicion)
|
||||||
|
- The investigation feels like "this was solved before" — read the topic's archive(s) before re-deriving
|
||||||
|
- The user explicitly asks about historical context
|
||||||
|
|
||||||
|
When read: include in `[LOADED_DOCS]` like any other `.md`. Rule #3 (no-re-read) applies. Cite from it like the active file.
|
||||||
|
|
||||||
|
This is a **read** — Rule #5 (consent for modifications) is not engaged. The "don't pre-load" rule is about token economy, not access control.
|
||||||
|
|
||||||
|
### Glob recap
|
||||||
|
|
||||||
|
Active-only (default for topic discovery):
|
||||||
|
- `**/docs/{TOKEN}/{TOKEN}_ISSUES.md`
|
||||||
|
- `**/docs/{TOKEN}/{TOKEN}_TODO.md`
|
||||||
|
- `**/docs/{TOKEN}/README.md`
|
||||||
|
|
||||||
|
On-demand archive lookup:
|
||||||
|
- `**/docs/{TOKEN}/{TOKEN}_ISSUES_*.md` (where `*` matches a 4-digit year)
|
||||||
|
- `**/docs/{TOKEN}/{TOKEN}_TODO_*.md`
|
||||||
|
- `**/LLM_PROTOCOL_DECISIONS_*.md`
|
||||||
|
|
||||||
## Step 6 — Proceed to the user's task
|
## Step 6 — Proceed to the user's task
|
||||||
|
|
||||||
The response's `[LOADED_DOCS: N files (+K this turn: <basenames>)]` prefix (per the active repo's Rule #1) already surfaces the newly-loaded filenames and the cumulative count. **No separate confirmation line is needed** — the prefix itself is the confirmation. Continue directly to the user's actual request.
|
The response's `[LOADED_DOCS: N files (+K this turn: <basenames>)]` prefix (per the active repo's Rule #1) already surfaces the newly-loaded filenames and the cumulative count. **No separate confirmation line is needed** — the prefix itself is the confirmation. Continue directly to the user's actual request.
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
### BIN-I-1: Non-array-backed memory — per-segment copy
|
### BIN-I-1: Non-array-backed memory — per-segment copy
|
||||||
|
|
||||||
**Status:** By design
|
**Status:** Open
|
||||||
**Affects:** `SequenceBinaryInput`
|
**Affects:** `SequenceBinaryInput`
|
||||||
**Path:** `ExtractArray()` fallback when `MemoryMarshal.TryGetArray` fails
|
**Path:** `ExtractArray()` fallback when `MemoryMarshal.TryGetArray` fails
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@ When `ReadOnlySequence<byte>` segments are backed by native memory (not managed
|
||||||
|
|
||||||
### BIN-I-2: Cross-boundary scratch buffer is not pooled across calls
|
### BIN-I-2: Cross-boundary scratch buffer is not pooled across calls
|
||||||
|
|
||||||
**Status:** Acceptable
|
**Status:** Open
|
||||||
**Affects:** `SequenceBinaryInput._scratchBuffer`
|
**Affects:** `SequenceBinaryInput._scratchBuffer`
|
||||||
|
|
||||||
The scratch buffer is `ArrayPool.Rent`-ed on first cross-boundary read and reused within a single deserialization. It is `Return`-ed in `Release()` after deserialization completes. However, the next deserialization will rent again.
|
The scratch buffer is `ArrayPool.Rent`-ed on first cross-boundary read and reused within a single deserialization. It is `Return`-ed in `Release()` after deserialization completes. However, the next deserialization will rent again.
|
||||||
|
|
@ -25,14 +25,14 @@ The scratch buffer is `ArrayPool.Rent`-ed on first cross-boundary read and reuse
|
||||||
|
|
||||||
### BIN-I-3: ReadBytes always copies
|
### BIN-I-3: ReadBytes always copies
|
||||||
|
|
||||||
**Status:** By design
|
**Status:** Open
|
||||||
**Affects:** `BinaryDeserializationContext.ReadBytes(int length)`
|
**Affects:** `BinaryDeserializationContext.ReadBytes(int length)`
|
||||||
|
|
||||||
`ReadBytes` allocates a new `byte[]` and copies from the buffer. This is unavoidable because the caller owns the returned array, and the source buffer (pipe segment or serialized data) may be recycled.
|
`ReadBytes` allocates a new `byte[]` and copies from the buffer. This is unavoidable because the caller owns the returned array, and the source buffer (pipe segment or serialized data) may be recycled.
|
||||||
|
|
||||||
### BIN-I-4: ReadStringUtf8 requires contiguous buffer
|
### BIN-I-4: ReadStringUtf8 requires contiguous buffer
|
||||||
|
|
||||||
**Status:** By design
|
**Status:** Open
|
||||||
**Affects:** `BinaryDeserializationContext.ReadStringUtf8(int length)`
|
**Affects:** `BinaryDeserializationContext.ReadStringUtf8(int length)`
|
||||||
|
|
||||||
`Encoding.GetString` and `Ascii.IsValid` require contiguous memory. For multi-segment reads, `EnsureAvailable` copies cross-boundary bytes into the scratch buffer first. This is the same approach `SequenceReader<byte>` uses internally.
|
`Encoding.GetString` and `Ascii.IsValid` require contiguous memory. For multi-segment reads, `EnsureAvailable` copies cross-boundary bytes into the scratch buffer first. This is the same approach `SequenceReader<byte>` uses internally.
|
||||||
|
|
@ -43,14 +43,14 @@ The scratch buffer is `ArrayPool.Rent`-ed on first cross-boundary read and reuse
|
||||||
|
|
||||||
### BIN-I-5: BufferWriterBinaryOutput fallback path allocates per-chunk
|
### BIN-I-5: BufferWriterBinaryOutput fallback path allocates per-chunk
|
||||||
|
|
||||||
**Status:** Acceptable
|
**Status:** Open
|
||||||
**Affects:** `BufferWriterBinaryOutput.AcquireChunk` fallback
|
**Affects:** `BufferWriterBinaryOutput.AcquireChunk` fallback
|
||||||
|
|
||||||
When `MemoryMarshal.TryGetArray` fails on `IBufferWriter.GetMemory()` (native memory-backed writer), a `byte[]` is rented from `ArrayPool` per chunk and copied to the writer on `Grow`/`Flush`. Same as BIN-I-1 — non-array-backed writers are extremely rare.
|
When `MemoryMarshal.TryGetArray` fails on `IBufferWriter.GetMemory()` (native memory-backed writer), a `byte[]` is rented from `ArrayPool` per chunk and copied to the writer on `Grow`/`Flush`. Same as BIN-I-1 — non-array-backed writers are extremely rare.
|
||||||
|
|
||||||
### BIN-I-6: AsyncPipeWriterOutput uses sync GetResult() for backpressure
|
### BIN-I-6: AsyncPipeWriterOutput uses sync GetResult() for backpressure
|
||||||
|
|
||||||
**Status:** By design (v1)
|
**Status:** Open
|
||||||
**Affects:** `AsyncPipeWriterOutput.Grow()` — `_lastFlush.GetAwaiter().GetResult()`
|
**Affects:** `AsyncPipeWriterOutput.Grow()` — `_lastFlush.GetAwaiter().GetResult()`
|
||||||
|
|
||||||
When the previous `PipeWriter.FlushAsync()` hasn't completed by the next `Grow()` call, the serializer blocks the thread until the flush completes. This is necessary because `IHubProtocol.WriteMessage` is `void` (synchronous by design).
|
When the previous `PipeWriter.FlushAsync()` hasn't completed by the next `Grow()` call, the serializer blocks the thread until the flush completes. This is necessary because `IHubProtocol.WriteMessage` is `void` (synchronous by design).
|
||||||
|
|
@ -61,7 +61,7 @@ When the previous `PipeWriter.FlushAsync()` hasn't completed by the next `Grow()
|
||||||
|
|
||||||
### BIN-I-7: AsyncPipeWriterOutput fallback path — same as BIN-I-5
|
### BIN-I-7: AsyncPipeWriterOutput fallback path — same as BIN-I-5
|
||||||
|
|
||||||
**Status:** Acceptable
|
**Status:** Open
|
||||||
**Affects:** `AsyncPipeWriterOutput.AcquireChunk` fallback
|
**Affects:** `AsyncPipeWriterOutput.AcquireChunk` fallback
|
||||||
|
|
||||||
Same `TryGetArray` fallback as `BufferWriterBinaryOutput` (BIN-I-5). Kestrel `PipeWriter.GetMemory()` always returns array-backed memory — fallback is for non-standard `PipeWriter` implementations only.
|
Same `TryGetArray` fallback as `BufferWriterBinaryOutput` (BIN-I-5). Kestrel `PipeWriter.GetMemory()` always returns array-backed memory — fallback is for non-standard `PipeWriter` implementations only.
|
||||||
|
|
@ -70,7 +70,7 @@ Same `TryGetArray` fallback as `BufferWriterBinaryOutput` (BIN-I-5). Kestrel `Pi
|
||||||
|
|
||||||
### BIN-I-8: PipeReaderBinaryInput uses sync ReadAsync().GetResult()
|
### BIN-I-8: PipeReaderBinaryInput uses sync ReadAsync().GetResult()
|
||||||
|
|
||||||
**Status:** By design (v1)
|
**Status:** Open
|
||||||
**Affects:** `PipeReaderBinaryInput.Initialize()` and `TryAdvanceSegment()`
|
**Affects:** `PipeReaderBinaryInput.Initialize()` and `TryAdvanceSegment()`
|
||||||
|
|
||||||
Same constraint as BIN-I-6 — `IBinaryInputBase` interface is synchronous. `ReadAsync().GetAwaiter().GetResult()` blocks when waiting for more data from the pipe. Currently not used in production (SignalR delivers complete messages via `TryParseMessage`). Reserved for future direct-pipe deserialization scenarios.
|
Same constraint as BIN-I-6 — `IBinaryInputBase` interface is synchronous. `ReadAsync().GetAwaiter().GetResult()` blocks when waiting for more data from the pipe. Currently not used in production (SignalR delivers complete messages via `TryParseMessage`). Reserved for future direct-pipe deserialization scenarios.
|
||||||
|
|
@ -79,14 +79,14 @@ Same constraint as BIN-I-6 — `IBinaryInputBase` interface is synchronous. `Rea
|
||||||
|
|
||||||
### BIN-I-9: CS8625 warnings for non-nullable reference types
|
### BIN-I-9: CS8625 warnings for non-nullable reference types
|
||||||
|
|
||||||
**Status:** Known
|
**Status:** Open
|
||||||
**Affects:** Generated reader code
|
**Affects:** Generated reader code
|
||||||
|
|
||||||
The source generator emits `null` assignments for non-nullable reference type properties during deserialization (before the value is read from the stream). This produces CS8625 warnings. Functionally harmless — the property is always assigned before use.
|
The source generator emits `null` assignments for non-nullable reference type properties during deserialization (before the value is read from the stream). This produces CS8625 warnings. Functionally harmless — the property is always assigned before use.
|
||||||
|
|
||||||
### BIN-I-10: First-run cold-start overhead
|
### BIN-I-10: First-run cold-start overhead
|
||||||
|
|
||||||
**Status:** Active — mitigation planned (see `BINARY_TODO.md#bin-t-3`, `BINARY_TODO.md#bin-t-4`)
|
**Status:** Open
|
||||||
**Affects:** First `Serialize<T>`/`Deserialize<T>` per `[AcBinarySerializable]` type, per process
|
**Affects:** First `Serialize<T>`/`Deserialize<T>` per `[AcBinarySerializable]` type, per process
|
||||||
|
|
||||||
Cold-start cost chain on first use of an SGen type (before BIN-T-3 lands):
|
Cold-start cost chain on first use of an SGen type (before BIN-T-3 lands):
|
||||||
|
|
@ -106,7 +106,7 @@ Subsequent calls hit cached metadata/wrappers → only Tier 0→1 JIT transition
|
||||||
|
|
||||||
### BIN-I-11: Consumer entity with `new` Id shadowing — excluded from SGen
|
### BIN-I-11: Consumer entity with `new` Id shadowing — excluded from SGen
|
||||||
|
|
||||||
**Status:** Workaround-in-place (compiled-expression fallback)
|
**Status:** Open
|
||||||
**Affects:** Any consumer entity whose base class hides `BaseEntity.Id` with `readonly new int Id { get; }` pattern (e.g. `DiscountProductMapping` in Mango.Nop.Core)
|
**Affects:** Any consumer entity whose base class hides `BaseEntity.Id` with `readonly new int Id { get; }` pattern (e.g. `DiscountProductMapping` in Mango.Nop.Core)
|
||||||
|
|
||||||
When the base class shadows `Id` with a setter-less `new int Id { get; }`, SGen can't emit a setter without CS0200. Runtime falls back to compiled-expression serialization for these types. Low priority — affects a small number of consumer entities.
|
When the base class shadows `Id` with a setter-less `new int Id { get; }`, SGen can't emit a setter without CS0200. Runtime falls back to compiled-expression serialization for these types. Low priority — affects a small number of consumer entities.
|
||||||
|
|
@ -117,28 +117,28 @@ When the base class shadows `Id` with a setter-less `new int Id { get; }`, SGen
|
||||||
|
|
||||||
### BIN-I-12: Struct copy semantics
|
### BIN-I-12: Struct copy semantics
|
||||||
|
|
||||||
**Status:** By design
|
**Status:** Open
|
||||||
**Affects:** `BufferWriterBinaryOutput` value-type assignment
|
**Affects:** `BufferWriterBinaryOutput` value-type assignment
|
||||||
|
|
||||||
Assigning a `BufferWriterBinaryOutput` value creates an independent copy. State changes (e.g. `_committedBytes` via `Grow`/`Flush`) are not reflected in the original. Copy back after use if needed.
|
Assigning a `BufferWriterBinaryOutput` value creates an independent copy. State changes (e.g. `_committedBytes` via `Grow`/`Flush`) are not reflected in the original. Copy back after use if needed.
|
||||||
|
|
||||||
### BIN-I-13: Initialize resets tracking
|
### BIN-I-13: Initialize resets tracking
|
||||||
|
|
||||||
**Status:** By design
|
**Status:** Open
|
||||||
**Affects:** `BufferWriterBinaryOutput.Initialize` (context mode)
|
**Affects:** `BufferWriterBinaryOutput.Initialize` (context mode)
|
||||||
|
|
||||||
`Initialize` sets `_committedBytes = 0`. Standalone bytes written before are lost if the BWO is then passed to a context. Call `FlushAndReset()` first, or track standalone bytes separately.
|
`Initialize` sets `_committedBytes = 0`. Standalone bytes written before are lost if the BWO is then passed to a context. Call `FlushAndReset()` first, or track standalone bytes separately.
|
||||||
|
|
||||||
### BIN-I-14: Constructor acquires chunk
|
### BIN-I-14: Constructor acquires chunk
|
||||||
|
|
||||||
**Status:** Acceptable (not a leak)
|
**Status:** Open
|
||||||
**Affects:** `BufferWriterBinaryOutput` ctor
|
**Affects:** `BufferWriterBinaryOutput` ctor
|
||||||
|
|
||||||
`AcquireChunk` runs in ctor for standalone readiness. Redundant if only context mode is used (context `Initialize` acquires its own). Not a leak — consecutive `GetMemory` without `Advance` returns overlapping memory.
|
`AcquireChunk` runs in ctor for standalone readiness. Redundant if only context mode is used (context `Initialize` acquires its own). Not a leak — consecutive `GetMemory` without `Advance` returns overlapping memory.
|
||||||
|
|
||||||
### BIN-I-15: No mode mixing
|
### BIN-I-15: No mode mixing
|
||||||
|
|
||||||
**Status:** By design
|
**Status:** Open
|
||||||
**Affects:** `BufferWriterBinaryOutput` — context vs standalone mode
|
**Affects:** `BufferWriterBinaryOutput` — context vs standalone mode
|
||||||
|
|
||||||
A single instance must not use context + standalone modes simultaneously — buffer states desynchronize. One mode per lifecycle phase; `FlushAndReset()` as boundary between modes.
|
A single instance must not use context + standalone modes simultaneously — buffer states desynchronize. One mode per lifecycle phase; `FlushAndReset()` as boundary between modes.
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ Console.Error noise tolerated. Alternatively, consumer uses DI-based `AddAcLogge
|
||||||
|
|
||||||
## LOG-I-2: AcEnv.AppConfiguration is filesystem-bound, MAUI/WASM-unsafe
|
## LOG-I-2: AcEnv.AppConfiguration is filesystem-bound, MAUI/WASM-unsafe
|
||||||
|
|
||||||
**Severity:** Minor · **Status:** Documented limitation · **Area:** `AyCode.Core.Consts.AcEnv`
|
**Severity:** Minor · **Status:** Open · **Area:** `AyCode.Core.Consts.AcEnv`
|
||||||
|
|
||||||
### Description
|
### Description
|
||||||
`AcEnv.AppConfiguration` is a static lazy singleton calling `new ConfigurationBuilder().AddJsonFile("appsettings.json").Build()` on first access. Reads current-working-directory filesystem. Throws on MAUI (no physical appsettings next to exe) and WASM (no filesystem at all).
|
`AcEnv.AppConfiguration` is a static lazy singleton calling `new ConfigurationBuilder().AddJsonFile("appsettings.json").Build()` on first access. Reads current-working-directory filesystem. Throws on MAUI (no physical appsettings next to exe) and WASM (no filesystem at all).
|
||||||
|
|
@ -40,7 +40,7 @@ Consumer avoids the config-reading `AcLoggerBase(string)` ctor on these platform
|
||||||
|
|
||||||
## LOG-I-3: Two parallel logger-setup patterns
|
## LOG-I-3: Two parallel logger-setup patterns
|
||||||
|
|
||||||
**Severity:** Minor (confusion, not functional) · **Status:** Documented · **Area:** LOGGING.md / consumer code
|
**Severity:** Minor (confusion, not functional) · **Status:** Open · **Area:** LOGGING.md / consumer code
|
||||||
|
|
||||||
### Description
|
### Description
|
||||||
Two ways to construct a logger coexist:
|
Two ways to construct a logger coexist:
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ For planned cross-cutting work, see `XCUT_TODO.md`.
|
||||||
|
|
||||||
## XCUT-I-1: JSON-in-Binary request parameters
|
## XCUT-I-1: JSON-in-Binary request parameters
|
||||||
|
|
||||||
**Status:** Major tech debt, planned replacement (coordinated)
|
**Status:** Open
|
||||||
**Affects:** BINARY serializer (wire format) ↔ SIGNALR transport (envelope) ↔ all consuming projects (caller code)
|
**Affects:** BINARY serializer (wire format) ↔ SIGNALR transport (envelope) ↔ all consuming projects (caller code)
|
||||||
|
|
||||||
### Description
|
### Description
|
||||||
|
|
|
||||||
|
|
@ -189,7 +189,15 @@ public class AcLoginServiceServer<TResultLoggedInModel, TDal, TDbContext, TUser,
|
||||||
throw new SecurityTokenException("Token is null");
|
throw new SecurityTokenException("Token is null");
|
||||||
|
|
||||||
var keyDetail = Encoding.UTF8.GetBytes(configuration["JWT:Key"] ?? string.Empty);
|
var keyDetail = Encoding.UTF8.GetBytes(configuration["JWT:Key"] ?? string.Empty);
|
||||||
|
// SECURITY: A következő DEBUG-only sor a JWT aláíró kulcsot logolja.
|
||||||
|
// Ha bármely log writer (fájl, HTTP, központi log-aggregator, bug-report
|
||||||
|
// dump) perzisztálja vagy szállítja, a kulcs kiszivároghat → bárki valid
|
||||||
|
// tokent gyárthat tetszőleges felhasználó nevében. Production build-ből
|
||||||
|
// kötelezően kimarad (#if DEBUG). NEM törölve, mert helyi auth-debug
|
||||||
|
// során hasznos lehet a kulcs-csere ellenőrzéséhez.
|
||||||
|
#if DEBUG
|
||||||
GlobalLogger.Detail($"Key: {configuration["JWT:Key"]}");
|
GlobalLogger.Detail($"Key: {configuration["JWT:Key"]}");
|
||||||
|
#endif
|
||||||
|
|
||||||
var claims = new List<Claim>
|
var claims = new List<Claim>
|
||||||
{
|
{
|
||||||
|
|
@ -209,7 +217,15 @@ public class AcLoginServiceServer<TResultLoggedInModel, TDal, TDbContext, TUser,
|
||||||
|
|
||||||
var token = tokenHandler.CreateToken(tokenDescriptor) as JwtSecurityToken;
|
var token = tokenHandler.CreateToken(tokenDescriptor) as JwtSecurityToken;
|
||||||
var writtenToken = tokenHandler.WriteToken(token);
|
var writtenToken = tokenHandler.WriteToken(token);
|
||||||
|
// SECURITY: A következő DEBUG-only sor a kibocsátott access token-t
|
||||||
|
// logolja. Ha a log perzisztálódik vagy elérhető (bug-report dump,
|
||||||
|
// log-aggregator, shared dev environment), a token lejáratáig bárki
|
||||||
|
// a felhasználó nevében authentikálhat — JWT esetén a token önmagában
|
||||||
|
// bearer credential. Production build-ből kötelezően kimarad
|
||||||
|
// (#if DEBUG). NEM törölve, mert helyi auth-debug során hasznos.
|
||||||
|
#if DEBUG
|
||||||
GlobalLogger.Detail($"AccesToken: {writtenToken}");
|
GlobalLogger.Detail($"AccesToken: {writtenToken}");
|
||||||
|
#endif
|
||||||
|
|
||||||
return writtenToken;
|
return writtenToken;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
### SIG-I-1: Server-side IsRawBytesData pre-serialize
|
### SIG-I-1: Server-side IsRawBytesData pre-serialize
|
||||||
|
|
||||||
**Status:** Planned removal
|
**Status:** Open
|
||||||
**Affects:** `AcWebSignalRHubBase.SendMessageToClient`
|
**Affects:** `AcWebSignalRHubBase.SendMessageToClient`
|
||||||
|
|
||||||
The server forwards the client's `IsRawBytesData` flag in the response `SignalParams`. This causes the protocol to return raw `byte[]` instead of deserializing. The original design pre-serialized on the server side, but with the zero-copy typed deserialization path (`SignalDataType`), this is redundant.
|
The server forwards the client's `IsRawBytesData` flag in the response `SignalParams`. This causes the protocol to return raw `byte[]` instead of deserializing. The original design pre-serialized on the server side, but with the zero-copy typed deserialization path (`SignalDataType`), this is redundant.
|
||||||
|
|
@ -13,7 +13,7 @@ The server forwards the client's `IsRawBytesData` flag in the response `SignalPa
|
||||||
|
|
||||||
### SIG-I-2: Parameter serialization is per-parameter
|
### SIG-I-2: Parameter serialization is per-parameter
|
||||||
|
|
||||||
**Status:** Known performance concern
|
**Status:** Open
|
||||||
**Affects:** `SignalParams.SetParameterValues` / `GetParameterValues`
|
**Affects:** `SignalParams.SetParameterValues` / `GetParameterValues`
|
||||||
|
|
||||||
Each parameter is individually serialized via `ToBinary()` / `BinaryTo(Type)` — N context pool acquire/release cycles. For many small primitives (int, bool, string) the per-call overhead may exceed a single bulk serialization.
|
Each parameter is individually serialized via `ToBinary()` / `BinaryTo(Type)` — N context pool acquire/release cycles. For many small primitives (int, bool, string) the per-call overhead may exceed a single bulk serialization.
|
||||||
|
|
@ -22,7 +22,7 @@ Each parameter is individually serialized via `ToBinary()` / `BinaryTo(Type)`
|
||||||
|
|
||||||
### SIG-I-3: Parameter serialization is AcBinary only
|
### SIG-I-3: Parameter serialization is AcBinary only
|
||||||
|
|
||||||
**Status:** Limitation
|
**Status:** Open
|
||||||
**Affects:** `SignalParams.SetParameterValues` / `GetParameterValues`
|
**Affects:** `SignalParams.SetParameterValues` / `GetParameterValues`
|
||||||
|
|
||||||
Uses `ToBinary()` / `BinaryTo()` exclusively. JSON parameter support would require dispatching on `DataSerializerType` + `AcJsonSerializer` reference. Low priority — binary is the primary transport.
|
Uses `ToBinary()` / `BinaryTo()` exclusively. JSON parameter support would require dispatching on `DataSerializerType` + `AcJsonSerializer` reference. Low priority — binary is the primary transport.
|
||||||
|
|
@ -31,14 +31,21 @@ Uses `ToBinary()` / `BinaryTo()` exclusively. JSON parameter support would requi
|
||||||
|
|
||||||
### SIG-I-4: BufferWriterChunkSize defaults to 64KB for SignalR
|
### SIG-I-4: BufferWriterChunkSize defaults to 64KB for SignalR
|
||||||
|
|
||||||
**Status:** DONE
|
**Status:** Closed (2026-04-25, retroactive — was `Status: DONE` pre-LLMP-DEC-44 vocabulary normalization)
|
||||||
**Affects:** `AcBinaryHubProtocol` constructor, write path
|
**Affects:** `AcBinaryHubProtocol` constructor, write path
|
||||||
|
|
||||||
`BufferWriterChunkSize = 4096` set in `AcBinaryHubProtocol` constructor. Aligns with Kestrel slab size, reduces latency-to-first-byte. Non-SignalR paths keep 64KB default.
|
The framework's `BufferWriterChunkSize` default was 64KB, suboptimal for SignalR chunked responses (misaligned with Kestrel slab size, higher latency-to-first-byte).
|
||||||
|
|
||||||
|
### Resolution
|
||||||
|
- **What:** Set `BufferWriterChunkSize = 4096` explicitly in `AcBinaryHubProtocol` constructor.
|
||||||
|
- **Where:** `AcBinaryHubProtocol` constructor (override applied at construction time).
|
||||||
|
- **Why:** Aligned with Kestrel's slab allocation size; reduces latency-to-first-byte for chunked SignalR responses.
|
||||||
|
- **Scope:** Override is SignalR-specific. Non-SignalR paths keep the 64KB default — this is intentional and stable behaviour now.
|
||||||
|
- **Date:** Code change predates this status update; the entry was already `Status: DONE` before the LLMP-DEC-44 vocabulary normalization. The current update only formalizes it as `Closed` per the new 3-value convention.
|
||||||
|
|
||||||
### SIG-I-5: WebSocket buffer sizes are hardcoded
|
### SIG-I-5: WebSocket buffer sizes are hardcoded
|
||||||
|
|
||||||
**Status:** Acceptable
|
**Status:** Open
|
||||||
**Affects:** `AcSignalRClientBase` connection setup
|
**Affects:** `AcSignalRClientBase` connection setup
|
||||||
|
|
||||||
Transport max message size (30MB) and application buffer (30MB) are hardcoded. Sufficient for current payloads but not configurable per-deployment.
|
Transport max message size (30MB) and application buffer (30MB) are hardcoded. Sufficient for current payloads but not configurable per-deployment.
|
||||||
|
|
@ -47,7 +54,7 @@ Transport max message size (30MB) and application buffer (30MB) are hardcoded. S
|
||||||
|
|
||||||
### SIG-I-6: GetAll returns raw byte[] for populate/merge
|
### SIG-I-6: GetAll returns raw byte[] for populate/merge
|
||||||
|
|
||||||
**Status:** By design
|
**Status:** Open
|
||||||
**Affects:** `AcSignalRDataSource.LoadDataSourceAsync`
|
**Affects:** `AcSignalRDataSource.LoadDataSourceAsync`
|
||||||
|
|
||||||
The `GetAll` path uses `IsRawBytesData = true` to receive raw `byte[]` from the protocol, then deserializes into the existing list via `PopulateMerge`. This avoids allocating a temporary `List<T>` for merge. The extra copy (pipe → byte[]) is the trade-off.
|
The `GetAll` path uses `IsRawBytesData = true` to receive raw `byte[]` from the protocol, then deserializes into the existing list via `PopulateMerge`. This avoids allocating a temporary `List<T>` for merge. The extra copy (pipe → byte[]) is the trade-off.
|
||||||
|
|
@ -88,7 +95,7 @@ See `SIGNALR_TODO.md#sig-t-5`.
|
||||||
|
|
||||||
### SIG-I-8: HubConnectionBuilder inner DI isolation
|
### SIG-I-8: HubConnectionBuilder inner DI isolation
|
||||||
|
|
||||||
**Status:** Workaround-in-place (dedicated options-passing overload)
|
**Status:** Open
|
||||||
**Affects:** Consumer client setup in `Program.cs` (MAUI, WASM, ASP.NET Core server prerender)
|
**Affects:** Consumer client setup in `Program.cs` (MAUI, WASM, ASP.NET Core server prerender)
|
||||||
|
|
||||||
`HubConnectionBuilder.Services` is a separate `IServiceCollection` from the outer host DI. `services.Configure<AcBinaryHubProtocolOptions>(...)` registered in the outer container does NOT flow into `HubConnectionBuilder.Services`. Calling `hubBuilder.AddAcBinaryProtocol()` with no args silently falls back to default options.
|
`HubConnectionBuilder.Services` is a separate `IServiceCollection` from the outer host DI. `services.Configure<AcBinaryHubProtocolOptions>(...)` registered in the outer container does NOT flow into `HubConnectionBuilder.Services`. Calling `hubBuilder.AddAcBinaryProtocol()` with no args silently falls back to default options.
|
||||||
|
|
@ -103,7 +110,7 @@ hubBuilder.AddAcBinaryProtocol(protocolOpts);
|
||||||
|
|
||||||
### SIG-I-9: First-call null response (observed)
|
### SIG-I-9: First-call null response (observed)
|
||||||
|
|
||||||
**Status:** Open — not diagnosed
|
**Status:** Open
|
||||||
**Affects:** `PostDataAsync<T>` awaiter / OnReceiveMessage → pending-request correlation
|
**Affects:** `PostDataAsync<T>` awaiter / OnReceiveMessage → pending-request correlation
|
||||||
|
|
||||||
Observed symptom: first `GetProductDtos_80`-style call returns null despite server serializing and sending a valid ~80KB chunked response. Second call (client-side retry) works normally.
|
Observed symptom: first `GetProductDtos_80`-style call returns null despite server serializing and sending a valid ~80KB chunked response. Second call (client-side retry) works normally.
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ For higher-level SignalR abstractions see `../SIGNALR/SIGNALR_ISSUES.md`.
|
||||||
|
|
||||||
## SBP-I-1: AsyncSegment send-path unsupported on WebAssembly
|
## SBP-I-1: AsyncSegment send-path unsupported on WebAssembly
|
||||||
|
|
||||||
**Severity:** Major (on WASM) · **Status:** Workaround-in-place · **Area:** `AsyncPipeWriterOutput` / WASM runtime
|
**Severity:** Major (on WASM) · **Status:** Open · **Area:** `AsyncPipeWriterOutput` / WASM runtime
|
||||||
|
|
||||||
### Description
|
### Description
|
||||||
`AsyncPipeWriterOutput.SyncAwaitFlush` uses `Task.Wait(timeout)` — deadlocks the single-threaded WASM UI thread. Therefore, WASM clients cannot SEND with `AsyncSegment` mode.
|
`AsyncPipeWriterOutput.SyncAwaitFlush` uses `Task.Wait(timeout)` — deadlocks the single-threaded WASM UI thread. Therefore, WASM clients cannot SEND with `AsyncSegment` mode.
|
||||||
|
|
@ -20,7 +20,7 @@ None — architectural constraint of browser WASM threading model.
|
||||||
|
|
||||||
## SBP-I-2: StaticWebAssets SDK "Illegal characters" noise (consumer build)
|
## SBP-I-2: StaticWebAssets SDK "Illegal characters" noise (consumer build)
|
||||||
|
|
||||||
**Severity:** Cosmetic (non-blocking) · **Status:** Upstream SDK limitation · **Area:** SDK, not our code
|
**Severity:** Cosmetic (non-blocking) · **Status:** Open · **Area:** SDK, not our code
|
||||||
|
|
||||||
### Description
|
### Description
|
||||||
Consumer projects using `Microsoft.NET.Sdk.BlazorWebAssembly` may see "Illegal characters in path" errors in the VS design-time error list. Originates from the SDK's `DefineStaticWebAssets` task calling legacy `FileIOPermission.EmulateFileIOPermissionChecks`, which is stricter than NTFS (e.g., double spaces or certain path patterns in any wwwroot asset trigger it).
|
Consumer projects using `Microsoft.NET.Sdk.BlazorWebAssembly` may see "Illegal characters in path" errors in the VS design-time error list. Originates from the SDK's `DefineStaticWebAssets` task calling legacy `FileIOPermission.EmulateFileIOPermissionChecks`, which is stricter than NTFS (e.g., double spaces or certain path patterns in any wwwroot asset trigger it).
|
||||||
|
|
|
||||||
|
|
@ -44,3 +44,142 @@ Alternative to wire-detection: use SignalR handshake message's `extensions` JSON
|
||||||
{ "protocol": "acbinary", "version": 1, "extensions": { "acbinary": { "preferredMode": "AsyncSegment" } } }
|
{ "protocol": "acbinary", "version": 1, "extensions": { "acbinary": { "preferredMode": "AsyncSegment" } } }
|
||||||
```
|
```
|
||||||
Zero first-message overhead, fully explicit. Both sides advertise their send-modes; pick intersection. Specification to be drafted; compatibility with non-AC clients (pure JSON etc.) must remain.
|
Zero first-message overhead, fully explicit. Both sides advertise their send-modes; pick intersection. Specification to be drafted; compatibility with non-AC clients (pure JSON etc.) must remain.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🟡 NuGet competitiveness ideas — NOT current priority
|
||||||
|
|
||||||
|
> **Status: speculative / future-only.** The following P3 entries are feature ideas for a public NuGet release to broaden adoption appeal vs. competing protocols (gRPC, MessagePack, MemoryPack, Protobuf-net). **None are committed work.** They exist here for future reference only — safe to skip during current sprint planning. Each requires:
|
||||||
|
> - Threat model / use-case justification document **before** any code
|
||||||
|
> - Benchmark of zero-copy / pipeline impact
|
||||||
|
> - Decorator pattern (compose, do not embed in `AcBinaryHubProtocol`)
|
||||||
|
> - Opt-in via options; default path **must** stay free of any added overhead
|
||||||
|
|
||||||
|
These ideas were captured because the **wider binary-protocol market is currently silent** on these features (no major .NET binary serializer ships built-in encryption / compression / tracing). Real differentiation potential, but only if executed correctly.
|
||||||
|
|
||||||
|
## SBP-T-5: Optional payload encryption (`AcEncryptionOptions`)
|
||||||
|
**Priority:** P3 — IDEA · **Type:** Feature (NuGet competitiveness) · **Status:** Open
|
||||||
|
|
||||||
|
### Niches where TLS alone is insufficient
|
||||||
|
- **SignalR backplane / Azure SignalR Service** — relay sees plaintext; payload encryption gives true end-to-end client↔client.
|
||||||
|
- **TLS-terminating proxies** (Cloudflare, nginx, ALB) — internal segment cleartext; payload encryption is TLS-topology-independent.
|
||||||
|
- **Mobile/MAUI reverse-engineering** — if the key derives from user credentials (not embedded), a leaked binary doesn't grant payload access.
|
||||||
|
- **Multi-tenant SaaS over shared channel** — per-tenant keys make broadcast leakage fail closed.
|
||||||
|
- **Zero-trust intra-cluster** (modern enterprise trend) — complementary to mTLS service mesh.
|
||||||
|
- **Regulatory compliance** (HIPAA, PCI-DSS 4.1, GDPR Art.32, FedRAMP) — auditors often reject "TLS only" for ePHI / cardholder / classified data.
|
||||||
|
|
||||||
|
### Design constraints
|
||||||
|
- **Decorator only** — `EncryptingHubProtocolWrapper`, never embedded into `AcBinaryHubProtocol` (SRP).
|
||||||
|
- **`System.Security.Cryptography.AesGcm`** — never custom crypto. Optional ChaCha20-Poly1305 for ARM/mobile (faster on those targets).
|
||||||
|
- **Authenticated encryption mandatory** — encryption-only modes (CBC etc.) **forbidden**. Tamper detection is baseline, not optional.
|
||||||
|
- **Pluggable `IAcEncryptionKeyProvider`** — caller chooses: env var, KeyVault, HKDF from user creds, hardware token. No baked-in key source.
|
||||||
|
- **Replay protection bundled** — monotonic per-connection nonce counter; rejects out-of-window packets.
|
||||||
|
- **Opt-in** — `AcEncryptionOptions = null` (default) means the encryption code path is **completely absent** from the hot path (no branch, no check).
|
||||||
|
- **WASM compatibility** mandatory test — `AesGcm` is available on net8+ WASM.
|
||||||
|
- **AsyncSegment compatibility decision** — per-chunk auth tag (~28 byte overhead per chunk) vs. single-stream cipher (loses pipeline parallelism). Benchmark required.
|
||||||
|
|
||||||
|
### Acceptance criteria (before any code)
|
||||||
|
- Threat model document: what does this defend against that TLS doesn't, in measurable terms.
|
||||||
|
- Benchmark plan: zero-copy loss percentage, AsyncSegment chunk overhead.
|
||||||
|
- Decorator API sketch reviewed.
|
||||||
|
|
||||||
|
## SBP-T-6: Optional message compression with `MinSize` threshold
|
||||||
|
**Priority:** P3 — IDEA · **Type:** Feature (NuGet competitiveness) · **Status:** Open
|
||||||
|
|
||||||
|
### Niches
|
||||||
|
- **Large structured payloads** (orders, product lists, shipping documents) — typical 50-90% compression on text-heavy DTOs.
|
||||||
|
- **Mobile / metered connections** — bandwidth cost reduction.
|
||||||
|
- **Legacy slow links** (satellite, GPRS-class IoT) — payload size matters more than CPU.
|
||||||
|
- **Cross-DC replication / SignalR backplane traffic** — bytes through Redis backplane × N consumers.
|
||||||
|
- **Mixed traffic real-time apps** (heartbeat / acks / lookups + occasional large DTO) — see `MinSize` rationale below.
|
||||||
|
|
||||||
|
### `MinSize` threshold — market-gap differentiator
|
||||||
|
|
||||||
|
**Industry observation:** every major compression-aware system today is "all or nothing": gRPC, HTTP/2 response compression, WebSocket per-message-deflate (RFC 7692), Kafka producer compression — none have per-message size threshold. They compress everything once enabled.
|
||||||
|
|
||||||
|
**Empirical reality:** every compression algorithm has a **break-even point** below which compression LOSES (output > input + CPU cost wasted):
|
||||||
|
|
||||||
|
| Algorithm | Approximate break-even (byte) | Recommended `MinSize` default |
|
||||||
|
|-----------|------------------------------|-------------------------------|
|
||||||
|
| LZ4 | ~64-128 | 128 |
|
||||||
|
| Snappy | ~100 | 128 |
|
||||||
|
| Zstd | ~256-512 | 512 |
|
||||||
|
| Brotli | ~512-1024 | 1024 |
|
||||||
|
| Gzip/Deflate | ~200-500 | 512 |
|
||||||
|
|
||||||
|
In a real-time SignalR app where small heartbeats / acks / status pings interleave with occasional large DTOs, "all or nothing" compression **wastes CPU on small frames AND increases their size**. A per-message `MinSize` skip-threshold turns this from a coin-flip into a measurable win.
|
||||||
|
|
||||||
|
**Proposed semantic:**
|
||||||
|
```csharp
|
||||||
|
public sealed class AcCompressionOptions
|
||||||
|
{
|
||||||
|
public AcCompressionAlgorithm Algorithm { get; set; } // None | Lz4 | Brotli | Zstd | Snappy | Gzip
|
||||||
|
public int? MinSize { get; set; } = null; // null = use per-algorithm default; 0 = always compress; explicit value = override
|
||||||
|
public CompressionLevel Level { get; set; } = CompressionLevel.Optimal;
|
||||||
|
public bool AlwaysCompressInAsyncSegment { get; set; } = true; // see AsyncSegment trade-off below
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### AsyncSegment interaction — the streaming dilemma
|
||||||
|
|
||||||
|
In `AsyncSegment` mode the total message size is unknown until `CHUNK_END`, so a post-serialization `MinSize` check is impossible without buffering the entire message — which would defeat the pipeline-parallelism advantage of `AsyncSegment` in the first place.
|
||||||
|
|
||||||
|
| Option | How it works | Trade-off |
|
||||||
|
|--------|-------------|-----------|
|
||||||
|
| **(A) Per-chunk threshold** | Each chunk independently compressed or not, marker byte per chunk | Streaming-friendly. Lose cross-chunk dictionary benefit (Brotli/Zstd). Small chunks may bypass compression even when whole-message would benefit. |
|
||||||
|
| **(B) Whole-message buffer** | Buffer until `MinSize`, then decide; if compress, buffer the rest too | **Kills AsyncSegment pipeline parallelism** — message becomes effectively non-streaming. |
|
||||||
|
| **(C) `AsyncSegment` = always compress** (PROPOSED DEFAULT) | `MinSize` ignored in AsyncSegment mode; `Bytes` and `Segment` modes honour it | Logical: anyone choosing `AsyncSegment` is already optimizing for large payloads (small ones would use `Bytes`/`Segment`), so the threshold would never trigger anyway. Simple mental model. |
|
||||||
|
| **(D) Sliding window first-chunk buffer** | Buffer first N bytes (= MinSize); if exceeded, that buffer flushes as compressed first chunk + downstream streaming with dictionary | Elegant, preserves both threshold and pipeline. Significantly more complex. Future optimization. |
|
||||||
|
|
||||||
|
**Default decision: (C)** — `AlwaysCompressInAsyncSegment = true`. Override possible per-options. Document the trade-off explicitly so users picking `AsyncSegment + compression + small messages` know what they're choosing.
|
||||||
|
|
||||||
|
### Other design constraints
|
||||||
|
- **Decorator pattern**, same as SBP-T-5.
|
||||||
|
- **Algorithm-pluggable** — at minimum LZ4 (fastest), Brotli (best ratio for text), Zstd (modern balanced). Default **none**.
|
||||||
|
- **Order with encryption matters**: compress FIRST, encrypt AFTER (compressing ciphertext is futile).
|
||||||
|
- **Wire marker byte** — 1-byte algorithm ID prefix so decoder knows what to expand. `0x00` = uncompressed (the small-message path), other values = algorithm IDs.
|
||||||
|
- **Per-algorithm `MinSize` default** — applied when `MinSize == null`; user override always wins.
|
||||||
|
- **Empty / single-byte messages** — bypass compression unconditionally regardless of `MinSize` (zero benefit, nonzero overhead).
|
||||||
|
|
||||||
|
### Acceptance criteria
|
||||||
|
- Benchmark: compression ratio + CPU cost per algorithm on representative payloads (Order, ProductList, ShippingDocument) AND small payloads (Ping, Ack, StatusUpdate). Verify break-even points empirically — adjust `MinSize` per-algorithm defaults if measured values diverge from the table above.
|
||||||
|
- Decorator API sketch reviewed.
|
||||||
|
- AsyncSegment interaction documented — the (C) default with override path; sample log output showing `MinSize`-skipped vs. compressed messages for diagnostics.
|
||||||
|
- Fallback / negotiation strategy: what happens if peer can't decompress? (Suggested: wire-marker `0x00` always works since it means "uncompressed" — sender can downgrade per-message if peer signals incompatibility, similar to HTTP `Accept-Encoding` semantics.)
|
||||||
|
|
||||||
|
## SBP-T-7: OpenTelemetry tracing integration
|
||||||
|
**Priority:** P3 — IDEA · **Type:** Feature (NuGet competitiveness) · **Status:** Open
|
||||||
|
|
||||||
|
### Niches
|
||||||
|
- **Distributed tracing** is a modern .NET observability standard — gRPC has it, MessagePack/MemoryPack/Protobuf-net don't.
|
||||||
|
- **Production diagnostics** — correlate SignalR call → server method → DB query in one trace.
|
||||||
|
- **Backpressure / latency analysis** — flush time, chunk-by-chunk progress as span events.
|
||||||
|
|
||||||
|
### Design constraints
|
||||||
|
- **`System.Diagnostics.ActivitySource`** based — no hard dependency on OTel SDK; consumers wire their own exporter.
|
||||||
|
- **Opt-in via `AcBinaryHubProtocolOptions.ActivitySource`** — null = zero overhead, no branch.
|
||||||
|
- **Span structure**: per `WriteMessage` / `TryParseMessage` invocation; chunked path emits chunk events as span events (not nested spans, to avoid cardinality explosion).
|
||||||
|
- **W3C Trace Context propagation** — read/write `traceparent` header in `Headers` dictionary for cross-service correlation.
|
||||||
|
|
||||||
|
### Acceptance criteria
|
||||||
|
- API sketch with sample exporter wiring (Jaeger / OTel Collector).
|
||||||
|
- Hot-path branch verification — when `ActivitySource == null`, JIT must eliminate the tracing code completely.
|
||||||
|
|
||||||
|
## SBP-T-8: Optional HMAC signing (without encryption)
|
||||||
|
**Priority:** P3 — IDEA · **Type:** Feature (NuGet competitiveness) · **Status:** Open
|
||||||
|
|
||||||
|
### Niches
|
||||||
|
- **Tamper detection where confidentiality is acceptable** — audit log forwarding, telemetry where data isn't sensitive but integrity must be provable.
|
||||||
|
- **Compliance lite** — "we sign all messages" satisfies some integrity-focused audit requirements without encryption complexity.
|
||||||
|
- **Plaintext debugging + integrity** — payload remains readable in tcpdump / Wireshark (debugging-friendly), but tampering is detected.
|
||||||
|
|
||||||
|
### Design constraints
|
||||||
|
- **Decorator pattern** — `SigningHubProtocolWrapper`, separate from encryption. Composable: encryption decorator + signing decorator can stack (encrypt-then-sign or sign-then-encrypt — pick canonical order, document it).
|
||||||
|
- **`System.Security.Cryptography.HMACSHA256`** default; SHA-512 optional.
|
||||||
|
- **Append-only on wire** — N-byte MAC tag at end of message, before any framing trailer.
|
||||||
|
- **Pluggable `IAcSigningKeyProvider`** — same idea as encryption key provider.
|
||||||
|
- **Constant-time comparison** mandatory — `CryptographicOperations.FixedTimeEquals`, never `==`.
|
||||||
|
|
||||||
|
### Acceptance criteria
|
||||||
|
- API sketch.
|
||||||
|
- Stack-with-encryption order decision documented (industry standard: **encrypt-then-MAC**, but evaluate trade-offs).
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue