The as-built audit of design §10 (the deviation ledger) and §11 (the reference-ambiguity resolutions), and the collection point for deviations found outside a phase. It is the second of this
repository's two registers: first-release.md holds what the release must know, this file holds whether the design's claims are true of the code. Roadmap phase 11 created it
(design, plan,
checklist). A judgment, not a mechanical append: a row changes only with a verdict a human has read against the source.
The numbering belongs to design §10. Entries keep their numbers (a repaired entry is marked retired, never renumbered), and this file is renumbered with §10 in the same commit if §10 ever is.
LedgerAuditTests (tests/Dexpace.Sdk.Conformance.Tests/Gates/) fails a missing or extra number.
Audit commit. eb3facc, the merged phase 11 tip when the evidence below was re-derived: every path:line in this file is a line of that tree, checked to exist by the gate (rule L3), and the
later commits of the phase touch documents, the ledger gate, BannedSymbols.txt, two scripts and ci.yml only. A claim of the design that the source contradicts is corrected in design §10 by a dated
correction, never by editing the entry (P11-3). The review fixes made after that commit moved some cited lines; each citation they moved was re-derived against the tree of the fix, by hand, because rule L3 checks that a line exists and not what it holds.
The counting rule (P11-2). A clause counts against the roadmap's parity target ("at most five clauses unsatisfied or unmet on a stated domain, each argued") when the observable outcome the
clause protects is not delivered on some stated domain. A mechanism substitution, where the letter names a mechanism the port replaces and the protected outcome is delivered or strengthened, is
retained and argued below but not counted. A clause the specification sanctions changing (REDIR-25, BODY-8, SSE-19) is never counted, and neither is one the specification makes
unsatisfiable (AUTH-16/AUTH-17/AUTH-22, design §11 item 62), which is an ambiguity.
Unmet clauses: 4 of at most 5. They are AUTH-15 on a host whose crypto refuses MD5 (entry 16), PAGE-21 for percent-encoded unreserved characters (entry 18), TRANSPORT-14's malformed
header name (entry 32) and CFG-2 for a section read from a configuration other than the container's registered root (entry 25); docs/first-release.md Unsatisfied MUSTs lists the same four, and rule L6 holds the two registers and this line to one another.
The fourth was found by the review after the audit commit: the first count wrote entry 25 off as repaired, but the repair needs the providers of the root the section came from, which no public API reaches from an IConfigurationSection, so for a section of any
other configuration the observable outcome (an empty value does not hide the lower layer) is still not delivered, and the counting rule counts it.
Reading the tables. Status is one of as-built: confirmed (the code does what the entry says), as-built: narrowed (true, but broader or narrower than the code), as-built: contradicted → doc corrected
(the code does otherwise and the document is corrected) or retired (repaired, task n.m) (the code was changed so the departure no longer exists). Disposition is retain, repaired or doc-corrected,
with , unmet-clause: <ID> on a retained row that is one of the counted clauses and an optional (+…) naming a repair made on the way. Reason is a class from the closed set platform, security,
aot-trim, measured-cost, styleguide, spec-sanctioned, or —; "already built", "easier" and "a lot of work" are not reasons. Evidence is path:line citations and test names (Class.Method).
| # | Title | IDs touched | Status | Disposition | Reason | Evidence |
|---|---|---|---|---|---|---|
| 1 | Core takes a runtime dependency on the logging facade | SEAM-1, NFR-1 |
as-built: narrowed | retain (+repaired: the audit's allowance is a fixed set, task 1.3) | spec-sanctioned | The runtime closure is the facade 10.0.12 and DependencyInjection.Abstractions 10.0.12 only, no DiagnosticSource (src/Dexpace.Sdk.Core/packages.lock.json:23), not the wider net8.0 closure the entry measured; the audit allows exactly those two (scripts/ci/dependency-audit.cs:124) and fails a third (scripts/ci/dependency-audit-selftest.sh:98); the audit is a CI script, not a deps.json test (tests/Dexpace.Sdk.Core.Tests/Architecture/Seam1ArchitectureTests.cs:24 defers to it) |
| 2 | The DI package's hosting family counts as one library | NFR-2 |
as-built: confirmed | retain | platform | One third-party root, Microsoft.Extensions.Http, with a 13-package band-matched closure (src/Dexpace.Sdk.Extensions.DependencyInjection/packages.lock.json:23); the audit's policy record names it (scripts/ci/dependency-audit.cs:53) |
| 3 | The byte-stream provider seam is retired; its contract is not | SEAM-3..10, IO-30..36, IO-39, XCUT-23 |
as-built: confirmed | retain | platform | Every IO/ type is internal (src/Dexpace.Sdk.Core/IO/CapturedBytes.cs:22, src/Dexpace.Sdk.Core/IO/StreamCopy.cs:23, src/Dexpace.Sdk.Core/IO/TeeStream.cs:31); scan N1 finds no Source/Sink/Buffer vocabulary in src/ |
| 4 | End of stream is Stream.Read returning 0 |
IO-1, IO-2, IO-11, IO-15..18, BODY-10, BODY-25 |
as-built: confirmed | retain | platform | Every read asks for at least one byte (src/Dexpace.Sdk.Core/IO/StreamCopy.cs:45); a known length is checked (src/Dexpace.Sdk.Core/IO/StreamCopy.cs:104); ZeroCountReadRuleTests (tests/Dexpace.Sdk.Core.Tests/IO/ZeroCountReadRuleTests.cs:20) scans for a zero-count read |
| 5 | One stream-ownership rule for request bodies | BODY-8, IO-6 |
as-built: confirmed | retain | spec-sanctioned | A request body closes what it opened (src/Dexpace.Sdk.Core/Http/Request/FileRequestBody.cs:101); FromStream never disposes the caller's stream (src/Dexpace.Sdk.Core/Http/Request/RequestBody.cs:360); the logging tee leaves the destination open (src/Dexpace.Sdk.Core/Http/Request/LoggingRequestBody.cs:92); ResponseBody.FromStream takes ownership (src/Dexpace.Sdk.Core/Http/Response/ResponseBody.cs:218) |
| 6 | A second open throws instead of returning the same handle | BODY-14, HTTP-41 |
retired (repaired, task 3.1) | repaired | — | A second open returns the first handle in both forms (src/Dexpace.Sdk.Core/Http/Response/ResponseBody.cs:331, src/Dexpace.Sdk.Core/Http/Response/ResponseBody.cs:272, src/Dexpace.Sdk.Http.SystemNet/HttpResponseMessageBody.cs:45); an open after dispose throws StreamClosedException; ResponseBodyLifecycleTests.A_second_open_returns_the_same_stream, HttpResponseMessageBodyTests.A_second_open_returns_the_same_stream; the transport's handle reports its own disposal, so a second GetError* over a wire response throws ResponseNotReadException (ResponseBodyReopenWireTests, src/Dexpace.Sdk.Http.SystemNet/DisposalAwareStream.cs:19); the first open runs under one gate for both forms, so concurrent first opens are handed one stream (HttpResponseMessageBodyOpenRaceTests, ResponseBodyReopenWireTests.Concurrent_first_opens_over_real_responses_agree). The logging wrapper keeps its own BODY-24 throw for a read past a finished capture (src/Dexpace.Sdk.Core/Http/Response/LoggingResponseBody.cs:353), which is that clause, not this one |
| 7 | Transport failures are not members of an I/O-error family | TRANSPORT-20, XCUT-4, RETRY-2, RETRY-4, RECOV-17 |
retired (repaired, task 2.1) | repaired | — | ServiceRequestException derives from IOException and keeps IRetryableError (src/Dexpace.Sdk.Core/Errors/TransportExceptions.cs:23, src/Dexpace.Sdk.Core/Errors/TransportExceptions.cs:56); the mapping chains the native exception (src/Dexpace.Sdk.Http.SystemNet/SendFailures.cs:54) and names an ended response truthfully (src/Dexpace.Sdk.Http.SystemNet/SendFailures.cs:86); the classifier reads IRetryableError and the cause chain (src/Dexpace.Sdk.Core/Resilience/RetryFacts.cs:106); TransportExceptionTaxonomyTests.ServiceRequestException_is_an_IOException; ServiceResponseException is removed (scan N3) |
| 8 | Cancellation is a cooperative token; interrupts have no counterpart | TRANSPORT-3, XCUT-1, ASYNC-3, ASYNC-4, ASYNC-14, SEAM-14, SEAM-18, PIPE-33, PIPE-34, XCUT-13, CFG-20, CFG-21 |
as-built: narrowed | retain (+repaired: AsAsync faults on rejection, WaitOrDisposeAsync, tasks 2.3 and 2.4) |
platform, security | The bridge takes a scheduler with no default (src/Dexpace.Sdk.Core/Client/HttpClientExtensions.cs:103), faults the task when the scheduler rejects the work (src/Dexpace.Sdk.Core/Client/HttpClientExtensions.cs:171) and returns a cancelled task for a signalled token (src/Dexpace.Sdk.Core/Client/HttpClientExtensions.cs:158); the cancel-without-interruption mode is the public WaitOrDisposeAsync (src/Dexpace.Sdk.Core/Client/ResponseTaskExtensions.cs:42, src/Dexpace.Sdk.Core/Internal/LateResult.cs:42); Thread.Interrupt is banned (BannedSymbols.txt:10); the transport replaces the native exception when a deadline is armed or the cancel is internal (src/Dexpace.Sdk.Http.SystemNet/SendFailures.cs:54), so "rethrown untouched" is narrower than the entry says; SyncToAsyncBridgeTests.A_scheduler_that_rejects_the_work_faults_the_returned_task_instead_of_throwing, WaitOrDisposeTests.Every_caller_gets_a_live_response_or_a_cancellation_and_every_produced_response_is_closed_once; scan N4 |
| 9 | No provider registry or auto-discovery: explicit construction, plus single-registration validation | SEAM-5..10, XCUT-23 |
as-built: narrowed | retain | aot-trim | Identity-checked slots throw at UseTransport/UseSerde (src/Dexpace.Sdk.Extensions.DependencyInjection/ServiceSlot.cs:25) and the validator scans the keyed descriptors (src/Dexpace.Sdk.Extensions.DependencyInjection/DexpaceClientValidator.cs:34); TryAdd is used nowhere, so "TryAdd gives SEAM-6" is the document's error; scan N2 finds no discovery |
| 10 | No builder objects beyond the multimap builders, and no IBuilder<T> |
SEAM-29, HTTP-2, HTTP-3, HTTP-4 |
as-built: narrowed | retain (+repaired: validating accessors name their field, task 2.5) | styleguide | The public *Builder types are four, none of them a model builder (src/Dexpace.Sdk.Core/Pipeline/PipelineBuilder.cs:36, src/Dexpace.Sdk.Extensions.DependencyInjection/DexpaceClientBuilder.cs:22, plus Headers.Builder and Query.Builder); a validating accessor throws with ParamName set to the property (src/Dexpace.Sdk.Core/Operations/OperationDescriptor.cs:43); InitAccessorParamNameTests.Every_validating_accessor_in_the_libraries_names_its_property; scan N5. Open: about fifteen range and format checks inside accessors still report ParamName value |
| 11 | Reflection and open body subclassing can bypass construction-time validation | HTTP-2, SEAM-29, XCUT-15 |
as-built: confirmed | retain | platform | The wire re-checks every header (src/Dexpace.Sdk.Http.SystemNet/OutboundHeaderMapper.cs:194) and the method token (src/Dexpace.Sdk.Http.SystemNet/OutboundHeaderMapper.cs:76) because TryAddWithoutValidation writes CR/LF raw |
| 12 | The orchestrator catches every non-fatal exception | RECOV-2, RECOV-8, RETRY-25, PIPE-30 |
as-built: contradicted → doc corrected | doc-corrected (+repaired: two catch-alls swallowed OutOfMemoryException, task 1.4) |
platform | The filter sits at six recovery sites (src/Dexpace.Sdk.Core/Recovery/RecoveryDispatcher.cs:156, src/Dexpace.Sdk.Core/Recovery/ResponseRecoveryChain.cs:127) among many filtered catch-alls, not "exactly four"; two catch-alls in UrlRedactor (src/Dexpace.Sdk.Core/Diagnostics/UrlRedactor.cs:100) had no fatal filter; FatalFilterArchitectureTests.No_catch_all_in_a_runtime_library_swallows_a_fatal_exception (tests/Dexpace.Sdk.Core.Tests/Architecture/FatalFilterArchitectureTests.cs:16) now holds every catch-all; scan N20 |
| 13 | Suppressed exceptions are an SDK-owned trail, stored in Exception.Data on foreign exceptions |
RECOV-12, RETRY-34, PAGE-13, SSE-29, SSE-36 |
as-built: confirmed | retain | platform | ExceptionTrail.AddSuppressed and GetSuppressed (src/Dexpace.Sdk.Core/Errors/ExceptionTrail.cs:28, src/Dexpace.Sdk.Core/Errors/ExceptionTrail.cs:67); ExceptionTrailTests.A_secondary_added_to_a_foreign_exception_lands_in_GetSuppressed; scan N6 finds no AggregateException |
| 14 | The async standard pipeline follows redirects, as the sync one does | PIPE-32, REDIR-25 |
as-built: confirmed | retain | spec-sanctioned | One RedirectPolicy in the preset (src/Dexpace.Sdk.Core/Pipeline/PipelineBuilder.cs:222); the SDK-built transport turns the runtime's follow off (src/Dexpace.Sdk.Http.SystemNet/SystemNetHttpClient.cs:516) |
| 15 | No cross-origin marker: redirect and auth compare against the immutable seed origin | REDIR-11, AUTH-29, REDIR-8, XCUT-17 |
as-built: confirmed | retain | security | The seed origin is taken once (src/Dexpace.Sdk.Core/Pipeline/Policies/Redirect/RedirectChain.cs:25); the authorization policy judges against it (src/Dexpace.Sdk.Core/Pipeline/Policies/AuthorizationPolicy.cs:154); the one PipelinePropertyKey left is the idempotency key (src/Dexpace.Sdk.Core/Pipeline/Policies/IdempotencyPolicy.cs:42); scan N7 |
| 16 | Digest MD5 only where the platform crypto provides it | AUTH-15, AUTH-16 |
as-built: confirmed | retain, unmet-clause: AUTH-15 (+repaired: the probe treats any non-fatal failure as refused, task 5.8) | security | One probe per process (src/Dexpace.Sdk.Core/Auth/Md5Availability.cs:22) that counts every non-fatal failure as a refusal (src/Dexpace.Sdk.Core/Auth/Md5Availability.cs:31); the handler drops MD5 from its preference (src/Dexpace.Sdk.Core/Auth/DigestChallengeHandler.cs:94); Md5AvailabilityTests.An_unexpected_probe_failure_means_refused; scan N8 finds no vendored MD5 |
| 17 | Pages are materialized values; no page owns a live response | PAGE-1, PAGE-3, PAGE-12, PAGE-15, PAGE-34 |
as-built: narrowed | retain (+repaired: Page copies its values, task 3.5) |
platform | The step closes the response before it yields (src/Dexpace.Sdk.Core/Pagination/PageStep.cs:30, src/Dexpace.Sdk.Core/Pagination/FetcherPageable.cs:28); Page stores an immutable snapshot of the list it is given (src/Dexpace.Sdk.Core/Pagination/Page.cs:32); PageTests.Values_is_a_snapshot_of_the_list_passed_in |
| 18 | The query splice is verbatim relative to System.Uri's canonical form |
PAGE-21, PAGE-24 |
as-built: narrowed | retain, unmet-clause: PAGE-21 (+repaired: empty segments and the Uri check, tasks 3.4 and 3.6) |
security | The splice copies an empty segment (src/Dexpace.Sdk.Core/Pagination/QuerySplice.cs:115), QuerySpliceTests.Stray_empty_segments_are_copied_verbatim; Request refuses a Uri built without canonicalization (src/Dexpace.Sdk.Core/Http/Request/Request.cs:218), HeaderInjectionValidationTests.A_uri_created_without_canonicalization_is_rejected; the residual is RFC 3986 §6.2.2.2 equivalence of unreserved characters |
| 19 | One pull-based paging engine: no executor mode and no rejection path | PAGE-29, PAGE-30 |
as-built: narrowed | retain | platform | One engine (src/Dexpace.Sdk.Core/Pagination/StrategyPageable.cs:20) and no scheduler in any factory; a consumer with no SynchronizationContext resumes inline, so await Task.Yield() is the way to move; scan N9 |
| 20 | SSE lines longer than a cap fail the read | SSE-19 |
as-built: confirmed | retain (+repaired: the per-event cap closes residual R1, task 5.6) | spec-sanctioned | A line over src/Dexpace.Sdk.Core/ServerSentEvents/ServerSentEventReader.cs:50 fails with src/Dexpace.Sdk.Core/ServerSentEvents/ServerSentEventLineTooLongException.cs:32; an event whose data passes src/Dexpace.Sdk.Core/ServerSentEvents/ServerSentEventReader.cs:56 fails with src/Dexpace.Sdk.Core/ServerSentEvents/ServerSentEventTooLargeException.cs:26 (src/Dexpace.Sdk.Core/ServerSentEvents/EventAccumulator.cs:107); ServerSentEventLineCapTests.Data_lines_that_each_fit_but_never_end_the_event_fail_once_the_event_cap_is_passed |
| 21 | Tristate<T> is an invariant struct, so the covariance SHOULD cannot be met |
SERDE-14 |
as-built: confirmed | retain | platform | src/Dexpace.Sdk.Core/Serialization/TristateOfT.cs:29; variance exists only on interfaces and delegates |
| 22 | No structured log-event object; ILogger delivers the guarantees |
OBS-1, OBS-3, OBS-4, OBS-5, OBS-8, OBS-9, OBS-40 |
as-built: narrowed | retain (+repaired: a null field is the literal null; the plain-name events use the generator, tasks 5.2 and 5.3) |
platform | The header-bearing events go through a record that maps a null value to null (src/Dexpace.Sdk.Core/Diagnostics/HttpLogRecord.cs:35, src/Dexpace.Sdk.Core/Diagnostics/HttpLogEmitter.cs:221); the fixed-key events keep LoggerMessage.Define for dotted keys (src/Dexpace.Sdk.Core/Diagnostics/RedirectLog.cs:28) and the plain-name ones use the generator (src/Dexpace.Sdk.Http.SystemNet/TransportLog.cs:20); HttpLogRecordTests.A_null_value_is_emitted_as_the_literal_null; DisabledPathAllocationTests (tests/Dexpace.Sdk.Core.Tests/Pipeline/Policies/DisabledPathAllocationTests.cs:27) |
| 23 | Tracing is System.Diagnostics.Activity; its no-op is null |
OBS-25, OBS-21, OBS-23, OBS-10, OBS-28 |
as-built: narrowed | retain (+repaired: Disposal writes only to a recording activity, task 1.5) |
platform | Every SDK tag, event and status write is guarded (src/Dexpace.Sdk.Core/Diagnostics/OperationTelemetry.cs:43), and so is the one that was not (src/Dexpace.Sdk.Core/Internal/Disposal.cs:106); the context carries an optional activity (src/Dexpace.Sdk.Core/Execution/InstrumentationContext.cs:73); DisposalTests.A_suppressed_dispose_failure_is_not_recorded_on_a_non_recording_activity |
| 24 | Only the W3C trace-id flavour and the invalid sentinel | OBS-27 |
retired (repaired, task 5.1) | repaired | — | TraceIdGeneration.W3C, Datadog and FormatDatadog, each re-drawing a zero draw (src/Dexpace.Sdk.Core/Diagnostics/TraceIdGeneration.cs:34); the SDK still never sets the process hook (BannedSymbols.txt:130); TraceIdGenerationTests.Datadog_ids_have_a_zero_high_half_a_non_zero_low_half_and_render_as_a_decimal_ulong |
| 25 | Layered lookup is Microsoft.Extensions.Configuration in the DI package |
CFG-1..4, CFG-10, CFG-13, CFG-14, CFG-24 |
as-built: narrowed | retain, unmet-clause: CFG-2 (+repaired: an empty value falls through for a section of the registered configuration, task 5.4) | platform, styleguide | When the registered configuration is the root that holds the section, the reader walks its providers from the top and takes the first non-blank value (src/Dexpace.Sdk.Extensions.DependencyInjection/ConfigSection.cs:51, src/Dexpace.Sdk.Extensions.DependencyInjection/DexpaceClientStateSetup.cs:27, src/Dexpace.Sdk.Extensions.DependencyInjection/ConfigurationReader.cs:45); EmptyValueTests.An_empty_value_falls_through_to_the_lower_provider; for a section of any other configuration the merged read stands, where an empty value in the highest provider hides the lower one (src/Dexpace.Sdk.Extensions.DependencyInjection/ConfigSection.cs:84), EmptyValueTests.A_section_from_an_unregistered_configuration_keeps_the_merged_read; no property tier, raw accessor or global slot (scan N10) |
| 26 | Unparseable configuration fails at startup instead of falling back | CFG-5, CFG-6, CFG-7, OBS-35 |
as-built: confirmed | retain | styleguide | The grammars never throw and keep the default (src/Dexpace.Sdk.Extensions.DependencyInjection/ScalarGrammar.cs:87, src/Dexpace.Sdk.Extensions.DependencyInjection/RecordBinder.cs:73); a separate validator fails ValidateOnStart (src/Dexpace.Sdk.Extensions.DependencyInjection/DexpaceClientRegistrations.cs:27). Reclassified (P11-10): the accessor letter is met, and the boot failure is a stricter host rule |
| 27 | Public API keeps the I prefix and the Async suffix |
— | as-built: confirmed | retain | styleguide | src/Dexpace.Sdk.Core/Client/IAsyncHttpClient.cs:53; src/Dexpace.Sdk.Core/Client/HttpClientExtensions.cs:43 (a sync Execute ships beside it) |
| 28 | CA1062 is dialled to none |
— | retired (repaired, task 1.6) | repaired | — | CA1062 is error for src/ (.editorconfig:148) and relaxed for applications only (.editorconfig:138); its one finding was a real null dereference in src/Dexpace.Sdk.Core/Errors/TransportExceptions.cs:83, now guarded; HttpResponseExceptionTests.The_constructor_rejects_a_null_response |
| 29 | A URL error carries the redacted input | HTTP-47, OBS-11, XCUT-19 |
as-built: confirmed | retain | security | src/Dexpace.Sdk.Core/Http/Request/Request.cs:238 |
| 30 | An absent response body is an empty replayable body | HTTP-6 |
as-built: confirmed | retain | styleguide | src/Dexpace.Sdk.Core/Http/Response/Response.cs:66; a mechanism, not a departure |
| 31 | "Regular file" is enforced only where the platform can see it | BODY-11, HTTP-40 |
retired (repaired, task 3.3) | repaired | — | A zero-length FromFile on Unix is classified by TarWriter and a FIFO, device or socket is refused (src/Dexpace.Sdk.Core/Http/Request/RequestBody.cs:313, src/Dexpace.Sdk.Core/IO/UnixFileKind.cs:33); FileRequestBodyTests.A_character_device_is_rejected_at_construction, FileRequestBodyTests.An_empty_regular_file_the_process_may_not_read_is_accepted (the writer opens a regular file, so a refusal to open reads as regular), UnixFileKindTests; the classification is verified on Linux only |
| 32 | A malformed inbound header name fails the whole response | TRANSPORT-14 |
as-built: contradicted → doc corrected | retain, unmet-clause: TRANSPORT-14 | security | A filter could drop the line, but only by parsing the head ahead of the runtime's parser, so the entry's "only workaround is replacing the parser" is corrected and the clause is retained for a different reason; the value half is met (src/Dexpace.Sdk.Core/Http/Common/Headers.cs:427); the driver carries the permanent waiver (tests/Dexpace.Sdk.Http.SystemNet.Tests/Conformance/SystemNetSubject.cs:139, tests/Dexpace.Sdk.Http.SystemNet.Tests/Conformance/SystemNetSubject.cs:141) |
| 33 | SocketsHttpHandler re-sends a body-less idempotent request |
TRANSPORT-2 |
retired (repaired, task 4.1) | repaired | — | A pass-through PlaintextStreamFilter turns "the connection ended before any response byte" into an I/O failure (src/Dexpace.Sdk.Http.SystemNet/FirstResponseByteStream.cs:54, src/Dexpace.Sdk.Http.SystemNet/SystemNetHttpClient.cs:521); the zero-length content for body-less POST/PUT/PATCH stays (src/Dexpace.Sdk.Http.SystemNet/OutboundHeaderMapper.cs:143); BodylessRetryWireTests.A_bodyless_request_reaches_the_server_once_on_an_sdk_built_handler; a caller's own handler keeps its native retry, BodylessRetryWireTests.A_caller_handler_keeps_its_native_retry. Not claimed: "never twice". A server close that lands after the runtime's reuse check of a pooled connection and before the first write of the request completes the idle read with 0 while the filter is not armed, and the runtime may re-send once on a new connection, a request that was written to an already-closed connection (reasoned from how the runtime reuses a pooled connection, not reproduced; docs/sdk-documentation/transport.md, asymmetry idle-connection-native-retry) |
Every deviation that survives phase 11, in the order of §10, each with what was verified against the source and why it stays. The six entries whose departure is gone (6, 7, 24, 28, 31, 33) have no block: the audit row above is their record. A clause counted against the parity target says so in its first line.
Verified. The closure is the facade and DependencyInjection.Abstractions, both band-matched (src/Dexpace.Sdk.Core/packages.lock.json:23), and the audit fails any third package (scripts/ci/dependency-audit.cs:124, scripts/ci/dependency-audit-selftest.sh:98). Not counted: SEAM-1's own appendix-C row allows "a logging facade".
Why it stays. NuGet has no compile-only scope for an assembly that is called at run time, and every .NET host and sink already speaks ILogger. Conforming would take ILogger off core's public surface and move logging into an adapter every consumer must add.
Verified. One root, Microsoft.Extensions.Http, and a closure of one publisher's lock-stepped family (src/Dexpace.Sdk.Extensions.DependencyInjection/packages.lock.json:23, scripts/ci/dependency-audit.cs:53).
Why it stays. NFR-2 is a SHOULD and splitting the family does not shrink the closure; it would hand the consumer four packages to wire one feature.
Verified. Every IO/ type is internal (src/Dexpace.Sdk.Core/IO/CapturedBytes.cs:22, src/Dexpace.Sdk.Core/IO/StreamCopy.cs:23, src/Dexpace.Sdk.Core/IO/TeeStream.cs:31) and scan N1 finds no provider vocabulary.
Why it stays. Stream, Memory<byte> and ArrayPool<byte> are in the shared framework, so there is nothing to plug in. Conforming would be an empty registry with one candidate. A mechanism substitution; not counted.
Verified. Core issues no zero-count read (src/Dexpace.Sdk.Core/IO/StreamCopy.cs:45) and checks a known length (src/Dexpace.Sdk.Core/IO/StreamCopy.cs:104); tests/Dexpace.Sdk.Core.Tests/IO/ZeroCountReadRuleTests.cs:20 holds it.
Why it stays. The Stream contract defines 0 as the end, and a zero-count request also returns 0, so the spec's separate sentinel cannot exist. A mechanism substitution; not counted.
Verified. src/Dexpace.Sdk.Core/Http/Request/FileRequestBody.cs:101, src/Dexpace.Sdk.Core/Http/Request/RequestBody.cs:360, src/Dexpace.Sdk.Core/Http/Request/LoggingRequestBody.cs:92, src/Dexpace.Sdk.Core/Http/Response/ResponseBody.cs:218.
Why it stays. BODY-8 itself asks the port to decide: "A port MUST decide its stream-ownership/close rule deliberately". A sanctioned choice; never counted.
Verified. src/Dexpace.Sdk.Core/Client/HttpClientExtensions.cs:103, src/Dexpace.Sdk.Core/Client/HttpClientExtensions.cs:171, src/Dexpace.Sdk.Core/Client/HttpClientExtensions.cs:158, src/Dexpace.Sdk.Core/Client/ResponseTaskExtensions.cs:42, BannedSymbols.txt:10; the transport's replacement of the native exception is src/Dexpace.Sdk.Http.SystemNet/SendFailures.cs:54.
Why it stays. Thread.Abort is unsupported on .NET, Thread.Interrupt interrupts arbitrary lock waits inside library code, and the token aborts the socket operation. The spec's own residual, a blocking call that ignores interrupts running to completion, is identical here. Conforming risks state corruption from interrupting foreign waits. A mechanism substitution; not counted.
Verified. src/Dexpace.Sdk.Extensions.DependencyInjection/ServiceSlot.cs:25, src/Dexpace.Sdk.Extensions.DependencyInjection/DexpaceClientValidator.cs:34; scan N2.
Why it stays. Assembly scanning and [ModuleInitializer] are invisible to ILLink and NativeAOT (NFR-8), so a trimmed or AOT application would silently lose providers.
Verified. Four public *Builder types, none a model builder (src/Dexpace.Sdk.Core/Pipeline/PipelineBuilder.cs:36, src/Dexpace.Sdk.Extensions.DependencyInjection/DexpaceClientBuilder.cs:22, Headers.Builder, Query.Builder); scan N5 allow-lists them.
Why it stays. Records, with and validating constructors are the language's builder (styleguide 06), and required makes a missing field a compile error. Conforming adds a parallel mutable type per model and a generic contract with no implementer.
Verified. src/Dexpace.Sdk.Http.SystemNet/OutboundHeaderMapper.cs:194, src/Dexpace.Sdk.Http.SystemNet/OutboundHeaderMapper.cs:76.
Why it stays. GetUninitializedObject cannot be closed from inside a library, so the wire re-check is the defence and is mandatory.
Verified. src/Dexpace.Sdk.Core/Recovery/RecoveryDispatcher.cs:156, src/Dexpace.Sdk.Core/Recovery/ResponseRecoveryChain.cs:127, src/Dexpace.Sdk.Core/Diagnostics/UrlRedactor.cs:100, tests/Dexpace.Sdk.Core.Tests/Architecture/FatalFilterArchitectureTests.cs:16.
Why it stays. StackOverflowException is uncatchable and AccessViolationException is not delivered on .NET, so no port can catch them.
Verified. src/Dexpace.Sdk.Core/Errors/ExceptionTrail.cs:28, src/Dexpace.Sdk.Core/Errors/ExceptionTrail.cs:67; scan N6.
Why it stays. .NET has no suppressed list, and AggregateException changes the thrown type and breaks RECOV-10's "rethrow unchanged"; callers' typed catches would stop matching.
Verified. src/Dexpace.Sdk.Core/Pipeline/PipelineBuilder.cs:222, src/Dexpace.Sdk.Http.SystemNet/SystemNetHttpClient.cs:516.
Why it stays. REDIR-25 lets callers install their own async redirect step and asks that the asymmetry be preserved or explicitly documented if changed; one policy implementation serves both paths, and the single-layer invariant is the transport's. A sanctioned change; not counted.
Verified. src/Dexpace.Sdk.Core/Pipeline/Policies/Redirect/RedirectChain.cs:25, src/Dexpace.Sdk.Core/Pipeline/Policies/AuthorizationPolicy.cs:154, src/Dexpace.Sdk.Core/Pipeline/Policies/IdempotencyPolicy.cs:42; scan N7.
Why it stays. A forgeable or leakable marker reintroduces the trap REDIR-11 itself flags, and a pipeline with no auth step would forward it. Stronger than the letter; not counted.
Verified. src/Dexpace.Sdk.Core/Auth/Md5Availability.cs:22, src/Dexpace.Sdk.Core/Auth/Md5Availability.cs:31, src/Dexpace.Sdk.Core/Auth/DigestChallengeHandler.cs:94; scan N8.
Why it stays. On a FIPS host the refusal is the operator's compliance control, and a vendored MD5 over the password would silently take the deployment out of its boundary. The SDK already refuses to override a host hook (TraceIdGenerator). The mapping has never run on a real FIPS host. Conforming makes FIPS deployments non-compliant without anyone knowing. Counted: AUTH-15.
Verified. src/Dexpace.Sdk.Core/Pagination/PageStep.cs:30, src/Dexpace.Sdk.Core/Pagination/FetcherPageable.cs:28, src/Dexpace.Sdk.Core/Pagination/Page.cs:32.
Why it stays. An abandoned async iterator never runs its finally, so a page holding a response strands a connection for a hand-driven enumerator. Stronger than the letter; not counted.
Verified. src/Dexpace.Sdk.Core/Pagination/QuerySplice.cs:115, src/Dexpace.Sdk.Core/Http/Request/Request.cs:218.
Why it stays. The only byte-preserving API disables Uri's validation, which puts CR/LF and the fragment on the wire. The residual is RFC 3986 §6.2.2.2 equivalence of unreserved characters. Conforming adds an injection surface and rewrites every GetComponents call. Counted: PAGE-21.
Verified. src/Dexpace.Sdk.Core/Pagination/StrategyPageable.cs:20; scan N9.
Why it stays. A pull iterator invokes no callback, so an executor has nothing to schedule and await Task.Yield() moves the consumer. A mechanism substitution; not counted.
Verified. src/Dexpace.Sdk.Core/ServerSentEvents/ServerSentEventReader.cs:50, src/Dexpace.Sdk.Core/ServerSentEvents/ServerSentEventReader.cs:56, src/Dexpace.Sdk.Core/ServerSentEvents/EventAccumulator.cs:107, src/Dexpace.Sdk.Core/ServerSentEvents/ServerSentEventTooLargeException.cs:26, src/Dexpace.Sdk.Core/ServerSentEvents/ServerSentEventLineTooLongException.cs:32.
Why it stays. SSE-19's chapter text says "a port MAY add a configurable cap and reject/truncate oversized lines, documenting the divergence"; an unbounded line or event from a hostile server is an unbounded allocation. A sanctioned divergence; never counted. (The design's retained table omitted this entry; the audit adds it because the divergence survives.)
Verified. src/Dexpace.Sdk.Core/Serialization/TristateOfT.cs:29.
Why it stays. Variance exists only on interfaces and delegates, and a class would make default null, breaking SERDE-17.
Verified. src/Dexpace.Sdk.Core/Diagnostics/HttpLogRecord.cs:35, src/Dexpace.Sdk.Core/Diagnostics/HttpLogEmitter.cs:221, src/Dexpace.Sdk.Core/Diagnostics/RedirectLog.cs:28, src/Dexpace.Sdk.Http.SystemNet/TransportLog.cs:20, tests/Dexpace.Sdk.Core.Tests/Pipeline/Policies/DisabledPathAllocationTests.cs:27.
Why it stays. The logging convergence already gives a zero-allocation disabled path; a second event model would sit beside every host's. The dotted keys the specification fixes cannot be generator placeholders (SYSLIB1014), which is the one departure from the styleguide's generator rule that remains.
Verified. src/Dexpace.Sdk.Core/Diagnostics/OperationTelemetry.cs:43, src/Dexpace.Sdk.Core/Internal/Disposal.cs:106, src/Dexpace.Sdk.Core/Execution/InstrumentationContext.cs:73.
Why it stays. OpenTelemetry .NET's tracing API is Activity; a shim would not be subscribed by the OTel SDK.
25. No system-property tier, raw accessor or global slot; a blank value shadows in a section of an unregistered configuration (counted)
Verified. src/Dexpace.Sdk.Extensions.DependencyInjection/ConfigSection.cs:51, src/Dexpace.Sdk.Extensions.DependencyInjection/DexpaceClientStateSetup.cs:27; scan N10.
Why it stays. .NET has no system properties, and a global slot is the service locator the styleguide rejects. The CFG-2 fall-through is repaired for a section of the container's registered IConfiguration, and stays unmet for a section of any other: the fall-through walks the providers of the root the section was taken from, IConfigurationSection exposes no way to that root (ConfigurationSection keeps it private, and reading it by reflection is not AOT-safe, NFR-8), so the reader walks the registered root's providers when that root's slice at the section's path holds the same pairs as the section, and otherwise reads the merged view. Conforming would need a public API that takes the root (an AddDexpaceClient overload over an IConfiguration and a path, a post-v1 candidate) or a reflection read the trimmed and AOT builds cannot do. Counted: CFG-2 (the section of an unregistered configuration; register the root the section is read from).
Verified. src/Dexpace.Sdk.Extensions.DependencyInjection/ScalarGrammar.cs:87, src/Dexpace.Sdk.Extensions.DependencyInjection/RecordBinder.cs:73, src/Dexpace.Sdk.Extensions.DependencyInjection/DexpaceClientRegistrations.cs:27.
Why it stays. The accessor letter of CFG-5 to CFG-7 is met (the default is kept, nothing throws); csharp-aspnetcore 1.4 requires ValidateOnStart on every bound options type. Conforming would let Transport:Proxy:BypassAll=on silently route through the proxy and Logging:Level=Header silently log nothing. Reclassified from an unmet clause to a stricter host rule (P11-10); not counted.
Verified. src/Dexpace.Sdk.Core/Client/IAsyncHttpClient.cs:53, src/Dexpace.Sdk.Core/Client/HttpClientExtensions.cs:43.
Why it stays. The Framework Design Guidelines outrank the vendored guide for a public NuGet surface; a prefix-less HttpClient collides with System.Net.Http.HttpClient.
Verified. src/Dexpace.Sdk.Core/Http/Request/Request.cs:238.
Why it stays. A userinfo password or a query token must not reach an exception message that is logged and surfaced (XCUT-19).
Verified. src/Dexpace.Sdk.Core/Http/Response/Response.cs:66.
Why it stays. Consumers never branch on a missing body (styleguide 03: prefer an empty object to null). A mechanism, not a departure.
Verified. tests/Dexpace.Sdk.Http.SystemNet.Tests/Conformance/SystemNetSubject.cs:139, tests/Dexpace.Sdk.Http.SystemNet.Tests/Conformance/SystemNetSubject.cs:141, src/Dexpace.Sdk.Core/Http/Common/Headers.cs:427.
Why it stays. The only repair is a filter that parses the HTTP/1.1 head ahead of the runtime's parser on every response (1xx heads, folded lines, lenient line endings); a defect there corrupts all traffic, to tolerate a non-compliant server. Contrast entry 33's filter, which never inspects or alters a byte. Conforming would put SDK-owned parsing in front of every response head. Counted: TRANSPORT-14's name half. The trigger to revisit is a consumer whose server sends such names.
One row per item of §11, 1 to 72 (items 69 to 72 are the phase 11 additions, written into §11 by its dated correction). Verdict is confirmed, narrowed, contradicted or added;
Disposition is retain, doc-corrected, repaired or added, with an optional (+…). A reading is not a departure: the letter of the specification is met or the clause has no subject, and the
item says how the port read it.
| Item | IDs | Verdict | Disposition | Evidence |
|---|---|---|---|---|
| 1 | CFG-15, RETRY-26 |
confirmed | retain | Waits hold no thread and long ones are chunked (src/Dexpace.Sdk.Core/Configuration/TimeProviderWaits.cs:69) |
| 2 | NFR-11, SEAM-17 |
narrowed | doc-corrected | No framework leaks (tests/Dexpace.Sdk.Core.Tests/Architecture/Nfr11PublicSurfaceArchitectureTests.cs:19); two documented sync-over-async defaults remain (src/Dexpace.Sdk.Core/Pipeline/HttpPipelinePolicy.cs:63, src/Dexpace.Sdk.Core/Auth/TokenCredential.cs:47) |
| 3 | SEAM-2 |
confirmed | retain | Retiring a seam reads SEAM-2 as constraining a retained concern; as entry 3 (src/Dexpace.Sdk.Core/IO/CapturedBytes.cs:22) |
| 4 | CFG-1 |
narrowed | doc-corrected | The DI package reads the caller's section; tier order is the host's provider order, and the Configure transforms are the first tier (src/Dexpace.Sdk.Extensions.DependencyInjection/DexpaceClientRegistration.cs:33) |
| 5 | SEAM-30 |
confirmed | retain | An orphaned response is closed once and never surfaces (tests/Dexpace.Sdk.Core.Tests/Client/SyncToAsyncBridgeTests.cs:265) |
| 6 | HTTP-2, SEAM-29 |
confirmed | retain | Constructor privacy is compiler-enforced (src/Dexpace.Sdk.Core/Http/Common/Method.cs:31, tests/Dexpace.Sdk.Core.Tests/Architecture/ModelConstructionArchitectureTests.cs:19) |
| 7 | BODY-3 |
confirmed | retain | The compare-and-set is Interlocked (src/Dexpace.Sdk.Core/Http/Request/RequestBody.cs:450) |
| 8 | XCUT-23 |
confirmed | retain | The package detects duplicate registrations itself (src/Dexpace.Sdk.Extensions.DependencyInjection/DexpaceClientValidator.cs:34, tests/Dexpace.Sdk.Extensions.DependencyInjection.Tests/SingleRegistrationTests.cs:15) |
| 9 | — | confirmed | retain | Appendix B is a weaker claim; the report is the worst-of view (src/Dexpace.Sdk.Conformance/Reporting/ConformanceReport.cs:61) |
| 10 | RETRY-38, RETRY-45 |
contradicted | doc-corrected (+repaired: the attempt header is set and may not be Idempotency-Key, task 5.7) |
Built, not deferred (src/Dexpace.Sdk.Core/Configuration/RetryOptions.cs:225); the stamp replaces (src/Dexpace.Sdk.Core/Resilience/RetryEngine.cs:134); RetryEngineTests.The_attempt_header_replaces_its_previous_value, RetryOptionsTests.AttemptHeaderName_rejects_the_idempotency_key_name; a configured idempotency header of the same name is refused where each stack is assembled (src/Dexpace.Sdk.Core/Resilience/AttemptHeaderCollision.cs:30, src/Dexpace.Sdk.Core/Pipeline/PipelineBuilder.cs:309), AttemptHeaderKeyCollisionTests |
| 11 | CTX-16, CTX-20 |
confirmed | retain | Embedded MUSTs of shipped SHOULD features are met (tests/Dexpace.Sdk.Core.Tests/Execution/InstrumentationContextTests.cs:19, tests/Dexpace.Sdk.Core.Tests/Execution/PromotionChainTests.cs:19) |
| 12 | BODY-8, RECOV-14, RETRY-34, AUTH-31 |
narrowed | doc-corrected | One control flow per policy; the credential hooks are sync/async twins (src/Dexpace.Sdk.Core/Pipeline/Policies/BearerTokenAuthPolicy.cs:86) |
| 13 | — | confirmed | retain | Citation hygiene: no code or test cites HTTP-16-body |
| 14 | SEAM-6, SEAM-8 |
narrowed | doc-corrected | ServiceSlot, not TryAdd, gives the idempotence (src/Dexpace.Sdk.Extensions.DependencyInjection/ServiceSlot.cs:25) |
| 15 | CFG-34, SERDE-11 |
narrowed | doc-corrected | The deep-equality helper compares zero sign explicitly (src/Dexpace.Sdk.Core/Internal/DeepValue.cs:29) and has no production caller |
| 16 | PIPE-32 |
confirmed | retain | Reversed; as entry 14 (src/Dexpace.Sdk.Core/Pipeline/PipelineBuilder.cs:222) |
| 17 | — | confirmed | retain | The SSE preamble's deviations are replicated; scan N13 finds no SseParser in src/ |
| 18 | SERDE-26, TRANSPORT-18 |
confirmed | retain (+doc: two wordings corrected in §11) | Options are copied; the bounded re-send capture is src/Dexpace.Sdk.Http.SystemNet/ResendCapture.cs:40 with its limit src/Dexpace.Sdk.Http.SystemNet/SystemNetHttpClientOptions.cs:116 |
| 19 | RETRY-28 |
confirmed | retain | Two stacks, neither unification sanction invoked (src/Dexpace.Sdk.Core/Pipeline/Policies/RetryPolicy.cs:121, src/Dexpace.Sdk.Core/Recovery/RetryRecovery.cs:35) |
| 20 | RECOV-31, RETRY-38 |
contradicted | doc-corrected | One feature under two IDs, built; as item 10 |
| 21 | ASYNC-21 |
confirmed | retain | No reactive adapter; the property is on the pull path (src/Dexpace.Sdk.Core/ServerSentEvents/ServerSentEventReader.cs:109); scan N12 |
| 22 | RETRY-6, HTTP-9 |
confirmed | retain | TRACE is dropped and the set is internal (src/Dexpace.Sdk.Core/Resilience/RetryFacts.cs:28) |
| 23 | XCUT-4, RETRY-2 |
confirmed | retain | The I/O family reading stays; the type clause is repaired under entry 7 (src/Dexpace.Sdk.Core/Resilience/RetryFacts.cs:106) |
| 24 | — | narrowed | doc-corrected | Nothing the SDK adds retries below it, and since task 4.1 neither does the handler the SDK builds (src/Dexpace.Sdk.Http.SystemNet/SystemNetHttpClient.cs:521); scan N15 |
| 25 | XCUT-16, AUTH-28 |
confirmed | retain | No loopback exemption (src/Dexpace.Sdk.Core/Pipeline/Policies/AuthorizationPolicy.cs:252, tests/Dexpace.Sdk.Core.Tests/Security/AuthHttpsGuardTests.cs:25); scan N16 |
| 26 | AUTH-11, AUTH-37 |
confirmed | retain | Background refresh in the expiring zone (src/Dexpace.Sdk.Core/Auth/AccessTokenCache.cs:43) |
| 27 | CFG-31 |
confirmed | retain | RFC 850 and asctime dates are rejected (src/Dexpace.Sdk.Core/Http/Common/HttpDate.cs:33) |
| 28 | RETRY-15, CFG-30 |
confirmed | retain | The weekday is not validated (src/Dexpace.Sdk.Core/Http/Common/HttpDate.cs:33) |
| 29 | PIPE-32, REDIR-25 |
confirmed | retain | The async redirect step is sanctioned; as entry 14 (src/Dexpace.Sdk.Core/Pipeline/PipelineBuilder.cs:222) |
| 30 | CTX-7..13, CTX-17..19 |
contradicted | doc-corrected | No CallKey on RequestOptions and no SDK reader; the store's lookup is for consumers (src/Dexpace.Sdk.Core/Execution/DexpaceCallContexts.cs:9); scan N19 |
| 31 | REDIR-11 |
confirmed | retain | The marker is removed; as entry 15 (src/Dexpace.Sdk.Core/Pipeline/Policies/Redirect/RedirectChain.cs:25) |
| 32 | XCUT-2 |
narrowed | doc-corrected | Classification tests the token first, then the inner TimeoutException (src/Dexpace.Sdk.Core/Resilience/RetryFacts.cs:22, src/Dexpace.Sdk.Http.SystemNet/SendFailures.cs:54) |
| 33 | RECOV-17..34 |
confirmed | retain | Stated in appendix C only; amendment N11-6 |
| 34 | HTTP-42 |
confirmed | retain | The BOM strip follows the charset's preamble (src/Dexpace.Sdk.Core/Internal/TextDecoding.cs:16); scan N14 |
| 35 | SEAM-27 |
confirmed | retain | A path value of . or .. is rejected (src/Dexpace.Sdk.Core/Operations/OperationDescriptor.cs:267) |
| 36 | HTTP-17, HTTP-19, HTTP-21, TRANSPORT-12 |
narrowed | repaired | The inbound path no longer trims a name (src/Dexpace.Sdk.Core/Http/Common/HeaderSyntax.cs:37); HeaderInjectionValidationTests.The_inbound_path_does_not_trim_a_name |
| 37 | XCUT-20, OBS-20, OBS-30 |
confirmed | retain | The SDK's own recording is total; listener callbacks are the other party (src/Dexpace.Sdk.Core/Diagnostics/EmissionGuard.cs:13) |
| 38 | OBS-32 |
confirmed | retain | Duration in seconds, no count instrument (src/Dexpace.Sdk.Core/Diagnostics/HttpClientMetrics.cs:25); scan N11 |
| 39 | HTTP-46 |
confirmed | retain | Body equality is by value or by identity (src/Dexpace.Sdk.Core/Http/Request/RequestBody.cs:397) |
| 40 | HTTP-48, HTTP-18 |
confirmed | retain | An obs-text ETag parses and cannot be sent (src/Dexpace.Sdk.Core/Http/Request/RequestConditions.cs:62) |
| 41 | SEAM-2 |
narrowed | doc-corrected | One public seam implementer, HttpPipeline; SeamImplementationArchitectureTests.Core_implements_a_seam_only_through_the_allow_listed_adapters |
| 42 | SEAM-22 |
confirmed | retain | No seam member takes a Type (tests/Dexpace.Sdk.Core.Tests/Architecture/SerdeSeamArchitectureTests.cs:17) |
| 43 | SEAM-30 |
contradicted | repaired | The documented cancel-without-interruption mode leaked 44 of 20 000 responses; WaitOrDisposeAsync closes the orphan (src/Dexpace.Sdk.Core/Client/ResponseTaskExtensions.cs:42, src/Dexpace.Sdk.Core/Internal/LateResult.cs:42) |
| 44 | IO-10, IO-21 |
contradicted | doc-corrected | CapturedBytes has no production caller (src/Dexpace.Sdk.Core/IO/CapturedBytes.cs:22) |
| 45 | IO-14 |
confirmed | retain | Malformed bytes decode to U+FFFD and a BOM is content (src/Dexpace.Sdk.Core/IO/Utf8LineReader.cs:35) |
| 46 | — | confirmed | retain | Both Apply and ApplyAsync are required (src/Dexpace.Sdk.Core/Recovery/IRequestStep.cs:34) |
| 47 | PIPE-2, PIPE-4 |
narrowed | doc-corrected | The operation span is opened by the pipeline, not by OperationPolicy (src/Dexpace.Sdk.Core/Pipeline/HttpPipeline.cs:263) |
| 48 | PIPE-3 |
confirmed | retain | Stage keys are sparse (src/Dexpace.Sdk.Core/Pipeline/PipelineStage.cs:29) |
| 49 | CFG-31, RETRY-15 |
confirmed | retain | A one- or two-digit day (src/Dexpace.Sdk.Core/Http/Common/HttpDate.cs:33) |
| 50 | CFG-23 |
confirmed | retain | A leading dot is a literal glob (src/Dexpace.Sdk.Core/Internal/ProxyGlob.cs:17) |
| 51 | CFG-12 |
narrowed | doc-corrected | Core has no configuration builder; DexpaceClientBuilder is one, used single-threaded (src/Dexpace.Sdk.Extensions.DependencyInjection/DexpaceClientBuilder.cs:22) |
| 52 | RETRY-41, RECOV-34, HTTP-35 |
confirmed | retain | A negative count is rejected (src/Dexpace.Sdk.Core/Configuration/RetryOptions.cs:60) |
| 53 | RETRY-10, RETRY-15 |
confirmed | retain | A sub-tick jitter returns the base (src/Dexpace.Sdk.Core/Resilience/RetryBackoff.cs:54) |
| 54 | RETRY-21 |
narrowed | repaired | The stage stack walks a caller-configured ordered list (src/Dexpace.Sdk.Core/Configuration/RetryOptions.cs:178, src/Dexpace.Sdk.Core/Resilience/RetryPacing.cs:52); RetryPolicyTests.The_stage_stack_walks_the_configured_pacing_header_order |
| 55 | RETRY-16, RETRY-18 |
confirmed | retain | A huge numeral saturates and clamps (src/Dexpace.Sdk.Core/Resilience/RetryPacing.cs:27) |
| 56 | RECOV-19 |
confirmed | retain | Every send is classified (src/Dexpace.Sdk.Core/Recovery/RecoveryDispatcher.cs:167) |
| 57 | REDIR-3 |
confirmed | retain | The original method (src/Dexpace.Sdk.Core/Pipeline/Policies/Redirect/RedirectChain.cs:37) |
| 58 | REDIR-20 |
confirmed | retain | The predicate replaces eligibility only (src/Dexpace.Sdk.Core/Pipeline/Policies/Redirect/RedirectDecider.cs:16) |
| 59 | PIPE-40, REDIR-6, REDIR-22 |
confirmed | retain | The union reading of the abandon paths (src/Dexpace.Sdk.Core/Pipeline/Policies/RedirectPolicy.cs:76) |
| 60 | — | confirmed | retain | Several Location values are malformed (src/Dexpace.Sdk.Core/Pipeline/Policies/Redirect/RedirectLocation.cs:12) |
| 61 | REDIR-28 |
confirmed | retain | The malformed-Location log is redacted (src/Dexpace.Sdk.Core/Diagnostics/RedirectLog.cs:135) |
| 62 | AUTH-16, AUTH-17, AUTH-22 |
confirmed | retain | -sess without qop is declined (src/Dexpace.Sdk.Core/Auth/DigestChallengeHandler.cs:176); amendment N11-3 |
| 63 | AUTH-28 |
confirmed | retain | The guard also runs on a reactive scheme's outbound pass (src/Dexpace.Sdk.Core/Pipeline/Policies/AuthorizationPolicy.cs:252) |
| 64 | AUTH-8, AUTH-9 |
confirmed | retain | The named-key clauses are vacuous; no NamedKey type exists |
| 65 | AUTH-36 |
confirmed | retain | A cross-origin 401 is not challenge-handled (src/Dexpace.Sdk.Core/Pipeline/Policies/AuthorizationPolicy.cs:199) |
| 66 | PAGE-19 |
confirmed | retain | The Link cross-origin guard (src/Dexpace.Sdk.Core/Pagination/LinkHeaderStrategy.cs:21) |
| 67 | CFG-7, CFG-5 |
narrowed | doc-corrected | The duration grammar also accepts "250 ms" and "1.5s" (src/Dexpace.Sdk.Extensions.DependencyInjection/DurationGrammar.cs:12) |
| 68 | CFG-2 |
contradicted | repaired | The fall-through half can be met for a section of the registered root (src/Dexpace.Sdk.Extensions.DependencyInjection/ConfigSection.cs:51); EmptyValueTests.An_empty_value_falls_through_to_the_lower_provider. It cannot for a section of any other configuration, which is the counted residual of entry 25 (EmptyValueTests.A_section_from_an_unregistered_configuration_keeps_the_merged_read) |
| 69 | XCUT-24, BODY-30, BODY-33, RECOV-16 |
added | added | The error snapshot's primary read path is the BODY-30 buffered copy, which is replayable (src/Dexpace.Sdk.Core/Http/Response/ErrorBodyBuffer.cs:22); ErrorBodyBufferTests.The_buffered_copy_is_replayable, ErrorBodyPreviewTests.Reading_the_error_body_twice_yields_the_same_bytes_and_GetErrorAsync_still_works |
| 70 | HTTP-17, HTTP-19, XCUT-18 |
added | added | The trim of HTTP-17 is outbound-only; an inbound name with surrounding whitespace is refused (src/Dexpace.Sdk.Core/Http/Common/HeaderSyntax.cs:37) |
| 71 | TRANSPORT-2 |
added | added | "SDK-managed transport" is a transport whose primary handler the SDK built; a caller's handler or HttpClient is the caller's native client (tests/Dexpace.Sdk.Http.SystemNet.Tests/Conformance/SystemNetSubject.cs:144, src/Dexpace.Sdk.Http.SystemNet/SystemNetHttpClient.cs:521) |
| 72 | CFG-5, CFG-6, CFG-7 |
added | added | The clauses bind the accessor; a host validator that refuses to start on a recorded parse failure is stricter, not contrary (src/Dexpace.Sdk.Extensions.DependencyInjection/RecordBinder.cs:73, src/Dexpace.Sdk.Extensions.DependencyInjection/DexpaceClientRegistrations.cs:27) |
A claim that something does not exist is backed by a scan with a probe that proves the scan is not vacuous (P11-6). Pattern is a .NET regular expression (\| is a literal bar inside a table cell and
stands for the regex alternation bar), run line by line over the files of Paths (<dir>/**/*.<ext> or *.{a,b}; bin/ and obj/ excluded; a line that starts with // is a comment and is not read); Allowed is the
file names that may match, else none; Probe is a line the pattern must match, which rule L10 checks; L10 also fails a Paths cell with no glob, a glob that is not of the two shapes above, and a glob whose directory holds no file of its extension (a scan that reads nothing proves nothing, so a renamed directory cannot make a scan pass). LedgerAuditTests rule L9 runs every row over the tree, and the scan N20 below is the one enforced by an
architecture test instead. Absence claims about the compiled surface are architecture tests under tests/Dexpace.Sdk.Core.Tests/Architecture/, and the gate checks that every test a row cites exists.
| Scan | Entry/item | Claim | Pattern | Paths | Allowed | Probe |
|---|---|---|---|---|---|---|
| N1 | 3 | absence of the provider vocabulary | \b(class|interface|struct|record)\s+(I?(Buffered)?(Source|Sink)|Buffer|I?IoProvider|ByteString)\b |
src/**/*.cs |
— | internal sealed class Buffer |
| N2 | 9 | no discovery | ModuleInitializer|AppDomain\.CurrentDomain\.GetAssemblies|Assembly\.(Load|LoadFrom|LoadFile)\b|GetExportedTypes|AssemblyLoadContext |
src/**/*.cs |
— | var all = Assembly.Load(name); |
| N3 | 7 | the unused failure type is gone | class\s+ServiceResponseException\b |
src/**/*.cs |
— | public class ServiceResponseException : Exception |
| N4 | 8 | no thread interruption | \bThread\.(Abort|Interrupt)\b|ThreadInterruptedException|ThreadAbortException |
src/**/*.cs |
— | worker.Interrupt(); Thread.Abort(); |
| N5 | 10 | the builders are the four listed | \b(class|record|struct|interface)\s+I?\w*Builder\b |
src/Dexpace.Sdk.Core/**/*.cs src/Dexpace.Sdk.Http.SystemNet/**/*.cs src/Dexpace.Sdk.Serialization.SystemTextJson/**/*.cs src/Dexpace.Sdk.Extensions.DependencyInjection/**/*.cs |
Headers.cs Query.cs PipelineBuilder.cs DexpaceClientBuilder.cs |
public sealed class RequestBuilder |
| N6 | 13 | no wrapper exception for the trail | new\s+AggregateException\b |
src/**/*.cs |
— | throw new AggregateException(errors); |
| N7 | 15 | one property key | PipelinePropertyKey<[^>]+>\s+\w+\s*= |
src/**/*.cs |
IdempotencyPolicy.cs |
PipelinePropertyKey<string> s_k = new("x"); |
| N8 | 16 | no vendored MD5 | (?i)0x67452301|0xefcdab89|0x98badcfe|0x10325476 |
src/**/*.cs |
— | uint a = 0x67452301; |
| N9 | 19 | the paging engine schedules nothing | TaskScheduler|SynchronizationContext|ThreadPool\.|ContinueWith |
src/Dexpace.Sdk.Core/Pagination/**/*.cs |
— | task.ContinueWith(next); |
| N10 | 25 | no global slot | public\s+static\s+(?!readonly)[^(=;]*\{\s*get;\s*(set|init); |
src/**/*.cs |
— | public static ClientOptions Current { get; set; } |
| N11 | 11 / 38 | one metrics file | Create(Counter|Histogram|UpDownCounter|Observable\w+|Gauge)\b |
src/**/*.cs |
HttpClientMetrics.cs |
var c = meter.CreateCounter<long>("x"); |
| N12 | 21 | no reactive adapter | IObservable<|IObserver<|System\.Reactive |
src/**/*.cs |
— | class Bridge : IObservable<int> |
| N13 | 17 | core ships its own SSE parser | System\.Net\.ServerSentEvents|\bSseParser\b|\bSseItem< |
src/**/*.cs |
— | var parser = SseParser.Create(stream); |
| N14 | 34 | nothing writes a BOM | \bStreamWriter\b|new\s+UTF8Encoding\(\s*true |
src/**/*.cs |
— | using var w = new StreamWriter(stream); |
| N15 | 24 | no second retry engine | AddStandardResilienceHandler|AddResilienceHandler|\bPolly\b |
src/**/*.{cs,csproj} |
— | services.AddResilienceHandler("x", b => {}); |
| N16 | 25 | no loopback exemption in core | IsLoopback|"localhost"|127\.0\.0\.1 |
src/Dexpace.Sdk.Core/**/*.cs |
— | if (host == "localhost") return; |
| N17 | 31 | no native stat | \[(DllImport|LibraryImport)\b|static\s+extern\b |
src/**/*.cs |
— | [DllImport("libc")] static extern int stat(); |
| N18 | 1 | background work has one site | Task\.Run\(|ThreadPool\.QueueUserWorkItem |
src/Dexpace.Sdk.Core/**/*.cs src/Dexpace.Sdk.Http.SystemNet/**/*.cs src/Dexpace.Sdk.Serialization.SystemTextJson/**/*.cs src/Dexpace.Sdk.Extensions.DependencyInjection/**/*.cs |
BackgroundWork.cs |
return Task.Run(work); |
| N19 | 30 | no CallKey stamp or reader outside core | \bCallKey\b |
src/Dexpace.Sdk.Http.SystemNet/**/*.cs src/Dexpace.Sdk.Extensions.DependencyInjection/**/*.cs |
— | var key = new CallKey(); |
| N20 | 12 | every broad catch filters the fatal exceptions | catch\s*(\(\s*(System\.)?Exception(\s+\w+)?\s*\))?\s*$ |
src/Dexpace.Sdk.Core/**/*.cs src/Dexpace.Sdk.Http.SystemNet/**/*.cs src/Dexpace.Sdk.Serialization.SystemTextJson/**/*.cs src/Dexpace.Sdk.Extensions.DependencyInjection/**/*.cs |
gate: FatalFilterArchitectureTests.No_catch_all_in_a_runtime_library_swallows_a_fatal_exception |
catch (Exception ex) |
Recommendations to the specification's author, recorded here and not applied: docs/product-spec/ is byte-identical to the Ruby and Node copies and docs/README.md freezes it (P11-4), so, as Ruby's
"C11" set lives in Ruby's docs/deviations.md, the .NET set lives in this file. Each names the rows, the problem and the proposed text.
| # | Rows | Problem | Proposed |
|---|---|---|---|
| N11-1 | XCUT-24, BODY-30, BODY-33, RECOV-16 |
XCUT-24 asks error snapshots to be non-consuming while BODY-30 and RECOV-16 require the cap to drain and release |
"For an error snapshot, the primary read path is the BODY-30 buffered copy; the original stream is released per RECOV-16." |
| N11-2 | HTTP-17, HTTP-19, XCUT-18 |
the trim and "reject HTAB" meet at the edge of a name; HTTP-19's strictness is easy to read as trimming too |
"Surrounding whitespace is trimmed for outbound names only; an inbound name with surrounding whitespace is invalid." |
| N11-3 | AUTH-16, AUTH-17, AUTH-22 |
a -sess challenge without qop cannot satisfy all three |
"A -sess algorithm without qop MUST be declined." |
| N11-4 | TRANSPORT-2 |
"SDK-managed transport" is undefined when the caller supplies the native client | "…a transport whose native client the SDK constructed; a caller-supplied client's retry is the caller's." |
| N11-5 | RETRY-38, RECOV-31 |
one feature, two IDs, two modal levels; RETRY-38's tag contradicts its prose |
merge under one ID at SHOULD, with "the stamp replaces any previous attempt header and MUST NOT reuse the idempotency key's name". |
| N11-6 | RECOV-17..34 |
stated in appendix C only | give them a chapter body in 08-execution-pipelines.md §8.2. |
| N11-7 | CFG-31 |
rejects date forms RFC 9110 §5.6.7 obliges recipients to accept | state the intent ("a rejected date is no hint") explicitly. |
| N11-8 | CFG-5, CFG-6, CFG-7 |
"never throw" is unclear for configuration read once at composition | "The accessor never throws; a host MAY refuse to start on a recorded parse failure." |
| N11-9 | PAGE-21 |
"byte-for-byte" is unattainable through a URL type that canonicalizes unreserved escapes | permit RFC 3986 §6.2.2.2 normalization of unreserved characters. |
| N11-10 | OBS-32 |
names http.client.request.count and ms while deferring to OpenTelemetry, which has neither |
follow the OpenTelemetry stable HTTP client metrics. |
| N11-11 | XCUT-20, OBS-20, OBS-30 |
"observability MUST NEVER throw" vs unwrapped listener calls | "…the SDK's own recording code; listener callbacks are the OBS-30 party." |
| N11-12 | SEAM-2 |
"never implements" forbids the bridges SEAM-18 requires |
"never supplies the external concern behind a seam". |
The collection point for a deviation found by a review, an incident or a consumer between phases: add the row here, with the evidence, and route the repair to its owner. Deliberately uncounted until a phase or the lead rules on it.
| Deviation | Found by | Date | IDs | Evidence | §10 status |
|---|