Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
b7c7413
feat: conformance kit runner generic over the suite context, Transpor…
Wahbeh-Mohammad Oct 14, 2026
a546154
feat: SuiteOptions, per-suite report preambles and ConformanceReport.…
Wahbeh-Mohammad Oct 14, 2026
c9c8d3f
feat: invariant suite skeleton and InvariantSubject (phase 10, #99)
Wahbeh-Mohammad Oct 14, 2026
3734645
feat: invariant assertions xcut-1 to xcut-3 with negative controls (p…
Wahbeh-Mohammad Oct 14, 2026
baf4ab2
feat: invariant assertions xcut-4 to xcut-8 with negative controls (p…
Wahbeh-Mohammad Oct 14, 2026
7f06a50
feat: invariant assertions xcut-9 to xcut-12 with negative controls (…
Wahbeh-Mohammad Oct 14, 2026
35be45e
feat: invariant assertions xcut-13 to xcut-15 with negative controls …
Wahbeh-Mohammad Oct 14, 2026
8bc7f0b
feat: invariant assertions xcut-16 to xcut-19 with negative controls …
Wahbeh-Mohammad Oct 14, 2026
e996381
feat: invariant assertions xcut-20 to xcut-24 and nfr-11 with negativ…
Wahbeh-Mohammad Oct 14, 2026
e1f0dd7
test: core driver for the invariant suite; SEAM-2 allow-list admits t…
Wahbeh-Mohammad Oct 14, 2026
0bc9ad9
test: DI driver for the invariant suite (SeamResolution) (phase 10, #99)
Wahbeh-Mohammad Oct 14, 2026
3e7072c
chore: RS0030 bans for Exception.GetBaseException and ConcurrentDicti…
Wahbeh-Mohammad Oct 14, 2026
5be45b1
chore: PR 1 gate fixes, import order and the banned-call grep (phase …
Wahbeh-Mohammad Oct 14, 2026
91422b8
feat: codec suite, CodecSubject and probe models with 11 assertions a…
Wahbeh-Mohammad Oct 14, 2026
faea581
test: STJ driver for the codec suite (phase 10, #99)
Wahbeh-Mohammad Oct 14, 2026
cd3c5fc
feat: packaging suite with 8 assertions, BCL package readers and nega…
Wahbeh-Mohammad Oct 14, 2026
2721daa
ci: packaging conformance step over the packed packages (phase 10, #99)
Wahbeh-Mohammad Oct 14, 2026
f87efc1
test: NFR-11 public-signature scan and NFR-4 public-API file presence…
Wahbeh-Mohammad Oct 14, 2026
23d342c
test: NFR-13 license header over every C# file including scripts/ci (…
Wahbeh-Mohammad Oct 14, 2026
9449519
chore: re-enable conditions for every analyzer exemption, gated by An…
Wahbeh-Mohammad Oct 14, 2026
40513c5
test: CI workflow completeness and no advisory gate (NFR-17) (phase 1…
Wahbeh-Mohammad Oct 14, 2026
6412070
ci: reproducible pack from two checkouts at different paths (phase 10…
Wahbeh-Mohammad Oct 14, 2026
5a3214d
feat: AppendixBMap covers all 61 appendix-B items (phase 10, #99)
Wahbeh-Mohammad Oct 14, 2026
c4e5e01
test: APPENDIX_B.md and its five-rule coverage gate (phase 10, #99)
Wahbeh-Mohammad Oct 14, 2026
3ae24c6
ci: requirement-coverage script over appendix C and every checklist (…
Wahbeh-Mohammad Oct 14, 2026
8ded3d8
test: AOT smoke runs the invariant and codec suites (phase 10, #99)
Wahbeh-Mohammad Oct 14, 2026
725d44a
docs: cross-cutting-invariants.md and the conformance page for the ne…
Wahbeh-Mohammad Oct 14, 2026
39390ab
docs: phase 10 checklist (phase 10, #99)
Wahbeh-Mohammad Oct 14, 2026
550b3ea
docs: changelog for phase 10 (phase 10, #99)
Wahbeh-Mohammad Oct 14, 2026
38b6a16
docs: phase 10 corrections, roadmap note and CLAUDE.md (phase 10, #99)
Wahbeh-Mohammad Oct 14, 2026
a27525a
ci: requirement-coverage check blocks every pull request (phase 10, #99)
Wahbeh-Mohammad Oct 14, 2026
ab8be72
docs: record the phase 10 gate results in the checklist (phase 10, #99)
Wahbeh-Mohammad Oct 14, 2026
a54aaf2
fix: xcut-17 composes the subject's auth step under the redirect loop…
Wahbeh-Mohammad Oct 14, 2026
42eece1
fix: xcut-18 accepts a refused or trimmed HTAB at the edge of a name,…
Wahbeh-Mohammad Oct 14, 2026
acd3638
fix: xcut-13.dispose-non-blocking fails a subject with nothing to clo…
Wahbeh-Mohammad Oct 14, 2026
4891ca0
fix: xcut-24 requires a body preview in the log and a non-empty prefi…
Wahbeh-Mohammad Oct 14, 2026
c9f8625
fix: xcut-21 accepts any nonce of at least 128 bits in hex or base64,…
Wahbeh-Mohammad Oct 14, 2026
7947c0b
test: CiWorkflowTests pins the triggers and rejects a job-level if, c…
Wahbeh-Mohammad Oct 14, 2026
55cf0e0
docs: first-release names the 16 MUST rows the coverage script reads …
Wahbeh-Mohammad Oct 14, 2026
f1a58c0
docs: phase 10 review resolution in the checklist, appendix B, user p…
Wahbeh-Mohammad Oct 14, 2026
da9448e
docs: record the gate re-run after the review resolution in the check…
Wahbeh-Mohammad Oct 15, 2026
f781ba9
fix: reproducible-pack.sh anchors a relative output directory before …
Wahbeh-Mohammad Oct 15, 2026
16c2fe6
test: the sync half of Both_waits_use_TimeProviderWaits advances the …
Wahbeh-Mohammad Oct 15, 2026
0ae87bd
ci: reproducible-pack.sh names the differing package entries and thei…
Wahbeh-Mohammad Oct 15, 2026
f1db46f
ci: reproducible-pack.sh prints the zip metadata lines that differ (p…
Wahbeh-Mohammad Oct 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -125,32 +125,38 @@ dotnet_naming_style.pascal_case.capitalization = pascal_case

# HTTP field names, media-type tokens, and protocol identifiers have a canonical *lower-case*
# form (RFC 7230 §3.2). Lowercasing is correct here; ToUpperInvariant would be wrong.
# Re-enable when: a non-protocol string is lower-cased (an HTTP token never is).
dotnet_diagnostic.CA1308.severity = none

# The SDK deliberately accepts string URLs at its ergonomic entry points (e.g. Request.Get).
# A System.Uri overload is also provided for callers that prefer it.
# Re-enable when: the string-URL entry points are removed.
dotnet_diagnostic.CA1054.severity = none
dotnet_diagnostic.CA1055.severity = none
dotnet_diagnostic.CA1056.severity = none

# Argument null-checks are applied where they matter via ArgumentNullException.ThrowIfNull;
# CA1062's blanket requirement on every public parameter is noise for this surface.
# Re-enable when: nullable annotations are turned off for any library.
dotnet_diagnostic.CA1062.severity = none

# --- Application-shaped code -----------------------------------------------------------------

# Tests, repository tools and the AOT smoke consumer are applications, not libraries: none runs under a
# SynchronizationContext it could deadlock, and xUnit's own analyzer (xUnit1030) forbids ConfigureAwait(false)
# in test methods. Styleguide 9.4 enables CA2007 in library projects and suppresses it in app/host projects.
# Re-enable when: never for src/ (this section holds application-shaped code only; the library rule above stays a warning).
[{tests,tools,.claude}/**/*.cs]
dotnet_diagnostic.CA2007.severity = none

[tests/**/*.cs]
# Test method names use Pascal_Snake casing by convention (e.g. Method_DoesThing_WhenCondition).
# Re-enable when: never for src/ (test names only).
dotnet_diagnostic.CA1707.severity = none
# CA2000 in tests: a test hands its fakes to the pipeline or transport under test through a factory call or a
# scripted-response list, which is an ownership hand-off the analyzer cannot follow. Method-call hand-off counts
# here (never in src/), and Response is exempt because scripted Responses wrap in-memory bodies that the
# scripted transport owns and hands on. A test that creates a Response it keeps still disposes it.
# Re-enable when: never for src/ (the method-call hand-off and the Response exemption are test-only relaxations).
dotnet_code_quality.CA2000.dispose_ownership_transfer_at_method_call = true
dotnet_code_quality.CA2000.excluded_type_names_with_derived_types = T:Dexpace.Sdk.Core.Http.Response.Response
31 changes: 26 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,14 @@ name: CI
# - SSE-37 architecture test (serde independence of SSE and paging): wired in
# tests/Dexpace.Sdk.Core.Tests/Architecture/Sse37ArchitectureTests.cs, and reported as skipped until the
# Dexpace.Sdk.Core.ServerSentEvents namespace holds a type; phase 7 lands it, and the test enforces with no edit.
# - NativeAOT smoke consumer: runs below over today's surface, with the Tristate, paging and SSE round trip of phase 7
# and a conformance slice from phase 8a; phase 10 completes it.
# - Package-validation baseline (PackageValidationBaselineVersion): phase 12, at the first published version.
# - NFR-11's public-signature type scan: phase 10. Strong naming and author signing (NFR-16): phase 12.
# - Strong naming and author signing (NFR-16): phase 12.
#
# Phase 10 completed what was inert: the NativeAOT smoke consumer below runs the invariant suite with core's defaults and the codec suite over the
# System.Text.Json adapter (tests/Dexpace.Sdk.AotSmoke/SmokeChecks.Phase10.cs); NFR-11's public-signature type scan is
# tests/Dexpace.Sdk.Core.Tests/Architecture/Nfr11PublicSurfaceArchitectureTests.cs; the packaging suite runs after Pack (below); and
# tests/Dexpace.Sdk.Conformance.Tests/Gates/CiWorkflowTests.cs fails if a gate here is dropped or made advisory (NFR-17).
# The requirement-coverage check (scripts/ci/requirement-coverage.cs --check, with its self-test) blocks every pull request on the Linux row.

on:
push:
Expand Down Expand Up @@ -118,6 +122,12 @@ jobs:
- name: Dependency audit
run: dotnet run scripts/ci/dependency-audit.cs -- Release artifacts/packages

# NFR-1/-2/-3/-8/-9/-10/-13/-14/-15 (phase 10, P10-10): the kit's PackagingSuite over the packed .nupkg files: the published metadata (nuspec
# dependencies, license, versions) and each packed assembly's attributes (IsTrimmable, IsAotCompatible, InternalsVisibleTo, the version),
# read without loading them. The audit above reads the build output's closure; this reads what was published (two checks, two subjects).
- name: Packaging conformance
run: dotnet run scripts/ci/packaging-conformance.cs -- artifacts/packages

- name: Tools - restore (locked)
run: dotnet restore tools/Dexpace.Tools.sln --locked-mode

Expand All @@ -136,6 +146,16 @@ jobs:
if: runner.os == 'Linux'
run: scripts/knowledge verify-structure

# Blocking. Every ID of appendix C is owned by a checklist row under docs/work/ with its level and a legend mark (roadmap phase 10 exit); a
# structural check over committed files, so one row suffices. The self-test first proves the check fails on each kind of finding.
- name: Requirement coverage self-test
if: runner.os == 'Linux'
run: scripts/ci/requirement-coverage-selftest.sh

- name: Requirement coverage
if: runner.os == 'Linux'
run: dotnet run scripts/ci/requirement-coverage.cs -- --check

- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
Expand All @@ -154,8 +174,9 @@ jobs:
with:
global-json-file: global.json

# NFR-12's build half: two packs from a clean src/ tree with SOURCE_DATE_EPOCH at the commit time must be
# byte-identical. The release half (a published artifact rebuilt from its tag) is phase 12's.
# NFR-12's build half: two packs of HEAD from two checkouts at different paths (git worktrees, phase 10, P10-14), with
# SOURCE_DATE_EPOCH at the commit time, must be byte-identical, so a path leaking into an artefact fails the job. The release
# half (a published artifact rebuilt from its tag) is phase 12's.
- name: Pack twice and compare
run: scripts/ci/reproducible-pack.sh artifacts/reproducible

Expand Down
14 changes: 14 additions & 0 deletions BannedSymbols.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,24 @@ M:System.Uri.ToString;Uri.ToString() unescapes; use OriginalString or GetCompone
# Context-store and lifetime discipline in core (design §5.4).
T:System.Threading.AsyncLocal`1;The SDK holds no ambient per-call state; correlate through CallKey and the context chain (design §5.4 P13)
M:System.Collections.Concurrent.ConcurrentDictionary`2.TryRemove(System.Collections.Generic.KeyValuePair{`0,`1});The value-comparing TryRemove belongs only in BoundedMap's slot (design §5.4, P4a-12)
# Every map keyed by callers or servers is a BoundedMap (XCUT-14, P10-18): all seven public ConcurrentDictionary constructors are banned, and the one
# sanctioned construction, BoundedMap's own, is a scoped pragma with a why-comment. The type is not banned: that would flag every use of BoundedMap's field
# and its cref, and the value-comparing TryRemove pair above would re-enable the rule inside any file-scoped exemption (design P10-18, validation F1).
M:System.Collections.Concurrent.ConcurrentDictionary`2.#ctor;A process- or instance-lived concurrent map is a BoundedMap, or it does not land (XCUT-14, design §5.4, P10-18)
M:System.Collections.Concurrent.ConcurrentDictionary`2.#ctor(System.Int32,System.Int32);A process- or instance-lived concurrent map is a BoundedMap, or it does not land (XCUT-14, design §5.4, P10-18)
M:System.Collections.Concurrent.ConcurrentDictionary`2.#ctor(System.Collections.Generic.IEnumerable{System.Collections.Generic.KeyValuePair{`0,`1}});A process- or instance-lived concurrent map is a BoundedMap, or it does not land (XCUT-14, design §5.4, P10-18)
M:System.Collections.Concurrent.ConcurrentDictionary`2.#ctor(System.Collections.Generic.IEqualityComparer{`0});A process- or instance-lived concurrent map is a BoundedMap, or it does not land (XCUT-14, design §5.4, P10-18)
M:System.Collections.Concurrent.ConcurrentDictionary`2.#ctor(System.Collections.Generic.IEnumerable{System.Collections.Generic.KeyValuePair{`0,`1}},System.Collections.Generic.IEqualityComparer{`0});A process- or instance-lived concurrent map is a BoundedMap, or it does not land (XCUT-14, design §5.4, P10-18)
M:System.Collections.Concurrent.ConcurrentDictionary`2.#ctor(System.Int32,System.Collections.Generic.IEnumerable{System.Collections.Generic.KeyValuePair{`0,`1}},System.Collections.Generic.IEqualityComparer{`0});A process- or instance-lived concurrent map is a BoundedMap, or it does not land (XCUT-14, design §5.4, P10-18)
M:System.Collections.Concurrent.ConcurrentDictionary`2.#ctor(System.Int32,System.Int32,System.Collections.Generic.IEqualityComparer{`0});A process- or instance-lived concurrent map is a BoundedMap, or it does not land (XCUT-14, design §5.4, P10-18)
T:System.Runtime.CompilerServices.ConditionalWeakTable`2;Weak tables are not used in core (design §5.4)
T:System.WeakReference`1;Weak references are not used in core (design §5.4)
T:System.WeakReference;Weak references are not used in core (design §5.4)

# Cause walks are cycle-safe (XCUT-9, P10-18): Exception.GetBaseException walks the inner-exception chain to its end with no cycle check, which is the walk
# behind the Windows test-host hang CyclicExceptions documents. Every cause walk goes through ExceptionFacts.EnumerateCauses.
M:System.Exception.GetBaseException;Walks the inner-exception chain to its end with no cycle check; use ExceptionFacts.EnumerateCauses (XCUT-9, P10-18)

# Unsafe deserialization (design §3.4). The Newtonsoft entries are inert while no project references it; they
# keep the ban in force if one ever does.
T:System.Runtime.Serialization.Formatters.Binary.BinaryFormatter;BinaryFormatter is prohibited (design §3.4)
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,9 +409,28 @@ Roadmap phase 7b, issue #10 (a later addition, not one of S1–S9), pinned by a
- **Guidance (phase 9, `XCUT-10`; documentation only):** composing `AddStandardResilienceHandler` under a named client is `o.Retry.ShouldHandle = _ => ValueTask.FromResult(false)`. The older guidance to set the SDK's own
`MaxRetryAttempts = 0` is withdrawn: the handler would still re-send a `POST` body behind the SDK's re-send gate, which measurably multiplies the attempts, and the handler-side `o.Retry.MaxRetryAttempts = 0` is refused by
the handler's own options validation. See `docs/sdk-documentation/dependency-injection.md`.
- **Phase 10, additive (`XCUT-1`..`XCUT-24`, `NFR-1`..`NFR-17`):** `TransportFace` gains `None` (value `2`), the face of an assertion that has none; `SuiteRunner` rejects it, the new suite runners accept it, and no existing value moves.
`Seam2ArchitectureTests` admits `Dexpace.Sdk.Conformance` as a reference of `Dexpace.Sdk.Core.Tests` (the core driver of the invariant suite), the kit itself staying core-only. `scripts/ci/reproducible-pack.sh` now packs `HEAD` from two
`git worktree`s at different paths, so an absolute path embedded in a package fails the job (a git checkout is the reproducible input; it warns on a dirty tree). `BannedSymbols.txt` bans `Exception.GetBaseException` and the seven public
`ConcurrentDictionary` constructors in `src/` (`RS0030`; `BoundedMap` is the one construction site, under one scoped pragma). Every `severity = none` block of `.editorconfig` states when to re-enable it. No production behaviour changed.

### Added

- **Phase 10 cross-cutting invariants and non-functional requirements (`XCUT-1`..`XCUT-24`, `NFR-1`..`NFR-17`; design and plan under `docs/work/mvp/phase10/`):** `Dexpace.Sdk.Conformance` gains three suites beside the transport suite, with one result model
(`TransportFace.None`, `SuiteRunnerCore`, `SuiteOptions`, a per-suite preamble, `ConformanceReport.Combine`). `InvariantSuite` runs 28 assertions over an `InvariantSubject` whose hooks default to core (a hook left `null` is `NotExercised`): cancellation and
timeout (`XCUT-1`..`-3`), the error taxonomy and the one classifier (`XCUT-4`..`-9`), retry safety, concurrency and the single-flight cache (`XCUT-10`..`-12`), lifecycle, bounded maps and no external aliasing (`XCUT-13`..`-15`), security by default
(`XCUT-16`..`-19`) and the host invariants (`XCUT-20`..`-24`, `NFR-11`'s scheduler-agnostic call). `CodecSuite` runs 11 assertions over an `ISerde` and the public probe models in `Dexpace.Sdk.Conformance.Codec` (`SERDE-1`..`-3`, `-9`, `-10`, `-12`, `-15`..`-17`,
`-19`, `-20`); `PackagingSuite` runs 8 assertions over the packed `.nupkg` files with BCL readers (`NFR-1`, `-2`, `-3`, `-8`, `-10`, `-13`, `-14`, `-15`). Each assertion has a deliberately broken subject in `tests/Dexpace.Sdk.Conformance.Tests/Controls/` that it must
fail. Three first-party drivers run them: the core driver, the dependency-injection driver (`xcut-23`) and the System.Text.Json driver, each pinning its expected `NotExercised` list; the NativeAOT smoke runs the invariant and codec suites (`CheckPhase10Async`).
Gates: `scripts/ci/packaging-conformance.cs` (a CI step after Pack); `scripts/ci/requirement-coverage.cs --check` with its self-test (every one of the 645 requirements of appendix C is owned by a checklist row, with its level and a legend mark; a Linux CI step);
`AppendixBCoverageTests` over the new `tests/Dexpace.Sdk.Conformance.Tests/APPENDIX_B.md` (61 items); `Nfr11PublicSurfaceArchitectureTests`, `NFR4ArchitectureTests`, `LicenseHeaderTests` (the `scripts/ci` apps included), `AnalyzerWaiverTests` and `CiWorkflowTests`.
Review resolution (2026-10-14): `xcut-17` composes the subject's Basic and bearer auth steps under the redirect loop and covers a permitted downgrade followed by a retry; `xcut-18` accepts a refusal or a trim for HTAB at the edge of a name
(`XCUT-18` and `HTTP-17` meet there; core's inbound trim is a possible `HTTP-19` gap, routed to phase 11); `xcut-13.dispose-non-blocking` fails a subject with nothing to close; `xcut-21` accepts any nonce of at least 128 bits in hex or base64; `xcut-24` requires a
body preview in the log and a non-empty prefix of the body as the error snapshot; `CiWorkflowTests` also pins the triggers and rejects a job-level `if:`, `continue-on-error` or matrix `exclude:`.
`AppendixBMap` covers all 61 appendix-B items. `docs/sdk-documentation/cross-cutting-invariants.md` is the phase page (where each `XCUT` row is enforced, the gate table, how to run the suites over your own composition); `conformance.md` describes the suites; the checklist
`docs/work/mvp/phase10/2026-10-13-phase10-cross-cutting-invariants-checklist.md` has the 41 rows. Declined with triggers: Stryker.NET (it refuses Microsoft.Testing.Platform projects) and `benchmarks/`; `XCUT-24`'s non-consuming clause for an error-body snapshot above 1 MiB
is a recorded SHOULD decline.

- **Phase 9 host integration (`CFG-1`, `-2`, `-3`, `-4`, `-5`, `-6`, `-7`, `-10`, `-11`, `-14`, `-37`, `-38`, `SEAM-5`, `SEAM-6`, `OBS-35`; design and plan under `docs/work/mvp/phase9/`):** the new package
`Dexpace.Sdk.Extensions.DependencyInjection`. `AddDexpaceClient(name[, IConfigurationSection])` returns a sealed `DexpaceClientBuilder` (`Configure`, `ConfigureTransport`, `ConfigureCredentials`, `UseTransport`, `UseSerde`,
`UseAuth`, `UseAuthPolicy`, `AddPolicy`, `UseProxyFromEnvironment` with and without a lookup, and `HttpClient`, the `IHttpClientBuilder` of the named client) and registers the client as keyed singletons (`HttpPipeline`,
Expand Down
Loading
Loading