[LOADED_DOCS: 4 files, no new loads]

Refactor docs: topic folders, TOON, XCUT, protocol sync

- Migrated all topic documentation into dedicated folders with canonical `README.md`, `ISSUES.md`, and `TODO.md` per topic (e.g., `LOGGING/`, `SIGNALR/`, `BINARY/`, `TOON/`).
- Added comprehensive TOON serializer documentation: design, format, options, attributes, inference, issues, and TODOs.
- Introduced `XCUT` folder for cross-cutting issues and TODOs, with canonical entries and topic cross-references.
- Updated all references and navigation to use new folder-based doc paths; fixed links and clarified doc structure.
- Enhanced AI agent protocol: enforce session skill preloading, `[LOADED_DOCS: ...]` short-name prefix, and mandatory `docs-check` skill for doc/code sync.
- Updated `.csproj` to include all `README.md` files for IDE visibility.
- Improved and clarified SignalR, grid, and project-level documentation.
- Minor code/test tweaks and doc content corrections for consistency.
This commit is contained in:
Loretta 2026-04-24 21:54:04 +02:00
parent 61509f1b95
commit affa85e5c5
76 changed files with 2389 additions and 1540 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

176
.github/skills/docs-check/SKILL.md vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -19,7 +19,7 @@ This skill READS `.md` files and updates the LLM's `[LOADED_DOCS: ...]` state. I
Parse the user's most recent message (and the wider conversation tail if relevant) for concrete concepts. Examples:
- Class / type names: `AcLoggerBase`, `SegmentBufferReader`, `AcBinaryHubProtocol`, `FruitBankSignalRClient`
- Class / type names: `AcLoggerBase`, `SegmentBufferReader`, `AcBinaryHubProtocol`, `<Consumer>SignalRClient` (any derived/consumer-specific type)
- Feature areas: "logger", "log writer", "serializer", "SignalR", "hub protocol", "chunked framing", "connection builder", "options"
- File hints: `Program.cs`, `AcLoggerBase.cs`, `SIGNALR.md`
- Patterns / idioms: "DI factory", "appsettings", "mode negotiation"
@ -35,21 +35,51 @@ Keep the set small (usually 1-3 root tokens). If the request genuinely spans mul
## Step 2 — Map tokens to glob patterns (semantic, not hardcoded)
For each root token, synthesize `.md` filename patterns using common conventions:
### ⚠️ CRITICAL — the recursive `**/` wildcard is MANDATORY in every glob
| Token example | Glob patterns to try |
|---|---|
| `logger`, `log`, `logging` | `**/docs/LOGGING*.md` |
| `binary`, `serializer` | `**/docs/BINARY*.md` |
| `signalr`, `hub` | `**/docs/SIGNALR*.md` |
| `protocol`, `wire`, `chunked` | `**/docs/*PROTOCOL*.md` |
| `grid`, `mggrid` | `**/docs/MGGRID*.md` |
The `**/` is NOT cosmetic. It matches `docs/` at **any depth** in the workspace:
- repo-root: `<Repo>/docs/TOPIC/`
- project-level: `<Repo>/<Project>/docs/TOPIC/` ← **very common for Pattern B layouts**
- nested: `<Repo>/<Project>/<SubProject>/docs/TOPIC/`
Do NOT require the tokens to match a pre-baked list — construct patterns from the token itself uppercased (e.g., `logger``**/docs/LOGGER*.md` + `**/docs/LOGGING*.md`). Natural language variants (logger/logging, serialize/serializer, binary/binaries) should all be attempted.
**Correct form — always**: `<OptionalRepoPrefix>/**/docs/{TOKEN}/**/*.md`
**Wrong form — never**: `<OptionalRepoPrefix>/docs/{TOKEN}/**/*.md` (missing the leading `**/`)
Also consider suffix patterns:
- `**/docs/*{TOKEN}*.md` (substring match)
- `**/docs/*{TOKEN}_ISSUES.md`, `**/docs/*{TOKEN}_TODO.md` (paired docs)
**Failure mode** (this happens often with Pattern B projects):
- You know the target repo (e.g. via `own-dep-repos`) — say `<Repo> = AyCode.Core`.
- You synthesize `<Repo>/docs/{TOKEN}/...` because "that's where docs usually live".
- Glob returns 0 matches (repo-root `docs/` doesn't contain topic folders — only flat reference docs).
- You conclude "no docs exist" and fall through to code-search.
- Meanwhile the actual docs sit at `<Repo>/<Project>/docs/{TOKEN}/` — one level deeper.
**The rule is absolute**: NEVER drop the leading `**/`, even when you "know" the repo. Let the recursive glob find the actual depth. Relative-path guesses based on "usual" layouts are a reliable source of false-empty conclusions.
### File layout convention
(See `LLM_PROTOCOL_DECISIONS.md` entry "Docs migrated to folder+README pattern".)
Topics with multiple files live in named folders: `docs/TOPIC/README.md` + `docs/TOPIC/TOPIC_ISSUES.md` + `docs/TOPIC/TOPIC_TODO.md` (or other `TOPIC_*.md` companions). Single-file reference docs remain flat at the `docs/` root (e.g., `docs/ARCHITECTURE.md`, `docs/GLOSSARY.md`).
For each root token, synthesize glob patterns targeting BOTH layouts:
| Token example | Primary glob (folder) | Companion glob (flat + variants) |
|---|---|---|
| `logger`, `log`, `logging` | `**/docs/LOGGING/**/*.md` | `**/docs/LOGGING_*.md` (legacy/variants) |
| `binary`, `serializer` | `**/docs/BINARY/**/*.md` | `**/docs/BINARY_*.md` |
| `signalr`, `hub` | `**/docs/SIGNALR*/**/*.md` | — (covers SIGNALR + SIGNALR_BINARY_PROTOCOL folders) |
| `protocol`, `wire`, `chunked` | `**/docs/*PROTOCOL*/**/*.md` | — |
| `grid`, `mggrid` | `**/docs/MGGRID/**/*.md` | — |
| `architecture`, `conventions`, `glossary` | — (flat, single-file) | `**/docs/ARCHITECTURE.md`, `**/docs/CONVENTIONS.md`, `**/docs/GLOSSARY.md` |
Do NOT require tokens to match a pre-baked list — construct patterns from the token itself uppercased:
- Primary: `**/docs/{TOKEN}/**/*.md` (matches everything inside the topic folder)
- Companion/variant: `**/docs/{TOKEN}_*.md` (matches flat files or variant prefix folders like `SIGNALR_BINARY_PROTOCOL`)
Natural language variants (logger/logging, serialize/serializer, binary/binaries) should all be attempted against both the primary and companion patterns.
**For README.md discovery** (folder-navigation rule): if a topic folder match is found, the `README.md` in that folder is the entry point and MUST be included in the load set (not just sibling `_ISSUES` / `_TODO` files).
(See the CRITICAL section at the top of this Step 2 for the full explanation of why the leading `**/` is mandatory — this is the most common cause of false-empty docs conclusions.)
## Step 3 — Execute the Glob and dedupe against already-loaded docs
@ -61,9 +91,11 @@ Run each glob pattern via the host agent's Glob tool. Collect all matching absol
If the total match count exceeds 10, narrow the glob pattern (e.g., require domain token near the filename start, not just substring). LLM context is finite.
**False-empty guardrail:** if the glob returns 0 matches OR all matched files are 0-byte, do NOT conclude "docs are empty" — first re-validate the glob (typo? literal path substituted?) and retry once with the same token under a corrected `**/docs/...` pattern (NEVER with an ad-hoc path guess). Only after the validated retry also fails should you fall through to code-search.
## Step 4 — Load the filtered set
Read all remaining matches in parallel (batch the Read calls in one tool-use block). The newly-loaded basenames will appear in your next response's `[LOADED_DOCS: ...]` prefix under the `+K this turn: <basenames>` delta, per the active repo's Rule #1 format.
Read all remaining matches in parallel (batch the Read calls in one tool-use block). The newly-loaded files will appear in your next response's `[LOADED_DOCS: ...]` prefix under the `+K this turn: <short names>` delta, per the active repo's Rule #1 format (basename by default; `TOPIC/README.md` for topic-folder READMEs to disambiguate across the many `README.md` files the Pattern-B docs layout introduces).
## Step 5 — Respect the paired-docs convention

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -17,6 +17,7 @@
<ItemGroup>
<None Include="docs\**\*.md" />
<None Include="**\README.md" Exclude="$(DefaultItemExcludes);docs\**" />
</ItemGroup>
</Project>

View File

@ -2,7 +2,7 @@
Server-side singleton logger for static access across the application.
> For full logging architecture see `docs/LOGGING.md`. For core logger and writer abstractions see `AyCode.Core/Loggers/README.md`.
> For full logging architecture see `docs/LOGGING/README.md`. For core logger and writer abstractions see `AyCode.Core/Loggers/README.md`.
## Key Files

View File

@ -10,7 +10,7 @@ Server-side extension of AyCode.Core. Provides server-specific implementations t
| Document | Topic |
|---|---|
| `LOGGING_SERVER.md` | GlobalLogger singleton, server-side logging |
| `LOGGING/README.md` | GlobalLogger singleton, server-side logging |
## Folder Structure

View File

@ -1,6 +1,6 @@
# Server Logging
Server-side logging extensions. For core framework (base classes, configuration, LogLevel, ILogger bridge) see `AyCode.Core/docs/LOGGING.md`. For remote writers (HTTP, browser, SignalR) see `AyCode.Services/docs/LOGGING_REMOTE.md`.
Server-side logging extensions. For core framework (base classes, configuration, LogLevel, ILogger bridge) see `AyCode.Core/AyCode.Core/docs/LOGGING/README.md`. For remote writers (HTTP, browser, SignalR) see `AyCode.Services/docs/LOGGING/README.md`.
## GlobalLogger

View File

@ -0,0 +1,16 @@
# AyCode.Core.Server documentation
Topic documentation for the `AyCode.Core.Server` project (Layer 0, server-side).
## Topics
- [`LOGGING/`](LOGGING/README.md) — Server-side logger (variant of AyCode.Core's base logger)
## Navigation
Per the AI Agent Core Protocol (folder navigation rule), start from this README when browsing `docs/`. Each topic folder has its own `README.md` with the main content, plus optional `TOPIC_ISSUES.md` (known issues) and `TOPIC_TODO.md` (planned work).
## See also
- **Base logger** (framework): `../../AyCode.Core/AyCode.Core/docs/LOGGING/README.md`
- **Remote logger** (AyCode.Services variant): `../../AyCode.Services/docs/LOGGING/README.md`

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
@ -27,6 +27,7 @@
<ItemGroup>
<None Include="docs\**\*.md" />
<None Include="**\README.md" Exclude="$(DefaultItemExcludes);docs\**" />
</ItemGroup>
<ItemGroup>

View File

@ -2,7 +2,7 @@
Custom logging framework with multi-writer fan-out and `Microsoft.Extensions.Logging` integration. This directory contains the **core** logger and writer abstractions — the defining layer for the logging system.
> For full architecture, configuration, all writers, and remote logging flow see `docs/LOGGING.md`.
> For full architecture, configuration, all writers, and remote logging flow see `docs/LOGGING/README.md`.
## Architecture
@ -18,7 +18,7 @@ IAcLogWriterBase
└─ [AcLogItemWriterBase<T> in AyCode.Entities — structured writers]
```
**Two-level filtering:** Logger has a global `LogLevel` gate; each writer has its own `LogLevel`. Both must pass for a log entry to be written. See `docs/LOGGING.md` → Design Overview.
**Two-level filtering:** Logger has a global `LogLevel` gate; each writer has its own `LogLevel`. Both must pass for a log entry to be written. See `docs/LOGGING/README.md` → Design Overview.
## Key Files

View File

@ -2,9 +2,9 @@
High-performance binary serialization/deserialization. Two-phase processing, multiple wire modes, string interning, source generation. Primary goal: **speed**.
> Implementation details (zero virtual dispatch, buffer management): `../../docs/BINARY_IMPLEMENTATION.md`
> Output writers (ArrayBinaryOutput, BufferWriterBinaryOutput, chunk sizing): `../../docs/BINARY_WRITERS.md`
> Source generation (SGen architecture, hybrid model, bridge methods): `../../docs/BINARY_SGEN.md`
> Implementation details (zero virtual dispatch, buffer management): `../../docs/BINARY/BINARY_IMPLEMENTATION.md`
> Output writers (ArrayBinaryOutput, BufferWriterBinaryOutput, chunk sizing): `../../docs/BINARY/BINARY_WRITERS.md`
> Source generation (SGen architecture, hybrid model, bridge methods): `../../docs/BINARY/BINARY_SGEN.md`
## Architecture
@ -24,7 +24,7 @@ Two root paths in `AcBinarySerializer.Serialize<T>`:
| **SGen fast** | `UseGeneratedCode` + `GeneratedWriter != null` | 3 checks → `WriteObject` directly |
| **Full runtime** | No GeneratedWriter or `UseGeneratedCode=false` | IQueryable → Expression → TryWritePrimitive → WriteValueNonPrimitive → WriteObject |
SGen fast path skips: `is IQueryable`, `IsExpressionType`, `TryWritePrimitive` (GetTypeCode + 15-case switch), `WriteValueNonPrimitive` (4 interface checks). Wire format identical. Details: `../../docs/BINARY_SGEN.md`.
SGen fast path skips: `is IQueryable`, `IsExpressionType`, `TryWritePrimitive` (GetTypeCode + 15-case switch), `WriteValueNonPrimitive` (4 interface checks). Wire format identical. Details: `../../docs/BINARY/BINARY_SGEN.md`.
### Wire Format
@ -43,7 +43,7 @@ SGen fast path skips: `is IQueryable`, `IsExpressionType`, `TryWritePrimitive` (
| 144+ | **Headers** — Metadata, RefHandling, CacheCount |
| 192255 | **Tiny ints** — single-byte -16..47 |
Full spec: `docs/BINARY_FORMAT.md`
Full spec: `docs/BINARY/BINARY_FORMAT.md`
## Key Files
@ -93,7 +93,7 @@ Key wire-format options: `WireMode` (Compact/Fast), `ReferenceHandling` (None/On
`ReferenceHandling=None` + `UseStringInterning=None` = no scan pass (single-phase, fastest).
Presets: `Default`, `FastMode`, `ShallowCopy`, `WasmOptimized`. Details: `docs/BINARY_OPTIONS.md`.
Presets: `Default`, `FastMode`, `ShallowCopy`, `WasmOptimized`. Details: `docs/BINARY/BINARY_OPTIONS.md`.
## Dependencies

View File

@ -26,6 +26,7 @@ public static partial class AcToonSerializer
context.CurrentIndentLevel++;
WriteValue(value, type, context, 0);
context.WriteLine();
context.CurrentIndentLevel--;
context.WriteLine("}");

View File

@ -1,76 +1,19 @@
# Toons
# Toons (code folder)
Token-Oriented Object Notation (Toon) — an LLM-optimized serialization format with separate schema (@meta) and data (@data) sections. The primary goal is **LLM accuracy**: maximizing the precision and correctness of LLM responses by providing structured, unambiguous context. Designed for human and LLM readability with minimal token usage.
Source code for `AcToonSerializer` — Token-Oriented Object Notation, the LLM-optimized serialization format.
## Architecture
> **Domain documentation:** [`../../docs/TOON/README.md`](../../docs/TOON/README.md). This file is only a local file index.
### Serialization Modes
## Files in this folder
| Mode | Description |
|---|---|
| `Full` | Both @meta (schema) and @data (values) — default |
| `MetaOnly` | Only @meta section (send schema once, reuse across conversation) |
| `DataOnly` | Only @data section (when schema already sent) |
- `AcToonSerializer.cs` — main entry point (public `Serialize` / `SerializeTypeMetadata` / `SerializeMetadata` API).
- `AcToonSerializer.*.cs` — 14 partial class files split topically (MetaWriter, DataSection, TypeDefinitions, Descriptions, Attributes, AttributeExtraction, Placeholders, TopologicalSort, Navigation, ForeignKeys, Validation, ToonSerializationContext, ToonSerializeTypeMetadata).
- `AcToonSerializerOptions.cs` — options POCO, `ToonSerializationMode` enum, preset factories.
- `AcToonContextBase.cs` — shared base for serialization contexts.
- `ToonDescriptionAttribute.cs``[ToonDescription]` attribute + `ToonRelationType` enum.
- `ToonTypeRelation.cs` — type relation string constants (`BaseOf`, `DtoOf`, `ModelOf`, etc.).
### Output Sections
## Cross-references
- **@meta** — Type definitions, property descriptions, navigation info, constraints.
- **@types** — Per-type schema with constraints and examples.
- **@data** — Actual object values with optional type hints.
### Key Features
- Triple-quote syntax for multi-line strings.
- Topological sorting for complex type relationships.
- Navigation property tracking (foreign keys and relationships).
- Type relation understanding (inheritance, interfaces).
## Key Files
### Core
- **`AcToonSerializer.cs`** — Main serializer entry point and orchestration.
- **`AcToonSerializer.ToonSerializationContext.cs`** — Serialization context.
- **`AcToonSerializer.ToonSerializeTypeMetadata.cs`** — Cached type metadata.
- **`AcToonSerializerOptions.cs`** — Configuration and presets.
- **`AcToonContextBase.cs`** — Base context.
### Output Generation (partial classes of AcToonSerializer)
- **`AcToonSerializer.MetaWriter.cs`** — @meta section generation.
- **`AcToonSerializer.TypeDefinitions.cs`** — @types section generation.
- **`AcToonSerializer.DataSection.cs`** — @data section generation.
- **`AcToonSerializer.Descriptions.cs`** — Property description generation.
### Type Analysis
- **`AcToonSerializer.TopologicalSort.cs`** — Dependency-aware type ordering.
- **`AcToonSerializer.Navigation.cs`** — Navigation property discovery.
- **`AcToonSerializer.ForeignKeys.cs`** — Foreign key relationship detection.
- **`AcToonSerializer.Validation.cs`** — Output validation.
- **`AcToonSerializer.Placeholders.cs`** — Placeholder value generation.
### Attributes & Metadata
- **`AcToonSerializer.Attributes.cs`** — Attribute processing.
- **`AcToonSerializer.AttributeExtraction.cs`** — Attribute value extraction.
- **`ToonDescriptionAttribute.cs`** — Per-property description attribute.
- **`AcNavigationPropertyInfo.cs`** — Navigation property metadata.
- **`ToonTypeRelation.cs`** — Type relationship tracking (inheritance, interfaces).
## Configuration
| Option | Default | Description |
|---|---|---|
| `Mode` | Full | Full/MetaOnly/DataOnly |
| `UseIndentation` | true | Readability control |
| `UseInlineTypeHints` | false | Type hints in data section |
| `UseInlineComments` | false | Comments in data section |
| `ShowCollectionCount` | true | Collection sizes |
| `UseMultiLineStrings` | true | Triple-quote long strings |
| `UseEnhancedMetadata` | true | Rich property metadata |
| `OmitDefaultValues` | true | Skip null/default values |
| `WriteTypeNames` | true | Root type names in data |
**Presets:** `Default`, `MetaOnly`, `DataOnly`, `Compact`, `Verbose`.
## Dependencies
- Base classes from parent `Serializers/` folder
- Expression utilities from `Expressions/` folder (for queryable serialization)
- Shared serializer infrastructure (metadata cache, reference tracking, `IdentityMap`): [`../README.md`](../README.md).
- Full Toon docs: [`../../docs/TOON/`](../../docs/TOON/README.md).

View File

@ -141,3 +141,18 @@ Two-phase:
- `WriteObjectFullMarkerIId` / `WriteObjectFullMarkerAll`: `wrapper.Metadata` cached at entry, reused in ref-handling and non-ref branches
- `GetWrapper(type, slot)` is O(1) array index after first call, but `value.GetType()` is a virtual call — avoid repeating it
## Metadata Lifecycle & Cold-Start (planned: BIN-T-3 / BIN-T-4)
Today `BinarySerializeTypeMetadata` and `BinaryDeserializeTypeMetadata` are built lazily in `GetWrapperSlow` via `GlobalMetadataCache.GetOrAdd(type, MetadataFactory)`. The factory runs reflection property enumeration, attribute scans, and `Expression.Compile` per property — the dominant first-call cost for SGen types (see `BINARY_ISSUES.md#bin-i-10`).
**Planned evolution** (`BINARY_TODO.md#bin-t-3`):
- **`GeneratedMetadataRegistry`**: `ModuleInit` registers pre-built metadata per `[AcBinarySerializable]` type alongside the existing `GeneratedWriterRegistry` / `GeneratedReaderRegistry` entries. Generator passes references to its static `s_typeNameHash` / `s_propertyHashes` fields — single source of truth, no duplicate computation, no hot-path indirection (generator keeps using its own static fields).
- **Metadata ctor split**: a second ctor on `BinarySerializeTypeMetadata` / `BinaryDeserializeTypeMetadata` accepts pre-computed values (hashes, `MinWriteSize`, `ComplexPropertyCount`, `IsIId`, `IdAccessorType`, flags). No reflection in this ctor.
- **Lazy `RuntimeInit`**: `TypeMetadataBase` gets `volatile bool _runtimeInitialized` + `internal void RuntimeInit()`. `GetWrapperSlow` calls it only when `wrapper.GeneratedWriter == null || !Options.UseGeneratedCode` — i.e. for runtime-only types and the `UseGeneratedCode=false` edge case. SGen types skip it. Thread-safe by idempotence + `volatile` (no lock).
- **Hybrid safety**: SGen root path (`WriteObjectProperties` → `generatedWriter.WriteProperties`) never touches the SGen type's own property accessors; non-SGen child types come through the `MetadataFactory` path as today.
**Follow-up** (`BINARY_TODO.md#bin-t-4`): after BIN-T-3 removes reflection + `Expression.Compile` from the cold path, JIT of generated methods becomes dominant — mitigated via `[AggressiveOptimization]`, background `RuntimeHelpers.PrepareMethod`, and/or R2R (consumer publish config).
Wire format unchanged; `UseGeneratedCode=false` fallback continues to work identically (triggers `RuntimeInit` for SGen types on demand).

View File

@ -2,7 +2,7 @@
## Deserialization
### DESER-1: Non-array-backed memory — per-segment copy
### BIN-I-1: Non-array-backed memory — per-segment copy
**Status:** By design
**Affects:** `SequenceBinaryInput`
@ -12,7 +12,7 @@ When `ReadOnlySequence<byte>` segments are backed by native memory (not managed
**Impact:** Negligible. Non-array-backed `ReadOnlyMemory` is extremely rare (custom `MemoryManager<T>` with native memory, memory-mapped files). All standard .NET pools (`ArrayPool`, `MemoryPool.Shared`, Kestrel pipe) are array-backed.
### DESER-2: Cross-boundary scratch buffer is not pooled across calls
### BIN-I-2: Cross-boundary scratch buffer is not pooled across calls
**Status:** Acceptable
**Affects:** `SequenceBinaryInput._scratchBuffer`
@ -23,14 +23,14 @@ The scratch buffer is `ArrayPool.Rent`-ed on first cross-boundary read and reuse
**Possible optimization:** Store the scratch buffer on the pooled `BinaryDeserializationContext` and reuse across deserializations. Low priority — `ArrayPool` overhead is negligible.
### DESER-3: ReadBytes always copies
### BIN-I-3: ReadBytes always copies
**Status:** By design
**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.
### DESER-4: ReadStringUtf8 requires contiguous buffer
### BIN-I-4: ReadStringUtf8 requires contiguous buffer
**Status:** By design
**Affects:** `BinaryDeserializationContext.ReadStringUtf8(int length)`
@ -41,14 +41,14 @@ The scratch buffer is `ArrayPool.Rent`-ed on first cross-boundary read and reuse
## Serialization
### SER-1: BufferWriterBinaryOutput fallback path allocates per-chunk
### BIN-I-5: BufferWriterBinaryOutput fallback path allocates per-chunk
**Status:** Acceptable
**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 DESER-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.
### SER-2: AsyncPipeWriterOutput uses sync GetResult() for backpressure
### BIN-I-6: AsyncPipeWriterOutput uses sync GetResult() for backpressure
**Status:** By design (v1)
**Affects:** `AsyncPipeWriterOutput.Grow()``_lastFlush.GetAwaiter().GetResult()`
@ -59,75 +59,92 @@ When the previous `PipeWriter.FlushAsync()` hasn't completed by the next `Grow()
**Possible optimization:** `AsyncSegment` mode (future) with a custom async `WriteMessageAsync` protocol interface, enabling `await` on flush instead of `GetResult()`.
### SER-3: AsyncPipeWriterOutput fallback path — same as SER-1
### BIN-I-7: AsyncPipeWriterOutput fallback path — same as BIN-I-5
**Status:** Acceptable
**Affects:** `AsyncPipeWriterOutput.AcquireChunk` fallback
Same `TryGetArray` fallback as `BufferWriterBinaryOutput` (SER-1). 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.
## Deserialization (PipeReader)
### DESER-5: PipeReaderBinaryInput uses sync ReadAsync().GetResult()
### BIN-I-8: PipeReaderBinaryInput uses sync ReadAsync().GetResult()
**Status:** By design (v1)
**Affects:** `PipeReaderBinaryInput.Initialize()` and `TryAdvanceSegment()`
Same constraint as SER-2`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.
## Source Generator (SGen)
### SGEN-1: CS8625 warnings for non-nullable reference types
### BIN-I-9: CS8625 warnings for non-nullable reference types
**Status:** Known
**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.
## Buffer Writer (BWO)
### BIN-I-10: First-run cold-start overhead
### BWO-1: Struct copy semantics
**Status:** Active — mitigation planned (see `BINARY_TODO.md#bin-t-3`, `BINARY_TODO.md#bin-t-4`)
**Affects:** First `Serialize<T>`/`Deserialize<T>` per `[AcBinarySerializable]` type, per process
**Status:** By design
**Affects:** `BufferWriterBinaryOutput` value-type assignment
Cold-start cost chain on first use of an SGen type (before BIN-T-3 lands):
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.
1. `BinarySerializeTypeMetadata` ctor — reflection property enumeration + `GetCustomAttribute` scans
2. `Expression.Compile` per property accessor (dynamic getter + typed getters) — **dominant cost**
3. `TypeMetadataWrapper` ctor — `GeneratedWriterRegistry` + `GeneratedReaderRegistry` lookups, tracking state init
4. JIT of `WriteObject` / `WriteObjectProperties` / scan pass
5. JIT of generated `WriteProperties` / `ScanObject` / `ScanForDuplicates` (size scales with property count)
6. Cascade: each referenced child type repeats steps 15
### BWO-2: Initialize resets tracking
Subsequent calls hit cached metadata/wrappers → only Tier 0→1 JIT transition remains (background, async).
**Status:** By design
**Affects:** `BufferWriterBinaryOutput.Initialize` (context mode)
**Dominant cost today:** #1#2 (reflection + `Expression.Compile`). After BIN-T-3, the dominant residual cost shifts to #4#5 (JIT), addressed by BIN-T-4.
`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.
**Impact:** Measurable first-call latency — larger for types with many properties or deep graphs. For SignalR workloads the first message per entity type pays this tax.
### BWO-3: Constructor acquires chunk
**Status:** Acceptable (not a leak)
**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.
### BWO-4: No mode mixing
**Status:** By design
**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.
### SGEN-2: 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)
**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.
**Related TODO:** `BINARY_TODO.md#todo-02`
**Related TODO:** `BINARY_TODO.md#bin-t-2`
## Cross-cutting (also tracked in SignalR-side docs)
## Buffer Writer (BWO)
### XCUT-1: JSON-in-Binary request parameters
### BIN-I-12: Struct copy semantics
**Status:** Major tech debt, planned replacement (coordinated)
**Affects:** SignalR client→server request parameter path
**Status:** By design
**Affects:** `BufferWriterBinaryOutput` value-type assignment
Request parameters currently use JSON inside a Binary envelope (`SignalPostJsonDataMessage<T>`). Response path already uses pure Binary. Planned migration is a cross-project coordinated change — see `AyCode.Services/docs/SIGNALR_ISSUES.md` PROTO section and `BINARY_TODO.md#todo-01` for the broader picture. Do NOT attempt as a side-effect of unrelated work.
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
**Status:** By design
**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.
### BIN-I-14: Constructor acquires chunk
**Status:** Acceptable (not a leak)
**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.
### BIN-I-15: No mode mixing
**Status:** By design
**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.
## Cross-cutting (canonical home: `../XCUT/`)
### XCUT-I-1: JSON-in-Binary request parameters — cross-ref
Canonical entry: **`../XCUT/XCUT_ISSUES.md#xcut-i-1`**. Summary: client→server request parameters currently use JSON inside a Binary envelope (`SignalPostJsonDataMessage<T>`); response path is already pure Binary. Planned migration is tracked in `BINARY_TODO.md#bin-t-1` but requires coordinated client+server+consumer changes. Do NOT attempt as a side-effect.

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,57 @@
# AcBinarySerializer — TODO
## Priority legend
- **P0** blocker · **P1** important · **P2** nice-to-have · **P3** idea
---
## BIN-T-1: Replace JSON-in-Binary request parameters
**Priority:** P1 · **Type:** Refactor · **Related:** `../XCUT/XCUT_ISSUES.md#xcut-i-1` (canonical), `AyCode.Services/docs/SIGNALR/SIGNALR_TODO.md`
Migrate client→server request parameters from JSON-in-Binary envelope to direct Binary serialization (matching response path). Coordinated change across client, server, and all consuming projects. Do NOT attempt as side-effect of unrelated work.
**Acceptance:** `SignalPostJsonDataMessage<T>` replaced by a `SignalPostBinaryDataMessage<T>` (or equivalent); no JSON round-trip on the wire for request params; benchmarks confirm no regression.
## BIN-T-2: Re-evaluate DiscountProductMapping SGen exclusion
**Priority:** P3 · **Type:** Investigation · **Related:** `BINARY_ISSUES.md#bin-i-11`
Investigate whether the `new int Id` shadowing pattern can be handled by SGen (via base-class introspection, property-setter lookup on the base) to eliminate the runtime compiled-expression fallback for this entity class.
## BIN-T-3: Generate `BinarySerializeTypeMetadata` / `BinaryDeserializeTypeMetadata` at compile time
**Priority:** P1 · **Type:** Performance · **Related:** `BINARY_ISSUES.md#bin-i-10`
Eliminate the dominant first-call cost (reflection + `Expression.Compile` in metadata ctor) for SGen types by emitting pre-built metadata from the source generator.
**Design outline:**
- `TypeMetadataBase` / `BinarySerializeTypeMetadata` / `BinaryDeserializeTypeMetadata` get a second constructor that accepts pre-computed values (hashes, `MinWriteSize`, `ComplexPropertyCount`, flags, `IsIId`, `IdAccessorType`, etc.). No reflection executes in this ctor.
- Source generator keeps its existing `s_typeNameHash` / `s_propertyHashes` static fields (hot-path access stays static, zero indirection) and passes the **same references** to the metadata — single source of truth, no duplicate computation.
- `ModuleInit` registers both the writer/reader **and** the pre-built metadata into a `GeneratedMetadataRegistry`. `GetWrapperSlow` consults this registry first, falling back to the reflection-based `MetadataFactory` for runtime-only types.
- Lazy `RuntimeInit()` pattern for `Expression.Compile` property accessors:
- `TypeMetadataBase` gets `volatile bool _runtimeInitialized` + `internal void RuntimeInit()` (idempotent, no lock needed).
- `GetWrapperSlow` calls `metadata.RuntimeInit()` only when `wrapper.GeneratedWriter == null || !Options.UseGeneratedCode` — SGen types skip it entirely (they never touch runtime accessors on their own metadata; non-SGen child types have their own metadata and run the factory path normally).
- Hybrid mode stays correct: an SGen type on the SGen path never uses its own property accessors; a non-SGen child type's metadata runs the reflection ctor as today.
- `volatile` guards the flag; multiple contexts may race into `RuntimeInit`, second run is a no-op.
**Thread safety:** `GlobalMetadataCache` is `ConcurrentDictionary`; generated metadata is registered once at `ModuleInit`; wrapper construction is per-context and unchanged.
**Acceptance:**
- Cold benchmark: first `Serialize<T>` of a fresh SGen type shows no reflection / `Expression.Compile` on the call stack.
- Runtime fallback (`UseGeneratedCode=false`) still produces identical wire output and uses the full metadata accessors.
- Deserialize side has parity (same approach for `BinaryDeserializeTypeMetadata`).
- Existing tests pass; wire format unchanged.
## BIN-T-4: JIT Tier 1 warmup for generated hot methods
**Priority:** P2 · **Type:** Performance · **Related:** `BINARY_ISSUES.md#bin-i-10`
After BIN-T-3 lands, JIT of generated `WriteProperties` / `ScanObject` / `ScanForDuplicates` becomes the dominant residual first-call cost for SGen types. Options to evaluate (benchmark before committing):
- **`[MethodImpl(MethodImplOptions.AggressiveOptimization)]`** on the generated hot methods — skips Tier 0, compiles directly at Tier 1. Simple generator change. Trade-off: larger one-time JIT cost in exchange for eliminating the Tier 0→1 recompile step.
- **Background prewarm from `ModuleInit`**: `Task.Run(() => RuntimeHelpers.PrepareMethod(handle))` for each registered writer/reader method. Parallelizes JIT with app startup. Keep it opt-in (option flag) to avoid surprising consumers with extra startup threads.
- **ReadyToRun (R2R)** in consuming projects' publish config — pre-compiles IL to native at publish time. External to SGen, complementary. Document as a recommended publish setting.
- **Code chunking** (split generated methods exceeding a property threshold into sub-methods, e.g. `WriteProperties_Part1` / `_Part2`) — **measure first**. Only beneficial for unusually large types (20+ properties / nested collections). Call overhead can offset gains; JIT inliner may already handle reasonably-sized methods well.
- **Native AOT** — out of scope for this TODO; separate architectural decision with deployment-model implications.
**Acceptance:**
- Benchmark a realistic entity graph (≥ 3 referenced child types) and show first-call time within ~10% of steady-state after BIN-T-3 + chosen mitigation(s).
- Document which combination is recommended for SignalR hot-path workloads vs. batch serialization.

View File

@ -0,0 +1,24 @@
# BINARY — AcBinary serializer
Reference documentation for the AcBinary serialization system. Primary goal: **speed** (two-phase scan+serialize, reference tracking, string interning).
## Files in this folder
- [`BINARY_FEATURES.md`](BINARY_FEATURES.md) — High-level features and capabilities
- [`BINARY_FORMAT.md`](BINARY_FORMAT.md) — Wire format specification
- [`BINARY_OPTIONS.md`](BINARY_OPTIONS.md) — Configuration options (`AcBinaryOptions`)
- [`BINARY_IMPLEMENTATION.md`](BINARY_IMPLEMENTATION.md) — Internal implementation details
- [`BINARY_WRITERS.md`](BINARY_WRITERS.md) — Writer internals (streaming, buffering)
- [`BINARY_SGEN.md`](BINARY_SGEN.md) — Source generator (`AyCode.Core.Serializers.SourceGenerator`)
- [`BINARY_ISSUES.md`](BINARY_ISSUES.md) — Known issues and limitations
- [`BINARY_TODO.md`](BINARY_TODO.md) — Planned work / open tickets
## Start here
For a first read-through, start with [`BINARY_FEATURES.md`](BINARY_FEATURES.md) for the overview, then dive into [`BINARY_FORMAT.md`](BINARY_FORMAT.md) for wire-level details. [`BINARY_SGEN.md`](BINARY_SGEN.md) explains how the code-gen integrates at build time.
## Cross-references
- **Serialization overview** (Toon vs AcBinary vs AcJson, shared infrastructure): `../../Serializers/README.md`
- **SignalR binary transport** (uses this serializer): `../../AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL/README.md`
- **Glossary terms**: `../../../docs/GLOSSARY.md`

View File

@ -1,18 +0,0 @@
# AcBinarySerializer — TODO
## Priority legend
- **P0** blocker · **P1** important · **P2** nice-to-have · **P3** idea
---
## TODO-01: Replace JSON-in-Binary request parameters
**Priority:** P1 · **Type:** Refactor · **Related:** `BINARY_ISSUES.md#issue-01`, `AyCode.Services/docs/SIGNALR_TODO.md`
Migrate client→server request parameters from JSON-in-Binary envelope to direct Binary serialization (matching response path). Coordinated change across client, server, and all consuming projects. Do NOT attempt as side-effect of unrelated work.
**Acceptance:** `SignalPostJsonDataMessage<T>` replaced by a `SignalPostBinaryDataMessage<T>` (or equivalent); no JSON round-trip on the wire for request params; benchmarks confirm no regression.
## TODO-02: Re-evaluate DiscountProductMapping SGen exclusion
**Priority:** P3 · **Type:** Investigation · **Related:** `BINARY_ISSUES.md#issue-02`
Investigate whether the `new int Id` shadowing pattern can be handled by SGen (via base-class introspection, property-setter lookup on the base) to eliminate the runtime compiled-expression fallback for this entity class.

View File

@ -2,7 +2,7 @@
For planned/actionable work see `LOGGING_TODO.md`.
## ISSUE-01: NopLogWriter ctor signature mismatch (consumer-specific but framework-exposed)
## LOG-I-1: NopLogWriter ctor signature mismatch (consumer-specific but framework-exposed)
**Severity:** Minor (caught, non-blocking, but noisy) · **Status:** Open · **Area:** Writer-instantiation contract (`AcLoggerBase(string)` config-reading ctor)
@ -20,9 +20,9 @@ Two logger-construction paradigms coexist:
Console.Error noise tolerated. Alternatively, consumer uses DI-based `AddAcLoggerFactory<TLogger>` (see LOGGING.md) instead of the config-reading ctor — this path doesn't touch `LogWriters[]`.
### Related TODO
`LOGGING_TODO.md#todo-01`
`LOGGING_TODO.md#log-t-1`
## ISSUE-02: 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`
@ -36,9 +36,9 @@ Design predates `IOptions`/DI pattern.
Consumer avoids the config-reading `AcLoggerBase(string)` ctor on these platforms. DI-based `AddAcLoggerFactory<TLogger>` + `services.Configure<AcLoggerOptions>(...)` is the replacement (see LOGGING.md).
### Related TODO
`LOGGING_TODO.md#todo-02`
`LOGGING_TODO.md#log-t-2`
## ISSUE-03: 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
@ -47,17 +47,17 @@ Two ways to construct a logger coexist:
1. **Config-reading:** `new Logger(categoryName)``AcLoggerBase` reads `AyCode:Logger` section via `AcEnv.AppConfiguration`, instantiates writers via `Activator.CreateInstance`
2. **DI factory:** `services.Configure<AcLoggerOptions>(...)` + `services.AddAcLoggerFactory<TLogger>()``Func<string, TLogger>` resolved from DI, writers pulled from DI
Consumer picks per scenario; no automatic bridge. Risk: mixing patterns causes subtle failures (e.g. `MissingMethodException` — see ISSUE-01).
Consumer picks per scenario; no automatic bridge. Risk: mixing patterns causes subtle failures (e.g. `MissingMethodException` — see LOG-I-1).
### Related TODO
`LOGGING_TODO.md#todo-03`
`LOGGING_TODO.md#log-t-3`
## ISSUE-04: Default LogLevel diverges across the two setup paths
## LOG-I-4: Default LogLevel diverges across the two setup paths
**Severity:** Minor (surprise, not broken) · **Status:** Open · **Area:** `AcLoggerBase` field initializer vs `AcLoggerOptions`
### Description
The two logger-setup paths (see ISSUE-03) ship with **different default LogLevels**:
The two logger-setup paths (see LOG-I-3) ship with **different default LogLevels**:
- `AcLoggerBase.cs:18` — field initializer: `LogLevel = Error`
- `AcLoggerOptions.cs:27` — DI options default: `LogLevel = Info`
@ -71,9 +71,9 @@ Same consumer code, same "no configuration" state, two different log volumes dep
Historical: field initializer predates the Options class; Options was added as part of the DI-factory refactor with a developer-friendly `Info` default.
### Related TODO
`LOGGING_TODO.md#todo-05`
`LOGGING_TODO.md#log-t-5`
## ISSUE-05: `AcConsoleLogWriter.Initialize()` runs twice on parameterless ctor
## LOG-I-5: `AcConsoleLogWriter.Initialize()` runs twice on parameterless ctor
**Severity:** Minor (wasted work, not broken) · **Status:** Open · **Area:** `AcConsoleLogWriter` ctor chain
@ -97,9 +97,9 @@ Chain `: this(null)` invokes the `(string?)` ctor, which calls `Initialize()`. C
Remove the second `Initialize()` call in the parameterless ctor body (the chain already covered it).
### Related TODO
`LOGGING_TODO.md#todo-06`
`LOGGING_TODO.md#log-t-6`
## ISSUE-06: `ILogger.IsEnabled(MsLogLevel.None)` incorrectly reports enabled
## LOG-I-6: `ILogger.IsEnabled(MsLogLevel.None)` incorrectly reports enabled
**Severity:** Low (semantic bug, rare path) · **Status:** Open · **Area:** `AcLoggerBase.IsEnabled` + `MapFromMsLogLevel`
@ -125,9 +125,9 @@ Either:
- **(b)** Change the comparison to strict `<` with `Disabled` as sentinel (larger refactor — affects semantic of `Disabled` elsewhere)
### Related TODO
`LOGGING_TODO.md#todo-07`
`LOGGING_TODO.md#log-t-7`
## ISSUE-07: Misleading inline comment in `AcLoggerBase.Log<TState>`
## LOG-I-7: Misleading inline comment in `AcLoggerBase.Log<TState>`
**Severity:** Trivial (doc-only) · **Status:** Open · **Area:** `AcLoggerBase.cs:210-211`
@ -143,7 +143,34 @@ Comment says fallback is `null` (empty display), the code assigns `"Log"`. Contr
Update comment to match code: `// Use eventId.Name:eventId.Id if Name is set, otherwise fallback to "Log" per LOGGING.md convention`.
### Related TODO
Folded into `LOGGING_TODO.md#todo-08` (cleanup batch).
Folded into `LOGGING_TODO.md#log-t-8` (cleanup batch).
## LOG-I-8: Server-side NopCommerce plugin still uses legacy config-reading Logger ctor
**Severity:** Minor (works, but inconsistent with modern pattern + triggers LOG-I-1 noise) · **Status:** Open · **Area:** Consumer adoption gap in `Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs`
### Description
Client side (`FruitBankHybridApp.*` — Web, Web.Client, MAUI) was migrated to the DI-factory pattern: `services.Configure<AcLoggerOptions>(...)` + `services.AddAcLoggerFactory<Logger>()`. The server-side plugin was NOT migrated — it still:
1. Directly constructs a logger via `new Logger(nameof(AyCodeBinaryHubProtocol))` (`PluginNopStartup.cs:169`) and passes it as `opts.Logger` to the protocol — bypasses `ILogger<AcBinaryHubProtocol>` DI resolution that `AcSignalRProtocolExtensions.BuildProtocol` already implements.
2. Does NOT call `services.Configure<AcLoggerOptions>(configuration.GetSection("AyCode:Logger"))`.
3. Does NOT call `services.AddAcLoggerFactory<Logger>()`.
4. Writer registration exists (`services.AddScoped<IAcLogWriterBase, ConsoleLogWriter>()` + `NopLogWriter`) but those DI-registered singletons are NOT the instances the `new Logger(...)` ctor sees — the legacy ctor creates a parallel set via `Activator.CreateInstance`.
The legacy config-reading ctor DOES find the appsettings `AyCode:Logger` section via `AcEnv.AppConfiguration` (filesystem-backed, works on server) — so logging functions. But every `new Logger(...)` call:
- Triggers LOG-I-1 (NopLogWriter ctor mismatch → Console.Error noise)
- Reconstructs writer instances via `Activator` (not singleton-shared with DI-registered writers)
- Is inconsistent with the client-side pattern → two mental models for the same framework
### Fix direction
See `LOGGING_TODO.md#log-t-11`.
### Related
- `LOG-I-1` (trigger — NopLogWriter ctor mismatch, currently causing Console.Error noise)
- `LOG-I-3` (root cause — two coexisting setup patterns)
- `LOG-I-4` (consequence — different defaults between paths; server legacy path silently ships with `Error` default unless `AyCode:Logger:LogLevel` is set)
- Sibling gap: `../SIGNALR/SIGNALR_ISSUES.md#sig-i-7` (same server-side plugin, protocol-options adoption gap)
- Plugin doc drift: `Nop.Plugin.Misc.AIPlugin/docs/SIGNALR/README.md:22` still documents the pre-migration `new AcBinaryHubProtocol()` registration (actual code uses `.AddAcBinaryProtocol(opts => {...})`). Update needed.
## Evaluated review findings — NOT bugs (by-design)

View File

@ -5,8 +5,8 @@
---
## TODO-01: Fix the writer-ctor mismatch for DI-injected writers
**Priority:** P2 · **Type:** Bug fix · **Related:** `LOGGING_ISSUES.md#issue-01`
## LOG-T-1: Fix the writer-ctor mismatch for DI-injected writers
**Priority:** P2 · **Type:** Bug fix · **Related:** `LOGGING_ISSUES.md#log-i-1`
Options:
- **A)** Provide a fallback `(AppType, LogLevel, string?)` ctor on consumer writers that have DI-heavy primary ctors, with DI resolution via service-locator
@ -15,20 +15,20 @@ Options:
Eliminate the per-startup Console.Error noise regardless.
## TODO-02: Expose `AcEnv.AppConfiguration` setter for consumer init
**Priority:** P2 · **Type:** Feature · **Related:** `LOGGING_ISSUES.md#issue-02`
## LOG-T-2: Expose `AcEnv.AppConfiguration` setter for consumer init
**Priority:** P2 · **Type:** Feature · **Related:** `LOGGING_ISSUES.md#log-i-2`
Allow `AcEnv.SetConfiguration(IConfiguration)` so consumer `Program.cs` can do `AcEnv.SetConfiguration(builder.Configuration)` at startup. Enables config-reading pattern on MAUI/WASM without filesystem assumptions. Backward-compat: fall back to filesystem if no explicit set.
## TODO-03: Unify or clearly separate config-reading and DI-based patterns
**Priority:** P2 · **Type:** Docs / Refactor · **Related:** `LOGGING_ISSUES.md#issue-03`
## LOG-T-3: Unify or clearly separate config-reading and DI-based patterns
**Priority:** P2 · **Type:** Docs / Refactor · **Related:** `LOGGING_ISSUES.md#log-i-3`
Decide the canonical direction:
- **(a)** Deprecate config-reading pattern → all consumers migrate to DI factory
- **(b)** Keep both, with compile-time guidance (analyzer / XML doc `[Obsolete]` hints / decision tree in LOGGING.md)
- **(c)** Merge: DI-factory internally falls back to config-reading when `TLogger` doesn't match the `Activator` ctor shape
## TODO-04: Per-writer LogLevel via appsettings
## LOG-T-4: Per-writer LogLevel via appsettings
**Priority:** P2 · **Type:** Feature
Extend `AcLoggerOptions` with per-writer LogLevel overrides. Example shape:
@ -42,25 +42,25 @@ Extend `AcLoggerOptions` with per-writer LogLevel overrides. Example shape:
```
Factory applies overrides when constructing writers. Currently writer-LogLevel is hardcoded in writer ctors.
## TODO-05: Unify default LogLevel across setup paths
**Priority:** P1 · **Type:** Behaviour decision + cleanup · **Related:** `LOGGING_ISSUES.md#issue-04`
## LOG-T-5: Unify default LogLevel across setup paths
**Priority:** P1 · **Type:** Behaviour decision + cleanup · **Related:** `LOGGING_ISSUES.md#log-i-4`
Decide which default wins (`AcLoggerBase.cs:18` = `Error` vs `AcLoggerOptions.cs:27` = `Info`) and make both paths consistent. Recommendation: **`Info`** — matches the DI/Options path already in use for modern consumers (MAUI, WASM, ASP.NET Core) and is the common developer expectation.
Change: drop the field initializer in `AcLoggerBase` (or set it to `Info`). Update LOGGING.md with a single "Default LogLevel = Info" line so this can't drift again.
## TODO-06: Fix `AcConsoleLogWriter.Initialize()` double-run
**Priority:** P3 · **Type:** Bug fix · **Related:** `LOGGING_ISSUES.md#issue-05`
## LOG-T-6: Fix `AcConsoleLogWriter.Initialize()` double-run
**Priority:** P3 · **Type:** Bug fix · **Related:** `LOGGING_ISSUES.md#log-i-5`
Remove the redundant `Initialize()` call from the parameterless ctor body — the `: this(null)` chain already invokes it. No behaviour change; removes wasted `Console.ForegroundColor` assignment.
## TODO-07: Fix `ILogger.IsEnabled(MsLogLevel.None)` wrongly reporting enabled
**Priority:** P2 · **Type:** Bug fix · **Related:** `LOGGING_ISSUES.md#issue-06`
## LOG-T-7: Fix `ILogger.IsEnabled(MsLogLevel.None)` wrongly reporting enabled
**Priority:** P2 · **Type:** Bug fix · **Related:** `LOGGING_ISSUES.md#log-i-6`
Add `if (logLevel == MsLogLevel.None) return false;` at the top of `AcLoggerBase.IsEnabled`. One-line fix. Optionally add a unit test covering all six MS LogLevel values → expected bool.
## TODO-08: Cleanup batch (low-risk micro-refactor, one commit)
**Priority:** P3 · **Type:** Cleanup · **Related:** `LOGGING_ISSUES.md#issue-07`
## LOG-T-8: Cleanup batch (low-risk micro-refactor, one commit)
**Priority:** P3 · **Type:** Cleanup · **Related:** `LOGGING_ISSUES.md#log-i-7`
Single commit covering:
- Remove unused usings in `AcLoggerBase.cs` (`System.Security.AccessControl`, `System.Net.Mime.MediaTypeNames`)
@ -68,14 +68,14 @@ Single commit covering:
- Fix misleading comment at `AcLoggerBase.cs:210` — current comment says "fallback null" but code assigns `"Log"`. Make comment match code.
- Decide on the commented-out batch block in `AcLogItemWriterBase.cs:90-119`: either delete (git history preserves) or convert to a single `// TODO: see LOGGING_TODO.md#todo-XX` marker with a new TODO entry for the batch work.
## TODO-09: Fail-fast ctor validation in `AddAcLoggerFactory<TLogger>`
## LOG-T-9: Fail-fast ctor validation in `AddAcLoggerFactory<TLogger>`
**Priority:** P2 · **Type:** Feature
`AcLoggerServiceExtensions.AddAcLoggerFactory<TLogger>` uses `Activator.CreateInstance(typeof(TLogger), AppType, LogLevel, categoryName, writers)` only on first logger resolution. Ctor-mismatch therefore surfaces only at first log call, not at `services.BuildServiceProvider()` time.
Add a one-time reflection check at registration: `typeof(TLogger).GetConstructor([typeof(AppType), typeof(LogLevel), typeof(string), typeof(IAcLogWriterBase[])])` — if null, throw `InvalidOperationException` with the expected signature in the message. Cost: one reflection call per registration. Benefit: app fails to start (loud) instead of logging being silently broken (quiet).
## TODO-10: Writer exception isolation
## LOG-T-10: Writer exception isolation
**Priority:** P2 · **Type:** Resilience
Currently `AcLoggerBase.Info(...)` and friends do `LogWriters.ForEach(x => x.Info(...))`. A throwing writer (e.g. `SignaRClientLogItemWriter` during network blip, `AcDbLogItemWriter` during a DB outage) takes down the fan-out → subsequent writers in the list never see the message.
@ -83,3 +83,41 @@ Currently `AcLoggerBase.Info(...)` and friends do `LogWriters.ForEach(x => x.Inf
Wrap each writer invocation in `try { ... } catch (Exception ex) { Console.Error.WriteLine($"Writer {x.GetType().Name} failed: {ex.Message}"); }`. Optional: escalate to a circuit-breaker pattern (N failures → disable that writer for M seconds) to avoid Console.Error flood on persistent outages.
Must NOT log to `AcLoggerBase` itself inside the catch (reentrancy / infinite loop risk).
## LOG-T-11: Migrate server-side NopCommerce plugin logger setup to DI-factory
**Priority:** P2 · **Type:** Consumer refactor · **Related:** `LOGGING_ISSUES.md#log-i-8`, `../../../AyCode.Services/docs/SIGNALR/SIGNALR_TODO.md#log-t-5`
Align the server-side plugin (`Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs`) with the client-side setup pattern already used by MAUI / Web / Web.Client consumers.
### Target diff
```csharp
// In PluginNopStartup.ConfigureServices:
// 1. Bind Logger options from appsettings.json (replaces AcEnv.AppConfiguration path)
services.Configure<AcLoggerOptions>(configuration.GetSection("AyCode:Logger"));
// 2. Register writers (already present)
services.AddScoped<IAcLogWriterBase, ConsoleLogWriter>();
services.AddScoped<IAcLogWriterBase, NopLogWriter>();
// 3. Register the logger factory — replaces direct `new Logger(...)` calls
services.AddAcLoggerFactory<Logger>();
// 4. In the SignalR registration — drop the manual Logger construction
.AddAcBinaryProtocol(opts =>
{
opts.ProtocolMode = BinaryProtocolMode.AsyncSegment;
// Remove: opts.Logger = new Logger(nameof(AyCodeBinaryHubProtocol));
// — AcSignalRProtocolExtensions.BuildProtocol auto-resolves ILogger<AcBinaryHubProtocol> from DI
});
```
### Consequences / checklist
- [ ] Consumers of `new Logger(...)` elsewhere in the plugin code must switch to injecting `Func<string, Logger>` (DI factory).
- [ ] LOG-I-1 Console.Error noise disappears once the config-reading path is no longer invoked.
- [ ] Legacy `AyCode:Logger:LogWriters[]` array in `appsettings.json` becomes unused — decide: **(a)** remove (breaking if any other consumer still reads it), **(b)** keep for back-compat with a comment "superseded by DI, see LOGGING.md".
- [ ] Update `Nop.Plugin.Misc.AIPlugin/docs/SIGNALR/README.md:22` — current text still describes `services.AddSingleton<IHubProtocol>(new AcBinaryHubProtocol())` which has NOT been the actual registration for months.
- [ ] Verify server startup log — `AcLoggerOptions` must bind cleanly (no "missing section" warnings from `IOptionsMonitor`).
### Why this is a separate TODO from LOG-T-3
LOG-T-3 is about the **framework-level decision** (keep both patterns vs deprecate legacy). LOG-T-11 is the **consumer-level execution** of that decision for the server plugin — independent of whether LOG-T-3 ultimately deprecates the legacy path or keeps it alive. Migrating the server plugin removes LOG-I-1 noise regardless of LOG-T-3's outcome.

View File

@ -2,8 +2,8 @@
Custom logging framework with multi-writer fan-out and `Microsoft.Extensions.Logging` integration. Source: `Loggers/` in this project.
> For server-side GlobalLogger see `AyCode.Core.Server/docs/LOGGING_SERVER.md`.
> For remote writers (HTTP, browser console, SignalR) see `AyCode.Services/docs/LOGGING_REMOTE.md`.
> For server-side GlobalLogger see `AyCode.Core.Server/docs/LOGGING/README.md`.
> For remote writers (HTTP, browser console, SignalR) see `AyCode.Services/docs/LOGGING/README.md`.
## Design Overview
@ -120,7 +120,7 @@ Each writer's constructor signature must accept `(AppType, LogLevel, string?)`.
## DI-Based Factory Pattern
Modern, framework-first alternative to the config-reading `AcLoggerBase(string)` ctor. No runtime reflection over writer config; concrete writer types resolved from DI. Recommended for all modern projects (MAUI, WASM, ASP.NET Core) — the config-reading path is filesystem-bound and unsuitable for MAUI/WASM (see `LOGGING_ISSUES.md#issue-02`).
Modern, framework-first alternative to the config-reading `AcLoggerBase(string)` ctor. No runtime reflection over writer config; concrete writer types resolved from DI. Recommended for all modern projects (MAUI, WASM, ASP.NET Core) — the config-reading path is filesystem-bound and unsuitable for MAUI/WASM (see `LOGGING_ISSUES.md#log-i-2`).
### Consumer setup in Program.cs
@ -186,7 +186,7 @@ Use the two-arg overload when the consumer separates client-only and server-only
### Companion extension: AddAcDefaults (SignalR)
For SignalR client setup that wires the same `AcLoggerBase` instance into Microsoft.Extensions.Logging, use `AcSignalRConnectionExtensions.AddAcDefaults(builder, logger, connectionOptions)` — it bundles `AddAcConnection` + `ConfigureLogging(AddAcLogger)` in a single call. See `AyCode.Services/docs/SIGNALR.md`.
For SignalR client setup that wires the same `AcLoggerBase` instance into Microsoft.Extensions.Logging, use `AcSignalRConnectionExtensions.AddAcDefaults(builder, logger, connectionOptions)` — it bundles `AddAcConnection` + `ConfigureLogging(AddAcLogger)` in a single call. See `AyCode.Services/docs/SIGNALR/README.md`.
### Comparison: config-reading ctor vs DI-based factory
@ -199,7 +199,7 @@ For SignalR client setup that wires the same `AcLoggerBase` instance into Micros
| Typical use | Legacy / server-side config-driven | Modern DI: MAUI, WASM, ASP.NET Core |
| MAUI/WASM-safe | ❌ (filesystem-bound `AcEnv.AppConfiguration`) | ✅ |
Both patterns can coexist — consumer picks per scenario. Related issues: `LOGGING_ISSUES.md`. Planned unification: `LOGGING_TODO.md#todo-03`.
Both patterns can coexist — consumer picks per scenario. Related issues: `LOGGING_ISSUES.md`. Planned unification: `LOGGING_TODO.md#log-t-3`.
## Core Components

View File

@ -0,0 +1,14 @@
# AyCode.Core documentation
Topic documentation for the `AyCode.Core` project (Layer 0 framework).
## Topics
- [`BINARY/`](BINARY/README.md) — AcBinary serializer: features, format, writers, source generator
- [`LOGGING/`](LOGGING/README.md) — Logger system: levels, writers, config-reading vs DI factory
- [`TOON/`](TOON/README.md) — Toon serializer: LLM-optimized format with @meta/@types/@data sections
- [`XCUT/`](XCUT/README.md) — Cross-cutting issues/TODOs that span ≥2 topics (canonical home; referenced from each affected topic)
## Navigation
Per the AI Agent Core Protocol (folder navigation rule), start from this README when browsing `docs/`. Each topic folder has its own `README.md` with the main content, plus optional `ISSUES.md` (known issues) and `TODO.md` (planned work).

View File

@ -0,0 +1,118 @@
# TOON — Token-Oriented Object Notation
LLM-optimized serializer. Primary goal: **maximize LLM accuracy** via explicit schema/data separation, rich metadata, and unambiguous structure boundaries. Serialize-only (no deserializer — see `TOON_TODO.md#toon-t-6`).
Source: `Serializers/Toons/` in this project.
## Design goals
1. **Zero ambiguity** — explicit `{}` / `[]` / `"""` boundaries; no indentation-only scoping.
2. **Rich semantic context** — every property carries description, purpose, constraints, examples in the schema section.
3. **Token efficiency** — schema sent once (`@meta`/`@types`), data streamed thereafter (`@data`); enum values numeric rather than by name.
4. **Smart defaults** — useful output without any attributes; `ToonDescriptionAttribute` for precision when conventions fall short.
## Three-section output
| Section | Contents |
|---|---|
| `@meta` | Format version, source language, type list, optional domain context. |
| `@types` | Per-type schema — description, purpose, constraints, examples, navigation metadata, enum value maps. |
| `@data` | Object instance values; nested with inline references for shared/circular objects. |
## Three modes (`ToonSerializationMode`)
| Mode | When to use |
|---|---|
| `Full` (default) | First serialization — LLM needs the full schema + data context. |
| `MetaOnly` | Send schema once at conversation start; subsequent sends can skip it. |
| `DataOnly` | Subsequent sends when the LLM already has the schema — 30-50% token savings. |
Details, preset breakdown, all options: `TOON_OPTIONS.md`.
## Quick start
```csharp
using AyCode.Core.Serializers.Toons;
var person = new Person { Id = 1, Name = "Alice", Email = "alice@example.com" };
string toon = AcToonSerializer.Serialize(person);
```
Output (Full mode, abbreviated):
```toon
@meta {
version = "1.0"
format = "toon"
source-code-language = "C#"
types = ["Person"]
}
@types {
Person: "Object of type Person"
Id: int32
description: "Unique identifier for Person"
purpose: "Primary key / unique identification"
constraints: "required"
Name: string
description: "Name of the Person"
constraints: "required"
Email: string
description: "Email address"
constraints: "required, email-format"
}
@data {
Person {
Id = 1
Name = "Alice"
Email = "alice@example.com"
}
}
```
## Multi-turn pattern
```csharp
// Turn 1 — send schema only
string schema = AcToonSerializer.Serialize(person, AcToonSerializerOptions.MetaOnly);
// Turn 2..N — send data only (LLM already has the schema from Turn 1)
string data = AcToonSerializer.Serialize(person, AcToonSerializerOptions.DataOnly);
```
## Public API entry points
| Method | Purpose |
|---|---|
| `AcToonSerializer.Serialize<T>(T value)` | Default options (Full mode). |
| `AcToonSerializer.Serialize<T>(T value, AcToonSerializerOptions)` | Custom options. |
| `AcToonSerializer.Serialize<T>(T value, string domainDescription, AcToonSerializerOptions)` | Adds `context = "..."` to `@meta`. |
| `AcToonSerializer.SerializeTypeMetadata<T>()` / `SerializeTypeMetadata(Type)` | Schema only (MetaOnly preset internally). |
| `AcToonSerializer.SerializeMetadata(IEnumerable<Type>)` / `SerializeMetadata(params Type[])` | Schema for multiple types without an instance. |
## Choosing between Toon, AcBinary, AcJson
| Use case | Choose |
|---|---|
| Wire format for LLM context / prompts | **Toon** |
| High-throughput wire format for non-LLM transport | `AcBinary` — see `../BINARY/README.md` |
| Interop with external JSON consumers | `AcJson` |
Full serializer comparison: see `../../Serializers/README.md`.
## Files in this folder
- `README.md` — this file (overview + navigation).
- `TOON_FORMAT.md` — wire format specification (exact grammar).
- `TOON_OPTIONS.md``AcToonSerializerOptions` reference, presets, API overloads.
- `TOON_INFERENCE.md` — smart description/constraint auto-generation from property names.
- `TOON_ATTRIBUTES.md``ToonDescriptionAttribute` API + placeholder system.
- `TOON_IMPLEMENTATION.md` — internals, partial class breakdown, context pattern.
- `TOON_ISSUES.md` — known limitations.
- `TOON_TODO.md` — planned work.
## Cross-references
- Shared serializer infrastructure (metadata cache, reference tracking, `IdentityMap`): `../../Serializers/README.md`.
- Folder-README-first navigation rule: `../../../.github/copilot-instructions.md` rule #21.

View File

@ -0,0 +1,194 @@
# Toon — `ToonDescriptionAttribute` + Placeholder System
Manual metadata override for types and properties. Definition: `Serializers/Toons/ToonDescriptionAttribute.cs`.
Use when smart inference (`TOON_INFERENCE.md`) is insufficient — domain-specific constraints, business rules, or navigation metadata.
## Attribute signature
```csharp
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class,
AllowMultiple = false, Inherited = true)]
public sealed class ToonDescriptionAttribute : Attribute
```
## Text properties
| Property | Emitted in `@types` as | Fallback if empty |
|---|---|---|
| `Description` | `description: "..."` | MS `[Description]` → smart inference (property) / smart inference (class) |
| `Purpose` | `purpose: "..."` | Smart inference (property only; classes get empty) |
| `Constraints` | `constraints: "..."` | MS DataAnnotations → type-derived → smart inference |
| `Examples` | `examples: "..."` | empty (no automatic fallback) |
| `BusinessRule` | `business-logic: "..."` | empty |
## Structural properties — class level
| Property | Emitted as | Notes |
|---|---|---|
| `TableName` | `table-name: "..."` | Fallback: EF `[Table]` / Linq2Db `[Table]` → class name. |
| `TypeRelation` | `related-type: "{relation} {types}"` | Use the `ToonTypeRelation` string constants (below). |
| `RelatedTypes` | (combined with `TypeRelation`) | `Type[]` — related type names joined by `, `. |
## Structural properties — property level (navigation)
| Property | Emitted as | Notes |
|---|---|---|
| `IsPrimaryKey` | `primary-key: true` | Convention: a property named `Id` is auto-detected without this. |
| `ForeignKey` | `foreign-key: "..."` | Convention: `CompanyId``Company` pair auto-detected. |
| `Navigation` | `navigation: "many-to-one"` | `ToonRelationType` enum; emitted kebab-case. |
| `InverseProperty` | `inverse-property: "..."` | For bidirectional relationships. |
## Constructors
```csharp
[ToonDescription] // Named-args only
[ToonDescription("human-readable description")] // Positional Description
```
## `ToonTypeRelation` string constants
Use with the `TypeRelation` property. Values serialize literally:
| Constant | String value | Example |
|---|---|---|
| `None` | `""` | — |
| `BaseOf` | `"base-of"` | `Order` is base-of `OrderDto` |
| `Entity` | `"entity"` | — |
| `DerivedFrom` | `"derived-from"` | `OrderDto` is derived-from `Order` |
| `DtoOf` | `"dto-of"` | `OrderDto` is dto-of `Order` |
| `ModelOf` | `"model-of"` | `OrderModel` is model-of `Order` |
| `ViewModelOf` | `"view-model-of"` | `OrderViewModel` is view-model-of `Order` |
| `ProjectionOf` | `"projection-of"` | `OrderSummary` is projection-of `Order` |
## `ToonRelationType` enum
For the `Navigation` property:
```csharp
public enum ToonRelationType { ManyToOne, OneToMany, OneToOne, ManyToMany }
```
Emitted kebab-case: `many-to-one`, `one-to-many`, `one-to-one`, `many-to-many`.
## Placeholder system
Placeholders of the form `[#Name]` inside `Description` / `Purpose` / `Constraints` / `Examples` resolve at serialization time against Microsoft DataAnnotations or smart inference. Implementation: `Serializers/Toons/AcToonSerializer.Placeholders.cs`.
### Available placeholders
**For `Description`, `Purpose`:**
| Placeholder | Resolves to |
|---|---|
| `[#Description]` | MS `[System.ComponentModel.Description]` value. |
| `[#DisplayName]` | MS `[DisplayName]` value. |
| `[#SmartDescription]` | Auto-inferred description (see `TOON_INFERENCE.md`). |
| `[#SmartPurpose]` | Auto-inferred purpose (properties only; empty for classes). |
**For `Constraints`:**
| Placeholder | Resolves to |
|---|---|
| `[#Required]` | `required` (when `[Required]` is present). |
| `[#Range]` | `range: min-max` (from `[Range(min, max)]`). |
| `[#MaxLength]` | `max-length: N`. |
| `[#MinLength]` | `min-length: N`. |
| `[#StringLength]` | `length: min-max` (from `[StringLength]`). |
| `[#EmailAddress]` | `email-format`. |
| `[#Phone]` | `phone-format`. |
| `[#Url]` | `url-format`. |
| `[#CreditCard]` | `credit-card-format`. |
| `[#RegularExpression]` | `pattern: ...`. |
| `[#SmartTypeConstraints]` | Type-derived constraints (nullable, numeric). |
| `[#SmartInferenceConstraints]` | Name-based inferred constraints. |
**For `Examples`:**
| Placeholder | Resolves to |
|---|---|
| `[#SmartGeneratedExample]` | Auto-generated example value. |
### Merge vs replace modes
The presence of a placeholder in `Constraints` flips the merge behavior:
- **Replace mode**`Constraints` contains **no** `[#...]` → MS attributes ignored, smart inference ignored, the literal string is used as-is.
- **Merge mode**`Constraints` contains **any** `[#...]` → placeholder resolved, then merged with type-derived + smart-inferred constraints.
```csharp
[Range(0, 150)]
[ToonDescription(Constraints = "custom-validation-only")] // REPLACE — Range ignored
public int Score { get; set; }
[Range(0, 150)]
[ToonDescription(Constraints = "[#Range], verified")] // MERGE — "range: 0-150, verified"
public int Age { get; set; }
```
`Description`, `Purpose`, `Examples` use straight substitution (no merge mode).
## Usage modes
### Full custom
```csharp
[ToonDescription("User email address",
Purpose = "Authentication and notifications",
Constraints = "required, email-format, unique",
Examples = "user@example.com")]
public string Email { get; set; }
```
### Merge with DataAnnotations
```csharp
[Description("Contact email")]
[Required]
[EmailAddress]
[ToonDescription(Constraints = "[#Required], [#EmailAddress], unique")]
public string Email { get; set; }
// description: "Contact email"
// constraints: "required, email-format, unique"
```
### Partial (some properties only, rest auto)
```csharp
[Description("Contact email")]
[EmailAddress]
[ToonDescription(Constraints = "[#EmailAddress], unique")]
public string Email { get; set; }
// description: "Contact email" (from [Description])
// purpose: (inferred, empty)
// constraints: "required, email-format, unique"
// examples: (empty)
```
### Full automatic (no `[ToonDescription]`)
Relies on MS DataAnnotations + smart inference. See `TOON_INFERENCE.md`.
### Class-level with inheritance
```csharp
[ToonDescription("Base user entity")]
public class User { }
[ToonDescription("Admin user account")] // own description
public class AdminUser : User { }
public class SuperUser : AdminUser { } // inherits AdminUser's description (Inherited = true)
```
## When to use which approach
| Scenario | Approach |
|---|---|
| Generic DTO with conventional property names | Nothing — smart inference covers it. |
| Existing MS DataAnnotations | Smart inference — auto-pulls from annotations. |
| Domain-specific constraint not covered by conventions | `[ToonDescription(Constraints = "...")]` replace mode. |
| Domain rule plus standard validation | `[ToonDescription(Constraints = "[#Required], [#Range], domain-rule")]` merge mode. |
| Navigation property relationship | `[ToonDescription(Navigation = ..., InverseProperty = ..., ForeignKey = ...)]`. |
| Entity metadata (tables, DTO-of mappings) | Class-level `[ToonDescription(TableName = ..., TypeRelation = ..., RelatedTypes = ...)]`. |
| Cross-property business logic | `[ToonDescription(BusinessRule = "this >= NetWeight")]` — descriptive only, not executed. |

View File

@ -0,0 +1,200 @@
# Toon — Wire Format Specification
Exact grammar of Toon output. Needed by LLMs parsing Toon back to objects, and for verifying generated output against expectations.
Format version: `"1.0"` (constant `AcToonSerializer.FormatVersion`, emitted as `version` in `@meta`).
## Top-level structure
Output contains up to three sections in this order:
```
@meta { ... }
@types { ... }
@data { ... }
```
Section presence per `ToonSerializationMode`:
| Mode | `@meta` | `@types` | `@data` |
|---|---|---|---|
| `Full` | ✓ | ✓ | ✓ |
| `MetaOnly` | ✓ | ✓ | — |
| `DataOnly` | — | — | ✓ |
In `Full` mode with `UseIndentation = true`, a blank line separates `@meta/@types` from `@data`.
## `@meta` section
```
@meta {
version = "1.0"
format = "toon"
source-code-language = "C#"
context = "optional domain description"
types = ["Type1", "Type2", "Type3"]
}
```
- `version` — Toon format version.
- `format` — always `"toon"`.
- `source-code-language` — always `"C#"`.
- `context` — emitted only when the `domainDescription` overload was invoked.
- `types` — type short names in **topological order** (dependencies before dependents). Excludes primitives, strings, framework collection generics (`List<>`, `Dictionary<>`, `IEnumerable<>`, `ICollection<>`), and generic type definitions.
The `@types` section body (when emitted) follows the same topological order.
## `@types` section
```
@types {
TypeName: "description"
[table-name: "..."]
[related-type: "dto-of OtherType"]
[purpose: "..."]
PropertyName: typeHint
[description: "..."]
[purpose: "..."]
[business-logic: "..."]
[constraints: "..."]
[examples: "..."]
[primary-key: true]
[foreign-key: "..."]
[other-key: "..."]
[navigation: "many-to-one"]
[inverse-property: "..."]
}
```
- Property metadata indented one level below its property header.
- Optional lines emitted only when the value is non-empty.
- `navigation` values are kebab-case of `ToonRelationType`: `many-to-one`, `one-to-many`, `one-to-one`, `many-to-many`.
- `typeHint` uses **C# short names** — primitives: `int`, `long`, `short`, `byte`, `sbyte`, `uint`, `ulong`, `ushort`, `double`, `float`, `decimal`, `bool`, `string`, `char`; special types: `DateTime`, `DateTimeOffset`, `TimeSpan`, `Guid`; collections: `Person[]`, `List<Person>`, `Dictionary<string, int>`; class / enum names as-is. Nullable suffix: `int?`, `DateTime?`, `TaxDisplayType?`.
- ⚠️ **Two naming conventions coexist.** The `@types` section uses C# short names (above). The `@data` inline type hints (when `UseInlineTypeHints = true`) use full-width tokens like `int32`, `float64`, `datetime` — see the Inline type hints subsection below.
### Enum types in `@types`
```
EnumName: enum
description: "..."
purpose: "..."
underlying-type: "int"
default-value: 0
values:
MemberName = numericValue
description: "..."
OtherMember = numericValue
```
- Members emitted as `Name = numericValue`; per-member description is an indented sub-property.
- When `UseEnhancedMetadata = false`, only the `MemberName = numericValue` list is emitted (no description, purpose, underlying-type, default-value).
## `@data` section
### Object syntax
```
TypeName {
PropertyName = value
OtherProperty = value
}
```
- `TypeName` prefix emitted when `WriteTypeNames = true` (default).
- Properties written one per line; `=` has surrounding spaces only when `UseIndentation = true`, else tight (`prop=value`).
- Properties whose value equals `default(T)` / `null` / `0` / `false` skipped when `OmitDefaultValues = true`.
### Primitive values
| Kind | Representation |
|---|---|
| `string` | `"escaped"` inline, or triple-quote for long strings. |
| `int32`/`int64`/`int16`/`byte`/`sbyte`/`uint16`/`uint32`/`uint64` | Plain number, `CultureInfo.InvariantCulture`. |
| `bool` | `true` / `false`. |
| `double` | `G17` format; `NaN`/`±Infinity` → `null`. |
| `float` | `G9` format; `NaN`/`±Infinity` → `null`. |
| `decimal` | Invariant culture. |
| `DateTime` | ISO 8601 in quotes: `"2026-04-24T10:30:00.0000000"` (`"O"` format). |
| `DateTimeOffset` | ISO 8601 with offset in quotes. |
| `TimeSpan` | `"c"` format in quotes. |
| `Guid` | `"D"` format (36-char dashed) in quotes. |
| `char` | single-char string in quotes. |
| `enum` | **Numeric** value of the underlying type (not the member name). `@types` holds the name→value map. |
| `null` | literal `null` (no quotes). |
### String escaping
JSON-like escapes: `\"`, `\\`, `\n`, `\r`, `\t`, `\uXXXX` for chars `< 32`. If no escape applies, raw double-quoted.
### Multi-line strings (triple-quote)
Automatic when `UseMultiLineStrings = true` AND `string.Length > MultiLineStringThreshold` (default 80):
```
Bio = """
Line 1
Line 2
Line 4
"""
```
- Each content line indented one level deeper than the owning property.
- Closing `"""` at the property's own indent level.
- Line terminators inside the string normalized to platform newline during split.
### Collection syntax
```
PropertyName = <ElementType[]> (count: N) [
item1
item2
]
```
- `<ElementType[]> (count: N) ` header emitted only when `ShowCollectionCount = true` (default).
- One item per line.
- Items serialize by runtime type (polymorphic collections supported).
### Dictionary syntax
```
PropertyName = <Dictionary<KeyType, ValueType>> (count: N) {
key => value
otherKey => otherValue
}
```
- Header shows the specific generic type when derivable (via `IsDictionaryType`), else `<dict>`.
- Key/value separator is literal ` => ` regardless of indentation setting.
### Reference syntax
When `ReferenceHandling != None` and an object is referenced more than once:
- **First occurrence:** `@N TypeName { ... }``N` is the reference ID.
- **Subsequent occurrences:** `@ref:N`.
Circular references safe — the second visit emits `@ref:N` instead of recursing. When the reference system is disabled (`ReferenceHandling = None`), shared objects serialize redundantly and true cycles cause depth-truncation to `null`.
### Inline type hints
When `UseInlineTypeHints = true`:
```
Age = 30 <int32>
Name = "Alice" <string>
```
Hint tokens (full-width naming — **distinct** from the C# short names used in `@types`): `string`, `int32`, `int64`, `int16`, `byte`, `sbyte`, `uint16`, `uint32`, `uint64`, `bool`, `float32`, `float64`, `decimal`, `char`, `datetime`, `datetimeoffset`, `timespan`, `guid`, `enum`. Disabled by default (the same information lives in `@types`).
## Compact mode quirks
When `UseIndentation = false`:
- No leading indent whitespace.
- Property assignments use `=` with no surrounding spaces.
- Enum member list uses `=` with no surrounding spaces.
- No blank line between `@meta` and `@data`.
- `@types` section drops the trailing blank line between type definitions.
- Closing braces/brackets remain on their own lines (structure is still line-based).

View File

@ -0,0 +1,127 @@
# Toon — Implementation Architecture
Internal design of `AcToonSerializer`. Read this when modifying the serializer. End users should stay in `TOON_OPTIONS.md` and `TOON_ATTRIBUTES.md`.
## Partial class breakdown
`AcToonSerializer` is a `static partial class` split across topical files for cohesion. Each file contributes methods to the same class — no separate types, no inheritance between partials.
| File | Responsibility |
|---|---|
| `AcToonSerializer.cs` | Public API (`Serialize`, `SerializeTypeMetadata`, `SerializeMetadata`), primitive fast-path, reference scanning, per-type metadata cache. |
| `AcToonSerializer.ToonSerializationContext.cs` | Per-serialization mutable state: buffer, indent level, reference maps. |
| `AcToonSerializer.ToonSerializeTypeMetadata.cs` | Per-type cached reflection data: properties, display names, navigation. |
| `AcToonSerializer.MetaWriter.cs` | `@meta` + `@types` orchestration, type collection, enum backing-field detection. |
| `AcToonSerializer.TypeDefinitions.cs` | Per-type schema emission — property metadata, navigation, enum definitions. |
| `AcToonSerializer.DataSection.cs` | `@data` emission, value dispatcher, object/collection/dict/primitive writers, multi-line strings. |
| `AcToonSerializer.Descriptions.cs` | Smart-inference pattern catalog, fallback chain resolvers. |
| `AcToonSerializer.Attributes.cs` | Attribute processing entry points. |
| `AcToonSerializer.AttributeExtraction.cs` | DataAnnotations → constraint string extraction. |
| `AcToonSerializer.Placeholders.cs` | `[#Name]` placeholder resolution. |
| `AcToonSerializer.TopologicalSort.cs` | Dependency-first type ordering. |
| `AcToonSerializer.Navigation.cs` | Navigation property metadata (conventions + attributes). |
| `AcToonSerializer.ForeignKeys.cs` | FK property detection, inverse property matching. |
| `AcToonSerializer.Validation.cs` | Output validation / sanity checks. |
Supporting non-partial files:
| File | Role |
|---|---|
| `AcToonSerializerOptions.cs` | Options POCO + preset factories + mode enum. |
| `AcToonContextBase.cs` | Shared base between serialization contexts. |
| `ToonDescriptionAttribute.cs` | Attribute definition + `ToonRelationType` enum. |
| `ToonTypeRelation.cs` | Type relation string constants. |
## Context pattern
```
AcSerializerContextBase<TMetadata, TOptions> (Serializers/ root)
└─ AcToonContextBase (Toons/ shared base)
└─ ToonSerializationContext (sealed, per-call)
```
`ToonSerializationContext` responsibilities:
- Owns the output `StringBuilder` and current indent level.
- Tracks reference IDs via typed `IdentityMap` instances (small-int bitmap + chained hash for larger keys).
- Pooled via `ToonSerializationContextPool.Get(options)` / `Return(context)` — reused across serializations after `ResetTracking()`.
`ToonSerializeTypeMetadata` (per-type, cached globally in `AcToonSerializer.MetadataCache`):
- Property accessors (`ToonPropertyAccessor`), display names, navigation metadata.
- `CustomDescription` — the `ToonDescriptionAttribute` instance if present.
- Flags: `IsCollection`, `IsDictionary`, `ElementType`.
Shared infrastructure (metadata cache semantics, `IdentityMap`, `ReferenceTracker`): see `../../Serializers/README.md`. Not duplicated here.
## Serialization flow
```
Serialize(value, options)
├─ null check → "null"
├─ TrySerializePrimitiveDirect (fast-path, no context allocated)
├─ Get context from pool
├─ ScanReferences (if ReferenceHandling != None) ← marks multi-referenced objects
├─ switch Mode
│ ├─ MetaOnly → WriteMetaSectionOnly
│ ├─ DataOnly → WriteDataSectionOnly
│ └─ Full → WriteMetaSection + blank line + WriteDataSection
└─ Return context to pool
```
## Type collection (`CollectTypes`)
Recursive traversal starting from the root type:
- Enums: added, not further traversed.
- Primitives and strings: skipped.
- Already-visited types: skipped (`HashSet` dedup).
- Dictionaries: traverse key type + value type.
- Collections (non-string `IEnumerable`): traverse element type.
- Objects: traverse each declared property's type.
Additional pass: `DetectAndCollectEnumBackingFields` scans for the `Status` + `StatusId` pattern (see `TOON_INFERENCE.md#enum-backing-field-detection`).
After collection, `TopologicalSortTypes` orders the set so dependencies come before dependents. Filter applied before sorting:
- Drops primitives, strings.
- Drops `IEnumerable`-implementing non-string types (framework collection machinery).
- Drops generic type definitions.
- Drops well-known collection prefixes (`List`, `ICollection`, `IEnumerable`, `Dictionary`).
The sorted order is used both for the `@meta.types` list and for the `@types` body emission.
## Reference scanning
Pre-pass when `ReferenceHandling != None`. Walks every reachable object exactly once via `TrackForScanning`. An object visited a second time is flagged as "multi-referenced" and assigned an ID.
During `@data` emission:
- First visit of a multi-referenced object → `@N TypeName { ... }`.
- Subsequent visits → `@ref:N`.
Circular cycles are safe — the second visit aborts with the `@ref:` form. When reference handling is disabled, shared subgraphs serialize redundantly and true cycles truncate at `MaxDepth` to `null`.
## Enum output
Enum values serialize as their **numeric underlying-type value** (not the member name) for token efficiency. The name ↔ value map lives in the `@types` `values:` sub-list; LLMs must cross-reference.
When `UseEnhancedMetadata = false`, only the bare name/value list is emitted — no description, purpose, underlying-type, or default-value fields.
## Buffer management
The context's `StringBuilder` is shared with write helpers:
- `Write(string)` / `Write(char)` — append, no newline.
- `WriteLine(string)` / `WriteLine()` — append with `\r\n`.
- `WriteIndent()` — emit `CurrentIndentLevel × IndentString`.
- `WriteIndentedLine(string)` — indent + content + newline.
- `WriteProperty(name, value)``@meta` helper for `name = value` lines (honors `UseIndentation` for `=` spacing).
`GetResult()` returns the final string and resets the context for pool reuse.
## Key design decisions
- **`static partial class`** — no instance state; per-call state lives only in `ToonSerializationContext`.
- **Sealed context classes** — enables JIT direct calling on hot paths (no virtual dispatch).
- **Context pooling** — repeat serializations allocate no context.
- **Cached per-type metadata** — reflection runs once per type, then hot cache.
- **Parallel code paths for `UseIndentation`** — compact mode avoids space emission on `=` and skips blank lines at write time (not a post-process strip).
- **Fast-path for primitive values**`Serialize(value)` with a primitive returns without pool access or reflection.
- **No deserialization** — Toon is intentionally one-way. See `TOON_TODO.md#toon-t-6`.

View File

@ -0,0 +1,109 @@
# Toon — Smart Inference
When no `ToonDescriptionAttribute` is present, Toon auto-generates description, purpose, and constraints by inspecting property names and types. Implemented in `Serializers/Toons/AcToonSerializer.Descriptions.cs`.
Inference **fills the `@types` section**. It has no effect on `@data` values — those serialize by runtime reflection regardless.
## Fallback chains
Per-field, the first non-empty value wins:
| Field | Priority order |
|---|---|
| `description` | `ToonDescription.Description` (with placeholders resolved) → `System.ComponentModel.[Description]` → inferred-from-name → `null` (line skipped). |
| `purpose` | `ToonDescription.Purpose` (with placeholders) → inferred-from-name → empty (line skipped). |
| `constraints` | `ToonDescription.Constraints` (merge if placeholder, else replace) → MS DataAnnotations (`[Required]`, `[Range]`, `[EmailAddress]`, etc.) → type-derived small-int ranges → name-based inference → post-process append of `readonly` / `not-mapped` / `enum-type`. **Redundancy-conscious**: no auto `nullable` / `required` — the type hint already communicates this. |
| `examples` | `ToonDescription.Examples` (placeholders resolved) → empty (no auto-fallback). |
Attribute precedence details: `TOON_ATTRIBUTES.md`.
## Name-based description patterns
| Property name pattern | Description emitted |
|---|---|
| `Id` (exact, case-insensitive) | `Unique identifier for {TypeName}` |
| `Name` (exact) | `Name of the {TypeName}` |
| contains `Email` | `Email address` |
| contains `Phone` | `Phone number` |
| contains `Address` | `Physical or mailing address` |
| contains `Date` OR type is `DateTime`/`DateTime?` | `Date/time value for {PropertyName}` |
| starts with `Is` + `bool` | `Boolean flag indicating {rest after "Is"}` |
| starts with `Has` + `bool` | `Boolean flag indicating possession of {rest after "Has"}` |
| ends with `Count` + integer type | `Count of {rest before "Count"}` |
| collection (element type known, not `object`) | `Collection of {ElementType} for {TypeName}` |
| dictionary | `Dictionary mapping for {PropertyName} in {TypeName}` |
| fallback | `Property {name} of type {baseType}[ (nullable)]` |
Matching is case-insensitive where noted and uses `string.Contains` or `StartsWith` as indicated.
## Name-based purpose patterns
| Property name pattern | Purpose emitted |
|---|---|
| `Id` (exact) | `Primary key / unique identification` |
| contains `CreatedAt` | `Timestamp when entity was created` |
| contains `UpdatedAt` OR `ModifiedAt` | `Timestamp of last update` |
| contains `DeletedAt` | `Soft delete timestamp` |
| starts with `Is` | `Status flag` |
| contains `Version` | `Version tracking / concurrency control` |
| (fallback) | empty (line skipped) |
## Type-derived and name-based constraints
Constraints join with `, `. Inference is **redundancy-conscious** — Toon skips constraints already implied by the property's type hint (visible on the `PropertyName: typeHint` line) to save LLM tokens. Source of this design decision: inline code comments in `AcToonSerializer.AttributeExtraction.cs``ExtractTypeConstraints`.
### Not emitted automatically
- **`nullable` / `required`** — the type hint conveys this (`int?` vs `int`, `DateTime?` vs `DateTime`, reference type vs non-nullable value type). `required` IS emitted when the property carries an explicit `[Required]` DataAnnotation — that signals intent beyond what the type says.
- **Large-range numeric bounds** — no auto range on `int`, `long`, `ulong`, `float`, `double`, `decimal`. The type name already bounds the value; `range: -2147483648-2147483647` would be noise.
- **Enum property constraints** — none. The `@types` enum definition holds the name ↔ numeric value map.
### Emitted automatically — small-integer ranges
Useful extra info because LLMs can misjudge narrow integer type sizes:
| Type | Added constraint |
|---|---|
| `byte` | `range: 0-255` |
| `sbyte` | `range: -128-127` |
| `short` (`int16`) | `range: -32768-32767` |
| `ushort` (`uint16`) | `range: 0-65535` |
| `uint` (`uint32`) | `range: 0-4294967295` |
### Emitted automatically — name-based additions
| Condition | Appended constraint |
|---|---|
| `string` + name contains `Email` | `email-format` |
| `string` + name contains `Url` | `url-format` |
| integer type + name contains `Age` | `range: 0-150` |
| integer type + name ends with `Count` | `non-negative` |
| property has `[NotMappedAttribute]` or `[NotColumnAttribute]` | `not-mapped` |
| read-only property (no public setter) | `readonly` |
| enum backing field detected (see below) | `enum-type: {EnumName}` |
> ⚠️ **`Age` pattern false positives** — the rule uses `string.Contains("Age", OrdinalIgnoreCase)`, which is not word-boundary-aware. Hits `LanguageId`, `Package*`, `Image*`, `Page*`, `Message*`, `Storage*` and similar. See `TOON_ISSUES.md#toon-i-2` for detected cases and fix options. Lesser risk applies to the `Email`, `Phone`, `Address`, `Url`, `Date`, `Version`, `CreatedAt`, `UpdatedAt`, `DeletedAt` patterns too.
## Enum backing field detection
Special pass in `AcToonSerializer.MetaWriter.cs`.
Trigger conditions — all must hold on the same declaring type:
- An **enum property** (e.g. `Status`) marked with `[NotMapped]` / `[NotColumn]` / `[JsonIgnore]`.
- A **backing property** named `{EnumPropertyName}Id` (e.g. `StatusId`).
- The backing property's type matches the enum's underlying type (`int`/`int?`, `byte`/`byte?`, etc.).
Effects:
- The enum type is collected and documented in `@types`.
- The backing `StatusId` property's constraint list gains `enum-type: Status`.
This supports the common "store enum as int in DB, expose as enum on the model" pattern without requiring manual annotation.
## What Toon does NOT infer
- **Ranges beyond `Age`** — no `[Range]` equivalent inferred from `Percent`, `Rating`, `Score`, etc.
- **Length limits** — no `max-length` inferred from `Name`, `Description`.
- **Uniqueness** — never inferred without `[ToonDescription(Constraints = "unique")]`.
- **Business logic**`BusinessRule` requires explicit `[ToonDescription]`.
For anything beyond the catalogs above, use `ToonDescriptionAttribute` — see `TOON_ATTRIBUTES.md`.

View File

@ -0,0 +1,181 @@
# Toon — Known Issues
For planned/actionable work see `TOON_TODO.md`.
## TOON-I-1: `Compact` preset XML doc contradicts code behaviour
**Severity:** Trivial (doc-only, no runtime effect) · **Status:** Open (direction not decided) · **Area:** `Serializers/Toons/AcToonSerializerOptions.cs`
### Description
The XML doc on `AcToonSerializerOptions.Compact` claims *"Minimal output, no meta, no indentation"*, but the property initializer sets `UseIndentation = true`. Real output is DataOnly + no type names + no reference handling **with** indentation whitespace. Consumers reading IntelliSense expect a tighter output than they receive.
```csharp
/// <summary>
/// Compact mode: Minimal output, no meta, no indentation. ← doc says no indent
/// ...
/// </summary>
public static AcToonSerializerOptions Compact => new()
{
Mode = ToonSerializationMode.DataOnly,
UseMeta = false,
UseIndentation = true, ← code sets true
...
};
```
### Root cause
Likely the XML comment predates a later preset revision that flipped `UseIndentation` to `true`. The comment was not updated to match.
### Fix options (undecided)
- **(a) Fix the doc** — update the XML comment to match current behaviour. "Compact" then means "DataOnly, no type names, no refs" — not literal whitespace-free.
- **(b) Fix the code** — flip `UseIndentation` to `false` in the preset to honour original intent. Output-breaking change for anyone relying on current `Compact` formatting.
### Known workaround
Build a custom options instance when truly token-minimal output is needed:
```csharp
var trulyCompact = new AcToonSerializerOptions {
Mode = ToonSerializationMode.DataOnly,
UseMeta = false,
UseIndentation = false,
OmitDefaultValues = true,
WriteTypeNames = false,
ReferenceHandling = ReferenceHandlingMode.None
};
```
`TOON_OPTIONS.md` already notes this divergence next to the `Compact` preset; decision here governs whether it persists or gets resolved.
### Related TODO
None yet — fix direction not decided.
## TOON-I-2: `Age` inference substring false positive
**Severity:** Minor (constraint-only, no runtime effect) · **Status:** Open · **Area:** `Serializers/Toons/AcToonSerializer.Descriptions.cs``GetPropertyConstraints` / `GetInferredConstraints`
### Description
Integer properties whose name **contains** the substring `age` (case-insensitive, not word-bounded) erroneously receive the `range: 0-150` constraint from the `Age` inference rule. False positives observed: `LanguageId`, and by pattern `Package*`, `Image*`, `Page*`, `Message*`, `Storage*`, `Stage*`.
Observed in a 2026-04-24 Toon dump:
```
LanguageId: int?
constraints: "range: 0-150"
```
### Root cause
```csharp
if (propertyName.Contains("Age", StringComparison.OrdinalIgnoreCase))
constraints.Add("range: 0-150");
```
`"LanguageId".Contains("Age", OrdinalIgnoreCase)` returns `true` — the substring `age` sits at position 5-7 inside `Language`. The rule's intent was to match an `Age`-named property, but `Contains` has no word-boundary awareness.
### Fix options
- **(a)** Exact match: `propertyName.Equals("Age", OrdinalIgnoreCase)` — loses legitimate matches like `CustomerAge`, `MinAge`.
- **(b)** Suffix: `propertyName.EndsWith("Age", OrdinalIgnoreCase)` — still matches `Language` (ends in `age`).
- **(c)** Word-boundary regex — accurate but adds a regex pass per property.
- **(d)** `EndsWith("Age")` + preceded-by-uppercase guard — `CustomerAge` ok (`r`→`A` capital transition), `Language` rejected (`u`→`a` lowercase transition).
Similar risk applies to other substring-based rules in `Descriptions.cs` (`Email`, `Phone`, `Address`, `Url`, `Date`, `Version`, `CreatedAt`, `UpdatedAt`, `DeletedAt`) — audit them under the same word-boundary lens.
### Known workaround
Explicit `[ToonDescription(Constraints = "...")]` on the affected property (replace mode — no placeholder).
### Related TODO
None yet.
## TOON-I-3: Property override duplicated on inheritance in `@types`
**Severity:** Minor (schema correctness; LLM may misinterpret) · **Status:** Open · **Area:** Property enumeration — likely `AcToonSerializer.ToonSerializeTypeMetadata.cs` or the shared base in `Serializers/` root.
### Description
When a derived class uses `override` on a property inherited from its base, both the derived and the base version appear in the `@types` schema — two entries for the same logical member, each with its own `business-logic` string.
Observed in a 2026-04-24 Toon dump on `OrderItemPallet`:
```
OrderItemPallet:
...
MeasuringStatus: MeasuringStatus
business-logic: "get => IsAudited ? MeasuringStatus.Audited : base.MeasuringStatus"
constraints: "readonly, not-mapped"
...
MeasuringStatus: MeasuringStatus
business-logic: "get => IsMeasured ? MeasuringStatus.Finnished : Id > 0 ? ..."
constraints: "readonly, not-mapped"
```
Only affected when a class uses `override` on a property. Sibling non-overriding classes (`ShippingItemPallet`, `StockTakingItemPallet`) emit the property once as expected.
### Root cause (likely)
`Type.GetProperties(BindingFlags.Public | BindingFlags.Instance)` returns overridden properties twice — once per declaring type in the inheritance chain. The Toon property enumeration does not dedupe on `Name`, so both versions reach the writer.
### Fix options
- **(a)** Filter in the metadata build step: group by `Name`, keep the most-derived override (lowest inheritance distance from the target type).
- **(b)** Walk the inheritance chain with `BindingFlags.DeclaredOnly` per level, collecting names into a set; skip if already seen.
### Known workaround
None — the schema is wrong as-is. Downstream consumers must tolerate the duplicate entry.
### Related TODO
None yet.
## TOON-I-4: Property order in `@types` is not pure alphabetical
**Severity:** Minor (readability / predictability) · **Status:** Open · **Area:** Property ordering — likely `AcToonSerializer.ToonSerializeTypeMetadata.cs`, inherited from the shared base in `Serializers/` root.
### Description
Per-class property emission in `@types` is **hierarchy-aware derived→base**, with alphabetical order within each inheritance level. The inherited `Id` property therefore appears at the very end of every class definition, instead of its alphabetical slot between `H…` and `J…`.
Observed in a 2026-04-24 Toon dump on `Customer` (simplified):
```
Customer:
Active, AdminComment, ..., ZipPostalCode, ← own properties, alphabetical
Id ← inherited, placed LAST
```
And on `OrderItemPallet` (which has multi-level inheritance):
```
OrderItemPallet:
AverageWeight, IsAudited, MeasuringStatus, ..., TrayQuantity, ← derived-class props
Created, CreatorId, ForeignKey, ..., TrayQuantity, ← base class props
Id ← base-base entity
```
### Expected behavior
**Pure alphabetical order across all properties** of the type, regardless of inheritance level. For Toon's LLM-readability goal, a predictable flat order matters more than hierarchy preservation.
### Docs inconsistency
`Serializers/README.md` "Property ordering" section states: *"Hierarchy-aware (base→derived) then alphabetical. Ensures stable property indices across type versions."* Two mismatches with actual behavior:
1. **Direction reversed** — real output is `derived→base`, not `base→derived` as the doc claims.
2. **Rationale irrelevant to Toon** — "stable property indices across type versions" is a wire-format concern for AcBinary. Toon is a descriptive schema format where property order is cosmetic, not semantic.
### Fix options
- **(a)** Override the property ordering specifically in `ToonSerializeTypeMetadata` to plain `OrderBy(p => p.Name)` — Toon diverges from the Binary/JSON shared ordering. Recommended: AcBinary needs the hierarchy-aware order for wire stability; Toon does not, and can opt out.
- **(b)** Fix the direction in the shared base (`base→derived` as the docs claim) — partial fix, still not pure alphabetical.
- **(c)** Accept current behavior and update `Serializers/README.md` to match observed reality — reject the issue. Not recommended: the user expectation is pure alphabetical, and the docs-vs-code gap is real either way.
### Known workaround
None — consumers must tolerate the current order.
### Related TODO
None yet. Fix also requires reconciling `Serializers/README.md` with the chosen direction.
## Issue entry template
```
## ISSUE-NN: Short title
**Severity:** Trivial / Low / Minor / Major · **Status:** Open / Documented / Mitigated · **Area:** <subsystem>
### Description
What breaks, and under what conditions.
### Root cause
Why it happens (code location + design mismatch).
### Known workaround
Steps a consumer can take until fixed.
### Related TODO
`TOON_TODO.md#todo-NN` (if applicable).
```

View File

@ -0,0 +1,139 @@
# Toon — Options & Presets
`AcToonSerializerOptions` controls every Toon output behavior. Definition: `Serializers/Toons/AcToonSerializerOptions.cs`.
## Toon-specific options
| Option | Type | Default | Purpose |
|---|---|---|---|
| `Mode` | `ToonSerializationMode` | `Full` | Which sections to emit. |
| `UseMeta` | `bool` | `true` | Include `@meta`/`@types` sections. Ignored when `Mode = DataOnly`. |
| `UseIndentation` | `bool` | `true` | Pretty-print with indentation. When `false`, `=` has no surrounding spaces. |
| `IndentString` | `string` | `" "` (2 spaces) | Indent unit when `UseIndentation = true`. |
| `UseInlineTypeHints` | `bool` | `false` | Emit `value <type>` hints in `@data` (schema already has type info). |
| `UseInlineComments` | `bool` | `false` | Emit property descriptions as inline comments. |
| `ShowCollectionCount` | `bool` | `true` | Emit `<Type[]> (count: N) ` header on arrays/dictionaries. |
| `UseMultiLineStrings` | `bool` | `true` | Triple-quote format for long strings. |
| `MultiLineStringThreshold` | `int` | `80` | Character count above which a string becomes multi-line. |
| `UseEnhancedMetadata` | `bool` | `true` | Full per-property metadata (description + purpose + constraints + examples + navigation) vs compact single-line form. |
| `OmitDefaultValues` | `bool` | `true` | Skip properties whose value equals `default(T)` / `null` / `0` / `false`. |
| `WriteTypeNames` | `bool` | `true` | Emit the type name before `{` on `@data` objects. |
| `MaxExampleStringLength` | `int` | `50` | Truncate example values in `@types` beyond this length. |
## Inherited from `AcSerializerOptions`
| Option | Type | Notes |
|---|---|---|
| `MaxDepth` | `byte` | Recursion guard; when exceeded, `null` is emitted. |
| `ReferenceHandling` | `ReferenceHandlingMode` | `None` / `OnlyId` / `All` — required for circular or shared-reference graphs. |
| `SerializerType` | `AcSerializerType` | Fixed to `Toon`. |
## `ToonSerializationMode` enum
```csharp
public enum ToonSerializationMode : byte
{
Full = 0, // @meta + @types + @data (default)
MetaOnly = 1, // @meta + @types only
DataOnly = 2, // @data only
}
```
## Presets
Each preset is a fresh instance (static property with `new` expression — no shared state between retrievals).
### `Default`
```
Mode = Full, UseMeta = true, UseIndentation = true,
OmitDefaultValues = true, WriteTypeNames = true
```
First-time serialization with full context for the LLM.
### `MetaOnly`
```
Mode = MetaOnly, UseMeta = true, UseIndentation = true,
UseInlineComments = true
```
Send schema once at conversation start. `UseInlineComments = true` adds clarity since no `@data` follows.
### `DataOnly`
```
Mode = DataOnly, UseMeta = false, UseIndentation = true,
OmitDefaultValues = true, WriteTypeNames = true
```
Subsequent sends once the LLM already has the schema.
### `Compact`
```
Mode = DataOnly, UseMeta = false, UseIndentation = true,
OmitDefaultValues = true, WriteTypeNames = false,
ReferenceHandling = None
```
Data-only, no type names, no reference tracking. Note: `UseIndentation = true` here — truly token-minimal output requires manually setting `UseIndentation = false` on a custom options instance.
### `Verbose`
```
Mode = Full, UseMeta = true, UseIndentation = true,
UseInlineTypeHints = true, UseInlineComments = true,
OmitDefaultValues = false, WriteTypeNames = true
```
Debugging / documentation — every hint turned on, defaults included. Bloats tokens; avoid for production LLM contexts.
## Factory helpers
```csharp
AcToonSerializerOptions.WithMaxDepth(10)
AcToonSerializerOptions.WithoutReferenceHandling()
```
Short-hand for common one-off customizations.
## Custom options
All properties are `init;` — assemble once per serialization call:
```csharp
var opts = new AcToonSerializerOptions
{
Mode = ToonSerializationMode.Full,
UseIndentation = false, // tight token-minimal output
ShowCollectionCount = true,
UseMultiLineStrings = false, // keep long strings inline
OmitDefaultValues = true,
MaxDepth = 20
};
string toon = AcToonSerializer.Serialize(value, opts);
```
## Public API overloads
| Method | Purpose |
|---|---|
| `Serialize<T>(T value)` | Default options. |
| `Serialize<T>(T value, AcToonSerializerOptions)` | Custom options. |
| `Serialize<T>(T value, string domainDescription, AcToonSerializerOptions)` | Adds `context = "..."` line to `@meta`. |
| `SerializeTypeMetadata<T>()` / `SerializeTypeMetadata(Type)` | Schema only — uses `MetaOnly` internally. |
| `SerializeMetadata(IEnumerable<Type>)` | Schema for multiple types (no instance required). |
| `SerializeMetadata(params Type[])` | Schema for multiple types (params syntax). |
| `SerializeMetadata(string domainDescription, params Type[])` | Multi-type schema with domain context in `@meta`. |
## Picking a preset / customization
| Scenario | Choice |
|---|---|
| First prompt to an LLM in a new conversation | `Default` |
| Send schema once, many data round-trips | `MetaOnly` then `DataOnly` |
| Token-minimal streaming after schema is established | Custom: `Mode = DataOnly` + `UseIndentation = false` + `WriteTypeNames = false` |
| Debugging, local inspection | `Verbose` |
| Cyclic object graph (entity with backrefs) | Any preset, but ensure `ReferenceHandling = OnlyId` or `All` on a custom options instance |

View File

@ -0,0 +1,131 @@
# Toon — TODO
## Priority legend
- **P0** blocker · **P1** important · **P2** nice-to-have · **P3** idea
---
## TOON-T-1: Tabular array mode (CSV-style headered lists)
**Priority:** P2 · **Type:** Feature (opt-in) · **Origin:** 2026-04-24 LLM-accuracy proposal
Emit arrays of homogeneous flat objects with a single column header instead of repeating property names on every element:
```
OrderItems: OrderItem[] = [
[Id, Quantity, ProductName]
[120, 10, "Blueberries"]
[121, 5, "Oranges"]
]
```
Activation conditions (all must hold):
- Every element's runtime type equals the declared element type (no polymorphism).
- All element properties are primitives/strings (no nested objects).
- `OmitDefaultValues = false` (otherwise empty columns are ambiguous), or a separate empty-cell encoding chosen.
When any condition fails → fall back to the current named syntax.
Rationale: the CSV pattern is heavily represented in LLM training data; a single header avoids per-row key repetition. Estimated 30-50% token savings on arrays with 50+ rows. Must remain OFF in `Verbose` preset for debug clarity.
## TOON-T-2: Schema-derived type elision in `@data`
**Priority:** P2 · **Type:** Feature · **Origin:** 2026-04-24 LLM-accuracy proposal
Omit type names on `@data` list elements when:
1. The containing property's declared type is a concrete class (not interface/abstract).
2. The collection is non-polymorphic at runtime.
3. The element is not the `@data` root.
```
// Currently
GenericAttributes = [
GenericAttributeDto { Id = 99, Key = "NetWeight" }
]
// Proposed (when conditions hold)
GenericAttributes = [
{ Id = 99, Key = "NetWeight" }
]
```
Must stay explicit in `Verbose` mode and in polymorphic collections (type name is the discriminator). The main design tension: this slightly erodes the "explicit is better" principle — schema drift in a long conversation could cause silent misinterpretation. Pair with TOON-T-5 to mitigate.
## TOON-T-3: Schema-declared default values
**Priority:** P2 · **Type:** Feature · **Origin:** 2026-04-24 LLM-accuracy proposal
Currently `OmitDefaultValues = true` removes fields equal to `default(T)`, but the LLM must guess what the default is. Extend `@types` with explicit per-property defaults so absence becomes unambiguous:
```
@types {
Product:
Stock: int32
description: "Available inventory"
default-value: 0
constraints: "required, non-negative"
}
```
Implementation: during `@types` emission, detect the property's `default(T)` value and emit `default-value: ...` when `OmitDefaultValues = true`. `@data` omission then carries documented semantics ("use the schema default"), not implicit guessing.
Deserializer impact: N/A — no deserializer yet (see TOON-T-6).
## TOON-T-4: Reference-handling preset audit
**Priority:** P3 · **Type:** Configuration review · **Origin:** 2026-04-24 LLM-accuracy proposal
The `@N` / `@ref:N` syntax exists and works (see `TOON_FORMAT.md#reference-syntax`). Audit whether the presets activate it aggressively enough:
- `Default` preset inherits `ReferenceHandling` from the base `AcSerializerOptions` default — verify the default is `OnlyId` or `All`, not `None`, for IId-bearing entity graphs.
- `Compact` explicitly sets `None` — intentional (token-minimal) but may duplicate shared objects.
- Document explicitly which preset to choose when the object graph has repeated entities.
Deliverable: a "When to choose which ReferenceHandling mode" section in `TOON_OPTIONS.md` with concrete examples.
## TOON-T-5: Schema hash in `@meta`
**Priority:** P3 · **Type:** Feature (defense-in-depth for TOON-T-2, TOON-T-3) · **Origin:** 2026-04-24
After TOON-T-2 and TOON-T-3 ship, `@data` payloads rely more heavily on a specific `@types` schema. In multi-turn conversations, the LLM's cached schema may drift out of sync with the sender's.
Emit a compact schema fingerprint in `@meta`:
```
@meta {
version = "1.0"
schema-hash = "a3f9b2"
types = [...]
}
```
The prompt template instructs the LLM: *"only use cached schema when the `schema-hash` matches; otherwise request the full schema again."* Cost: ~20-30 tokens per payload. Benefit: silent schema drift becomes detectable.
## TOON-T-6: Deserialization support
**Priority:** P3 · **Type:** Feature (major) · **Origin:** 2026-04-24 during docs migration
Toon is currently **serialize-only** — there is no `AcToonDeserializer`. An LLM can produce Toon-formatted responses, but the framework cannot parse them back into C# objects.
Decision needed before implementation:
- Is Toon deserialization in-scope? (Primary goal is LLM accuracy — deserialization is mostly useful for LLM-generated outputs.)
- Or is `AcBinary`/`AcJson` the intended deserialization path, with Toon strictly a presentation format for LLM inputs?
If in-scope, the parser must handle: `@meta`/`@types`/`@data` sections, reference resolution (`@N` / `@ref:N`), multi-line strings, numeric enum values (requires `@types` for name mapping), tabular arrays (if TOON-T-1 lands), polymorphic collections.
## TOON-T-7: Validate benchmark claims before re-adding to docs
**Priority:** P3 · **Type:** Doc correctness · **Origin:** 2026-04-24 during `ToonExtendedInfo.txt` removal
The original `ToonExtendedInfo.txt` (deleted during migration) claimed:
- First-time Full mode: ~85% of JSON speed.
- Subsequent DataOnly: ~95% of JSON speed.
- With attributes: ~90% of JSON speed.
No benchmark harness was found in `AyCode.Core.Tests` or `AyCode.Benchmark` backing these numbers. Either:
- (a) Run real benchmarks and replace with measured values in `TOON_OPTIONS.md`.
- (b) Drop the claims entirely — marketing filler without provenance is worse than no claim.
## TODO entry template
```
## TODO-NN: Short title
**Priority:** P0 / P1 / P2 / P3 · **Type:** Bug fix / Feature / Cleanup / Docs · **Related:** `TOON_ISSUES.md#issue-NN` (if applicable)
Description of what and why, including the trigger (user request, audit finding, incident).
Options / sub-tasks / acceptance criteria.
```

View File

@ -0,0 +1,42 @@
# XCUT — Cross-cutting issues and TODOs
Canonical home for issues, TODOs, bugs, and critical items that span **two or more topics** (e.g., BINARY + SIGNALR, LOGGING + SIGNALR, BINARY + TOON).
## When to use `XCUT-*-N` vs a topic-specific ID
- **Single topic** (concern, bug, or planned work inside one domain) → `{TOPIC}-{TYPE}-{N}` in that topic's `{TOPIC}_ISSUES.md` / `{TOPIC}_TODO.md` (e.g., `LOG-I-5` in `LOGGING_ISSUES.md`).
- **Two or more topics** (coordinated change needed across domains, or the issue's root cause spans multiple subsystems) → `XCUT-{TYPE}-{N}` here, with cross-references from each affected topic's file.
## Examples of cross-cutting items
- **`XCUT-I-1: JSON-in-Binary request parameters`** — affects both the BINARY serializer (wire format) and SIGNALR transport (envelope). Neither side can fix it alone; it needs a coordinated client+server+consumer migration.
- *(hypothetical)* `XCUT-T-N` for a shared infrastructure refactor that touches Logger, SignalR, and SourceGenerator simultaneously.
- *(hypothetical)* `XCUT-B-N` for a bug where the symptom appears in one topic but the root cause lives in another.
## Files in this folder
- [`XCUT_ISSUES.md`](XCUT_ISSUES.md) — cross-cutting known issues (canonical entries)
- [`XCUT_TODO.md`](XCUT_TODO.md) — cross-cutting planned work (canonical entries)
## Cross-reference convention
1. **Canonical entry** lives here. Full body: status, affects (list of topics), rationale, migration plan.
2. **Each affected topic** adds a short `### XCUT-{TYPE}-{N}: <title> — cross-ref` section in its own `_ISSUES.md` / `_TODO.md`, pointing back to this canonical entry.
3. The **topic-specific cross-ref** body is one sentence: "See canonical entry at `../XCUT/XCUT_ISSUES.md#xcut-{type}-{n}`" + a 1-2 line summary of what this topic contributes.
## How `XCUT-*-N` IDs are numbered
Per the `TOPIC_CODES.md` registry:
- Counter is `XCUT` + type (`I` / `T` / `B` / `C`).
- `XCUT-I-1`, `XCUT-I-2`, ... share one counter across all cross-cutting issues regardless of which topics they span.
- `XCUT-T-1`, `XCUT-T-2`, ... share another counter for cross-cutting TODOs.
- Append-only — once assigned, IDs never change.
## Historical note
Before this folder existed, cross-cutting entries were duplicated in each affected topic's file (e.g., `XCUT-1` appeared both in `BINARY_ISSUES.md` and `SIGNALR_ISSUES.md`). The canonical-home pattern here replaces that — one authoritative entry, short cross-refs from each affected topic.
## See also
- **Registry** (topic codes, type codes, ID format rules): `../../../.github/skills/docs-check/references/TOPIC_CODES.md`
- **Decision Log** (why this folder exists): `../../../.github/LLM_PROTOCOL_DECISIONS.md` — entry "XCUT consolidation to dedicated folder".

View File

@ -0,0 +1,45 @@
# XCUT — Cross-cutting Known Issues
Canonical entries for known issues that span two or more topics. See `README.md` in this folder for the cross-cutting convention.
For planned cross-cutting work, see `XCUT_TODO.md`.
---
## XCUT-I-1: JSON-in-Binary request parameters
**Status:** Major tech debt, planned replacement (coordinated)
**Affects:** BINARY serializer (wire format) ↔ SIGNALR transport (envelope) ↔ all consuming projects (caller code)
### Description
Client→server request parameters currently travel as JSON inside a Binary envelope:
- `SignalPostJsonDataMessage<T>` is the current envelope
- Response path already uses pure Binary (no JSON)
- Asymmetry: request is JSON-in-Binary, response is Binary
### Why it spans multiple topics
- **BINARY side**: the serializer has to special-case JSON payloads inside a Binary stream; ideal Binary-only flow is broken on the request side.
- **SIGNALR side**: the transport carries the JSON-wrapped Binary payload; `SignalPostJsonDataMessage<T>` is a SignalR-level concept.
- **Consumer side**: every caller sends requests via this asymmetric path; changing the wire format requires coordinated client + server + all-consuming-project updates.
### Planned migration (tracked in TODO)
- `BINARY_TODO.md#bin-t-1` — "Replace JSON-in-Binary request parameters"
- Acceptance: `SignalPostJsonDataMessage<T>` replaced by `SignalPostBinaryDataMessage<T>` (or equivalent); no JSON round-trip on the wire for request params; benchmarks confirm no regression.
### Do NOT attempt as a side-effect
Any client or server change in isolation will break the other side. Requires:
1. Binary envelope for both request and response defined
2. Client code updated to serialize via Binary
3. Server code updated to deserialize via Binary
4. All consuming projects rebuilt against new API
5. Version bump coordinated
### Cross-references
- **BINARY_ISSUES.md** (`../BINARY/BINARY_ISSUES.md#xcut-i-1`): cross-ref pointing here
- **SIGNALR_ISSUES.md** (`../../../AyCode.Services/docs/SIGNALR/SIGNALR_ISSUES.md#xcut-i-1`): cross-ref pointing here
- **BINARY_TODO.md#bin-t-1**: migration plan

View File

@ -0,0 +1,37 @@
# XCUT — Cross-cutting TODO
Canonical entries for planned work that spans two or more topics. See `README.md` in this folder for the cross-cutting convention.
For known cross-cutting issues, see `XCUT_ISSUES.md`.
## Priority legend
- **P0** blocker · **P1** important · **P2** nice-to-have · **P3** idea
---
*(No cross-cutting TODO entries yet. When adding one, use the `XCUT-T-N` format and follow the canonical + per-topic cross-ref pattern documented in `README.md`.)*
## Entry template
```
## XCUT-T-N: Short title
**Priority:** P1/P2/P3 · **Type:** Refactor/Feature/etc. · **Related:** `<topic>_TODO.md#<id>`, `<topic>_ISSUES.md#<id>`
**Affects:** <topic-1><topic-2> [ ↔ consumer projects ]
### Why it spans multiple topics
<1-2 sentences per topic explaining its role>
### Migration plan
<ordered steps>
### Acceptance criteria
<bullet list>
### Do NOT attempt as a side-effect
<why it requires coordination>
```

View File

@ -30,5 +30,8 @@
<Folder Include="DbSets\Loggers\" />
<Folder Include="SqlScripts\" />
</ItemGroup>
<ItemGroup>
<None Include="**\README.md" Exclude="$(DefaultItemExcludes)" />
</ItemGroup>
</Project>

View File

@ -2,7 +2,7 @@
Logger-specific EF Core DbContext with `NoTracking` query behavior for read performance. Used by `AcDbLogItemWriter` to persist log items.
> For full logging architecture see `docs/LOGGING.md`.
> For full logging architecture see `docs/LOGGING/README.md`.
## Key Files

View File

@ -2,7 +2,7 @@
Log item DbSet interface for EF Core log storage.
> For full logging architecture see `docs/LOGGING.md`.
> For full logging architecture see `docs/LOGGING/README.md`.
## Key Files

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
</PropertyGroup>
@ -14,5 +14,8 @@
<ProjectReference Include="..\AyCode.Entities\AyCode.Entities.csproj" />
<ProjectReference Include="..\AyCode.Utils\AyCode.Utils.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="**\README.md" Exclude="$(DefaultItemExcludes)" />
</ItemGroup>
</Project>

View File

@ -2,7 +2,7 @@
Server-side log item entity and interface, extending the client-side `AcLogItemClient` with database-mapped identity and header reference.
> For full logging architecture see `docs/LOGGING.md`. For client-side entity see `AyCode.Entities/LogItems/README.md`.
> For full logging architecture see `docs/LOGGING/README.md`. For client-side entity see `AyCode.Entities/LogItems/README.md`.
## Key Files

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
</PropertyGroup>
@ -16,5 +16,8 @@
<ItemGroup>
<PackageReference Include="MessagePack.Annotations" Version="3.1.4" />
</ItemGroup>
<ItemGroup>
<None Include="**\README.md" Exclude="$(DefaultItemExcludes)" />
</ItemGroup>
</Project>

View File

@ -2,7 +2,7 @@
Client-side log item entity used for structured logging. Serialized with MessagePack for efficient transport over SignalR.
> For full logging architecture see `docs/LOGGING.md`.
> For full logging architecture see `docs/LOGGING/README.md`.
## Key Files

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
</PropertyGroup>
@ -19,5 +19,8 @@
<ProjectReference Include="..\AyCode.Models\AyCode.Models.csproj" />
<ProjectReference Include="..\AyCode.Services\AyCode.Services.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="**\README.md" Exclude="$(DefaultItemExcludes)" />
</ItemGroup>
</Project>

View File

@ -3,7 +3,7 @@
Reflection-based infrastructure for dynamically dispatching method calls by message tag, primarily used for SignalR message routing.
> **Context:** This is the server-side dispatch engine for the SignalR tag-based architecture.
> See `AyCode.Services.Server/docs/SIGNALR_SERVER.md` for the full message flow.
> See `AyCode.Services.Server/docs/SIGNALR/README.md` for the full message flow.
## How It Fits

View File

@ -1,4 +1,4 @@
 <Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
</PropertyGroup>
@ -35,6 +35,7 @@
<ItemGroup>
<None Include="docs\**\*.md" />
<None Include="**\README.md" Exclude="$(DefaultItemExcludes);docs\**" />
</ItemGroup>
</Project>

View File

@ -10,8 +10,8 @@ Server-side service implementations: JWT authentication, SendGrid email delivery
| Document | Topic |
|---|---|
| `SIGNALR_SERVER.md` | Server-side SignalR hub (dispatch, session, broadcast) |
| `SIGNALR_DATASOURCE.md` | Real-time DataSource with CRUD & change tracking |
| `SIGNALR/README.md` | Server-side SignalR hub (dispatch, session, broadcast) |
| `SIGNALR/SIGNALR_DATASOURCE.md` | Real-time DataSource with CRUD & change tracking |
## Folder Structure

View File

@ -2,7 +2,7 @@
Server-side SignalR hub infrastructure: hub base class, session management, data source with change tracking, and client broadcast service.
> **Architecture:** For full dispatch flow, tag system, and tech debt documentation see `AyCode.Services/docs/SIGNALR.md`.
> **Architecture:** For full dispatch flow, tag system, and tech debt documentation see `AyCode.Services/docs/SIGNALR/README.md`.
## Key Files
@ -18,7 +18,7 @@ Server-side SignalR hub infrastructure: hub base class, session management, data
### Data Source
> **Full specification:** `AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md`
> **Full specification:** `AyCode.Services.Server/docs/SIGNALR/SIGNALR_DATASOURCE.md`
- **`AcSignalRDataSource.cs`** — Generic real-time collection (`AcSignalRDataSource<TDataItem, TId, TIList>`) implementing `IList<T>` with full CRUD and change tracking.
- **Change tracking:** `TrackingItem<T, TId>` wraps each modified item with `TrackingState` + `OriginalValue` for rollback. `ChangeTracking<T, TId>` manages the tracking list.

View File

@ -0,0 +1,16 @@
# AyCode.Services.Server documentation
Topic documentation for the `AyCode.Services.Server` project (Layer 0, server-side services).
## Topics
- [`SIGNALR/`](SIGNALR/README.md) — Server-side SignalR (hub base + data source pattern)
## Navigation
Per the AI Agent Core Protocol (folder navigation rule), start from this README when browsing `docs/`. Each topic folder has its own `README.md` with the main content, plus optional `TOPIC_ISSUES.md` (known issues) and `TOPIC_TODO.md` (planned work).
## See also
- **Client-side SignalR**: `../../AyCode.Services/docs/SIGNALR/README.md`
- **Binary-over-SignalR wire format**: `../../AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL/README.md`

View File

@ -2,7 +2,7 @@
Server-side SignalR hub infrastructure: method dispatch, session management, broadcast, and diagnostics. Source: `SignalRs/` in this project.
> For client-side transport (tags, wire protocol, client base) see `AyCode.Services/docs/SIGNALR.md`.
> For client-side transport (tags, wire protocol, client base) see `AyCode.Services/docs/SIGNALR/README.md`.
> For the DataSource collection see `SIGNALR_DATASOURCE.md`.
## Server Processing

View File

@ -2,8 +2,8 @@
Change-tracked real-time collection built on top of the SignalR transport layer. Source: `SignalRs/AcSignalRDataSource.cs` in this project.
> For the underlying transport (tag system, wire protocol, client base) see `AyCode.Services/docs/SIGNALR.md`.
> For server hub infrastructure see `SIGNALR_SERVER.md`.
> For the underlying transport (tag system, wire protocol, client base) see `AyCode.Services/docs/SIGNALR/README.md`.
> For server hub infrastructure see `README.md`.
## Overview
@ -179,7 +179,7 @@ DataSource.SaveChanges()
→ Server method with [SignalR(tag)] ← tag dispatch
```
Projects can also call the transport directly without DataSource — see `AyCode.Services/docs/SIGNALR.md`.
Projects can also call the transport directly without DataSource — see `AyCode.Services/docs/SIGNALR/README.md`.
## Key Source Files

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
</PropertyGroup>
@ -20,6 +20,7 @@
<ItemGroup>
<None Include="docs\**\*.md" />
<None Include="**\README.md" Exclude="$(DefaultItemExcludes);docs\**" />
</ItemGroup>
</Project>

View File

@ -2,7 +2,7 @@
Remote log writers for sending log items over HTTP, SignalR, or to the browser console. All three implement `IAcLogWriterClientBase`.
> For full logging architecture see `docs/LOGGING.md`. For core logger and writer abstractions see `AyCode.Core/Loggers/README.md`.
> For full logging architecture see `docs/LOGGING/README.md`. For core logger and writer abstractions see `AyCode.Core/Loggers/README.md`.
## Key Files

View File

@ -10,8 +10,9 @@ Shared service implementations: SignalR communication (custom binary protocol),
| Document | Topic |
|---|---|
| `SIGNALR.md` | Client-side SignalR transport (tags, wire protocol, req/resp flow) |
| `LOGGING_REMOTE.md` | Remote log writers (HTTP, browser console, SignalR) |
| `SIGNALR/README.md` | Client-side SignalR transport (tags, wire protocol, req/resp flow) |
| `SIGNALR_BINARY_PROTOCOL/README.md` | Binary-over-SignalR wire format, chunked framing |
| `LOGGING/README.md` | Remote log writers (HTTP, browser console, SignalR) |
## Folder Structure

View File

@ -2,9 +2,9 @@
Custom binary SignalR protocol, client infrastructure, message tagging, and serialization helpers.
> **Architecture:** For full dispatch flow, tag system, and tech debt documentation see `AyCode.Services/docs/SIGNALR.md`.
> **Binary protocol:** For wire format, zero-copy pipeline, and three-path read logic see `AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL.md`.
> **Known issues:** `AyCode.Services/docs/SIGNALR_ISSUES.md`
> **Architecture:** For full dispatch flow, tag system, and tech debt documentation see `AyCode.Services/docs/SIGNALR/README.md`.
> **Binary protocol:** For wire format, zero-copy pipeline, and three-path read logic see `AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL/README.md`.
> **Known issues:** `AyCode.Services/docs/SIGNALR/SIGNALR_ISSUES.md`
## Key Files
@ -21,7 +21,7 @@ Custom binary SignalR protocol, client infrastructure, message tagging, and seri
### Message Tagging
- **`SignalMessageTagAttribute.cs`** — Three attributes: `TagAttribute` (base, int messageTag), `SignalRAttribute` (server method routing + client notification), `SignalRSendToClientAttribute` (client-side receive).
- **`AcSignalRTags.cs`** — Static constants: `None`, `PingTag`, `EchoTag`.
- **`SignalRCrudTags.cs`** — Sealed class bundling 5 independent CRUD tag integers. `GetMessageTagByTrackingState()` maps `TrackingState` -> tag. See `AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md`.
- **`SignalRCrudTags.cs`** — Sealed class bundling 5 independent CRUD tag integers. `GetMessageTagByTrackingState()` maps `TrackingState` -> tag. See `AyCode.Services.Server/docs/SIGNALR/SIGNALR_DATASOURCE.md`.
- **`SendToClientType.cs`** — Enum: None, Others, Caller, All.
### Serialization & Pooling

View File

@ -1,6 +1,6 @@
# Remote Log Writers
Client-side log writers that send log data to remote endpoints. Source: `Loggers/` in this project. For core logging framework see `AyCode.Core/docs/LOGGING.md`. For server-side GlobalLogger see `AyCode.Core.Server/docs/LOGGING_SERVER.md`.
Client-side log writers that send log data to remote endpoints. Source: `Loggers/` in this project. For core logging framework see `AyCode.Core/AyCode.Core/docs/LOGGING/README.md`. For server-side GlobalLogger see `AyCode.Core.Server/docs/LOGGING/README.md`.
## AcBrowserConsoleLogWriter

View File

@ -0,0 +1,20 @@
# AyCode.Services documentation
Topic documentation for the `AyCode.Services` project (Layer 0, service abstractions).
## Topics
- [`LOGGING/`](LOGGING/README.md) — Remote logger (variant — sends log entries over the wire)
- [`SIGNALR/`](SIGNALR/README.md) — SignalR transport (tag-based protocol, generic hub methods)
- [`SIGNALR_BINARY_PROTOCOL/`](SIGNALR_BINARY_PROTOCOL/README.md) — Binary-over-SignalR wire format, chunked framing
## Navigation
Per the AI Agent Core Protocol (folder navigation rule), start from this README when browsing `docs/`. Each topic folder has its own `README.md` with the main content, plus optional `TOPIC_ISSUES.md` (known issues) and `TOPIC_TODO.md` (planned work).
## See also
- **Base logger** (framework): `../../AyCode.Core/AyCode.Core/docs/LOGGING/README.md`
- **Server-side logger** (variant): `../../AyCode.Core.Server/docs/LOGGING/README.md`
- **Server-side SignalR**: `../../AyCode.Services.Server/docs/SIGNALR/README.md`
- **Binary serializer** (used by SIGNALR_BINARY_PROTOCOL): `../../AyCode.Core/AyCode.Core/docs/BINARY/README.md`

View File

@ -2,8 +2,8 @@
Client-side SignalR transport: custom binary protocol, tag-based dispatch. Source: `SignalRs/`
> Server-side hub, session, broadcast: `AyCode.Services.Server/docs/SIGNALR_SERVER.md`
> DataSource collection: `AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md`
> Server-side hub, session, broadcast: `AyCode.Services.Server/docs/SIGNALR/README.md`
> DataSource collection: `AyCode.Services.Server/docs/SIGNALR/SIGNALR_DATASOURCE.md`
## Design
@ -74,7 +74,7 @@ Custom `IHubProtocol` (`"acbinary"`), replaces JSON. Zero-copy write via `Buffer
`AcBinaryHubProtocol` is the base (unsealed) — general binary framing only. `AyCodeBinaryHubProtocol` derives from it with consumer-specific logic: `SignalParams` capture (via `OnArgumentRead` hook), `IsRawBytesData` path, `SignalDataType` type resolution. Register `AyCodeBinaryHubProtocol` in both client and server.
> Wire format, argument framing, dual BWO pattern, length prefix patching: `SIGNALR_BINARY_PROTOCOL.md`
> Wire format, argument framing, dual BWO pattern, length prefix patching: `../SIGNALR_BINARY_PROTOCOL/README.md`
### SignalParams + Payload Separation
@ -152,7 +152,7 @@ GetParameterValues(ParameterInfo[]):
Type-guided deserialization — each parameter is individually serialized/deserialized with its concrete type, avoiding the `object[]` → dictionary problem of untyped binary deserialization.
> Known concerns and limitations on parameter serialization (per-parameter overhead, AcBinary-only) are tracked in `SIGNALR_ISSUES.md` under `PROTO-2` and `PROTO-3`.
> Known concerns and limitations on parameter serialization (per-parameter overhead, AcBinary-only) are tracked in `SIGNALR_ISSUES.md` under `SIG-I-2` and `SIG-I-3`.
## Response Patterns

View File

@ -2,7 +2,7 @@
## Protocol
### PROTO-1: Server-side IsRawBytesData pre-serialize
### SIG-I-1: Server-side IsRawBytesData pre-serialize
**Status:** Planned removal
**Affects:** `AcWebSignalRHubBase.SendMessageToClient`
@ -11,7 +11,7 @@ The server forwards the client's `IsRawBytesData` flag in the response `SignalPa
**Plan:** Remove `IsRawBytesData` forwarding from server response path. The client should use `SignalDataType` for typed deserialization and explicit `byte[]` type for raw data.
### PROTO-2: Parameter serialization is per-parameter
### SIG-I-2: Parameter serialization is per-parameter
**Status:** Known performance concern
**Affects:** `SignalParams.SetParameterValues` / `GetParameterValues`
@ -20,7 +20,7 @@ Each parameter is individually serialized via `ToBinary()` / `BinaryTo(Type)`
**Possible optimization:** Batch fast-path — single serialization context for all parameters. Benchmark first.
### PROTO-3: Parameter serialization is AcBinary only
### SIG-I-3: Parameter serialization is AcBinary only
**Status:** Limitation
**Affects:** `SignalParams.SetParameterValues` / `GetParameterValues`
@ -29,14 +29,14 @@ Uses `ToBinary()` / `BinaryTo()` exclusively. JSON parameter support would requi
## Transport
### TRANS-1: BufferWriterChunkSize defaults to 64KB for SignalR
### SIG-I-4: BufferWriterChunkSize defaults to 64KB for SignalR
**Status:** DONE
**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.
### TRANS-2: WebSocket buffer sizes are hardcoded
### SIG-I-5: WebSocket buffer sizes are hardcoded
**Status:** Acceptable
**Affects:** `AcSignalRClientBase` connection setup
@ -45,7 +45,7 @@ Transport max message size (30MB) and application buffer (30MB) are hardcoded. S
## DataSource
### DS-1: GetAll returns raw byte[] for populate/merge
### SIG-I-6: GetAll returns raw byte[] for populate/merge
**Status:** By design
**Affects:** `AcSignalRDataSource.LoadDataSourceAsync`
@ -54,9 +54,39 @@ The `GetAll` path uses `IsRawBytesData = true` to receive raw `byte[]` from the
**Possible optimization:** Direct typed deserialization with merge support in the deserializer (PopulateMerge from `ReadOnlySequence<byte>`). Requires deserializer API changes.
## Server-side Setup & DI
### SIG-I-7: Server-side NopCommerce plugin AcBinaryHubProtocolOptions not bound from appsettings
**Status:** Open · **Severity:** Minor (works, but hardcoded + bypasses DI logger) · **Area:** Consumer adoption gap in `Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs`
The framework overload chain is complete on the server side — `AcSignalRProtocolExtensions.BuildProtocol` already resolves `IOptions<AcBinaryHubProtocolOptions>` from DI and falls back to defaults otherwise. The consumer plugin does NOT use it:
Current `PluginNopStartup.ConfigureServices`:
```csharp
.AddAcBinaryProtocol(opts =>
{
opts.ProtocolMode = BinaryProtocolMode.AsyncSegment; // ← HARDCODED
opts.Logger = new Logger(nameof(AyCodeBinaryHubProtocol)); // ← BYPASSES ILogger<T> DI
});
```
What's missing:
- No `services.Configure<AcBinaryHubProtocolOptions>(configuration.GetSection("AyCode:SignalR:Protocol"))``ProtocolMode`, `BufferSize`, `WaitForFlush`, `FlushTimeout` are all hardcoded / default.
- The `appsettings.json` has no `AyCode:SignalR` (or equivalent) section at all — so per-deploy tuning (e.g. increasing `FlushTimeout` for a satellite link, switching `ProtocolMode` for diagnostics) requires a code change + redeploy.
- Manual `new Logger(...)` sidesteps the DI `ILogger<AcBinaryHubProtocol>` auto-resolution that `BuildProtocol` provides → creates a parallel logger instance (see `../../../AyCode.Core/docs/LOGGING/LOGGING_ISSUES.md#log-i-8`).
### Fix direction
See `SIGNALR_TODO.md#sig-t-5`.
### Related
- `../../../AyCode.Core/docs/LOGGING/LOGGING_ISSUES.md#log-i-8` (sibling gap — same plugin, logger setup)
- `SIG-I-8` (client-side equivalent — `HubConnectionBuilder.Services` inner DI isolation forces a different workaround)
- Plugin doc drift: `Nop.Plugin.Misc.AIPlugin/docs/SIGNALR/README.md:22` documents `services.AddSingleton<IHubProtocol>(new AcBinaryHubProtocol())` — the actual code uses `.AddAcBinaryProtocol(opts => {...})`. Doc needs a rewrite.
## Client-side Setup & DI
### CONN-1: HubConnectionBuilder inner DI isolation
### SIG-I-8: HubConnectionBuilder inner DI isolation
**Status:** Workaround-in-place (dedicated options-passing overload)
**Affects:** Consumer client setup in `Program.cs` (MAUI, WASM, ASP.NET Core server prerender)
@ -71,7 +101,7 @@ hubBuilder.AddAcBinaryProtocol(protocolOpts);
## Dispatch
### DISPATCH-1: First-call null response (observed)
### SIG-I-9: First-call null response (observed)
**Status:** Open — not diagnosed
**Affects:** `PostDataAsync<T>` awaiter / OnReceiveMessage → pending-request correlation
@ -87,10 +117,10 @@ Log timeline:
Hypothesis (unverified): `PostDataAsync<T>` awaiter's null-mapping path misroutes the parsed result, or `requestId → Task<T>` correlation has a race on the first response of a fresh connection. Client auto-retry hides the user-visible impact.
**Related TODO:** `SIGNALR_TODO.md#todo-01`
**Related TODO:** `SIGNALR_TODO.md#sig-t-1`
## Cross-cutting (also tracked in serializer-side docs)
## Cross-cutting (canonical home: `AyCode.Core` repo's `docs/XCUT/`)
### XCUT-1: JSON-in-Binary request parameters — cross-ref
### XCUT-I-1: JSON-in-Binary request parameters — cross-ref
Same tech debt as `../../AyCode.Core/docs/BINARY_ISSUES.md#xcut-1`. Planned replacement: migrate client→server request parameters from JSON-in-Binary envelope to direct Binary serialization. Coordinated change across all consuming projects.
Canonical entry: **`../../../AyCode.Core/docs/XCUT/XCUT_ISSUES.md#xcut-i-1`**. Summary: SignalR transport carries `SignalPostJsonDataMessage<T>` (JSON inside a Binary envelope) for request params, while response path is pure Binary. Planned replacement is coordinated across BINARY serializer + SIGNALR transport + all consuming projects.

View File

@ -0,0 +1,75 @@
# SignalR — TODO
## Priority legend
- **P0** blocker · **P1** important · **P2** nice-to-have · **P3** idea
---
## SIG-T-1: Diagnose first-call null in PostDataAsync<T>
**Priority:** P2 · **Type:** Investigation · **Related:** `SIGNALR_ISSUES.md#issue-02`
Reproduce the `GetProductDtos_80`-style first-call null. Add trace logs to `PostDataAsync<T>` awaiter path and `OnReceiveMessage → pending request` dictionary lookup. Verify `requestId → Task<T>` correlation on the very first chunked response of a fresh connection.
## SIG-T-2: Document asymmetric send/receive capability
**Priority:** P1 · **Type:** Docs
Current behaviour: sender selects `BinaryProtocolMode` independently; receiver detects the wire format from the first byte (`CHUNK_START=200` → chunked path; else non-chunked). This means client and server can run DIFFERENT `ProtocolMode` settings independently — a core feature amplifying interoperability.
Document in `../SIGNALR_BINARY_PROTOCOL/README.md` as a dedicated "Asymmetric send/receive contract" section. Key selling points:
- WASM client + AsyncSegment server → works (WASM downgrades send, receives chunked happily)
- Third-party client on NuGet can pick any mode → server doesn't care
- Gradual mode rollouts possible (no synchronized deploy)
## SIG-T-3: Code-level guard for FlushTimeout < ClientTimeoutInterval
**Priority:** P2 · **Type:** Feature
`AcBinaryHubProtocolOptions.Validate()` currently documents (in XML doc) that `FlushTimeout` should be less than the SignalR `HubOptions.ClientTimeoutInterval`, but there is no code-level check. Add validation at protocol registration time — if both options are resolvable from the DI scope, verify the constraint and emit a startup warning (or throw, pending decision).
## SIG-T-4: `BinaryProtocolMode.Auto` — adaptive send-mode
**Priority:** P3 · **Type:** Feature · **Related:** `../SIGNALR_BINARY_PROTOCOL/SIGNALR_BINARY_PROTOCOL_TODO.md#sbp-t-3`
Design: on first received message, inspect first byte to determine peer's send format. On subsequent sends, match it (subject to local-platform constraints, e.g. WASM never actually sends AsyncSegment). Per-`HubConnection` state. Optional upfront handshake-extension negotiation as an alternative — see wire-level TODO.
## SIG-T-5: Server-side NopCommerce plugin — expose `AcBinaryHubProtocolOptions` via appsettings
**Priority:** P2 · **Type:** Consumer refactor · **Related:** `SIGNALR_ISSUES.md#sig-i-7`, `../../../AyCode.Core/docs/LOGGING/LOGGING_TODO.md#log-t-11`
Bind `AcBinaryHubProtocolOptions` from `appsettings.json` instead of hardcoding `ProtocolMode` and constructing a manual `Logger` instance in `PluginNopStartup.cs`. This sibling task is paired with LOGGING_TODO.md#log-t-11 (same plugin, logger-setup migration) — best landed in one commit.
### Target diff
```csharp
// In PluginNopStartup.ConfigureServices, BEFORE AddSignalR(...):
// 1. Bind Protocol options from appsettings.json
services.Configure<AcBinaryHubProtocolOptions>(configuration.GetSection("AyCode:SignalR:Protocol"));
// 2. AddSignalR + AddAcBinaryProtocol — drop the inline Action<T> entirely
services.AddSignalR(hubOptions => { /* unchanged */ })
.AddAcBinaryProtocol(); // ← BuildProtocol auto-resolves IOptions<T> + ILogger<T> from DI
```
### Appsettings.json addition (sibling to `AyCode:Logger`)
```json
{
"AyCode": {
"Logger": { "AppType": "Server", "LogLevel": "Debug" },
"SignalR": {
"Protocol": {
"ProtocolMode": "AsyncSegment",
"BufferSize": 4096,
"WaitForFlush": true,
"FlushTimeout": "00:00:10"
}
}
}
}
```
### Consequences / checklist
- [ ] `new Logger(...)` line removed from SignalR registration → server-side logger now goes through the DI factory (see LOGGING_TODO.md#log-t-11).
- [ ] Per-deploy tuning possible without recompile: switching `ProtocolMode` for diagnostics, extending `FlushTimeout` for slow links, adjusting `BufferSize` for different Kestrel slab sizes.
- [ ] `Name` stays at `"acbinary"` default — changing it would break wire-level compat with existing clients.
- [ ] `AcBinaryHubProtocolOptions.Validate()` still runs — invalid config (e.g. `ProtocolMode=AsyncSegment` on a WASM server, which is impossible here but hypothetically) throws at startup.
- [ ] Plugin doc correction: `Nop.Plugin.Misc.AIPlugin/docs/SIGNALR/README.md:22` — the legacy `services.AddSingleton<IHubProtocol>(new AcBinaryHubProtocol())` line must be replaced with the real registration. Cross-ref the new `AyCode:SignalR:Protocol` section here.
### Why this belongs in AyCode.Services (framework layer) docs
The gap is consumer-level, but the canonical "server-side registration recipe" is a FRAMEWORK responsibility — LOGGING.md already shows it for the logger side. Adding a matching recipe to `../SIGNALR_BINARY_PROTOCOL/README.md#Registration in Program.cs → Server` would prevent the next consumer from making the same mistake. That doc update is part of this TODO's acceptance criteria.

View File

@ -5,7 +5,7 @@
`AyCodeBinaryHubProtocol` (derived) — project-specific consumer logic: `SignalParams` capture via `OnArgumentRead`, `IsRawBytesData` path, `SignalDataType` type resolution in `ReadSingleArgument` override.
> Architecture (tag system, dispatch, request/response): `SIGNALR.md`
> Output writers (cached chunk, buffer states, chunk sizing): `AyCode.Core/docs/BINARY_WRITERS.md`
> Output writers (cached chunk, buffer states, chunk sizing): `AyCode.Core/AyCode.Core/docs/BINARY/BINARY_WRITERS.md`
## Wire Format
@ -78,7 +78,7 @@ Protocol and serializer each create own `BufferWriterBinaryOutput` on the same `
**Cost:** one extra `GetMemory` per argument (nanoseconds).
**Benefit:** zero-copy end-to-end, no intermediate `byte[]`, no wrapper class.
Why two BWOs: serializer writes must live on `BinarySerializationContext` (sealed class) for JIT optimization — context owns its own BWO. See `AyCode.Core/docs/BINARY_WRITERS.md` § "Why Writes Are on the Context".
Why two BWOs: serializer writes must live on `BinarySerializationContext` (sealed class) for JIT optimization — context owns its own BWO. See `AyCode.Core/AyCode.Core/docs/BINARY/BINARY_WRITERS.md` § "Why Writes Are on the Context".
### Length Prefix Patching
@ -148,7 +148,7 @@ The context's `_buffer` always points directly to the current segment's backing
Typical overhead for 225KB payload with 4096-byte segments: ~224.5KB zero-copy, ~500 bytes scratch copy at ~55 boundaries. The scratch buffer is rented once (lazy, on first boundary) and reused across all boundaries. `Release()` returns it to `ArrayPool` after deserialization.
> Known issues: `AyCode.Core/docs/BINARY_ISSUES.md`
> Known issues: `AyCode.Core/AyCode.Core/docs/BINARY/BINARY_ISSUES.md`
## Configuration

View File

@ -1,9 +1,9 @@
# Binary Hub Protocol (wire) — Known Issues
For planned/actionable work see `SIGNALR_BINARY_PROTOCOL_TODO.md`.
For higher-level SignalR abstractions see `SIGNALR_ISSUES.md`.
For higher-level SignalR abstractions see `../SIGNALR/SIGNALR_ISSUES.md`.
## ISSUE-01: 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
@ -18,7 +18,7 @@ For higher-level SignalR abstractions see `SIGNALR_ISSUES.md`.
### Related TODO
None — architectural constraint of browser WASM threading model.
## ISSUE-02: 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

View File

@ -5,7 +5,7 @@
---
## TODO-01: SegmentBufferReader isolated unit tests
## SBP-T-1: SegmentBufferReader isolated unit tests
**Priority:** P1 · **Type:** Test coverage
Original `vast-brewing-moonbeam` refactor plan (chunked receive-path) listed these tests in its verification section; they were never written. Needed:
@ -14,7 +14,7 @@ Original `vast-brewing-moonbeam` refactor plan (chunked receive-path) listed the
- Missed-signal double-check pattern under `ManualResetEventSlim` reset
- `Dispose` lifecycle (buffer pool return, old-buffer cleanup)
## TODO-02: Chunked protocol integration test
## SBP-T-2: Chunked protocol integration test
**Priority:** P1 · **Type:** Test coverage
End-to-end round-trip:
@ -27,8 +27,8 @@ Cover asymmetric cases:
- Bytes sender, AsyncSegment receiver
- WASM-downgraded sender (Segment), server AsyncSegment receiver
## TODO-03: `BinaryProtocolMode.Auto` wire-detection implementation
**Priority:** P3 · **Type:** Feature · **Related:** `SIGNALR_TODO.md#todo-04`
## SBP-T-3: `BinaryProtocolMode.Auto` wire-detection implementation
**Priority:** P3 · **Type:** Feature · **Related:** `../SIGNALR/SIGNALR_TODO.md#sig-t-4`
Client-side adaptive mode: on first received message, inspect first payload byte:
- `CHUNK_START (200)` → peer uses AsyncSegment → match it on subsequent sends (subject to local-platform constraint — WASM safety-net overrides to Segment)
@ -36,8 +36,8 @@ Client-side adaptive mode: on first received message, inspect first payload byte
Per-`HubConnection` state. Requires changes to `BinaryProtocolMode` enum + detection wiring in `TryParseMessage`.
## TODO-04: SignalR handshake-extension for upfront mode negotiation
**Priority:** P3 · **Type:** Feature · **Related:** TODO-03
## SBP-T-4: SignalR handshake-extension for upfront mode negotiation
**Priority:** P3 · **Type:** Feature · **Related:** SBP-T-3
Alternative to wire-detection: use SignalR handshake message's `extensions` JSON field to carry protocol-capability info — e.g.
```json

View File

@ -1,31 +0,0 @@
# SignalR — TODO
## Priority legend
- **P0** blocker · **P1** important · **P2** nice-to-have · **P3** idea
---
## TODO-01: Diagnose first-call null in PostDataAsync<T>
**Priority:** P2 · **Type:** Investigation · **Related:** `SIGNALR_ISSUES.md#issue-02`
Reproduce the `GetProductDtos_80`-style first-call null. Add trace logs to `PostDataAsync<T>` awaiter path and `OnReceiveMessage → pending request` dictionary lookup. Verify `requestId → Task<T>` correlation on the very first chunked response of a fresh connection.
## TODO-02: Document asymmetric send/receive capability
**Priority:** P1 · **Type:** Docs
Current behaviour: sender selects `BinaryProtocolMode` independently; receiver detects the wire format from the first byte (`CHUNK_START=200` → chunked path; else non-chunked). This means client and server can run DIFFERENT `ProtocolMode` settings independently — a core feature amplifying interoperability.
Document in `SIGNALR_BINARY_PROTOCOL.md` as a dedicated "Asymmetric send/receive contract" section. Key selling points:
- WASM client + AsyncSegment server → works (WASM downgrades send, receives chunked happily)
- Third-party client on NuGet can pick any mode → server doesn't care
- Gradual mode rollouts possible (no synchronized deploy)
## TODO-03: Code-level guard for FlushTimeout < ClientTimeoutInterval
**Priority:** P2 · **Type:** Feature
`AcBinaryHubProtocolOptions.Validate()` currently documents (in XML doc) that `FlushTimeout` should be less than the SignalR `HubOptions.ClientTimeoutInterval`, but there is no code-level check. Add validation at protocol registration time — if both options are resolvable from the DI scope, verify the constraint and emit a startup warning (or throw, pending decision).
## TODO-04: `BinaryProtocolMode.Auto` — adaptive send-mode
**Priority:** P3 · **Type:** Feature · **Related:** `SIGNALR_BINARY_PROTOCOL_TODO.md#todo-03`
Design: on first received message, inspect first byte to determine peer's send format. On subsequent sends, match it (subject to local-platform constraints, e.g. WASM never actually sends AsyncSegment). Per-`HubConnection` state. Optional upfront handshake-extension negotiation as an alternative — see wire-level TODO.

View File

@ -24,10 +24,10 @@ Project-level docs — each project's `docs/` folder documents the code it defin
| Project | Documents |
|---|---|
| `AyCode.Core/docs/` | `BINARY_FORMAT.md`, `BINARY_FEATURES.md`, `BINARY_OPTIONS.md`, `LOGGING.md` |
| `AyCode.Core.Server/docs/` | `LOGGING_SERVER.md` |
| `AyCode.Services/docs/` | `SIGNALR.md`, `LOGGING_REMOTE.md` |
| `AyCode.Services.Server/docs/` | `SIGNALR_SERVER.md`, `SIGNALR_DATASOURCE.md` |
| `AyCode.Core/docs/` | `BINARY/` (README, FORMAT, FEATURES, OPTIONS, …), `LOGGING/` (README, ISSUES, TODO) |
| `AyCode.Core.Server/docs/` | `LOGGING/README.md` (server-side variant) |
| `AyCode.Services/docs/` | `SIGNALR/`, `SIGNALR_BINARY_PROTOCOL/`, `LOGGING/README.md` (remote variant) |
| `AyCode.Services.Server/docs/` | `SIGNALR/` (README + SIGNALR_DATASOURCE) |
## Solution Structure

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -24,16 +24,16 @@
## SignalR Conventions
See `AyCode.Services/docs/SIGNALR.md` for full architecture documentation.
See `AyCode.Services/docs/SIGNALR/README.md` for full architecture documentation.
- **Single dispatch method** — all communication goes through `OnReceiveMessage(int messageTag, int? requestId, SignalParams signalParams, object data)`. Do not add new hub methods.
- **Tag-based routing** — associate methods with integer tags via `[SignalR(tag)]` (server) or `[SignalRSendToClient(tag)]` (client). Tags must be unique across the entire system.
- **CRUD bundles** — entities use `SignalRCrudTags(getAllTag, getItemTag, addTag, updateTag, removeTag)` with 5 independent tag integers. Tags must be unique across the system. See `AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md`.
- **CRUD bundles** — entities use `SignalRCrudTags(getAllTag, getItemTag, addTag, updateTag, removeTag)` with 5 independent tag integers. Tags must be unique across the system. See `AyCode.Services.Server/docs/SIGNALR/SIGNALR_DATASOURCE.md`.
- **Binary protocol**`AyCodeBinaryHubProtocol` (derived from `AcBinaryHubProtocol`) is the transport protocol. Zero-copy write: `AcBinarySerializer.Serialize(value, output)` directly to pipe. Zero-copy read: `SequenceReader<byte>` + type-aware deserialization via `SignalParams.SignalDataType`. Three read paths: byte[] fast-path (0x44 tag), IsRawBytesData (raw byte[]), typed deserialization.
### ⚠️ Temporary: JSON-in-Binary Request Parameters
Client→server request parameters currently use a JSON-inside-Binary envelope — a cross-cutting tech debt planned for migration to pure Binary. Details in `AyCode.Core/docs/BINARY_ISSUES.md#xcut-1` + `AyCode.Services/docs/SIGNALR_ISSUES.md#xcut-1`. Migration is tracked in `BINARY_TODO.md#todo-01`. Do NOT attempt as a side-effect of unrelated work — requires coordinated client+server+consuming-project changes.
Client→server request parameters currently use a JSON-inside-Binary envelope — a cross-cutting tech debt planned for migration to pure Binary. Canonical entry: `AyCode.Core/AyCode.Core/docs/XCUT/XCUT_ISSUES.md#xcut-i-1`. Cross-refs: `BINARY_ISSUES.md#xcut-i-1` (serializer side) and `SIGNALR_ISSUES.md#xcut-i-1` (transport side). Migration is tracked in `BINARY_TODO.md#bin-t-1`. Do NOT attempt as a side-effect of unrelated work — requires coordinated client+server+consuming-project changes.
## Testing

File diff suppressed because one or more lines are too long

20
docs/README.md Normal file
View File

@ -0,0 +1,20 @@
# AyCode.Core documentation
Top-level documentation for the `AyCode.Core` repo (Layer 0 — core framework).
## Reference docs (flat)
- [`ARCHITECTURE.md`](ARCHITECTURE.md) — Repo architecture overview
- [`CONVENTIONS.md`](CONVENTIONS.md) — Coding conventions
- [`GLOSSARY.md`](GLOSSARY.md) — Domain glossary
## Sub-projects with docs
- `AyCode.Core/docs/` — Logger, Binary serializer (paired topics: LOGGING/, BINARY/)
- `AyCode.Core.Server/docs/` — Server-side logger variant (LOGGING/)
- `AyCode.Services/docs/` — Remote logger variant, SignalR, SignalR binary protocol
- `AyCode.Services.Server/docs/` — Server-side SignalR + data source
## Navigation
Per the AI Agent Core Protocol (folder navigation rule), start from this README when browsing `docs/`. Single-file reference docs remain flat at the repo-root level; multi-file topics live in named subfolders at the sub-project level.