Independent review fix commit (cold fork MERGEABLE-WITH-NITS + Codex BLOCKED, 2 Highs):
- Codex H1: a comma (0x2C) is a valid etagc and can appear INSIDE a quoted opaque-tag
("3,5" is ONE tag). The old Split(',') broke it into two malformed tokens → 400. Replaced
with a quote-aware position scanner that treats a comma as a separator only outside the
quotes; "3,5" is now one valid non-canonical tag → 412.
- Codex H2: RFC 7230 OWS is SP/HTAB only. string.Trim() also strips NBSP and other Unicode
whitespace, letting " * " masquerade as the "*" force-write escape. Trim only
(' ', '\t'); such input is now Malformed → 400.
- Fork nit: corrected the canonical-guard comment (interior-whitespace tags are rejected by
IsEtagc, not NumberStyles.None).
- CI Formatting gate: de-BOM the 8 touched legacy Application .cs (charset=utf-8, #311/#310).
- Tests: added comma-in-tag ("3,5", "x,y","3"), empty-element tolerance, NBSP-not-OWS,
trailing-junk, lowercase-weak, wildcard-in-list cases. Full ErsatzTV.Tests green (1556).
Refs #253#197
The shared optimistic-concurrency parser (ConcurrencyHeaders.ParseIfMatch) classified any
non-canonical/weak/list If-Match value as Malformed → 400. Per RFC 7232 §3.1 a syntactically
-valid entity-tag that simply doesn't strong-match must be 412; 400 is only for a genuine
grammar violation.
- Rewrite ParseIfMatch as a real RFC 7232 entity-tag/list parser: walks the comma-separated
1#entity-tag list, validates each [W/]DQUOTE *etagc DQUOTE member, and collects the strong
members whose opaque text is our canonical decimal. Weak / empty / non-canonical /
out-of-range tags are valid but contribute no version (→ empty set → 412); genuine grammar
violations (unquoted, SP-in-tag, unterminated, garbage) → 400.
- Reshape IfMatchCondition.ExpectedVersion : Option<int> → ExpectedVersions : Option<Seq<int>>
and VersionedAggregateExtensions.CheckVersion → set membership (any strong match proceeds;
empty set always 412). Threads through 10 replace/update commands + handlers + request
mappers + 9 controllers.
- No wire-contract change (400 + 412 already declared on every PUT; the field is header-derived
and internal — no DTO/route/response-type/OpenAPI change).
- Tests: ConcurrencyHeadersTests rewritten for the new classification (lists, weak, empty,
non-canonical → Version/empty-set; grammar violations → Malformed) + new
VersionedAggregateExtensionsTests for CheckVersion membership/empty-set/force-write.
- Docs: api-conventions.md §7a rewritten; decisions.md entry appended.
Refs #253#197fixes#265
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* initial work on maintaining playout state
* debugging wip
* fix refresh playout logic
* fix failing test
* more fixes
* update changelog
* comment out some debug logs
* comment out more logs