diff --git a/.claude/probe-axes.md b/.claude/probe-axes.md deleted file mode 100644 index b19a06b0..00000000 --- a/.claude/probe-axes.md +++ /dev/null @@ -1,755 +0,0 @@ -# Probe axes: CodoMetis.TypeKit - -The `prober` agent reads this file at the start of every round. It says how to write and run a probe -here, where the claims come from, which external APIs to enumerate, which type shapes to generate, -what to compare against, and what is intended. Everything specific to this repository lives here; -the agent itself is generic. - -## Harness - -Write probes as xunit theories in **`test/CodoMetis.TypeKit.Samples`**, in a folder `Probes/` of the -scratch copy, one file per claim, class name ending in `Probe`, rows fed through `[MemberData]` from -the covering array (`prober rows`), read from a JSON file beside the probe. That project is the -closest thing to a consumer: it references every package, weaves value objects through the real -fabric, and loads the analyzer as an analyzer. A probe file may declare its own value objects -(`readonly partial record struct ProbeId : IValue;`); the declarations of the type-shape -catalogue, `tools/probe/corpus/harness/*.g.cs`, are written by the file-based apps of -`tools/probe/generators/` (`dotnet run tools/probe/generators/.cs -- ...`, its README), never by hand, -and `GeneratedHarnessTests` in the conventions tests holds each file to its app's output. - -- Start a probe with `prober new --claim ` and iterate with `prober try --probe

[--rows i,j,...]` - (tools/Prober/README.md, "Writing a probe"): `new` stages the skeleton in this harness's form, with the - claim's obligations quoted and a throwing check each, the probe's own rows and the registry entry; `try` - builds the corpus and the staging in the replay's worktree and runs that one probe, printing each row's - outcome and recording nothing. Never copy the corpus into a worktree or write a build or sync script. -- Build and run only through the verdict tool, which pins English output, builds before it tests, - names the exit codes and prints failing tests by name with their assertion: - `dotnet tools/BuildVerdict/bin/Release/net10.0/build-verdict.dll build test/CodoMetis.TypeKit.Samples` - (cold, about 1 to 2 minutes), then `dotnet tools/BuildVerdict/bin/Release/net10.0/build-verdict.dll test - --project test/CodoMetis.TypeKit.Samples --filter-class "*Probe"`. Build the tool first, `dotnet build - tools/BuildVerdict -c Release`: a fresh worktree has no dll and a stale one refuses (exit 3). - The full log is at `$(git rev-parse --absolute-git-dir)/build-verdict-last.log`. -- Warnings are errors. Add `#pragma warning disable xUnit1051` at the top of a probe that awaits - without a cancellation token; an obsolete setting (`IgnoreNullValues`) needs its `SYSLIB` id - suppressed. A probe that deliberately uses something the analyzer refuses suppresses that one CMTK - id around the line and says why. -- HTTP probes: `WebApplication.CreateBuilder(new WebApplicationOptions { EnvironmentName = - Environments.Production })` (Development adds the developer exception page, and a refusal then - arrives as a 500), `builder.WebHost.UseTestServer()`, the JSON options through - `builder.Services.ConfigureHttpJsonOptions(...)`, then `app.StartAsync()` and `app.GetTestClient()`. - The OpenAPI document is at `/openapi/v1.json` after `builder.Services.AddOpenApi(o => { - o.OpenApiVersion = ...; o.AddTypeKit(); })` and `app.MapOpenApi()`. A controller must be `internal` - and added through an `IApplicationFeatureProvider`; a public one is discovered by - every other host in the project. A host with controllers must put its own converters and resolvers - on MVC's `JsonOptions` too, or the document is refused on purpose (`DescribedOptionsAgreement`). - `test/CodoMetis.TypeKit.AspNetCore.Tests/ProbeHost.cs` is a working example to copy from. -- How a row ends: a check returns null or the failure text, and runs through `Rec.Run`, which fails the - test for a violation or a harness error and skips it, with its reasons, for any other outcome but a - pass; a check that throws ends as a harness error, never a violation, and no seed counts it caught. A check - says how it compared with `r.Note(NoteKeys.Name, detail)`. The key, a member of `NoteKeys` in `harness/Row.cs`, - alone decides the note's outcome: every key is a pass key but `framework-refuses`, the three-sided - differential's expected refusal, and a check that did not apply, refused or could not tell says so through - its scope with a citation (below), never a note key. Keys are constants the compiler checks: a new one goes - into `NoteKeys` with its outcome on one line, and a string, a misspelled member or a key built from text does - not compile (decision 103). A probe outside the namespace `Probes.Shapes` writes `using NoteKeys = Probes.Shapes.NoteKeys;`. - Never match a failure's text to excuse it. A difference the framework makes on its own is shown by a - baseline in the same row (the document as ASP.NET writes it without `AddTypeKit()`, - `Hx.DocRefusedByAspNetAlone`, a refusal compared by exception type and stage as `Sides.Alike` compares one), - not by a substring. A document both refuse alike is the framework's refusal for a check that compared nothing - else, and leaves a check that compared an answer on the wire first to be judged by that answer. A row that would fail for an open finding calls - `r.Skip("F7", why)`, the finding as a string literal: `register` and `publish` refuse a skip whose finding - is built from text or is not open in the ledger, naming the file and line. The gate refuses while any row - skips, and the replay fails a skip whose finding the ledger has fixed or dismissed. -- Checks: a comparison is `r.Check("obligation", Baseline.Twin, c => ...)` (or the `async` form), one per - obligation of the claim (`claim` for a claim without them), against one baseline of `Baseline` (`Twin`, - `Framework`, `RoundTrip`, `Model`, `Refusal`, `Cost`, `Fact("KB-031")`, `Docs("file#anchor")`). The check reads - through `c["axis"]` (or the row, inside the body: both read for the check), notes how it compared with - `c.Note(NoteKeys.Compared, ...)`, and says why it did not compare with `c.Inapplicable(Cite...)`, - `c.Refused(Cite...)` or `c.Inconclusive(reason)`, each returning null. A citation is `Cite.NotPromised("id")` (an - entry of the claim's `not-promised`), `Cite.Docs("file#anchor")`, `Cite.Fact("KB-031")` or `Cite.Record("0094")`, - written with a string literal; `register`, `publish` and the conventions tests refuse anything else and a - citation that names nothing. A failure returned is the check's violation, an exception its harness error (the - row's other checks still run), and a pass with no baseline a harness error. A row passes when no check failed - and one passed, so a check that did not apply voids no other; the ledger credits each passing check with the - axes it read and the row's, never a pair put together from two checks. A seed naming an obligation is caught - only by a check naming it. -- Two-sided checks: the twin is `Entry.Plain`, the wrapped type at the value object's nullability (a class value object over a - struct faces the struct made nullable; the closed set faces a string), written by `shapes.cs` beside the value object. Build both - sides with `Sides.Twins(position, containing, entry, plain, vo)`, which puts the row's position and containing type around each and - throws, a harness error, where the two do not hold the same shape. Under `json.TypeInfoResolver=source-generated-context` the options - resolve through `ProbeContext` alone, which lists every shape, position and containing type and each twin, with no reflection behind - it; `json.TypeInfoResolverChain=context-then-default` puts the reflection fallback after it. A shape the context does not list is a - harness error, never a fallback (tools/probe/generators/README.md, "The twin and the context"). -- EF probes: SQLite in memory (`Data Source=:memory:` with the connection held open). PostgreSQL needs - a server and is out of reach. -- Analyzer probes (what compiles and what CMTK reports): in `test/CodoMetis.TypeKit.Analyzers.Tests`, - a `CSharpCompilation` over `RealTypeKit.References` (`AnalyzerTest.cs`) with the analyzers attached, - asserting the diagnostics. The type-shape catalogue is the row there, as source text. -- Native AOT is measured only by `./test/consumer-smoke-test.sh` (minutes, needs clang). Probe it only - when the caller asks for it. - -## Seeds: every probe shows it can fail - -A probe that passes proves something only if it can fail. Every registered probe names one seeded -product defect ([decision 100](../design/adr/0100-every-probe-is-shown-to-fail-against-a-seeded-defect.md)): -a patch against `src/` that breaks what its claim promises, in `tools/probe/corpus/seeds/.patch`, -and a sentence saying what it breaks, in its registry entry: - -```json -"seed": { "patch": "seeds/L01-json-bytes.patch", "defect": "A dictionary key is written with ToString(), not the wrapped type's key converter and DictionaryKeyPolicy." } -``` - -- Write the seed with the probe. Make the defect in `src/` of the scratch copy, take `git diff -- ` - as the patch, undo it by inverse edit (`git apply -R `), and register with `prober register ... - --seed seeds/.patch --defect ""`. An environment toggle (`"env": {...}`, set for the - build and every run) is a seed too, where the product reads one; none does today. -- Seed the defect the claim is about, the one a consumer would meet: a facet dropped, a refusal that - quotes its input, a rule skipped, a key no longer generated. A seed whose build fails proves nothing, - and the command counts it as a failure. -- `prober seeds [--probe P]` applies each seed in a scratch worktree of its own (`replay --where` names - it), builds, replays the probe there and requires a row that ends as a violation the unseeded probe - does not have; a missed seed fails it. It records no evidence and takes no lock. The worktree is per - checkout, so runs in two checkouts (a linked worktree each) never meet, but two runs at once in one - checkout share it. One build per seed, so it runs locally, never in CI: after writing a probe or - changing its harness, and before a release. -- Without a build, the prober tests hold the rest on every run (`SeedsTests`): every registered probe - has a seed, and its patch touches only `src/` and still applies. A product change that moves the - patched lines fails there; make the seed again against the new code. -- A seed per obligation ([decision 104](../design/adr/0104-a-claim-states-its-obligations-and-each-has-a-seed.md)). - A seed that breaks some assertion of a probe shows the probe can fail, not that the claim is checked, so - a probe whose claim lists `obligations` carries `seeds`, one per obligation it checks, each a seed as - above with an `obligation` naming it; `seed` may name one too. A seed that names none counts only for a - claim without `obligations`. Register each with `prober register --probe P --claim C --filter "*P" - --obligation --seed seeds/-.patch --defect ""`, the command `prober new` prints - for each obligation: it replaces a seed of the entry that names the same obligation and refuses an id the - claim does not list. Never edit the staged registry by hand. `prober seeds` runs each seed of a probe on its own and reports it as - `P/`, in its output and in `seeds-last.json`; `prober status` shows per claim its obligations - and the ones no seed names; and `SeedsTests` refuses, without a build, a seed naming an obligation its - claim does not list and a listed obligation of a claim with probes that no seed of them names. - -## Claim sources - -In this order: the root `README.md` ("What TypeKit guarantees" first), `docs/*.md`, the package -READMEs (`src/*/README.md`), then `design/adr/*.md` for intent. Statements in `docs/` and the READMEs -are promises to consumers; a design record explains, and decides what is intended. - -## Axes - -The axes are data, read by `prober universe` (tools/Prober), which builds each claim's -axes from its surfaces. The prober assigns surfaces to a claim; it does not choose axes. - -- **Generated axes** come from `tools/probe/enumerate.cs`, which enumerates by reflection every - public settable property of `JsonSerializerOptions` (`json.*`), `OpenApiOptions` (`openapi.*`) and - `MvcOptions` (`mvc.*`) at the versions this repository pins. `prober replay` runs it - outside the repository (so neither `global.json` nor central package management applies to it), with - its package versions taken from `Directory.Packages.props`, whenever a dependency input changes, and - `--all` always does. A setting whose type is not a bool or an enum takes its values from `hand` below. -- Every generated axis is either reached by a surface's pattern or listed under `ignored` with the - reason. The script reports any other as **unassigned**: a setting a new framework version added, - which needs a decision here. -- `shape.*` is the type-shape catalogue: the forms a consumer's own type takes. Generate the - declarations a round needs with an app of `tools/probe/generators/` (extend one, or add one and its row - in `GeneratedHarnessTests`), once per round, as one pairwise set shared by every claim. -- A claim may drop a surface axis only by quoting the document sentence that limits it (`narrow`). -- A claim may drop pairs no row can hold, or that the documents do not promise, only with the reason - (`exclude`: a list of `{"reason": "...", "pairs": [["a=1", "b=2"], ...]}`): one sentence, citing the docs - sentence, the record or the KB fact where one applies. Nothing checks a reason mechanically, so - `prober universe` prints each one beside the claim's narrowings for a reviewer, and the prober refuses a - claims.json with any field its schema (`tools/Prober/Schemas/claims.schema.json`) does not have - ([decision 102](../design/adr/0102-an-exclusion-carries-its-reason.md)). -- An obligation may say where it does not apply, by the claim's own axis values and a citation - (`inapplicable`: a list of `{"axes": {"shape.kind": ["closed-set"]}, "cite": "not-promised:closed-set-has-no-scale"}`, - the cite `not-promised:id`, `docs:file#anchor`, `fact:KB-NNN` or `record:NNNN`). `Row.Check` reads the same - declarations and records such a check inapplicable before its body runs; a pair no obligation wants is never - put in a generated row, and the gate leaves it out as it leaves out an exclusion. A condition only the shape - instance decides at run time stays in the check's body as `c.Inapplicable(cite)`. -- `refused` drops a value the framework itself refuses on its own, so no row wastes its pairs on it. A reason that - rests on a verified fact names it (`fact:KB-NNN`), and that fact has a test: `docs-query check` fails - `fact-not-executable-cited` for a refused entry, a not-promised entry, an applicability condition or a harness - citation of a fact without one ([decision 107](../design/adr/0107-a-cited-fact-is-executable-and-seeds-run-by-footprint.md)). - `shape.refused-kind` holds the declarations the generators refuse; only the generation surface takes - it, where the refusal is the claim. Each value is a declaration and the id the Generators README's - "Build errors" table gives it, declared as C22's probe does (`harness/BuildRefusals.cs`): an unsealed - record class; a value object over another value object; over `List`; an abstract record or class with - a marker (CMTK1022 since `1ef30be`: the label keeps the id it had when C09's and C11's rows were - generated, which carry it); over `object`; over `System.Enum`; a value object named as a member the - generators add (`From`, `Parse`, `MinValue`, ...); a closed set named as one of its generated members - (`Name`, `All`, `Match`, ...); a value object with a hand-written `operator ==` and `!=` between two of - the type. The last five are the refusals of the fixes `53188ed` and `e4b751c`, which no probe reached - until C22 (design/finder-benchmark-2026-10-16.md). -- A claim lists what it promises in `obligations`, one entry per promise a probe can check on its own: an - `id`, the `sentence` quoted verbatim from the claim's source and checked as a narrowing is, and the `axes` - values it concerns where it is not the whole claim. A claim without the field has one obligation, the - claim itself, under the id `claim`. Each obligation of a claim with probes has a seed that names it - ([decision 104](../design/adr/0104-a-claim-states-its-obligations-and-each-has-a-seed.md)). -- What a claim does not promise goes in `not-promised`: an `id`, the `source` (`file#heading`) of the - document or record that makes the difference intended, its `sentence`, quoted verbatim and checked against - that source, and the `axes` values it concerns. A note of outcome expected-refusal, a narrowing or an - applicability condition cites the entry by its id. `prober universe` refuses an obligation or an entry - whose sentence its document does not hold, and an id listed twice, naming the claim and the id. -- `out-of-reach` names a surface no probe reaches, with the reason: one that needs a server, a toolchain - or minutes a replay does not have. `prober gaps` prints it beside the settings no surface reaches, the - settings without values, the refused shapes and values, and every not-promised entry and narrowing, each - with its recorded reason. It is a report for the release checklist and a feature-research round; it - decides nothing and runs no model. -- `http.host` has two values without `[ApiController]`, where MVC infers no `[FromBody]` and answers no - automatic 400. `controller-without-ApiController` is for **runtime claims only**: ASP.NET's API explorer - leaves such a controller out of the document (docs/aspnet-core.md, "Bind value objects in MVC"), so a - document check has nothing there to read. A document (OpenAPI) claim lists `http.host` in its own - `axes` in `claims.json` without it. `controller-without-ApiController-visible` hosts the same - controllers made visible by an `IApplicationModelConvention` that sets `ApiExplorer.IsVisible = true` - (`VisibleToApiExplorer` in the harness), so document checks reach them; every http claim takes it. -- `process.*` holds what other code in the same process may have done first (2026-10-15: a wrapped - type's `TypeConverter` looked up before the value object's parsing broke that parsing). - -```json probe-axes -{ - "hand": { - "json.PropertyNamingPolicy": [ - "CamelCase", - "SnakeCaseLower", - "SnakeCaseUpper", - "KebabCaseLower", - "KebabCaseUpper", - "own-upper-case" - ], - "json.DictionaryKeyPolicy": [ - "CamelCase", - "SnakeCaseLower", - "KebabCaseUpper", - "own-upper-case" - ], - "json.ReferenceHandler": [ - "Preserve", - "IgnoreCycles" - ], - "json.Converters": [ - "own-for-wrapped", - "own-for-value-object", - "JsonStringEnumConverter", - "StoredJsonConverterFactory" - ], - "json.TypeInfoResolver": [ - "source-generated-context", - "default-with-renaming-modifier" - ], - "json.TypeInfoResolverChain": [ - "context-then-default" - ], - "json.Encoder": [ - "UnsafeRelaxedJsonEscaping" - ], - "json.MaxDepth": [ - "3" - ], - "json.DefaultBufferSize": [ - "1" - ], - "json.IndentCharacter": [ - "tab" - ], - "json.IndentSize": [ - "1" - ], - "json.NewLine": [ - "lf" - ], - "openapi.CreateSchemaReferenceId": [ - "own-prefixing", - "null-inlines-all" - ], - "openapi.ShouldInclude": [ - "default-only" - ], - "openapi.transformers": [ - "own-schema-transformer", - "own-operation-transformer", - "own-document-transformer" - ], - "mvc.InputFormatters": [ - "xml-added" - ], - "mvc.OutputFormatters": [ - "xml-added" - ], - "mvc.ModelBinderProviders": [ - "own-provider-first" - ], - "mvc.Conventions": [ - "own-route-prefix" - ], - "mvc.MaxModelBindingCollectionSize": [ - "2" - ], - "mvc.MaxModelValidationErrors": [ - "1" - ], - "mvc.MaxValidationDepth": [ - "1" - ] - }, - "ignored": { - "json.WriteIndented": "layout only; JSON parity compares under the same options", - "json.IndentCharacter": "layout only", - "json.IndentSize": "layout only", - "json.NewLine": "layout only", - "json.DefaultBufferSize": "buffering only; DefaultBufferSize 1 is kept in hand for a stress round on request", - "openapi.ShouldInclude": "selects endpoints, never a schema's shape", - "mvc.EnableActionInvokers": "invocation pipeline, not binding or formatting", - "mvc.EnableEndpointRouting": "TypeKit supports endpoint routing only", - "mvc.RequireHttpsPermanent": "transport", - "mvc.SslPort": "transport", - "mvc.CacheProfiles": "caching headers", - "mvc.SuppressAsyncSuffixInActionNames": "action naming", - "mvc.SuppressInputFormatterBuffering": "buffering", - "mvc.SuppressOutputFormatterBuffering": "buffering", - "mvc.MaxIAsyncEnumerableBufferLimit": "streaming", - "mvc.MaxModelBindingRecursionDepth": "recursion limit of binding, reached only by self-referencing models", - "mvc.ModelMetadataDetailsProviders": "covered by own-provider-first on binders", - "mvc.ModelValidatorProviders": "validation providers; TypeKit validates in Create", - "mvc.ValueProviderFactories": "value sources; the binding axis covers them" - }, - "axes": { - "shape.kind": [ - "record-struct", - "sealed-record-class", - "validated-struct", - "validated-class", - "closed-set" - ], - "shape.site": [ - "top-level", - "nested-in-class", - "nested-in-struct", - "nested-two-levels", - "in-generic-type" - ], - "shape.inheritance": [ - "none", - "base-depth-1", - "base-depth-2", - "base-depth-3", - "base-explicit-IEquatable", - "explicit-interface-member" - ], - "shape.wrapped": [ - "string", - "Guid", - "int", - "long", - "byte", - "ulong", - "decimal", - "double", - "bool", - "char", - "DateTime", - "DateTimeOffset", - "DateOnly", - "TimeOnly", - "TimeSpan", - "enum", - "flags-enum", - "Uri", - "NodaTime.Instant", - "own-record-class" - ], - "shape.position": [ - "root", - "property", - "nullable-property", - "field", - "list-element", - "array-element", - "immutable-array-element", - "read-only-list-element", - "dictionary-key", - "dictionary-value", - "record-ctor-parameter", - "required-init", - "in-patch", - "polymorphic-derived-property" - ], - "shape.containing": [ - "class", - "record-class", - "struct", - "record-struct", - "generic-type" - ], - "input.json": [ - "valid", - "refused-by-create", - "wrong-token", - "null", - "empty-string", - "at-type-limit", - "quoted-number", - "escape-sequence", - "duplicate-property", - "metadata-property" - ], - "http.host": [ - "minimal", - "mvc", - "both", - "route-group", - "controller-without-ApiController", - "controller-without-ApiController-visible" - ], - "http.binding": [ - "route", - "query", - "header", - "body", - "form", - "as-parameters" - ], - "http.response": [ - "result-to-http-result", - "composed-fault", - "two-faults-same-name", - "group-with-fault-status", - "endpoint-without-faults", - "declared-produces-problem" - ], - "ef.query": [ - "where-equals", - "where-value-compare", - "order-by", - "contains", - "first-or-none", - "select", - "group-by", - "execute-update", - "compiled-query", - "as-no-tracking" - ], - "ef.mapping": [ - "property", - "key", - "integer-key", - "nullable", - "owned", - "complex", - "closed-set" - ], - "text.entry": [ - "Parse", - "TryParse", - "TryFrom", - "type-converter", - "IParsable", - "ISpanParsable", - "ToString-format", - "TryFormat-short-span" - ], - "text.culture": [ - "invariant", - "de-DE", - "tr-TR", - "ar-SA", - "globalization-invariant" - ], - "or.shape": [ - "Option", - "Result-T-E", - "Result-E", - "Task-of-Result", - "ValueTask-of-Result" - ], - "or.operation": [ - "Match", - "Bind-Map-chain", - "query-syntax", - "Sequence-Transpose", - "Result.Catch", - "FirstOrNone", - "default-instance", - "after-await-elsewhere" - ], - "shape.refused-kind": [ - "unsealed-record-class (CMTK1006)", - "over-value-object (CMTK1005)", - "generic-wrapped (CMTK1005)", - "abstract (CMTK1005)", - "over-object (CMTK1005)", - "over-System.Enum (CMTK1005)", - "named-as-generated-member (CMTK1005)", - "closed-set-named-as-generated-member (CMTK1021)", - "equality-operator (CMTK1011)" - ], - "process.converter-lookup": [ - "none", - "wrapped-type-converter-looked-up-first" - ], - "http.nullability": [ - "as-declared", - "nullable" - ] - }, - "surfaces": { - "json": { - "axes": [ - "json.*", - "shape.kind", - "shape.wrapped", - "shape.position", - "shape.containing", - "input.json" - ], - "watched": [ - "src/CodoMetis.TypeKit.Generators", - "src/CodoMetis.TypeKit/ValueObjects", - "src/CodoMetis.TypeKit/DecimalJsonConverters.cs", - "src/CodoMetis.TypeKit/Patch.cs" - ] - }, - "http": { - "axes": [ - "json.*", - "openapi.*", - "mvc.*", - "http.*", - "process.*", - "shape.kind", - "shape.wrapped", - "shape.position", - "shape.containing", - "input.json" - ], - "watched": [ - "src/CodoMetis.TypeKit.AspNetCore", - "src/CodoMetis.TypeKit/Fault.cs", - "src/CodoMetis.TypeKit/FaultOfSets.cs", - "src/CodoMetis.TypeKit.Generators" - ] - }, - "ef": { - "axes": [ - "ef.*", - "shape.kind", - "shape.wrapped", - "shape.site" - ], - "watched": [ - "src/CodoMetis.TypeKit.EntityFrameworkCore", - "src/CodoMetis.TypeKit.Generators" - ] - }, - "text": { - "axes": [ - "text.*", - "process.*", - "shape.kind", - "shape.wrapped" - ], - "watched": [ - "src/CodoMetis.TypeKit/StringTextExtensions.cs", - "src/CodoMetis.TypeKit/TextFault.cs", - "src/CodoMetis.TypeKit.Generators" - ] - }, - "generation": { - "axes": [ - "shape.*" - ], - "watched": [ - "src/CodoMetis.TypeKit.Generators", - "src/CodoMetis.TypeKit/ValueObjects" - ] - }, - "option-result": { - "axes": [ - "or.*", - "shape.wrapped" - ], - "watched": [ - "src/CodoMetis.TypeKit/Option.cs", - "src/CodoMetis.TypeKit/OptionExtensions.cs", - "src/CodoMetis.TypeKit/Result.cs", - "src/CodoMetis.TypeKit/ResultT.cs", - "src/CodoMetis.TypeKit/ResultExtensions.cs", - "src/CodoMetis.TypeKit/SequenceOptionExtensions.cs", - "src/CodoMetis.TypeKit/ValueTypeOptionExtensions.cs" - ] - }, - "analyzers": { - "axes": [ - "shape.*" - ], - "watched": [ - "src/CodoMetis.TypeKit.Analyzers", - "src/CodoMetis.TypeKit/TypeArgumentConstraints.cs" - ] - }, - "logging": { - "axes": [ - "shape.kind", - "shape.wrapped", - "input.json" - ], - "watched": [ - "src/CodoMetis.TypeKit.Serilog", - "src/CodoMetis.TypeKit/Refusals.cs" - ] - } - }, - "out-of-reach": { - "postgresql": "needs a server; the EF probes run SQLite in memory, and the EF translation claims are checked by translation alone", - "native-aot": "measured only by the consumer smoke test (tools/ConsumerSmokeTest), minutes and a platform linker; no probe publishes with Native AOT" - }, - "refused": { - "shape.site": { - "in-generic-type": "CMTK1005: the generators refuse a value object nested in a generic type at build (Generators README, Build errors)" - }, - "json.ReadCommentHandling": { - "Allow": "System.Text.Json refuses it on the options object" - }, - "json.DefaultIgnoreCondition": { - "Always": "System.Text.Json refuses it on the options object" - } - }, - "replay": { - "project": "test/CodoMetis.TypeKit.Samples", - "probeDir": "test/CodoMetis.TypeKit.Samples/Probes", - "prepare": [ - [ - "dotnet", - "build", - "tools/BuildVerdict", - "-c", - "Release", - "-nologo", - "-v:q" - ] - ], - "build": [ - "dotnet", - "tools/BuildVerdict/bin/Release/net10.0/build-verdict.dll", - "build", - "{project}" - ], - "test": [ - "dotnet", - "tools/BuildVerdict/bin/Release/net10.0/build-verdict.dll", - "test", - "--no-build", - "--project", - "{project}", - "--filter-class", - "{filter}", - "--", - "--results-directory", - "{results}" - ], - "coverage": [ - "--coverage", - "--coverage-output-format", - "cobertura", - "--coverage-output", - "{cobertura}" - ], - "families": { - "http": [ - "http" - ], - "ef": [ - "ef" - ], - "json": [ - "json" - ], - "text": [ - "text", - "logging" - ], - "build": [ - "generation", - "analyzers", - "option-result" - ] - }, - "include": [ - "src" - ], - "compileTimeFootprint": [ - "src/CodoMetis.TypeKit.Generators", - "Directory.Packages.props", - "Directory.Build.props", - "global.json", - "test/CodoMetis.TypeKit.Samples/CodoMetis.TypeKit.Samples.csproj" - ], - "coverageTargets": "\n \n \n \n\n", - "toolchain": { - "dotnet": [ - "dotnet", - "--version" - ] - }, - "enumerate": { - "script": "tools/probe/enumerate.cs", - "inputs": [ - "Directory.Packages.props", - "global.json" - ] - } - } -} -``` - - -## Oracles this repository promises - -- **JSON parity (differential).** A shape with a value object and the same shape with the wrapped - type, serialized under the same options, give the same bytes, and each reads the other's JSON. - `test/CodoMetis.TypeKit.Generators.Tests/JsonParityTests.cs` shows the pattern. -- **Schema parity (differential).** The OpenAPI schema of a value-object property equals the one - ASP.NET Core publishes for the wrapped type in the same position, in every document version. -- **Wire and document agree (differential).** The statuses and problem types a fault response sends - are the ones the document lists for that operation, in both hosting models. -- **Column parity (differential).** The EF model gives a value-object property the wrapped type's - column type, and a query over `.Value` translates to the same SQL as the query over the wrapped - property. -- **One set of rules (differential).** `TryFrom`, `Parse`, `TryParse`, the JSON converter, the type - converter and `FromKnownGood` accept exactly what `Create` accepts, and every refusal names the - fault `Create` returned. -- **No refusal quotes the input (exception).** Put a sentinel such as `SECRET-7f3a` in refused input; - it must not appear in any exception message, inner exception, `ToString()`, ProblemDetails body or - log line. The one exception is a dictionary key in `JsonException.Path`. -- **Off the wire (exception).** `Option` and `Result` throw `NotSupportedException` from - System.Text.Json in both directions, in every position, unless the application registered its - own converter. -- **Equality (metamorphic).** Equal value objects have equal hash codes, `CompareTo` is zero exactly - when `Equals` is true, and both agree with the wrapped type's, through every base in the - inheritance chain. -- **Round trip (metamorphic).** `Parse(x.ToString())`, a JSON write then read, and a column write - then read give back an equal value object. -- **Loud, not silent (exception).** A declaration that cannot be generated fails the build with a - CMTK id; it never compiles into a type with nothing in it. - -## Intended: a refusal or difference here is a pass - -- A `DateTime` is written as UTC; number handling set by an attribute on the containing type reaches - no converter (README, "What TypeKit guarantees"). -- A struct value object refuses a JSON `null`. -- A rejected dictionary key appears in `JsonException.Path`, and so in MVC's 400. -- Under a source-generated context, a value object over an enum needs the enum listed in the context. -- A validated value object has no `From`; an abstract value object fails with CMTK1022, and a generic - wrapped type with CMTK1005. -- A missing `Option` property, and an empty collection or dictionary of `Option` or `Result`, pass - through System.Text.Json without an exception, since no converter runs (a wording question for the - README, raised 2026-10-15, not a finding). -- The EF Core package and `StoredJsonConverterFactory` skip validation by design. -- A host whose MVC and minimal-API JSON options describe a type differently has its document refused - (`DescribedOptionsAgreement`). -- System.Text.Json refusing a combination of its own options with an exception of its own (for - example `IgnoreNullValues` together with a `DefaultIgnoreCondition`). -- Anything a record in `design/adr/` decides. Search the records before calling a refusal a finding. -- Findings already reported by `code-analyst` are in - `.claude/agent-memory-local/code-analyst/review-2-1-0-findings.md`. Do not report them again. - -## Budget - -Twelve claims a round unless the caller says otherwise. One build of the Samples project serves them -all; group the round's probes there. diff --git a/.claude/settings.json b/.claude/settings.json index 5429aeeb..4c835160 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -11,7 +11,7 @@ }, { "type": "command", - "command": "dotnet build tools/AgentTools.slnf -c Release -nologo -v q -tl:off -clp:NoSummary >/dev/null 2>&1 || PATH=\"/usr/local/share/dotnet:$PATH\" dotnet build tools/AgentTools.slnf -c Release -nologo -v q -tl:off -clp:NoSummary || echo \"Session start: the tools in $PWD (build-verdict, prober, docs-query, consumer-smoke-test) did not build with the dotnet on PATH, nor with the installer's in /usr/local/share/dotnet; a dotnet without the SDK global.json pins is the usual cause. Run dotnet build tools/AgentTools.slnf -c Release to see why, and before using them.\"", + "command": "dotnet build tools/AgentTools.slnf -c Release -nologo -v q -tl:off -clp:NoSummary >/dev/null 2>&1 || PATH=\"/usr/local/share/dotnet:$PATH\" dotnet build tools/AgentTools.slnf -c Release -nologo -v q -tl:off -clp:NoSummary || echo \"Session start: the tools in $PWD (build-verdict, docs-query, consumer-smoke-test, pr-hygiene) did not build with the dotnet on PATH, nor with the installer's in /usr/local/share/dotnet; a dotnet without the SDK global.json pins is the usual cause. Run dotnet build tools/AgentTools.slnf -c Release to see why, and before using them.\"", "timeout": 300 } ] diff --git a/.claude/skills/probe-triage/SKILL.md b/.claude/skills/probe-triage/SKILL.md deleted file mode 100644 index 1373245c..00000000 --- a/.claude/skills/probe-triage/SKILL.md +++ /dev/null @@ -1,261 +0,0 @@ ---- -name: probe-triage -description: Decide what one failing probe row means, by four questions in a fixed order — a harness error, a pinned framework fact, an intended difference the claim does not promise, or a finding for the code-analyst — stopping at the first that applies and recording the step where the corpus keeps it. Use for every failing row at the end of a probing round, for a `prober status` finding (`R--`, `F`), for a row a replay or `prober try` printed as failed, and whenever someone asks whether a probe failure is real. ---- - -# Probe triage - -A failing row is a question, not a finding. Most of what the prober has cost went into answering it by -judgement, mid-round: the gate round of 2026-10-16 wrote 18 probes, found two product defects and about -ten harness traps, and left five failing rows of C07 untriaged. This skill answers the question by -lookup, in a fixed order, so two people (or a person and an agent) reach the same answer for the same -row, and the answer is written where the next replay and the next reader find it. - -**The order is the rule.** Ask the four questions in turn and stop at the first that applies. Do not skip -ahead because a row "looks like" a product bug: a harness that compares unlike things produces the most -convincing product bugs there are. Every outcome names its step, `triage 1` to `triage 4`. - -The writer of a probe never decides what a refusal means. The probe checks; this skill decides. - -## Before the steps: get the row, and make it fail again - -You start from one of these: - -- **A finding id** from `prober status`: `R--` (the replay saw a probe that passed with the - same identity fail) or `F`. Its last line in the branch's `findings.jsonl` names the probe and the - commit. The state directory, `$STATE` below, is `PROBE_STATE_DIR` when set, else the main checkout's - `.claude/agent-memory-local/prober/`; the branch's directory is `branches//`. -- **A row a run printed:** `prober try` and `prober replay` print the failed probe and how each row ended. - A replay that says "fails after a change to its corpus, axes, environment or this tool; triage the probe, - not the product" opened no finding: start at step 1 and expect to stop there. -- **A row's evidence line:** the line `Rec.Run` writes, or the probe's evidence after a replay: - - ```bash - jq '.rows["358"] | {category, reasons, applied}' "$STATE/branches/release-2.2.0/evidence/BaseProbe.json" - ``` - -From it, write down: the claim, the probe, the row's index, its category (`violation`, `harness-error`, -`inconclusive`), its reasons, and the settings that applied. Then run the row at HEAD, three times: - -```bash -dotnet tools/Prober/bin/Release/net10.0/prober.dll try --probe BaseProbe --rows 358 -dotnet tools/Prober/bin/Release/net10.0/prober.dll try --probe BaseProbe --rows 358 --no-build # twice more -``` - -A row that passes at HEAD was fixed meanwhile: say so with the commit, and stop. A row that does not -fail every time is flaky, and flaky is not a finding: record it as `flaky` (a round's ledger line, or -your report) and stop. Only a -row that fails every time goes on to step 1. - -## 1. Is it a harness error? - -**It applies when** the row's reasons carry `harness-error`, or the failure comes from the harness rather -than from the product: - -- the category is `harness-error`: a check threw (`UNEXPECTED : ...`, which decision 103 makes - a harness error and never a violation), a check `prober new` staged and nobody wrote, or the equivalence - precondition (`inputs not equivalent: wrapped [...] vs value object [...]`), which says the two sides - were not the same shape and so were never comparable; -- a 404 on a bind route, a context type the source-generated `ProbeContext` does not list, a host that did - not start for a reason the row's settings do not explain; -- a `violation` whose whole difference lies in something the harness built or named rather than something - TypeKit wrote: a component or route name made from the twin's own type name, a twin at another - nullability, kind or position than the value object (concept H2), an input the harness placed - differently on each side. Read the two sides' answers in the reasons, or in the full log of the `try` - run, and find the first byte that differs. If it is the harness's, this step applies. - -**Then** fix the harness so the row compares like with like, and run the row again. No finding is filed. -A row that now fails with a different difference starts again at step 1. - -**Record:** - -- the harness fix, as a `test:` commit whose body names the row and says `triage 1`: what the harness - did and what it does now (94554b3 is the model). In a round, the fix goes to the staging's `harness/` - and the round's report says it; the session that publishes commits it; -- with a finding id, a line appended to the branch's `findings.jsonl`: - - ```json - {"schemaVersion":1,"id":"R-BaseProbe-1a2b3c4","status":"withdrawn","reason":"triage 1 harness-error: the /schema/ endpoint had no twin at the value object's nullability; fixed in 94554b3"} - ``` - -## 2. Does a fact at the pinned version explain it? - -**It applies when** a fact in `design/knowledge/` says the framework, without TypeKit, does what the row -saw: the same refusal at the same operation, or the same document, at the version the repository pins. -A fact about the framework explains a row only where the framework alone makes the difference. A fact -that describes how the framework treats the wrapped type, while the value object differs from it, does -not explain the row; it is the evidence of step 4. - -**Check with** the framework, the setting and the operation the row names: - -```bash -dotnet tools/DocsQuery/bin/Release/net10.0/docs-query.dll find controller API explorer document visible --kind fact -dotnet tools/DocsQuery/bin/Release/net10.0/docs-query.dll entry KB-002 -dotnet tools/DocsQuery/bin/Release/net10.0/docs-query.dll check # no fact-behind or fact-ahead for it -``` - -A miss of `find` proves nothing: try the setting's own name and the exception type too. Read the fact's -frontmatter. It is **executable** when its `test:` line names a test under `test/CodoMetis.TypeKit.Facts` -(`test: test/CodoMetis.TypeKit.Facts/KB002.cs`). Run that test: - -```bash -dotnet tools/BuildVerdict/bin/Release/net10.0/build-verdict.dll test --project test/CodoMetis.TypeKit.Facts --filter-class "*KB002" -``` - -**Then:** - -- **An executable fact that passes and explains the row** makes it a framework refusal. The check is - changed to cite the fact, no finding is filed. -- **A fact whose pin is stale**, or whose test fails, settles nothing until it is verified again - (decision 93). Verify it first. -- **A fact without a test** (`test: none, ...` or `prose`) that would explain the row is a research task: - measure the behaviour in `test/CodoMetis.TypeKit.Facts` as `KB.cs`, give the fact its `test:` - line, then cite it. A fact without a test may be cited in a reason and nothing more: it never prunes a - row and never excuses one. -- **No fact, but the row's own baseline shows the framework alone doing it** (the document - `Hx.DocRefusedByAspNetAlone` refuses for ASP.NET alone too, or the wrapped type alone refuses under the - same options): that is a new framework behaviour. It becomes a new fact with its test under - `design/knowledge/` (its README, "Adding one"), never a new note key. Then cite it. -- Otherwise go to step 3. - -**Record:** - -- the citation, where the corpus holds one: a `not-promised` entry of the claim whose `source` is the - fact's file and whose `sentence` is quoted from it (step 3's form), an `exclude` group whose `reason` - names `KB-NNN`, or a value in the axes file's `refused` object whose text names `KB-NNN`, or the check's - own `Cite.Fact("KB-NNN")`; -- with a finding id: - - ```bash - dotnet tools/Prober/bin/Release/net10.0/prober.dll dismiss --id R-C03ComposedProbe-1a2b3c4 --reason "triage 2 fact KB-002: a controller without [ApiController] is missing from the document for ASP.NET alone, so the row's document part has nothing to check" - ``` - - `dismiss` stages the line; `prober publish` puts it in `tools/probe/corpus/dismissed.jsonl`; -- in a round, a fact to measure goes in the report under "Facts to measure", with the row and the - configuration. The agent writes no test; the session that takes the report does. - -## 3. Does the claim say it does not promise this? - -**It applies when** a `not-promised` entry of the row's claim covers the row's settings, or a sentence of -the docs or of a decision record says the difference is intended. - -**Check with** the claim's entries and narrowings, and the docs: - -```bash -dotnet tools/Prober/bin/Release/net10.0/prober.dll gaps | grep 'C07-fault-responses-blanket' -jq '.[] | select(.id == "C07-fault-responses-blanket") | {narrow, "not-promised"}' tools/probe/corpus/claims.json -dotnet tools/DocsQuery/bin/Release/net10.0/docs-query.dll find nullable value object schema null --top 5 -``` - -An entry covers the row when each of its `axes` values is among the row's applied settings. A docs -sentence counts only when it says this difference, in this place, is intended. Your sense that it "must be -on purpose" does not count, and neither does the axes file's "Intended" list alone: an item there that no -document or record states is a gap of this step, not an answer. - -**Then:** - -- **An entry covers it:** the difference is intended. The row's check cites the entry by its id, no - finding is filed. -- **A document or record says so, but the claim has no entry:** that is a docs or claims gap, filed as - `docs:` work, no finding. Add the entry; if the consumer docs do not say it plainly, add the sentence - first (the `plain-docs` skill), then quote it. -- **Two documents disagree,** or a record calls intended what a consumer page promises: not this step's - to settle. It goes to the code-analyst as a question (step 4's hand-off, "a contradiction"). -- Otherwise go to step 4. - -**Record:** - -- the entry, in `claims.json` (in a round, the staging's `claims.json`), in the form of - `tools/Prober/Schemas/claims.schema.json`. `universe` refuses a sentence its `source` does not hold: - - ```json - "not-promised": [ - { - "id": "invariant-globalization-refuses-as-text", - "source": "docs/text.md#globalization-invariant-mode", - "sentence": "So `AsText()` throws `PlatformNotSupportedException` there, on its first call, whatever the string.", - "axes": { "text.culture": ["globalization-invariant"], "entry": ["AsText", "AsOptionalText"] } - } - ] - ``` - - committed as `docs:` with the sentence it quotes, beside any docs change; -- with a finding id: - - ```bash - dotnet tools/Prober/bin/Release/net10.0/prober.dll dismiss --id R-C02OptionalTextProbe-1a2b3c4 --reason "triage 3 not-promised C02-optional-text/invariant-globalization-refuses-as-text" - ``` - - and for a gap, `--reason "triage 3 docs-gap: ; entry to add"`. - -## 4. A finding: hand it to the code-analyst - -Only a row that none of steps 1 to 3 explains gets here. It is still not a finding until the -`code-analyst` agent, which reads the product path and seeds the defect in a scratch copy, confirms it. - -**Prepare the hand-off:** - -- **The row**: claim, probe, index, commit, category, the reasons, the settings that applied. -- **The shrunk row**, from the harness's `Shrinker`. There is no prober command for it yet. Run it in the - replay's worktree, which the `try` above has just built with the corpus overlaid: - - ```bash - WT=$(dotnet tools/Prober/bin/Release/net10.0/prober.dll replay --where | awk '/^replay:/ {print $2}') - cd "$WT" && PROBE_SHRINK="C07-fault-responses-blanket:358:baseline" \ - dotnet "$OLDPWD/tools/BuildVerdict/bin/Release/net10.0/build-verdict.dll" test --no-build \ - --project test/CodoMetis.TypeKit.Samples --filter-class "Probes.ShrinkProbe" - cat /tmp/shrink.txt - ``` - - `PROBE_SHRINK` is `::`: the rows file the probe reads (`Rows.Data("...")` in the - probe), the rows, and the check, `baseline` for the HTTP calibration or a name registered in - `ShrinkChecks.All` (C03, C05, L02, L03, L04). A check with no name there is handed over unshrunk, and - the hand-off says so. -- **The obligation** the failing check serves: its id and sentence from the claim's `obligations` (the - probe's header quotes them). When the check serves none of the claim's obligations, say so: "none - listed; the check holds : ". That is itself something for the analyst to - report (below). -- **Each side's answer**: the wrapped twin's with TypeKit, the value object's with TypeKit, and the - framework alone where the row ran it, each as the reasons or the log give it. -- **Why steps 1 to 3 did not apply**, one line each, with the facts and entries you looked at. - -**Hand it over** with the Agent tool, `subagent_type: code-analyst`, and a prompt that starts -`Target: triage hand-off` and carries the five items above. The analyst reads the path the row exercised, -reproduces it outside the probe, seeds the defect, and answers Confirmed, Likely or Question. Where you -cannot start an agent (the `prober` agent started as a subagent), put the hand-off in your report under -"For code-analyst", marked "awaiting confirmation", file nothing, and let the caller hand it over. - -**Then:** - -- **Confirmed:** file `F`, the next number not used in the branch's `findings.jsonl` or the corpus's - `dismissed.jsonl`. The fix is a `fix:` commit with the regression test the analyst named (the - `verify-the-guard` skill: revert it, then the variants), ending with `Defect-Class:` and - `Found-By: PR`, and its body names the analyst's confirmation. 6866822 is the model. -- **Likely or Question:** no `F`. The row stays failing, and the question goes to the maintainer in - the round's report under "For code-analyst", with the analyst's answer. -- **A missing obligation** the analyst reports (a check that holds a promise no claim lists): it is filed - as claims work, the obligation added to the claim whose source states it, or a new claim, with its seed - (decision 104). - -**Record**, only after the analyst confirms, a line appended to the branch's `findings.jsonl`, with the -shrunk probe kept in the memory's `open/`: - -```json -{"schemaVersion":1,"id":"F3","claim":"C07-fault-responses-blanket","status":"open","commit":"00d3b8f6...","probe":"open/F3NullableBodySchemaProbe.cs","reason":"triage 4 finding: an MVC [FromBody] VO? over ulong writes oneOf[null, {type:[integer,string]}] where ulong? writes [null,integer,string]; confirmed by code-analyst 2026-10-16"} -``` - -An `R-` finding that became `F` gets a `withdrawn` line whose reason is `triage 4 finding: filed as F`, -so one defect is open once. - -## What each step leaves behind - -| Step | Finding | Change | Commit | -|---|---|---|---| -| 1 harness error | `withdrawn`, `triage 1 harness-error: ...` | the harness | `test:` | -| 2 fact | `prober dismiss`, `triage 2 fact KB-NNN: ...` | the check cites the fact; a new fact gets its test first | `test:` for the citation, `docs:` or `test:` for the fact | -| 3 not promised | `prober dismiss`, `triage 3 not-promised /` or `triage 3 docs-gap: ...` | the `not-promised` entry, the docs sentence | `docs:` | -| 4 finding | `F` open, `triage 4 finding: ...; confirmed by code-analyst` | the fix and its test | `fix:` with `Found-By: PR` | - -A row without a finding id leaves the change and the commit alone. Every reason starts with -`triage `, so `grep '"triage [123]' findings.jsonl dismissed.jsonl` counts what the rounds cost -and what they found. diff --git a/.claude/skills/verify-the-guard/SKILL.md b/.claude/skills/verify-the-guard/SKILL.md index 1db2c705..b83c563d 100644 --- a/.claude/skills/verify-the-guard/SKILL.md +++ b/.claude/skills/verify-the-guard/SKILL.md @@ -51,8 +51,8 @@ before it is reported done: | `AIS`, what a type argument is | `TypeArgumentKinds`, beside it, 79 kinds and 37 pairs: classes to delegates, arrays, `dynamic`, interfaces, the abstract bases, a type built from a parameter, a parameter under each constraint and attribute, and pairs | An oracle per constraint over what each kind is, from the rule's docs, run as `TypeArgumentConstraintKindTests` runs CMTK0010 | | `AIS`, any other input | The kinds the rule registers (`Register*Action`), plus method groups, crefs, extension blocks (method, property, static member, operator, the lowered call), generic attributes, `nameof`, null-conditional access, lambdas, local functions and generated code (`GeneratedCodeTests`) | No catalogue yet: build it in `Variants/` on `TypeArgumentSites`' pattern, the subject from the rule and the shapes from the catalogue | | `EFT`, a key or a query of the EF satellite | `KeyShapes`, `QueryShapes` and `WrappedKinds` in `test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/Variants/`: 13 key shapes (alone, each mapping's root, abstract or not, the dependents whose key is a foreign key, composite, configured) by 13 wrapped types on three providers; 22 wrapped types, own orderings included, by accessor, site (column, nullable, collection element, captured list, navigation, JSON, query filter), converter (TypeKit's, named, three foreign forms) and operation | A differential against the same entity over the wrapped type, with the docs' departures as the oracle, run as `KeyGenerationVariantTests` and `QueryTranslationVariantTests` do; a new case is a new value of an axis | - | `GTS` | The `shape.*` axes of `.claude/probe-axes.md` and the Samples' value objects | One declaration per value of each axis the fixed code reads (kind, site, inheritance at least), or the rows of the claim that covers it: `prober rows --claim C --full` | - | `FCI` | The settings of the API the fix touches: the `json.*`, `openapi.*`, `mvc.*` axes | The claim's pairwise rows, `prober rows --claim C`, as a theory or as the claim's probe | + | `GTS` | The declaration shapes of `test/oracles/axes.md` and the Samples' value objects | One declaration per value of each axis the fixed code reads (kind, site, inheritance at least) | + | `FCI` | The settings of the API the fix touches: the JSON, OpenAPI and host axes of `test/oracles/axes.md` | A theory over every value of the axes the fix reads, or a probe class over their product (`test/oracles/axes.md`) | Run every shape both ways where the catalogue has two: the breaking input reported (or refused) once, the meeting one never. A false report is a sibling as much as a silence is. diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 4d3e20e4..94e23d85 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -8,6 +8,13 @@ "dotnet-CycloneDX" ], "rollForward": false + }, + "docfx": { + "version": "2.81.0", + "commands": [ + "docfx" + ], + "rollForward": false } } } diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 00000000..7c8e76a5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,69 @@ +name: Bug +description: Something compiles but does less than it should, throws when it shouldn't, or doesn't refuse when it should. +labels: [bug] +body: + - type: markdown + attributes: + value: | + The bugs that matter most here don't crash. A value object builds but was never generated, a + failed `Result` goes unnoticed, an analyzer stops reporting, a column reads back a value the + type would have refused. If that's what you found, please say so below. It decides how soon + the fix ships. + - type: dropdown + id: package + attributes: + label: Package + multiple: true + options: + - CodoMetis.TypeKit + - CodoMetis.TypeKit.Analyzers + - CodoMetis.TypeKit.Generators + - CodoMetis.TypeKit.EntityFrameworkCore + - CodoMetis.TypeKit.AspNetCore + - CodoMetis.TypeKit.Serilog + - CodoMetis.TypeKit.Testing + validations: + required: true + - type: input + id: version + attributes: + label: Version + placeholder: "2.2.0" + validations: + required: true + - type: dropdown + id: failure + attributes: + label: How does it fail? + options: + - Silently. It compiles or answers, and does less than it appears to + - Loudly. It throws, fails the build or reports a diagnostic where it should work + - I'm not sure + validations: + required: true + - type: textarea + id: repro + attributes: + label: What you ran + description: The smallest code that shows it. For EF Core, include the query and the SQL it translated to; for JSON or OpenAPI, the payload or the schema. + render: csharp + validations: + required: true + - type: textarea + id: actual + attributes: + label: What you got + validations: + required: true + - type: textarea + id: expected + attributes: + label: What you expected + description: If a docs page or a README says what should happen, link it. + validations: + required: true + - type: input + id: environment + attributes: + label: Environment + description: .NET SDK version, OS, whether you publish with Native AOT, and the database provider if EF Core is involved. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..d99c7094 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Security report + url: https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/security/advisories/new + about: Report a vulnerability privately, as SECURITY.md describes. Please don't open a public issue for it. diff --git a/.github/ISSUE_TEMPLATE/enhancement.yml b/.github/ISSUE_TEMPLATE/enhancement.yml new file mode 100644 index 00000000..eb3cb593 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement.yml @@ -0,0 +1,28 @@ +name: Enhancement +description: A new member, generated feature, analyzer rule, satellite or option. +labels: [enhancement] +body: + - type: textarea + id: problem + attributes: + label: What you're trying to do + description: The task, not the API. What do you write today, and why doesn't it work? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: What you'd like + description: A sketch of the code you'd write with it. + render: csharp + - type: dropdown + id: breaking + attributes: + label: Does it change something that compiles or works today? + description: A change to an existing signature, a generated member, a wire form or a diagnostic's severity needs a major version, even if the new behaviour is better. + options: + - No, it only adds + - Yes, existing code would compile or behave differently + - I'm not sure + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/epic.yml b/.github/ISSUE_TEMPLATE/epic.yml new file mode 100644 index 00000000..b408153d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/epic.yml @@ -0,0 +1,28 @@ +name: Epic +description: A release or a theme. Its sub-issues are the work. (Maintainers.) +title: "Epic: " +labels: [epic] +body: + - type: textarea + id: goal + attributes: + label: Goal + description: What is true when this is done, in one or two sentences. For a release, link its section of design/roadmap.md. + validations: + required: true + - type: textarea + id: scope + attributes: + label: In scope and out of scope + validations: + required: true + - type: textarea + id: done + attributes: + label: Done when + description: Every sub-issue is closed, plus anything that isn't an issue (a tag, a published package, a doc). + validations: + required: true + - type: markdown + attributes: + value: Add the work as sub-issues once this exists. For a release, set the milestone on the epic and on every sub-issue. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..bd2c952d --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,22 @@ +Closes # + + + +## Problem + + + +## Change + + + +## Tests + + diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..d13c205b --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,139 @@ +# Builds the docs site from the guides in docs/, the root README, the package READMEs, the changelog and the API of +# the run-time packages, and publishes it to GitHub Pages from main (#115, design/adr/0116-the-docs-site-is-docfx-built-from-main.md): +# +# https://caffeinatedcoder.github.io/CodoMetis.TypeKit/ +# +# Only main is published, because main is the released state: topics merge into release/, which merges into +# main when it is tagged, so the site always describes the version `dotnet add package` installs. A release branch +# is never published. A second line under its own path (/2.x/, /3.x/) waits until two lines are supported at once; +# CONTRIBUTING.md ("The docs site") says how one would be added. +# +# A pull request into main or a release branch that touches what a page is built from builds the site and publishes +# nothing. DocFX runs with warnings as errors, so a link to a missing page, a missing heading or a missing file of the +# repository fails the pull request rather than the deployment. +# +# The pages are the Markdown files as they are: nothing copies or edits them, so ReadmeSampleTests and +# OracleCatalogueTests hold the text readers see. A link to a file of the repository that is not a page (design/, +# CONTRIBUTING.md) becomes a GitHub address at the built commit in site/template, at build time. +# +# Pages is configured to deploy from GitHub Actions (Settings > Pages > Source), the one setting no workflow can make. +# +# Actions are pinned to commit SHAs, as in dotnet.yml. + +name: Docs + +# Everything a page is built from: the Markdown, the XML docs in src/, the site's config, and the SDK and tool pins. +# Keep the two lists the same. +on: + push: + branches: [ "main" ] + paths: + - "*.md" + - "LICENSE" + - "docs/**" + - "design/**" + - "src/**" + - "site/**" + - "global.json" + - "Directory.Build.props" + - "Directory.Packages.props" + - ".config/dotnet-tools.json" + - ".github/workflows/docs.yml" + pull_request: + branches: [ "main", "release/**" ] + paths: + - "*.md" + - "LICENSE" + - "docs/**" + - "design/**" + - "src/**" + - "site/**" + - "global.json" + - "Directory.Build.props" + - "Directory.Packages.props" + - ".config/dotnet-tools.json" + - ".github/workflows/docs.yml" + workflow_dispatch: + +permissions: + contents: read + +defaults: + run: + shell: bash + +# A pull request's run is superseded by its next push. Publishing runs queue instead, so a deployment is never +# cancelled halfway and the last one to finish publishes the latest main. +concurrency: + group: ${{ github.event_name == 'pull_request' && format('docs-{0}', github.ref) || 'docs-publish' }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + +jobs: + build: + name: Build the site + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false + + - name: Setup .NET + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0 + with: + global-json-file: global.json + + # The API pages are read from published assemblies, which carry the XML docs and every dependency DocFX needs + # to resolve a signature. The packages are the ones site/docfx.json names, so the two cannot disagree. + - name: Publish the run-time packages + run: | + for assembly in $(jq -r '.metadata[].src[].files[]' site/docfx.json); do + dotnet publish "src/${assembly%.dll}" -c Release -o site/obj/pub + done + + - name: Build the site, warnings as errors + run: | + dotnet tool restore + dotnet docfx site/docfx.json --warningsAsErrors --metadata _commit="$GITHUB_SHA" + + # The members of C# 14 extension blocks are listed as extension methods, on their static class and on the type + # they extend: measured with DocFX 2.81.0 (decision 116). A DocFX that stops listing them fails here, not on + # a reader who finds Option's page without its combinators. + - name: The extension-block members are listed + run: | + grep -qF ' - CodoMetis.TypeKit.Option`1.CodoMetis.TypeKit.Option.Select``2(' site/obj/api/CodoMetis.TypeKit.Option-1.yml + grep -qF ' - CodoMetis.TypeKit.Result`2.CodoMetis.TypeKit.Result.Select``3(' site/obj/api/CodoMetis.TypeKit.Result-2.yml + + # The files a link of a page names to be checked, which the site links on GitHub instead (site/template). + - name: Leave the repository's own files out + run: rm -rf site/obj/_site/_repository + + - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' + with: + path: site/obj/_site + + publish: + name: Publish + needs: build + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deploy.outputs.page_url }} + + steps: + - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 + + - id: deploy + uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1 diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 0924e153..ea2bc800 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -1,6 +1,10 @@ -# Build, test and pack on every push to main or a release branch and every PR into one; a PR packs -# only when its diff can change a package (the changes job). Topics -# merge into release/ and the release branch into main +# Build, test and pack on every push to main or a release branch and every PR into one, in lanes: the Linux +# test leg always, the Windows leg and the pack (Pack and Consumer) when the change can affect them, and the +# macOS leg only in a run that selects nothing. +# tools/CiLanes selects them from the changed paths in the changes job, and the CI job judges the run: every +# selected job passed and every other was skipped (design/adr/0113-ci-runs-the-lanes-a-change-can-affect.md). +# A PR into main, a push to main, a dispatch, the nightly run (nightly.yml) and the release (release.yml calls +# this workflow) run every lane. Topics merge into release/ and the release branch into main # (design/adr/0094-topic-branches-into-a-release-branch-and-fix-trailers.md). # # The tests run on Linux, Windows and macOS: a contributor needs git and the pinned SDK on any of @@ -27,6 +31,8 @@ on: pull_request: branches: [ "main", "release/**" ] workflow_dispatch: + # release.yml runs every lane before it verifies. Its event is the tag's push, which selects every lane. + workflow_call: permissions: contents: read @@ -35,9 +41,11 @@ defaults: run: shell: bash -# A new push to a PR makes the in-flight run pointless; main is never cancelled. +# A new push to a PR makes the in-flight run pointless; main is never cancelled. The prefix keeps a call from +# release.yml out of its caller's group: a called workflow's github.workflow is the caller's name, and groups +# ignore case, so without it both would be release- and the call would wait for its caller (KB-043). concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ci-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: @@ -46,16 +54,17 @@ env: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true jobs: - # Whether this run can change a package or a trimmed consumer: on a push and in the release always, on - # a pull request only when the diff against the base branch touches a shipping project, a build - # property file, a project file, the SDK pin, the package versions, a workflow or the smoke test - # itself. The pack and consumer jobs below run on that answer; the deciding paths are in this job's log. + # Which lanes this run needs, from the paths it changes (tools/CiLanes, design/adr/0113): a PR into a release + # branch against its base, a push to one against the commit it replaced, every other run every lane. The job + # summary lists each path's class. A diff that fails fails this job, and the CI job with it, never skips a lane. changes: name: Changes runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 10 outputs: - product: ${{ steps.product.outputs.product }} + legs: ${{ steps.select.outputs.legs }} + pack: ${{ steps.select.outputs.pack }} + suite: ${{ steps.select.outputs.suite }} steps: # The whole history: the diff is against the base branch, which a shallow clone has not fetched. @@ -63,47 +72,38 @@ jobs: with: fetch-depth: 0 - # Bash runs with -e; `git diff` of a base branch that was not fetched fails the job rather than - # answering false, so a broken diff can never skip the pack. - - name: Can the diff change a package? - id: product - run: | - if [ "$GITHUB_EVENT_NAME" != pull_request ]; then - echo "product=true" >> "$GITHUB_OUTPUT" - echo "A $GITHUB_EVENT_NAME: the packages and the consumer are always built." - exit 0 - fi - git diff --name-only "origin/$GITHUB_BASE_REF...HEAD" > changed-paths.txt - product=false - while IFS= read -r path; do - case "$path" in - global.json) reason='global.json' ;; - Directory.Packages.props) reason='Directory.Packages.props' ;; - src/*) reason='src/' ;; - Directory.*.props|*/Directory.*.props) reason='Directory.*.props' ;; - Directory.*.targets|*/Directory.*.targets) reason='Directory.*.targets' ;; - *.csproj) reason='*.csproj' ;; - .github/workflows/*) reason='.github/workflows/' ;; - tools/ConsumerSmokeTest/*) reason='tools/ConsumerSmokeTest/' ;; - *) continue ;; - esac - echo "$path ($reason)" - product=true - done < changed-paths.txt - echo "product=$product" >> "$GITHUB_OUTPUT" - if [ "$product" = true ]; then - echo "The paths above can change a package or a trimmed consumer: the packages and the consumer are built." - else - echo "No changed path can change a package or a trimmed consumer: the pack and consumer jobs are skipped." - fi + - name: Setup .NET + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0 + with: + global-json-file: global.json + # A tool project, run as its built dll (design/adr/0092). + - name: Build the selection + run: dotnet build tools/CiLanes -c Release + + - name: Select the lanes + id: select + env: + BASE_REF: ${{ github.base_ref }} + BEFORE: ${{ github.event.before }} + run: > + dotnet tools/CiLanes/bin/Release/net10.0/ci-lanes.dll select + --event "$GITHUB_EVENT_NAME" --ref "$GITHUB_REF" --base "$BASE_REF" --before "$BEFORE" + --output "$GITHUB_OUTPUT" --summary "$GITHUB_STEP_SUMMARY" + + # The legs the changes job selected: Linux in every run, Windows when the change can affect it, macOS only in a + # run that selects nothing. A prose-only change builds and runs test/Prose.slnf alone, the test projects that + # read prose (ProseReaderTests holds the set); anything else, the whole solution. test: name: Test (${{ matrix.os }}) runs-on: ${{ matrix.os }} + needs: changes + env: + SOLUTION: ${{ needs.changes.outputs.suite == 'prose' && 'test/Prose.slnf' || 'CodoMetis.TypeKit.slnx' }} strategy: fail-fast: false matrix: - os: [ ubuntu-latest, windows-latest, macos-latest ] + os: ${{ fromJSON(needs.changes.outputs.legs) }} steps: # The whole history and its tags: CommitTrailerTests reads the fix commits since v2.1.0, and @@ -118,19 +118,19 @@ jobs: global-json-file: global.json - name: Restore - run: dotnet restore CodoMetis.TypeKit.slnx + run: dotnet restore "$SOLUTION" - name: Build - run: dotnet build CodoMetis.TypeKit.slnx -c Release --no-restore + run: dotnet build "$SOLUTION" -c Release --no-restore - # The EF Core round trips start a PostgreSQL container. Ubuntu runners have Docker; macOS - # runners have none and Windows runners run only Windows containers, so those legs leave the - # one class that needs it out. + # The EF Core tests on PostgreSQL and SQL Server, and the SQL Server facts, start a Linux container. + # Ubuntu runners have Docker; macOS runners have none and Windows runners run only Windows + # containers, so those legs leave out the classes that need it, the trait Needs=Docker. - name: Test run: > - dotnet test --solution CodoMetis.TypeKit.slnx -c Release --no-build + dotnet test --solution "$SOLUTION" -c Release --no-build --report-xunit-trx --results-directory TestResults - ${{ runner.os != 'Linux' && '--filter-not-class "*PostgreSqlRoundTripTests"' || '' }} + ${{ runner.os != 'Linux' && '--filter-not-trait "Needs=Docker"' || '' }} - name: Upload test results uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 @@ -140,153 +140,16 @@ jobs: path: TestResults/**/*.trx if-no-files-found: ignore - # The probe corpus (tools/probe/corpus), replayed with no model, split by family into legs that run at - # once, each running its probes as many at a time as the runner has cores (design/adr/0101, which amends - # 0096). On a push the whole corpus is replayed from an empty state, with coverage, and the merged state - # is saved for the pull requests into this branch. On a pull request the base branch's saved state is - # restored and only the probes whose identity or footprint the branch changed are replayed, judged - # against the base's footprints, without coverage, since nothing of that state is kept; with no saved - # state every probe runs, as from an empty state. A failing probe fails its leg and the run. Linux only: - # the probes test documented claims, not the platform, and the test job's three legs test the prober. - probes: - name: Probe replay (${{ matrix.family }}) - runs-on: ubuntu-latest - timeout-minutes: 60 - strategy: - fail-fast: false - matrix: - # The families of the axes file's replay configuration, every one: ReleaseWiringTests holds this - # list to that, and `prober replay --family` refuses while a probe's surface is in no family. - family: [ http, ef, json, text, build ] - - steps: - # The whole history: the replay adds a git worktree at HEAD, and `universe` reads the claims - # against origin/main. - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - with: - fetch-depth: 0 - - - name: Setup .NET - uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0 - with: - global-json-file: global.json - - # The prober's state and its replay worktree, in this job's own temporary directory, never a - # developer's (the default without PROBE_STATE_DIR). The state is the branch's: on a pull request - # the base branch's, so that its saved footprints decide what the branch stales. - - name: Prober state directory - run: | - echo "PROBE_STATE_DIR=$RUNNER_TEMP/probe-state" >> "$GITHUB_ENV" - echo "PROBE_CACHE_DIR=$RUNNER_TEMP/probe-cache" >> "$GITHUB_ENV" - echo "PROBE_BRANCH=${{ github.event_name == 'pull_request' && github.base_ref || github.ref_name }}" >> "$GITHUB_ENV" - - # The base branch's state as its last green push saved it; the newest entry under the prefix. With - # none (the first run, or an evicted entry) the replay starts empty and runs every probe. - - name: Restore the base branch's probe state - if: github.event_name == 'pull_request' - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: ${{ runner.temp }}/probe-state - key: probe-state-${{ github.base_ref }}-${{ github.sha }} - restore-keys: probe-state-${{ github.base_ref }}- - - # The prober is a tool project, run as its built dll (design/adr/0092). - - name: Build the prober - run: dotnet build tools/Prober -c Release - - # Bash runs with -e, so the exit code of the replay is the step's. - - name: Replay the family's probes - run: | - if [ "$GITHUB_EVENT_NAME" = pull_request ]; then - dotnet tools/Prober/bin/Release/net10.0/prober.dll replay --branch "$PROBE_BRANCH" --family "${{ matrix.family }}" --no-footprint - else - dotnet tools/Prober/bin/Release/net10.0/prober.dll replay --all --branch "$PROBE_BRANCH" --family "${{ matrix.family }}" - fi - - # The leg's state, for the merge below and for triage of a failure. - - name: Upload the leg's state - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - if: always() - with: - name: probe-state-${{ matrix.family }} - path: ${{ runner.temp }}/probe-state - if-no-files-found: error - - # The legs' states joined into one: `prober merge` refuses while a registered probe was in no leg and - # fails when a leg did, so this job is red whenever the replay is. Then the status in the job summary, - # and on a push the merged state saved for the branch's pull requests. - probe-status: - name: Probe status - runs-on: ubuntu-latest - needs: probes - if: always() - timeout-minutes: 15 - - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - with: - fetch-depth: 0 - - - name: Setup .NET - uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0 - with: - global-json-file: global.json - - - name: Prober state directory - run: | - echo "PROBE_STATE_DIR=$RUNNER_TEMP/probe-state" >> "$GITHUB_ENV" - echo "PROBE_CACHE_DIR=$RUNNER_TEMP/probe-cache" >> "$GITHUB_ENV" - echo "PROBE_BRANCH=${{ github.event_name == 'pull_request' && github.base_ref || github.ref_name }}" >> "$GITHUB_ENV" - - - name: Download the legs' states - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 - with: - pattern: probe-state-* - path: ${{ runner.temp }}/probe-legs - - - name: Build the prober - run: dotnet build tools/Prober -c Release - - - name: Merge the legs - run: dotnet tools/Prober/bin/Release/net10.0/prober.dll merge --branch "$PROBE_BRANCH" --from "$RUNNER_TEMP"/probe-legs/probe-state-* - - # A report: the documented claims' pair coverage and the open findings, in the job summary. It - # fails only when the prober cannot read the claims. `universe` builds what `status` reads. - - name: Probe status - if: always() - run: | - dotnet tools/Prober/bin/Release/net10.0/prober.dll universe --branch "$PROBE_BRANCH" > /dev/null - dotnet tools/Prober/bin/Release/net10.0/prober.dll status --branch "$PROBE_BRANCH" | tee status.txt - { echo '### Probe status'; echo; echo '```text'; cat status.txt; echo '```'; } >> "$GITHUB_STEP_SUMMARY" - - # The branch's state after a green push: what its pull requests replay against. A red push saves - # nothing, so a pull request judges against the last green state and replays more. - - name: Save the branch's probe state - if: github.event_name == 'push' - uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: ${{ runner.temp }}/probe-state - key: probe-state-${{ github.ref_name }}-${{ github.sha }} - - # The merged evidence of every probe, for triage of a failure. - - name: Upload the probe state - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - if: always() - with: - name: probe-state - path: ${{ runner.temp }}/probe-state - if-no-files-found: ignore - # The pack catches a package that names a README it does not pack and a broken SBOM step, and the two # smoke tests a consumer that no longer restores, compiles or publishes with Native AOT against the - # packed feed and against one packed on the spot. A pull request that touches none of the paths the - # changes job lists cannot change any of them, so it skips both, while every push and the release - # (release.yml) still run them. + # packed feed and against one packed on the spot. Both run when the changes job selects the pack lane, + # beside the test legs rather than after them: neither reads the tests' outcome, and the CI job requires + # all of them. The release (release.yml) runs them again on what it publishes. pack: name: Pack runs-on: ubuntu-latest - needs: [ test, changes ] - if: needs.changes.outputs.product == 'true' + needs: [ changes ] + if: needs.changes.outputs.pack == 'true' steps: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 @@ -342,8 +205,8 @@ jobs: consumer: name: Consumer smoke test runs-on: ubuntu-latest - needs: [ test, changes ] - if: needs.changes.outputs.product == 'true' + needs: [ changes ] + if: needs.changes.outputs.pack == 'true' steps: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 @@ -364,3 +227,32 @@ jobs: - name: Consumer smoke test, packing its own feed run: dotnet tools/ConsumerSmokeTest/bin/Release/net10.0/consumer-smoke-test.dll + + # The one required build check: every job the changes job selected succeeded and every other was skipped + # (tools/CiLanes gate, design/adr/0113). A skipped job reads as a passing required check, so the legs + # themselves cannot be required once they may be skipped (KB-043). Under always(), so a failed selection or a + # cancelled leg fails this job rather than skipping it, and a skipped CI is never what a merge sees. + ci: + name: CI + runs-on: ubuntu-latest + timeout-minutes: 10 + needs: [ changes, test, pack, consumer ] + if: always() + + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + + - name: Setup .NET + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0 + with: + global-json-file: global.json + + - name: Build the gate + run: dotnet build tools/CiLanes -c Release + + - name: Every selected job passed, every other was skipped + env: + NEEDS: ${{ toJSON(needs) }} + run: | + printf '%s' "$NEEDS" > "$RUNNER_TEMP/needs.json" + dotnet tools/CiLanes/bin/Release/net10.0/ci-lanes.dll gate "$RUNNER_TEMP/needs.json" --summary "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 00000000..9fb10832 --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,48 @@ +# Runs the build workflow with every lane, once a night, on main and on every release branch +# (design/adr/0113-ci-runs-the-lanes-a-change-can-affect.md). Pull requests into a release branch run only the +# lanes their changes can affect, so this is where the runner images, the SDK band global.json admits, the +# PostgreSQL image and nuget.org's packages in the smoke consumers are met when nothing is pushed. +# +# It dispatches dotnet.yml rather than running it: a schedule runs only from the default branch, and a dispatch +# runs on the branch it names. Events the GITHUB_TOKEN raises start no run, except a dispatch (KB-043), which is +# why the job needs actions: write and nothing more. GitHub turns a public repository's schedule off after 60 +# days without activity, so nothing depends on this: the release calls dotnet.yml itself. + +name: Nightly + +on: + schedule: + - cron: "17 3 * * *" + workflow_dispatch: + +permissions: + contents: read + +defaults: + run: + shell: bash + +jobs: + dispatch: + name: Dispatch the build workflow + runs-on: ubuntu-latest + timeout-minutes: 5 + + permissions: + actions: write # dispatch dotnet.yml; nothing else + + steps: + - name: Dispatch dotnet.yml on main and every release branch + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + branches="$(gh api "repos/$GH_REPO/branches" --paginate --jq '.[].name | select(. == "main" or startswith("release/"))')" + if [[ -z "$branches" ]]; then + echo "::error::No main or release/* branch to dispatch the build workflow on." + exit 1 + fi + while IFS= read -r branch; do + echo "==> dotnet.yml on $branch" + gh workflow run dotnet.yml --ref "$branch" + done <<< "$branches" diff --git a/.github/workflows/pr-hygiene.yml b/.github/workflows/pr-hygiene.yml new file mode 100644 index 00000000..53caa3d1 --- /dev/null +++ b/.github/workflows/pr-hygiene.yml @@ -0,0 +1,60 @@ +# Every pull request into main or a release branch carries one type label, a severity if it is a bug, +# the milestone of the release it goes into, a branch named / that agrees with its type, and a +# closing reference to the issue it resolves; a breaking one goes into a major (CONTRIBUTING.md, "Issues and +# labels"; design/adr/0109-work-is-tracked-as-labelled-issues-and-ci-checks-each-pull-request.md). The rules +# live in tools/PrHygiene, a tool project like the others (design/adr/0092), which reads the event payload +# alone: no token, nothing of the pull request's code run, and runnable locally on a saved payload. +# +# Triggered on label, milestone and description edits as well as pushes, so adding what is missing turns +# the check green without a new commit. `pull_request`, not `pull_request_target`: a pull request from a +# fork runs with a read-only token and never sees a secret, and builds the tool from its own checkout, +# which can only weaken the check on that pull request's own run, where a reviewer reads the diff. +# +# No concurrency group: every event's run completes (#144). The check takes seconds and reads only its +# payload, so cancelling a superseded run saves nothing, and a run cancelled while its job was going leaves +# a cancelled check run, which blocks a required check (#141, #142). Measured on 2026-10-29 on two draft +# pull requests whose labels, milestone and body were set within seconds of opening them, five events each: +# - a group with cancel-in-progress: false still cancels: a newer pending run replaces the older pending +# one, and three of four runs were cancelled. Nothing was blocked, since a run cancelled before its job +# started leaves no check run, but the runs the check is meant to have are missing; +# - no group: all five ran, none was cancelled. Earlier runs failed (their payload had no labels yet) and +# the pull request was CLEAN once CI passed: the newest run decides the required check, not the last to +# finish. With the first run held back 75 s to finish after the one that succeeded, it was CLEAN too. +# A run reads the payload of its own event, so a pull request that fails here is fixed by an event after the +# fix, or by re-running the newest run. +# +# Actions are pinned to commit SHAs, as in dotnet.yml. + +name: PR hygiene + +on: + pull_request: + branches: [ "main", "release/**" ] + types: [ opened, edited, reopened, synchronize, labeled, unlabeled, milestoned, demilestoned ] + +permissions: + contents: read + +env: + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + +jobs: + check: + name: Labels and issue link + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + + - name: Setup .NET + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0 + with: + global-json-file: global.json + + - name: Build the check + run: dotnet build tools/PrHygiene -c Release + + - name: Check labels, milestone, branch and issue link + run: dotnet tools/PrHygiene/bin/Release/net10.0/pr-hygiene.dll "$GITHUB_EVENT_PATH" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2d9df4a8..22930355 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,10 @@ # validates it against a policy configured there, and issues an API key valid for one hour. A # compromise of this repository's secrets yields nothing that can publish. # -# Three jobs on purpose: everything that can fail runs unattended, only the push waits behind the +# First the build workflow, called with every lane (dotnet.yml under workflow_call; design/adr/0113): the +# Windows and macOS legs and both smoke-test modes, which a pull request may have skipped. +# +# Then three jobs on purpose: everything that can fail runs unattended, only the push waits behind the # environment's approval gate, and the GitHub release is created afterwards. A reviewer is asked # once the full suite has already passed, and no OIDC token is minted until they approve. The job # that can mint a token cannot write to the repository, and the job that can write to the repository @@ -66,9 +69,17 @@ env: NUGET_SOURCE: https://api.nuget.org/v3/index.json jobs: + # Every lane of the build workflow, on the commit this run publishes. Its jobs show as "CI / ...". + ci: + name: CI + uses: ./.github/workflows/dotnet.yml + permissions: + contents: read + verify: name: Verify and pack runs-on: ubuntu-latest + needs: ci permissions: contents: read # deliberately nothing more: this job runs unattended and must not mint a token @@ -131,47 +142,11 @@ jobs: run: dotnet build CodoMetis.TypeKit.slnx -c Release # The full suite gates the release, the conventions project included: it holds the analyzer - # release tracking to the changelog, and this workflow to the names above. + # release tracking to the changelog, this workflow to the names above, and the oracle catalogue + # (test/oracles) to the documents, the public surface and its seeds (design/adr/0112). - name: Test run: dotnet test --solution CodoMetis.TypeKit.slnx -c Release --no-build - # The prober's state and its replay worktree, in this job's own temporary directory, never a - # developer's (the default without PROBE_STATE_DIR). - - name: Prober state directory - run: | - echo "PROBE_STATE_DIR=$RUNNER_TEMP/probe-state" >> "$GITHUB_ENV" - echo "PROBE_CACHE_DIR=$RUNNER_TEMP/probe-cache" >> "$GITHUB_ENV" - - # Every probe of the corpus, with no model, as CI replays it on every push (design/adr/0096). A probe - # that fails fails the release, like a test. The Build above built the prober. Every probe's seeds are - # shown caught before the tag, locally, by `prober seeds --all` (AGENTS.md, Releasing): a build per seed, - # which this job does not pay (design/adr/0100), and --all because a run without it skips the probes - # unchanged since their seeds were caught (design/adr/0107). - - name: Replay every probe - run: dotnet tools/Prober/bin/Release/net10.0/prober.dll replay --all - - # The release gate, against the previous release: on a tag push `git describe` alone would name the - # tag being released and compare the commit with itself. It binds from 2.2.0 (design/adr/0097): a - # refusal fails the release, as any other exit does. A refusal is answered by a probe, a fix or a - # reviewed waiver in tools/probe/corpus/waivers.json, never by editing this step. - - name: Probe gate - env: - VERSION: ${{ steps.version.outputs.version }} - run: | - since="$(git describe --tags --abbrev=0 --exclude "v$VERSION")" - code=0 - dotnet tools/Prober/bin/Release/net10.0/prober.dll gate --since "$since" > gate.txt 2>&1 || code=$? - cat gate.txt - case "$code" in - 0) verdict="passes" ;; - 1) verdict="refused" - echo "::error::The probe gate refused $VERSION since $since; see the job summary." ;; - *) verdict="did not run (exit $code)" - echo "::error::The probe gate did not run (exit $code)." ;; - esac - { echo "### Probe gate since $since: $verdict"; echo; echo '```text'; cat gate.txt; echo '```'; } >> "$GITHUB_STEP_SUMMARY" - exit "$code" - - name: Pack run: dotnet pack CodoMetis.TypeKit.slnx -c Release --no-build -o dist diff --git a/AGENTS.md b/AGENTS.md index 06300ff6..ae2b6c1e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,53 +62,27 @@ dotnet test --project test/CodoMetis.TypeKit.Conventions.Tests --filter-method " dotnet build spikes/FabricSpike/FabricSpike.slnx # the fabric spike, standalone dotnet tools/ConsumerSmokeTest/bin/Release/net10.0/consumer-smoke-test.dll # the packages, installed into throwaway consumers, one published with Native AOT (needs the platform linker); build it with -c Release first dotnet tools/BuildVerdict/bin/Release/net10.0/build-verdict.dll test [--project P] [--filter-class C] [--filter-method M] [-- flags] # an agent's build and test: a verdict, not the transcript +dotnet tools/PrHygiene/bin/Release/net10.0/pr-hygiene.dll event.json # a PR's labels, milestone, branch and issue link, as CI's PR hygiene check reads them from the payload +dotnet tools/CiLanes/bin/Release/net10.0/ci-lanes.dll classify ... # the CI lanes a changed path selects (Linux, Windows, the pack; macOS only on the release's runs), as CI's changes job decides them ``` -The probing tool, prober (`tools/Prober`, started as its built dll like docs-query). These need no -model, no login and no API key, and run in CI as anywhere: the `probes` legs of `dotnet.yml` replay -the corpus by family, every probe on a push and the probes the branch stales on a pull request, judged -against the base branch's saved state, and `probe-status` merges the legs and reports -([decision 101](design/adr/0101-ci-replays-by-family-and-footprint.md)); `release.yml` runs `replay --all` -and `gate` before it packs ([decision 96](design/adr/0096-ci-replays-every-probe-and-the-release-runs-the-gate.md)): +The oracle kit's local tool, which needs no model and never runs in CI +([test/oracles/README.md](test/oracles/README.md)): ```bash -dotnet tools/Prober/bin/Release/net10.0/prober.dll status # coverage of the branch's documented claims, open findings -dotnet tools/Prober/bin/Release/net10.0/prober.dll replay # re-run the probes whose identity or footprint changed -dotnet tools/Prober/bin/Release/net10.0/prober.dll gate # the release gate, from the tracked corpus and a replay +dotnet tools/Oracles/bin/Release/net10.0/oracles.dll status # the worklist: unheld claims, unread waivers, unseeded oracles, candidates +dotnet tools/Oracles/bin/Release/net10.0/oracles.dll seeds [filter] # each seed: a copy of the tree, the patch, one project built, one class run ``` -Proving that each probe can fail needs no model either, but builds once per seed, so it runs locally -only ([decision 100](design/adr/0100-every-probe-is-shown-to-fail-against-a-seeded-defect.md)), and only for the -probes changed since their seeds were last caught; a release runs `--all` -([decision 107](design/adr/0107-a-cited-fact-is-executable-and-seeds-run-by-footprint.md)): - -```bash -dotnet tools/Prober/bin/Release/net10.0/prober.dll seeds # each probe's seeded defect, applied in a scratch worktree, must fail it -``` - -Exploration needs a model and runs locally only, never in CI: - -```bash -dotnet tools/Prober/bin/Release/net10.0/prober.dll round explore --minutes 10 --claims 2 -``` - -An agent runs builds and tests through `tools/BuildVerdict`, run as its built dll (`build -[]` or `test`). It prints the exit code named, each diagnostic code once with its remedy -from `tools/BuildVerdict/build-verdict-remedies.tsv`, and the failing tests with their assertion; the -full log is `$GIT_DIR/build-verdict-last.log`. A code it prints as UNMAPPED gets a row once its fix is -known. In an agent's Bash tool, `tools/BashGuard` (registered in `.claude/settings.json`) refuses -`dotnet build` of anything but a tool, `dotnet test` and `dotnet msbuild` (a `-getProperty`/`-getItem` query -without a target excepted), and the verdict piped into -`head` or `tail`, and prints the verdict's form of the command. The raw commands above are for a -terminal. - -Both are tool projects under `tools/` (design/adr/0092): BCL only, in the solution, each with its -tests (`tools/BuildVerdict.Tests`, `tools/BashGuard.Tests`), and run from `bin/Release`, which -`.claude/settings.json` builds at session start: the guard in the main checkout, every other tool -(`tools/AgentTools.slnf`) in the session's own, a worktree included, with the SDK installer's `dotnet` -when the one on PATH lacks the pinned SDK; a session start that builds nothing says so in your context -(design/adr/0105). Each records the sources it was built from and refuses to run from others, naming -the command that rebuilds it: `dotnet build tools/ -c Release`, which the guard lets through. +An agent builds and tests through `tools/BuildVerdict` (`build []` or `test`). It prints a +verdict: the exit code named, each diagnostic once with its remedy from +`tools/BuildVerdict/build-verdict-remedies.tsv` (give an UNMAPPED code a row once its fix is known), and the +failing tests; the full log is `$GIT_DIR/build-verdict-last.log`. In an agent's Bash tool, `tools/BashGuard` +refuses the raw `dotnet build`, `dotnet test` and `dotnet msbuild` and prints the verdict's form of the command; +the raw commands above are for a terminal. The tools run from `bin/Release`, built at session start in the +session's own checkout ([decision 105](design/adr/0105-every-tool-is-built-in-the-session-s-checkout.md)). +A tool built from other sources refuses to run and names the command that rebuilds it, +`dotnet build tools/ -c Release`. The SDK is pinned in `global.json` (10.0.4xx band, `latestPatch`). Tests run on Microsoft Testing Platform (xunit.v3 4.x), so `dotnet test` takes `--solution`/`--project`, and TRX output is @@ -131,33 +105,16 @@ here, so it leads and the source decides. before researching what ASP.NET, System.Text.Json, EF Core, the runtime or the platform do, and a new `design/knowledge/` entry when research settles one that something here turned on ([decision 93](design/adr/0093-verified-facts-are-pinned-dated-and-executable-where-they-can-be.md)). -- **An empty result proves nothing.** Fall back to text search when the graph is unavailable, stale - or silent, and never conclude "unused", "no other caller" or "safe to change" from it alone. - Measured on 2026-10-13, the graph: - - finds types, interfaces, enums and ordinary methods, a type's implementers through generic - interfaces (84 of the Samples' 88 value objects), and the callers of an ordinary method across - files; - - has no node for a member of a C# 14 `extension(...)` block, which holds most of the `Option` and - `Result` extensions (`Choose`, `FirstOrNone`, `Transpose`, `Sequence`, the continuations of a - `Task>`) and `AsText`, and puts the calls inside one on the file; - - has nothing Metalama generates: none of the tests' 222 `.From(` calls has an edge, nor has a call - to `TryFrom`, `FromKnownGood`, `Parse` or a converter, and a template an aspect introduces by - `nameof` looks unreferenced. Read woven code as the Generators README's "Reading the generated - code" says; - - misses a reference that is not a call: a DI registration, a type argument, `typeof`, an attribute; - - resolves a call on a generic instance poorly (`Option.Match` has one resolved caller, where - the repository writes `.Match(` about 200 times across its types) and guesses wrong callers: a low-confidence `suffix_match` edge, or a same-named method of another API - (Metalama's `Diagnostics.Report` listed as calling `GeneratedCode.Report`). Overloads share one - node. -- **Refresh it**, since it does not follow the working tree: `index_repository` with the repository - root as `repo_path` and `name: "CodoMetis.TypeKit"`, at the start of a session and after a branch - switch, a rebase, a merge or a pull (about 6 s). Without `name` it starts a second project under a - derived name. `index_status` reports the checkout's HEAD, not the commit the graph was built from, - so refresh rather than check. -- It indexes `spikes/` and leaves out `.claude/`, `bin` and `obj`, so an agent in a worktree under - `.claude/worktrees/` reads the main checkout's graph, not its own: index the worktree under a name - of its own and delete that project when done, or search the source. `.codebase-memory/`, the - tool's local artifact, is ignored by git. +- **An empty result proves nothing.** The graph has no node for a member of a C# 14 `extension(...)` + block (most of the `Option` and `Result` extensions), nothing Metalama generates (no `.From(` or + `TryFrom` call has an edge), and no reference that is not a call; it resolves calls on a generic + instance poorly and guesses wrong callers ([design/code-graph.md](design/code-graph.md) has the + measurements). Fall back to text search, and never conclude "unused", "no other caller" or "safe to + change" from it alone. +- **Refresh it**, since it does not follow the working tree: `index_repository` with the repository root + as `repo_path` and `name: "CodoMetis.TypeKit"`, at the start of a session and after a branch switch, a + rebase, a merge or a pull. A worktree under `.claude/worktrees/` isn't indexed: index it under a name of + its own and delete that project when done, or search the source. The way into the docs is docs-query (`dotnet tools/DocsQuery/bin/Release/net10.0/docs-query.dll`, [its README](tools/DocsQuery/README.md)): `entry`, `cites`, `cited-by`, `list`, `claims` and `check` @@ -221,6 +178,20 @@ the whole page. A ranked miss proves nothing. analyzer that stops firing, or a converter that materialises `default` are this repo's characteristic failures. Prefer a diagnostic or an exception to a plausible result. +## Issues +Work is tracked as GitHub issues, not as lists in docs or memory +([decision 109](design/adr/0109-work-is-tracked-as-labelled-issues-and-ci-checks-each-pull-request.md)): +open the issue before starting the work. The labels, the epics and what a PR carries are CONTRIBUTING's +"Issues and labels" and "Pull requests"; `gh label list` is the source, and a label is never invented. +What trips agents here: +- A release's work is a native sub-issue of its `epic`, with the version's milestone: + `gh issue create --parent N --milestone M`. A finding belongs to the release it ships in. +- Name the branch from the type label before the first push (`feat/`, `fix/`, `docs/`, `test/`, `build/`, + never `chore/`): PR hygiene fails a `/` that disagrees, and renaming a pushed head closes its PR. +- A topic merges into its release branch, where `Closes` closes nothing: close its issue by hand on merge, + with a comment linking the PR. +- When the work changes what an issue says (its scope, its state, a measured fact), edit the issue. + ## Workflow 1. Read `design/plan.md`, the roadmap, the decision records and the design note for the area you touch, and the relevant spike README, before starting. A decision is changed by a new record. @@ -242,53 +213,19 @@ the whole page. A ranked miss proves nothing. tables of [decision 94](design/adr/0094-topic-branches-into-a-release-branch-and-fix-trailers.md) (`CommitTrailerTests`). Work happens on a `/` branch from `release/`, reaches it by a PR, and every merge is a merge commit: never squash or rebase a pushed branch. -7. **Probes replay themselves; exploring is a separate, capped, local job.** Consumer probes check the - claims in `docs/` and the READMEs, pairwise over the axes in - [.claude/probe-axes.md](.claude/probe-axes.md); they, their rows, the claims and the waivers are the - corpus in `tools/probe/corpus/`, tracked like tests. - - **Replay** runs in CI on every push and pull request, every probe from an empty state, and a probe - that fails fails the job. Locally it runs after every commit from a git `post-commit` hook, in the - background and with no model: `prober replay` re-runs only the probes whose identity or coverage - footprint the commit changed, and blocks nothing. Read `prober status` before your next change. A probe that passed with - the same identity and fails now is an open finding `R--`, fixed like any failing test. - The hook is opt-in, per clone and untracked: put this in `$(git rev-parse --git-common-dir)/hooks/post-commit` - and make it executable (Git for Windows runs it through its own `sh`): - - ```sh - #!/bin/sh - [ -f tools/Prober/Prober.csproj ] && nohup dotnet tools/Prober/bin/Release/net10.0/prober.dll replay > "$(git rev-parse --git-dir)/prober-replay-last.log" 2>&1 < /dev/null & - exit 0 - ``` - - **Explore** when a feature lands or `status` shows a claim without probes: `/probe explore`, the - `prober` agent under `prober round`, whose time and claim cap the tool enforces. Never in CI. Its - probes land in its staging; `prober publish` moves them into the corpus, committed as `test:`. - - **Full sweep** on request (`/probe full`) or nightly, locally: the whole corpus replayed, the axes - derived again, then exploration up to the cap. A report, not a gate. - - A probe that found a bug becomes a test in the suite (steps 3 and 4); a dismissed finding is recorded with - `prober dismiss` and the reason. An intended difference a probe accepts cites the sentence of the - docs, the record, or the verified fact (`KB-NNN`, `design/knowledge/`) that makes it intended. +7. **Hold what the documents promise** ([decision 112](design/adr/0112-the-prober-gives-way-to-valueranges-oracle-kit.md), + [test/oracles/README.md](test/oracles/README.md) for the files and how to add an oracle). A new public member, + or a new promise in `docs/` or a README, gets a claim in `test/oracles/claims.json`, quoted verbatim and naming + the test that holds it, or a waiver in `surface-waivers.json` that says why it needs none; a claim is reworded + with its document. A new oracle is registered in `oracles.json` with a seed, shown caught by + `oracles seeds `. A disagreement nobody fixes yet goes to `candidates.json` with its issue, never into + an assertion that hides it, and a combination a finding names joins `axes.md`. ## Releasing -Before `git tag`, replay the whole probe corpus and run the gate, and show every probe's seeds caught (locally, a -build per seed; without `--all` it runs only the probes changed since their seeds were last caught, decision 107): - -```bash -dotnet tools/Prober/bin/Release/net10.0/prober.dll replay --all -dotnet tools/Prober/bin/Release/net10.0/prober.dll gate -dotnet tools/Prober/bin/Release/net10.0/prober.dll seeds --all -``` - -The gate never starts a model and needs nothing but the tracked corpus and a replay, so the release -workflow runs it after `replay --all`, with `PROBE_STATE_DIR` in the job's temporary directory and -`--since` the previous release's tag, reports its verdict in the job summary, and fails the release on a -refusal. It refuses while a finding is open, a probe has not -passed at its current identity, or a claim touched since the last tag has pairwise combinations no -probe covers; it lists what is missing. Every obligation is met by a probe in the corpus, written by -hand or by a round, or by a reviewed waiver in `tools/probe/corpus/waivers.json` (claim, reason, -reviewer). - -**The gate binds from 2.2.0** -([decision 97](design/adr/0097-2-2-0-is-a-consolidation-release-and-the-gate-binds.md)). Never tag -2.2.0 or a later version over a refusal without the user's word, which is a waiver naming them as its -reviewer. Answer a refusal with a probe, a fix or such a waiver, never by editing the workflow's step. +Follow CONTRIBUTING's "Releasing". Its step 1 runs locally, before `git tag`, and is the one easy to skip: +`oracles seeds` shows every seed caught, and `docs-query due --nuget` lists the competitor facts to re-read. On an +arm64 Mac set `TYPEKIT_MSSQL_IMAGE=mcr.microsoft.com/azure-sql-edge:latest`, or the SQL Server oracles are `ERROR` +([decision 117](design/adr/0117-a-seed-is-caught-against-a-passing-baseline.md)). A seed that is not caught is +answered by a stronger oracle or a better seed, never by deleting it, and an `ERROR` by fixing the environment or +the baseline, never by reading it as caught. diff --git a/CHANGELOG.md b/CHANGELOG.md index 19cb7e37..6422f6ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,64 +10,94 @@ A version's heading reads `## — Unreleased` until the release is pre the release date. The release workflow refuses a tag whose section still says `Unreleased`, and the section is the GitHub release's notes. -## 2.3.0 — Unreleased +## 2.3.0 — 2026-10-29 + +### Added + +- AspNetCore: `FaultResponse.WithoutFaultName()` answers a member without naming it: no `fault`, no `rule` and no member description in the problem, so two causes on one status look alike; the document says that status names no fault, and `IFaultFeature.NameWithheld` tells the host ([Faults a client must not tell apart](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.3.0/src/CodoMetis.TypeKit.AspNetCore/README.md#faults-a-client-must-not-tell-apart)). +- `Result.CatchAsync` takes a call that returns a `ValueTask`, `stream.ReadAsync(buffer)` without `.AsTask()`; an `async` lambda binds as before ([An exception as an outcome](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.3.0/docs/result.md#an-exception-as-an-outcome)). +- `TraverseAsync` on `Option` and `Patch`: an asynchronous check of an optional input or a patch field, with a token form; nothing is called without a value ([Collect many results](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.3.0/docs/result.md#collect-many-results)). +- `Choose()`, `Choose(selector)`, `SequenceAsync` and `TraverseAsync` read an `IAsyncEnumerable`, in order and without a list first; the token reaches the stream, nothing is read after the first error, and the enumerator is disposed however they stop ([Chain steps](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.3.0/src/CodoMetis.TypeKit/README.md#chain-steps)). +- `Fault.Cause`: `Fault.Of(reason, detail, exception)` keeps the exception underneath for your log; equality and `ToString()` leave it out, and `ToHttpResult()` hands it to the host as `IFaultFeature.Cause` and `FaultProblem.Cause`, never into the problem ([What failed underneath](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.3.0/src/CodoMetis.TypeKit.AspNetCore/README.md#what-failed-underneath)). +- Testing: every assertion works on a `Task` or `ValueTask` of what it checks, `await service.FindAsync(id).ShouldBeSome()`; a task that throws fails the test with its own exception, and an assertion left without `await` is reported (CMTK0003): through `ConfigureAwait` or `WaitAsync`, in a branch of a `?:`, `??` or `switch`, and handed to `Task.WhenAll`, `Task.Run` or `Task.WhenEach` and dropped, or to `Task.WhenAny` or `Task.WaitAny`, nested at any depth, or awaited in an `async` lambda whose `catch` throws again or can't catch it ([CodoMetis.TypeKit.Testing](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.3.0/src/CodoMetis.TypeKit.Testing/README.md)). +- OpenAPI: `AddTypeKit(typeKit => typeKit.InlineValueObjects = true)` describes every value object in place by its underlying type's schema, in every position and in OpenAPI 3.1, 3.0 and 2.0, with no component of its own; closed sets keep theirs, and without the setting the document is unchanged ([Describe value objects as plain types](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.3.0/docs/aspnet-core.md#describe-value-objects-as-plain-types)). +- Analyzers: CMTK0009 warns at `FirstOrNone`, `SingleOrNone` or `FirstOrError` on a query given a `Func` variable or a method group, which binds to the `IEnumerable` overload and reads every row; a lambda at the call or an `Expression` stays silent ([A condition that reads every row](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.3.0/src/CodoMetis.TypeKit.Analyzers/README.md#a-condition-that-reads-every-row)). +- Docs: the guides, the package READMEs, the changelog and the API of every run-time package as one site with a menu and search, built from the latest release ([caffeinatedcoder.github.io/CodoMetis.TypeKit](https://caffeinatedcoder.github.io/CodoMetis.TypeKit/)). +- Analyzers: a validated value object without `Create` gets a code fix, **Add Create**, that writes a `Create` accepting every value, ready for your rules ([Add Create](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.3.0/src/CodoMetis.TypeKit.Analyzers/README.md#add-create)). +- Docs: a walkthrough follows one orders service from request to response, saying what changes at each boundary; the service is a sample that CI builds from the packages and runs, and the answers the page states are checked against it ([Walkthrough](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.3.0/docs/walkthrough.md)). +- Docs: the root README and the Generators README show, folded, the code the generators write for `OrderId`, `Email` and a closed set; a test holds each block to what the build emits ([Declare a value object](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.3.0/src/CodoMetis.TypeKit.Generators/README.md#declare-a-value-object)). +- EF Core: queries, keys and round trips are tested on a real SQL Server, and the README says what differs there from memory: text compared ignoring case, Guids sorted by their last group, `decimal(18,2)` by default, a `TimeSpan` under a day, `Greatest` from compatibility level 160 ([On SQL Server](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.3.0/src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#on-sql-server)). + +### Changed + +- Analyzers: CMTK0003 now reports four forms that code built on 2.2.0 can contain, so your build can fail with it when you upgrade. They are a `Task.WhenAny` of `Result` tasks you don't hold (dropped, awaited, waited for or returned as a plain `Task`), a dropped `Task.WhenEach` of them, `Wait()` on a `?:`, `??` or `switch` of them, and an array of them written in a `Task.WaitAll` or `Task.WaitAny` call. Each one loses results that nobody reads: handle them, or discard them with `_ =`. An awaited `Task.WhenAny` of tasks you hold is no longer reported ([What a new version may change](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.3.0/src/CodoMetis.TypeKit.Analyzers/README.md#configuration)). +- EF Core: a default value-object key over `ulong` gets a temporary value at `Add`, as a plain `ulong` key does, where it threw `OverflowException` on SQL Server and PostgreSQL; a `char` key configured `ValueGeneratedOnAdd()` gets a temporary `char` on SQL Server, not a character of a GUID string. HiLo and your own generator still come first. A compiled model made with `--nativeaot`, and a key you configure with `ValueObjectConverter` yourself, still throw ([Keys](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.3.0/src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#keys)). +- Packages: nuget.org's Release Notes tab and Visual Studio's Updates tab show the version's section of this changelog, as plain text, where they showed a link to the file. +- Changelog: a released version's links name its tag, `blob/v2.2.0/`, so its notes open the pages as they were released, not as they read on `main` today. +- Comparison: SuperStrong.Types 1.0.0 joins the value-object table, Thinktecture.Runtime.Extensions is compared at 10.6.0 and LanguageExt at 5.0.0-beta-78, and the page's facts are re-read before every release ([How it compares](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.3.0/docs/comparison.md)). +- README: introduces TypeKit by example, a one-line `OrderId`, a validated `Email`, an `Option` lookup and a `Result` chain, and thanks Vogen, which inspired the value objects; every README says what Metalama is where it first names it ([CodoMetis.TypeKit](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.3.0/README.md#what-you-get)). + +### Fixed + +- Generators: the aspects keep no compilation objects in static fields, which in an IDE that keeps Metalama's process across edits held every earlier compilation in memory. +- Generators: a generated `TryParse(string?, IFormatProvider?, out)` ends at its `return`; the unreachable statements after it are gone from the woven code ([Reading the generated code](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.3.0/src/CodoMetis.TypeKit.Generators/README.md#reading-the-generated-code)). ## 2.2.0 — 2026-10-18 ### Fixed -- Generators: a value object over `string` implements `ISpanParsable`, as `string` does, and its span `Parse` runs `Create` as `Parse` does; a span `Parse` you wrote on one now fails the build (CMTK1011) ([What is generated](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.Generators/README.md#what-is-generated)). -- EF Core: a query or a query filter that orders a value object with its own `CompareTo` through `EF.Functions.Greatest` or `Least`, or through a row-value comparison such as Npgsql's `EF.Functions.GreaterThan((o.Priority, o.Id), (priority, id))`, is refused and names the value object, where it used to order by the column ([Queries](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#queries)). -- OpenAPI: a route, query or header parameter of a type a converter describes, such as a `decimal` under `DecimalStringJsonConverter`, is what ASP.NET writes without the converter: an OpenAPI 2.0 document builds, and a nullable one no longer admits `null` ([Parameters aren't JSON](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)). -- OpenAPI: a query or header array of a type a converter describes, such as `decimal[]` or `decimal?[]` under `DecimalStringJsonConverter`, keeps its `items`, which are what ASP.NET writes for the element without the converter ([Parameters aren't JSON](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)). -- OpenAPI: a value object over a type a converter describes, such as `Money : IValue` under `DecimalStringJsonConverter`, is what ASP.NET writes for the underlying type without the converter as a route, query, header or form parameter and as a query or header array's items, no longer its JSON spelling; where value objects are components, such a parameter is written in place ([Parameters aren't JSON](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)). -- OpenAPI: the default of a parameter of a type a converter describes, or of a value object over one, such as `[DefaultValue(typeof(decimal), "1.5")]` under `DecimalStringJsonConverter`, is written as ASP.NET writes it without the converter, `1.5`, not `"1.5"` ([Parameters aren't JSON](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)). -- OpenAPI: a non-abstract `[JsonPolymorphic]` base's own branch (`{Base}Base`) describes its value objects, patches, closed sets and described types as the derived types' branches do, where it had `{}` and a list without `items`, in OpenAPI 3.1, 3.0 and 2.0 ([What the document says](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)). -- OpenAPI: a list or dictionary of a nullable value object whose converter a described one replaces, such as `List` under a converter that writes cents, admits `null` in its elements where value objects are components ([A described type](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)). -- OpenAPI: a nullable value-object property where value objects are components is the component or `null` wherever the underlying type's property admits `null`: get-only, a field, typed by a type parameter, under `[MaybeNull]` or `[DisallowNull]`, or without nullable annotations, where it was the bare reference; under OpenAPI 2.0 it's ASP.NET's `x-nullable` without the reference there too, as a settable one already was ([What the document says](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)). -- OpenAPI: a nullable value object is its wrapped type at the same nullability in every position: an MVC `[FromBody] Quantity?` admits `null` inside its schema as `int?` does, and, where value objects are inlined, a query, header or form parameter no longer admits `null`, nor does a body, response or element of a nullable struct over `string` or `Uri` ([What the document says](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)). -- OpenAPI: a nullable value object over an enum written by name lists `null` among its values as a route, query, header or form parameter, as the enum's own `DayOfWeek?` does; where value objects alone are inlined, a nullable one over an enum no longer puts a second null-or inside a body's or a response's, and as an element or a dictionary value keeps its type in OpenAPI 2.0 ([What the document says](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)). -- OpenAPI: a problem response an endpoint declares as `application/json`, such as `Produces(404)` or `[ProducesResponseType(404)]`, describes `fault` and `rule` and names its problem types, and keeps its media type; so does one of a type derived from `ProblemDetails`, such as `ProducesValidationProblem(404)` ([A Result as a response](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.AspNetCore/README.md#a-result-as-a-response)). -- EF Core: a key over a `Guid` or a `string` configured `ValueGeneratedOnAdd()` is generated as the plain key is, not as a random Guid: a version 7 Guid, or its text, on PostgreSQL, and a Guid sequential in SQL Server's order there ([Keys](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#keys)). +- Generators: a value object over `string` implements `ISpanParsable`, as `string` does, and its span `Parse` runs `Create` as `Parse` does; a span `Parse` you wrote on one now fails the build (CMTK1011) ([What is generated](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.2.0/src/CodoMetis.TypeKit.Generators/README.md#what-is-generated)). +- EF Core: a query or a query filter that orders a value object with its own `CompareTo` through `EF.Functions.Greatest` or `Least`, or through a row-value comparison such as Npgsql's `EF.Functions.GreaterThan((o.Priority, o.Id), (priority, id))`, is refused and names the value object, where it used to order by the column ([Queries](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.2.0/src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#queries)). +- OpenAPI: a route, query or header parameter of a type a converter describes, such as a `decimal` under `DecimalStringJsonConverter`, is what ASP.NET writes without the converter: an OpenAPI 2.0 document builds, and a nullable one no longer admits `null` ([Parameters aren't JSON](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.2.0/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)). +- OpenAPI: a query or header array of a type a converter describes, such as `decimal[]` or `decimal?[]` under `DecimalStringJsonConverter`, keeps its `items`, which are what ASP.NET writes for the element without the converter ([Parameters aren't JSON](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.2.0/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)). +- OpenAPI: a value object over a type a converter describes, such as `Money : IValue` under `DecimalStringJsonConverter`, is what ASP.NET writes for the underlying type without the converter as a route, query, header or form parameter and as a query or header array's items, no longer its JSON spelling; where value objects are components, such a parameter is written in place ([Parameters aren't JSON](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.2.0/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)). +- OpenAPI: the default of a parameter of a type a converter describes, or of a value object over one, such as `[DefaultValue(typeof(decimal), "1.5")]` under `DecimalStringJsonConverter`, is written as ASP.NET writes it without the converter, `1.5`, not `"1.5"` ([Parameters aren't JSON](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.2.0/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)). +- OpenAPI: a non-abstract `[JsonPolymorphic]` base's own branch (`{Base}Base`) describes its value objects, patches, closed sets and described types as the derived types' branches do, where it had `{}` and a list without `items`, in OpenAPI 3.1, 3.0 and 2.0 ([What the document says](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.2.0/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)). +- OpenAPI: a list or dictionary of a nullable value object whose converter a described one replaces, such as `List` under a converter that writes cents, admits `null` in its elements where value objects are components ([A described type](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.2.0/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)). +- OpenAPI: a nullable value-object property where value objects are components is the component or `null` wherever the underlying type's property admits `null`: get-only, a field, typed by a type parameter, under `[MaybeNull]` or `[DisallowNull]`, or without nullable annotations, where it was the bare reference; under OpenAPI 2.0 it's ASP.NET's `x-nullable` without the reference there too, as a settable one already was ([What the document says](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.2.0/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)). +- OpenAPI: a nullable value object is its wrapped type at the same nullability in every position: an MVC `[FromBody] Quantity?` admits `null` inside its schema as `int?` does, and, where value objects are inlined, a query, header or form parameter no longer admits `null`, nor does a body, response or element of a nullable struct over `string` or `Uri` ([What the document says](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.2.0/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)). +- OpenAPI: a nullable value object over an enum written by name lists `null` among its values as a route, query, header or form parameter, as the enum's own `DayOfWeek?` does; where value objects alone are inlined, a nullable one over an enum no longer puts a second null-or inside a body's or a response's, and as an element or a dictionary value keeps its type in OpenAPI 2.0 ([What the document says](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.2.0/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)). +- OpenAPI: a problem response an endpoint declares as `application/json`, such as `Produces(404)` or `[ProducesResponseType(404)]`, describes `fault` and `rule` and names its problem types, and keeps its media type; so does one of a type derived from `ProblemDetails`, such as `ProducesValidationProblem(404)` ([A Result as a response](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.2.0/src/CodoMetis.TypeKit.AspNetCore/README.md#a-result-as-a-response)). +- EF Core: a key over a `Guid` or a `string` configured `ValueGeneratedOnAdd()` is generated as the plain key is, not as a random Guid: a version 7 Guid, or its text, on PostgreSQL, and a Guid sequential in SQL Server's order there ([Keys](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.2.0/src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#keys)). ## 2.1.0 — 2026-10-15 Still `net10.0` and Metalama 2026.1. What builds or runs differently, and what to write instead: -[Migrating from 2.0 to 2.1](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/docs/migrating-to-2-1.md). +[Migrating from 2.0 to 2.1](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/250f11ae0a838e3ad68e3c7cce091028b504ea25/docs/migrating-to-2-1.md). ### Added -- **Decimals in one spelling**: `DecimalNumberJsonConverter` and `DecimalStringJsonConverter` write the smallest scale, keys included, and read either token ([JSON](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/docs/json.md#decimals-in-one-spelling)). -- **Converters that say what they write**: `IDescribedJsonConverter` and `JsonSchemaDescriptions`; the schema exporter (`DescribedJsonSchema`) and `AddTypeKit()` describe them everywhere ([JSON](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/docs/json.md#describing-what-a-converter-writes)). -- **The rule that refused**: `Fault.Of(reason).WithRule(member)` names an enum's or a closed set's member, written as the problem's `rule` and on `IFaultFeature` ([The rule that refused](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.AspNetCore/README.md#the-rule-that-refused)). -- **`FaultKind.PreconditionFailed`** (412) and **`FaultKind.Malformed`** (400), answered with no mapping ([closed sets as faults](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.AspNetCore/README.md#a-closed-set-as-the-fault-type)). -- **CMTK0014** (Warning): a `FaultKind` or `ConstructorAccessibility` that names no member, such as `(FaultKind)9` or `default` ([CMTK0014](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.Analyzers/README.md#cmtk0014)). -- **`AsOptionalText()`/`AsOptionalIdentifier()`**: a blank is no text, a hidden character still `Hidden` ([Written text](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/docs/text.md#required-and-optional)). -- **Faults of several types**: `Fault` and `Fault`, each answered as its type is mapped ([Faults of several types](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.AspNetCore/README.md#faults-of-several-types)). -- **`WithoutFaults`** and `[WithoutFaults]`: a route leaves out of its document the members it never answers with, and reporting one throws ([Leaving out what a route cannot answer](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.AspNetCore/README.md#leaving-out-what-a-route-cannot-answer)). -- **CMTK1023** (Warning): a base record's `Equals`, `GetHashCode` or `IEquatable`, which the value object's equality no longer calls ([Build errors](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.Generators/README.md#build-errors)). -- **`[Interface]`, `[Unsealed]`, `[ValueObject]`, `[ClosedSet]`, `[FlagsEnum]`, `[ReadOnlyStruct]`** (CMTK0010): six more kinds a type argument can be required to be ([Type constraints](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/docs/type-constraints.md#the-kind-of-a-type-argument)). -- **CMTK0010 at the methods the compiler calls for you**: a `foreach`'s `GetEnumerator`, an `await`'s `GetAwaiter`, a `Deconstruct`, and a collection expression's `Add` or builder, for every attribute but 2.0's four ([Type constraints](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/docs/type-constraints.md)). -- **`[Distinct]` and `[Unrelated]`** (CMTK0010): two type arguments that must differ, or must not derive from, implement or box to each other ([Type constraints](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/docs/type-constraints.md#two-parameters-apart)). -- **One mapping for every fault set**: `AddFaultResponses()` by kind, or `AddFaultResponses(fault => …)` over `IFault`, for each set with no mapping of its own ([Every fault set at once](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.AspNetCore/README.md#every-fault-set-at-once)). -- **Testing: `ShouldHold(reason)`** on `Fault` and `Fault` hands back the fault it holds ([CodoMetis.TypeKit.Testing](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.Testing/README.md)). +- **Decimals in one spelling**: `DecimalNumberJsonConverter` and `DecimalStringJsonConverter` write the smallest scale, keys included, and read either token ([JSON](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.1.0/docs/json.md#decimals-in-one-spelling)). +- **Converters that say what they write**: `IDescribedJsonConverter` and `JsonSchemaDescriptions`; the schema exporter (`DescribedJsonSchema`) and `AddTypeKit()` describe them everywhere ([JSON](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.1.0/docs/json.md#describing-what-a-converter-writes)). +- **The rule that refused**: `Fault.Of(reason).WithRule(member)` names an enum's or a closed set's member, written as the problem's `rule` and on `IFaultFeature` ([The rule that refused](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.1.0/src/CodoMetis.TypeKit.AspNetCore/README.md#the-rule-that-refused)). +- **`FaultKind.PreconditionFailed`** (412) and **`FaultKind.Malformed`** (400), answered with no mapping ([closed sets as faults](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.1.0/src/CodoMetis.TypeKit.AspNetCore/README.md#a-closed-set-as-the-fault-type)). +- **CMTK0014** (Warning): a `FaultKind` or `ConstructorAccessibility` that names no member, such as `(FaultKind)9` or `default` ([CMTK0014](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.1.0/src/CodoMetis.TypeKit.Analyzers/README.md#cmtk0014)). +- **`AsOptionalText()`/`AsOptionalIdentifier()`**: a blank is no text, a hidden character still `Hidden` ([Written text](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.1.0/docs/text.md#required-and-optional)). +- **Faults of several types**: `Fault` and `Fault`, each answered as its type is mapped ([Faults of several types](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.1.0/src/CodoMetis.TypeKit.AspNetCore/README.md#faults-of-several-types)). +- **`WithoutFaults`** and `[WithoutFaults]`: a route leaves out of its document the members it never answers with, and reporting one throws ([Leaving out what a route cannot answer](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.1.0/src/CodoMetis.TypeKit.AspNetCore/README.md#leaving-out-what-a-route-cannot-answer)). +- **CMTK1023** (Warning): a base record's `Equals`, `GetHashCode` or `IEquatable`, which the value object's equality no longer calls ([Build errors](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.1.0/src/CodoMetis.TypeKit.Generators/README.md#build-errors)). +- **`[Interface]`, `[Unsealed]`, `[ValueObject]`, `[ClosedSet]`, `[FlagsEnum]`, `[ReadOnlyStruct]`** (CMTK0010): six more kinds a type argument can be required to be ([Type constraints](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.1.0/docs/type-constraints.md#the-kind-of-a-type-argument)). +- **CMTK0010 at the methods the compiler calls for you**: a `foreach`'s `GetEnumerator`, an `await`'s `GetAwaiter`, a `Deconstruct`, and a collection expression's `Add` or builder, for every attribute but 2.0's four ([Type constraints](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.1.0/docs/type-constraints.md)). +- **`[Distinct]` and `[Unrelated]`** (CMTK0010): two type arguments that must differ, or must not derive from, implement or box to each other ([Type constraints](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.1.0/docs/type-constraints.md#two-parameters-apart)). +- **One mapping for every fault set**: `AddFaultResponses()` by kind, or `AddFaultResponses(fault => …)` over `IFault`, for each set with no mapping of its own ([Every fault set at once](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.1.0/src/CodoMetis.TypeKit.AspNetCore/README.md#every-fault-set-at-once)). +- **Testing: `ShouldHold(reason)`** on `Fault` and `Fault` hands back the fault it holds ([CodoMetis.TypeKit.Testing](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.1.0/src/CodoMetis.TypeKit.Testing/README.md)). ### Changed - The OpenAPI problem schema describes `rule` beside `fault`: regenerate clients. -- A host whose endpoint answers two fault types that write one fault name logs a warning at startup naming both; 3.0.0 refuses it ([Faults of several types](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.AspNetCore/README.md#faults-of-several-types)). +- A host whose endpoint answers two fault types that write one fault name logs a warning at startup naming both; 3.0.0 refuses it ([Faults of several types](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.1.0/src/CodoMetis.TypeKit.AspNetCore/README.md#faults-of-several-types)). - The new attributes in the `CodoMetis.TypeKit` namespace (`[ValueObject]`, `[Interface]`, …) can be ambiguous beside another library's of the same name (CS0104): qualify or alias one. -- OpenAPI: in a host with controllers, a document whose MVC and minimal-API JSON options write a type differently is refused, naming what differs ([OpenAPI](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)). +- OpenAPI: in a host with controllers, a document whose MVC and minimal-API JSON options write a type differently is refused, naming what differs ([OpenAPI](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.1.0/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)). ### Fixed -- OpenAPI: a value object or closed set the JSON options write with a converter of their own is described as that converter says, or as ASP.NET writes it, not as its wrapped type ([OpenAPI](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)). -- OpenAPI: a `Patch` the serializer requires stays `required`; under `RespectRequiredConstructorParameters`, declare patches as `init` properties ([Merge patches](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/docs/merge-patches.md#required-constructor-parameters)). +- OpenAPI: a value object or closed set the JSON options write with a converter of their own is described as that converter says, or as ASP.NET writes it, not as its wrapped type ([OpenAPI](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.1.0/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)). +- OpenAPI: a `Patch` the serializer requires stays `required`; under `RespectRequiredConstructorParameters`, declare patches as `init` properties ([Merge patches](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.1.0/docs/merge-patches.md#required-constructor-parameters)). - OpenAPI: a `WithFaultStatus` on the builder `MapControllers()` returns reaches the document, as it reached the response. - OpenAPI: a problem response an endpoint declares itself (`ProducesProblem`, `[ProducesResponseType]`) describes `fault`, and names its problem types. - OpenAPI: an operation with fault responses lists every status ascending, the ones its endpoint declares itself among them; where an endpoint declares its own, the document's response order changes, so update snapshot tests. - Over a base record, a value object's equality is its wrapped value's, in a collection of the base too, whatever equality the base declares. -- Under `ReferenceHandler.Preserve`, a value object over a type that could hold an `$id` throws `NotSupportedException` instead of writing a duplicate `$id` ([JSON](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.Generators/README.md#json)). +- Under `ReferenceHandler.Preserve`, a value object over a type that could hold an `$id` throws `NotSupportedException` instead of writing a duplicate `$id` ([JSON](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.1.0/src/CodoMetis.TypeKit.Generators/README.md#json)). - A wrapped type's `Parse` whose result can't be assigned to it, or that takes its text by `ref`, or an abstract class's string constructor, no longer breaks the build; the value object has no parsing. - A value object over a NodaTime type parses after MVC bound a plain one first; it threw `TypeInitializationException` for the rest of the process. - OpenAPI: an MVC action binding a complex type from the route under a name of its own (`[FromRoute(Name = "id")]`) no longer fails the whole document with `AddTypeKit()`. @@ -75,7 +105,7 @@ Still `net10.0` and Metalama 2026.1. What builds or runs differently, and what t ## 2.0.0 — 2026-10-14 Still `net10.0` and Metalama 2026.1. What builds or runs differently, and what to write instead: -[Migrating from 1.5 to 2.0](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/docs/migrating-to-2.md). +[Migrating from 1.5 to 2.0](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/v2.0.0/docs/migrating-to-2.md). ### Breaking diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8f94af09..4cc44779 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,9 +20,13 @@ a file-based app (`dotnet run tools/x.cs`) or a tool project under `tools/`, nev ([decision 92](design/adr/0092-repository-tooling-is-dotnet-and-ci-needs-no-ai.md)). Two tasks need one thing more each: -- **Docker, running** (Docker Desktop, OrbStack, Colima), for the PostgreSQL round-trip tests, which - start a Linux container through Testcontainers. Without it those fail with - `DockerUnavailableException`, and every other test passes. +- **Docker, running** (Docker Desktop, OrbStack, Colima), for the EF Core tests on PostgreSQL and SQL + Server and the SQL Server facts, which start a Linux container through Testcontainers and carry the + trait `Needs=Docker`. Without it those fail with `DockerUnavailableException`, and every other test + passes; `--filter-not-trait "Needs=Docker"` leaves them out. SQL Server's image is amd64 only: on an + arm64 Mac it starts under Rosetta, not under QEMU (OrbStack's default without Rosetta), so set + `TYPEKIT_MSSQL_IMAGE=mcr.microsoft.com/azure-sql-edge:latest` there, whose older engine runs natively; + CI runs SQL Server 2022. - **The platform's native toolchain** for the smoke test, which publishes two consumers with Native AOT, a web host and a console application with EF Core: Xcode's command-line tools on macOS (`xcode-select --install`), `clang` and `zlib1g-dev` on Debian and Ubuntu, MSVC's C++ build tools @@ -70,7 +74,7 @@ test that passes both with and without the fix is not a regression test. See **A bug fix covers its siblings.** Name the class of input the defect lives in, and run the fix's test across that class's variant catalogue: every shape through which a type argument reaches a parameter -(`test/CodoMetis.TypeKit.Analyzers.Tests/Variants/`), every declaration shape of `.claude/probe-axes.md`, +(`test/CodoMetis.TypeKit.Analyzers.Tests/Variants/`), every declaration shape the generation tests vary (`test/oracles/axes.md`), every setting of the framework option the fix touches. Fix what fails there too, rather than leave it for the next review ([decision 95](design/adr/0095-a-fix-is-tested-across-its-input-class.md)). @@ -88,6 +92,56 @@ string, an assembly-name prefix or a type-name list will be asked to replace it. **Prefer a loud failure to a silent one.** Where the package cannot do something correctly, it should fail the build or throw with an actionable message. +## Issues and labels + +Use the **Bug** or **Enhancement** form. The bug form asks whether the failure is silent or loud, because a +silent one, code that compiles and does less than it appears to, is the failure this repository exists to +prevent. + +Each release is an `epic` issue with a milestone of its version (`2.3.0`), and its work is attached as +sub-issues with the same milestone; a theme that spans releases is an `epic` without one. The plan stays in +[design/roadmap.md](design/roadmap.md), which an epic links rather than copies +([decision 109](design/adr/0109-work-is-tracked-as-labelled-issues-and-ci-checks-each-pull-request.md)). +A maintainer sets the labels when triaging, and a pull request gets the same labels and milestone as the +issue it closes: + +| Label | Means | +|---|---| +| `bug`, `enhancement`, `documentation`, `tests`, `chore` | The type. Exactly one | +| `severity/critical` | Data loss, or silently wrong results, in common use | +| `severity/high` | Wrong results, a crash or a failed build in a realistic scenario | +| `severity/medium` | An inconsistency or a failure in a narrow scenario | +| `severity/low` | A cosmetic, message or documentation-level difference | +| `silent` | A bug that compiles or answers and does less than it appears to | +| `breaking` | Changes a signature, a generated member, a wire form or a diagnostic's severity: needs a major | +| `found-by/prober` | Found by the prober, before it was retired (decision 112); kept for the issues it labels | +| `stacked` | Based on another open pull request: merge in order | +| `epic` | A release or a theme; its sub-issues are the work | +| `package/core`, `package/analyzers`, `package/generators`, `package/efcore`, `package/aspnetcore`, `package/serilog`, `package/testing` | The package it affects | +| `area/option-result` | `Option`, `Result` and their extensions | +| `area/value-objects` | The value-object contracts and what the aspects generate for them | +| `area/json` | System.Text.Json wire forms, values and dictionary keys | +| `area/aot` | Native AOT and trimming | +| `area/parsing` | `Parse`, `TryParse`, formatting and the type converter | +| `area/release` | Versioning, changelogs, packaging and release wiring | +| `area/tooling` | The repository's tools: the verdict, the guard, docs-query, the oracle kit, the smoke test | + +A bug carries exactly one severity. CI's "PR hygiene" check reads each pull request into `main` or a release +branch and fails it with no type label, a bug with no severity, `silent` on anything but a bug, no closing +reference, a branch whose `/` disagrees with its type label (`feat` is `enhancement`, `fix` is `bug`, +`docs` is `documentation`, `test` is `tests`, the rest `chore`), a pull request into `release/` +without that milestone, or `breaking` on a release that is not a major. It turns green when you add what is +missing, without a new commit. To run it on a saved payload: + +```bash +dotnet build tools/PrHygiene -c Release +dotnet tools/PrHygiene/bin/Release/net10.0/pr-hygiene.dll path/to/event.json +``` + +`Closes #12` closes an issue only when the pull request merges into `main`, and topics merge into a release +branch, so close the issue by hand when your topic merges, with a comment linking the pull request. The +release's pull request into `main` closes its epic. + ## Pull requests Work for the next version goes into its release branch, `release/` (`release/2.2.0`), not @@ -96,7 +150,9 @@ Branch from it as `/`, where the type is your main commit's (`feat/ `build/…`), and open the PR against it. PRs are merged with a merge commit, never squashed or rebased: the design records cite commits by hash. -- One concern per PR, with the reasoning in the description rather than only in the diff. +- One concern per PR, closing one issue, with its labels and milestone ([above](#issues-and-labels)). + The description follows the template: `Closes #n`, then Problem, Change and Tests, with the reasoning + there rather than only in the diff. - Full suite green. - User-visible changes get a changelog entry. - Match the surrounding style. Comments explain *why*, especially where behaviour is load-bearing @@ -116,23 +172,87 @@ the design records cite commits by hash. The packages share one version and are released together, only through `.github/workflows/release.yml` (see [SECURITY.md](SECURITY.md)). -1. In `CHANGELOG.md`, replace `Unreleased` in the version's heading with today's date - (`## 1.0.0 — 2026-10-01`). The section becomes the GitHub release's notes. -2. Move the rules in `src/CodoMetis.TypeKit.Analyzers/AnalyzerReleases.Unshipped.md` to +1. Check what the documents rest on, locally: + - Show every oracle seed caught: `dotnet tools/Oracles/bin/Release/net10.0/oracles.dll seeds` (a baseline with + no seed, then a build of one project per seed; [test/oracles/README.md](test/oracles/README.md)). It exits 0 + only when every oracle passes without its seed and fails with it. An `ERROR` (exit 4) is an environment or a + baseline to fix, such as a SQL Server container that never started, never a seed to accept. + - Re-read the competitor facts that are due: `dotnet tools/DocsQuery/bin/Release/net10.0/docs-query.dll due --nuget` + lists each `design/knowledge/` entry pinned `external` whose `verified:` date is more than six months old, or + whose pinned package NuGet has a later release of. Read each again at the latest release, from its source and + release notes, then move its pins and `verified:` date, or correct it, and correct `docs/comparison.md` with it: + the version a table names, the cells, and the page's list of dates. Check every TypeKit cell of the page + against the release branch, and name the version being released in TypeKit's columns and the root README's + Status. The listing runs by hand and never in CI, because a build mustn't fail just because time passed. +2. In `CHANGELOG.md`, replace `Unreleased` in the version's heading with today's date + (`## 1.0.0 — 2026-10-01`). The section becomes the GitHub release's notes, and every package's + release notes on nuget.org: pack writes it into each nuspec as plain text, and fails while the + section is missing, empty or over nuget.org's 35,000 characters (`PackageReleaseNotesTests`). + Point the section's links at the version's tag, `blob/v/` in place of `blob/main/`, so + they open the pages as they were released; `ChangelogTests` fails on a dated section that links + anything but its tag or a full commit id. +3. Move the rules in `src/CodoMetis.TypeKit.Analyzers/AnalyzerReleases.Unshipped.md` to `AnalyzerReleases.Shipped.md`, under `## Release `. Their ids and severities are public - contract from then on. `ChangelogTests` fails until steps 1 and 2 agree. -3. Commit on the release branch, open its PR into `main`, and merge it with a merge commit. Tag that - merge commit and push the tag: `git tag v1.0.0 && git push origin v1.0.0`. The workflow checks the tag - against `Version` and the dated heading, runs the suite and the smoke test, and waits for approval - in the `nuget` environment before pushing. Delete the release branch once the tag is pushed. -4. Afterwards: raise `Version` in `Directory.Build.props`, set `PackageValidationBaselineVersion` to + contract from then on. `ChangelogTests` fails until steps 2 and 3 agree. A release that ships no + rule gets the header alone, and loses it when the next release adds its own: the analyzer build + refuses any header after an empty release (RS2007). +4. Commit on the release branch, open its PR into `main` (`chore` and `area/release`, the version's + milestone, `Closes #`), and merge it with a merge commit. Tag that + merge commit and push the tag: `git tag v1.0.0 && git push origin v1.0.0`. The workflow runs every + lane of the build workflow (Linux, Windows and macOS, the pack and both smoke tests), checks the tag + against `Version` and the dated heading, runs the suite and the smoke test on what it packs, and waits + for approval in the `nuget` environment before pushing. Delete the release branch once the tag is pushed. +5. Afterwards: raise `Version` in `Directory.Build.props`, set `PackageValidationBaselineVersion` to the version just released, so ApiCompat guards the public surface from then on, and open a new `## — Unreleased` section. `ChangelogTests` fails while the baseline trails the last - version the changelog dates. Once that is on `main`, cut the next `release/` from it and - push it. + version the changelog dates, and pack while the new section is empty, so open it with its first + line. Once that is on `main`, cut the next `release/` from it and + push it, and open the version's milestone and its `epic` issue. `workflow_dispatch` with `dry_run` (the default) rehearses everything but the push. +## The docs site + +The guides in `docs/`, the root README, the package READMEs, the changelog and the API pages of the +run-time packages are published to GitHub Pages, at +[caffeinatedcoder.github.io/CodoMetis.TypeKit](https://caffeinatedcoder.github.io/CodoMetis.TypeKit/), by +`.github/workflows/docs.yml` with DocFX +([decision 116](design/adr/0116-the-docs-site-is-docfx-built-from-main.md)). + +- **Only `main` is published**, on every push to it, so the site always shows the latest release: the + release's PR into `main` publishes it. A release branch is never published. +- **The pages are the files as they are.** Nothing copies or edits them for the site. The menu is + `site/toc.yml`: a new guide goes into the README's Documentation table and into the menu's Guides, in + the same order, and `DocsSiteTests` fails until both list it. A new run-time package goes into + `site/docfx.json`'s `metadata`, or into `DocsSiteTests.WithoutApiPages` with the reason it has no API + pages. +- **Links are checked on every pull request** that touches what a page is built from. DocFX runs with + `--warningsAsErrors`, so a link to a missing page, heading or file fails the PR. A link to a file that + is not a page (`design/`, this file) becomes a GitHub address at the built commit, at build time + (`site/template/conceptual.extension.js`). +- **Pages is configured** to deploy from GitHub Actions (Settings > Pages > Source), the one setting a + workflow cannot make. + +To build the site locally (`jq` reads the packages from `site/docfx.json`) and serve it at +http://localhost:8080: + +```bash +for assembly in $(jq -r '.metadata[].src[].files[]' site/docfx.json); do dotnet publish "src/${assembly%.dll}" -c Release -o site/obj/pub; done +dotnet tool restore +dotnet docfx site/docfx.json --warningsAsErrors --metadata _commit="$(git rev-parse HEAD)" --serve +``` + +**A second line**, such as `3.x` beside `2.x`, waits until two lines are supported at once. GitHub Pages +serves one artifact per repository, so each deployment then carries every line: + +1. Build each line from its own branch into its own folder (`/2.x/`, `/3.x/`), with `main`'s + `site/` config, as a matrix in the build job; DocFX's `_appTitle` names the line. +2. Assemble the folders into one artifact with a root `index.html` that redirects to the default line, + the latest release, and deploy that. +3. Start the workflow from both branches. A push to the older line's branch cannot run a workflow that + branch lacks, so run on `workflow_run` of the build workflow there, and check that line out by name, + never by the triggering run's SHA. CodoMetis.ValueRanges' `docs.yml` does this for its two lines. + ## AI-assisted development A substantial portion of this codebase was written with AI assistance, using diff --git a/CodoMetis.TypeKit.slnx b/CodoMetis.TypeKit.slnx index a53f69b2..429e119e 100644 --- a/CodoMetis.TypeKit.slnx +++ b/CodoMetis.TypeKit.slnx @@ -13,6 +13,7 @@ + @@ -20,7 +21,8 @@ - + + @@ -29,9 +31,11 @@ + - + + diff --git a/Directory.Build.props b/Directory.Build.props index 3f40888e..4883433f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,6 @@ CaffeinatedCoder Copyright (c) 2026 CaffeinatedCoder MIT - https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/CHANGELOG.md https://github.com/CaffeinatedCoder/CodoMetis.TypeKit https://github.com/CaffeinatedCoder/CodoMetis.TypeKit git diff --git a/Directory.Packages.props b/Directory.Packages.props index bcebbd99..699da861 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -50,14 +50,15 @@ + diff --git a/README.md b/README.md index f4204584..04d5db6f 100644 --- a/README.md +++ b/README.md @@ -9,51 +9,38 @@ Strong types for .NET 10, with one set of rules from the request to the database ## What you get -**Value objects in one line** -- **Declare it, the rest is generated.** `OrderId : IValue` gets its constructor, equality, - comparison, parsing, formatting, JSON converter and type converter at compile time. -- **Validation in one place.** Write `Create` once, and JSON, route binding, parsing and `TryFrom` - all run it. Nothing skips it by accident. -- **A drop-in for primitives.** The JSON, the database column and the OpenAPI schema are the same as - the underlying type's. It works for strings, numbers, `Guid`, dates, enums, `Uri`, NodaTime types - and your own types. -- **Smart enums.** One line per member gives you parsing by name, JSON as the name, an OpenAPI string - enum and a text column. `Match` breaks the build when a member is added and not handled. - -**Option and Result you can't misuse** -- No `.Value` to forget a check on. A `default` value is a compile error, and an ignored `Result` - is flagged. -- Pipelines, async chains, collecting many results, and `Result.Catch` for code that throws. -- Neither type serializes silently to `{}`. The serializer refuses them, so a missing value can't - reach the wire unnoticed. - -**ASP.NET Core and EF Core, with nothing to register per type** -- **EF Core:** value objects map to plain columns and keys, identity keys included, and `.Value` in - a LINQ query becomes plain SQL. -- **ASP.NET Core:** return a `Result` from an endpoint and its errors become problem-details - responses. The OpenAPI document shows each value object as its underlying type, plus the error - responses an endpoint can return. -- **JSON Merge Patch:** `Patch` tells "absent", "null" and "a value" apart. - -**Mistakes caught at build time** -- Analyzers flag default values, ignored results, bypassed validation, and generic type arguments - that break declared constraints (`[Sealed]`, `[Distinct]` and more). They also run in Razor files. -- A declaration that can't be generated fails the build with its name on it, instead of compiling - into an empty type. - -**Ready for production** -- **Native AOT and trimming** in every run-time package, checked in CI by publishing real apps as - native binaries ([what it asks of you](docs/native-aot.md)). -- **Secrets stay out of logs.** Error messages never quote the input, and Serilog logs a value - object as it prints itself. -- **Test assertions** for `Option`, `Result` and `Patch`, with no dependency on a test framework. -- **Take only what you need.** The `Option`/`Result` package brings no code generator with it. +TypeKit is built around three things: + +- **Value objects, generated for you.** You declare an `OrderId` in one line, and the rest is + written at compile time. +- **`Option`** for a value that may be missing. +- **`Result`** for an operation that can fail. + +The value objects work with System.Text.Json, EF Core and ASP.NET Core, with nothing to register per +type. + +### Value objects -Two value objects, the second one validated: +An order id is one line: ```csharp public readonly partial record struct OrderId : IValue; +``` + +When you build, the generators fill in everything an `OrderId` needs to stand in for a `Guid`: + +- **Make one and read it back** with `From` and `Value`. Two ids are equal when their `Guid`s are. +- **Sort and compare them** through `IComparable` and the `<`, `<=`, `>` and `>=` operators. +- **Bind one from a route or a query string,** because `Parse`, `TryParse` and `ToString` are there, + span forms included. +- **Send it and store it** through its JSON converter and its type converter. + +The JSON, the database column and the OpenAPI schema are the same as the underlying type's. So you +can change a `Guid` property to an `OrderId` without changing your API or your tables. +When a value has rules, you write them once, in `Create`: + +```csharp public readonly partial record struct Email : IValidatedValue { public static Result Create(string value) => @@ -61,105 +48,146 @@ public readonly partial record struct Email : IValidatedValue -{ - public static readonly OrderStatus Open = new(); - public static readonly OrderStatus Shipped = new(); - public static readonly OrderStatus Cancelled = new(); -} +var id = OrderId.New(); // a version 7 Guid + +Result created = Email.Create(input); // the fault says which rule refused it +Option maybe = Email.TryFrom(input); // is it valid? +Email known = Email.FromKnownGood("ops@example.com"); // a literal you vouch for; throws otherwise + +JsonSerializer.Serialize(known); // "ops@example.com" +JsonSerializer.Deserialize("\"nobody\""); // JsonException naming Email and NoAt, never the text +Email.Parse("ops@example.com", null); // IParsable, so it binds as a route or query parameter ``` -The generators add `All`, parsing by name, JSON as the name, an OpenAPI string enum and a text -column in EF Core, with nothing registered per set. `Match` takes one required delegate per member, -so a member added later fails every call until it is handled. +- **`Create`** when you want to know which rule failed. +- **`TryFrom`** when yes or no is enough. +- **`FromKnownGood`** for a value you vouch for, such as a literal. It throws if you were wrong. -The base package also gives you: +A struct always has a public parameterless constructor, and C# won't let you take it away. So the +analyzers make `new OrderId()` and `default(OrderId)` build errors (CMTK0001). An array created with +a length, whose slots all start out as `default`, gets a warning (CMTK0005). -- **`Patch`**, a field of a JSON Merge Patch: absent, null or a value. -- **`Result.Catch`**, which turns an exception into a result. -- **`AsText()` and `AsIdentifier()`**, which check text a person wrote and a value another system - issued. -- **`[Sealed]`, `[Concrete]` and `[RequireParameterlessConstructor]`**, constraints that C# has no - `where` clause for. The analyzers check them. +

+What the generators write for OrderId, trimmed -## Packages + +```csharp +public readonly partial record struct OrderId : IValue, IValueObject, IParsable, IComparable +{ + private readonly Guid _value; -| Package | What it does | Metalama | -|---|---|---| -| [CodoMetis.TypeKit](src/CodoMetis.TypeKit/README.md) | `Option`, `Result` and `Result`, the interfaces value objects are built on, `Patch`, and the text guards `AsText()` and `AsIdentifier()`. Brings the analyzers that check them | no | -| [CodoMetis.TypeKit.Analyzers](src/CodoMetis.TypeKit.Analyzers/README.md) | Build-time checks, CMTK0001 to CMTK0014. They arrive with the base package and run in Razor components too. They flag:
- a `default` value object, `Option` or `Result`, written in your code or handed out by `FirstOrDefault` and its kind
- a value object nothing generates
- an ignored result
- skipped validation
- a comparison of two value objects' values
- a type argument that breaks `[Abstract]`, `[Sealed]`, `[Record]`, `[Concrete]`, `[Interface]`, `[Unsealed]`, `[ValueObject]`, `[ClosedSet]`, `[FlagsEnum]` or `[ReadOnlyStruct]` on its parameter, or two that `[Distinct]` or `[Unrelated]` keep apart
- a type without the constructor its host will look for
- a closed-set `Match` bound by position, and a closed-set member written after it was named
- a `FaultKind` or `ConstructorAccessibility` that names no member | no | -| [CodoMetis.TypeKit.Generators](src/CodoMetis.TypeKit.Generators/README.md) | Generates your value objects at compile time. Add it to the project that declares them | yes | -| [CodoMetis.TypeKit.EntityFrameworkCore](src/CodoMetis.TypeKit.EntityFrameworkCore/README.md) | Maps value objects to columns with nothing to register per type, and makes `.Value` work in LINQ | no | -| [CodoMetis.TypeKit.AspNetCore](src/CodoMetis.TypeKit.AspNetCore/README.md) | Shows value objects in the OpenAPI document with the schema of their underlying type. Returns a `Result` as the response, and a fault as problem details | no | -| [CodoMetis.TypeKit.Serilog](src/CodoMetis.TypeKit.Serilog/README.md) | Logs value objects as what they print, also where Serilog destructures them, so a value masked by its `ToString()` stays masked | no | -| [CodoMetis.TypeKit.Testing](src/CodoMetis.TypeKit.Testing/README.md) | Test assertions for `Option`, `Result` and `Patch`. They hand back the value or the error, and fail saying what the outcome was. No test framework needed | no | + private OrderId(Guid value) + { + _value = value; + } -Take only what you need. If you only want `Option` and `Result`, the base package brings no code -generator with it. You take Metalama on once, on purpose, by adding `CodoMetis.TypeKit.Generators` to -your domain project. + public Guid Value + { + get { return _value; } + } -The EF Core and ASP.NET Core packages work at run time through the interfaces. So a host that maps -value objects from a referenced domain assembly needs no generator itself. + public static OrderId From(Guid value) + { + return new OrderId(value); + } -## Quick start + public int CompareTo(OrderId other) + { + return Comparer.Default.Compare(Value, other.Value); + } -Add the generators to your domain project, which brings the base package along. Add the EF Core -package to your data project, if you have one. + public override readonly string ToString() + { + return ToString(null, CultureInfo.InvariantCulture); + } -```bash -dotnet add package CodoMetis.TypeKit.Generators # the domain project: brings CodoMetis.TypeKit along -dotnet add package CodoMetis.TypeKit.EntityFrameworkCore # the data project, if there is one + // Also: Parse and TryParse, the formatting interfaces, the comparison operators, + // a JSON converter and a type converter, each nested in the type. +} ``` -Declare a value object with its validation: +The [Generators README](src/CodoMetis.TypeKit.Generators/README.md#what-is-generated) lists every +member, and says how to read the full generated code of your own types. -```csharp -using CodoMetis.TypeKit; -using CodoMetis.TypeKit.ValueObjects; +
-public enum EmailFault { Blank, NoAt } +### Option -public readonly partial record struct Email : IValidatedValue -{ - public static Result Create(string value) - { - if (string.IsNullOrWhiteSpace(value)) return Result.Error(EmailFault.Blank); - if (!value.Contains('@')) return Result.Error(EmailFault.NoAt); +An `Option` holds a value or nothing. Use it instead of `null` where a value may be missing: - return new Email(value.Trim()); - } -} +```csharp +Option found = customers.FirstOrNone(c => c.Email == email); + +string greeting = found.Match( + customer => $"Welcome back, {customer.Email}", + () => "Please sign up first"); ``` -Then use it: +What it gives you: + +- **No `.Value` to forget a check on.** You reach the value through `Match`, `TryGetValue` and the + other members, and each one asks what should happen when there is nothing. +- **No null in disguise.** `Option.Some(null)` throws, and writing `default(Option)` fails the + build (CMTK0001). + +### Result + +A `Result` holds a value, or an error of a type you choose. Use it for failures you expect, +such as invalid input, instead of an exception: ```csharp -var id = OrderId.New(); // a version 7 Guid +Result signedUp = Email.Create(input) + .MapError(_ => SignUpFault.InvalidEmail) + .Ensure(email => !customers.Exists(c => c.Email == email), SignUpFault.EmailTaken) + .Map(email => new Customer(CustomerId.New(), email)); + +string reply = signedUp.Match( + customer => $"Welcome, {customer.Email}", + fault => $"Sign-up failed: {fault}"); +``` -Result created = Email.Create(input); // the fault says which rule refused it -Option maybe = Email.TryFrom(input); // is it valid? -Email known = Email.FromKnownGood("ops@example.com"); // a literal you vouch for; throws otherwise +Each step runs only while everything before it succeeded. The first error skips the rest and ends up +in `Match`. -string reply = created.Match( - email => $"Welcome, {email}", - fault => $"Please check the address ({fault})"); +What it gives you: -JsonSerializer.Serialize(known); // "ops@example.com" -JsonSerializer.Deserialize("\"nobody\""); // JsonException naming Email and NoAt, never the text -Email.Parse("ops@example.com", null); // IParsable, so it binds as a route or query parameter +- **The error is part of the signature.** Whoever calls your method sees what can go wrong, and has + to handle it to get the value. +- **A result you ignore is reported** (CMTK0003). +- **Errors become HTTP responses.** Return `ToHttpResult()` from an endpoint, and an error becomes + a problem-details response. +- **Exceptions become results** where you want them to, with `Result.Catch`. + +`Option` and `Result` never end up on the wire by accident: System.Text.Json rejects both, rather +than writing `{}`. + +### Smart enums + +A closed set is an enum whose members can carry data. You declare it one line per member: + +```csharp +public sealed partial class OrderStatus : IClosedSet +{ + public static readonly OrderStatus Open = new(); + public static readonly OrderStatus Shipped = new(); + public static readonly OrderStatus Cancelled = new(); +} ``` -Set up EF Core and OpenAPI once, for every value object: +You get `All`, parsing by name, JSON as the name, an OpenAPI string enum and a text column in EF +Core. `Match` takes one required delegate per member, so a member added later fails every call until +you handle it ([Closed sets](docs/closed-sets.md)). + +### EF Core and ASP.NET Core + +You set them up once, for every value object: ```csharp services.AddDbContext(options => options.UseNpgsql(connectionString).UseTypeKit()); @@ -169,15 +197,41 @@ db.Customers.Where(c => c.Email.Value.EndsWith("@example.com")); // WHERE services.AddOpenApi(options => options.AddTypeKit()); // OrderId: {"type":"string","format":"uuid"} ``` +Value objects map to plain columns and keys, identity keys included, and `.Value` in a LINQ query +becomes plain SQL ([EF Core](docs/ef-core.md)). The OpenAPI document shows each value object as its +underlying type, plus the error responses an endpoint can return ([ASP.NET Core](docs/aspnet-core.md)). + +### And also + +- **JSON Merge Patch.** `Patch` tells "absent", "null" and "a value" apart + ([Merge patches](docs/merge-patches.md)). +- **Checks for written text.** `AsText()` and `AsIdentifier()` check text a person typed and an id + another system issued ([Written text](docs/text.md)). +- **Mistakes caught at build time.** Analyzers flag default values, ignored results, skipped + validation, and type arguments that break constraints C# can't express (`[Sealed]`, `[Distinct]` + and more). They also run in Razor files. A declaration that can't be generated fails the build with + its name on it. +- **Native AOT and trimming** in every run-time package, checked in CI by publishing real apps as + native binaries ([Native AOT](docs/native-aot.md)). +- **Secrets stay out of logs.** Error messages never quote the input, and Serilog logs a value object + as it prints itself ([Logging and secrets](docs/logging-and-secrets.md)). +- **Test assertions** for `Option`, `Result` and `Patch`, with no dependency on a test framework + ([Testing](docs/testing.md)). + ## Documentation -The pages below are guides, one topic each, built around examples. The package READMEs above are -the reference for each package: what it adds, how to set it up, and the rules it follows. Every +The pages below are guides, one topic each, built around examples. The package READMEs, listed +under [Packages](#packages), are the reference for each package: what it adds, how to set it up, and the rules it follows. Every exception message and analyzer help link points into them. Every C# block in the guides and the READMEs is compiled, and the outputs it states are tested. +You can also read all of it as one website, with a menu, search and the API of every run-time +package: [caffeinatedcoder.github.io/CodoMetis.TypeKit](https://caffeinatedcoder.github.io/CodoMetis.TypeKit/). +It shows the latest release. + | Page | Covers | |---|---| +| [Walkthrough](docs/walkthrough.md) | One orders service from request to response: value objects bound, validated, stored, queried, patched, logged and described, and what changes at each boundary | | [Value objects](docs/value-objects.md) | Declare one, pick a struct or a class, validate input, design its errors, pick the right factory, write your own members, generic code | | [Closed sets](docs/closed-sets.md) | Smart enums that carry data: declare one, handle every member with `Match`, send it as JSON, store it as a text column, read it from configuration, use one as an error type, and when an enum is still the right choice | | [Option](docs/option.md) | A value or nothing: create one, get the value out, combine options, convert at the boundary, and every public member | @@ -195,6 +249,34 @@ READMEs is compiled, and the outputs it states are tested. | [Migrating from 1.5 to 2.0](docs/migrating-to-2.md) | Move a project from 1.5 to 2.0: dropped results as build errors, queries that now run in the database or throw, names 2.0 adds, fault responses, Native AOT | | [How it compares](docs/comparison.md) | When another library fits better, and feature tables against the value-object, result and smart-enum libraries, versioned and dated | +## Packages + +One package uses [Metalama](https://postsharp.net/metalama), a framework that generates C# code at +compile time from templates written in C#. TypeKit's generators are built on it. The last column +shows which package brings it. + +| Package | What it does | Metalama | +|---|---|---| +| [CodoMetis.TypeKit](src/CodoMetis.TypeKit/README.md) | `Option`, `Result` and `Result`, the interfaces value objects are built on, `Patch`, and the text guards `AsText()` and `AsIdentifier()`. Brings the analyzers that check them | no | +| [CodoMetis.TypeKit.Analyzers](src/CodoMetis.TypeKit.Analyzers/README.md) | Build-time checks, CMTK0001 to CMTK0014. They arrive with the base package and run in Razor components too. They flag:
- a `default` value object, `Option` or `Result`, written in your code or handed out by `FirstOrDefault` and its kind
- a value object nothing generates
- an ignored result
- skipped validation
- a comparison of two value objects' values
- a type argument that breaks `[Abstract]`, `[Sealed]`, `[Record]`, `[Concrete]`, `[Interface]`, `[Unsealed]`, `[ValueObject]`, `[ClosedSet]`, `[FlagsEnum]` or `[ReadOnlyStruct]` on its parameter, or two that `[Distinct]` or `[Unrelated]` keep apart
- a type without the constructor its host will look for
- a closed-set `Match` bound by position, and a closed-set member written after it was named
- a `FaultKind` or `ConstructorAccessibility` that names no member | no | +| [CodoMetis.TypeKit.Generators](src/CodoMetis.TypeKit.Generators/README.md) | Generates your value objects at compile time. Add it to the project that declares them | yes | +| [CodoMetis.TypeKit.EntityFrameworkCore](src/CodoMetis.TypeKit.EntityFrameworkCore/README.md) | Maps value objects to columns with nothing to register per type, and makes `.Value` work in LINQ | no | +| [CodoMetis.TypeKit.AspNetCore](src/CodoMetis.TypeKit.AspNetCore/README.md) | Shows value objects in the OpenAPI document with the schema of their underlying type. Returns a `Result` as the response, and a fault as problem details | no | +| [CodoMetis.TypeKit.Serilog](src/CodoMetis.TypeKit.Serilog/README.md) | Logs value objects as what they print, also where Serilog destructures them, so a value masked by its `ToString()` stays masked | no | +| [CodoMetis.TypeKit.Testing](src/CodoMetis.TypeKit.Testing/README.md) | Test assertions for `Option`, `Result` and `Patch`. They hand back the value or the error, and fail saying what the outcome was. No test framework needed | no | + +Take only what you need. If you only want `Option` and `Result`, the base package brings no code +generator with it. You take Metalama on once, on purpose, by adding `CodoMetis.TypeKit.Generators` to +your domain project: + +```bash +dotnet add package CodoMetis.TypeKit.Generators # the domain project: brings CodoMetis.TypeKit along +dotnet add package CodoMetis.TypeKit.EntityFrameworkCore # the data project, if there is one +``` + +The EF Core and ASP.NET Core packages work at run time through the interfaces. So a host that maps +value objects from a referenced domain assembly needs no generator itself. + ## What TypeKit guarantees - **No `.Value` on `Option` or `Result`.** You reach the content through `Match`, `TryGetValue` and @@ -266,8 +348,8 @@ Good libraries exist for both halves of TypeKit. TypeKit fits when you want: [How it compares](docs/comparison.md) says what to choose instead when TypeKit doesn't meet your need. It also has detailed tables against Vogen, Thinktecture.Runtime.Extensions, StronglyTypedId, -LanguageExt, CSharpFunctionalExtensions, ErrorOr, Ardalis.SmartEnum and Intellenum, with versions -and dates. +SuperStrong.Types, LanguageExt, CSharpFunctionalExtensions, ErrorOr, Ardalis.SmartEnum and +Intellenum, with versions and dates. ## Built with Metalama @@ -293,7 +375,7 @@ your projects. Thanks to the Metalama team. ## Status -2.1.0 is the current version. 1.0.0, released on 28 September 2026, was the first. +2.3.0 is the current version. 1.0.0, released on 28 September 2026, was the first. [Migrating from 2.0 to 2.1](docs/migrating-to-2-1.md) lists what builds or runs differently since 2.0, and [Migrating from 1.5 to 2.0](docs/migrating-to-2.md) what changed since 1.5. @@ -304,7 +386,7 @@ one. [CHANGELOG.md](CHANGELOG.md) lists what each version changed. The design notes, the decision records and the roadmap are under [design/](design/plan.md). The measurements that decided the design are in -[spikes/](spikes/). +[spikes/](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/tree/main/spikes). ## Building @@ -323,6 +405,14 @@ Docker. Everything else runs without it. Much of the code was written with AI assistance, under the maintainer's direction and review. [CONTRIBUTING.md](CONTRIBUTING.md#ai-assisted-development) says what that does and doesn't mean. +## Acknowledgements + +TypeKit's value objects owe a great deal to [Vogen](https://github.com/SteveDunn/Vogen) by Steve +Dunn. Much of how they are generated started as an idea from Vogen. Thank you, Steve. + +The generators themselves run on [Metalama](https://postsharp.net/metalama), and +[Built with Metalama](#built-with-metalama) says what that makes possible. + ## License [MIT](LICENSE) diff --git a/design/adr/0109-work-is-tracked-as-labelled-issues-and-ci-checks-each-pull-request.md b/design/adr/0109-work-is-tracked-as-labelled-issues-and-ci-checks-each-pull-request.md new file mode 100644 index 00000000..2844b560 --- /dev/null +++ b/design/adr/0109-work-is-tracked-as-labelled-issues-and-ci-checks-each-pull-request.md @@ -0,0 +1,88 @@ +# 0109. Work is tracked as labelled issues under a release epic, and CI checks each pull request's labels, milestone, branch and issue link + +Date: 2026-10-18 +Status: accepted + +Context: 2026-10-18, after the release of 2.2.0. The repository had 66 pull requests and not one issue: none +of the 66 descriptions closed an issue, and the repository had no milestone and no label beyond GitHub's +defaults and Dependabot's two. Work was planned in [the roadmap](../roadmap.md) and recorded in the commits, +the decision records and [the fix history](../fix-history.md), so what was open, what a pull request was for +and which release it belonged to could be read only from files and branch names. CodoMetis.ValueRanges, the +sibling library, tracks its work as labelled issues and has a CI check that holds each pull request to its +labels and its issue; this repository adopts the same scheme, fitted to its own branches (decision 94) and +its own tooling (decision 92). + +## Decision + +**Work is tracked as issues.** A bug, an enhancement, a follow-up, a finding from a review or a probe: each is +an issue, opened before the work starts, through one of the forms in `.github/ISSUE_TEMPLATE/` (Bug, +Enhancement, Epic; no blank issues, and a security report goes to the private advisory form). + +**A release is an epic.** Each version has an `epic` issue and a milestone of the same name (`2.3.0`); its work +is attached as native sub-issues and carries the milestone. A theme that spans releases is an `epic` without a +milestone. [The roadmap](../roadmap.md) stays the plan and the decision records the reasons: an epic links its +section of the roadmap rather than copying it, and there is no roadmap issue. + +**Labels, on issues and pull requests alike**, as ValueRanges has them: + +| Label | Means | +|---|---| +| `bug`, `enhancement`, `documentation`, `tests`, `chore` | The type, exactly one | +| `severity/critical`, `severity/high`, `severity/medium`, `severity/low` | How bad a bug is, exactly one on a bug | +| `silent` | A bug that compiles or answers and does less than it appears to | +| `breaking` | Changes a signature, a generated member, a wire form or a diagnostic's severity: needs a major | +| `found-by/prober` | Found by the prober; the fix's commit carries `Found-By: PR` | +| `stacked` | Based on another open pull request | +| `epic` | A release or a theme; its sub-issues are the work | +| `package/core`, `package/analyzers`, `package/generators`, `package/efcore`, `package/aspnetcore`, `package/serilog`, `package/testing` | The package affected, one per package | +| `area/option-result`, `area/value-objects`, `area/json`, `area/aot`, `area/parsing`, `area/release`, `area/tooling` | The cross-cutting area | + +ValueRanges' `found-by/oracle` is `found-by/prober` here, its oracles' counterpart; its `roadmap` label has no +counterpart, there being no roadmap issue. The legend a contributor reads is CONTRIBUTING's "Issues and labels". + +**A pull request carries its issue's labels and milestone** and opens with a closing keyword (`Closes #12`), +from the template in `.github/pull_request_template.md`: Problem, Change, Tests. Since `Closes` closes an issue +only on a merge into `main`, and a topic merges into its release branch, the issue is closed by hand when its +topic merges, with a comment linking the pull request; the release's pull request into `main` closes its epic. + +**Held by CI.** `.github/workflows/pr-hygiene.yml` runs on every pull request into `main` or a release branch, +and again when its labels, milestone or description change. It runs `tools/PrHygiene`, a tool project under +decision 92 (BCL only, tested in `test/CodoMetis.TypeKit.PrHygiene.Tests`), on the event payload alone, and +fails a pull request that has + +- not exactly one type label, or a bug without exactly one severity, or `silent` on anything but a bug; +- no closing reference in its description; +- a head branch not named `/` (decision 94), or whose type disagrees with its type label: `feat` is + `enhancement`, `fix` is `bug`, `docs` is `documentation`, `test` is `tests`, and `build`, `refactor`, `spike` + and `release` are `chore`; +- a base `release/`, or a head `release/` into `main`, without the milestone ``; +- the label `breaking` and a release branch that is not a major (`x.0.0`). + +ValueRanges' script checks the first two; the branch and milestone rules hold decision 94's naming and the +release a pull request belongs to, which here are visible in the payload. Its rule for a `silent` fix in a minor +has no counterpart, since this repository ships no "changed answers" section; its rule that a breaking change +waits for a major is the one above. Dependabot's pull requests are not checked. + +Its job, "Labels and issue link", is a required status check: on `main` in its branch protection, beside +the test, pack and smoke-test jobs, and on `release/**` through the ruleset "Release branches: PR hygiene", +which does not apply to creating a branch, so a release branch is still cut by a push. + +## Consequences + +- What is open, and in which release, is a GitHub query rather than a reading of the roadmap. +- The labels count what the commit trailers count from the other side: `found-by/prober` beside `Found-By: PR`, + and the severity a trailer does not record. +- A pull request that misses a label fails a check that turns green when the label is added, without a commit. +- A pull request from a fork builds the check from its own checkout, so it can weaken the check on its own run + only, where the reviewer reads the diff; the workflow uses `pull_request`, never `pull_request_target`, and has + a read-only token. + +## Evidence + +- Counted on 2026-10-18 with `gh`: 66 pull requests, 0 issues, 0 milestones; 0 of the 66 descriptions match + GitHub's closing keywords. +- CodoMetis.ValueRanges at 1da4fa8: `.github/ISSUE_TEMPLATE/{bug,enhancement,epic,config}.yml`, + `.github/pull_request_template.md`, `.github/workflows/pr-hygiene.yml` and `.github/scripts/pr-hygiene.sh`, + and its 35 labels. +- The check's rules were each proved by deleting the rule from `tools/PrHygiene` and watching its cases fail + (12 seeds, each caught). diff --git a/design/adr/0110-catchasync-takes-a-valuetask-at-a-lower-priority.md b/design/adr/0110-catchasync-takes-a-valuetask-at-a-lower-priority.md new file mode 100644 index 00000000..eb62b342 --- /dev/null +++ b/design/adr/0110-catchasync-takes-a-valuetask-at-a-lower-priority.md @@ -0,0 +1,37 @@ +# 0110. `CatchAsync` takes a `ValueTask` at a lower overload priority, and `TraverseAsync` reaches `Option` and `Patch` + +Date: 2026-10-28 +Status: accepted + +Context: 2026-10-28, issue #88, the 2.3.0 roadmap item "`CatchAsync` over a `ValueTask`, and +`TraverseAsync` on `Option` and `Patch`". [Decision 30](0030-asynchronous-callbacks-return-task.md) +left `ValueTask` overloads out because beside the `Task` ones every `async` lambda became ambiguous +(CS0121). Since then a `ValueTask` reached `CatchAsync` only through `.AsTask()` or an `async` +lambda, and `stream.ReadAsync(buffer)`, the commonest `ValueTask` call, was the README's example +of the detour. C# 13's `[OverloadResolutionPriority]` removes the ambiguity: among the applicable +candidates of one type, the ones of lower priority drop out. + +## Decision + +**`CatchAsync` has four `ValueTask` forms**, over `Func>` and `Func`, valued and +command, wide and narrowed to one exception type, each `[OverloadResolutionPriority(-1)]`. They catch as +the `Task` forms do: an exception thrown before the task or after its first `await`, a timeout as the +error, a cancellation and an `AggregateException` of cancellations passed through, a null value and a task +of a task refused. They return `Task>`, as every asynchronous member does. + +Measured in `CatchAsyncValueTaskTests`, compiled as a consumer: an `async` lambda, valued or command, +with or without type arguments, a lambda that only throws with its type argument spelled, and a delegate +or method group over a `Task` all bind to the `Task` form, as before the `ValueTask` forms existed; a lambda +or method group over a `ValueTask` binds to the new form. Without the attribute the first group is CS0121. + +**`Catch` keeps refusing a `ValueTask` at run time.** A compile-time refusal beside `Catch(Func)` +is now possible the same way, but it would turn a call that builds today into a build error, which is a +major version's change, as a new rule at Error severity is. + +**Decision 30 stands for callbacks.** `TapAsync`, `MapAsync` and the rest take `Task`; a lambda given +to a callback is usually `async`, and it already binds to the `Task` form. + +**`TraverseAsync` on `Option` and `Patch`**, over `Func>>` and its +`CancellationToken` form, is the synchronous `Traverse` with the operation awaited: `None`, a clear and +an absent field call nothing and check no token, as the sequence's `TraverseAsync` checks nothing over +an empty sequence; with a value the token is checked before the operation and handed to it. diff --git a/design/adr/0111-a-mapping-can-withhold-the-fault-name.md b/design/adr/0111-a-mapping-can-withhold-the-fault-name.md new file mode 100644 index 00000000..0db61a8d --- /dev/null +++ b/design/adr/0111-a-mapping-can-withhold-the-fault-name.md @@ -0,0 +1,37 @@ +# 0111. A mapping can withhold a fault's name from its problem + +Date: 2026-10-28 +Status: accepted + +Context: 2026-10-28, issue #85, the 2.3.0 roadmap item that [decision 64](0064-fault-name-reaches-client-causes-not-told-apart-one-fault.md) +named: an additive opt-in that withholds the `fault` name for chosen members or a whole fault type, so two +causes can share a status without a mapping at the boundary. + +## Decision + +**The opt-in is part of the response a mapping gives a member**: `FaultResponse.WithoutFaultName()`, beside +`WithType` and `WithTitle`, and `FaultResponse.NamesFault` reads it. A mapping already answers member by +member, so chosen members are `fault => fault is SignInFault.Locked ? 403 : FaultResponse.Status(401).WithoutFaultName()` +and a whole type is the same call for every member. An endpoint's `WithFaultStatus` changes the status and +keeps the choice; there is no endpoint-level switch, since what a client may know about a fault is the host's +decision about the fault, not about a route. + +**A withheld member's problem names nothing of the member.** It has no `fault` and no `rule`, the rule naming +the cause as surely as the member does. Its `detail` is the occurrence's alone (`Fault.Detail`): a fault +set member's description, which otherwise stands in for a missing detail, is per member, and is left out with +the name. So two withheld members with the same status, `type` and `title` write the same problem but for the +detail the application wrote at the occurrence. A `type` or `title` the mapping gives one and not the other +tells them apart, and is the host's to give. + +**The host still sees the fault.** `IFaultFeature` carries the name and the rule as before, and gains +`NameWithheld`, a default member, so a `CustomizeProblemDetails` that copies the name into the problem can see +it would undo the mapping. + +**The document says it.** The problem schema is shared and its `fault` stays optional; a response whose every +fault, across the fault types the endpoint answers with, withholds its name has " The problem names no fault." +at the end of its description. A status a named member shares keeps its description. The startup check that +warns of two fault types writing one name counts only the members that write it. + +Rejected: a separate registration (`WithholdFaultNames()`), which a second place to read would make a +mapping's response depend on; and an attribute on the fault's member, which puts an HTTP decision into the +domain that declares the fault, as decision 64 rejected. diff --git a/design/adr/0112-the-prober-gives-way-to-valueranges-oracle-kit.md b/design/adr/0112-the-prober-gives-way-to-valueranges-oracle-kit.md new file mode 100644 index 00000000..ec65b332 --- /dev/null +++ b/design/adr/0112-the-prober-gives-way-to-valueranges-oracle-kit.md @@ -0,0 +1,55 @@ +# 0112. The prober gives way to CodoMetis.ValueRanges' oracle kit + +Date: 2026-10-28 +Status: accepted + +Context: 2026-10-28, issue #97, while 2.3.0's features landed. The prober (`tools/Prober`, about 5,500 lines) held +44 claims with 169 obligations, 38 probes over pairwise covering arrays, a ledger, a replay by footprint in CI and +a release gate ([decisions 96](0096-ci-replays-every-probe-and-the-release-runs-the-gate.md), +[99](0099-a-probe-row-says-how-it-ended.md) to [104](0104-a-claim-states-its-obligations-and-each-has-a-seed.md), +[107](0107-a-cited-fact-is-executable-and-seeds-run-by-footprint.md) and +[108](0108-a-probe-check-earns-credit-against-a-baseline-and-ws2-closes.md)). Its seeds cost a build of the whole +probe harness each: 170 seeds at about four minutes, so a release's `prober seeds --all` took roughly eleven hours. +One change to the fault responses, #85, staled 23 seeds of five probes, about ninety minutes, and a checkout +whose ledger had never recorded a catch wanted all 170. CodoMetis.ValueRanges had taken the same approach and kept +the parts that pay for themselves (its `docs/oracles.md`): no covering arrays, no ledger, no replay footprints, no +agent, with the sweeps as ordinary tests. + +## Decision + +**The prober is retired.** The tool, its tests, the corpus under `tools/probe/`, its 170 seeds, the `probes` and +`probe-status` jobs of `dotnet.yml`, the replay and gate steps of `release.yml`, `.claude/probe-axes.md`, the +`probe-triage` skill and the probe conventions tests are removed. DocsQuery's verified facts stay +([decision 93](0093-verified-facts-are-pinned-dated-and-executable-where-they-can-be.md)), fixtures included; its +check of the facts the corpus cited goes with the corpus. + +**ValueRanges' oracle kit takes its place**, under `test/oracles/` (its README is the manual): + +- `claims.json`: one entry per sentence a consumer-facing document promises, its source heading, the sentence + verbatim, the oracle kind, the public members it is about and the test classes that hold it. The first + catalogue is the prober's obligations, one claim per distinct sentence, 150 in all, 112 of them held by a test + named for it; an empty `heldBy` is the worklist. +- `surface-waivers.json`: every public member of the five run-time libraries is named by a claim or waived with a + reason. Members whose documents the prober never read are waived as `unread:`, naming #98, which reads them. +- `oracles.json` and `seeds/*.patch`: the oracles are ordinary test classes (five at first: `JsonParityTests`, + `NullDelegateTests`, `AsynchronousContextTests`, `NotWireTypeTests`, `KeyGenerationVariantTests`), each with a + seed in `src/`. CI checks, without building, that every oracle has a seed and every seed applies. +- `candidates.json`: known disagreements, each tied to an issue, so a sweep stays green without hiding them. +- `axes.md`: written by hand, what a consumer varies and the pairs no test varies yet. + +`test/CodoMetis.TypeKit.Oracles.Tests` holds the catalogue, in a project of its own because the surface is read by +reflection over the libraries, which the conventions project does not reference. `tools/Oracles` is the local tool, +a tool project like the others ([decision 92](0092-repository-tooling-is-dotnet-and-ci-needs-no-ai.md)): `seeds` +copies the working tree, applies one seed, builds the oracle's project alone and runs its class alone, in minutes, +and `status` prints the worklist. The seed verdict is one file compiled into both. + +**The release runs the suite, which holds the catalogue, and no gate.** Decision 97's gate, which bound from +2.2.0, is withdrawn with the replay it read; 2.2.0 shipped under it. Before a tag, `oracles seeds` shows every seed +caught, as `prober seeds --all` did, at a fraction of the cost. + +Superseded: decisions 96, 99, 100, 101, 102, 103, 104 and 108; decision 107 but for its fixture rule, which +DocsQuery keeps; and the gate of decision 97. + +Rejected: keeping the prober and dropping only its seeds, since the replay, the footprints and the gate carried the +same weight; and porting the 38 probes as they were, since their harness compiled consumer projects per row. A probe +that found a bug is ported as an ordinary oracle class when its area is next touched, with its seed. diff --git a/design/adr/0113-ci-runs-the-lanes-a-change-can-affect.md b/design/adr/0113-ci-runs-the-lanes-a-change-can-affect.md new file mode 100644 index 00000000..6eb9ebc8 --- /dev/null +++ b/design/adr/0113-ci-runs-the-lanes-a-change-can-affect.md @@ -0,0 +1,138 @@ +# 0113. CI runs the lanes a change can affect, a gate job decides the merge, and the release runs every lane + +Date: 2026-10-29 +Status: accepted + +Context: 2026-10-29, issue #118, with #70, #71, #73, #74 and #117 as its parts. Since the prober left CI +([decision 112](0112-the-prober-gives-way-to-valueranges-oracle-kit.md)), the build workflow had five jobs, and +every pull request into a release branch ran all of them whatever it changed. Its `changes` job skipped Pack and +Consumer when no listed path changed, but it failed open, missed a rename's source and missed two inputs (#70). +GitHub reads a skipped job as a passing required check, so nothing told a deliberate skip from an accidental one +(#71). Pack and Consumer waited for the whole test matrix (#117). The release ran neither the Windows and macOS +legs nor the smoke test's own-feed mode, and nothing ran CI unless something was pushed (#74). The maintainer's +view: the three-platform build, packing and the consumer smoke test are mostly release work, and a pull request +that changes only prose should not pay for them. Over the 150 runs before this decision, a Windows or macOS leg +failed with Linux passing three times. Two of those were prober tooling. One was a defect in `src/`: the analyzer's +code fix wrote CRLF beside LF in a Windows checkout (f9f64e5). macOS never failed alone. + +## Decision + +**Four lanes.** Every run has the Linux test leg, which runs the whole suite (a prose-only change, the prose suite +below): the conventions, the compiled docs samples, the oracle catalogue and the PostgreSQL round trips. The +`windows` lane is the Windows test leg. The `macos` lane is the macOS test leg, which only the runs that select +nothing get. The `pack` lane is the Pack job (pack, the smoke test against the packed feed, the SBOMs) and the +Consumer job (the smoke test packing its own feed). The Windows and macOS legs test the repository's own handling +of paths, line endings and child processes, not the .NET runtime, so Windows runs where code does that itself, and +macOS, which adds a case-insensitive file system and caught nothing alone, waits for the release's checks. + +**A changed path selects lanes by its class.** The first class whose pattern matches decides. A path that no +class names selects every lane a selection can (all but macOS), so a new kind of file costs time rather than +coverage: + +| Class | Paths | Lanes | +|---|---|---| +| changelog | `CHANGELOG.md` | Linux, pack: the packages' release notes are its section (#75), read by a property function that no evaluation lists | +| source text | `src/CodoMetis.TypeKit.Analyzers/**`, `src/CodoMetis.TypeKit.Generators/**` | Linux, Windows, pack: the packages that write or edit source text, whose line endings a Windows checkout changes | +| package | `src/**` | Linux, pack: the run-time packages, which use the runtime's own paths and text | +| smoke test | `tools/ConsumerSmokeTest/**`, `tools/Shared/**`, `tools/Directory.Build.*`, `tools/CiLanes/**` | Linux, Windows, pack: the smoke test's own sources and what it compiles in, and the selection itself | +| prose | `docs/**`, `design/**`, `.github/ISSUE_TEMPLATE/**`, `**/*.md` | Linux, with the prose suite when every path is prose | +| code no package reads | `test/**`, `spikes/**`, `tools/**` | Linux, Windows: they handle paths, line endings and child processes themselves | +| anything else | the build files (`Directory.*.props`/`targets`, `Directory.Packages.props`, `global.json`), `.config/`, the workflows, `benchmarks/`, `.claude/`, any path not listed | Linux, Windows, pack | + +**Which runs select.** A pull request into `release/**` and a push to `release/**` are selected by their diff, +`git diff --name-only --no-renames -z`: a pull request against `origin/...HEAD`, a push against the commit +it replaced (`github.event.before`). A push that creates the branch, or whose previous commit the clone does not +have, runs every lane, macOS included. So does every other run: a pull request into `main` (the release's), a push +to `main`, a dispatch, the nightly run and the call from `release.yml`. A diff that names no path also runs every +lane. + +**A prose-only change runs the prose suite.** When every changed path is prose, the Linux leg restores, builds and +tests `test/Prose.slnf` instead of the solution: the conventions tests, the samples, the docs-query tests and the +oracle tests, the four projects that read prose, and the woven probes the conventions tests read. Measured on the +first docs-only run, the whole suite's test step took 3.6 min, set by projects that read no prose: EF Core 2m44, +conventions 1m50, analyzers 1m40, ASP.NET Core 1m32. `ProseReaderTests` holds the set: a test project outside it +whose sources name `docs`, `design`, `.github`, a path under one of them, or a Markdown file outside `src/` fails it, +unless the file is waived with the reason it reads nothing. A package's README is classed with its package, so the +analyzer, generator, core and ASP.NET Core tests that read theirs stay outside the set. + +**The selection is `tools/CiLanes`**, a tool project under [decision +92](0092-repository-tooling-is-dotnet-and-ci-needs-no-ai.md). Its `select` command answers `legs` (the test +matrix, a JSON array of one, two or three runners), `pack` (`true` or `false`) and `suite` (`prose` or `full`), +and writes each path's class to the job summary. Its `classify` command answers the same table for paths given on +the command line. A conventions test runs `classify` over every file MSBuild evaluates for each shipping project +and the smoke test, items and imports both, and fails a file that does not select `pack`. + +**A gate job, `CI`, decides the merge.** It needs every other job, runs under `always()`, and runs `ci-lanes gate` +on `toJSON(needs)`. It passes only when `changes` succeeded, its `legs` is exactly one of the three matrices it +writes (Linux; Linux and Windows; all three), its `pack` exactly `true` or `false`, its `suite` exactly `prose` or +`full`, and `prose` only beside the Linux leg alone and no pack, the test matrix succeeded, and Pack and Consumer +succeeded when selected and were skipped when not. A failed selection, a cancelled leg, a misspelled output and a +selected job that was skipped each fail it. A job the gate's table does not know also fails it, and a conventions +test holds `CI`'s `needs` to every other job of the workflow. Once `CI` reports on `release/2.3.0` and `main`, the +required checks on `main` become `CI` and `Labels and issue link`, and the release-branch ruleset adds `CI`. Every +selected leg blocks. + +**Pack and Consumer need only `changes`.** They start beside the test legs and no longer wait for them; the gate +still requires both (#117). + +**Before the Windows and macOS legs may skip, a Linux conventions test refuses non-portable file names** (#73). It +fails a character Windows refuses (`<>:"|?*\` and controls), a segment ending in a dot or a space, a reserved +device name, two paths equal under case folding and Unicode normalization, and a path longer than 160 characters, +so a prose-only change cannot break the Windows checkout unseen. + +**The release runs every lane before it verifies.** `dotnet.yml` is also `on: workflow_call`, and `release.yml`'s +`ci` job calls it; `verify` needs `ci`. A called workflow takes its caller's `github.workflow`, and concurrency +groups ignore case (KB-043), so `dotnet.yml`'s group is `ci-${{ github.workflow }}-${{ github.ref }}`. Without the +prefix the call would share the group `release-` with its caller and wait on it. + +**A nightly run.** `nightly.yml` runs on a schedule and on dispatch. It dispatches `dotnet.yml` on `main` and on +every `release/*` branch, each a full run. A schedule runs only from the default branch, so it starts once 2.3.0 +merges into `main`. GitHub turns a public repository's schedules off after 60 days without activity, so nothing +depends on it: the release calls the workflow itself. + +## Consequences + +- A prose-only pull request runs Changes, the Linux leg and the gate. A change to a run-time package adds the pack, + a test or tool change adds Windows, and a change to the analyzers, the generators, the smoke test, the build or + the workflows runs Linux, Windows and the pack. +- No selected run has the macOS leg. A defect only macOS shows (a case collision the file-name test misses, a + symlinked temporary directory) surfaces on the release's pull request into `main`, the nightly run or the + release. +- A line-ending or path defect in a run-time package other than the analyzers and generators is tested on Windows + only by those same runs. None has been seen. +- Merging a topic into its release branch selects again by the merge's own diff, so a docs merge is cheap on push + as well. +- Prose read by a test on Windows or macOS (a README whose stated outputs a sample test reads) is tested there + only by the release's pull request into `main`, the nightly run and the release. The repository has no + `.gitattributes`, so Windows checks such files out with CRLF. A prose change that breaks a test only under CRLF + surfaces on the release pull request, not on its own. +- Every run builds `tools/CiLanes` twice, in Changes and in the gate. Measured on the first runs, the Changes job + took 0.3 min and the gate 0.2. That is the cost of selecting in C# under decision 92 rather than in a shell `case`. +- A prose-only change compiles none of the tests that read no prose, so a document that breaks one of them (by a + read nobody declared, which `ProseReaderTests` looks for in the sources' literals) surfaces on the next run that is + not prose-only. +- A path class is changed here, by a new record, and in `tools/CiLanes` together. The conventions test reads the + build, not this table. + +## Evidence + +The 11 successful runs of `dotnet.yml` since the prober left (2026-10-28 21:08 to 2026-10-29 08:21 UTC), from +`gh run view`'s job timings: + +| Job | Median | Max | +|---|---|---| +| Changes | 0.1 min | 0.1 | +| Test (ubuntu-latest) | 6.3 min | 7.0 | +| Test (windows-latest) | 8.5 min | 9.3 | +| Test (macos-latest) | 7.2 min | 15.2 | +| Pack | 6.2 min | 6.6 | +| Consumer smoke test | 5.7 min | 6.3 | + +Wall clock was 15.0 min median (12.6 to 21.4) and 33 runner-minutes. Over the last 150 runs, those where a +Windows or macOS leg failed and the Linux leg passed were 38927646009 and 38929696748 (prober tooling, both +Windows) and 40827936927 (the analyzer's code fix, Windows, fixed by f9f64e5). The critical path was the slowest test leg +followed by Pack. Within a leg, the build took 1.2 to 2.1 min and the tests 3.1 to 5.5 min. Pack's smoke test took +3.0 to 3.7 min, and Consumer's 3.2 to 3.6. The repository is public, so the minutes cost nothing, and the target is +wall clock. + +The before and after runs of a docs-only change and of a `src/` change are recorded on #118 and its pull request. diff --git a/design/adr/0114-a-value-object-s-temporary-number-is-its-wrapped-type-s.md b/design/adr/0114-a-value-object-s-temporary-number-is-its-wrapped-type-s.md new file mode 100644 index 00000000..c136f65a --- /dev/null +++ b/design/adr/0114-a-value-object-s-temporary-number-is-its-wrapped-type-s.md @@ -0,0 +1,59 @@ +# 0114. A value object's temporary number is its wrapped type's, carried by its converter's mapping hints + +Date: 2026-10-29 +Status: accepted + +Context: [#86](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/issues/86), the roadmap's 2.3.0 item; +[EF Core](../ef-core.md); supersedes the `ulong` half of [decision 63](0063-char-keys-application-assigned-ulong-documented.md). + +Decision 63 left a default value-object key over `ulong` throwing `OverflowException` at `Add` on SQL +Server and PostgreSQL, whose column is a `decimal`, and ruled out three routes to a temporary `ulong`: a +delegate given to `HasValueGenerator` (refused by `dbcontext optimize`), a factory type on the property +(it comes before the providers' HiLo), and a type mapping's own factory (no public setter on the +provider's mapping). Read in EF Core 10.0.12's source, the cause is narrower than the column: EF gives a +generated property a temporary number by its type mapping's CLR type +(`TemporaryNumberValueGeneratorFactory.Create`), which for a value object is the value object, so it +falls back to the converter's provider type, the column's `decimal`. A plain `ulong` key's mapping says +`ulong`. + +**A value object over a number gets the temporary values a property of its wrapped type gets.** The +converter `UseTypeKit()` composes carries `ConverterMappingHints` with a value generator factory, which +`CoreTypeMapping` takes into the value object's mapping where the provider's has none. That is the fourth +route, and the only one in the right place: `ValueGeneratorSelector` consults a mapping's factory after a +factory on the property, and the SQL Server and PostgreSQL selectors choose HiLo before either. So an +application's own generator and HiLo still come first, as for a plain key. + +- The numbers are EF's list (`RelationalValueGeneratorSelector.FindForType`): every integer type, `char` + included, and `decimal`, `float` and `double`. Where the column is of the wrapped type, the factory + returns EF's own temporary generator, as before; where the provider stores the wrapped type as another, + it asks the provider for a key of the wrapped type generated on add, in a model of its own, as the Guid + and string keys' generator already does. +- **Siblings, found by the variant catalogue** (`KeyGenerationVariantTests`, configured `ValueGeneratedOnAdd()` + over every key kind on three providers): a key over `char` configured on add got one character of a GUID + string on SQL Server, from the column's `nvarchar(1)`; it now gets a temporary `char`, as a plain one does. + The catalogue's two pinned departures are gone. +- **A compiled model keeps it, except under `--nativeaot`.** `dbcontext optimize` leaves the type mappings to + be found when the model is first used, where the plugin composes them. `--nativeaot` writes each mapping + out as code (`SqlServerDecimalTypeMapping.Default.Clone(converter: new ValueConverter(…))`), + and a factory in the hints is a delegate it cannot write: a default `ulong` key still throws at `Add` + there, and `ValueGeneratedNever()` remains the workaround. A test pins it. +- **Nor does a converter the application names.** `HasConversion>()` builds the + mapping on the public converter, which carries no hints, and the key, declared generated, still throws at `Add`. + Hints on that converter would make `dbcontext optimize` write the mapping of every property that names it out as + code (`CSharpRuntimeModelCodeGenerator`, a converter with hints), which loses the factory there too, so it is + documented and pinned instead. +- `ConverterMappingHints.ValueGeneratorFactory` is marked `[Obsolete]` without a message in EF Core 10; the + constructor parameter is not. A later EF that removes it fails the tests that hold this, not silently. +- PostgreSQL's provider refuses HiLo on a `ulong` key, plain or value object ("Sequences can only be used + with integer properties"), so only SQL Server's HiLo is tested for one. + +Measured with EF Core 10.0.12, Npgsql 10.0.3 and SQL Server's provider 10.0.12, at `Add` and in the +selector, no database opened. EF caches a property's generator by the model's id +(`ValueGeneratorCache.CacheKey`), which a compiled model keeps from the model it was written from: a test +that builds a model twice in one service provider reads the first one's generator, so the compiled-model +tests each get a service provider of their own. + +`KeyGenerationTests` holds it: a default `ulong` key gets the temporary values a plain one does on three +providers, SQL Server's HiLo is still chosen, an application's generator still wins, a compiled model gives +the temporary value, and a `--nativeaot` one still throws. Without the hints, seven of them fail; with the +factory set on the property instead, the HiLo test fails. diff --git a/design/adr/0115-value-objects-can-be-inlined-everywhere.md b/design/adr/0115-value-objects-can-be-inlined-everywhere.md new file mode 100644 index 00000000..b56608cd --- /dev/null +++ b/design/adr/0115-value-objects-can-be-inlined-everywhere.md @@ -0,0 +1,71 @@ +# 0115. A setting of `AddTypeKit` writes every value object in place, with no component of its own + +Date: 2026-10-29 +Status: accepted + +Context: [#83](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/issues/83), in the 2.3.0 epic; +[OpenAPI](../openapi.md). With `AddTypeKit()` a value object in a JSON position keeps a component named after it +([decision 24](0024-nested-value-objects-named-after-nesting.md)), and a parameter is written in place where ASP.NET +writes one of the wrapped type so, or where the value object wraps a type the options describe +([decision 106](0106-a-parameter-of-a-value-object-over-a-described-type-is-inlined.md)). Some clients want plain +types: a generator that makes a class of every component gets a `CustomerId` wrapper where the application wanted a +`string`, and under OpenAPI 2.0 a nullable property of a component loses its type +([KB-026](../knowledge/KB-026-openapi-2-nullable-component-property-loses-its-reference.md)). + +A host could already inline value objects with a `CreateSchemaReferenceId` of its own, set after `AddTypeKit()`, +that returned `null` for every type carrying `GeneratedValueObjectAttribute`: the package README showed that +predicate, and the transformer's inline path, nullable positions included, was measured and tested through it. It +asks the host to reflect over a `CompilerServices` attribute, and it replaces the delegate `AddTypeKit()` installed, +so a closed set lost its nesting-chain name and decision 55's check with it. + +## Decision + +**`AddTypeKit(typeKit => typeKit.InlineValueObjects = true)` writes every value object in place.** An overload of +`AddTypeKit` takes an `Action`; the setting is off by default, and `AddTypeKit()`, or the +overload with the setting off, writes the document it wrote before, byte for byte. A settings object rather than a +`bool` parameter leaves room for a second setting without another overload, and it is per document, as `AddTypeKit` +is per `OpenApiOptions`. + +**It is done where the names are given.** The `CreateSchemaReferenceId` that `AddTypeKit` wraps answers `null` for a +value object, or the `Nullable` of one, before it asks the host's delegate. Every position then takes the path a +host's own `null` took: a property, a body, a response, a list's or a dictionary's elements, a parameter and an element +of a query or header array, at any depth, each the wrapped type's schema in place, and a nullable value object admits +null in its own schema as a nullable property of the wrapped type does +([decision 36](0036-nullable-value-object-admits-null-openapi.md)). A dictionary keyed by a value object is the one +keyed by the wrapped type, since ASP.NET describes no key. + +**Value objects only.** A closed set keeps its component, and so does the type a value object wraps where ASP.NET makes +it one (an enum): the value object holds that component's content in place, as with the host's predicate. +A closed set is an enum of its own names, not a wrapper of another type's schema, and inlining the wrapped types would +be `CreateSchemaReferenceId = _ => null`, which ASP.NET already offers. + +**With decision 55**, an inlined value object claims no name: a nested value object and a top-level one of its chain's +name are two schemas in place, not a refusal. A closed set still claims its name, and a document in which two types +claim one is still refused. A name the host's own delegate gives a value object is not used; one it gives any other +type is kept, as before. A delegate set after `AddTypeKit` replaces the setting, as it replaces the naming. + +**With decision 106**, the setting leaves nothing to decide: where no value object is a component, its JSON positions +and its parameters share no schema, so a parameter over a described type is written in place as a parameter, and its +JSON positions as the converter writes them, each where it is used. + +## Consequences + +- The package README's predicate gives way to the setting in the documents. It still works, and + `InlinedValueObjectTests`, `HostConfigurationTests`, `DescribedParameterTests` and `PolymorphicBaseTests` keep holding + a host's own `null`. +- `SchemaTests` holds each value-object property to the wrapped type's under both settings, in OpenAPI 3.1, 3.0 and 2.0; + inlined, the nullable properties, the containers and the nested containers too, whose controls were added. It is + registered as an oracle, with the seed `inline-value-objects-ignored`. +- `NullablePositionTests`' `inline-value-objects` mode is the setting, in every position and version it already ran. +- `InlineValueObjectsSettingTests` holds the rest: no component of a value object in any version, the default document + unchanged, a later plain `AddTypeKit()` keeping them inlined, the host's names for other types, and decision 55's + claims. + +## Evidence + +- At the change: `SchemaTests`, `NullablePositionTests` and `InlineValueObjectsSettingTests` 705 green. +- With the setting ignored (the `null` taken out): 169 of 522 fail: 92 inline rows of `SchemaTests` (all but the + dictionary keyed by a value object, in each version), 72 `inline-value-objects` rows of `NullablePositionTests`, and + five of `InlineValueObjectsSettingTests`. `oracles seeds inline-value-objects-ignored`: caught, 92 of 164. +- With the setting on by default: 70 of 174 fail, every component row of `SchemaTests` and the default-document test + in each version. diff --git a/design/adr/0116-the-docs-site-is-docfx-built-from-main.md b/design/adr/0116-the-docs-site-is-docfx-built-from-main.md new file mode 100644 index 00000000..2aae3b3c --- /dev/null +++ b/design/adr/0116-the-docs-site-is-docfx-built-from-main.md @@ -0,0 +1,67 @@ +# 0116. The docs site is DocFX, built from main, from the sources as they are + +Date: 2026-10-29 +Status: accepted + +Context: [#115](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/issues/115); CodoMetis.ValueRanges chose DocFX +in its spike ([CaffeinatedCoder/CodoMetis.ValueRanges#417](https://github.com/CaffeinatedCoder/CodoMetis.ValueRanges/issues/417)); +[decision 113](0113-ci-runs-the-lanes-a-change-can-affect.md) holds the CI lanes this adds a path to. + +The guides in `docs/`, the root README and the package READMEs could only be read as Markdown on GitHub: no +navigation beyond the README's Documentation table, no search across them, and the XML docs of every public +member shown only by an IDE. + +**The site is DocFX, publishing to GitHub Pages from `main` alone.** `.github/workflows/docs.yml` builds +`docs/*.md`, `README.md`, `src/*/README.md`, `CHANGELOG.md` and the API pages of the run-time packages +(`CodoMetis.TypeKit`, `.EntityFrameworkCore`, `.AspNetCore`, `.Serilog`, `.Testing`) and deploys them on a push to +`main`, which is the released state: topics merge into `release/`, which merges into `main` when it is +tagged. A release branch is never published, so the site always describes the version `dotnet add package` +installs. The Analyzers' surface is their rules and the Generators' is what they generate into a consumer, so +their READMEs are their reference and they get no API pages. DocFX is pinned in `.config/dotnet-tools.json` +(2.81.0) and configured in `site/docfx.json`, inside the repository because DocFX refuses a content root outside +its base directory. A second line (`/2.x/`, `/3.x/`) waits until two are supported at once; CONTRIBUTING.md says +how one would be added. + +**The pages are the files as they are.** Nothing copies or edits a page for the site, so `ReadmeSampleTests` and +`OracleCatalogueTests` hold the text a reader sees. A link between two pages resolves as written, since every +page is a site page. A link to a file of the repository that is not a page (`design/`, `CONTRIBUTING.md`, +`AGENTS.md`, `SECURITY.md`, `LICENSE`) becomes a GitHub address at the built commit, rewritten by +`site/template/conceptual.extension.js` at build time. Those files are the build's resources, under `_repository/`, +so DocFX checks that each exists; the workflow then deletes them from the output. A link to a directory names no +file DocFX can check, so the README's one link to `spikes/` became an absolute address on `main`, which +`DocumentationLinkTests` checks. + +**A pull request that touches a page checks its links.** On a pull request into `main` or a release branch whose +paths a page is built from (`*.md`, `docs/`, `design/`, `src/`, `site/`, the SDK and tool pins), the workflow runs +the whole build with `--warningsAsErrors` and publishes nothing. Measured on 2026-10-29: a seeded link to a missing +page (`InvalidFileLink`), to a missing heading of a page (`InvalidBookmark`) and to a missing design record +(`InvalidFileLink`, through the resources) each fail the build; without them it reports no warning. An anchor into +a file that is not a page is left to `DocumentationLinkTests`, which reads every anchor as GitHub derives it. + +**`site/**` is prose** in `tools/CiLanes`: the docs workflow and the conventions tests read it, and nothing is +built from it, so a change to it alone runs the prose suite. `ProseReaderTests` counts a literal naming `site` as +prose, so no test outside `test/Prose.slnf` reads it unseen. The workflow itself is a workflow, which decision 113 +already classes as anything else. `DocsSiteTests` holds the menu (`site/toc.yml`) to the README's Documentation +table, in its order, and to every shipping package's README, and the API pages to every run-time package. + +**The spike: DocFX lists the members of C# 14 extension blocks.** `src/` holds 35 `extension(...)` blocks, which +carry most of the `Option` and `Result` extensions, `AsText` and the `IQueryable` helpers, and ValueRanges had none. +Measured with DocFX 2.81.0 on the published assemblies: + +- Every instance member of a block is listed on its static class as the extension method the compiler lowers it + to, with its XML docs: `Option.FirstOrNone(this IEnumerable source)`, `StringTextExtensions.AsText(this + string? text)`: 37 extension methods on `Option` and 52 on `Result`. +- The type a block extends lists them under "Extension Methods": `Option`'s page shows `Select`, `SelectMany`, + `Where`, `Zip`, `ToResult` and `Result.Traverse`. A member whose constraints the open type does not meet is + left out there, as for any extension method: `OrDefault()` (`where T : struct`) and `OrNull()` + (`where T : class`) are on `Option`'s page only. +- The signature shown is the lowered one, `this in Option a`, not the `extension(...)` block it is declared in. +- The one static member of a block, `GuidValueExtensions.New()`, is listed as `New()` with its constraints, + on `GuidValueExtensions` only, not as `OrderId.New()` on the value objects it serves. + +So the API pages ship. The workflow fails if `Option`'s or `Result`'s page stops listing its +extension-block `Select`, so a DocFX update that loses them fails there, not on a reader. + +**Pages is configured** to deploy from GitHub Actions (Settings > Pages > Source), the one setting no workflow can +make: on 2026-10-29 the API reports `build_type: workflow` at `https://caffeinatedcoder.github.io/CodoMetis.TypeKit/`. +The first deployment follows the 2.3.0 release's merge into `main`. diff --git a/design/adr/0117-a-seed-is-caught-against-a-passing-baseline.md b/design/adr/0117-a-seed-is-caught-against-a-passing-baseline.md new file mode 100644 index 00000000..637f323c --- /dev/null +++ b/design/adr/0117-a-seed-is-caught-against-a-passing-baseline.md @@ -0,0 +1,54 @@ +# 0117. A seed is caught against a passing baseline, by a test of its own + +Date: 2026-10-29 +Status: accepted + +Context: [#151](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/issues/151), found by the 2.3.0 pre-release +review. [Decision 112](0112-the-prober-gives-way-to-valueranges-oracle-kit.md) made `oracles seeds` the release's +proof that every oracle can fail; [decision 108](0108-a-probe-check-earns-credit-against-a-baseline-and-ws2-closes.md) +had held the prober it replaced to a baseline, and the port did not carry that over. + +`oracles seeds` credited a seed whenever one test of its oracle failed with a row that was not a known candidate, +whatever the failure was. On an arm64 Mac, SQL Server 2022 under QEMU stops before it is ready, so the SQL Server +collection fixture throws `ContainerNotRunningException` in `InitializeAsync` and every test of both SQL Server +oracles fails, with and without a seed. The tool reported both seeds `CAUGHT` (10,071 of 10,071 and 170 of 170 tests +failing) and exited 0, ending `every seed is caught`, while no query or insert under review had run. + +**A seed is caught only when its oracle passes without it and fails with it, in a test of its own.** `oracles seeds` +first runs every oracle the selected seeds name on a copy of the tree with no seed, built and filtered as the seeded +runs are, in the same process and environment. An oracle that does not pass there (a run that never reached its +tests, or a test that fails on a row that is not a known candidate) makes each of its pairs `ERROR`, and is not run +with the seed. A baseline whose only failures are known candidates passes, since an unfiled candidate fails its sweep +until it is filed. + +**A run that did not reach the tests' own code is `ERROR`, never `CAUGHT`, with or without the seed.** What the TRX +holds decides it, measured with xunit.v3 4.0.2 on Microsoft Testing Platform on 2026-10-29, one class per kind of +failure: + +- A class, collection or assembly fixture that throws in its constructor or `InitializeAsync` fails every test that + uses it, each with xunit's own message (`Collection fixture type 'T' threw in InitializeAsync`, the fixture's + exception on the next line, the stack only an inner one). xunit.v3.core 4.0.2 holds four more setup messages of + the same kind: unresolved constructor arguments, more than one public constructor, constructor parameters without + fixture data, and a type it cannot create. A failed test whose message begins with one of them is setup. +- A fixture that throws in `Dispose` or `DisposeAsync` fails no test: the TRX counts it as an `error` in its + `Counters` and holds no result for it. A nonzero `error`, `aborted`, `timeout` or `disconnected` counter is an error. +- A test host that crashes (`Environment.FailFast`) writes no TRX and exits 134. Microsoft Testing Platform exits 0 + when every test passes and 2 when one fails; any other exit code, or no TRX, is an error. +- A run whose every test is skipped ran nothing, and is an error. +- A failure in the test class's own constructor or `InitializeAsync` is written like one in the test's body: the + exception's message, and a stack trace under the test's code. It counts as the test's: the oracles build there + what they assert on (`KeyGenerationVariantTests` opens its connection in its constructor), and the baseline has + shown that it passes without the seed. + +The rule is one file, `test/oracles/SeedVerdict.cs`, compiled into the tool and into `SeedVerdictTests`, which hold +it on TRX documents of the measured shape. With the setup check and the baseline taken out, the dead fixture of #151 +reads as caught again and three of those tests fail. + +**`ERROR` exits 4**, beside 1 for a seed missed, so a release script tells a seed that needs a stronger oracle from an +environment that needs fixing. A release on an arm64 Mac runs `oracles seeds` with `TYPEKIT_MSSQL_IMAGE` naming +Azure SQL Edge, as the suite does (CONTRIBUTING's Docker bullet). + +Rejected: telling a setup failure apart by its stack trace (a frame of `InitializeAsync`, a constructor), since a +test's own exception passes through product code whose frames carry the same names, and xunit writes the fixture +cases in fixed words of its own; and requiring the seeded run to fail on the rows the baseline passed, row by row, +since a test's rows are not in the TRX and the baseline already rules out a failure that does not depend on the seed. diff --git a/design/adr/0118-an-error-may-learn-a-form-only-new-api-can-write.md b/design/adr/0118-an-error-may-learn-a-form-only-new-api-can-write.md new file mode 100644 index 00000000..7bb3e799 --- /dev/null +++ b/design/adr/0118-an-error-may-learn-a-form-only-new-api-can-write.md @@ -0,0 +1,49 @@ +# 0118. An Error may learn, in a minor version, a form only that version's new API can write + +Date: 2026-10-29 +Status: accepted + +Context: 2026-10-29, issue #153, found by the pre-release sweep of `release/2.3.0` at `99c6032`; refines +[decision 34](0034-errors-fixed-within-major-version-warnings.md). 2.3.0 gives every assertion of +`CodoMetis.TypeKit.Testing` a form on a `Task` or `ValueTask` of what it checks (#87), and CMTK0003, an +Error since 2.0.0 ([decision 50](0050-warnings-cmtk0003-alone-becomes-error-2.md)), reports such an +assertion left without `await` (`IgnoredOutcomeAnalyzer.IsPendingAssertion`): it fails the test only through +the task it returns, so the dropped task passes whatever the value is. Decision 34 says an Error that +reports more waits for a major version, and the Analyzers README promised the same, so the 2.3.0 analyzer +contradicted the 2.3.0 README with no record of why. + +## Decision + +**An Error may report, in a minor version, a form that only an API new in that same version can write.** +Decision 34 keeps Errors fixed within a major so that an update never fails a build that worked before. +A form that needs the new API to be written cannot occur in code that built on the previous version, so +reporting it fails no such build. The rule holds when every reported form calls the new API itself; a +form that merely sits beside it, or that a name alone recognises, does not qualify. + +**What qualifies in 2.3.0.** CMTK0003's dropped pending assertion: an expression statement that drops, +without `await`, the task an assertion of `CodoMetis.TypeKit.Testing` returns on a `Task` or `ValueTask`. +The rule knows the assertion by the package's own assembly (`TypeKitSymbols.IsTestingAssembly`), so a +consumer's extension of the same name on a pending result, which may have built on 2.2.0, stays silent +(`IgnoredOutcomeAnalyzerTests.A_task_of_another_assembly_s_extension_on_a_pending_value_is_silent`). No +preview of 2.3.0 was published (nuget.org lists 2.2.0 as the latest of both packages on 2026-10-29), so +no build on these assertions exists before the tag. + +**It covers #152's forms.** The same assertions passed through `ConfigureAwait` or `WaitAsync` of a plain +`Task`, and reached through a branch of a `?:`, a `??` or a `switch` expression, are written only with +the same new API, so their reach widens before the tag under this decision. + +**The forms freeze at the tag.** Once 2.3.0 ships, its API is no longer new: a further form of these +assertions, or of any other 2.3.0 API, that CMTK0003 learns afterwards waits for a major version, as +decision 34 says. A Warning or an Info may still learn more forms in any minor version. + +**One exception in 2.3.0, taken by the maintainer.** #159: CMTK0003 also reports a `Task.WhenAny` of tasks +of results dropped without `await`, converted to a plain `Task` or waited for with `Wait`, which code built +on 2.2.0 can contain. The maintainer took it into 2.3.0 on 2026-10-29, before the tag: such a statement +loses every result in any build, so the report finds a defect, never correct code. A `WhenAny` of tasks the +caller holds completes with one of them and stays silent. The Analyzers README names the exception beside +the rule. + +**Decision 34 stands otherwise.** A new rule ships at Warning or Info in a minor version, and an Error +that reports more of the code a consumer could already write waits for a major. The Analyzers README's +"What a new version may change" states both, and claim `T01-an-error-learns-only-new-api-forms` in +`test/oracles/claims.json` quotes the exception. diff --git a/design/adr/0119-a-gap-found-before-the-tag-ships-in-that-release.md b/design/adr/0119-a-gap-found-before-the-tag-ships-in-that-release.md new file mode 100644 index 00000000..b48c8fd0 --- /dev/null +++ b/design/adr/0119-a-gap-found-before-the-tag-ships-in-that-release.md @@ -0,0 +1,40 @@ +# 0119. A gap an Error has, found before the tag, ships in that release + +Date: 2026-10-29 +Status: accepted + +Context: 2026-10-29, the sweep of CMTK0003 before 2.3.0 was tagged (#152, #157, #159 to #162); refines +[decision 34](0034-errors-fixed-within-major-version-warnings.md) and +[decision 118](0118-an-error-may-learn-a-form-only-new-api-can-write.md). Decision 118 lets a minor version's +Error report forms that only that version's new API can write, and names one exception the maintainer took +into 2.3.0: a dropped `Task.WhenAny` of tasks of results (#159). The sweep that followed found three more +forms of the same kind, each written with APIs 2.2.0 had. The maintainer decided, in these words: "decision +0119 widens 0118 to 'a gap found before the tag ships in that release'". + +## Decision + +**A form an Error misses, found before its version is tagged, is reported in that version**, when the form +loses what the rule protects in every build that contains it. A build that worked on the previous minor +version can then fail on update. The failure names a defect, never correct code, and the changelog's +Changed section says which forms and what to write instead. Decision 34 keeps an Error fixed within a major +so that an update never fails correct code; it does not keep a defect found before the tag unreported for a +major version. + +**What 2.3.0 reports that 2.2.0 code can contain**, CMTK0003 on tasks of results: + +- a `Task.WhenAny` of tasks the statement made, dropped, awaited, waited for or converted to a plain `Task` + (#159): `Tasks.PendingOutcome` looked one task deep, and the `Task>` hid the result; +- `Wait()` on a `?:`, `??` or `switch` expression, at each branch's call (#160): `AnalyzeWait` read only the + statement's own call, where every other path goes through `Producers`; +- an array of them written in a `Task.WaitAll` or `Task.WaitAny` call (#161), which converts whole, so no + element's call was converted on its own; +- a dropped `Task.WhenEach` of them (#162), which yields the tasks it was given. + +**It relaxes too.** A `Task.WhenAny` of tasks the caller holds completes with one of them and loses nothing. +2.2.0 reported it awaited; 2.3.0 is silent on it. + +**The rule stops at the tag.** Once a version is tagged, decision 34 holds for it: an Error that reports +more code its major version could already write waits for the next major, 3.0.0 for 2.x. Decision 118 +stands for forms only a minor version's new API writes, and its one exception is this decision's first +case. The Analyzers README's "What a new version may change" lists each form a minor version reports that +the previous one's code can contain. diff --git a/design/adr/0120-a-net11-0-target-and-documented-floor-raises-join-a-minor.md b/design/adr/0120-a-net11-0-target-and-documented-floor-raises-join-a-minor.md new file mode 100644 index 00000000..e96d7ea2 --- /dev/null +++ b/design/adr/0120-a-net11-0-target-and-documented-floor-raises-join-a-minor.md @@ -0,0 +1,203 @@ +# 0120. A `net11.0` target and a documented dependency-floor raise join a 2.x minor, and so may a C# 15 union; 3.0.0 stays the change of an existing contract + +Date: 2026-10-29 +Status: accepted + +Context: 2026-10-29, issue #169; refines [decision 44](0044-2-0-0-net10-0-metalama.md). Decision 44 says a +`net11.0` target is additive and joins a 2.x minor when a feature needs it, and that unions and the `net10.0` +drop are 3.0.0, timed to .NET 12 LTS. The maintainer asked what making 3.0.0 the .NET 11 release would +change, and decided, in these words: "stick to the original plans regarding 3.0.0, add details to new 2.x +minors and .NET 11 related specifics". On Metalama 2027, and on floor raises in general, the maintainer decided +the same day (see the first rule and the Metalama paragraph). .NET 11 is a release candidate (SDK 11.0.100-rc.1 measured here, RC2 +named in [KB-046](../knowledge/KB-046-csharp-15-unions-ship-with-the-net-11-sdk.md)), with general +availability expected in November 2026. + +## Decision + +**3.0.0 stays timed as decision 44 and the roadmap have it:** the `net10.0` drop, and `Option` and `Result` +becoming C# 15 unions if the spike allows it, at .NET 12 LTS (November 2027) or at the end of .NET 10's +support, whichever its consumers need. A 3.0.0 for .NET 11 was considered and refused. A `net11.0`-only major +strands the consumers on the .NET 10 LTS line, supported to November 2028, which is the argument that refused +a `net11.0`-only 2.0.0. And nothing .NET 11 offers needs a break: unions in `Option` and `Result` wait for the +spike the roadmap names, since the compiler-made union types have a public `object? Value` and are serialized +by System.Text.Json. The version number names the API, never the runtime. + +**What 3.0.0 holds is a change to an existing contract, never an addition.** Decision 44 puts "unions in the +public surface" in 3.0.0 as "two contracts under one version", which is true of a union that replaces or +retypes an existing member, and of `Option` or `Result` becoming unions: their identity, their serialization +and their pattern matching would differ between two versions of the same member. A union that is new, and +exists on `net11.0` only, is an addition like any other, and may join a 2.x minor (second rule below). A minor +that fixes bugs for every consumer and adds to `net11.0` alone is a plain 2.x minor. + +**A `net11.0` asset joins a 2.x minor when a feature needs it, on these rules.** No patch adds a target. The next +minor after the one in flight carries it; the number is not reserved here. + +1. **A dependency floor may rise in a minor, documented, on every target or on `net11.0` alone.** It is a + dependency update with an unchanged public API, which the Semantic Versioning FAQ calls compatible, leaving the + conflict to whoever references the dependency at a lower version. The conflict is real, and measured: an + application on `net11.0` that holds EF Core 10 (or Microsoft.AspNetCore.OpenApi 10) while the `net11.0` group + requires 11 fails restore with NU1605, an error by default (below). So a raise: + - has a reason, a feature, a fix or a supported version, and is the lowest floor that does the work; + - is a release decision named in the changelog "Changed": the dependency, the old and the new floor, the + targets it covers, who it reaches (a project that references the dependency at a lower version) and both + ways out (raise that reference, or stay on the previous minor); + - changes no public API and no generated member; a consequence beyond the restore (a compiler, a language + version, a supported IDE, a translation, a schema) is assessed on its own and documented, as Metalama's is + below; + - is shown by the smoke test: a consumer that holds the lower version gets the NU1605 the changelog + describes. The satellites may compile against EF Core 11 and OpenAPI 11 and require them in the `net11.0` + group. +2. **Its public surface is a superset of `net10.0`'s, and a shared member behaves the same on both.** A member + only `net11.0` has is available on that target alone, and the changelog says so. That includes members that + use C# 15 unions or `closed` types: the `net10.0` asset has none of them, and a `net11.0` consumer who does + not use them is untouched. One who uses them on `LangVersion` 14 gets CS9327, an error, never a different + answer (measured below). What stays out of a 2.x minor is a change to a member both targets have: its + signature, its return type, its serialization or its behaviour on one target. Two conditions on a union + member are this repository's own and are decided by the unions spike the roadmap names, not by the version: + it gives no path to the content of an `Option` or `Result` past `Match` and `TryGetValue` (a compiler-made + union has a public `object? Value`), and it is not written by System.Text.Json where `Option` and `Result` + refuse it. A project that has the Generators package, on Metalama 2026.1, can neither declare nor match a + union (measured below), so until 2.5.0 a union member reaches the consumers without the Generators package + and those that reference Metalama 2027 themselves. +3. **Generated members are the same on every target.** The generators run in the consumer's compiler whatever + the asset, and a value object's generated surface is the contract. A member that exists on one target only + would make a consumer that multi-targets see two value objects under one name; any member the generators + introduce on `net11.0` is introduced on `net10.0` too, or not at all. +4. **`LangVersion` stays 14 for `net10.0`, and is 15 for `net11.0` once its code uses a C# 15 feature**, in an + implementation or in a new member's signature. The consumer needs nothing from the compiler the assembly was + built with, except to use a union or a `closed` type, which needs C# 15 and says so (CS9327). In a project + that has the Generators package, Metalama's compiler sets the language version, not the SDK: 14 on both + targets with Metalama 2026.1, 15 on `net11.0` with 2027 (measured below). Generated code compiles under both. +5. **Each target is run, not assumed:** the tests, the Native AOT smoke run and package validation per target, + and the smoke test's consumer on `net11.0` that holds the lower satellite versions. + +**The SDK comes first.** `global.json` pins SDK 10.0.4xx, because a different Roslyn band can change what +Metalama and the analyzers emit. A `net11.0` target needs SDK 11, which changes the compiler under the `net10.0` +assets too, and a consumer on SDK 11 meets it already with the 2.x `net10.0` asset. Before any `net11.0` +target, a spike with no release attached (#170) measures on an SDK 11 compiler: the build and tests, the packed +`net10.0` assets unchanged, a consumer on SDK 11 building and weaving, and the smoke test's SDK 11 lane. The +pin moves by a record of its own, after the SDK is generally available. The pin protects the packages that +Metalama does not touch and this repository's own build: in a project woven by Metalama the SDK does not choose +the compiler either, Metalama's does, so what such a project compiles with follows the Metalama year. + +**Metalama is one version for every target of a release, and TypeKit follows the maintained Metalama year in a +minor.** The Generators package stays on Metalama 2026.1 in 2.3 and 2.4, and 2.5.0 moves both targets to 2027.0 +once it is stable. Other floors may differ between targets (first rule); this one never does: NuGet resolves one +`Metalama.Framework` per project, that version brings the engine and Metalama's compiler, and different floors +per target would give a consumer that multi-targets two compilers for its two inner builds (LAMA0054 refuses a +project whose `Metalama.Framework` is newer than its engine). The raise itself is the first rule applied to the +dependency with the widest reach: Metalama is also the compiler of the consumer's woven projects, so it carries +the extra consequences below. + +It is not new. 1.5.0, a minor, raised the floor from 2026.1.28 to 2026.1.29 (`CHANGELOG.md`, 1.5.0 "Changed"), +the same restore mechanism in a smaller step, because ADR 4 follows the only line Metalama's MIT maintenance +covers, the latest `YYYY.N`. Semantic Versioning's FAQ calls an update of a library's own dependencies with an +unchanged public API compatible, and leaves the conflicts to the dependents. The maintainer decided the move, +in these words: "every consumer that somehow needs Metalama 2026.x pinned for whatever he wants to achieve is +hitting TypeKit's version ceiling, so can continue using any TypeKit version that still allows any Metalama +2026 version; from 2.5.0 by depending my own packages on Metalama 2027 this is a clear, transparent ceiling". +Holding 2026.1 has a cost too: on SDK 11 it reports LAMA0617, its Razor analyzer needing a Roslyn newer than +the one it bundles (the review of 2026-10-29; a warning, falling back to the SDK 10 Razor compiler). + +**What a consumer sees is not only a restore error.** A project that references `Metalama.Framework` 2026.1 +directly gets NU1605, loud, with a one-line way out (raise it) or the line it already has (TypeKit 2.4.x). The +rest is quieter, and the documents say so: + +- a woven `net11.0` project moves from C# 14 to C# 15, with C# 15's own breaking changes (`union` and `closed` + become contextual keywords, `with(...)` in a collection expression binds differently); +- an IDE host Metalama 2027 does not support shows no Metalama diagnostics, code lens or generated code, and + Visual Studio 2022 gets a LAMA0602 warning at build (Metalama's platform support document); +- Metalama 2027 needs .NET SDK 10 or newer and declares 11 as its newest, so 3.0.0, or any SDK 12 build, will + need Metalama 2028; +- a consumer on `Metalama.Patterns` or `Metalama.Extensions` 2026.1 has a floor and no ceiling, so it resolves to + Framework 2027 with no restore error (inferred; the manifest format is 1 in both, so LAMA0061 should not + fire; the mix is untested). + +The move holds when: + +1. **Metalama 2027.0 is stable.** A stable package should not depend on a prerelease: NU5104 is a warning, and + an error under the warnings-as-errors this repository sets. +2. **A build with no Metalama license passes** in CI against it, as `dotnet.yml` has every job on 2026.1, and in + a clean container. The preview passed in a clean container (below); the general-availability build is + repeated, then a CI branch with the floor raised. A conventions test keeps `Metalama.Licensing`, whose + description says it fails a build without a valid license, out of the restore output. +3. **The weaving review and the smoke test run on 2027.0:** a consumer on Metalama 2027 beside the packages, + one with a direct 2026.1 reference that shows the NU1605 the changelog describes, one on `Metalama.Patterns` + 2026.1, and the design-time experience in Visual Studio, Rider and VS Code, which no command line shows. + The `[Durable]` placeholder in `ValueObjectAspect.Templates.cs` returns with the upgrade. +4. **The ceiling is written where a consumer reads it:** 2.5.0's changelog "Changed" and the Generators README + name the Metalama version and both ways out, the language-version change on `net11.0`, the minimum IDE + versions with the silent failure named, and what Metalama 2027 drops (Visual Studio 2022, .NET SDK 8 and 9). +5. **The line below the ceiling is declared:** whether 2.4.x gets patches after 2.5.0, and whether Metalama + 2026.1 is still maintained then. ADR 4 says MIT maintenance covers only the latest `YYYY.N`, while Metalama's + versioning policy page, as summarised, lists 2026.1 as a long-term release; the two are not reconciled. + +A consumer that needs Metalama 2027 before TypeKit moves does not wait: a direct reference beside 2.x weaves +(measured below, on the command line only). Metalama checks an order, not equality: compile-time code from +another version is accepted when its manifest format matches (LAMA0061; both read in the 2026.1.31 and +2027.0.4-preview sources by the research of 2026-10-29). + +**The 2.x line after 3.0.0** is decided with 3.0.0, not here. + +## Evidence + +SDK 11.0.100-rc.1.26425.128, a throwaway package `FloorLib` 1.0.0 with a `net10.0` group requiring +`Newtonsoft.Json >= 12.0.3` and a `net11.0` group requiring `>= 13.0.3`. An application that references +`Newtonsoft.Json 12.0.3` directly and `FloorLib`: + +- on `net11.0`: `NU1605`, "Detected package downgrade: Newtonsoft.Json from 13.0.3 to 12.0.3", with the path + `App -> FloorLib 1.0.0 -> Newtonsoft.Json (>= 13.0.3)`; an error under the SDK's defaults; +- on `net10.0`: restores. + +A library with `net10.0` (`LangVersion` 14) and `net11.0` assets, whose `net11.0` asset alone declares +`public union Pet(Cat, Dog)` and a method returning it, consumed by: + +- `net11.0`, C# 15 by default, with an exhaustive `switch` over the union: builds; +- `net11.0` with `LangVersion` 14, holding the union in a `var` and printing it: builds; +- `net11.0` with `LangVersion` 14, pattern matching on it: `CS9327`, "The feature 'unions' is not available in + C# 14.0. Use language version 15.0 or greater"; +- `net10.0`: builds, and sees only the shared members. + +The SDK needs no `preview` flag for any of it (RC1), as [KB-046](../knowledge/KB-046-csharp-15-unions-ship-with-the-net-11-sdk.md) +says of RC2. So a union is addable on `net11.0` in a minor. What measurement one cannot make from this repository +yet is the unions spike's: what `Option` and `Result` may offer a union without losing the guarantees above. + +**Metalama.** `net11.0` console projects on SDK 11.0.100-rc.1 with the 2.2.0 packages (Metalama 2026.1), a +value object declared and used: + +- no union anywhere: builds and weaves; +- a `union` declared in the project: `CS0106`, `CS8803` and `CS1001`, since Metalama's compiler runs the project + at language version 14, where unions are still a preview feature (the same declaration compiles with no + Metalama); +- a union returned by a `net11.0`-only library: holding it builds; matching on it: `CS8652`, "unions are in + preview", where the same match compiles without Metalama. + +The same project with a direct `Metalama.Framework 2027.0.4-preview` reference beside the 2.2.0 packages +weaves the value object, and a declared union with a `switch` over it builds and runs. `#error version` in the same +projects names the compiler: Metalama's `2026.1.20` at language version 14.0 on `net10.0` and on `net11.0`, and +`2027.0.4-preview` at 15.0 on `net11.0`, whatever the SDK. One limit on my runs: this machine has a Metalama +license entry for 2027.0 in its licensing file and compiler servers were already running, so they say nothing +about a build without a license. CI on 2026.1 does (`dotnet.yml`, every job runs with no Metalama license on +the machine), and so did the review's run in a clean `mcr.microsoft.com/dotnet/sdk:10.0` container with +`UseSharedCompilation=false` and warnings as errors: a `net10.0` application with the 2.2.0 packages and +`Metalama.Framework 2027.0.4-preview` restored, wove and ran (`From`, `Parse`, JSON), and created no licensing +file; the same with 2026.1.30. Not established: the general-availability build, the IDEs, and +`Metalama.Patterns` beside it. + +Adding a target is not additive for a consumer on a lower version of a dependency, though. The roadmap's "EF Core +11 and Microsoft.AspNetCore.OpenApi 11 in the satellites" is that case. It is allowed and documented under the +first rule, and the test lane of that rule shows it. + +## Consequences + +- The roadmap's `net11.0` item names these rules, and its 3.0.0 item is unchanged except that it says what a + union may do in a 2.x minor. Decision 44's "unions in the public surface are 3.0.0" is narrowed here to a + union that changes an existing member; the maintainer's reading of Microsoft's compatibility guidance + (a newer target may add to an older one's surface), checked against the measurement above. +- Decision 44 stands; its index row says it is refined here. +- Metalama 2026.1 and the 2.2.0 packages build and weave a `net11.0` project on SDK 11 RC1 (measured above). + What #170 still owns is the rest of the SDK 11 spike, the general-availability SDKs and `rc.3` packages, the + analyzers on that compiler, the NU1605 measurement repeated there, and the license-free build on Metalama 2027. +- The roadmap's Metalama 2027.0 item becomes 2.5.0 (#173), on the conditions above. Its old test, "a minor if a + consumer's own aspects still build and weave", missed the restore error of a direct lower reference, the + language-version change and the IDE hosts. diff --git a/design/adr/README.md b/design/adr/README.md index b4692bc5..245695ff 100644 --- a/design/adr/README.md +++ b/design/adr/README.md @@ -36,11 +36,11 @@ per area ([design](../plan.md)) link to the records they rest on. | [0027](0027-no-refusal-quotes-input.md) | No refusal quotes the input | 2026-09-28 | accepted | | [0028](0028-seams-declared-everything-else-refused.md) | Seams are declared, everything else refused | 2026-09-28 | accepted | | [0029](0029-only-result-terror-converts-bool.md) | Only `Result` converts to `bool` | 2026-09-28 | accepted | -| [0030](0030-asynchronous-callbacks-return-task.md) | Asynchronous callbacks return `Task` | 2026-09-28 | accepted | +| [0030](0030-asynchronous-callbacks-return-task.md) | Asynchronous callbacks return `Task` | 2026-09-28 | accepted; `CatchAsync` takes a `ValueTask` by [0110](0110-catchasync-takes-a-valuetask-at-a-lower-priority.md) | | [0031](0031-no-extension-object-unconstrained-t.md) | No extension on `object` or on an unconstrained `T` | 2026-09-28 | accepted | | [0032](0032-value-object-holds-wrapped-value-alone.md) | A value object holds its wrapped value alone, and its equality is that value's | 2026-09-28 | accepted | | [0033](0033-analyzers-run-code-metalama-transformed.md) | The analyzers run on the code Metalama transformed | 2026-09-28 | accepted | -| [0034](0034-errors-fixed-within-major-version-warnings.md) | Errors are fixed within a major version; Warnings may learn | 2026-09-28 | accepted | +| [0034](0034-errors-fixed-within-major-version-warnings.md) | Errors are fixed within a major version; Warnings may learn | 2026-09-28 | accepted; refined by [0118](0118-an-error-may-learn-a-form-only-new-api-can-write.md) | | [0035](0035-sqlite-s-autoincrement-application-s-line.md) | SQLite's autoincrement is the application's line, not the convention's | 2026-09-28 | accepted | | [0036](0036-nullable-value-object-admits-null-openapi.md) | A nullable value object admits null in the OpenAPI document wherever it appears | 2026-09-28 | accepted | | [0037](0037-refusal-s-message-landing-page.md) | A refusal's message is its landing page | 2026-09-28 | accepted | @@ -50,7 +50,7 @@ per area ([design](../plan.md)) link to the records they rest on. | [0041](0041-sensitive-value-object-declares-tostring-serilog.md) | A sensitive value object is one that declares `ToString()`; Serilog gets a satellite | 2026-10-11 | accepted | | [0042](0042-2-0-0-next-release-bundles.md) | 2.0.0 is the next release, and it bundles the 1.x candidates | 2026-10-12 | accepted | | [0043](0043-exceptions-type-argument-constraints-constructor-guarantee.md) | Exceptions, type-argument constraints, a constructor guarantee and the text guards join 2.0.0 | 2026-10-13 | accepted | -| [0044](0044-2-0-0-net10-0-metalama.md) | 2.0.0 is `net10.0` on Metalama 2026.1; the version number names the API, never the runtime | 2026-10-13 | accepted | +| [0044](0044-2-0-0-net10-0-metalama.md) | 2.0.0 is `net10.0` on Metalama 2026.1; the version number names the API, never the runtime | 2026-10-13 | accepted; refined by [0120](0120-a-net11-0-target-and-documented-floor-raises-join-a-minor.md) | | [0045](0045-topic-pages-docs-compiled-like-readmes.md) | Topic pages in `docs/`, compiled like the READMEs; the plan in `design/` | 2026-10-13 | accepted | | [0046](0046-timeout-caught-result-catch-error.md) | A timeout caught by `Result.Catch` is the error | 2026-10-13 | accepted | | [0047](0047-firstornone-query-runs-query.md) | `FirstOrNone` on a query runs in the query | 2026-10-13 | accepted | @@ -69,8 +69,8 @@ per area ([design](../plan.md)) link to the records they rest on. | [0060](0060-value-object-over-closed-set-generated-in-full.md) | A value object over a closed set is generated in full wherever the set is declared | 2026-10-13 | accepted | | [0061](0061-scope-s-fault-status-applies-where-fault-type-answered.md) | A scope's fault status applies where its fault type is answered | 2026-10-13 | accepted | | [0062](0062-asynchronous-callbacks-run-where-awaited-task-completed.md) | An asynchronous callback runs where the task it waited for completed | 2026-10-13 | accepted | -| [0063](0063-char-keys-application-assigned-ulong-documented.md) | A key over `char` is the application's to assign; a default key over `ulong` throws at `Add` | 2026-10-13 | accepted; to 2.3 by [0097](0097-2-2-0-is-a-consolidation-release-and-the-gate-binds.md) | -| [0064](0064-fault-name-reaches-client-causes-not-told-apart-one-fault.md) | A fault's name reaches the client; causes it must not tell apart are one fault | 2026-10-13 | accepted; its opt-in moved to 2.2 by [0088](0088-2-1-0-ships-without-its-open-roadmap-items.md); to 2.3 by [0097](0097-2-2-0-is-a-consolidation-release-and-the-gate-binds.md) | +| [0063](0063-char-keys-application-assigned-ulong-documented.md) | A key over `char` is the application's to assign; a default key over `ulong` throws at `Add` | 2026-10-13 | accepted; to 2.3 by [0097](0097-2-2-0-is-a-consolidation-release-and-the-gate-binds.md); its `ulong` half superseded by [0114](0114-a-value-object-s-temporary-number-is-its-wrapped-type-s.md) | +| [0064](0064-fault-name-reaches-client-causes-not-told-apart-one-fault.md) | A fault's name reaches the client; causes it must not tell apart are one fault | 2026-10-13 | accepted; its opt-in moved to 2.2 by [0088](0088-2-1-0-ships-without-its-open-roadmap-items.md); to 2.3 by [0097](0097-2-2-0-is-a-consolidation-release-and-the-gate-binds.md); the opt-in is [0111](0111-a-mapping-can-withhold-the-fault-name.md) | | [0065](0065-2-0-0-ships-without-preview.md) | 2.0.0 ships without a preview | 2026-10-14 | accepted | | [0066](0066-precondition-failed-malformed-fault-kinds.md) | Two more fault kinds: `PreconditionFailed` (412) and `Malformed` (400) | 2026-10-14 | accepted | | [0067](0067-rule-that-refused-on-fault.md) | A fault names the rule that refused, as a member of a fixed set | 2026-10-14 | accepted | @@ -102,16 +102,28 @@ per area ([design](../plan.md)) link to the records they rest on. | [0093](0093-verified-facts-are-pinned-dated-and-executable-where-they-can-be.md) | Verified facts are pinned, dated and executable where they can be: one fact per file in `design/knowledge/`, a characterization test per observable fact, and `docs-query check` holding the pins to what is installed | 2026-10-15 | accepted; a cited fact is executable and an answer is a fixture beside the test by [0107](0107-a-cited-fact-is-executable-and-seeds-run-by-footprint.md) | | [0094](0094-topic-branches-into-a-release-branch-and-fix-trailers.md) | From 2.2.0, topic branches merge into a release branch with merge commits only, and every `fix:` commit carries `Defect-Class:` and `Found-By:` trailers, held by a test | 2026-10-15 | accepted | | [0095](0095-a-fix-is-tested-across-its-input-class.md) | A fix is tested across its input class: the fixer names the class, runs the fix's test over that class's variant catalogue (`TypeArgumentSites` for a type argument, the `shape.*` axes, the settings axes) and fixes the siblings it finds; `Found-By: VA` | 2026-10-15 | accepted | -| [0096](0096-ci-replays-every-probe-and-the-release-runs-the-gate.md) | CI replays every probe, from an empty state, on every push and pull request, and a failing probe fails the run; the release replays them and runs the gate before it packs, the gate advisory; held by `ReleaseWiringTests` | 2026-10-15 | accepted; the gate binds from 2.2.0 by [0097](0097-2-2-0-is-a-consolidation-release-and-the-gate-binds.md); the build workflow's replay runs by family and footprint by [0101](0101-ci-replays-by-family-and-footprint.md) | -| [0097](0097-2-2-0-is-a-consolidation-release-and-the-gate-binds.md) | 2.2.0 is a consolidation release: no public API, its additive items move to 2.3.0, and the probe gate binds from it | 2026-10-15 | accepted | +| [0096](0096-ci-replays-every-probe-and-the-release-runs-the-gate.md) | CI replays every probe, from an empty state, on every push and pull request, and a failing probe fails the run; the release replays them and runs the gate before it packs, the gate advisory; held by `ReleaseWiringTests` | 2026-10-15 | superseded by [0112](0112-the-prober-gives-way-to-valueranges-oracle-kit.md) | +| [0097](0097-2-2-0-is-a-consolidation-release-and-the-gate-binds.md) | 2.2.0 is a consolidation release: no public API, its additive items move to 2.3.0, and the probe gate binds from it | 2026-10-15 | accepted; the gate withdrawn by [0112](0112-the-prober-gives-way-to-valueranges-oracle-kit.md) | | [0098](0098-a-null-passes-no-comparison-in-a-query.md) | A nullable record-class value object compared with `<`, `<=`, `>` or `>=` in a query answers as SQL does, leaving out its null rows; documented, not refused | 2026-10-15 | accepted | -| [0099](0099-a-probe-row-says-how-it-ended.md) | A probe row says how it ended: notes are a closed catalogue of keys and outcomes, a row that earned nothing is a skipped test labelled with its settings, a skip for a finding fails the replay once the finding is fixed and the gate while it stands, and a refused document is judged against ASP.NET alone | 2026-10-15 | accepted; note keys are a typed catalogue the compiler checks by [0103](0103-a-harness-error-is-never-a-catch-and-note-keys-are-constants.md); a refused document is compared by type and stage, and earns nothing in a check that compared nothing else, by [0108](0108-a-probe-check-earns-credit-against-a-baseline-and-ws2-closes.md) | -| [0100](0100-every-probe-is-shown-to-fail-against-a-seeded-defect.md) | Every registered probe names a seeded product defect, and `prober seeds` (local, a build per seed) requires a violation it causes; the prober tests hold that every seed exists and applies; pairwise coverage is a floor, three-way combinations belong to the variant catalogue | 2026-10-15 | accepted; a seed per obligation of a claim by [0104](0104-a-claim-states-its-obligations-and-each-has-a-seed.md); seeds run by footprint, a release `--all`, by [0107](0107-a-cited-fact-is-executable-and-seeds-run-by-footprint.md) | -| [0101](0101-ci-replays-by-family-and-footprint.md) | The build workflow replays the probes in legs by family, each running its probes in parallel, under a time limit: a push every probe with coverage and the merged state saved, a pull request only the probes it stales against its base branch's saved state and without a footprint; `prober merge` joins the legs and refuses a probe no leg saw | 2026-10-16 | accepted | -| [0102](0102-an-exclusion-carries-its-reason.md) | Each `exclude` entry of claims.json is a required one-sentence reason and its pairs; the prober refuses a claims file its schema does not match, naming the claim and the field, so an invented reason field is refused; `prober universe` prints each exclusion's reason beside the claim's narrowings; the prober writes JSON with only JSON's own escapes, so an unchanged publish leaves a tracked file as it is | 2026-10-16 | accepted | -| [0103](0103-a-harness-error-is-never-a-catch-and-note-keys-are-constants.md) | A harness error is never a catch: `Rec.Run` ends a row whose check throws as a harness error, not a violation, and `prober seeds` credits only a violation no harness error raised; note keys are members of a typed `NoteKeys` the compiler checks, and `register`, `publish` and a conventions test refuse a note or a skip whose key or finding is not written out | 2026-10-16 | accepted | -| [0104](0104-a-claim-states-its-obligations-and-each-has-a-seed.md) | A claim lists its `obligations`, each a sentence quoted verbatim from its source, and what it does not promise in `not-promised`, each quoted from the document or record that makes it intended; a claim without the field has one obligation, `claim`; a probe carries a seed per obligation it checks, and the check without a build refuses a seed naming an unlisted obligation and a listed obligation no seed names; `status` shows each claim's obligations, and `prober gaps` reports what the corpus leaves out, deciding nothing | 2026-10-16 | accepted; `gaps` reads the last replay's evidence too by [0108](0108-a-probe-check-earns-credit-against-a-baseline-and-ws2-closes.md) | +| [0099](0099-a-probe-row-says-how-it-ended.md) | A probe row says how it ended: notes are a closed catalogue of keys and outcomes, a row that earned nothing is a skipped test labelled with its settings, a skip for a finding fails the replay once the finding is fixed and the gate while it stands, and a refused document is judged against ASP.NET alone | 2026-10-15 | superseded by [0112](0112-the-prober-gives-way-to-valueranges-oracle-kit.md) | +| [0100](0100-every-probe-is-shown-to-fail-against-a-seeded-defect.md) | Every registered probe names a seeded product defect, and `prober seeds` (local, a build per seed) requires a violation it causes; the prober tests hold that every seed exists and applies; pairwise coverage is a floor, three-way combinations belong to the variant catalogue | 2026-10-15 | superseded by [0112](0112-the-prober-gives-way-to-valueranges-oracle-kit.md) | +| [0101](0101-ci-replays-by-family-and-footprint.md) | The build workflow replays the probes in legs by family, each running its probes in parallel, under a time limit: a push every probe with coverage and the merged state saved, a pull request only the probes it stales against its base branch's saved state and without a footprint; `prober merge` joins the legs and refuses a probe no leg saw | 2026-10-16 | superseded by [0112](0112-the-prober-gives-way-to-valueranges-oracle-kit.md) | +| [0102](0102-an-exclusion-carries-its-reason.md) | Each `exclude` entry of claims.json is a required one-sentence reason and its pairs; the prober refuses a claims file its schema does not match, naming the claim and the field, so an invented reason field is refused; `prober universe` prints each exclusion's reason beside the claim's narrowings; the prober writes JSON with only JSON's own escapes, so an unchanged publish leaves a tracked file as it is | 2026-10-16 | superseded by [0112](0112-the-prober-gives-way-to-valueranges-oracle-kit.md) | +| [0103](0103-a-harness-error-is-never-a-catch-and-note-keys-are-constants.md) | A harness error is never a catch: `Rec.Run` ends a row whose check throws as a harness error, not a violation, and `prober seeds` credits only a violation no harness error raised; note keys are members of a typed `NoteKeys` the compiler checks, and `register`, `publish` and a conventions test refuse a note or a skip whose key or finding is not written out | 2026-10-16 | superseded by [0112](0112-the-prober-gives-way-to-valueranges-oracle-kit.md) | +| [0104](0104-a-claim-states-its-obligations-and-each-has-a-seed.md) | A claim lists its `obligations`, each a sentence quoted verbatim from its source, and what it does not promise in `not-promised`, each quoted from the document or record that makes it intended; a claim without the field has one obligation, `claim`; a probe carries a seed per obligation it checks, and the check without a build refuses a seed naming an unlisted obligation and a listed obligation no seed names; `status` shows each claim's obligations, and `prober gaps` reports what the corpus leaves out, deciding nothing | 2026-10-16 | superseded by [0112](0112-the-prober-gives-way-to-valueranges-oracle-kit.md) | | [0105](0105-every-tool-is-built-in-the-session-s-checkout.md) | SessionStart builds the guard in `$CLAUDE_PROJECT_DIR` and every other tool, through `tools/AgentTools.slnf`, in the session's own checkout; a hook, the prober, the smoke test and the verdict fall back to the SDK installer's `dotnet` when the one on PATH lacks the pinned SDK, and a session start that builds nothing tells the agent; the guard lets `dotnet msbuild -getProperty/-getItem` without a target through | 2026-10-17 | accepted | | [0106](0106-a-parameter-of-a-value-object-over-a-described-type-is-inlined.md) | A value object over a type the JSON options describe is written inline as a route, query, header or form parameter and as an element of a query or header array, where value objects are components: the component is its JSON positions'; every other value object keeps its reference; the schema below a parameter is asked for with the parameter's source and type alone | 2026-10-17 | accepted | -| [0107](0107-a-cited-fact-is-executable-and-seeds-run-by-footprint.md) | A fact that prunes or excuses a row is executable: `docs-query check` fails `fact-not-executable-cited` where a refused entry, a not-promised entry, an obligation's declaration or the harness cites a fact without its test; a fact's measured answer is `KBNNN.fixture.json` beside its test, named by `fixture:`, read by a BCL-only reader the harness will use; `prober seeds` runs only the probes whose seeds identity or product footprint changed since every seed was caught, and a release runs `--all` | 2026-10-17 | accepted | -| [0108](0108-a-probe-check-earns-credit-against-a-baseline-and-ws2-closes.md) | A probe row runs a check per obligation, each against a baseline, and the ledger credits checks; an excuse is a closed citation, applicability is declared on the obligation, the http, json and text harness compares three sides by exception type and stage, decision 99's refused document among them, and the twins and the context are generated with the shapes; `NoteKeys` keeps its pass keys and `framework-refuses`; `prober fix`, a covering array that places no blocked pair, and `gaps` listing the pairs no passing check read both axes of; WS2 closes, and WS3 to WS6 are dropped | 2026-10-17 | accepted; the gate passed on release/2.2.0 on 2026-10-18 (#62, with the fix #63 for the finding F4 the regenerated C21 rows raised) | +| [0107](0107-a-cited-fact-is-executable-and-seeds-run-by-footprint.md) | A fact that prunes or excuses a row is executable: `docs-query check` fails `fact-not-executable-cited` where a refused entry, a not-promised entry, an obligation's declaration or the harness cites a fact without its test; a fact's measured answer is `KBNNN.fixture.json` beside its test, named by `fixture:`, read by a BCL-only reader the harness will use; `prober seeds` runs only the probes whose seeds identity or product footprint changed since every seed was caught, and a release runs `--all` | 2026-10-17 | accepted in part: its fixture rule stands; the rest superseded by [0112](0112-the-prober-gives-way-to-valueranges-oracle-kit.md) | +| [0108](0108-a-probe-check-earns-credit-against-a-baseline-and-ws2-closes.md) | A probe row runs a check per obligation, each against a baseline, and the ledger credits checks; an excuse is a closed citation, applicability is declared on the obligation, the http, json and text harness compares three sides by exception type and stage, decision 99's refused document among them, and the twins and the context are generated with the shapes; `NoteKeys` keeps its pass keys and `framework-refuses`; `prober fix`, a covering array that places no blocked pair, and `gaps` listing the pairs no passing check read both axes of; WS2 closes, and WS3 to WS6 are dropped | 2026-10-17 | superseded by [0112](0112-the-prober-gives-way-to-valueranges-oracle-kit.md) | +| [0109](0109-work-is-tracked-as-labelled-issues-and-ci-checks-each-pull-request.md) | Work is tracked as issues through the forms in `.github/ISSUE_TEMPLATE/`; a release is an `epic` with a milestone of its version and its work as sub-issues, beside the roadmap; issues and pull requests carry ValueRanges' labels (one type, a severity on a bug, `package/*`, `area/*`, `silent`, `breaking`, `found-by/prober`, `stacked`); a pull request opens with `Closes #n` and carries its issue's labels and milestone; `pr-hygiene.yml` runs `tools/PrHygiene` on the payload and fails a missing type, severity or closing reference, a branch whose `/` disagrees with its type label, a pull request into `release/` without that milestone, and a `breaking` one into anything but a major | 2026-10-18 | accepted; its required build checks replaced by `CI` in [0113](0113-ci-runs-the-lanes-a-change-can-affect.md) | +| [0110](0110-catchasync-takes-a-valuetask-at-a-lower-priority.md) | `CatchAsync` has `ValueTask` forms marked `[OverloadResolutionPriority(-1)]`, so an `async` lambda binds to the `Task` form as before (measured); `Catch` refuses a `ValueTask` at run time still; `TraverseAsync` on `Option` and `Patch` awaits the one operation, and calls nothing and checks no token without a value | 2026-10-28 | accepted | +| [0111](0111-a-mapping-can-withhold-the-fault-name.md) | A mapping withholds a member's name with `FaultResponse.WithoutFaultName()`: its problem has no `fault`, no `rule` and no member description as its detail; `IFaultFeature.NameWithheld` tells the host; a response whose every fault withholds its name is described as naming none; the same-name warning counts only members that write their name | 2026-10-28 | accepted | +| [0112](0112-the-prober-gives-way-to-valueranges-oracle-kit.md) | The prober, its corpus, its seeds, the CI replay and the release gate are retired for CodoMetis.ValueRanges' oracle kit under `test/oracles/`: claims quoted verbatim and held by named tests, every public member claimed or waived, oracles as ordinary test classes each with a seed CI checks applies, candidates, and a hand-written axes file; `tools/Oracles seeds` builds one project and runs one class per seed | 2026-10-28 | accepted | +| [0113](0113-ci-runs-the-lanes-a-change-can-affect.md) | CI runs the lanes a change can affect: each changed path selects Linux, Windows and the pack by its class in `tools/CiLanes` (Windows for the analyzers, the generators, tests and tools; the pack for the packages), a path no class names selects all three, a prose-only change runs only the test projects that read prose (`test/Prose.slnf`), and only a pull request into `main`, a push to `main`, the nightly and the release run macOS; a gate job, `CI`, over every job is the required check; a convention test refuses non-portable file names; `release.yml` calls the build workflow before it verifies | 2026-10-29 | accepted | +| [0114](0114-a-value-object-s-temporary-number-is-its-wrapped-type-s.md) | A value object over a number gets the temporary values a property of its wrapped type gets, from a factory in its converter's mapping hints, which EF consults after a generator on the property and after HiLo: a default `ulong` key no longer throws at `Add`, and a `char` key configured on add gets a temporary `char` on SQL Server; under `--nativeaot` the factory is lost and a default `ulong` key still throws | 2026-10-29 | accepted | +| [0115](0115-value-objects-can-be-inlined-everywhere.md) | `AddTypeKit(typeKit => typeKit.InlineValueObjects = true)` writes every value object in place, by its wrapped type's schema, in every position and version, with no component of its own; off by default, the document unchanged; closed sets and wrapped enums keep their components; an inlined value object claims no name (decision 55) and shares no schema between its JSON positions and its parameters (decision 106) | 2026-10-29 | accepted | +| [0116](0116-the-docs-site-is-docfx-built-from-main.md) | The docs site is DocFX, published to GitHub Pages from `main` alone: the guides, the READMEs, the changelog and the API pages of the run-time packages, from the files as they are; a link to a file that is not a page becomes a GitHub address at the built commit; a pull request touching a page builds it with warnings as errors; `site/**` is prose; DocFX 2.81.0 lists the members of C# 14 extension blocks as extension methods | 2026-10-29 | accepted | +| [0117](0117-a-seed-is-caught-against-a-passing-baseline.md) | `oracles seeds` credits a seed only when its oracle passes without it, in a baseline run with the same build and filter, and fails with it in a test of its own; a run that never reached the tests (a fixture xunit could not set up, an error counted outside any test, a crashed host, no TRX, every test skipped) or a failing baseline is `ERROR`, exit 4, never `CAUGHT` | 2026-10-29 | accepted | +| [0118](0118-an-error-may-learn-a-form-only-new-api-can-write.md) | An Error may report, in a minor version, a form that only an API new in that version can write, since no build that worked before contains it: CMTK0003's dropped assertion of `CodoMetis.TypeKit.Testing` on a `Task` or `ValueTask` in 2.3.0, its `ConfigureAwait`, `WaitAsync` and branch forms (#152) included, known by the package's assembly; the forms freeze at the tag; refines decision 34; one exception the maintainer took into 2.3.0, a dropped `Task.WhenAny` of tasks of results (#159) | 2026-10-29 | accepted; widened by [0119](0119-a-gap-found-before-the-tag-ships-in-that-release.md) | +| [0119](0119-a-gap-found-before-the-tag-ships-in-that-release.md) | A form an Error misses, found before its version is tagged, is reported in that version when it loses what the rule protects in every build, though code built on the previous minor can contain it: 2.3.0's CMTK0003 on a `Task.WhenAny` of results (#159), `Wait()` on a branch (#160), an array in `Task.WaitAll`/`WaitAny` (#161), a dropped `Task.WhenEach` (#162), and a `WhenAny` of held tasks silent; the rule stops at the tag, after which decision 34 holds; refines 34 and 118 | 2026-10-29 | accepted | +| [0120](0120-a-net11-0-target-and-documented-floor-raises-join-a-minor.md) | A `net11.0` target joins a 2.x minor when a feature needs it, and a dependency floor may rise in a minor, on every target or on `net11.0` alone, if it is documented (the Semantic Versioning FAQ calls a dependency update with an unchanged public API compatible; a project that references the dependency lower gets NU1605, measured on SDK 11 RC1, so the changelog names the floor, who it reaches and both ways out, and the smoke test shows it); a union new on `net11.0` is an addition and may join a 2.x minor (a `LangVersion` 14 consumer who matches on it gets CS9327, and on Metalama 2026.1 CS8652, measured); Metalama is one version for every target and TypeKit follows the maintained Metalama year in a minor: 2.3 and 2.4 stay on 2026.1 and 2.5.0 moves both targets to 2027.0 once stable (a direct 2026.1 reference gets NU1605 and stays on 2.4.x; a woven `net11.0` project goes from C# 14 to 15; conditions: a license-free CI and clean-container build, the ceiling and the 2.4.x line documented); the SDK 11 spike comes first (#170); 3.0.0 stays the `net10.0` drop and a change of an existing contract, timed to .NET 12 LTS, and .NET 11 is no reason for a major; refines decision 44 | 2026-10-29 | accepted | diff --git a/design/analyzers.md b/design/analyzers.md index e5a61fce..a1054091 100644 --- a/design/analyzers.md +++ b/design/analyzers.md @@ -33,18 +33,19 @@ exist. |---|---|---|---| | CMTK0001 | No `default`/`default(T)`/`new()`/`new T()` of a value object or a `[RequireCustomInitialization]` type. An operand of `==`/`!=`, or an argument of a call whose name has the word `Equal`, `Equals` or `Compare` or ends in `Be` (`ThrowIfEqual`, `Assert.NotEqual`, `ShouldNotBe`), is exempt: a guard check is the only defence against the defaults the rule cannot see. So is a `default` assigned to an `out` parameter of a method returning `bool`, the Try pattern. Where a sibling branch does not bind (same-project value objects), the target type is taken from the enclosing conditional, switch arm or collection | Error | shipped in 1.0.0 | | CMTK0002 | Type implements `IValue<>`/`IValidatedValue<,,>`, but the compilation does not reference `CodoMetis.TypeKit.Generators`, so it is never woven | Error | shipped in 1.0.0 | -| CMTK0003 | `Result`/`Option` a call returns, dropped by an expression statement, awaited or not, or a `Task` of one converted to a plain `Task` (`Task Cancel() => CancelAsync();`); `_ =` opts out. `Tap`, `TapAsync`, `TapError` and `TapErrorAsync` on a stored result or task are exempt, since they return their receiver, and so is a chain of them on one (`r.Tap(log).TapError(warn)`, `(await pending).Tap(log)`, `pending.WaitAsync(t).TapAsync(log)`), measured as an Error's false report before 2.0.0 was tagged; so is such a tap converted to a plain `Task` or waited for (`Task Log() => pending.TapAsync(log);`, `held.TapAsync(log).Wait()`), on a `Result` and on a `Task` of one, which an independent review found reported on 2026-10-14. Also a collection of results the statement made (an array, `ICollection` or `IReadOnlyCollection`, such as `await Task.WhenAll(ids.Select(orders.CancelAsync))`), and a method group of a `Task` method converted to a delegate returning `Task`; tasks held in variables are silent, and so is a lazy `IEnumerable`, where nothing has run, unless a call that was given no result returned it and the statement materialises it. Also `Wait` on a task the expression made, tasks of results the statement made converted to plain ones (`Task.WaitAll`, `WaitAny`), and a `foreach` or `await foreach` over results its header made whose body never reads the iteration variable; a variable named `_` opts out. CA1806 can only enforce this per method name, not per return type | Error | shipped in 1.0.0 at Warning; Error in 2.0.0 ([decision 50](adr/0050-warnings-cmtk0003-alone-becomes-error-2.md)), its 1.x gaps closed first | +| CMTK0003 | `Result`/`Option` a call returns, dropped by an expression statement, awaited or not, or a `Task` of one converted to a plain `Task` (`Task Cancel() => CancelAsync();`); `_ =` opts out. `Tap`, `TapAsync`, `TapError` and `TapErrorAsync` on a stored result or task are exempt, since they return their receiver, and so is a chain of them on one (`r.Tap(log).TapError(warn)`, `(await pending).Tap(log)`, `pending.WaitAsync(t).TapAsync(log)`), measured as an Error's false report before 2.0.0 was tagged; so is such a tap converted to a plain `Task` or waited for (`Task Log() => pending.TapAsync(log);`, `held.TapAsync(log).Wait()`), on a `Result` and on a `Task` of one, which an independent review found reported on 2026-10-14. Also a collection of results the statement made (an array, `ICollection` or `IReadOnlyCollection`, such as `await Task.WhenAll(ids.Select(orders.CancelAsync))`), and a method group of a `Task` method converted to a delegate returning `Task`; tasks held in variables are silent, and so is a lazy `IEnumerable`, where nothing has run, unless a call that was given no result returned it and the statement materialises it. Also `Wait` on a task the expression made, tasks of results the statement made converted to plain ones (`Task.WaitAll`, `WaitAny`), and a `foreach` or `await foreach` over results its header made whose body never reads the iteration variable; a variable named `_` opts out. From 2.3.0, an assertion of `CodoMetis.TypeKit.Testing` on a `Task` or `ValueTask` of what it checks, dropped by an expression statement without `await`: `service.FindAsync(id).ShouldBeNone();`, the lowered static call, through `?.`, the body of a lambda that returns nothing, through `ConfigureAwait` or `WaitAsync` of the `Task` or `Task` it returns (every overload), and at each branch of a `?:`, `??` or `switch` expression so passed through, judged by the path every dropped call takes (#152); handed to `Task.WhenAll`, `Task.Run` or `Task.WhenEach` whose task is dropped, or to `Task.WhenAny` or `Task.WaitAny` in any use, as arguments, an array, a collection expression, a `Select`, a query, a `ToList`, a method group or an `async` lambda that awaits it with no `catch` between that keeps its failure, and a `WhenAny` of them converted to a plain `Task` or waited for, and a loop that never reads the assertions its header made (#157); `Task.WaitAll` throws, so it is silent. A combinator inside another is judged by how deep its task holds the failure: `WhenAll` and `Run` fault with a task that faults, `WhenAny`, `WhenEach` and an `async` lambda that returns the task complete with it as a value, each `await` unwraps one, and an awaited, waited-for or converted combinator observes only a failure at depth 0, so `await Task.WhenAll(Task.WhenAny(…))` and `await Task.Run(() => Task.WhenAny(…))` report, at any depth, and `await await Task.WhenAny(…)` is silent (#165). A `catch` keeps the failure only when its `try` block throws it, its type is or is a base of `TypeKitAssertionException` (resolved in the Testing assembly), and its handler can complete, which a constant `false` filter leaves unreachable: `catch { throw; }`, a log and `throw;`, `throw new …`, `catch (OperationCanceledException)` and `when (false)` hand it on and report; a filter that is no constant counts as keeping it, since an Error reports only a failure lost whatever the filter answers (#166). It fails the test only through its task. Known by the package's assembly, so a consumer's extension of the same name on a pending result is silent; awaited, held, returned or discarded with `_ =`, it is silent too, and so are held tasks combined. Also from 2.3.0, on tasks of results, forms 2.2.0 code can contain: a `Task.WhenAny` of tasks the statement made, in any use (#159; of held tasks it is silent, where 2.2.0 reported it awaited), `Wait()` on a `?:`, `??` or `switch` (#160), an array written in `Task.WaitAll` or `Task.WaitAny` (#161) and a dropped `Task.WhenEach` (#162). All held by `DroppedTaskVariantTests` over `Variants/DroppedTaskShapes`, 1837 cases. CA1806 can only enforce this per method name, not per return type | Error | shipped in 1.0.0 at Warning; Error in 2.0.0 ([decision 50](adr/0050-warnings-cmtk0003-alone-becomes-error-2.md)), its 1.x gaps closed first; the dropped pending assertion in 2.3.0, a form only 2.3.0's API writes ([decision 118](adr/0118-an-error-may-learn-a-form-only-new-api-can-write.md)), its nested combinators and catches (#165, #166) included; the 2.2-writable forms #159 to #162 in 2.3.0, gaps found before the tag ([decision 119](adr/0119-a-gap-found-before-the-tag-ships-in-that-release.md)) | | CMTK0004 | `IValueObjectMaterializer<,>.Materialize`, `ValueObjectConverter<,>.Materialize` or a hand-written value object's public `Materialize`, called or referenced (expression trees included). The satellite's own call is compiled in the satellite; EF's compiled model is generated code, whose diagnostics are not reported (measured: reported, it is six errors in the smoke test's AOT consumer). `StoredJsonConverterFactory` goes through `IStoredJsonConverterSource`, not `Materialize` | Error | shipped in 1.0.0 | | CMTK0005 | `new T[n]`, `stackalloc T[n]`, `GC.AllocateUninitializedArray/AllocateArray` or `Array.Resize` of a no-default struct: every new slot starts as `default`. A constant length of zero (for `Array.Resize` and the `GC` methods too) or initial elements are silent | Warning | shipped in 1.0.0 | | CMTK0006 | Field or auto-property of a no-default struct type in a class that no initializer, `required` or constructor sets; a non-public parameterless constructor (EF, serializers) and a record's copy constructor are exempt. Measured 2026-09-28 on the repository's EF entities and request types: 12 of 12 reports on settable properties of a type with only the implicit constructor, each a real way to a default. `required`, the fix the message names where it compiles, works with EF's compiled model, precompiled queries and Native AOT (the smoke test rerun with it). A `[SetsRequiredMembers]` constructor must assign the `required` members it promises | Warning | shipped in 1.0.0 | | CMTK0007 | `FromKnownGood` given a value straight from a parameter of the enclosing method or lambda, or a member or element of one. A value the code produced itself is legitimate and silent; a parameter copied into a local first is not seen. Info, since test theories and helpers that only receive constants are reported too | Info | shipped in 1.0.0 | | CMTK0008 | The wrapped values of two different value objects compared (`==`, `!=`, ordering, `Equals`, `CompareTo`) through `.Value`, `?.Value`, `GetValue()` or `ValueOrNull()`: the typed ids unwrapped into the bug they prevent. Recognised by syntax, since `.Value` of a same-project value object does not bind where analyzers run. Also `string.Equals`, `object.Equals`, a comparer's `Equals`, `string.Compare`/`CompareOrdinal` over two such values (the forms MA0006 rewrites `==` into), and two value objects of different types through `Equals(object)`, which is always false. Also a LINQ join on two such values: a query's `join … on a.Value equals b.Value`, with or without `into`, and `Join`/`GroupJoin`/`LeftJoin`/`RightJoin` of `Enumerable`, `Queryable` and `AsyncEnumerable` whose key selectors are lambdas returning them, resolved by symbol, with the candidates where a key selector does not bind; a composite key is not looked into. A raw value on one side, or an explicit cast, is silent | Warning | shipped in 1.0.0 | -| CMTK0009 | A call that returns the `default` of a no-default struct when it finds nothing: `Enumerable`/`Queryable` `FirstOrDefault`, `LastOrDefault`, `SingleOrDefault`, `ElementAtOrDefault`, `DefaultIfEmpty` without a default value, `Nullable.GetValueOrDefault()`, `GetValueOrDefault(dictionary, key)`, `Activator.CreateInstance`, `RuntimeHelpers.GetUninitializedObject`, and `Option.OrDefault()`. The message names `FirstOrNone`, `GetValueOrNone` or `Or`. Warning, since a sequence checked for emptiness first is correct. Also `ImmutableArray`'s own, `Find`/`FindLast` of `List`, `Array` and `ImmutableList`, `ImmutableDictionary.GetValueOrDefault` and the immutable builders', and the async `FirstOrDefaultAsync`, `LastOrDefaultAsync`, `SingleOrDefaultAsync` and `ElementAtOrDefaultAsync` of `System.Linq.AsyncEnumerable` and EF Core, whose type is the method's type argument; EF Core's only where the project references it | Warning | shipped in 1.0.0 | +| CMTK0009 | A call that returns the `default` of a no-default struct when it finds nothing: `Enumerable`/`Queryable` `FirstOrDefault`, `LastOrDefault`, `SingleOrDefault`, `ElementAtOrDefault`, `DefaultIfEmpty` without a default value, `Nullable.GetValueOrDefault()`, `GetValueOrDefault(dictionary, key)`, `Activator.CreateInstance`, `RuntimeHelpers.GetUninitializedObject`, and `Option.OrDefault()`. The message names `FirstOrNone`, `GetValueOrNone` or `Or`. Warning, since a sequence checked for emptiness first is correct. Also `ImmutableArray`'s own, `Find`/`FindLast` of `List`, `Array` and `ImmutableList`, `ImmutableDictionary.GetValueOrDefault` and the immutable builders', and the async `FirstOrDefaultAsync`, `LastOrDefaultAsync`, `SingleOrDefaultAsync` and `ElementAtOrDefaultAsync` of `System.Linq.AsyncEnumerable` and EF Core, whose type is the method's type argument; EF Core's only where the project references it. From 2.3.0, with a message of its own, `FirstOrNone`, `SingleOrNone`, `LastOrNone`, `FirstOrError` and `LastOrError` on a receiver that is an `IQueryable` (an implementer or a type parameter constrained to one included) bound to the `IEnumerable` overload because the condition is a delegate: a `Func` variable, field or property, a method group, an anonymous method, a call that returns one. A receiver typed as a sequence, `AsEnumerable()` or a cast, is silent; no code fix, since the condition's rewrite is the author's (#129) | Warning | shipped in 1.0.0 | | CMTK0010 | A type argument that breaks a constraint attribute on its parameter (`[Abstract]`, `[Sealed]`, `[Record]`, `[Concrete]`; from 2.1.0 also `[Interface]`, `[Unsealed]`, `[ValueObject]`, `[ClosedSet]`, `[FlagsEnum]`, `[ReadOnlyStruct]`, [decision 81](adr/0081-six-more-kinds-of-type-argument.md), the two marker ones proved by a bound to the marker, the other four by the attribute alone; an interface is no value object, [decision 85](adr/0085-an-interface-is-no-value-object.md); a proof reads what a closed use can be, [decision 84](adr/0084-cmtk0010-proofs-read-what-a-closed-use-can-be.md)), or a pair of arguments for two parameters marked `[Distinct]` or `[Unrelated]` (2.1.0, [decision 79](adr/0079-distinct-and-unrelated-type-parameters.md)) that are one symbol (nullable annotations aside, `dynamic` as `object`) or related by an implicit identity, reference or boxing conversion either way, reported once at the later argument, a forwarding pair a breach unless both carry the attribute in one generic or their C# constraints admit no common (or no related) type, at any closed use: a base type, an implemented interface, a member type, a local, a `typeof`, a call with its type arguments written (at the argument) or inferred (at the call), a method group whose arguments the delegate infers (at the group). A type parameter forwarded without the same attribute is a breach too, unless its C# constraints leave it no breaking argument: `struct`/`unmanaged` or a bound proving `[Sealed]` proves `[Sealed]`; `new()` or whatever proves `[Sealed]` proves `[Concrete]`; a bound that is or proves a record proves `[Record]` (CS8865); nothing but the attribute proves `[Abstract]`. `[Concrete]` accepts an enum, a delegate and `dynamic`, as `new()` and `struct` do for an enum. A default interface method reached through a derived interface carries its own constraint once: it was its own implementation there, and the rule recursed until the compiler's stack overflowed (an independent review, 2026-10-14). Both settled 2026-10-13 before the tag, measured as false reports (an enum, a delegate, `dynamic`; six forwarding forms) with no miss among their neighbours. The open generic in its own body and an unbound `typeof` are silent. Reflection is out of reach | Error | built 2026-10-13 for 2.0.0 (phase 13), a major, so Error is within [decision 34](adr/0034-errors-fixed-within-major-version-warnings.md) | | CMTK0011 | A concrete class implementing or deriving from a type marked `[RequireParameterlessConstructor]`, directly or through another type, without a parameterless constructor of the required accessibility; a class declaring no constructor satisfies `Public`. A struct is never reported (decided 2026-10-13, before the tag): its parameterless constructor, implicit or declared, is public and can be nothing else (CS8958), so it satisfies `Public` as it is, and under any other accessibility the rule reported an Error only a suppression could answer, and the fix wrote a constructor that did not compile. A wrong accessibility is reported at the constructor. The code fix, the package's first, in `CodoMetis.TypeKit.Analyzers.CodeFixes.dll` beside the analyzer, writes or changes the constructor; a type with a primary constructor in any of its parts gets none (CS8862), nor, since 2026-10-14, one whose implicit `base()` binds to nothing (CS7036, CS0122, CS0121), which a review found the fix writing. It writes its own trivia and asks no formatter, which laid out the whole type again and wrote the machine's line endings: doc comments, attributes, comments and the members' layout stay, added lines take the members' indentation and the brace's line ending, a one-line type gets the constructor on its line | Error | built 2026-10-13 for 2.0.0 (phase 13) | | CMTK0012 | A closed set's generated `Match` given an argument by position: every parameter is a `Func` in declaration order, so reordering two members swapped their branches and compiled. Bound, only the generated signature counts; unbound (the declaring project, before weaving), a call named `Match` on a closed set with one argument per member, its names the generators' rule (the member's name, first letter lower-cased, a keyword escaped). The code fix names every argument after the parameter it binds to now | Error | built 2026-10-13 for 2.0.0 (phase 13), after a review found the swap | | CMTK0013 | A closed set's member written outside its declaration: an assignment, compound or `??=`, a deconstruction, an increment, a `ref` or `out` argument, or a writable `ref` taken to it, each also through a `ref` conditional (`(flag ? ref Open : ref Closed) = x`, found by the review of the rule itself). A `ref readonly` local or field reassigned to a member reads it and is silent (a false report found by the weaving review, 2026-10-14). C# allows such a write only in the set's static constructor and static field initializers, which run before (initializers) or after (the constructor's body) the generated initializer that names the members and fills `All`; either way an instance answered to another member's name, `Match` took another's branch, and `All` disagreed with `Parse`. An analyzer, not the aspect: Metalama 2026.1's code model gives no operations of a constructor body, and a textual scan would miss a deconstruction or a `ref` | Error | built 2026-10-14 for 2.0.0, a major, after a review reproduced the swap against the packed generators | | CMTK0014 | A `FaultKind` or `ConstructorAccessibility` constant that names no member: a conversion, a `default`, or a folded binary or unary expression or constant conditional, judged only at the outermost, so an operand folded into a member is silent; one only compared (equality, relational, pattern, `case`) is a guard. The members are read from the enum's fields, never listed ([decision 70](adr/0070-undefined-enum-value-warning.md)) | Warning | built 2026-10-14 for 2.1.0 | +| — | Add Create: the fix for the compiler's CS0535 where the missing member is `IValidatedValue`'s `Create`, matched by the message's `.Create(` (one CS0535 per missing member, at the base type that brings it, the contract or an interface deriving from it) and by the symbols: `TSelf` the type itself, no member `Create` taking a `T`. It writes a public static `Create` that returns `new TSelf(value)`, its types spelled minimally at the type, and lays it out with its own trivia as CMTK0011's fix does (`TypeLayout`); a body on one line that holds members gets none. Swept over `TypeArgumentKinds` for the wrapped type and the fault and over the declaration shapes (`MissingCreateVariantTests`); the Generators README's block is its output, compiled and generated by the Samples (#82) | — | built 2026-10-29 for 2.3.0 | | — | Code fixes for the aspect's shape diagnostics CMTK1000–1002 (missing `partial`/`record`/`readonly`) | — | proposed. The aspect keeps its own error as a backstop | **Rules that read calls see the source before weaving (measured 2026-09-28).** Metalama runs @@ -77,5 +78,7 @@ still runs on the source, where the name-based forms above remain the reach. **Release discipline.** Keep `AnalyzerReleases.Shipped/Unshipped.md` tracking (RS2008). A new rule ships at Warning or Info in a minor version and is raised to Error only in a major. Consumers build with warnings as errors, so a new Error rule in a minor version would break their builds on -update. Within a major version an Error never reports more than it did; a Warning or Info may learn -more forms in a minor version ([decision 34](adr/0034-errors-fixed-within-major-version-warnings.md), stated in the analyzer README). +update. Within a major version an Error never reports more than it did, unless the form is one only +that minor version's new API can write ([decision 118](adr/0118-an-error-may-learn-a-form-only-new-api-can-write.md)), +or a gap found before the version's tag ([decision 119](adr/0119-a-gap-found-before-the-tag-ships-in-that-release.md)); +a Warning or Info may learn more forms in a minor version ([decision 34](adr/0034-errors-fixed-within-major-version-warnings.md), stated in the analyzer README). diff --git a/design/code-graph.md b/design/code-graph.md new file mode 100644 index 00000000..0928df8c --- /dev/null +++ b/design/code-graph.md @@ -0,0 +1,35 @@ +# The code graph + +The codebase-memory MCP server keeps a graph of this repository, the project `CodoMetis.TypeKit`. +[AGENTS.md](../AGENTS.md), "Finding code", says how to use it: the graph leads and the source decides. +This note holds what that rests on: what the graph was measured to find and miss. + +## What it finds and misses + +Measured on 2026-10-13, the graph: + +- finds types, interfaces, enums and ordinary methods, a type's implementers through generic interfaces + (84 of the Samples' 88 value objects), and the callers of an ordinary method across files; +- has no node for a member of a C# 14 `extension(...)` block, which holds most of the `Option` and `Result` + extensions (`Choose`, `FirstOrNone`, `Transpose`, `Sequence`, the continuations of a `Task>`) + and `AsText`, and puts the calls inside one on the file; +- has nothing Metalama generates: none of the tests' 222 `.From(` calls has an edge, nor has a call to + `TryFrom`, `FromKnownGood`, `Parse` or a converter, and a template an aspect introduces by `nameof` looks + unreferenced. Read woven code as the Generators README's "Reading the generated code" says; +- misses a reference that is not a call: a DI registration, a type argument, `typeof`, an attribute; +- resolves a call on a generic instance poorly (`Option.Match` has one resolved caller, where the + repository writes `.Match(` about 200 times across its types) and guesses wrong callers: a low-confidence + `suffix_match` edge, or a same-named method of another API (Metalama's `Diagnostics.Report` listed as + calling `GeneratedCode.Report`). Overloads share one node. + +## Keeping it current + +The graph does not follow the working tree. `index_repository` with the repository root as `repo_path` and +`name: "CodoMetis.TypeKit"` refreshes it in about 6 s; without `name` it starts a second project under a +derived name. `index_status` reports the checkout's HEAD, not the commit the graph was built from, so +refresh rather than check. + +It indexes `spikes/` and leaves out `.claude/`, `bin` and `obj`. So an agent in a worktree under +`.claude/worktrees/` reads the main checkout's graph, not its own: it indexes the worktree under a name of +its own and deletes that project when done, or searches the source. `.codebase-memory/`, the tool's local +artifact, is ignored by git. diff --git a/design/ef-core.md b/design/ef-core.md index b1b3a94b..f83a23f2 100644 --- a/design/ef-core.md +++ b/design/ef-core.md @@ -108,11 +108,13 @@ leaves out (read from the model; no SQL Server here). On SQL Server the value object's key, declared generated, got a permanent character of a GUID string at `Add`, which was stored. It is `Never`, as in 1.5.1; an explicit `ValueGeneratedOnAdd()` or value generator still applies. A default `'\0'` is stored as it is. - - **A default key over `ulong` throws `OverflowException` at `Add`** on SQL Server and PostgreSQL, - whose column is a `decimal`: EF's temporary `decimal` is negative. A plain `ulong` key gets a - temporary `ulong`. It stays declared generated, since it fails loudly and a fix needs a value - generator per key that a compiled model can write and that leaves HiLo alone; the workaround is - `ValueGeneratedNever()` and an assigned key, which a test pins with the exception. + - **A default key over `ulong` threw `OverflowException` at `Add`** on SQL Server and PostgreSQL, + whose column is a `decimal`: EF's temporary `decimal` is negative. Since 2.3.0 ([decision 114](adr/0114-a-value-object-s-temporary-number-is-its-wrapped-type-s.md)) + a value object over a number gets the temporary values of its wrapped type, from a factory in its + converter's mapping hints, which EF consults after a generator on the property and after HiLo + ([KB-047](knowledge/KB-047-a-converter-s-hints-factory-comes-after-hilo.md)). The same reaches a `char` + key configured on add, which got a character of a GUID string on SQL Server. `--nativeaot` writes the + mappings out as code without the factory, so there a default `ulong` key still throws, and a test pins it. - **Conventions for the wrapped type do not reach a value object.** `ConfigureConventions` (`Properties()`) matches the property's CLR type; the README says to configure the value object (`Properties()`). Measured 2026-09-28 for precision, maximum length, column type and diff --git a/design/finder-benchmark-2026-10-16.md b/design/finder-benchmark-2026-10-16.md index 66b433ad..3d1b016c 100644 --- a/design/finder-benchmark-2026-10-16.md +++ b/design/finder-benchmark-2026-10-16.md @@ -57,7 +57,7 @@ violates (L01's row #242 ends inconclusive, seeded and unseeded alike). A probe `prober seeds` counts a catch ([decision 103](adr/0103-a-harness-error-is-never-a-catch-and-note-keys-are-constants.md)): a row that ends as a violation, raised no harness error, and does not violate unseeded. A seed was run against the probes of the surfaces its fix touched (the surfaces in `claims.json`, matched to the files -the fix changed by the `watched` paths of [.claude/probe-axes.md](../.claude/probe-axes.md); a +the fix changed by the `watched` paths of `.claude/probe-axes.md` (retired by [decision 112](adr/0112-the-prober-gives-way-to-valueranges-oracle-kit.md)); a Generators fix took the generation, text and JSON probes): HTTP 12 probes, EF 16, generators 13, Option/Result 8. To save builds, the seeds of one surface ran together first and each probe that failed was then run on each seed alone; every catch attributed this way is a single seed's, and a probe that diff --git a/design/knowledge/KB-012-thinktecture-value-objects.md b/design/knowledge/KB-012-thinktecture-value-objects.md index ce16a7bb..b1e85e19 100644 --- a/design/knowledge/KB-012-thinktecture-value-objects.md +++ b/design/knowledge/KB-012-thinktecture-value-objects.md @@ -1,20 +1,23 @@ --- -verified: 2026-09-28 +verified: 2026-10-29 pins: - - external Thinktecture.Runtime.Extensions 10.5.0 -source: the Thinktecture.Runtime.Extensions repository at tag 10.5.0 and its nuspecs on api.nuget.org, read by a research agent + - external Thinktecture.Runtime.Extensions 10.6.0 +source: the Thinktecture.Runtime.Extensions repository at tag 10.6.0 (commit e3f41f7a), its release notes from 10.5.0 to 10.6.0, and its nuspecs and nupkgs on api.nuget.org, read by a research agent; first read at 10.5.0 on 2026-09-28 turned-on: the Thinktecture column of the value-object table and the rows of "Choose something else when" that name it in docs/comparison.md test: none, a competitor's feature, read from its source and package --- -# KB-012. Thinktecture.Runtime.Extensions 10.5.0 declares value objects, smart enums and unions by attribute, for net8.0 and net9.0 +# KB-012. Thinktecture.Runtime.Extensions 10.6.0 declares value objects, smart enums and unions by attribute, for net8.0 and net9.0 - Declared by attribute on a partial type: `[ValueObject]`, `[ComplexValueObject]` (several members), `[SmartEnum]` and `[Union]`. - Validation is a `ValidateFactoryArguments` partial method. - EF Core through a package per EF Core version (8, 9, 10). JSON through System.Text.Json, Newtonsoft.Json and - MessagePack packages. -- Targets `net8.0` and `net9.0`. License BSD-3-Clause. + MessagePack packages. Packages for ASP.NET Core, Swashbuckle and Serilog too. +- Targets `net8.0` and `net9.0`, no `net10.0`. License BSD-3-Clause (the license file's text; the nuspec names no + SPDX id). Generated by Roslyn incremental source generators. +- 10.6.0 (2026-10-19) changed only Swashbuckle files and dependency floors since 10.5.1; 10.5.0 added enum types + as smart-enum keys. -Its OpenAPI and Native AOT facts, at 10.5.1, are [KB-013](KB-013-thinktecture-openapi-and-native-aot.md); its -smart enums, [KB-022](KB-022-thinktecture-smart-enums.md). +Its OpenAPI and Native AOT facts are [KB-013](KB-013-thinktecture-openapi-and-native-aot.md); its smart enums, +[KB-022](KB-022-thinktecture-smart-enums.md). diff --git a/design/knowledge/KB-013-thinktecture-openapi-and-native-aot.md b/design/knowledge/KB-013-thinktecture-openapi-and-native-aot.md index 5ccae436..2f1034e4 100644 --- a/design/knowledge/KB-013-thinktecture-openapi-and-native-aot.md +++ b/design/knowledge/KB-013-thinktecture-openapi-and-native-aot.md @@ -1,16 +1,19 @@ --- -verified: 2026-10-14 +verified: 2026-10-29 pins: - - external Thinktecture.Runtime.Extensions 10.5.1 -source: the Thinktecture.Runtime.Extensions repository at tag 10.5.1 (its Swashbuckle package and project files), read without running anything + - external Thinktecture.Runtime.Extensions 10.6.0 +source: the Thinktecture.Runtime.Extensions repository at tag 10.6.0 (its Swashbuckle package and project files) and the 10.6.0 release notes, read without running anything; first read at 10.5.1 on 2026-10-14 turned-on: the OpenAPI, host converter, Native AOT and targets rows of the value-object table in docs/comparison.md test: none, a competitor's feature, read from its source and package --- -# KB-013. Thinktecture.Runtime.Extensions 10.5.1 documents OpenAPI through Swashbuckle alone, and does not declare Native AOT compatibility +# KB-013. Thinktecture.Runtime.Extensions 10.6.0 documents OpenAPI through Swashbuckle alone, and does not declare Native AOT compatibility -- OpenAPI is a Swashbuckle package only, with `type` and `format` copied from Swashbuckle's static table; nothing - for Microsoft.AspNetCore.OpenApi. A smart enum's listed values are written with the host's JSON options. +- OpenAPI is a Swashbuckle package only, with `type` and `format` copied from the schema Swashbuckle generates for + the key type; nothing for Microsoft.AspNetCore.OpenApi. +- A smart enum's listed values are written with the host's JSON options: since 10.6.0, MVC's `JsonOptions` when MVC + is registered, otherwise the Minimal API (`Http.Json`) options, as Swashbuckle 10.3 resolves them. A + `JsonSerializerOptions` registered in the service provider is no longer used. - A converter the host registers is not reflected in type and format. - The Swashbuckle and ASP.NET Core packages target `net8.0` only; the core and JSON packages `net8.0` and `net9.0`. -- No package declares `IsAotCompatible`. +- No package declares `IsAotCompatible` or `IsTrimmable`. diff --git a/design/knowledge/KB-015-languageext-option.md b/design/knowledge/KB-015-languageext-option.md index a3032c50..a1580b22 100644 --- a/design/knowledge/KB-015-languageext-option.md +++ b/design/knowledge/KB-015-languageext-option.md @@ -1,16 +1,19 @@ --- -verified: 2026-09-28 +verified: 2026-10-29 pins: - external LanguageExt.Core 4.4.9 - - external LanguageExt.Core 5.0.0-beta-77 -source: the language-ext repository at tags 4.4.9 and 5.0.0-beta-77 and their nuspecs on api.nuget.org, read by a research agent + - external LanguageExt.Core 5.0.0-beta-78 +source: the language-ext repository at 4.4.9 (commit 1551a092, its nuspec's) and at tag v5.0.0-beta-78, the beta-78 release notes, and their nuspecs and nupkgs on api.nuget.org, read by a research agent; first read at 4.4.9 and 5.0.0-beta-77 on 2026-09-28 turned-on: the LanguageExt column of the Option and Result table in docs/comparison.md, and decision 5 (pure Option and Result, not a LanguageExt-style framework) test: none, a competitor's feature, read from its source and package --- # KB-015. LanguageExt has no public `.Value` on `Option`, and its `default` is `None`, in 4.4.9 and in the 5.0 beta -- `Option` has no public `.Value`; the content is reached through `Match`, `IfNone` and the like. `default(Option)` - is `None`. Both hold in 4.4.9 and 5.0.0-beta-77, so neither is a difference from TypeKit. +- `Option` has no public `.Value` (its field is `internal`); the content is reached through `Match`, `IfNone` and + the like. `default(Option)` is `None`. Both hold in 4.4.9 and 5.0.0-beta-78, so neither is a difference from + TypeKit. - No System.Text.Json converter of its own, and no analyzer. -- 4.4.9 targets `netstandard2.0`; 5.0.0-beta-77 targets `net10.0` only. +- 4.4.9 targets `netstandard2.0`; 5.0.0-beta-78 targets `net10.0` only. +- 4.4.9 (2024-06-26) is still the latest stable release. 5.0.0-beta-78 (2026-10-09) fixes one bug and leaves + `Option` as beta-77 had it. diff --git a/design/knowledge/KB-022-thinktecture-smart-enums.md b/design/knowledge/KB-022-thinktecture-smart-enums.md index ca53071b..c9e937aa 100644 --- a/design/knowledge/KB-022-thinktecture-smart-enums.md +++ b/design/knowledge/KB-022-thinktecture-smart-enums.md @@ -1,20 +1,23 @@ --- -verified: 2026-10-13 +verified: 2026-10-29 pins: - - external Thinktecture.Runtime.Extensions 10.5.1 -source: the Thinktecture.Runtime.Extensions repository at tag 10.5.1 and its nuspecs on api.nuget.org, read by a research agent + - external Thinktecture.Runtime.Extensions 10.6.0 +source: the Thinktecture.Runtime.Extensions repository at tag 10.6.0 (its generators, attributes and README), its release notes from 10.5.1 to 10.6.0, and its nuspecs on api.nuget.org, read by a research agent; first read at 10.5.1 on 2026-10-13 turned-on: the closed-set table and the smart-enum row of "Choose something else when" in docs/comparison.md, and decision 51 (a closed set parses ignoring case only on request) test: none, a competitor's feature, read from its source and package --- -# KB-022. Thinktecture.Runtime.Extensions 10.5.1 smart enums have a key apart from the identifier, a `Switch` over every member, and look keys up ignoring case +# KB-022. Thinktecture.Runtime.Extensions 10.6.0 smart enums have a key apart from the identifier, a `Switch` over every member, and look keys up ignoring case - `[SmartEnum]` on a partial class, each member `new("KEY")`; declared properties become constructor - parameters. -- Generated `Switch` and `Map` take every member as a required argument. + parameters. The key can be an enum type since 10.5.0. +- Generated `Switch` and `Map` take every member as a required argument by default. + `SwitchMapMethodsGeneration.DefaultWithPartialOverloads` also generates `SwitchPartially` and `MapPartially`, + where a member may be left out for a default. - `Get` and `TryGet`; string keys compare `OrdinalIgnoreCase` by default (ordinal on request through `[KeyMemberEqualityComparer<...>]`). A case clash throws `ArgumentException` on the first lookup. - JSON and MessagePack in separate packages, the wire is the key. EF Core through a package per EF Core version with `UseThinktectureValueConverters()`, the column is the key. OpenAPI through a Swashbuckle package. - TTRESG analyzers, with refactorings. The key can differ from the identifier. -- Released 2026-10-08. Targets `net8.0` and `net9.0`. License BSD-3-Clause. +- 10.6.0 released 2026-10-19, with no generator, analyzer or attribute change since 10.5.1. Targets `net8.0` and + `net9.0`. License BSD-3-Clause. diff --git a/design/knowledge/KB-043-github-actions-skips-concurrency-and-dispatch.md b/design/knowledge/KB-043-github-actions-skips-concurrency-and-dispatch.md new file mode 100644 index 00000000..ac259d30 --- /dev/null +++ b/design/knowledge/KB-043-github-actions-skips-concurrency-and-dispatch.md @@ -0,0 +1,24 @@ +--- +verified: 2026-10-29 +pins: + - external GitHub-Actions 2026-10-29 +source: docs.github.com, "Using conditions to control job execution", "Control the concurrency of workflows and jobs", "Reuse workflows" and "Triggering a workflow" (events triggered by the GITHUB_TOKEN), and "Disabling and enabling a workflow", read through Context7 on 2026-10-29; the skipped-required-check rule as issue 70 states it, not reproduced on a live pull request +turned-on: decision 113 (the gate job, the concurrency group of dotnet.yml under release.yml's call, and the nightly dispatch) +test: none, a hosted service's behaviour, read from its documentation +--- + +# KB-043. On GitHub Actions, a skipped job passes a required check, concurrency groups ignore case and a called workflow takes its caller's name, and the GITHUB_TOKEN can dispatch a workflow + +- **A skipped job reports Success.** A job skipped by its `if`, or because a job it needs failed, does not + block a merge even if it is a required check. A required check cannot tell a deliberate skip from an + accidental one. A job that judges the others under `always()` can. +- **A concurrency group's name ignores case.** `prod` and `Prod` are the same group. +- **A called workflow uses its caller's name in `${{ github.workflow }}`.** A reusable workflow whose + workflow-level group is `${{ github.workflow }}-${{ github.ref }}`, called from a workflow whose group is + `release-${{ github.ref }}`, shares that group with its caller (the names differ only in case) and waits + for it. +- **The GITHUB_TOKEN can start a run by dispatch.** Events the GITHUB_TOKEN triggers create no new workflow + run, except `workflow_dispatch` and `repository_dispatch`, which always do. So a scheduled job with + `actions: write` can dispatch the build workflow on other branches. +- **A schedule stops.** In a public repository, GitHub turns scheduled workflows off after 60 days without + repository activity, and a schedule runs only from the default branch. diff --git a/design/knowledge/KB-044-superstrong-types.md b/design/knowledge/KB-044-superstrong-types.md new file mode 100644 index 00000000..68b94445 --- /dev/null +++ b/design/knowledge/KB-044-superstrong-types.md @@ -0,0 +1,31 @@ +--- +verified: 2026-10-29 +pins: + - external SuperStrong.Types 1.0.0 +source: the SuperStrong.Types repository at tag v1.0.0 (commit 1d059e0) and its nuspecs on api.nuget.org, read by a research agent and spot-checked (2026-10-29) +turned-on: the SuperStrong.Types column of the value-object table in docs/comparison.md +test: none, a competitor's feature, read from its source and package +--- + +# KB-044. SuperStrong.Types 1.0.0 is an attribute on a partial class, validated everywhere it creates a value, with no Native AOT declaration + +- Released 2026-07-21 (1.0.0-alpha.1 on 2026-06-14). Packages: `SuperStrong.Types` (the generator, analyzers and code + fixes ship inside it), `.EntityFrameworkCore`, `.AspNetCore.OpenApi`, `.HotChocolate` and `.NewtonsoftJson`, all 1.0.0. +- Declared by `[StrongType]` (or `[StrongType]` for a shared definition) on a + `partial class`. The attribute targets classes only, and a record, a base type other than `object` or an abstract + class fails the build (SST003, SST004, SST006). +- Validation is a `Define()` method returning a definition built from validators (`IsPositive()`, `HasMaxLength()`, + `MatchesRegex()` and others). `From` throws `StrongTypeValidationException`, `TryFrom` returns `false`. Parsing, the + System.Text.Json and Newtonsoft.Json converters, the type converter, the EF Core value converter (reads included) and + Hot Chocolate's input coercion all go through `From` or `TryFrom`. +- EF Core: `UseStrongTypes()` adds a type-mapping plugin for every strong type, nothing per type. A query interceptor + translates `AsPrimitive()` (filter, projection, ordering, aggregates). +- OpenAPI: Microsoft.AspNetCore.OpenApi only, opt-in, `AddStrongTypes(representation)`: the underlying type's schema + inline, or a named component with the underlying type's `type` and `format`. Both come from + `GetOrCreateSchemaAsync` for the underlying type, and no code of its own describes a converter the host + registers (read from its code; not run against such a converter). +- System.Text.Json is built in (a generated `[JsonConverter]` that writes the underlying value under the host's + options); Newtonsoft.Json is opt-in, registered on the settings. +- One analyzer: SST005 (Warning), `Equals(T)` and `GetHashCode()` declared together. None reports `default` or `new()`. +- No `IsAotCompatible` or `IsTrimmable` in any project file, and no Native AOT step in CI. No `Option`, `Result` or smart + enum. Targets `net10.0`; a Roslyn incremental source generator; license MIT. diff --git a/design/knowledge/KB-045-first-releases-of-the-compared-libraries.md b/design/knowledge/KB-045-first-releases-of-the-compared-libraries.md new file mode 100644 index 00000000..074bb9ab --- /dev/null +++ b/design/knowledge/KB-045-first-releases-of-the-compared-libraries.md @@ -0,0 +1,28 @@ +--- +verified: 2026-10-29 +pins: + - external NuGet registration data of the compared libraries, 2026-10-29 +source: the registration index of each package on api.nuget.org (registration5-gz-semver2), earliest and latest listed publish dates, read 2026-10-29 +turned-on: the "long release history" row of "Choose something else when" in docs/comparison.md +test: none, a competitor's release history, read from NuGet +--- + +# KB-045. The compared libraries were first published on NuGet between 2015 and 2026 + +First listed version and its date, and the latest listed version, on 2026-10-29: + +| Package | First | Latest | +|---|---|---| +| LanguageExt.Core | 1.7.5, 2015-12-09 | 5.0.0-beta-78, 2026-10-09 | +| OneOf | 1.0.5, 2016-01-12 | 3.0.271, 2024-05-16 | +| CSharpFunctionalExtensions | 1.0.1, 2016-06-20 | 3.7.0, 2026-03-02 | +| FluentResults | 0.5.0, 2017-05-01 | 4.0.0, 2025-06-29 | +| Ardalis.SmartEnum | 1.0.0, 2017-12-01 | 8.2.0, 2024-11-19 | +| Thinktecture.Runtime.Extensions | 0.1.0-beta1, 2018-02-11 | 10.6.0, 2026-10-19 | +| StronglyTypedId | 0.1.2, 2019-06-05 | 1.0.0-beta08, 2024-04-25 | +| Vogen | 1.0.1, 2021-12-01 | 8.0.7, 2026-07-21 | +| ErrorOr | 0.0.2, 2022-05-31 | 2.1.1, 2026-05-15 | +| Intellenum | 0.0.1-alpha.1, 2023-03-12 | 2.0.1, 2025-09-30 | +| SuperStrong.Types | 1.0.0-alpha.1, 2026-06-14 | 1.0.0, 2026-07-21 | + +A first publish date says how long a package has been released, not how widely it is used in production. diff --git a/design/knowledge/KB-046-csharp-15-unions-ship-with-the-net-11-sdk.md b/design/knowledge/KB-046-csharp-15-unions-ship-with-the-net-11-sdk.md new file mode 100644 index 00000000..1e9b07db --- /dev/null +++ b/design/knowledge/KB-046-csharp-15-unions-ship-with-the-net-11-sdk.md @@ -0,0 +1,18 @@ +--- +verified: 2026-10-29 +pins: + - external C# 15 and the .NET 11 SDK, release candidate 2 +source: Microsoft Learn, "What's new in C# 15" (updated 2026-10-23, docs commit 433547d), and the .NET blog's "Explore union types in C# 15" (2026-04-02), read 2026-10-29 +turned-on: the C# union types row of "Choose something else when" in docs/comparison.md +test: none, a language feature's release status, read from Microsoft's documentation +--- + +# KB-046. C# 15's union types and closed hierarchies need no preview flag in the .NET 11 SDK, which is still prerelease + +- Union types (`public union Pet(Cat, Dog, Bird);`) and closed hierarchies (`closed` on a class) are C# 15 features, + with exhaustive `switch` checking. +- C# 15 is the default language version for `net11.0` in the .NET 11 SDK, and except for memory safety its features + need no `preview`. +- The .NET 11 SDK is still prerelease. Microsoft Learn's .NET 11 overview (updated 2026-10-21, docs commit 015d8a2) was + last updated for release candidate 2, and expects general availability in November 2026. +- Unions first appeared as a preview feature in .NET 11 Preview 2 (the .NET blog, 2026-04-02). diff --git a/design/knowledge/KB-047-a-converter-s-hints-factory-comes-after-hilo.md b/design/knowledge/KB-047-a-converter-s-hints-factory-comes-after-hilo.md new file mode 100644 index 00000000..7ac610f0 --- /dev/null +++ b/design/knowledge/KB-047-a-converter-s-hints-factory-comes-after-hilo.md @@ -0,0 +1,23 @@ +--- +verified: 2026-10-29 +pins: + - Microsoft.EntityFrameworkCore.Relational 10.0.12 + - Npgsql.EntityFrameworkCore.PostgreSQL 10.0.3 +source: EF Core's source at v10.0.12 (ValueGeneratorSelector.CreateFromFactory and TryCreate, RelationalValueGeneratorSelector.FindForType, TemporaryNumberValueGeneratorFactory.Create, the CoreTypeMapping constructor, SqlServerValueGeneratorSelector.TrySelect) and Npgsql's at v10.0.3 (NpgsqlValueGeneratorSelector.TrySelect), read 2026-10-29; measured with plain EF Core and Npgsql, no database opened +turned-on: decision 114, the temporary value of a value-object key over a number (#86) +test: test/CodoMetis.TypeKit.Facts/KB047.cs +--- + +# KB-047. EF Core consults a converter's mapping-hints value generator after a property's generator and after HiLo + +- EF Core chooses a temporary number for a generated property by its type mapping's CLR type, then by its + converter's provider type (`TemporaryNumberValueGeneratorFactory.Create`). A key of a type that is no number, + converted to a `decimal` column, gets a negative temporary `decimal`, and a conversion back to `ulong` throws + `OverflowException` at `Add`. +- A type mapping built on a converter takes the value generator factory of the converter's `ConverterMappingHints` + where the mapping has none of its own (the `CoreTypeMapping` constructor). +- `ValueGeneratorSelector` uses a generator configured on the property first, then the type mapping's factory, then + its own choice. The SQL Server and Npgsql selectors choose HiLo before calling it, for a property without a + generator of its own. +- `ConverterMappingHints.ValueGeneratorFactory` is marked `[Obsolete]` without a message; the constructor parameter + that sets it is not. diff --git a/design/knowledge/KB-048-sql-server-compares-text-ignoring-case.md b/design/knowledge/KB-048-sql-server-compares-text-ignoring-case.md new file mode 100644 index 00000000..9e001cae --- /dev/null +++ b/design/knowledge/KB-048-sql-server-compares-text-ignoring-case.md @@ -0,0 +1,21 @@ +--- +verified: 2026-10-29 +pins: + - external SQL-Server 2022 + - Microsoft.EntityFrameworkCore.SqlServer 10.0.12 +source: https://learn.microsoft.com/sql/t-sql/language-elements/string-comparison-assignment ("Transact-SQL considers the strings 'abc' and 'abc ' to be equivalent for most comparison operations") and EF Core's Entity Properties page, whose example configures "a SQL Server column to be case-insensitive" with SQL_Latin1_General_CP1_CI_AS; measured with plain EF Core on the mssql/server 2022 image, whose databases take that collation +turned-on: the SQL Server query catalogue's text cases (SqlServerQueryVariantTests, issue 81), where a text or char value object compared, ordered or aggregated answers in another order than memory's ordinal one, and the EF README's "Ordering in SQL follows the database's collation", extended to comparison +test: test/CodoMetis.TypeKit.Facts/KB048.cs +--- + +# KB-048. SQL Server compares and orders text ignoring case, and trailing spaces + +A `nvarchar` column holding `alpha`, `Bravo` and `charlie`, under the default collation +`SQL_Latin1_General_CP1_CI_AS`: + +- `Where(n => n.Name == "BRAVO")` finds `Bravo`, and `n.Name == "alpha "` finds `alpha`; +- `upper.Contains(n.Name)` over `["ALPHA"]` finds `alpha`, and `StartsWith("b")` finds `Bravo`; +- `OrderBy(n => n.Name)` gives `["alpha", "Bravo", "charlie"]`, where ordinal order is `["Bravo", "alpha", "charlie"]`; +- `OrderBy(n => EF.Functions.Collate(n.Name, "Latin1_General_BIN2"))` gives the ordinal order. + +So a query over text matches and orders rows that ordinal comparison in memory keeps apart. diff --git a/design/knowledge/KB-049-sql-server-orders-a-uniqueidentifier-by-its-last-group.md b/design/knowledge/KB-049-sql-server-orders-a-uniqueidentifier-by-its-last-group.md new file mode 100644 index 00000000..a070c3e1 --- /dev/null +++ b/design/knowledge/KB-049-sql-server-orders-a-uniqueidentifier-by-its-last-group.md @@ -0,0 +1,16 @@ +--- +verified: 2026-10-29 +pins: + - external SQL-Server 2022 + - Microsoft.EntityFrameworkCore.SqlServer 10.0.12 +source: https://learn.microsoft.com/sql/connect/ado-net/sql/compare-guid-uniqueidentifier-values (SqlGuid "implements CompareTo using SQL Server behavior, in the last six bytes of a value are most significant", where Guid "evaluates all 16 bytes"), measured with plain EF Core on the mssql/server 2022 image +turned-on: the SQL Server query catalogue's Guid cases (SqlServerQueryVariantTests, issue 81), where a Guid value object compared, ordered or aggregated answers in another order than memory's, and the catalogue's Guid values, which ascend by their first group and not by their last +test: test/CodoMetis.TypeKit.Facts/KB049.cs +--- + +# KB-049. SQL Server orders a `uniqueidentifier` by its last group first + +Three Guids that ascend in memory by their first group, `00000001-…-000000000003`, `00000002-…-000000000001` and +`00000003-…-000000000002`, order on SQL Server by their last group: `OrderBy` answers the second, the third, then the +first, `Min` the second, and `< first` finds the other two. `Guid.CompareTo` compares the first group first, so +an ordering, a comparison or a `Min` or `Max` over Guids answers otherwise in memory. diff --git a/design/knowledge/KB-050-a-decimal-is-decimal-18-2-on-sql-server.md b/design/knowledge/KB-050-a-decimal-is-decimal-18-2-on-sql-server.md new file mode 100644 index 00000000..82f5e266 --- /dev/null +++ b/design/knowledge/KB-050-a-decimal-is-decimal-18-2-on-sql-server.md @@ -0,0 +1,15 @@ +--- +verified: 2026-10-29 +pins: + - external SQL-Server 2022 + - Microsoft.EntityFrameworkCore.SqlServer 10.0.12 +source: measured with plain EF Core on the mssql/server 2022 image, the model's column type and the values read back. EF's documentation names no default precision, and SQL Server's precision page no rounding on write +turned-on: the SQL Server query catalogue's decimal cases and the key catalogue's decimal keys (SqlServerQueryVariantTests, SqlServerKeyVariantTests, issue 81), which read 10.00 where memory holds 10, and the EF README's HavePrecision guidance for SQL Server +test: test/CodoMetis.TypeKit.Facts/KB050.cs +--- + +# KB-050. A `decimal` is a `decimal(18,2)` on SQL Server, read back at scale 2 and rounded on write + +A `decimal` property without `HasPrecision` is a `decimal(18,2)` column. Written as `10m`, `1.005m` and +`-0.004m`, the values read back as `10.00m`, `1.01m` and `0.00m`: every value at scale 2, which is equal to +`10m` but prints `"10.00"`, and more fractional digits rounded half away from zero, without an error. diff --git a/design/knowledge/KB-051-sql-server-s-time-holds-less-than-a-day.md b/design/knowledge/KB-051-sql-server-s-time-holds-less-than-a-day.md new file mode 100644 index 00000000..503a0f9b --- /dev/null +++ b/design/knowledge/KB-051-sql-server-s-time-holds-less-than-a-day.md @@ -0,0 +1,16 @@ +--- +verified: 2026-10-29 +pins: + - external SQL-Server 2022 + - Microsoft.EntityFrameworkCore.SqlServer 10.0.12 +source: https://learn.microsoft.com/sql/t-sql/data-types/time-transact-sql (its range, 00:00:00.0000000 through 23:59:59.9999999), measured with plain EF Core on the mssql/server 2022 image, the column type, the refusal and the JSON read +turned-on: the SQL Server query catalogue's TimeSpan cases (SqlServerQueryVariantTests, issue 81), whose 1.02:00:00 is refused in a column and read as 01:02:00 from a JSON column, and the EF README's TimeSpan warning for SQL Server +test: test/CodoMetis.TypeKit.Facts/KB051.cs +--- + +# KB-051. SQL Server's `time` holds less than a day, so a longer `TimeSpan` is refused in a column and misread in JSON + +EF maps a `TimeSpan` to `time`. `SaveChanges` of `TimeSpan.FromHours(26)` into such a column throws a +`DbUpdateException`. In a JSON column (`OwnsOne(…).ToJson()`) the same value is written as its text, `"1.02:00:00"`; +an entity read whole has `1.02:00:00`, but a query that reads the property (`Select(h => h.Doc.Length)`) converts +the text to `time` and answers `01:02:00`, without an error. diff --git a/design/knowledge/KB-052-sql-server-takes-no-max-of-a-bit.md b/design/knowledge/KB-052-sql-server-takes-no-max-of-a-bit.md new file mode 100644 index 00000000..9f36d912 --- /dev/null +++ b/design/knowledge/KB-052-sql-server-takes-no-max-of-a-bit.md @@ -0,0 +1,14 @@ +--- +verified: 2026-10-29 +pins: + - external SQL-Server 2022 + - Microsoft.EntityFrameworkCore.SqlServer 10.0.12 +source: https://learn.microsoft.com/sql/t-sql/functions/max-transact-sql (MAX "can be used with numeric, char, nchar, varchar, nvarchar, uniqueidentifier, or datetime columns, but not with bit columns"), measured with plain EF Core on the mssql/server 2022 image +turned-on: the SQL Server query catalogue's bool cases (SqlServerQueryVariantTests, issue 81), where a Max or Min of a bool value object is refused for the wrapped type too +test: test/CodoMetis.TypeKit.Facts/KB052.cs +--- + +# KB-052. SQL Server takes neither `MAX` nor `MIN` of a `bit` + +EF translates `Max(f => f.Flag)` and `Min` of a `bool` column to `MAX` and `MIN` of a `bit`, and SQL Server refuses +them: a `SqlException`, "Operand data type bit is invalid for max operator" (`min` for `MIN`). diff --git a/design/knowledge/KB-053-greatest-and-least-need-compatibility-level-160.md b/design/knowledge/KB-053-greatest-and-least-need-compatibility-level-160.md new file mode 100644 index 00000000..b698279d --- /dev/null +++ b/design/knowledge/KB-053-greatest-and-least-need-compatibility-level-160.md @@ -0,0 +1,15 @@ +--- +verified: 2026-10-29 +pins: + - Microsoft.EntityFrameworkCore.SqlServer 10.0.12 +source: measured with plain EF Core, no server opened, ToQueryString at EF's default level, at 150 and at 160. EF's SQL Server function-mappings page (learn.microsoft.com/ef/core/providers/sql-server/functions) lists Math.Max as GREATEST from EF Core 9.0 and names no level +turned-on: the SQL Server query catalogue's Greatest and Least cases (SqlServerQueryVariantTests, issue 81), refused at EF's default level and translated, as the wrapped type's are, at 160 +test: test/CodoMetis.TypeKit.Facts/KB053.cs +--- + +# KB-053. EF translates `Greatest` and `Least` on SQL Server only from compatibility level 160 + +Under `UseSqlServer` at EF's default compatibility level, or at 150, `Where(p => EF.Functions.Greatest(p.Left, p.Right) > 2)` +is refused, "could not be translated", and `Select(p => EF.Functions.Least(p.Left, p.Right))` reads the two columns and +calls `Least` in memory, where it throws "switched to client-evaluation" when the query runs. With +`UseCompatibilityLevel(160)` both translate to `GREATEST` and `LEAST`. diff --git a/design/knowledge/KB-054-datetime2-keeps-every-tick-and-forgets-the-kind.md b/design/knowledge/KB-054-datetime2-keeps-every-tick-and-forgets-the-kind.md new file mode 100644 index 00000000..00aaf090 --- /dev/null +++ b/design/knowledge/KB-054-datetime2-keeps-every-tick-and-forgets-the-kind.md @@ -0,0 +1,15 @@ +--- +verified: 2026-10-29 +pins: + - external SQL-Server 2022 + - Microsoft.EntityFrameworkCore.SqlServer 10.0.12 +source: EF Core's Entity Properties page ("the SQL Server provider maps DateTime properties to datetime2(7) columns"), measured with plain EF Core on the mssql/server 2022 image +turned-on: the SQL Server round trips (SqlServerRoundTripTests, issue 81), where a DateTime value object written as UTC reads back equal, with every tick and without its kind, as a plain DateTime does +test: test/CodoMetis.TypeKit.Facts/KB054.cs +--- + +# KB-054. `datetime2` keeps every tick of a `DateTime`, and reads it back without its kind + +EF maps a `DateTime` to `datetime2`, precision 7 unless configured, so nothing is rounded: a value with 1234567 extra +ticks reads back with all of them. A `DateTime` written as UTC reads back with `Kind` Unspecified, and equal, since +`DateTime` equality ignores the kind. A `DateTimeOffset` (`datetimeoffset`) keeps its ticks and its offset. diff --git a/design/knowledge/README.md b/design/knowledge/README.md index cdcad740..c9e4b6fd 100644 --- a/design/knowledge/README.md +++ b/design/knowledge/README.md @@ -12,7 +12,9 @@ re-verified; `docs-query check` lists the stale ones. **Adding one.** Only what something here turned on (a decision, a fix, a probe's verdict), pinned and falsifiable, checked against a primary source. A competitor's feature is a fact too, pinned -`external {name} {version}`: no check compares it, so it is as fresh as its date. Take the next number, copy an +`external {name} {version}`: `check` never compares it, so it is as fresh as its date, and `docs-query due --nuget` +lists the ones to re-read before a release (CONTRIBUTING, "Releasing"): verified more than six months ago, or pinned +to a package NuGet has a later release of. Take the next number, copy an entry's frontmatter, write the test first where a test can observe it, and add a row below. **A fact that prunes or excuses is executable** ([decision 107](../adr/0107-a-cited-fact-is-executable-and-seeds-run-by-footprint.md)). @@ -40,17 +42,17 @@ test and later for the harness's `Baseline.Fact`. | [KB-009](KB-009-a-modifier-s-converter-wins-over-the-attribute.md) | A resolver modifier's `CustomConverter` writes a property over its `[JsonConverter]` | SDK 10.0.4xx | `KB009` | | [KB-010](KB-010-vogen-declaring-validating-storing.md) | Vogen 8.0.7 declares a value object by attribute, validates through `Validate`, and targets .NET Standard 2.0 | Vogen 8.0.7, 9.0.0-beta.2 | prose | | [KB-011](KB-011-vogen-openapi-and-native-aot.md) | Vogen 8.0.7 generates OpenAPI schemas only on request, from a built-in type table, and does not declare Native AOT compatibility | Vogen 8.0.7, 9.0.0-beta.2 | prose | -| [KB-012](KB-012-thinktecture-value-objects.md) | Thinktecture.Runtime.Extensions 10.5.0 declares value objects, smart enums and unions by attribute, for net8.0 and net9.0 | Thinktecture 10.5.0 | prose | -| [KB-013](KB-013-thinktecture-openapi-and-native-aot.md) | Thinktecture.Runtime.Extensions 10.5.1 documents OpenAPI through Swashbuckle alone, and does not declare Native AOT compatibility | Thinktecture 10.5.1 | prose | +| [KB-012](KB-012-thinktecture-value-objects.md) | Thinktecture.Runtime.Extensions 10.6.0 declares value objects, smart enums and unions by attribute, for net8.0 and net9.0 | Thinktecture 10.6.0 | prose | +| [KB-013](KB-013-thinktecture-openapi-and-native-aot.md) | Thinktecture.Runtime.Extensions 10.6.0 documents OpenAPI through Swashbuckle alone, and does not declare Native AOT compatibility | Thinktecture 10.6.0 | prose | | [KB-014](KB-014-stronglytypedid.md) | StronglyTypedId 1.0.0-beta08 is an attribute on a partial struct, with no validation, analyzers or OpenAPI support | StronglyTypedId 1.0.0-beta08 | prose | -| [KB-015](KB-015-languageext-option.md) | LanguageExt has no public `.Value` on `Option`, and its `default` is `None`, in 4.4.9 and the 5.0 beta | LanguageExt 4.4.9, 5.0.0-beta-77 | prose | +| [KB-015](KB-015-languageext-option.md) | LanguageExt has no public `.Value` on `Option`, and its `default` is `None`, in 4.4.9 and the 5.0 beta | LanguageExt 4.4.9, 5.0.0-beta-78 | prose | | [KB-016](KB-016-csharpfunctionalextensions.md) | CSharpFunctionalExtensions 3.7.0 throws from `.Value` on a failure, and its `default(Result)` is a success | CSharpFunctionalExtensions 3.7.0 | prose | | [KB-017](KB-017-erroror.md) | ErrorOr 2.1.1 returns `default` from `.Value` on an error, and its `default` instance is a success | ErrorOr 2.1.1 | prose | | [KB-018](KB-018-fluentresults.md) | FluentResults 4.0.0 results are classes that carry reasons, errors and successes, and an error can name its cause | FluentResults 4.0.0 | prose | | [KB-019](KB-019-oneof.md) | OneOf 3.0.271 is a readonly struct whose `default` is its first case | OneOf 3.0.271 | prose | | [KB-020](KB-020-ardalis-smartenum.md) | Ardalis.SmartEnum 8.2.0 is a run-time base class, finds its members by reflection, and requires no member in `When` | Ardalis.SmartEnum 8.2.0 | prose | | [KB-021](KB-021-intellenum.md) | Intellenum 2.0.1 has one underlying value per member, and its case-insensitive lookup answers the first declared | Intellenum 2.0.1 | prose | -| [KB-022](KB-022-thinktecture-smart-enums.md) | Thinktecture.Runtime.Extensions 10.5.1 smart enums have a key apart from the identifier, a `Switch` over every member, and look keys up ignoring case | Thinktecture 10.5.1 | prose | +| [KB-022](KB-022-thinktecture-smart-enums.md) | Thinktecture.Runtime.Extensions 10.6.0 smart enums have a key apart from the identifier, a `Switch` over every member, and look keys up ignoring case | Thinktecture 10.6.0 | prose | | [KB-023](KB-023-a-converter-cannot-describe-its-schema.md) | A JSON converter of one's own cannot say what it writes: the schema exporter writes the empty schema, ASP.NET only a format | OpenApi 10.0.12, SDK 10.0.4xx | `KB023` | | [KB-024](KB-024-no-library-constrains-type-arguments-by-kind.md) | No .NET library constrains a type argument to be abstract, sealed, a record or concrete, or two arguments to differ | AdvancedGenericTypeConstraints 0.4.1, RossLean.GenericsAnalyzer 1.4.0 | prose | | [KB-025](KB-025-the-coverage-collector-rewrites-assemblies-on-disk.md) | The code coverage collector instruments the assemblies on disk, so test hosts started together from one output directory corrupt each other, and its in-memory mode records no coverage on macOS arm64 | Microsoft.Testing.Extensions.CodeCoverage 18.12.0, SDK 10.0.4xx | prose | @@ -71,3 +73,15 @@ test and later for the harness's `Baseline.Fact`. | [KB-040](KB-040-a-converter-is-charged-a-level-of-max-depth.md) | Under `MaxDepth`, System.Text.Json refuses a value a converter of one's own writes at the limit, where its own converter writes it | SDK 10.0.4xx | `KB040` | | [KB-041](KB-041-mvc-cannot-write-its-400-under-a-context-without-validation-problem-details.md) | MVC cannot write its automatic 400 under a source-generated context that does not list `ValidationProblemDetails` | SDK 10.0.4xx | `KB041` | | [KB-042](KB-042-a-char-schema-is-refused-under-write-as-string.md) | Under `NumberHandling` with `WriteAsString`, ASP.NET refuses the OpenAPI document of an endpoint whose schema holds a `char` | OpenApi 10.0.12, SDK 10.0.4xx | `KB042` | +| [KB-043](KB-043-github-actions-skips-concurrency-and-dispatch.md) | On GitHub Actions, a skipped job passes a required check, concurrency groups ignore case and a called workflow takes its caller's `github.workflow`, and the GITHUB_TOKEN can dispatch a workflow | external GitHub-Actions 2026-10-29 | prose | +| [KB-044](KB-044-superstrong-types.md) | SuperStrong.Types 1.0.0 is an attribute on a partial class, validated everywhere it creates a value, with no Native AOT declaration | SuperStrong.Types 1.0.0 | prose | +| [KB-045](KB-045-first-releases-of-the-compared-libraries.md) | The compared libraries were first published on NuGet between 2015 and 2026 | NuGet, 2026-10-29 | prose | +| [KB-046](KB-046-csharp-15-unions-ship-with-the-net-11-sdk.md) | C# 15's union types and closed hierarchies need no preview flag in the .NET 11 SDK, which is still prerelease | .NET 11 SDK RC 2 | prose | +| [KB-047](KB-047-a-converter-s-hints-factory-comes-after-hilo.md) | EF Core consults a converter's mapping-hints value generator after a property's generator and after HiLo, and gives a key that is no number the temporary number of its column | EF Core 10.0.12, Npgsql 10.0.3 | `KB047` | +| [KB-048](KB-048-sql-server-compares-text-ignoring-case.md) | Under SQL Server's default collation text compares and orders ignoring case and trailing spaces: `==`, `IN` and `LIKE` match rows that differ in case | SQL Server 2022, EF SqlServer 10.0.12 | `KB048`, with Docker | +| [KB-049](KB-049-sql-server-orders-a-uniqueidentifier-by-its-last-group.md) | SQL Server orders a `uniqueidentifier` by its last group first, where `Guid.CompareTo` starts at the first | SQL Server 2022, EF SqlServer 10.0.12 | `KB049`, with Docker | +| [KB-050](KB-050-a-decimal-is-decimal-18-2-on-sql-server.md) | A `decimal` is a `decimal(18,2)` on SQL Server: every value reads back at scale 2, more digits rounded on write | SQL Server 2022, EF SqlServer 10.0.12 | `KB050`, with Docker | +| [KB-051](KB-051-sql-server-s-time-holds-less-than-a-day.md) | SQL Server's `time` holds less than a day: a longer `TimeSpan` is refused in a column, and read by a query as another value from JSON | SQL Server 2022, EF SqlServer 10.0.12 | `KB051`, with Docker | +| [KB-052](KB-052-sql-server-takes-no-max-of-a-bit.md) | SQL Server takes neither `MAX` nor `MIN` of a `bit` | SQL Server 2022, EF SqlServer 10.0.12 | `KB052`, with Docker | +| [KB-053](KB-053-greatest-and-least-need-compatibility-level-160.md) | EF translates `Greatest` and `Least` on SQL Server only from compatibility level 160 | EF SqlServer 10.0.12 | `KB053` | +| [KB-054](KB-054-datetime2-keeps-every-tick-and-forgets-the-kind.md) | `datetime2` keeps every tick of a `DateTime` and reads it back with `Kind` Unspecified | SQL Server 2022, EF SqlServer 10.0.12 | `KB054`, with Docker | diff --git a/design/native-aot.md b/design/native-aot.md index 78bdf5fa..36da59f0 100644 --- a/design/native-aot.md +++ b/design/native-aot.md @@ -39,6 +39,7 @@ host, EF Core through `dotnet ef dbcontext optimize --precompile-queries --nativ unchanged (measured with `DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1`, which leaves the `System.Globalization.Invariant` switch unset). `AsText()` asks once whether a decomposed letter composes, and throws `PlatformNotSupportedException` where it does not; `InvariantGlobalizationTests` - runs the test assembly again in that mode. The smoke test's consumers do not call `AsText()`. + runs the test assembly again in that mode. The smoke test's `aot` consumers do not call `AsText()`; its + `walkthrough` consumer does, on the JIT, without that mode. - **Third-party packages** report their own warnings (NodaTime.Serialization.SystemTextJson 1.4.0, EF Core). The smoke test asserts only that none names a CodoMetis package or a woven value object. diff --git a/design/openapi.md b/design/openapi.md index d17eb87a..3f1927b2 100644 --- a/design/openapi.md +++ b/design/openapi.md @@ -75,7 +75,9 @@ position, and the change lands in the hoisted component. default name is named after its nesting chain (`ShopId`, as its companion class is `ShopIdExtensions`); a name the host's own delegate chose, or `null` for an inlined value object, is kept. A name that is also another type's, `Order.Status` beside a top-level `OrderStatus`, refuses the document - ([decision 55](adr/0055-component-name-claimed-twice-refused.md)). + ([decision 55](adr/0055-component-name-claimed-twice-refused.md)). With `InlineValueObjects`, the wrapper answers + `null` for every value object before the host's delegate, so no value object is a component or claims a name, and + closed sets keep theirs ([decision 115](adr/0115-value-objects-can-be-inlined-everywhere.md)). - **Fault responses** ([decision 49](adr/0049-fault-responses-client-can-act.md)): an operation transformer adds a problem response per status an endpoint answers its fault type with, referring to the problem schema the host's `CreateSchemaReferenceId` names (the shared `ProblemDetails` diff --git a/design/option-result.md b/design/option-result.md index cc443135..07930acb 100644 --- a/design/option-result.md +++ b/design/option-result.md @@ -53,8 +53,9 @@ is the value-less shape. Anything that turns this into a second LanguageExt is o occurrence. It is made by `Fault.Of` alone, and comparing it with a bare reason is refused, since a fault with a detail never equals one ([decision 53](adr/0053-fault-made-by-fault-of-alone.md)); `Reason` is what a caller compares. - A `Cause` beside them joins 2.1 ([decision 52](adr/0052-cause-on-fault-joins-2-1.md)), and so does - `Rule`, the qualified name of the enum or closed-set member that refused, which the problem carries + A `Cause` beside them (2.3.0) is an exception for the log, which equality and printing leave out + and no problem carries ([decision 52](adr/0052-cause-on-fault-joins-2-1.md)). `Rule` (2.1.0) is the + qualified name of the enum or closed-set member that refused, which the problem carries as `rule` ([decision 67](adr/0067-rule-that-refused-on-fault.md)); equality compares the rule's type, since two types' rules can read alike ([decision 72](adr/0072-rule-identity-is-its-type.md)). - **`Fault` and `Fault` (2.1.0)** hold one fault of one of several types, so a @@ -75,7 +76,8 @@ is the value-less shape. Anything that turns this into a second LanguageExt is o non-goals exclude. A zip still inspects every argument, so an uninitialized one after an error throws. Callbacks return `Task` ([decision 30](adr/0030-asynchronous-callbacks-return-task.md)): `TapAsync` and `TapErrorAsync` take a `Func<…, Task>` beside the `Action`, so an `async` lambda is awaited; a `ValueTask` one needs - `.AsTask()`. + `.AsTask()`. `CatchAsync` is no callback and takes a `ValueTask` too, at a lower overload priority, + so an `async` lambda still binds to its `Task` form ([decision 110](adr/0110-catchasync-takes-a-valuetask-at-a-lower-priority.md)). - **A callback runs where the task it waited for completed (decided 2026-10-13, [decision 62](adr/0062-asynchronous-callbacks-run-where-awaited-task-completed.md)).** Every `await` in the package is `ConfigureAwait(false)`, so nothing resumes on the caller's @@ -84,7 +86,19 @@ is the value-less shape. Anything that turns this into a second LanguageExt is o once on it, so in a UI or a Blazor component a callback that touches dispatcher-bound state works or fails by timing. That is documented rather than changed: await the task and call the synchronous combinator, or marshal inside the callback. `AsynchronousContextTests` holds every public member - that returns a task to it. + that returns a task or an async stream to it. +- **Async streams (2.3.0, #128).** `Choose()` and `Choose(selector)` over an `IAsyncEnumerable` + return one, lazily, the caller's token reaching the source through `[EnumeratorCancellation]`. + `SequenceAsync(cancellationToken)` and `TraverseAsync` over one read the next element only after the + one before it succeeded, check a token they are given before each read and each operation, and + dispose the enumerator on every exit. Measured 2026-10-29 against .NET 10's + `System.Linq.AsyncEnumerable` and EF Core 10.0.12: no name binds ambiguously beside its operators, + and a `T[]` or a `DbSet` (which EF Core 10 does not convert to `IAsyncEnumerable`) still binds + to the sequence members. A type that implements both `IEnumerable` and `IAsyncEnumerable` is + ambiguous (CS0121) for `Choose(selector)` and `TraverseAsync`, as it is for the BCL's own `Where`. That + is left as the BCL leaves it, not settled by an overload priority: a silent pick of either reading is + worse than a compile error. CMTK0003 reports a dropped `SequenceAsync` by its type, as it does + `TraverseAsync`, with no change to the analyzer. **Decided 2026-09-27: an uninitialized `Result` throws.** - A `default` result is `State == Uninitialized`. If the branching members tested only diff --git a/design/plan.md b/design/plan.md index 9000757b..9a76a937 100644 --- a/design/plan.md +++ b/design/plan.md @@ -23,6 +23,8 @@ you touch; the rest is reference. | [Fix history](fix-history.md) | Every `fix:` to 2026-10-15 by defect class, finder, sibling chain and release escape: the baseline for reviewing | when the classification is run again | | [Mutation survivors, 2026-10-16](mutation-2026-10-16.md) | The first Stryker.NET run, on the Serilog satellite: counts per package, each survivor read as a missing assertion or an equivalent mutant, and the valid mutants the compile lost | one dated page per run | | [Finder benchmark, 2026-10-16](finder-benchmark-2026-10-16.md) | The 24 release escapes put back as seeds: which test, probe row or nothing detects each, totals per finder as applied, reached and detected, and the accounting every probe round and analyst run reports | one dated page per run | +| [Oracles](../test/oracles/README.md) | What the documents promise as claims held by named tests, every public member claimed or waived, the oracle classes and their seeds, the candidates and the axes a consumer varies ([decision 112](adr/0112-the-prober-gives-way-to-valueranges-oracle-kit.md)) | with each feature and each promise a document gains | +| [The code graph](code-graph.md) | What the codebase-memory graph was measured to find and miss, and how to keep it current; AGENTS.md's "Finding code" rests on it | when the graph is measured again | | [Probing without the tooling](probing-without-tooling.md) | How BLite applied the probing concept by hand, with no prober, covering array or ledger: what each tool function became, the process as it ran, the issues and PRs it produced, and an adoption guide for trying the same here without the tooling | copied 2026-10-18; as the method is tried here | The spikes that decided something are in [spikes/](../spikes/), each with a README stating the diff --git a/design/roadmap.md b/design/roadmap.md index 716f8ba0..11d478ed 100644 --- a/design/roadmap.md +++ b/design/roadmap.md @@ -3,7 +3,8 @@ What is open, and nothing else. Done work is in the [changelog](../CHANGELOG.md) and in git. Each item names the decision that scoped it, or the review that found it. A Warning may learn and a refusal may relax in any minor version; an Error that reports more waits for a major -([decision 34](adr/0034-errors-fixed-within-major-version-warnings.md)). +([decision 34](adr/0034-errors-fixed-within-major-version-warnings.md)), unless only that minor version's +new API can write the form ([decision 118](adr/0118-an-error-may-learn-a-form-only-new-api-can-write.md)). ## 2.2.0, consolidation @@ -15,31 +16,25 @@ defect, a cost or a missing guard may land here; one that adds surface waits for ([decision 108](adr/0108-a-probe-check-earns-credit-against-a-baseline-and-ws2-closes.md)). WS3 (beyond pairs), WS4 (the mutation nightly), the second half of WS5 (the finder benchmark at equal budgets) and WS6 (PostgreSQL in CI) were dropped on 2026-10-17 by the maintainer's decision, and are not scheduled. -- The generated-surface snapshot under "Generators" below. ## 2.3.0, additive Decided, moved to 2.2 when 2.1.0 shipped without them ([decision 88](adr/0088-2-1-0-ships-without-its-open-roadmap-items.md)), and then to 2.3 when 2.2.0 became a consolidation release ([decision 97](adr/0097-2-2-0-is-a-consolidation-release-and-the-gate-binds.md)). -- An optional `Cause` on `Fault`, an exception kept beside the reason and the detail for the - log and the fault feature, never written to the problem; `Fault.Of(reason, detail, cause)`, and a - `MapError` from a `Result` that keeps the exception ([decision 52](adr/0052-cause-on-fault-joins-2-1.md)). -- An opt-in that withholds the `fault` name from the response for chosen members or a whole fault - type, so two causes can share a status without a mapping at the boundary - ([decision 64](adr/0064-fault-name-reaches-client-causes-not-told-apart-one-fault.md)). -- A default value-object key over `ulong` generated as a plain one is, which needs a per-key value - generator a compiled model can write ([decision 63](adr/0063-char-keys-application-assigned-ulong-documented.md)). -- Testing: async assertions. -- `CatchAsync` over a `ValueTask`, and `TraverseAsync` on `Option` and `Patch`. +Its one item, a default `ulong` key, landed ([decision 114](adr/0114-a-value-object-s-temporary-number-is-its-wrapped-type-s.md)); +what else 2.3.0 ships is tracked in its epic. ## Gaps, in any 2.x Small, each closable in a minor version. Found by the reviews named. +- A default `ulong` value-object key in a compiled model made with `--nativeaot`, which writes the type mappings out as + code without the factory their converters' hints carry, or configured with `ValueObjectConverter` by hand, whose + mapping has none: both still throw at `Add` where the column is a `decimal` + ([decision 114](adr/0114-a-value-object-s-temporary-number-is-its-wrapped-type-s.md)). + **Analyzers** -- CMTK0009, a Warning, reports `FirstOrNone`, `SingleOrNone` and `FirstOrError` on an `IQueryable` - given a `Func` or a method group, which binds to the sequence overload and reads every row. - CMTK0003: `WhenAny` over held tasks; an async expression-bodied member in the IDE, where Metalama has not run; and decide whether a tap on a property or an indexer, and a result a cache's `GetOrAdd` still holds, are drops, which it reports today. @@ -49,11 +44,6 @@ Small, each closable in a minor version. Found by the reviews named. **Generators** - A C# keyword in a namespace or a member name fails with LAMA0041, not a CMTK error. -- Package validation compares the packages' own public surface, not what the generators write into a - consumer: a `Parse` that 2.1.0's prerelease stopped generating (a wrapped `Parse` returning a type with - an implicit conversion) broke a consumer's call with CS0117 and passed ApiCompat. Snapshot the - generated public members of the test corpus' value objects, and fail a change that removes one - without a decision (review of 2026-10-14). **Run time** - The Serilog policy ignores `ToMaximumStringLength`. @@ -68,12 +58,24 @@ Small, each closable in a minor version. Found by the reviews named. converters' trimming warnings decide whether it is a package ([decision 71](adr/0071-converters-say-what-they-write.md)). - A stable external name for a closed-set member, apart from its C# identifier: parsing, JSON, EF, OpenAPI and the fault identifiers at once (proposed in the review of 2026-10-13). -- The Metalama 2027.0 upgrade, once it is stable (`2027.0.4-preview` on 2026-10-14; - [decision 4](adr/0004-metalama-2026-1-x-latest-stable.md)): a minor if a consumer's own aspects beside - the packed generators still build and weave, measured as the fabric spike measured 2026.1 against the - 2027.0 preview, and a major otherwise; `[Durable]` returns to the `_value` placeholder with it. -- A `net11.0` target beside `net10.0`, in a 2.x minor when a feature needs it ([decision 44](adr/0044-2-0-0-net10-0-metalama.md)): EF Core 11 - and Microsoft.AspNetCore.OpenApi 11 in the satellites, and a Native AOT smoke run per target. +- The Metalama 2027.0 upgrade, in 2.5.0 (#173), once 2027.0 is stable (`2027.0.4-preview` on 2026-10-14; + [decision 4](adr/0004-metalama-2026-1-x-latest-stable.md)), for both targets at once and with 2.3 and 2.4 on + 2026.1 ([decision 120](adr/0120-a-net11-0-target-and-documented-floor-raises-join-a-minor.md)). It raises a floor in + a minor, which decision 120 allows when documented: a project that references `Metalama.Framework` 2026.1 + directly gets NU1605 and stays on TypeKit 2.4.x or raises its reference, and a woven `net11.0` project moves + from C# 14 to 15. Its conditions: a build with no Metalama license passes in CI and a clean container on 2027 + (#170), the weaving review and the smoke test run on it, the changelog and the Generators README name the + ceiling, the language-version change, the supported IDEs and what Metalama 2027 drops, and the release notes + say whether 2.4.x is patched. `[Durable]` returns to the `_value` placeholder with it. +- A `net11.0` target beside `net10.0`, in a 2.x minor when a feature needs it ([decision 44](adr/0044-2-0-0-net10-0-metalama.md)), + on the terms of [decision 120](adr/0120-a-net11-0-target-and-documented-floor-raises-join-a-minor.md): a higher + floor in the `net11.0` group (EF Core 11 and Microsoft.AspNetCore.OpenApi 11 in the satellites) allowed when the + changelog names it, who it reaches (an application that holds the lower version gets NU1605) and both ways out, + and the smoke test shows it; the same generated members on every target; `LangVersion` 14 for `net10.0` and 15 for + `net11.0` once it uses a C# 15 feature, a union a new `net11.0`-only member among them; the tests, a Native AOT + smoke run and package validation per target. First the spike on an SDK 11 + compiler, no release attached (#170): `global.json` pins SDK 10.0.4xx because the Roslyn band changes what + Metalama emits, and the SDK is generally available in November 2026. ## Blocked upstream @@ -93,8 +95,11 @@ Small, each closable in a minor version. Found by the reviews named. warning ([decision 90](adr/0090-faults-alike-are-a-warning-until-3-0.md)). - CMTK1023, a base record's equality that the generated one does not call, becomes an Error ([decision 75](adr/0075-equality-over-base-record-generated.md)). -- C# 15 unions and the `net10.0` drop, timed to .NET 12 LTS (November 2027) or to the end of .NET - 10's life, whichever its consumers need ([decision 44](adr/0044-2-0-0-net10-0-metalama.md)). The compiler-made union types have a public +- The `net10.0` drop, and a change of `Option` or `Result` into C# 15 unions, timed to .NET 12 LTS (November + 2027) or to the end of .NET 10's life, whichever its consumers need ([decision 44](adr/0044-2-0-0-net10-0-metalama.md), + kept for .NET 11 by [decision 120](adr/0120-a-net11-0-target-and-documented-floor-raises-join-a-minor.md): .NET 11 + is not a reason for a major, and a change to a member both targets have is). A union that is new on `net11.0` + is an addition, and may join a 2.x minor. The compiler-made union types have a public `object? Value` and are serialized by System.Text.Json, so `Option` and `Result` take part in their - pattern matching only after a spike shows neither returns, on a branch of its own with no release - attached. + pattern matching, in either release, only after a spike shows neither returns, on a branch of its own with no + release attached. diff --git a/design/test-strategy.md b/design/test-strategy.md index 7e1da4d4..4a85ae9b 100644 --- a/design/test-strategy.md +++ b/design/test-strategy.md @@ -55,7 +55,7 @@ attributes, implementations reached through another declaration, methods bound by pattern, crefs and `nameof`, and generated code, each run with an argument that breaks the rule's constraint and one that meets it. CMTK0010 runs it under three constraints (`TypeArgumentConstraintVariantTests`). For a - generated type the class is the `shape.*` axes of `.claude/probe-axes.md`, for a framework option the + generated type the class is the declaration shapes of `test/oracles/axes.md`, for a framework option the settings axes. Measured: at the origin of CMTK0010's chain of fourteen fixes the catalogue fails on 20 sites, each fixed by a later member of the chain; seeded, without the rule's method-group and property registrations, 15 cases fail. diff --git a/docs/aspnet-core.md b/docs/aspnet-core.md index eb69da25..f2c6af01 100644 --- a/docs/aspnet-core.md +++ b/docs/aspnet-core.md @@ -246,6 +246,43 @@ string. A client generated from the document then has the member it matches on. ends up in the value object's shared component, and from there it applies to every other use of that value object. +## Describe value objects as plain types + +Some clients work better without a type per value object. A generator that turns each component into a +class gives you a `CustomerId` wrapper where you wanted a `string`. Turn on `InlineValueObjects`, and +every value object is described in place by the schema of the type inside it: + +```csharp +builder.Services.AddOpenApi(options => options.AddTypeKit(typeKit => typeKit.InlineValueObjects = true)); +``` + +The `CustomerDto` from above then has plain properties, and the document has no `CustomerId` or `Email` +component: + +```json +"CustomerDto": { "type": "object", "properties": { + "id": { "type": "string", "format": "uuid" }, + "email": { "type": "string" }, + "name": { "type": "string" } } } +``` + +The same goes for every place a value object appears: a body, a response, a list's elements, a +dictionary's values, a route, query or header parameter, and anything nested in those. Each one gets +exactly the schema the underlying type gets in that place. + +**Good to know** + +- **Without the setting, nothing changes.** `AddTypeKit()` writes the same document as before, with a + component per value object. +- **A nullable value object allows null in its own schema**, as a nullable property of the underlying + type does. That holds under OpenAPI 2.0 too, where a nullable property of a component loses its type. +- **Closed sets keep their components**, and so does an enum inside a value object. The value object's + schema holds the enum's content in place. +- **Two value objects with one name no longer clash.** Neither has a component, so neither needs a + name of its own. +- **Your own `CreateSchemaReferenceId` still names your other types** when you set it before + `AddTypeKit`. One set after `AddTypeKit` replaces TypeKit's naming, and this setting with it. + ## Native AOT The transformer and `ToHttpResult()` are trim- and AOT-safe, and the smoke test publishes a host diff --git a/docs/comparison.md b/docs/comparison.md index b3839b83..a14ce08b 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -13,6 +13,8 @@ Every fact here was read from each library's source, at the release named in the where one is in preview. - **13 October 2026** for closed sets. - **14 October 2026**, again, for OpenAPI and Native AOT. +- **29 October 2026** for SuperStrong.Types. Again for Thinktecture.Runtime.Extensions, at 10.6.0, and + LanguageExt, at 5.0.0-beta-78. And again for TypeKit's own columns, at 2.3.0. What was read for each library, and when, is kept in the repository's [verified facts](../design/knowledge/README.md). Each table below lists the entries it rests on. @@ -25,7 +27,8 @@ If something here is wrong or has changed, please open an issue. |---|---|---| | .NET Framework, .NET Standard or a .NET before 10 | Vogen for value objects; CSharpFunctionalExtensions, ErrorOr or FluentResults for results | TypeKit targets .NET 10 only. These ship for .NET Standard 2.0 | | Newtonsoft.Json, Dapper, LINQ to DB, MongoDB, Orleans, ServiceStack or protobuf-net for value objects | Vogen | It ships an opt-in integration for each. TypeKit covers System.Text.Json, EF Core and ASP.NET Core | -| A plain Roslyn source generator in the build rather than Metalama | Vogen or Thinktecture.Runtime.Extensions | TypeKit's generators run on Metalama (open source, no license key) in the project that declares value objects | +| GraphQL with Hot Chocolate, or Newtonsoft.Json, for value objects on .NET 10 | SuperStrong.Types | It ships a package for each. TypeKit covers System.Text.Json, EF Core and ASP.NET Core | +| A plain Roslyn source generator in the build rather than Metalama | Vogen, Thinktecture.Runtime.Extensions or SuperStrong.Types | TypeKit's generators run on Metalama (open source, no license key) in the project that declares value objects | | Value objects of several members, or discriminated unions | Thinktecture.Runtime.Extensions | It has `[ComplexValueObject]` and `[Union]`. A TypeKit value object wraps exactly one value. Its closed sets are the smart enums | | Several errors per failure, of typed kinds | ErrorOr | An `ErrorOr` carries a list of errors. A TypeKit `Result` carries one `TError`, which can be a list of your own | | A cause nested under an error, metadata per occurrence, or success messages | FluentResults | Its results carry reasons, errors and successes. An error can name what caused it, and both take metadata. A TypeKit `Result` carries one `TError`. A `Fault` is one reason from a fixed set, with a sentence about the occurrence and the rule that rejected the value. A closed-set fault carries a kind and constant data. `Catch` carries an exception as the whole error. Nothing attaches a cause to a fault, and a success carries only its value | @@ -33,39 +36,41 @@ If something here is wrong or has changed, please open an issue. | Unions of arbitrary types | OneOf | `OneOf` is general-purpose. TypeKit has only `Option` and `Result` | | A `Result` on the wire | CSharpFunctionalExtensions | It ships opt-in System.Text.Json converters for its `Result`. TypeKit rejects serializing `Option` and `Result`, by design | | A smart enum whose JSON or column is a number, or whose wire name differs from its C# identifier | Ardalis.SmartEnum, Intellenum or Thinktecture.Runtime.Extensions | A TypeKit closed set is its member's name everywhere, and the name is the field's | -| Only your own closed set of outcomes, on .NET 11 | C#'s union types | They arrive with .NET 11 | -| Years of production use behind the library | Any of the above | TypeKit is new: 1.0.0 shipped in September 2026 | +| Only your own closed set of outcomes, on .NET 11 | C# 15's union types or closed hierarchies | They need no preview flag in the .NET 11 SDK, which was still a release candidate on 29 October 2026 | +| A long release history behind the library | LanguageExt, OneOf, CSharpFunctionalExtensions, FluentResults, Ardalis.SmartEnum, Thinktecture.Runtime.Extensions or Vogen | Each was first published on NuGet between 2015 and 2021. TypeKit is new: 1.0.0 shipped in September 2026, and SuperStrong.Types 1.0.0 in July 2026 | -Sources: FluentResults [KB-018](../design/knowledge/KB-018-fluentresults.md), OneOf [KB-019](../design/knowledge/KB-019-oneof.md), and the entries under each table below. +Sources: FluentResults [KB-018](../design/knowledge/KB-018-fluentresults.md), OneOf [KB-019](../design/knowledge/KB-019-oneof.md), first releases [KB-045](../design/knowledge/KB-045-first-releases-of-the-compared-libraries.md), C# 15 [KB-046](../design/knowledge/KB-046-csharp-15-unions-ship-with-the-net-11-sdk.md), and the entries under each table below. ## Value objects in detail -| | TypeKit 2.1.0 | Vogen 8.0.7 (the 9.0.0-beta.2 tag the same; not on NuGet) | Thinktecture.Runtime.Extensions 10.5.0 (OpenAPI and AOT: 10.5.1) | StronglyTypedId 1.0.0-beta08 | -|---|---|---|---|---| -| Declared by | Implementing `IValue` or `IValidatedValue<...>` on a `partial record` | `[ValueObject]` on a partial type | `[ValueObject]`, `[ComplexValueObject]` or `[SmartEnum]` on a partial type | `[StronglyTypedId]` on a partial struct | -| Validation | `Create` returns a `Result`, and every generated way of creating a value runs it | `Validate` returns a `Validation`. `From` throws | A `ValidateFactoryArguments` partial method | None | -| EF Core | `UseTypeKit()` maps every value object, nothing per type. `.Value` in a query translates to the column | A generated converter per value object, which you register. Comparing with a primitive in a query is an error (VOG034) | Packages per EF Core version (8, 9, 10) | Through a template of your own | -| OpenAPI | Microsoft.AspNetCore.OpenApi: the schema ASP.NET publishes for the underlying type, under the host's JSON options | Opt-in, off by default: a generated schema transformer, with type and format from a built-in table. Swashbuckle support | Swashbuckle filters only, with type and format from Swashbuckle's table | None built in | -| A JSON converter the host registers, such as a decimal written as a string | Described as it writes, wherever it applies. Either the converter says what it writes (`IDescribedJsonConverter`), or the options say it for that converter (`JsonSchemaDescriptions`). A document that would describe different JSON from what the controllers write is rejected | Not reflected: the table's type and format stay | Not reflected for type and format. A smart enum's listed values are written with the host's options | None built in | -| Native AOT | Declared (`IsAotCompatible`). On every CI build, a consumer with value objects, JSON, OpenAPI and EF Core is published with Native AOT | Not declared. Its docs say the Swashbuckle schema filter needs reflection that Native AOT lacks | Not declared | Not declared | -| JSON | System.Text.Json, byte for byte the underlying type's JSON | System.Text.Json by default. Newtonsoft.Json and more, opt-in | System.Text.Json, Newtonsoft.Json, MessagePack | System.Text.Json (more through templates) | -| Targets | `net10.0` | `netstandard2.0`, .NET Framework projects included | `net8.0`, `net9.0`. The ASP.NET Core and Swashbuckle packages `net8.0` only | `netstandard2.0` | -| Generated by | Metalama, in the declaring project | A Roslyn source generator | Roslyn source generators | A Roslyn source generator | -| License | MIT | Apache-2.0 | BSD-3-Clause | MIT | +| | TypeKit 2.3.0 | Vogen 8.0.7 (the 9.0.0-beta.2 tag the same; not on NuGet) | Thinktecture.Runtime.Extensions 10.6.0 | StronglyTypedId 1.0.0-beta08 | SuperStrong.Types 1.0.0 | +|---|---|---|---|---|---| +| Declared by | Implementing `IValue` or `IValidatedValue<...>` on a `partial record` | `[ValueObject]` on a partial type | `[ValueObject]`, `[ComplexValueObject]` or `[SmartEnum]` on a partial type | `[StronglyTypedId]` on a partial struct | `[StrongType]` on a partial class, not a record or a struct | +| Validation | `Create` returns a `Result`, and every generated way of creating a value runs it | `Validate` returns a `Validation`. `From` throws | A `ValidateFactoryArguments` partial method | None | A `Define()` method built from validators such as `IsPositive()`. `From` throws, `TryFrom` returns `false`, and JSON, parsing, the type converter and EF Core reads run it | +| EF Core | `UseTypeKit()` maps every value object, nothing per type. `.Value` in a query translates to the column | A generated converter per value object, which you register. Comparing with a primitive in a query is an error (VOG034) | Packages per EF Core version (8, 9, 10) | Through a template of your own | `UseStrongTypes()` maps every strong type, nothing per type. `AsPrimitive()` in a query translates to the column | +| OpenAPI | Microsoft.AspNetCore.OpenApi: the schema ASP.NET publishes for the underlying type, under the host's JSON options | Opt-in, off by default: a generated schema transformer, with type and format from a built-in table. Swashbuckle support | Swashbuckle filters only, with type and format from Swashbuckle's table | None built in | Microsoft.AspNetCore.OpenApi, opt-in: the schema ASP.NET publishes for the underlying type, inline or as a named component | +| A JSON converter the host registers, such as a decimal written as a string | Described as it writes, wherever it applies. Either the converter says what it writes (`IDescribedJsonConverter`), or the options say it for that converter (`JsonSchemaDescriptions`). A document that would describe different JSON from what the controllers write is rejected | Not reflected: the table's type and format stay | Not reflected for type and format. A smart enum's listed values are written with the host's MVC or Minimal API JSON options | None built in | Nothing of its own describes it: the schema is the one ASP.NET publishes for the underlying type. Read from its code, not run | +| Native AOT | Declared (`IsAotCompatible`). On every CI build, a consumer with value objects, JSON, OpenAPI and EF Core is published with Native AOT | Not declared. Its docs say the Swashbuckle schema filter needs reflection that Native AOT lacks | Not declared | Not declared | Not declared | +| JSON | System.Text.Json, byte for byte the underlying type's JSON | System.Text.Json by default. Newtonsoft.Json and more, opt-in | System.Text.Json, Newtonsoft.Json, MessagePack | System.Text.Json (more through templates) | System.Text.Json, the underlying type's JSON. Newtonsoft.Json, opt-in | +| Other integrations | Serilog, and test assertions. Nothing for another serializer, ORM or GraphQL | Opt-in packages for Newtonsoft.Json, Dapper, LINQ to DB, MongoDB, Orleans, ServiceStack and protobuf-net | Packages for ASP.NET Core, Swashbuckle, Newtonsoft.Json, MessagePack and Serilog | Templates for EF Core, Newtonsoft.Json and others | Packages for Hot Chocolate (GraphQL) and Newtonsoft.Json | +| Targets | `net10.0` | `netstandard2.0`, .NET Framework projects included | `net8.0`, `net9.0`. The ASP.NET Core and Swashbuckle packages `net8.0` only | `netstandard2.0` | `net10.0` | +| Generated by | Metalama, in the declaring project | A Roslyn source generator | Roslyn source generators | A Roslyn source generator | A Roslyn source generator | +| License | MIT | Apache-2.0 | BSD-3-Clause | MIT | MIT | Sources: Vogen [KB-010](../design/knowledge/KB-010-vogen-declaring-validating-storing.md) and [KB-011](../design/knowledge/KB-011-vogen-openapi-and-native-aot.md), Thinktecture.Runtime.Extensions [KB-012](../design/knowledge/KB-012-thinktecture-value-objects.md) and [KB-013](../design/knowledge/KB-013-thinktecture-openapi-and-native-aot.md), -StronglyTypedId [KB-014](../design/knowledge/KB-014-stronglytypedid.md). +StronglyTypedId [KB-014](../design/knowledge/KB-014-stronglytypedid.md), SuperStrong.Types [KB-044](../design/knowledge/KB-044-superstrong-types.md). **A gap none of them fills.** ASP.NET Core and System.Text.Json's schema exporter can't see through a JSON converter you write yourself ([KB-023](../design/knowledge/KB-023-a-converter-cannot-describe-its-schema.md)). They write it as the empty schema, or with only the `format` its CLR type maps to. The serializer has a hook for a converter to describe itself, but it is internal. The proposal to make it public ([dotnet/runtime#105769](https://github.com/dotnet/runtime/issues/105769)) -is open, without a release. None of the libraries above fills that gap, so with any of them, a -converter you register means describing it in the document by hand, type by type. +is open, without a release. None of the value-object libraries in the table above, at the versions +named, has code of its own that describes such a converter. So with any of them, a converter you +register means describing it in the document by hand, type by type. ## Option and Result in detail -| | TypeKit 2.1.0 | LanguageExt 4.4.9 (5.0.0-beta-77) | CSharpFunctionalExtensions 3.7.0 | ErrorOr 2.1.1 | +| | TypeKit 2.3.0 | LanguageExt 4.4.9 (5.0.0-beta-78) | CSharpFunctionalExtensions 3.7.0 | ErrorOr 2.1.1 | |---|---|---|---|---| | Reaching the value | `Match`, `TryGetValue` and the combinators. No `.Value` | `Match`, `IfNone` and the like. No public `.Value` on `Option` | `.Value`, which throws on a failure | `.Value`, which returns `default` on an error | | A `default` instance | `Option`: `None`. `Result`: throws when used, and fails the build when written in code (CMTK0001) | `Option`: `None` | `Maybe`: none. `Result`: a success holding `default(T)` | A success holding `default(T)` | @@ -77,11 +82,11 @@ Sources: LanguageExt [KB-015](../design/knowledge/KB-015-languageext-option.md), ## Closed sets in detail -| | TypeKit 2.1.0 | Ardalis.SmartEnum 8.2.0 | Intellenum 2.0.1 | Thinktecture.Runtime.Extensions 10.5.1 | +| | TypeKit 2.3.0 | Ardalis.SmartEnum 8.2.0 | Intellenum 2.0.1 | Thinktecture.Runtime.Extensions 10.6.0 | |---|---|---|---|---| | Declared by | Implementing `IClosedSet` on a `sealed partial class`, one `public static readonly` field per member | Deriving from `SmartEnum`, each member `new("Name", value)` | `[Intellenum]` on a partial class. Members as fields, `[Member]` attributes or a static constructor | `[SmartEnum]` on a partial class, each member `new("KEY")` | | Data on a member | Properties, set through the private constructor | The subclass's own fields | One underlying value. Richer data through a custom underlying type | Declared properties, which become constructor parameters | -| Deciding by member | `Match`, one required delegate per member, every argument named (CMTK0012) | `When(...).Then(...)`. Nothing requires every member | A `switch` over `.Value` with generated constants. Nothing requires every member | Generated `Switch` and `Map`, every member a required argument | +| Deciding by member | `Match`, one required delegate per member, every argument named (CMTK0012) | `When(...).Then(...)`. Nothing requires every member | A `switch` over `.Value` with generated constants. Nothing requires every member | Generated `Switch` and `Map`, every member a required argument. Overloads that leave members to a default, on request | | Parsing by name | `Parse` and `TryParse`, ordinal. `ignoreCase: true` on request, and two members differing only in case fail the build (CMTK1021) | `FromName` and `TryFromName`, ordinal. `ignoreCase` on request. A case clash throws at the first such lookup | `FromName` and `TryFromName`, ordinal. `ignoreCase` on request, and the first declared member answers | `Get` and `TryGet`, ignoring case by default. A case clash throws at the first lookup | | JSON | System.Text.Json, the name, nothing registered | A separate package per serializer, a converter attribute per type. The name or the value | Generated into your assembly. The value, as an `enum` writes it | A separate package per serializer. The key | | EF Core | `UseTypeKit()`, a text column of the name, nothing per set | A separate package, a convention or per model. The value | An opt-in generated converter, registered per property. The value | A separate package per EF version, `UseThinktectureValueConverters()`. The key | diff --git a/docs/ef-core.md b/docs/ef-core.md index a2f5159c..9e31d8e9 100644 --- a/docs/ef-core.md +++ b/docs/ef-core.md @@ -130,9 +130,11 @@ A composite key of value objects is `HasKey(x => new { x.Order, x.Line })`, as f - **A key over `char` is always yours to assign**, on every provider, since none generates one. A default `'\0'` is stored as it is, so assign a meaningful character. -- **A key over `ulong` needs `ValueGeneratedNever()` on SQL Server and PostgreSQL.** Their column is a - `decimal`, so adding the entity with the key's default value throws `OverflowException` at `Add`. - Configure `ValueGeneratedNever()` and assign the key. +- **Under Native AOT, a key over `ulong` needs `ValueGeneratedNever()` on SQL Server and PostgreSQL.** + Their column is a `decimal`, and in a compiled model made with `--nativeaot`, adding the entity with + the key's default value throws `OverflowException` at `Add`. The same goes for a key you configure + with `ValueObjectConverter` yourself. Configure `ValueGeneratedNever()` and assign the key. + Otherwise it gets a temporary value, as a plain `ulong` key does. - **Inheritance hierarchies.** The root of a TPH or TPT hierarchy is generated too. Under TPC, as for an `int` key, it's generated only where the provider has a sequence for the hierarchy (PostgreSQL, SQL Server), not on SQLite. @@ -221,7 +223,7 @@ To get one element of a query as an option, use `FirstOrNone`, `SingleOrNone` or - **Write the condition as a lambda at the call**, or hold it as an `Expression>`. Only then does it run in the database. A `Func` variable or a method group binds the synchronous methods to their overloads on a sequence. Those read every row of the table and run the condition - in memory, without a warning. + in memory, and the analyzer warns at the call (CMTK0009). ## Compiled models @@ -264,8 +266,8 @@ The block under [Keys](#keys) is the whole setup: EF's in-memory provider won't work. It has no relational type mapping, so nothing is mapped there. -This repository tests the mapping on SQLite and PostgreSQL, and every family of underlying types makes -a round trip through a real PostgreSQL. +This repository tests the mapping on SQLite, PostgreSQL and SQL Server, and every family of underlying +types makes a round trip through a real PostgreSQL and a real SQL Server. ## If something does not map diff --git a/docs/migrating-to-2.md b/docs/migrating-to-2.md index 8ec91c0a..3d0934f1 100644 --- a/docs/migrating-to-2.md +++ b/docs/migrating-to-2.md @@ -159,8 +159,9 @@ key column. - **On SQL Server**, the migrations generator rejects the change for all four types ("To change the IDENTITY property of a column, the column needs to be dropped and recreated"). - **A key over `ulong`** isn't generated on PostgreSQL, because `numeric(20,0)` has no identity. - Adding an entity with a default `ulong` key throws `OverflowException` at `Add`, on PostgreSQL and - SQL Server alike, where 1.5 stored 0. + From 2.0.0 to 2.2.0, adding an entity with a default `ulong` key threw `OverflowException` at + `Add`, on PostgreSQL and SQL Server alike, where 1.5 stored 0. From 2.3.0 it gets a temporary value, + as a plain `ulong` key does, except in a compiled model made with `--nativeaot`. To keep 1.5's behaviour for any of these keys, configure `ValueGeneratedNever()` on it and assign the key yourself. diff --git a/docs/option.md b/docs/option.md index 17b08be5..82be3447 100644 --- a/docs/option.md +++ b/docs/option.md @@ -52,8 +52,8 @@ compile, because a query has no last row until you order it. The predicate runs in the database only when it is an expression, which a lambda written at the call is. A `Func` held in a variable, or a method group, binds to the sequence overload instead. -That reads every row and checks the condition in memory, with no warning. Pass an -`Expression>` there. +That reads every row and checks the condition in memory, and the analyzer warns at the call +(CMTK0009). Pass an `Expression>` there. **Good to know** @@ -189,6 +189,7 @@ elements `Choose(selector)` reads may be null, and only what the selector finds | `GetValueOrNone(key)` | `IReadOnlyDictionary` | `Option` | | `Choose(selector)` | `IEnumerable`, its elements nullable or not | the values the selector answers `Some` for, in order and lazily: a filter and a map in one | | `Choose()` | `IEnumerable>` | the values of the options that hold one, in order and lazily | +| `Choose(selector)`, `Choose()` | `IAsyncEnumerable`, `IAsyncEnumerable>` | the same, as an `IAsyncEnumerable` of the values as they arrive; the token given to `WithCancellation` reaches the stream | | `IsSome()`, `IsNone()` | `Option` | `bool` | | `Match(onSome, onNone)` | `Option` | what the branch returns | | `TryGetValue(out value)` | `Option` | `bool`; `value` is non-null when true | diff --git a/docs/result.md b/docs/result.md index 3aca7781..0ba9d4f0 100644 --- a/docs/result.md +++ b/docs/result.md @@ -230,10 +230,15 @@ token before each operation and hands it on. A cancellation propagates as an exc become an error. When the operations may all run at once, use `Task.WhenAll` over their tasks and `Sequence()` over what it returns. +On an `Option` or a `Patch`, `TraverseAsync` runs the operation once, when there is a value. `None`, +a clear and an absent field call nothing and pass through. + ```csharp Result, CheckoutFault> reserved = await carts.TraverseAsync(checkout.ReserveAsync); // one at a time, in order; nothing starts after a fault Result charged = await totals.TraverseAsync(checkout.ChargeAsync); // a command per element, likewise Result, CheckoutFault> found = await ids.TraverseAsync(checkout.LoadCartAsync, cancellationToken); // the token checked before each, and handed to it +Result, CheckoutFault> saved = await savedCart.TraverseAsync(checkout.FindCartAsync); // None calls nothing; a value is looked up +Result, CheckoutFault> moved = await movedTo.TraverseAsync(checkout.FindCartAsync); // a clear or an absence passes through ``` ## An exception as an outcome @@ -257,7 +262,7 @@ An exception is for the code that called, not for a client. `ToHttpResult()` doe - **Name both type arguments to narrow.** `Catch` spells both, since C# infers all type arguments or none. - **Use `CatchAsync` for a task.** It awaits the task, so an exception thrown after the task's first - `await` is the error too. Passing a delegate that returns a `Task` to `Catch` fails to compile, and + `await` is the error too. A `ValueTask` works the same way: `Result.CatchAsync(() => stream.ReadAsync(buffer))`. Passing a delegate that returns a `Task` to `Catch` fails to compile, and the error names `CatchAsync`. The task's exception would otherwise be thrown after `Catch` had returned. - **A cancellation is not an outcome.** An `OperationCanceledException` passes through every @@ -311,15 +316,18 @@ throughout. | implicit from `T`, `Success`, `Error` | `Result` | a bare value is a success; a bare error is not, since it could be either | | implicit from `TError`, `Success`, `Error` | `Result` | a bare error converts, and so does the marker | | `Catch(action)`, `Catch(action)`, `Catch(action)` | `Result` | `Result` or `Result`; narrowed to one exception type | -| `CatchAsync(action)` and the same type-argument forms | `Result` | the same, awaiting the task | +| `CatchAsync(action)` and the same type-argument forms | `Result` | the same, awaiting the `Task` or `ValueTask` | | `ToResult(error)` | `Option` | `Result` | | `FirstOrError(predicate, error)`, `LastOrError(predicate, error)` | `IEnumerable`; `FirstOrError` on `IQueryable` too | the first or last match, or the error; on a query the provider runs the predicate, and `LastOrError` is a compile error | | `Traverse(selector)` | `IEnumerable` | `Result, TError>`, or `Result` for a command selector; stops at the first error | | `Traverse(selector)` | `Option` | `Result, TError>`: an optional input validated when present | | `Traverse(selector)` | `Patch` | `Result, TError>`: a patch field validated when it carries a value; a clear or an absence passes through | | `TraverseAsync(selector)`, `TraverseAsync(selector, cancellationToken)` | `IEnumerable` | as `Traverse`, one operation awaited at a time; nothing starts after an error, or once the token is cancelled | +| `TraverseAsync(selector)`, `TraverseAsync(selector, cancellationToken)` | `IAsyncEnumerable` | as over `IEnumerable`; nothing more is read after an error, and the token reaches the stream too | +| `TraverseAsync(selector)`, `TraverseAsync(selector, cancellationToken)` | `Option`, `Patch` | as `Traverse`, the operation awaited; nothing is called without a value | | `Transpose()` | `Option>`, `Result, TError>` | the one turned inside out: `None` is a success without a value, an error stays the error | | `Sequence()` | `IEnumerable>`, `IEnumerable>` | the list of values, or the first error | +| `SequenceAsync()`, `SequenceAsync(cancellationToken)` | `IAsyncEnumerable>`, `IAsyncEnumerable>` | the same, awaited; nothing more is read after the first error, and the token reaches the stream | **Reaching the outcome** diff --git a/docs/testing.md b/docs/testing.md index 9b8cb278..8c670f85 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -167,3 +167,5 @@ The analyzers run in test projects as they do everywhere else. Neither is a `default` assigned to the `out` parameter of a `Try` method. - **A dropped `Result` is reported (CMTK0003)**, in a test too. Write `_ = ProductCode.Create(input);` to show the drop is meant. +- **So is an assertion on a task you don't await (CMTK0003).** `service.FindAsync(id).ShouldBeSome();` + without `await` passes whatever the task holds. diff --git a/docs/value-objects.md b/docs/value-objects.md index 76f8cc6f..d31036e4 100644 --- a/docs/value-objects.md +++ b/docs/value-objects.md @@ -8,7 +8,9 @@ This page helps you design them: pick a struct or a class, write the validation, error carries, and call the right factory. The [Generators README](../src/CodoMetis.TypeKit.Generators/README.md) lists everything that is generated, member by member, and every build error. The interfaces themselves are in the -[base README](../src/CodoMetis.TypeKit/README.md#value-objects). +[base README](../src/CodoMetis.TypeKit/README.md#value-objects). To see value objects cross every +boundary of one service, from the request to the database, the log and back, read the +[Walkthrough](walkthrough.md). Reference `CodoMetis.TypeKit.Generators` in the project that declares value objects. The examples import `CodoMetis.TypeKit` and `CodoMetis.TypeKit.ValueObjects`. diff --git a/docs/walkthrough.md b/docs/walkthrough.md new file mode 100644 index 00000000..5d4e2304 --- /dev/null +++ b/docs/walkthrough.md @@ -0,0 +1,369 @@ +# Walkthrough: one orders service + +This page follows one small orders service from the request to the database and back. Each guide +covers one boundary. Here you see one e-mail address cross all of them, and at each step what +happens to it, or that nothing does. + +The service is a minimal API on SQLite. It uses the generators, the EF Core package, the ASP.NET Core +package and the Serilog package. Its code is a sample application. CI runs it on every change, and +builds it from the packed packages too, so every answer below is one it really gives: +[Orders.cs](../tools/ConsumerSmokeTest/Consumers/walkthrough/Orders.cs), +[OrderEndpoints.cs](../tools/ConsumerSmokeTest/Consumers/walkthrough/OrderEndpoints.cs), +[Program.cs](../tools/ConsumerSmokeTest/Consumers/walkthrough/Program.cs), and the requests in +[Tour.cs](../tools/ConsumerSmokeTest/Consumers/walkthrough/Tour.cs). + +| Step | What happens to the e-mail address | +|---|---| +| [Declare the values](#declare-the-values) | `Create` trims it, composes it and checks it, once | +| [Store them](#store-them) | it becomes a plain `TEXT` column | +| [Place an order](#place-an-order) | JSON reads it through `Create` and writes it back as a string. The log masks it | +| [Reject what doesn't fit](#reject-what-doesnt-fit) | a rejected value gets a 400 that never quotes it | +| [Find an order](#find-an-order) | nothing: an `Option` becomes a 404 | +| [Query on `.Value`](#query-on-value) | nothing: `.Value` is the column in SQL | +| [Change an order](#change-an-order) | a `Patch` reads each field through its own `Create` | +| [Read the OpenAPI document](#read-the-openapi-document) | it is described as the string it is | + +## Declare the values + +```csharp +public readonly partial record struct OrderId : IValue; + +public enum EmailFault { Blank, Hidden, NoAt } + +public readonly partial record struct Email : IValidatedValue +{ + public static Result Create(string value) => + value.AsText() // trimmed, composed, no hidden characters + .MapError(fault => fault == TextFault.Blank ? EmailFault.Blank : EmailFault.Hidden) + .Ensure(text => text.IndexOf('@') > 0, EmailFault.NoAt) + .Map(text => new Email(text)); + + public override string ToString() => $"{Value[0]}***{Value[Value.IndexOf('@')..]}"; // what a log shows +} + +public enum QuantityFault { NotPositive } + +public readonly partial record struct Quantity : IValidatedValue +{ + public static Result Create(int value) => + value > 0 ? new Quantity(value) : Result.Error(QuantityFault.NotPositive); +} + +public sealed partial class OrderStatus : IClosedSet +{ + public static readonly OrderStatus Placed = new(); + public static readonly OrderStatus Shipped = new(); + public static readonly OrderStatus Cancelled = new(); +} +``` + +These are the service's whole domain: + +- **`OrderId`** wraps a `Guid`. `OrderId.New()` makes a version 7 one. +- **`Email`** is validated. Its `Create` runs the text guard `AsText()`, which trims the address, + composes it to one spelling and rejects hidden characters. Then it checks for an `@`. Every way an + `Email` gets created runs this `Create`: JSON, a route or query value, `TryFrom`. +- **`Email.ToString()`** masks the address. The generators keep a `ToString()` you write, and every + log line prints it. +- **`Quantity`** rejects zero and less. +- **`OrderStatus`** is a closed set: an enum whose members are instances, written as their names. + +The [Value objects](value-objects.md), [Written text](text.md) and [Closed sets](closed-sets.md) +guides cover each kind. + +## Store them + +```csharp +public sealed class Order +{ + public required OrderId Id { get; init; } + public required Email Email { get; set; } + public required Quantity Quantity { get; set; } + public required OrderStatus Status { get; set; } +} + +public sealed class OrdersDb(DbContextOptions options) : DbContext(options) +{ + public DbSet Orders => Set(); +} +``` + +The entity uses the value objects as its properties, with nothing to register per type. +`Program.cs` sets up the host. `UseTypeKit()` maps each value object to a column of the type inside +it, and `EnsureCreated()` shows the table that comes of it. `MapOrders()` maps the endpoints the next +sections show. + +```csharp +using CodoMetis.TypeKit.AspNetCore; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Serilog; +using Walkthrough; +``` + +```csharp +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddOpenApi(options => options.AddTypeKit()); +builder.Services.AddFaultResponses(fault => fault switch +{ + OrderFault.NotFound => StatusCodes.Status404NotFound, + OrderFault.AlreadyShipped or OrderFault.Cancelled => StatusCodes.Status409Conflict, + OrderFault.Cleared => StatusCodes.Status422UnprocessableEntity, + _ => throw new ArgumentOutOfRangeException(nameof(fault)), +}); +builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.RespectRequiredConstructorParameters = true); + +var connection = new SqliteConnection("Data Source=:memory:"); // the database lives as long as this connection is open +connection.Open(); +builder.Services.AddDbContext(options => options.UseSqlite(connection).UseTypeKit() + .LogTo(Console.WriteLine, [RelationalEventId.CommandExecuted])); + +builder.Services.AddSingleton(new LoggerConfiguration() + .Destructure.WithTypeKit() + .WriteTo.Console(outputTemplate: "[{Level:u3}] {Message:lj}{NewLine}") + .CreateLogger()); +``` + +```csharp +var app = builder.Build(); +app.UseStatusCodePages(); +app.MapOpenApi(); +app.MapOrders(); + +using (var scope = app.Services.CreateScope()) +{ + scope.ServiceProvider.GetRequiredService().Database.EnsureCreated(); + // CREATE TABLE "Orders" ( + // "Id" TEXT NOT NULL CONSTRAINT "PK_Orders" PRIMARY KEY, + // "Email" TEXT NOT NULL, + // "Quantity" INTEGER NOT NULL, + // "Status" TEXT NOT NULL + // ); +} +``` + +What changes here: a value object becomes its underlying type. The id is a `TEXT` key, as a `Guid` is +on SQLite, and the quantity an `INTEGER`. The status is its member's name. Reading a row back skips +`Create`, because the service wrote that value itself ([EF Core](ef-core.md)). + +**Good to know** + +- **The database lives in one open connection.** SQLite in memory keeps a database as long as its + connection is open, so the service opens one and keeps it. Your own service passes a connection + string instead. +- **`LogTo` prints the SQL** that EF Core runs, which is how this page shows it. +- **`RespectRequiredConstructorParameters`** makes a body that leaves out `email` a 400. Without it, + the `Email` would be `default`, an address that never went through `Create` + ([ASP.NET Core](aspnet-core.md#what-a-refused-value-answers)). + +## Place an order + +```csharp +public sealed record PlaceOrder(Email Email, Quantity Quantity); + +public sealed record OrderView(OrderId Id, Email Email, Quantity Quantity, OrderStatus Status) +{ + public static OrderView Of(Order order) => new(order.Id, order.Email, order.Quantity, order.Status); +} + +public enum OrderFault { NotFound, AlreadyShipped, Cancelled, Cleared } +``` + +```csharp +app.MapPost("/orders", async (PlaceOrder request, OrdersDb db, Serilog.ILogger log) => +{ + var order = new Order { Id = OrderId.New(), Email = request.Email, Quantity = request.Quantity, Status = OrderStatus.Placed }; + db.Orders.Add(order); + await db.SaveChangesAsync(); + + var view = OrderView.Of(order); + log.Information("Placed {@Order}", view); // Placed {"Id": "…", "Email": "a***@example.com", "Quantity": "2", "Status": "Placed", "$type": "OrderView"} + return TypedResults.Created($"/orders/{order.Id}", view); +}); +``` + +The requests are in [Tour.cs](../tools/ConsumerSmokeTest/Consumers/walkthrough/Tour.cs). `Send` +sends one and returns an `Answer`: its status and its body. The comment after each request is what it +gets back, with `…` for an id. The client sends the address with spaces around it: + +```csharp +Answer placed = await Send("POST", "/orders", """{"email":" ann@example.com ","quantity":2}"""); // 201 {"id":"…","email":"ann@example.com","quantity":2,"status":"Placed"} +OrderId id = JsonSerializer.Deserialize(placed.Body, JsonSerializerOptions.Web)!.Id; +``` + +Here the address crosses three boundaries: + +- **On the way in**, the JSON converter calls `Create`. So the handler receives `ann@example.com`, + trimmed, and never sees the spaces. +- **On the way out**, the JSON is the underlying type's: `"email":"ann@example.com"`, a plain + string, and the closed set is its name ([JSON](json.md)). +- **In the log**, the address is masked. Serilog destructures `{@Order}` by its properties, and + `Destructure.WithTypeKit()` logs each value object as what its `ToString()` prints. Without it, + Serilog would read `Value` and log the address ([Logging and secrets](logging-and-secrets.md)). + +The client reads the answer back into the same types. Its id is an `OrderId` again. + +## Reject what doesn't fit + +```csharp +Answer noAt = await Send("POST", "/orders", """{"email":"ann","quantity":2}"""); // 400 {"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"Bad Request","status":400,"traceId":"…"} +Answer noEmail = await Send("POST", "/orders", """{"quantity":2}"""); // 400 {"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"Bad Request","status":400,"traceId":"…"} +Answer notAnId = await Send("GET", "/orders/42"); // 400 {"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"Bad Request","status":400,"traceId":"…"} +Answer noneAtAll = await Send("GET", "/orders?minimum=0"); // 400 {"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"Bad Request","status":400,"traceId":"…"} +``` + +Each of these is a 400 with problem details, and none of them quotes what the client sent: + +- **A body** that `Create` rejects throws a `JsonException` that names `Email` and `NoAt`, never the + text. The handler never runs. +- **A body without `email`** is rejected because of `RespectRequiredConstructorParameters`. +- **A route value** that isn't an `OrderId`, and **a query value** that `Create` rejects, fail the + generated `TryParse`. + +`UseStatusCodePages()` gives these empty 400s their problem body. The body is ASP.NET's own and +names neither the field nor the rule. ASP.NET logs the reason. + +## Find an order + +```csharp +app.MapGet("/orders/{id}", async (OrderId id, OrdersDb db) => + (await db.Orders.FirstOrNoneAsync(order => order.Id == id)) // an Option: the order, or none + .ToResult(OrderFault.NotFound) + .Map(OrderView.Of) + .ToHttpResult()) + .WithoutFaults(OrderFault.AlreadyShipped, OrderFault.Cancelled, OrderFault.Cleared); // finding one reports only NotFound +``` + +```csharp +Answer found = await Send("GET", $"/orders/{id}"); // 200 {"id":"…","email":"ann@example.com","quantity":2,"status":"Placed"} +Answer missing = await Send("GET", $"/orders/{OrderId.New()}"); // 404 {"type":"https://tools.ietf.org/html/rfc9110#section-15.5.5","title":"Not Found","status":404,"fault":"NotFound","traceId":"…"} +``` + +`FirstOrNoneAsync` asks the database for one row and returns an `Option`, so there is no `null` to +forget. `ToResult` turns a missing order into the error `NotFound`, and `ToHttpResult()` answers it +with the status the setup maps it to, as problem details. The `fault` member names the error, so a +client can tell a missing order from any other 404 ([Result](result.md), +[ASP.NET Core](aspnet-core.md#a-result-as-a-response)). + +`WithoutFaults` lists the errors this endpoint never reports. The OpenAPI document then shows only +the 404 for it, where it would otherwise list every status `OrderFault` maps to. + +Nothing changes for the id here. The route value becomes an `OrderId` through `TryParse`, and the +query compares it with the key column. + +**Good to know** + +- **ASP0020 is a false alarm here.** The service declares its value objects in the web project, so + ASP.NET's route analyzer reads `OrderId` before the generators make it parsable. `OrderEndpoints.cs` + suppresses the warning. Value objects in a domain project of their own need nothing + ([ASP.NET Core](aspnet-core.md#bind-value-objects-in-a-minimal-api)). + +## Query on `.Value` + +```csharp +app.MapGet("/orders", async (Quantity minimum, OrdersDb db) => + await db.Orders.Where(order => order.Quantity.Value >= minimum.Value) // WHERE "o"."Quantity" >= @minimum_Value + .Select(order => OrderView.Of(order)) + .ToListAsync()); +``` + +```csharp +Answer atLeastTwo = await Send("GET", "/orders?minimum=2"); // 200 [{"id":"…","email":"ann@example.com","quantity":2,"status":"Placed"}] +Answer atLeastFive = await Send("GET", "/orders?minimum=5"); // 200 [] +``` + +`minimum` comes from the query string, through `Create`. In the query, `order.Quantity.Value` is the +`Quantity` column itself, so the comparison runs in the database. `.Value` works the same way in memory +and in SQL ([EF Core](ef-core.md)). + +## Change an order + +```csharp +public sealed record OrderPatch +{ + public Patch Quantity { get; init; } + public Patch Status { get; init; } +} + +public static class OrderPatching +{ + public static Result Apply(this Order order, OrderPatch patch) + { + if (order.Status == OrderStatus.Shipped) return Result.Error(OrderFault.AlreadyShipped); + if (order.Status == OrderStatus.Cancelled) return Result.Error(OrderFault.Cancelled); + if (patch.Quantity.State == PatchState.Null || patch.Status.State == PatchState.Null) return Result.Error(OrderFault.Cleared); // neither field can be cleared + + patch.Quantity.Tap(quantity => order.Quantity = quantity); + patch.Status.Tap(status => order.Status = status); + return order; + } +} +``` + +```csharp +app.MapPatch("/orders/{id}", async (OrderId id, OrderPatch patch, OrdersDb db) => +{ + var changed = (await db.Orders.FirstOrNoneAsync(order => order.Id == id)) + .ToResult(OrderFault.NotFound) + .Bind(order => order.Apply(patch)); + await db.SaveChangesAsync(); + return changed.Map(OrderView.Of).ToHttpResult(); +}); +``` + +```csharp +Answer three = await Send("PATCH", $"/orders/{id}", """{"quantity":3}"""); // 200 {"id":"…","email":"ann@example.com","quantity":3,"status":"Placed"} +Answer zero = await Send("PATCH", $"/orders/{id}", """{"quantity":0}"""); // 400 {"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"Bad Request","status":400,"traceId":"…"} +Answer cleared = await Send("PATCH", $"/orders/{id}", """{"quantity":null}"""); // 422 {"type":"https://tools.ietf.org/html/rfc4918#section-11.2","title":"Unprocessable Entity","status":422,"fault":"Cleared","traceId":"…"} +Answer shipped = await Send("PATCH", $"/orders/{id}", """{"status":"Shipped"}"""); // 200 {"id":"…","email":"ann@example.com","quantity":3,"status":"Shipped"} +Answer tooLate = await Send("PATCH", $"/orders/{id}", """{"quantity":4}"""); // 409 {"type":"https://tools.ietf.org/html/rfc9110#section-15.5.10","title":"Conflict","status":409,"fault":"AlreadyShipped","traceId":"…"} +``` + +A `Patch` tells apart a field the request left out, a field set to `null`, and a new value. Each +field reads its value through its own `Create`, so `{"quantity":0}` is a 400 like any other body. +`Apply` decides what a `null` means. Here neither field can be cleared, so a clear gets the error +`Cleared`. A shipped order changes no more, and gets `AlreadyShipped` +([Merge patches](merge-patches.md)). + +`Bind` runs `Apply` only on an order that was found, so one `ToHttpResult()` answers every error. + +## Read the OpenAPI document + +```csharp +JsonNode document = JsonNode.Parse(await http.GetStringAsync("/openapi/v1.json"))!; +JsonNode schemas = document["components"]!["schemas"]!; +JsonNode find = document["paths"]!["/orders/{id}"]!["get"]!; + +string idParameter = find["parameters"]![0]!["schema"]!.ToJsonString(); // {"type":"string","format":"uuid"} +string findAnswers = string.Join(",", find["responses"]!.AsObject().Select(answer => answer.Key)); // 200,404 +string emailSchema = schemas["Email"]!.ToJsonString(); // {"type":"string"} +string statusSchema = schemas["OrderStatus"]!.ToJsonString(); // {"enum":["Placed","Shipped","Cancelled"],"type":"string"} +string patchType = schemas["OrderPatch"]!["properties"]!["quantity"]!["type"]!.ToJsonString(); // ["null","integer","string"] +``` + +`AddTypeKit()` describes every value object as the schema of the type inside it: + +- **The route's `id`** is a `uuid` string, as a `Guid` parameter would be. +- **The errors** are listed beside the success: `GET /orders/{id}` answers 200 or 404. +- **`Email`** is a string in a body, and keeps a component of its own. +- **The closed set** is a string enum of its names. +- **A patch field** is its underlying type's schema, and may also be `null`. The `string` is + ASP.NET's own: a minimal API reads a number from a string too. + +So a client generated from the document sends and receives plain strings and numbers +([ASP.NET Core](aspnet-core.md#see-value-objects-in-the-openapi-document)). + +## What changes where + +| Boundary | The e-mail address there | +|---|---| +| `Create` | trimmed and composed, or rejected | +| JSON, in a request or a response | the string itself | +| A route or query value | read by the generated `TryParse`, which calls `Create` | +| The database | a `TEXT` column, read back without `Create` | +| A LINQ query | `.Value` is the column | +| A rejected request | a 400 that names neither the value nor the rule | +| A log line | what `ToString()` prints: masked | +| The OpenAPI document | a string | diff --git a/site/docfx.json b/site/docfx.json new file mode 100644 index 00000000..0dbd56ab --- /dev/null +++ b/site/docfx.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/docfx/main/schemas/docfx.schema.json", + "metadata": [ + { + "src": [ + { + "src": "obj/pub", + "files": [ + "CodoMetis.TypeKit.dll", + "CodoMetis.TypeKit.EntityFrameworkCore.dll", + "CodoMetis.TypeKit.AspNetCore.dll", + "CodoMetis.TypeKit.Serilog.dll", + "CodoMetis.TypeKit.Testing.dll" + ] + } + ], + "dest": "obj/api" + } + ], + "build": { + "content": [ + { "files": [ "index.md", "toc.yml" ] }, + { "src": "obj/api", "dest": "api", "files": [ "**.yml" ] }, + { "src": "..", "files": [ "README.md", "CHANGELOG.md", "docs/*.md", "src/*/README.md" ] } + ], + "resource": [ + { "src": "..", "dest": "_repository", "files": [ "design/**", "CONTRIBUTING.md", "SECURITY.md", "AGENTS.md", "LICENSE", "tools/ConsumerSmokeTest/Consumers/walkthrough/*.cs" ] } + ], + "output": "obj/_site", + "template": [ "default", "modern", "template" ], + "globalMetadata": { + "_appTitle": "CodoMetis.TypeKit", + "_appName": "CodoMetis.TypeKit", + "_appFooter": "MIT licensed. GitHub", + "_enableSearch": true, + "_disableContribution": true, + "_repository": "https://github.com/CaffeinatedCoder/CodoMetis.TypeKit" + } + } +} diff --git a/site/index.md b/site/index.md new file mode 100644 index 00000000..a9560a15 --- /dev/null +++ b/site/index.md @@ -0,0 +1,3 @@ +--- +redirect_url: README.html +--- diff --git a/site/template/conceptual.extension.js b/site/template/conceptual.extension.js new file mode 100644 index 00000000..21f604c4 --- /dev/null +++ b/site/template/conceptual.extension.js @@ -0,0 +1,28 @@ +// A link from a page to a file of the repository that is not a page of the site (a design note, a decision record, +// CONTRIBUTING.md, spikes/) becomes a GitHub address at the commit the site is built from (#115). The sources are +// never edited for the site. +// +// DocFX hands this script each page's HTML with its links still rooted at site/ (`~/../design/plan.md`), before it +// resolves and checks them. A link to a page is left as it is, so DocFX checks it and its anchor and fails the build +// with --warningsAsErrors when either is broken; a link rewritten here is checked by DocumentationLinkTests instead. +// The patterns are the content globs of site/docfx.json: change the two together. +var pages = [/^README\.md$/, /^CHANGELOG\.md$/, /^docs\/[^\/]+\.md$/, /^src\/[^\/]+\/README\.md$/]; + +exports.preTransform = function (model) { + return model; +}; + +exports.postTransform = function (model) { + var commit = model._commit; + if (!commit) { + throw new Error('site/template: no commit to link the repository at. Pass --metadata _commit=, as CONTRIBUTING.md shows.'); + } + + model.conceptual = model.conceptual.replace(/href="~\/\.\.\/([^"#]*)(#[^"]*)?"/g, function (link, path, anchor) { + if (pages.some(function (page) { return page.test(path); })) return link; + + var kind = path === '' || path.charAt(path.length - 1) === '/' ? 'tree' : 'blob'; + return 'href="' + model._repository + '/' + kind + '/' + commit + '/' + path + (anchor || '') + '"'; + }); + return model; +}; diff --git a/site/toc.yml b/site/toc.yml new file mode 100644 index 00000000..e18bfb5f --- /dev/null +++ b/site/toc.yml @@ -0,0 +1,58 @@ +- name: Home + href: ../README.md +- name: Guides + items: + - name: Walkthrough + href: ../docs/walkthrough.md + - name: Value objects + href: ../docs/value-objects.md + - name: Closed sets + href: ../docs/closed-sets.md + - name: Option + href: ../docs/option.md + - name: Result + href: ../docs/result.md + - name: Written text + href: ../docs/text.md + - name: Type constraints + href: ../docs/type-constraints.md + - name: Merge patches + href: ../docs/merge-patches.md + - name: JSON + href: ../docs/json.md + - name: EF Core + href: ../docs/ef-core.md + - name: ASP.NET Core + href: ../docs/aspnet-core.md + - name: Native AOT + href: ../docs/native-aot.md + - name: Logging and secrets + href: ../docs/logging-and-secrets.md + - name: Testing + href: ../docs/testing.md + - name: Migrating from 2.0 to 2.1 + href: ../docs/migrating-to-2-1.md + - name: Migrating from 1.5 to 2.0 + href: ../docs/migrating-to-2.md + - name: How it compares + href: ../docs/comparison.md +- name: Reference + items: + - name: CodoMetis.TypeKit + href: ../src/CodoMetis.TypeKit/README.md + - name: CodoMetis.TypeKit.Analyzers + href: ../src/CodoMetis.TypeKit.Analyzers/README.md + - name: CodoMetis.TypeKit.Generators + href: ../src/CodoMetis.TypeKit.Generators/README.md + - name: CodoMetis.TypeKit.EntityFrameworkCore + href: ../src/CodoMetis.TypeKit.EntityFrameworkCore/README.md + - name: CodoMetis.TypeKit.AspNetCore + href: ../src/CodoMetis.TypeKit.AspNetCore/README.md + - name: CodoMetis.TypeKit.Serilog + href: ../src/CodoMetis.TypeKit.Serilog/README.md + - name: CodoMetis.TypeKit.Testing + href: ../src/CodoMetis.TypeKit.Testing/README.md +- name: API + href: obj/api/ +- name: Changelog + href: ../CHANGELOG.md diff --git a/spikes/AspectTestingSpike/AspectTests/AspectTests.csproj b/spikes/AspectTestingSpike/AspectTests/AspectTests.csproj new file mode 100644 index 00000000..c5438dd2 --- /dev/null +++ b/spikes/AspectTestingSpike/AspectTests/AspectTests.csproj @@ -0,0 +1,41 @@ + + + + + Library + false + true + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + diff --git a/spikes/AspectTestingSpike/AspectTests/Email.EmailExtensions.i.cs b/spikes/AspectTestingSpike/AspectTests/Email.EmailExtensions.i.cs new file mode 100644 index 00000000..f0aab30f --- /dev/null +++ b/spikes/AspectTestingSpike/AspectTests/Email.EmailExtensions.i.cs @@ -0,0 +1,19 @@ +namespace AspectTestingSpike +{ + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static partial class EmailExtensions + { + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::CodoMetis.TypeKit.CompilerServices.TranslatedAsWrappedValueAttribute] + public static global::System.String GetValue(this global::AspectTestingSpike.Email instance) + { + return (global::System.String)instance.Value; + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::CodoMetis.TypeKit.CompilerServices.TranslatedAsWrappedValueAttribute] + public static global::System.String? ValueOrNull(this global::AspectTestingSpike.Email? instance) + { + return (global::System.String? )instance?.Value; + } + } +} \ No newline at end of file diff --git a/spikes/AspectTestingSpike/AspectTests/Email.cs b/spikes/AspectTestingSpike/AspectTests/Email.cs new file mode 100644 index 00000000..f11aa9c3 --- /dev/null +++ b/spikes/AspectTestingSpike/AspectTests/Email.cs @@ -0,0 +1,17 @@ +using CodoMetis.TypeKit; +using CodoMetis.TypeKit.ValueObjects; + +namespace AspectTestingSpike; + +public enum EmailFault { Blank, NoAt } + +public readonly partial record struct Email : IValidatedValue +{ + public static Result Create(string value) + { + if (string.IsNullOrWhiteSpace(value)) return Result.Error(EmailFault.Blank); + if (!value.Contains('@')) return Result.Error(EmailFault.NoAt); + + return new Email(value.Trim()); + } +} diff --git a/spikes/AspectTestingSpike/AspectTests/Email.t.cs b/spikes/AspectTestingSpike/AspectTests/Email.t.cs new file mode 100644 index 00000000..572c00ab --- /dev/null +++ b/spikes/AspectTestingSpike/AspectTests/Email.t.cs @@ -0,0 +1,350 @@ +using CodoMetis.TypeKit; +using CodoMetis.TypeKit.ValueObjects; +namespace AspectTestingSpike; +public enum EmailFault +{ + Blank, + NoAt +} +[global::CodoMetis.TypeKit.CompilerServices.GeneratedValueObjectAttribute] +[global::System.Text.Json.Serialization.JsonConverterAttribute(typeof(global::AspectTestingSpike.Email.EmailJsonConverter))] +[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] +[global::System.ComponentModel.TypeConverterAttribute(typeof(global::AspectTestingSpike.Email.EmailTypeConverter))] +public readonly partial record struct Email : IValidatedValue, global::CodoMetis.TypeKit.ValueObjects.IValueObjectMaterializer, global::CodoMetis.TypeKit.ValueObjects.IValueObject, global::System.Numerics.IEqualityOperators, global::System.IParsable, global::System.ISpanParsable, global::System.IFormattable, global::System.IComparable, global::System.IComparable, global::System.Numerics.IComparisonOperators, global::System.IConvertible +{ + public static Result Create(string value) + { + if (string.IsNullOrWhiteSpace(value)) + return Result.Error(EmailFault.Blank); + if (!value.Contains('@')) + return Result.Error(EmailFault.NoAt); + return new Email(value.Trim()); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + private readonly global::System.String _value; + [global::System.Diagnostics.DebuggerHiddenAttribute] + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public Email() + { + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::System.Diagnostics.DebuggerStepThroughAttribute] + private Email(global::System.String value) + { + this._value = value; + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public global::System.String Value + { + get + { + return (global::System.String)this._value; + } + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public global::System.Int32 CompareTo(global::AspectTestingSpike.Email other) + { + return (global::System.Int32)(int)global::System.String.CompareOrdinal(this.Value, other.Value); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public global::System.Int32 CompareTo(global::System.Object? obj) + { + if (obj is null) + return (global::System.Int32)1; + if (!(bool)obj is global::AspectTestingSpike.Email __comparand) + throw new global::System.ArgumentException("Object must be of type Email.", nameof(obj)); + return (global::System.Int32)this.CompareTo(__comparand); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static global::AspectTestingSpike.Email FromKnownGood(global::System.String value, [global::System.Runtime.CompilerServices.CallerArgumentExpressionAttribute("value")] global::System.String? source = null) + { + return (global::AspectTestingSpike.Email)global::CodoMetis.TypeKit.CompilerServices.GeneratedFactories.OrInvalidOperationException(global::AspectTestingSpike.Email.Create(value), source); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + global::System.TypeCode global::System.IConvertible.GetTypeCode() + { + return (global::System.TypeCode)global::System.Convert.GetTypeCode((object? )this.Value); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + static global::AspectTestingSpike.Email global::CodoMetis.TypeKit.ValueObjects.IValueObjectMaterializer.Materialize(global::System.String value) + { + return new global::AspectTestingSpike.Email(value); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static global::AspectTestingSpike.Email Parse(global::System.String s, global::System.IFormatProvider? provider) + { + global::System.ArgumentNullException.ThrowIfNull(s); + return __FromText(s); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static global::AspectTestingSpike.Email Parse(global::System.ReadOnlySpan s, global::System.IFormatProvider? provider) + { + return __FromText((global::CodoMetis.TypeKit.CompilerServices.GeneratedParsing.TryParseSpan(s, (provider ?? global::System.Globalization.CultureInfo.InvariantCulture), out var __parsed) ? __parsed : throw global::CodoMetis.TypeKit.CompilerServices.GeneratedParsing.Unreadable())); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public global::CodoMetis.TypeKit.Result Revalidate() + { + return (global::CodoMetis.TypeKit.Result)global::AspectTestingSpike.Email.Create(this.Value); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + global::System.Boolean global::System.IConvertible.ToBoolean(global::System.IFormatProvider? provider) + { + return global::System.Convert.ToBoolean((object? )this.Value, (provider ?? global::System.Globalization.CultureInfo.InvariantCulture)); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + global::System.Byte global::System.IConvertible.ToByte(global::System.IFormatProvider? provider) + { + return global::System.Convert.ToByte((object? )this.Value, (provider ?? global::System.Globalization.CultureInfo.InvariantCulture)); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + global::System.Char global::System.IConvertible.ToChar(global::System.IFormatProvider? provider) + { + return global::System.Convert.ToChar((object? )this.Value, (provider ?? global::System.Globalization.CultureInfo.InvariantCulture)); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + global::System.DateTime global::System.IConvertible.ToDateTime(global::System.IFormatProvider? provider) + { + return global::System.Convert.ToDateTime((object? )this.Value, (provider ?? global::System.Globalization.CultureInfo.InvariantCulture)); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + global::System.Decimal global::System.IConvertible.ToDecimal(global::System.IFormatProvider? provider) + { + return global::System.Convert.ToDecimal((object? )this.Value, (provider ?? global::System.Globalization.CultureInfo.InvariantCulture)); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + global::System.Double global::System.IConvertible.ToDouble(global::System.IFormatProvider? provider) + { + return global::System.Convert.ToDouble((object? )this.Value, (provider ?? global::System.Globalization.CultureInfo.InvariantCulture)); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + global::System.Int16 global::System.IConvertible.ToInt16(global::System.IFormatProvider? provider) + { + return global::System.Convert.ToInt16((object? )this.Value, (provider ?? global::System.Globalization.CultureInfo.InvariantCulture)); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + global::System.Int32 global::System.IConvertible.ToInt32(global::System.IFormatProvider? provider) + { + return global::System.Convert.ToInt32((object? )this.Value, (provider ?? global::System.Globalization.CultureInfo.InvariantCulture)); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + global::System.Int64 global::System.IConvertible.ToInt64(global::System.IFormatProvider? provider) + { + return global::System.Convert.ToInt64((object? )this.Value, (provider ?? global::System.Globalization.CultureInfo.InvariantCulture)); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + global::System.SByte global::System.IConvertible.ToSByte(global::System.IFormatProvider? provider) + { + return global::System.Convert.ToSByte((object? )this.Value, (provider ?? global::System.Globalization.CultureInfo.InvariantCulture)); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + global::System.Single global::System.IConvertible.ToSingle(global::System.IFormatProvider? provider) + { + return global::System.Convert.ToSingle((object? )this.Value, (provider ?? global::System.Globalization.CultureInfo.InvariantCulture)); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + global::System.String global::System.IConvertible.ToString(global::System.IFormatProvider? provider) + { + return (global::System.String)(string)global::System.Convert.ToString((object? )this.Value, (provider ?? global::System.Globalization.CultureInfo.InvariantCulture)); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public global::System.String ToString(global::System.String? format, global::System.IFormatProvider? formatProvider) + { + return (global::System.String)(string)(this.Value ?? string.Empty); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + global::System.Object global::System.IConvertible.ToType(global::System.Type conversionType, global::System.IFormatProvider? provider) + { + if ((bool)(conversionType == typeof(global::AspectTestingSpike.Email) || conversionType == typeof(object))) + return this; + if (conversionType == typeof(global::System.String)) + return global::System.Convert.ToString((object? )this.Value, (provider ?? global::System.Globalization.CultureInfo.InvariantCulture)); + return global::System.Convert.ChangeType((object? )this.Value, conversionType, (provider ?? global::System.Globalization.CultureInfo.InvariantCulture))!; + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + global::System.UInt16 global::System.IConvertible.ToUInt16(global::System.IFormatProvider? provider) + { + return global::System.Convert.ToUInt16((object? )this.Value, (provider ?? global::System.Globalization.CultureInfo.InvariantCulture)); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + global::System.UInt32 global::System.IConvertible.ToUInt32(global::System.IFormatProvider? provider) + { + return global::System.Convert.ToUInt32((object? )this.Value, (provider ?? global::System.Globalization.CultureInfo.InvariantCulture)); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + global::System.UInt64 global::System.IConvertible.ToUInt64(global::System.IFormatProvider? provider) + { + return global::System.Convert.ToUInt64((object? )this.Value, (provider ?? global::System.Globalization.CultureInfo.InvariantCulture)); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static global::CodoMetis.TypeKit.Option TryFrom(global::System.String value) + { + return (global::CodoMetis.TypeKit.Option)global::CodoMetis.TypeKit.Result.ToOption(global::AspectTestingSpike.Email.Create(value)); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static global::System.Boolean TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] global::System.String? s, global::System.IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out global::AspectTestingSpike.Email result) + { + result = default(global::AspectTestingSpike.Email); + return (global::System.Boolean)(bool)(s is not null && __TryFromText(s, out result)); + global::System.String __inner; + try + { + __inner = s; + } + catch (global::System.FormatException) + { + return false; + } + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static global::System.Boolean TryParse(global::System.ReadOnlySpan s, global::System.IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out global::AspectTestingSpike.Email result) + { + result = default(global::AspectTestingSpike.Email); + return (global::System.Boolean)(bool)(global::CodoMetis.TypeKit.CompilerServices.GeneratedParsing.TryParseSpan(s, (provider ?? global::System.Globalization.CultureInfo.InvariantCulture), out var innerValue) && __TryFromText(innerValue, out result)); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Diagnostics.DebuggerStepThroughAttribute] + private static global::AspectTestingSpike.Email __FromJson(global::System.String value, global::System.Boolean materialize) + { + if (materialize) + return new global::AspectTestingSpike.Email(value); + return (global::AspectTestingSpike.Email)global::CodoMetis.TypeKit.CompilerServices.GeneratedFactories.OrJsonException(global::AspectTestingSpike.Email.Create(value)); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Diagnostics.DebuggerStepThroughAttribute] + private static global::AspectTestingSpike.Email __FromText(global::System.String value) + { + return (global::AspectTestingSpike.Email)global::CodoMetis.TypeKit.CompilerServices.GeneratedFactories.OrFormatException(global::AspectTestingSpike.Email.Create(value)); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Diagnostics.DebuggerStepThroughAttribute] + private static global::System.Boolean __TryFromText(global::System.String value, out global::AspectTestingSpike.Email result) + { + result = default(global::AspectTestingSpike.Email); + return (global::System.Boolean)(bool)global::AspectTestingSpike.Email.Create(value).TryGetValue(out result, out _); + } + [global::System.Runtime.CompilerServices.MethodImplAttribute(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static global::System.Boolean operator>(global::AspectTestingSpike.Email left, global::AspectTestingSpike.Email right) + { + return (global::System.Boolean)(bool)left.CompareTo(right) > 0; + } + [global::System.Runtime.CompilerServices.MethodImplAttribute(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static global::System.Boolean operator >=(global::AspectTestingSpike.Email left, global::AspectTestingSpike.Email right) + { + return (global::System.Boolean)(bool)left.CompareTo(right) >= 0; + } + [global::System.Runtime.CompilerServices.MethodImplAttribute(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static global::System.Boolean operator <(global::AspectTestingSpike.Email left, global::AspectTestingSpike.Email right) + { + return (global::System.Boolean)(bool)left.CompareTo(right) < 0; + } + [global::System.Runtime.CompilerServices.MethodImplAttribute(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static global::System.Boolean operator <=(global::AspectTestingSpike.Email left, global::AspectTestingSpike.Email right) + { + return (global::System.Boolean)(bool)left.CompareTo(right) <= 0; + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public partial class EmailJsonConverter : global::System.Text.Json.Serialization.JsonConverter, global::CodoMetis.TypeKit.CompilerServices.IStoredJsonConverterSource + { + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + private global::CodoMetis.TypeKit.CompilerServices.GeneratedJsonPlan? __jsonPlan; + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + private readonly global::System.Boolean _materialize; + public EmailJsonConverter() + { + } + private EmailJsonConverter(global::System.Boolean materialize) + { + this._materialize = materialize; + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + global::System.Text.Json.Serialization.JsonConverter global::CodoMetis.TypeKit.CompilerServices.IStoredJsonConverterSource.CreateStoredJsonConverter(global::CodoMetis.TypeKit.ValueObjects.StoredJsonConverterFactory factory) + { + global::System.ArgumentNullException.ThrowIfNull(factory); + return new global::AspectTestingSpike.Email.EmailJsonConverter(true); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public override global::AspectTestingSpike.Email Read(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + { + if (reader.TokenType == global::System.Text.Json.JsonTokenType.Null) + throw (global::System.Exception)global::CodoMetis.TypeKit.CompilerServices.GeneratedJson.NullToken(); + return global::AspectTestingSpike.Email.__FromJson((this.__jsonPlan is { } __plan && global::System.Object.ReferenceEquals(__plan.Options, options) ? __plan : this.__jsonPlan = global::CodoMetis.TypeKit.CompilerServices.GeneratedJsonPlan.For(options, global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.StringConverter)).Read(ref reader, options), (global::System.Boolean)this._materialize); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public override global::AspectTestingSpike.Email ReadAsPropertyName(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + { + return global::AspectTestingSpike.Email.__FromJson((this.__jsonPlan is { } __plan && global::System.Object.ReferenceEquals(__plan.Options, options) ? __plan : this.__jsonPlan = global::CodoMetis.TypeKit.CompilerServices.GeneratedJsonPlan.For(options, global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.StringConverter)).ReadKey(ref reader, options), (global::System.Boolean)this._materialize); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public override void Write(global::System.Text.Json.Utf8JsonWriter writer, global::AspectTestingSpike.Email value, global::System.Text.Json.JsonSerializerOptions options) + { + (this.__jsonPlan is { } __plan && global::System.Object.ReferenceEquals(__plan.Options, options) ? __plan : this.__jsonPlan = global::CodoMetis.TypeKit.CompilerServices.GeneratedJsonPlan.For(options, global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.StringConverter)).Write(writer, value!.Value, options); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public override void WriteAsPropertyName(global::System.Text.Json.Utf8JsonWriter writer, global::AspectTestingSpike.Email value, global::System.Text.Json.JsonSerializerOptions options) + { + (this.__jsonPlan is { } __plan && global::System.Object.ReferenceEquals(__plan.Options, options) ? __plan : this.__jsonPlan = global::CodoMetis.TypeKit.CompilerServices.GeneratedJsonPlan.For(options, global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.StringConverter)).WriteKey(writer, value!.Value, options); + } + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public partial class EmailTypeConverter : global::System.ComponentModel.TypeConverter + { + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public override global::System.Boolean CanConvertFrom(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Type sourceType) + { + return (global::System.Boolean)(sourceType == typeof(global::System.String) || base.CanConvertFrom(context, sourceType)); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public override global::System.Boolean CanConvertTo(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Type? destinationType) + { + return (global::System.Boolean)(destinationType == typeof(global::System.String) || base.CanConvertTo(context, destinationType)); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public override global::System.Object? ConvertFrom(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Globalization.CultureInfo? culture, global::System.Object value) + { + if (value is string s) + { + global::System.IFormatProvider provider; + if (culture == null) + { + provider = global::System.Globalization.CultureInfo.InvariantCulture; + } + else + { + provider = culture; + } + return global::AspectTestingSpike.Email.Parse(s, provider); + } + return (global::System.Object? )base.ConvertFrom(context, culture, value); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public override global::System.Object? ConvertTo(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Globalization.CultureInfo? culture, global::System.Object? value, global::System.Type destinationType) + { + if (destinationType == typeof(global::System.String) && value is not null) + { + global::System.IFormatProvider provider; + if (culture == null) + { + provider = global::System.Globalization.CultureInfo.InvariantCulture; + } + else + { + provider = culture; + } + return (((global::AspectTestingSpike.Email)value).Value ?? string.Empty); + } + return (global::System.Object? )base.ConvertTo(context, culture, value, destinationType); + } + } + public override readonly global::System.String ToString() + { + return (global::System.String)this.ToString(null, global::System.Globalization.CultureInfo.InvariantCulture); + } +} \ No newline at end of file diff --git a/spikes/AspectTestingSpike/AspectTests/NotReadonly.cs b/spikes/AspectTestingSpike/AspectTests/NotReadonly.cs new file mode 100644 index 00000000..9ced8315 --- /dev/null +++ b/spikes/AspectTestingSpike/AspectTests/NotReadonly.cs @@ -0,0 +1,6 @@ +using CodoMetis.TypeKit.ValueObjects; + +namespace AspectTestingSpike; + +// A refusal: not readonly, so CMTK1002 and nothing generated. +public partial record struct Draft : IValue; diff --git a/spikes/AspectTestingSpike/AspectTests/NotReadonly.t.cs b/spikes/AspectTestingSpike/AspectTests/NotReadonly.t.cs new file mode 100644 index 00000000..4ad4c4dd --- /dev/null +++ b/spikes/AspectTestingSpike/AspectTests/NotReadonly.t.cs @@ -0,0 +1 @@ +// Error CMTK1002 on `Draft`: `'Draft' is a value object and must be declared readonly. See https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.Generators/README.md#build-errors` \ No newline at end of file diff --git a/spikes/AspectTestingSpike/AspectTests/OrderId.OrderIdExtensions.i.cs b/spikes/AspectTestingSpike/AspectTests/OrderId.OrderIdExtensions.i.cs new file mode 100644 index 00000000..3244ebfc --- /dev/null +++ b/spikes/AspectTestingSpike/AspectTests/OrderId.OrderIdExtensions.i.cs @@ -0,0 +1,19 @@ +namespace AspectTestingSpike +{ + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static partial class OrderIdExtensions + { + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::CodoMetis.TypeKit.CompilerServices.TranslatedAsWrappedValueAttribute] + public static global::System.Guid GetValue(this global::AspectTestingSpike.OrderId instance) + { + return (global::System.Guid)instance.Value; + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::CodoMetis.TypeKit.CompilerServices.TranslatedAsWrappedValueAttribute] + public static global::System.Guid? ValueOrNull(this global::AspectTestingSpike.OrderId? instance) + { + return (global::System.Guid? )instance?.Value; + } + } +} \ No newline at end of file diff --git a/spikes/AspectTestingSpike/AspectTests/OrderId.cs b/spikes/AspectTestingSpike/AspectTests/OrderId.cs new file mode 100644 index 00000000..8e29d8ac --- /dev/null +++ b/spikes/AspectTestingSpike/AspectTests/OrderId.cs @@ -0,0 +1,5 @@ +using CodoMetis.TypeKit.ValueObjects; + +namespace AspectTestingSpike; + +public readonly partial record struct OrderId : IValue; diff --git a/spikes/AspectTestingSpike/AspectTests/OrderId.t.cs b/spikes/AspectTestingSpike/AspectTests/OrderId.t.cs new file mode 100644 index 00000000..899b62bb --- /dev/null +++ b/spikes/AspectTestingSpike/AspectTests/OrderId.t.cs @@ -0,0 +1,259 @@ +using CodoMetis.TypeKit.ValueObjects; +namespace AspectTestingSpike; +[global::CodoMetis.TypeKit.CompilerServices.GeneratedValueObjectAttribute] +[global::System.Text.Json.Serialization.JsonConverterAttribute(typeof(global::AspectTestingSpike.OrderId.OrderIdJsonConverter))] +[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] +[global::System.ComponentModel.TypeConverterAttribute(typeof(global::AspectTestingSpike.OrderId.OrderIdTypeConverter))] +public readonly partial record struct OrderId : IValue, global::CodoMetis.TypeKit.ValueObjects.IValueObjectMaterializer, global::CodoMetis.TypeKit.ValueObjects.IValueObject, global::CodoMetis.TypeKit.ValueObjects.IPlainValueObject, global::System.Numerics.IEqualityOperators, global::System.IParsable, global::System.ISpanParsable, global::System.IUtf8SpanParsable, global::System.IFormattable, global::System.ISpanFormattable, global::System.IUtf8SpanFormattable, global::System.IComparable, global::System.IComparable, global::System.Numerics.IComparisonOperators +{ + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + private readonly global::System.Guid _value; + [global::System.Diagnostics.DebuggerHiddenAttribute] + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public OrderId() + { + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::System.Diagnostics.DebuggerStepThroughAttribute] + private OrderId(global::System.Guid value) + { + this._value = value; + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public global::System.Guid Value + { + get + { + return (global::System.Guid)this._value; + } + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public global::System.Int32 CompareTo(global::AspectTestingSpike.OrderId other) + { + return (global::System.Int32)(int)global::System.Collections.Generic.Comparer.Default.Compare(this.Value, other.Value); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public global::System.Int32 CompareTo(global::System.Object? obj) + { + if (obj is null) + return (global::System.Int32)1; + if (!(bool)obj is global::AspectTestingSpike.OrderId __comparand) + throw new global::System.ArgumentException("Object must be of type OrderId.", nameof(obj)); + return (global::System.Int32)this.CompareTo(__comparand); + } + [global::System.Runtime.CompilerServices.MethodImplAttribute(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static global::AspectTestingSpike.OrderId From(global::System.Guid value) + { + return new global::AspectTestingSpike.OrderId(value); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + static global::AspectTestingSpike.OrderId global::CodoMetis.TypeKit.ValueObjects.IValueObjectMaterializer.Materialize(global::System.Guid value) + { + return new global::AspectTestingSpike.OrderId(value); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static global::AspectTestingSpike.OrderId Parse(global::System.String s, global::System.IFormatProvider? provider) + { + global::System.ArgumentNullException.ThrowIfNull(s); + return __FromText((global::CodoMetis.TypeKit.CompilerServices.GeneratedParsing.TryParseSpan(s, (provider ?? global::System.Globalization.CultureInfo.InvariantCulture), out var __parsed) ? __parsed : throw global::CodoMetis.TypeKit.CompilerServices.GeneratedParsing.Unreadable())); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static global::AspectTestingSpike.OrderId Parse(global::System.ReadOnlySpan s, global::System.IFormatProvider? provider) + { + return __FromText((global::CodoMetis.TypeKit.CompilerServices.GeneratedParsing.TryParseSpan(s, (provider ?? global::System.Globalization.CultureInfo.InvariantCulture), out var __parsed) ? __parsed : throw global::CodoMetis.TypeKit.CompilerServices.GeneratedParsing.Unreadable())); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static global::AspectTestingSpike.OrderId Parse(global::System.ReadOnlySpan utf8Text, global::System.IFormatProvider? provider) + { + return __FromText(global::CodoMetis.TypeKit.CompilerServices.GeneratedParsing.TryParseUtf8(utf8Text, (provider ?? global::System.Globalization.CultureInfo.InvariantCulture), out var __parsed) ? __parsed : throw global::CodoMetis.TypeKit.CompilerServices.GeneratedParsing.Unreadable()); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public global::System.String ToString(global::System.String? format, global::System.IFormatProvider? formatProvider) + { + return (global::System.String)(string)global::CodoMetis.TypeKit.CompilerServices.GeneratedFormatting.ToString(this.Value, format, formatProvider ?? global::System.Globalization.CultureInfo.InvariantCulture); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public global::System.Boolean TryFormat(global::System.Span destination, out global::System.Int32 charsWritten, global::System.ReadOnlySpan format, global::System.IFormatProvider? provider) + { + charsWritten = 0; + return (global::System.Boolean)(bool)global::CodoMetis.TypeKit.CompilerServices.GeneratedFormatting.TryFormat(this.Value, destination, out charsWritten, format, provider ?? global::System.Globalization.CultureInfo.InvariantCulture); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public global::System.Boolean TryFormat(global::System.Span utf8Destination, out global::System.Int32 bytesWritten, global::System.ReadOnlySpan format, global::System.IFormatProvider? provider) + { + bytesWritten = 0; + return (global::System.Boolean)(bool)global::CodoMetis.TypeKit.CompilerServices.GeneratedFormatting.TryFormatUtf8(this.Value, utf8Destination, out bytesWritten, format, provider ?? global::System.Globalization.CultureInfo.InvariantCulture); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static global::System.Boolean TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] global::System.String? s, global::System.IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out global::AspectTestingSpike.OrderId result) + { + result = default(global::AspectTestingSpike.OrderId); + return (global::System.Boolean)(bool)(global::CodoMetis.TypeKit.CompilerServices.GeneratedParsing.TryParseSpan(s, (provider ?? global::System.Globalization.CultureInfo.InvariantCulture), out var innerValue) && __TryFromText(innerValue, out result)); + global::System.Guid __inner; + try + { + __inner = (global::CodoMetis.TypeKit.CompilerServices.GeneratedParsing.TryParseSpan(s, (provider ?? global::System.Globalization.CultureInfo.InvariantCulture), out var __parsed) ? __parsed : throw global::CodoMetis.TypeKit.CompilerServices.GeneratedParsing.Unreadable()); + } + catch (global::System.FormatException) + { + return false; + } + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static global::System.Boolean TryParse(global::System.ReadOnlySpan s, global::System.IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out global::AspectTestingSpike.OrderId result) + { + result = default(global::AspectTestingSpike.OrderId); + return (global::System.Boolean)(bool)(global::CodoMetis.TypeKit.CompilerServices.GeneratedParsing.TryParseSpan(s, (provider ?? global::System.Globalization.CultureInfo.InvariantCulture), out var innerValue) && __TryFromText(innerValue, out result)); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static global::System.Boolean TryParse(global::System.ReadOnlySpan utf8Text, global::System.IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out global::AspectTestingSpike.OrderId result) + { + result = default(global::AspectTestingSpike.OrderId); + return (global::System.Boolean)(bool)(global::CodoMetis.TypeKit.CompilerServices.GeneratedParsing.TryParseUtf8(utf8Text, (provider ?? global::System.Globalization.CultureInfo.InvariantCulture), out var innerValue) && __TryFromText(innerValue, out result)); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Diagnostics.DebuggerStepThroughAttribute] + private static global::AspectTestingSpike.OrderId __FromJson(global::System.Guid value, global::System.Boolean materialize) + { + return new global::AspectTestingSpike.OrderId(value); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Diagnostics.DebuggerStepThroughAttribute] + private static global::AspectTestingSpike.OrderId __FromText(global::System.Guid value) + { + return new global::AspectTestingSpike.OrderId(value); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Diagnostics.DebuggerStepThroughAttribute] + private static global::System.Boolean __TryFromText(global::System.Guid value, out global::AspectTestingSpike.OrderId result) + { + result = new global::AspectTestingSpike.OrderId(value); + return (global::System.Boolean)true; + } + [global::System.Runtime.CompilerServices.MethodImplAttribute(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static global::System.Boolean operator>(global::AspectTestingSpike.OrderId left, global::AspectTestingSpike.OrderId right) + { + return (global::System.Boolean)(bool)left.CompareTo(right) > 0; + } + [global::System.Runtime.CompilerServices.MethodImplAttribute(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static global::System.Boolean operator >=(global::AspectTestingSpike.OrderId left, global::AspectTestingSpike.OrderId right) + { + return (global::System.Boolean)(bool)left.CompareTo(right) >= 0; + } + [global::System.Runtime.CompilerServices.MethodImplAttribute(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static global::System.Boolean operator <(global::AspectTestingSpike.OrderId left, global::AspectTestingSpike.OrderId right) + { + return (global::System.Boolean)(bool)left.CompareTo(right) < 0; + } + [global::System.Runtime.CompilerServices.MethodImplAttribute(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static global::System.Boolean operator <=(global::AspectTestingSpike.OrderId left, global::AspectTestingSpike.OrderId right) + { + return (global::System.Boolean)(bool)left.CompareTo(right) <= 0; + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public partial class OrderIdJsonConverter : global::System.Text.Json.Serialization.JsonConverter, global::CodoMetis.TypeKit.CompilerServices.IStoredJsonConverterSource + { + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + private global::CodoMetis.TypeKit.CompilerServices.GeneratedJsonPlan? __jsonPlan; + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + private readonly global::System.Boolean _materialize; + public OrderIdJsonConverter() + { + } + private OrderIdJsonConverter(global::System.Boolean materialize) + { + this._materialize = materialize; + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + global::System.Text.Json.Serialization.JsonConverter global::CodoMetis.TypeKit.CompilerServices.IStoredJsonConverterSource.CreateStoredJsonConverter(global::CodoMetis.TypeKit.ValueObjects.StoredJsonConverterFactory factory) + { + global::System.ArgumentNullException.ThrowIfNull(factory); + return new global::AspectTestingSpike.OrderId.OrderIdJsonConverter(true); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public override global::AspectTestingSpike.OrderId Read(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + { + if (reader.TokenType == global::System.Text.Json.JsonTokenType.Null) + throw (global::System.Exception)global::CodoMetis.TypeKit.CompilerServices.GeneratedJson.NullToken(); + return global::AspectTestingSpike.OrderId.__FromJson((this.__jsonPlan is { } __plan && global::System.Object.ReferenceEquals(__plan.Options, options) ? __plan : this.__jsonPlan = global::CodoMetis.TypeKit.CompilerServices.GeneratedJsonPlan.For(options, global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.GuidConverter)).Read(ref reader, options), (global::System.Boolean)this._materialize); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public override global::AspectTestingSpike.OrderId ReadAsPropertyName(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + { + return global::AspectTestingSpike.OrderId.__FromJson((this.__jsonPlan is { } __plan && global::System.Object.ReferenceEquals(__plan.Options, options) ? __plan : this.__jsonPlan = global::CodoMetis.TypeKit.CompilerServices.GeneratedJsonPlan.For(options, global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.GuidConverter)).ReadKey(ref reader, options), (global::System.Boolean)this._materialize); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public override void Write(global::System.Text.Json.Utf8JsonWriter writer, global::AspectTestingSpike.OrderId value, global::System.Text.Json.JsonSerializerOptions options) + { + (this.__jsonPlan is { } __plan && global::System.Object.ReferenceEquals(__plan.Options, options) ? __plan : this.__jsonPlan = global::CodoMetis.TypeKit.CompilerServices.GeneratedJsonPlan.For(options, global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.GuidConverter)).Write(writer, value!.Value, options); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public override void WriteAsPropertyName(global::System.Text.Json.Utf8JsonWriter writer, global::AspectTestingSpike.OrderId value, global::System.Text.Json.JsonSerializerOptions options) + { + (this.__jsonPlan is { } __plan && global::System.Object.ReferenceEquals(__plan.Options, options) ? __plan : this.__jsonPlan = global::CodoMetis.TypeKit.CompilerServices.GeneratedJsonPlan.For(options, global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.GuidConverter)).WriteKey(writer, value!.Value, options); + } + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public partial class OrderIdTypeConverter : global::System.ComponentModel.TypeConverter + { + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public override global::System.Boolean CanConvertFrom(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Type sourceType) + { + return (global::System.Boolean)(sourceType == typeof(global::System.String) || base.CanConvertFrom(context, sourceType)); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public override global::System.Boolean CanConvertTo(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Type? destinationType) + { + return (global::System.Boolean)(destinationType == typeof(global::System.String) || base.CanConvertTo(context, destinationType)); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public override global::System.Object? ConvertFrom(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Globalization.CultureInfo? culture, global::System.Object value) + { + if (value is string s) + { + global::System.IFormatProvider provider; + if (culture == null) + { + provider = global::System.Globalization.CultureInfo.InvariantCulture; + } + else + { + provider = culture; + } + return global::AspectTestingSpike.OrderId.Parse(s, provider); + } + return (global::System.Object? )base.ConvertFrom(context, culture, value); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public override global::System.Object? ConvertTo(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Globalization.CultureInfo? culture, global::System.Object? value, global::System.Type destinationType) + { + if (destinationType == typeof(global::System.String) && value is not null) + { + global::System.IFormatProvider provider; + if (culture == null) + { + provider = global::System.Globalization.CultureInfo.InvariantCulture; + } + else + { + provider = culture; + } + return global::CodoMetis.TypeKit.CompilerServices.GeneratedFormatting.ToString(((global::AspectTestingSpike.OrderId)value).Value, null, provider); + } + return (global::System.Object? )base.ConvertTo(context, culture, value, destinationType); + } + } + public override readonly global::System.String ToString() + { + return (global::System.String)this.ToString(null, global::System.Globalization.CultureInfo.InvariantCulture); + } +} \ No newline at end of file diff --git a/spikes/AspectTestingSpike/AspectTests/OrderStatus.cs b/spikes/AspectTestingSpike/AspectTests/OrderStatus.cs new file mode 100644 index 00000000..368ca24a --- /dev/null +++ b/spikes/AspectTestingSpike/AspectTests/OrderStatus.cs @@ -0,0 +1,10 @@ +using CodoMetis.TypeKit.ClosedSets; + +namespace AspectTestingSpike; + +public sealed partial class OrderStatus : IClosedSet +{ + public static readonly OrderStatus Open = new(); + public static readonly OrderStatus Shipped = new(); + public static readonly OrderStatus Cancelled = new(); +} diff --git a/spikes/AspectTestingSpike/AspectTests/OrderStatus.t.cs b/spikes/AspectTestingSpike/AspectTests/OrderStatus.t.cs new file mode 100644 index 00000000..e4d61029 --- /dev/null +++ b/spikes/AspectTestingSpike/AspectTests/OrderStatus.t.cs @@ -0,0 +1,236 @@ +using CodoMetis.TypeKit.ClosedSets; +namespace AspectTestingSpike; +[global::System.Text.Json.Serialization.JsonConverterAttribute(typeof(global::AspectTestingSpike.OrderStatus.OrderStatusJsonConverter))] +[global::System.ComponentModel.TypeConverterAttribute(typeof(global::AspectTestingSpike.OrderStatus.OrderStatusTypeConverter))] +[global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSetAttribute] +[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] +public sealed partial class OrderStatus : IClosedSet, global::System.IParsable +{ + public static readonly OrderStatus Open = new(); + public static readonly OrderStatus Shipped = new(); + public static readonly OrderStatus Cancelled = new(); + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + private static readonly global::System.Collections.Generic.IReadOnlyList _all = null !; + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + private global::System.String _name = null !; + static OrderStatus() + { + global::AspectTestingSpike.OrderStatus.Open._name = "Open"; + global::AspectTestingSpike.OrderStatus.Shipped._name = "Shipped"; + global::AspectTestingSpike.OrderStatus.Cancelled._name = "Cancelled"; + global::AspectTestingSpike.OrderStatus._all = global::System.Array.AsReadOnly(new global::AspectTestingSpike.OrderStatus[] { global::AspectTestingSpike.OrderStatus.Open, global::AspectTestingSpike.OrderStatus.Shipped, global::AspectTestingSpike.OrderStatus.Cancelled }); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + private OrderStatus() + { + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static global::System.Collections.Generic.IReadOnlyList All + { + get + { + return (global::System.Collections.Generic.IReadOnlyList)(global::AspectTestingSpike.OrderStatus._all ?? throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.StillInitializing("OrderStatus")); + } + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public global::System.String Name + { + get + { + return (global::System.String)(this._name ?? throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.NotAMember("OrderStatus")); + } + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public global::System.String QualifiedName + { + get + { + return (global::System.String)("OrderStatus." + this.Name); + } + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public TResult Match(global::System.Func open, global::System.Func shipped, global::System.Func cancelled) + { + global::System.ArgumentNullException.ThrowIfNull(open, "open"); + global::System.ArgumentNullException.ThrowIfNull(shipped, "shipped"); + global::System.ArgumentNullException.ThrowIfNull(cancelled, "cancelled"); + if (global::System.Object.ReferenceEquals(this, global::AspectTestingSpike.OrderStatus.Open)) + return (TResult)open.Invoke(); + if (global::System.Object.ReferenceEquals(this, global::AspectTestingSpike.OrderStatus.Shipped)) + return (TResult)shipped.Invoke(); + if (global::System.Object.ReferenceEquals(this, global::AspectTestingSpike.OrderStatus.Cancelled)) + return (TResult)cancelled.Invoke(); + throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.NotAMember("OrderStatus"); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + static global::AspectTestingSpike.OrderStatus global::System.IParsable.Parse(global::System.String s, global::System.IFormatProvider? provider) + { + global::System.ArgumentNullException.ThrowIfNull(s); + return (global::AspectTestingSpike.OrderStatus)global::AspectTestingSpike.OrderStatus.Parse(s); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static global::AspectTestingSpike.OrderStatus Parse(global::System.String? name) + { + if (name == "Open") + return (global::AspectTestingSpike.OrderStatus)(global::AspectTestingSpike.OrderStatus.Open ?? throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.StillInitializing("OrderStatus")); + if (name == "Shipped") + return (global::AspectTestingSpike.OrderStatus)(global::AspectTestingSpike.OrderStatus.Shipped ?? throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.StillInitializing("OrderStatus")); + if (name == "Cancelled") + return (global::AspectTestingSpike.OrderStatus)(global::AspectTestingSpike.OrderStatus.Cancelled ?? throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.StillInitializing("OrderStatus")); + throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.NoMember("OrderStatus"); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static global::AspectTestingSpike.OrderStatus Parse(global::System.String? name, global::System.Boolean ignoreCase) + { + var comparison = ignoreCase ? global::System.StringComparison.OrdinalIgnoreCase : global::System.StringComparison.Ordinal; + if (string.Equals(name, "Open", comparison)) + return (global::AspectTestingSpike.OrderStatus)(global::AspectTestingSpike.OrderStatus.Open ?? throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.StillInitializing("OrderStatus")); + if (string.Equals(name, "Shipped", comparison)) + return (global::AspectTestingSpike.OrderStatus)(global::AspectTestingSpike.OrderStatus.Shipped ?? throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.StillInitializing("OrderStatus")); + if (string.Equals(name, "Cancelled", comparison)) + return (global::AspectTestingSpike.OrderStatus)(global::AspectTestingSpike.OrderStatus.Cancelled ?? throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.StillInitializing("OrderStatus")); + throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.NoMember("OrderStatus"); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public override global::System.String ToString() + { + return (global::System.String)this.Name; + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + static global::System.Boolean global::System.IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] global::System.String? s, global::System.IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out global::AspectTestingSpike.OrderStatus result) + { + if (s == "Open") + { + result = global::AspectTestingSpike.OrderStatus.Open ?? throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.StillInitializing("OrderStatus"); + return (global::System.Boolean)true; + } + if (s == "Shipped") + { + result = global::AspectTestingSpike.OrderStatus.Shipped ?? throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.StillInitializing("OrderStatus"); + return (global::System.Boolean)true; + } + if (s == "Cancelled") + { + result = global::AspectTestingSpike.OrderStatus.Cancelled ?? throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.StillInitializing("OrderStatus"); + return (global::System.Boolean)true; + } + result = null !; + return (global::System.Boolean)false; + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static global::System.Boolean TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] global::System.String? name, [global::System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out global::AspectTestingSpike.OrderStatus? member) + { + if (name == "Open") + { + member = global::AspectTestingSpike.OrderStatus.Open ?? throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.StillInitializing("OrderStatus"); + return (global::System.Boolean)true; + } + if (name == "Shipped") + { + member = global::AspectTestingSpike.OrderStatus.Shipped ?? throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.StillInitializing("OrderStatus"); + return (global::System.Boolean)true; + } + if (name == "Cancelled") + { + member = global::AspectTestingSpike.OrderStatus.Cancelled ?? throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.StillInitializing("OrderStatus"); + return (global::System.Boolean)true; + } + member = null; + return (global::System.Boolean)false; + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static global::System.Boolean TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] global::System.String? name, global::System.Boolean ignoreCase, [global::System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out global::AspectTestingSpike.OrderStatus? member) + { + var comparison = ignoreCase ? global::System.StringComparison.OrdinalIgnoreCase : global::System.StringComparison.Ordinal; + if (string.Equals(name, "Open", comparison)) + { + member = global::AspectTestingSpike.OrderStatus.Open ?? throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.StillInitializing("OrderStatus"); + return (global::System.Boolean)true; + } + if (string.Equals(name, "Shipped", comparison)) + { + member = global::AspectTestingSpike.OrderStatus.Shipped ?? throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.StillInitializing("OrderStatus"); + return (global::System.Boolean)true; + } + if (string.Equals(name, "Cancelled", comparison)) + { + member = global::AspectTestingSpike.OrderStatus.Cancelled ?? throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.StillInitializing("OrderStatus"); + return (global::System.Boolean)true; + } + member = null; + return (global::System.Boolean)false; + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public sealed class OrderStatusJsonConverter : global::System.Text.Json.Serialization.JsonConverter + { + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public override global::AspectTestingSpike.OrderStatus Read(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + { + var name = reader.TokenType == global::System.Text.Json.JsonTokenType.String ? reader.GetString() : null; + if (name is not null) + { + if (name == "Open") + return (global::AspectTestingSpike.OrderStatus)(global::AspectTestingSpike.OrderStatus.Open ?? throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.StillInitializing("OrderStatus")); + if (name == "Shipped") + return (global::AspectTestingSpike.OrderStatus)(global::AspectTestingSpike.OrderStatus.Shipped ?? throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.StillInitializing("OrderStatus")); + if (name == "Cancelled") + return (global::AspectTestingSpike.OrderStatus)(global::AspectTestingSpike.OrderStatus.Cancelled ?? throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.StillInitializing("OrderStatus")); + } + throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.NoMemberInJson("OrderStatus"); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public override global::AspectTestingSpike.OrderStatus ReadAsPropertyName(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) + { + var name = reader.GetString(); + if (name == "Open") + return (global::AspectTestingSpike.OrderStatus)(global::AspectTestingSpike.OrderStatus.Open ?? throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.StillInitializing("OrderStatus")); + if (name == "Shipped") + return (global::AspectTestingSpike.OrderStatus)(global::AspectTestingSpike.OrderStatus.Shipped ?? throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.StillInitializing("OrderStatus")); + if (name == "Cancelled") + return (global::AspectTestingSpike.OrderStatus)(global::AspectTestingSpike.OrderStatus.Cancelled ?? throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.StillInitializing("OrderStatus")); + throw global::CodoMetis.TypeKit.CompilerServices.GeneratedClosedSets.NoMemberInJson("OrderStatus"); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public override void Write(global::System.Text.Json.Utf8JsonWriter writer, global::AspectTestingSpike.OrderStatus value, global::System.Text.Json.JsonSerializerOptions options) + { + writer.WriteStringValue((string)value.Name); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public override void WriteAsPropertyName(global::System.Text.Json.Utf8JsonWriter writer, global::AspectTestingSpike.OrderStatus value, global::System.Text.Json.JsonSerializerOptions options) + { + writer.WritePropertyName((string)value.Name); + } + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public sealed class OrderStatusTypeConverter : global::System.ComponentModel.TypeConverter + { + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public override global::System.Boolean CanConvertFrom(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Type sourceType) + { + return (global::System.Boolean)(sourceType == typeof(global::System.String) || base.CanConvertFrom(context, sourceType)); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public override global::System.Boolean CanConvertTo(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Type? destinationType) + { + return (global::System.Boolean)(destinationType == typeof(global::System.String) || base.CanConvertTo(context, destinationType)); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public override global::System.Object? ConvertFrom(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Globalization.CultureInfo? culture, global::System.Object value) + { + if (value is string s) + { + return global::AspectTestingSpike.OrderStatus.Parse(s); + } + return (global::System.Object? )base.ConvertFrom(context, culture, value); + } + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public override global::System.Object? ConvertTo(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Globalization.CultureInfo? culture, global::System.Object? value, global::System.Type destinationType) + { + if (destinationType == typeof(global::System.String) && value is not null) + { + return ((global::AspectTestingSpike.OrderStatus)value).Name; + } + return (global::System.Object? )base.ConvertTo(context, culture, value, destinationType); + } + } +} \ No newline at end of file diff --git a/spikes/AspectTestingSpike/Directory.Build.props b/spikes/AspectTestingSpike/Directory.Build.props new file mode 100644 index 00000000..742cd979 --- /dev/null +++ b/spikes/AspectTestingSpike/Directory.Build.props @@ -0,0 +1,13 @@ + + + + net10.0 + 14 + enable + enable + + diff --git a/spikes/AspectTestingSpike/README.md b/spikes/AspectTestingSpike/README.md new file mode 100644 index 00000000..440d02fe --- /dev/null +++ b/spikes/AspectTestingSpike/README.md @@ -0,0 +1,124 @@ +# Spike: Metalama.Testing.AspectTesting beside xunit.v3 on Microsoft Testing Platform + +**Question.** Can `Metalama.Testing.AspectTesting` 2026.1.30 (MIT) test this repository's aspects? It +runs on xunit v2 (it depends on `xunit.extensibility.core`, `xunit.extensibility.execution` and +`xunit.assert` 2.9.3), and this repository runs xunit.v3 4.0.2 on Microsoft Testing Platform, chosen for +`dotnet test` by `"test": {"runner": "Microsoft.Testing.Platform"}` in the root `global.json`. In detail: + +1. Can an aspect-test project live in a project of its own and run its tests, and with which runner? +2. Added to `CodoMetis.TypeKit.slnx`, would `dotnet test --solution` still run the rest? +3. If not, can it run as a separate command, locally and in CI? +4. Does it test the real `src/CodoMetis.TypeKit.Generators`, whose aspects are `internal` and applied by a + `TransitiveProjectFabric`, through a project reference rather than a copy? +5. What do the expected files look like for a plain value object, a validated one and a closed set, and + can a reviewer verify one by eye? (GitHub issue #93; the motivation is the reviewer.) + +**Answer: it works, beside the solution, not inside its test run.** Measured 2026-10-28 on macOS arm64, +.NET SDK 10.0.403 (the root pin 10.0.402 with `latestPatch`), Metalama.Framework and +Metalama.Testing.AspectTesting 2026.1.30, xunit 2.9.3, xunit.runner.visualstudio 3.1.5, +Microsoft.NET.Test.Sdk 17.14.1 (VSTest 18.7.0). + +1. Yes, as an xunit v2 project on VSTest, which needs a `global.json` of its own: the root one makes + `dotnet test` refuse it. With this folder's `global.json` (the same SDK pin, no `test` key), + `dotnet test` run from this folder passes 4 of 4. +2. No. One VSTest project in the solution makes `dotnet test --solution` refuse the whole run, before + building, and no test of the other projects runs. Marked `IsTestProject=false`, the project is skipped + by the solution's test run, which then runs the rest, but VSTest's `dotnet test` then also runs + nothing and exits 0. +3. Yes, as a separate command: `dotnet test` from this folder, or `dotnet vstest` on the built dll from + anywhere. CI would need one more step with `working-directory` set to this folder. That is + plausible but was not run in CI. +4. Yes. The project references the real generators project, and the fabric applies inside the test + framework's compilation of each input file: the closed set, the plain value object and the validated + one come out fully woven, and a refusal comes out as its CMTK diagnostic. A defect seeded in a scratch + copy of `src/` fails the two tests whose output it changes. +5. The expected files have 235 to 349 lines each (15 to 23 KB) for an input of 5 to 17 lines. They are + formatted, one member per block, so one member can be checked by eye. A whole file cannot, because + it has every generated member in full, with every name fully qualified. Reading them turned up dead + code after a `return` in every generated `TryParse(string?, …)` (finding 12). + +## Layout + +| Path | Role | +|---|---| +| `global.json` | The root SDK pin without the `test` key, so `dotnet test` run from here uses VSTest | +| `Directory.Build.props` | Stops the root props (warnings as errors, documentation, package validation) from reaching the spike | +| `AspectTests/AspectTests.csproj` | xunit 2.9.3, Microsoft.NET.Test.Sdk, xunit.runner.visualstudio, Metalama.Testing.AspectTesting, and a project reference to `src/CodoMetis.TypeKit.Generators`. Versions are inline (`spikes/Directory.Packages.props` turns central management off) | +| `AspectTests/OrderId.cs` → `OrderId.t.cs`, `OrderId.OrderIdExtensions.i.cs` | A plain value object, `IValue`, and the companion class it gets | +| `AspectTests/Email.cs` → `Email.t.cs`, `Email.EmailExtensions.i.cs` | The root README's validated `Email : IValidatedValue` | +| `AspectTests/OrderStatus.cs` → `OrderStatus.t.cs` | The root README's closed set | +| `AspectTests/NotReadonly.cs` → `NotReadonly.t.cs` | A refusal: a value object that is not `readonly` (CMTK1002) | + +Every `.cs` file that is not a `.t.cs` or `.i.cs` is a test case, and its `.t.cs` holds the expected +output. A type that the aspects introduce in a file of its own gets a `.i.cs` file. Both kinds of file +are compared. + +## Running it + +```bash +cd spikes/AspectTestingSpike +dotnet test AspectTests/AspectTests.csproj # VSTest, through this folder's global.json: 4 passed + +# from anywhere, once built +dotnet vstest spikes/AspectTestingSpike/AspectTests/bin/Debug/net10.0/AspectTests.dll +``` + +A new or changed case writes its actual output to `AspectTests/obj/transformed/net10.0/`. On the first +run the framework creates a placeholder `.t.cs` (and `.i.cs`) that holds a `// TODO` line, so the test +fails. Accept the output by copying it over, which is what the package's `AcceptTestOutput` target does +(`dotnet build -t:AcceptTestOutput`). Commit first, then review the diff. + +## Findings + +| # | Claim | Result | Evidence | +|---|---|---|---| +| 1 | The package works on xunit v2 and VSTest, not xunit.v3 or MTP | measured | Its nuspec's dependencies for `net8.0` are `xunit.assert`, `xunit.extensibility.core` and `xunit.extensibility.execution` 2.9.3. Its `build/….targets` registers `Xunit.TestFrameworkAttribute("Metalama.Testing.AspectTesting.AspectTestFramework", …)`, an xunit v2 extension point. The 2026.1 documentation's sample project uses `Microsoft.NET.Test.Sdk`, `xunit` 2.x and `xunit.runner.visualstudio` ([snapshot-testing.md, release/2026.1](https://github.com/metalama/metalama.documentation/blob/release/2026.1/content/conceptual/aspects/testing/snapshot-testing.md)) | +| 2 | Under the root `global.json`, `dotnet test` refuses the project | measured | `dotnet test --project spikes/AspectTestingSpike/AspectTests/AspectTests.csproj` from the root: exit 1, `global.json defines test runner to be Microsoft.Testing.Platform. All projects must use that test runner. The following test projects are using VSTest test runner: AspectTests.csproj`. The same through `build-verdict test --project …`, which always runs from the root | +| 3 | With this folder's `global.json`, `dotnet test` from this folder runs it | measured | exit 0, `Passed! - Failed: 0, Passed: 4, Skipped: 0, Total: 4, Duration: 3 s`. The SDK reads the `global.json` nearest the current directory, so the same command from the root is finding 2 | +| 4 | `dotnet vstest` runs the built dll from the root, whatever `global.json` says | measured | exit 0, `Passed! - Failed: 0, Passed: 4` (with `IsTestProject` `false` and `true` alike) | +| 5 | One VSTest project in the solution stops `dotnet test --solution` for every project | measured on a scratch `.slnx` at the root (`tools/PrHygiene`, `test/CodoMetis.TypeKit.PrHygiene.Tests`, this project): exit 1, the message of finding 2, nothing built and no test run. The control, the same `.slnx` without this project: exit 0, 41 passed. The real `CodoMetis.TypeKit.slnx` was not changed | +| 6 | `IsTestProject=false` hides it from the solution's test run | measured on the same `.slnx`: exit 0, 41 passed. `AspectTests` is neither built nor named by `dotnet test`. `build-verdict build` of that `.slnx` builds it (7 projects) | +| 7 | …and VSTest's `dotnet test` on it then runs nothing and exits 0 | measured: from this folder, `dotnet test AspectTests/AspectTests.csproj` prints the restore and stops, exit 0, no test line. `-p:IsTestProject=true` brings back 4 passed. Setting it `false` only when `$(SolutionName)` is set does not help: the solution's test run still names the project as VSTest (exit 1, the message of finding 2) | +| 8 | The project's own compilation cannot hold the inputs | measured: the package sets `MetalamaEnabled=False` for the project, so the C# compiler sees the inputs unwoven, and the closed set fails with CS0535 (`All`, `Name`, `QualifiedName`, `TryParse` not implemented). The inputs are removed from `Compile`. The framework still finds them in the folder, as the documentation says it does with every file. The documented alternative, `#if METALAMA` around code that uses introduced members, does not help a declaration whose interface needs those members | +| 9 | The internal aspects and the `TransitiveProjectFabric` of the real generators apply in each test's compilation | measured: `OrderStatus.t.cs` has `_all`, `_name`, the static constructor naming each member, `All`, `Parse`, `TryParse`, the JSON and type converters. `OrderId.t.cs` and `Email.t.cs` have `_value`, the constructors, `Value`, `From`, `TryFrom`, `FromKnownGood`, parsing, formatting, comparison and converters, and each a `.i.cs` with its `GetValue` / `ValueOrNull` companion class. The project lists no compile-time assembly (`compile-time-assemblies.txt` is empty), so the framework reads the fabric from the referenced `CodoMetis.TypeKit.Generators.dll` in `assemblies.txt` (218 references) | +| 10 | An aspect diagnostic is part of the expected output | measured: `NotReadonly.t.cs` is the one line ``// Error CMTK1002 on `Draft`: `'Draft' is a value object and must be declared readonly. See …` ``, with no code. An expected error is a passing test | +| 11 | The tests catch a change in the generators | measured twice. Editing one name in the expected `Email.EmailExtensions.i.cs` fails `Email` (`Expected: …GetValu(this… Actual: …GetValue(this…`). In a scratch copy of `src/` and this spike, outside the repository, `ValueObjectParsableAspect.Provider` was seeded to ignore the caller's provider. That failed `OrderId` and `Email` and passed `OrderStatus` and `NotReadonly`, which do not parse through it (`Failed: 2, Passed: 2`, 7 changed lines in `OrderId.t.cs`). The repository's `src/` was not touched | +| 12 | Reading the snapshot shows what a test of behaviour does not | measured: every generated `TryParse(string?, IFormatProvider?, out …)` has an unreachable block after its `return`, a `__inner` declaration and a `try { __inner = …; } catch (FormatException) { return false; }`. It is in `OrderId.t.cs`, in `Email.t.cs`, and in the woven probes (`test/CodoMetis.TypeKit.Generators.Probes/obj/Debug/net10.0/metalama/Probes.cs`, `ProbeId`), so it is the real output, not the framework's. Its source is `ValueObjectParsableAspect.Templates.cs`, `TryParseTemplate`: the two `meta.InsertStatement` calls come after a compile-time `if (…) return …;` and are still emitted after the `return` | +| 13 | Size of the expected outputs | measured, with LF line endings: `OrderId.t.cs` 258 lines (19,721 bytes, longest line 1,078 characters, the base list of 14 interfaces), `Email.t.cs` 349 (23,264 bytes), `OrderStatus.t.cs` 235 (15,520 bytes), each `.i.cs` 18. 47 to 62 lines per file are longer than 120 characters. `[CompilerGenerated]` appears 25 to 52 times per file, `(global::System.Boolean)(bool)` 7 to 9 times | +| 14 | The output is reformatted, the input included | measured: the hand-written `Create` comes back with its `if` statements reformatted, and every member is on lines of its own. The woven file that the build writes (`MetalamaEmitCompilerTransformedFiles`, as the probes do) puts a member's attributes, signature and first statement on one line | +| 15 | Line endings do not matter to the comparison | measured on macOS: the framework wrote its output with CRLF, and git stores it with LF (`core.autocrlf`). With the expected files converted to LF, as a checkout gives them, the tests still pass (4 of 4). On Windows and Linux it was not run | +| 16 | Cost | measured: 4 tests in 2 to 3 s once built. The project builds the four `src/` projects it needs, in Debug | + +## Consequences for the design + +**A. It cannot join the solution's test run.** With MTP selected in the root `global.json`, a VSTest +project in the solution stops the whole test run (finding 5). The ways around that leave the project +built but untested, and a project that runs nothing and exits 0 is the failure this repository is most +wary of (finding 7). The aspect tests are a command of their own: `dotnet test` from this folder, +which has its own `global.json`, or `dotnet vstest` on the built dll. Only an MTP host for xunit v2 +would remove the split. One exists on nuget.org (`YTest.MTP.XUnit2` 1.0.3). It is third-party, was not +tried, and nothing here shows it can host Metalama's custom test framework. + +**B. CI would need one step and a check that it ran.** The step is `dotnet test +AspectTests/AspectTests.csproj` with `working-directory: spikes/AspectTestingSpike` (or wherever the +project ends up), after `setup-dotnet` with the root `global.json`, whose SDK is the same. Not run in +CI. Line endings do not matter to the comparison (finding 15), but whether the output is otherwise the +same on Windows and Linux was not measured. The CI matrix has both. + +**C. It answers "what does the generator emit", at a reviewer's scale per member.** It tests the real +generators through the real fabric (findings 9 to 11), so it adds to the woven probes, the build +outcomes and `GeneratedSurface.verified.txt` without a copy of anything. A change to a template shows +up as a diff of the lines it changed, which is what issue #93 asks for. A whole `.t.cs` is too long to +verify by eye (finding 13), but a diff is short: the seeded defect changed 7 lines. Finding 12 shows the +reading finds things. + +**D. The output is too verbose for the READMEs as it is.** Issue #92's generated-code blocks would need a +trimmed, simplified excerpt. The fully qualified names, the cast noise and a 1,079-character base list +are not what a consumer should read. A snapshot could be the source the excerpt is checked against. + +**E. Still open.** +- Whether `// ` (trim the output to the marked declarations, per the documentation) or the + HTML writer makes a whole file reviewable. +- Expected output on Windows and Linux (B). +- The IDE: Rider needs its xunit adapter configured to discover these tests (per the documentation), + and was not tried. diff --git a/spikes/AspectTestingSpike/global.json b/spikes/AspectTestingSpike/global.json new file mode 100644 index 00000000..0b257e34 --- /dev/null +++ b/spikes/AspectTestingSpike/global.json @@ -0,0 +1,13 @@ +{ + "//": [ + "The repository's SDK pin, without its test key: Metalama.Testing.AspectTesting runs on xunit v2", + "through VSTest, which dotnet test refuses once global.json selects Microsoft.Testing.Platform.", + "dotnet reads the global.json nearest the current directory, so this one applies only to a", + "command run from this folder or below." + ], + "sdk": { + "version": "10.0.402", + "rollForward": "latestPatch", + "allowPrerelease": false + } +} diff --git a/src/CodoMetis.TypeKit.Analyzers/AnalyzerReleases.Shipped.md b/src/CodoMetis.TypeKit.Analyzers/AnalyzerReleases.Shipped.md index 7721d6b8..10d875a9 100644 --- a/src/CodoMetis.TypeKit.Analyzers/AnalyzerReleases.Shipped.md +++ b/src/CodoMetis.TypeKit.Analyzers/AnalyzerReleases.Shipped.md @@ -62,4 +62,4 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------------------------------------------------------------- CMTK0014 | Usage | Warning | UndefinedEnumValueAnalyzer, a FaultKind or ConstructorAccessibility that names no member -## Release 2.2.0 +## Release 2.3.0 diff --git a/src/CodoMetis.TypeKit.Analyzers/CodeFixes/MissingCreateCodeFixProvider.cs b/src/CodoMetis.TypeKit.Analyzers/CodeFixes/MissingCreateCodeFixProvider.cs new file mode 100644 index 00000000..a6922c6b --- /dev/null +++ b/src/CodoMetis.TypeKit.Analyzers/CodeFixes/MissingCreateCodeFixProvider.cs @@ -0,0 +1,245 @@ +using System.Collections.Immutable; +using System.Composition; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace CodoMetis.TypeKit.Analyzers; + +/// +/// The fix for a validated value object that lacks Create, the one member of +/// IValidatedValue<TSelf, T, TFault> written by hand: the compiler's CS0535 at the +/// interface. It writes Create as a public static method that accepts every value, for the +/// author to add the rules to. +/// +/// +/// +/// Offered where the interface's TSelf is the type itself and no member of the type named +/// Create takes a T, which would clash with the one written. The types are spelled as +/// they are named at the type, so a fault or a wrapped type of another namespace is qualified, and +/// the body builds the instance with the private constructor the generators add. +/// +/// +/// Like the fix for CMTK0011 it writes its own trivia and asks no formatter: Create becomes the +/// first member, indented as the members are; a declaration ending in ;, or an empty body on one +/// line, gets a body on lines of its own. A body on one line that holds members gets no fix. +/// +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MissingCreateCodeFixProvider))] +[Shared] +public sealed class MissingCreateCodeFixProvider : CodeFixProvider +{ + /// The compiler's "does not implement interface member", which a missing Create is. + public const string DiagnosticId = "CS0535"; + + /// The metadata name of the validated value-object contract; held to the real type by its tests. + public const string ValidatedValueMetadataName = "CodoMetis.TypeKit.ValueObjects.IValidatedValue`3"; + + /// The member the fix writes; held to the contract's by its tests. + public const string CreateName = "Create"; + + /// + public override ImmutableArray FixableDiagnosticIds => [DiagnosticId]; + + /// + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + /// + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (root is null) return; + + var model = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + if (model is null) return; + + var validated = model.Compilation.GetTypesByMetadataName(ValidatedValueMetadataName); + if (validated.IsEmpty) return; + + foreach (var diagnostic in context.Diagnostics) + { + // One CS0535 per missing member, each at the base type that brings it: only the one for + // Create, which the message names whatever the language. + if (diagnostic.GetMessage(CultureInfo.InvariantCulture).IndexOf($".{CreateName}(", System.StringComparison.Ordinal) < 0) continue; + + var node = root.FindNode(diagnostic.Location.SourceSpan); + if (node.FirstAncestorOrSelf() is not { } written) continue; + if (node.FirstAncestorOrSelf() is not { } type || !CanLayOut(type)) continue; + if (model.GetDeclaredSymbol(type, context.CancellationToken) is not { } self) continue; + if (!Brings(model.GetTypeInfo(written.Type, context.CancellationToken).Type, validated)) continue; + if (MissingCreate(self, validated) is not { } create) continue; + + var position = type.OpenBraceToken.IsKind(SyntaxKind.OpenBraceToken) && !type.OpenBraceToken.IsMissing + ? type.OpenBraceToken.Span.End + : type.Identifier.SpanStart; + + context.RegisterCodeFix( + CodeAction.Create( + "Add Create", + cancellationToken => AddCreateAsync(context.Document, root, type, Signature(create, self, model, position), cancellationToken), + equivalenceKey: nameof(MissingCreateCodeFixProvider)), + diagnostic); + } + } + + /// Whether the base type written is the contract, or an interface that derives from it. + private static bool Brings(ITypeSymbol? written, ImmutableArray validated) => + written is INamedTypeSymbol named + && (IsValidated(named, validated) || named.AllInterfaces.Any(candidate => IsValidated(candidate, validated))); + + private static bool IsValidated(INamedTypeSymbol type, ImmutableArray validated) => + validated.Contains(type.OriginalDefinition, SymbolEqualityComparer.Default); + + /// + /// The contract's Create that the type leaves unimplemented, closed over the type's arguments, + /// or : where TSelf is another type, the instance to return is not this + /// type's to build, and a member of the type named Create taking a T would clash. + /// + private static IMethodSymbol? MissingCreate(INamedTypeSymbol self, ImmutableArray validated) + { + foreach (var contract in self.AllInterfaces.Where(candidate => IsValidated(candidate, validated))) + { + if (!SymbolEqualityComparer.Default.Equals(contract.TypeArguments[0], self)) continue; + + var create = contract.GetMembers(CreateName).OfType().FirstOrDefault(method => method.IsStatic && method.Parameters.Length == 1); + if (create is null || self.FindImplementationForInterfaceMember(create) is not null) continue; + + var wrapped = create.Parameters[0].Type; + var clash = self.GetMembers(CreateName).OfType() + .Any(method => method.Parameters.Length == 1 && SymbolEqualityComparer.Default.Equals(method.Parameters[0].Type, wrapped)); + + return clash ? null : create; + } + + return null; + } + + /// What the method is made of, its types spelled as the type's own code would name them. + private static Parts Signature(IMethodSymbol create, INamedTypeSymbol self, SemanticModel model, int position) + { + var parameter = create.Parameters[0]; + var fault = ((INamedTypeSymbol)create.ReturnType).TypeArguments[1]; + + // A fault that is an enum names its first member in the comment, the shape a rule takes. + var example = fault is INamedTypeSymbol { TypeKind: TypeKind.Enum } enumeration + && enumeration.GetMembers().OfType().FirstOrDefault(field => field.HasConstantValue) is { } member + ? $"{fault.ToMinimalDisplayString(model, position)}.{Escaped(member.Name)}" + : "..."; + + return new Parts( + create.ReturnType.ToMinimalDisplayString(model, position), + parameter.Type.ToMinimalDisplayString(model, position), + Escaped(parameter.Name), + self.ToMinimalDisplayString(model, position), + example); + } + + private static string Escaped(string name) => SyntaxFacts.GetKeywordKind(name) == SyntaxKind.None ? name : "@" + name; + + /// + /// Whether the fix can lay the method out: in a body of several lines, an empty body on one line, or a + /// declaration ending in ;, whose line starts with the type, not a body on one line that holds + /// members. + /// + private static bool CanLayOut(TypeDeclarationSyntax type) + { + if (!HasBody(type)) return true; + return type.Members.Count == 0 || type.OpenBraceToken.TrailingTrivia.Any(trivia => trivia.IsKind(SyntaxKind.EndOfLineTrivia)); + } + + private static bool HasBody(TypeDeclarationSyntax type) => + type.OpenBraceToken.IsKind(SyntaxKind.OpenBraceToken) && !type.OpenBraceToken.IsMissing; + + private static async Task AddCreateAsync(Document document, SyntaxNode root, TypeDeclarationSyntax type, Parts parts, CancellationToken cancellationToken) + { + var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + return document.WithSyntaxRoot(root.ReplaceNode(type, WithCreate(type, parts, text))); + } + + /// + /// The type with Create as its first member. In a body of several lines it goes on lines of + /// its own, indented as the members are and ending as the brace's line does, a blank line before the + /// next member. A declaration ending in ;, or an empty body on one line, gets a body on lines of + /// its own at the type's indentation, ending as the line of the ; or the brace does, or the + /// nearest line before it, the ;'s trivia, a comment included, kept after it. + /// + private static TypeDeclarationSyntax WithCreate(TypeDeclarationSyntax type, Parts parts, SourceText text) + { + var end = HasBody(type) ? type.OpenBraceToken.TrailingTrivia.LastOrDefault(trivia => trivia.IsKind(SyntaxKind.EndOfLineTrivia)) : default; + + if (end.IsKind(SyntaxKind.EndOfLineTrivia)) + { + var eol = end.ToString(); + var indent = TypeLayout.MemberIndentation(type, text); + var outer = TypeLayout.Indentation(text, type.CloseBraceToken.SpanStart) ?? ""; + var unit = indent.Length > outer.Length && indent.StartsWith(outer, System.StringComparison.Ordinal) ? indent.Substring(outer.Length) : TypeLayout.Unit(indent, text); + var after = type.Members.Count > 0 ? SyntaxFactory.TriviaList(SyntaxFactory.EndOfLine(eol), SyntaxFactory.EndOfLine(eol)) : SyntaxFactory.TriviaList(SyntaxFactory.EndOfLine(eol)); + + return type.WithMembers(type.Members.Insert(0, Method(parts, indent, unit, eol, after))); + } + + // A body opened here: the brace on a line of its own at the type's indentation. + var typeIndent = TypeLayout.Indentation(text, type.SpanStart) ?? ""; + var unitHere = TypeLayout.Unit(typeIndent, text); + var ending = HasBody(type) ? type.CloseBraceToken : type.SemicolonToken; + var lineEnd = TypeLayout.LineEndingAt(text, ending.SpanStart); + var closing = ending.TrailingTrivia; + var last = (HasBody(type) ? type.OpenBraceToken : type.SemicolonToken).GetPreviousToken(); + + var opened = type.ReplaceToken(last, last.WithTrailingTrivia(last.TrailingTrivia.Where(trivia => !trivia.IsKind(SyntaxKind.WhitespaceTrivia)))); + var lead = SyntaxFactory.TriviaList(SyntaxFactory.EndOfLine(lineEnd)).AddRange(Whitespace(typeIndent)); + var open = SyntaxFactory.Token(lead, SyntaxKind.OpenBraceToken, SyntaxFactory.TriviaList(SyntaxFactory.EndOfLine(lineEnd))); + var close = SyntaxFactory.Token(Whitespace(typeIndent), SyntaxKind.CloseBraceToken, closing); + + return opened.WithSemicolonToken(default) + .WithOpenBraceToken(open) + .WithMembers(SyntaxFactory.SingletonList(Method(parts, typeIndent + unitHere, unitHere, lineEnd, SyntaxFactory.TriviaList(SyntaxFactory.EndOfLine(lineEnd))))) + .WithCloseBraceToken(close); + } + + /// + /// Create parsed from its text, so every token carries the trivia written there and none is + /// elastic trivia a code action's cleanup would lay out again. + /// + private static MemberDeclarationSyntax Method(Parts parts, string indent, string unit, string eol, SyntaxTriviaList trailing) + { + var source = + $"public static {parts.Returns} {CreateName}({parts.Wrapped} {parts.Parameter}){eol}" + + $"{indent}{{{eol}" + + $"{indent}{unit}// Add your rules: if (...) return Result.Error({parts.Example});{eol}" + + $"{indent}{unit}return new {parts.Self}({parts.Parameter});{eol}" + + $"{indent}}}"; + + return (SyntaxFactory.ParseMemberDeclaration(source) ?? throw new System.InvalidOperationException($"'{source}' does not parse as a method.")) + .WithLeadingTrivia(Whitespace(indent)) + .WithTrailingTrivia(trailing); + } + + private static SyntaxTriviaList Whitespace(string indent) => + indent.Length == 0 ? SyntaxFactory.TriviaList() : SyntaxFactory.TriviaList(SyntaxFactory.Whitespace(indent)); + + private sealed class Parts + { + public Parts(string returns, string wrapped, string parameter, string self, string example) + { + Returns = returns; + Wrapped = wrapped; + Parameter = parameter; + Self = self; + Example = example; + } + + public string Returns { get; } + public string Wrapped { get; } + public string Parameter { get; } + public string Self { get; } + public string Example { get; } + } +} diff --git a/src/CodoMetis.TypeKit.Analyzers/CodeFixes/RequiredParameterlessConstructorCodeFixProvider.cs b/src/CodoMetis.TypeKit.Analyzers/CodeFixes/RequiredParameterlessConstructorCodeFixProvider.cs index a39d4023..f0558366 100644 --- a/src/CodoMetis.TypeKit.Analyzers/CodeFixes/RequiredParameterlessConstructorCodeFixProvider.cs +++ b/src/CodoMetis.TypeKit.Analyzers/CodeFixes/RequiredParameterlessConstructorCodeFixProvider.cs @@ -191,7 +191,7 @@ private static TypeDeclarationSyntax WithConstructor(TypeDeclarationSyntax type, // The line ending of the brace the constructor follows, so a CRLF file stays CRLF. var eol = end.ToString(); - var indent = MemberIndentation(type, text); + var indent = TypeLayout.MemberIndentation(type, text); var leading = indent.Length == 0 ? SyntaxFactory.TriviaList() : SyntaxFactory.TriviaList(SyntaxFactory.Whitespace(indent)); var trailing = type.Members.Count > 0 ? SyntaxFactory.TriviaList(SyntaxFactory.EndOfLine(eol), SyntaxFactory.EndOfLine(eol)) @@ -209,31 +209,6 @@ private static MemberDeclarationSyntax Constructor(string source, SyntaxTriviaLi .WithLeadingTrivia(leading) .WithTrailingTrivia(trailing); - /// - /// The indentation of the first member's line; without a member, the closing brace's, or the - /// type's, one level deeper, a tab where the file indents with tabs. - /// - private static string MemberIndentation(TypeDeclarationSyntax type, SourceText text) - { - if (type.Members.Count > 0 && Indentation(text, type.Members[0].SpanStart) is { } member) return member; - - var outer = Indentation(text, type.CloseBraceToken.SpanStart) ?? Indentation(text, type.SpanStart) ?? ""; - var tabs = outer.IndexOf('\t') >= 0 || text.Lines.Any(line => line.Span.Length > 0 && text[line.Start] == '\t'); - - return outer + (tabs ? "\t" : " "); - } - - /// The whitespace that starts the line of , or when anything else stands before it. - private static string? Indentation(SourceText text, int position) - { - var line = text.Lines.GetLineFromPosition(position); - var end = line.Start; - - while (end < position && text[end] is ' ' or '\t') end++; - - return end == position ? text.ToString(TextSpan.FromBounds(line.Start, end)) : null; - } - /// The accessibility's keywords, the first led by , the last followed by . private static IEnumerable Keywords(Accessibility accessibility, SyntaxTriviaList leading, SyntaxTriviaList trailing) { diff --git a/src/CodoMetis.TypeKit.Analyzers/CodeFixes/TypeLayout.cs b/src/CodoMetis.TypeKit.Analyzers/CodeFixes/TypeLayout.cs new file mode 100644 index 00000000..68eebada --- /dev/null +++ b/src/CodoMetis.TypeKit.Analyzers/CodeFixes/TypeLayout.cs @@ -0,0 +1,55 @@ +using System.Linq; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace CodoMetis.TypeKit.Analyzers; + +/// +/// How a type's members are laid out, read from the text, for a fix that adds one and asks no +/// formatter. +/// +internal static class TypeLayout +{ + /// + /// The indentation of the first member's line; without a member, the closing brace's, or the + /// type's, one level deeper, a tab where the file indents with tabs. + /// + public static string MemberIndentation(TypeDeclarationSyntax type, SourceText text) + { + if (type.Members.Count > 0 && Indentation(text, type.Members[0].SpanStart) is { } member) return member; + + var outer = Indentation(text, type.CloseBraceToken.SpanStart) ?? Indentation(text, type.SpanStart) ?? ""; + return outer + Unit(outer, text); + } + + /// One level of indentation: a tab where the file indents with tabs, otherwise four spaces. + public static string Unit(string indentation, SourceText text) => + indentation.IndexOf('\t') >= 0 || text.Lines.Any(line => line.Span.Length > 0 && text[line.Start] == '\t') ? "\t" : " "; + + /// The whitespace that starts the line of , or when anything else stands before it. + public static string? Indentation(SourceText text, int position) + { + var line = text.Lines.GetLineFromPosition(position); + var end = line.Start; + + while (end < position && text[end] is ' ' or '\t') end++; + + return end == position ? text.ToString(TextSpan.FromBounds(line.Start, end)) : null; + } + + /// + /// The line ending of the line holding , or of the nearest line before it + /// that has one, so lines added there end as their neighbours do in a file whose endings are mixed; + /// LF in a file of one line. + /// + public static string LineEndingAt(SourceText text, int position) + { + for (var number = text.Lines.GetLineFromPosition(position).LineNumber; number >= 0; number--) + { + var line = text.Lines[number]; + if (line.EndIncludingLineBreak > line.End) return text.ToString(TextSpan.FromBounds(line.End, line.EndIncludingLineBreak)); + } + + return "\n"; + } +} diff --git a/src/CodoMetis.TypeKit.Analyzers/DefaultProducingCallAnalyzer.cs b/src/CodoMetis.TypeKit.Analyzers/DefaultProducingCallAnalyzer.cs index 34e5e66f..23cde09d 100644 --- a/src/CodoMetis.TypeKit.Analyzers/DefaultProducingCallAnalyzer.cs +++ b/src/CodoMetis.TypeKit.Analyzers/DefaultProducingCallAnalyzer.cs @@ -41,6 +41,15 @@ namespace CodoMetis.TypeKit.Analyzers; /// where the generated TryFrom does not bind and neither does OrDefault on it; /// X.TryFrom(…).OrDefault() is recognised there by name. /// +/// +/// With a message of its own, the rule also reports FirstOrNone, SingleOrNone, +/// LastOrNone, FirstOrError and LastOrError called on a query with a delegate for +/// a condition: a Func variable, a field, a method group, an anonymous method, a call that +/// returns one. The query's overloads take an expression, so the call binds to the sequence's, which +/// reads the whole query and runs the condition in memory. A lambda written at the call and an +/// Expression bind to the query's, and a receiver typed as a sequence (AsEnumerable(), +/// a cast) reads the query in full on purpose. +/// /// [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class DefaultProducingCallAnalyzer : DiagnosticAnalyzer @@ -56,10 +65,22 @@ public sealed class DefaultProducingCallAnalyzer : DiagnosticAnalyzer helpLinkUri: DiagnosticIds.HelpLink(DiagnosticIds.DefaultProducingCall) ); + /// A delegate condition on a query, which binds the call to its sequence overload. + private static readonly DiagnosticDescriptor QueryDelegateRule = new( + DiagnosticIds.DefaultProducingCall, + title: "A delegate condition reads the whole query", + messageFormat: "'{0}' on the query binds to its IEnumerable overload, since the condition is a '{1}' and not an expression: it reads every row and runs the condition in memory. {2}.", + category: "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "FirstOrNone, SingleOrNone and FirstOrError on an IQueryable take the condition as an Expression>, which the query provider runs. A Func held in a variable, a field, a method group or a call that returns one is no expression, so the call binds to the overload on IEnumerable: the query is read in full and the condition runs in memory, on every row.", + helpLinkUri: DiagnosticIds.HelpLink(DiagnosticIds.DefaultProducingCall) + ); + private const string TryFrom = "TryFrom"; /// - public override ImmutableArray SupportedDiagnostics => [Rule]; + public override ImmutableArray SupportedDiagnostics => [Rule, QueryDelegateRule]; /// public override void Initialize(AnalysisContext context) @@ -71,6 +92,11 @@ public override void Initialize(AnalysisContext context) { if (TypeKitSymbols.Resolve(start.Compilation) is not { } symbols) return; + var queryable = start.Compilation.GetTypeByMetadataName("System.Linq.IQueryable`1"); + var func = start.Compilation.GetTypeByMetadataName("System.Func`2"); + if (queryable is not null && func is not null && QueryDelegateForms(symbols, func) is { Count: > 0 } delegates) + start.RegisterOperationAction(operation => AnalyzeQueryDelegate(operation, delegates, queryable, func), OperationKind.Invocation); + var forms = Forms(start.Compilation, symbols); if (forms.Count == 0) return; @@ -81,6 +107,77 @@ public override void Initialize(AnalysisContext context) }); } + /// + /// query.FirstOrNone(isBeta): a sequence overload that takes a Func, called on a + /// receiver that is a query before the implicit conversion to IEnumerable<T>. + /// + private static void AnalyzeQueryDelegate(OperationAnalysisContext context, Dictionary forms, INamedTypeSymbol queryable, INamedTypeSymbol func) + { + var invocation = (IInvocationOperation)context.Operation; + var method = invocation.TargetMethod; + + if (!forms.TryGetValue(method.OriginalDefinition, out var advice)) return; + + // An extension block's member has its receiver as the instance; the static form, Option.FirstOrNone(query, f), + // and the block's implementation have it as the first argument. + var receiver = invocation.Instance ?? invocation.Arguments.FirstOrDefault(argument => argument.Parameter?.Ordinal == 0)?.Value; + + // Only the conversion the compiler wrote: a cast to IEnumerable reads the query in full on purpose. + while (receiver is IConversionOperation { IsImplicit: true } conversion) receiver = conversion.Operand; + + if (receiver?.Type is not { } type || !IsQuery(type, queryable)) return; + + var condition = method.Parameters.First(parameter => IsFunc(parameter.Type, func)).Type; + var element = ((INamedTypeSymbol)condition).TypeArguments[0].ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); + + context.Report(Diagnostic.Create( + QueryDelegateRule, + invocation.Syntax.GetLocation(), + method.Name, + condition.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), + string.Format(advice, element))); + } + + /// Whether is an IQueryable<T>, implements one, or is a type parameter constrained to one. + private static bool IsQuery(ITypeSymbol type, INamedTypeSymbol queryable) => + type switch + { + ITypeParameterSymbol parameter => parameter.ConstraintTypes.Any(constraint => IsQuery(constraint, queryable)), + _ => SymbolEqualityComparer.Default.Equals(type.OriginalDefinition, queryable) + || type.AllInterfaces.Any(candidate => SymbolEqualityComparer.Default.Equals(candidate.OriginalDefinition, queryable)) + }; + + private static bool IsFunc(ITypeSymbol type, INamedTypeSymbol func) => SymbolEqualityComparer.Default.Equals(type.OriginalDefinition, func); + + /// + /// The sequence overloads that take a Func condition and have a query twin that takes an + /// expression, by original definition, with the advice for each, {0} for the element type. The + /// Last forms' twins on a query refuse to compile, so their advice is to take the first instead. + /// + private static Dictionary QueryDelegateForms(TypeKitSymbols symbols, INamedTypeSymbol func) + { + var forms = new Dictionary(SymbolEqualityComparer.Default); + + const string asExpression = "Write the condition as a lambda at the call, or hold it as an Expression>, so that the query runs it, or call AsEnumerable() first to read every row on purpose"; + + foreach (var (methods, advice) in new[] + { + (symbols.OptionClassMethods("FirstOrNone"), asExpression), + (symbols.OptionClassMethods("SingleOrNone"), asExpression), + (symbols.ResultClassMethods("FirstOrError"), asExpression), + (symbols.OptionClassMethods("LastOrNone"), "A query has no last element until it is ordered: order it the other way and use FirstOrNone with the condition as a lambda, or call AsEnumerable() first to read every row on purpose"), + (symbols.ResultClassMethods("LastOrError"), "A query has no last element until it is ordered: order it the other way and use FirstOrError with the condition as a lambda, or call AsEnumerable() first to read every row on purpose"), + }) + { + foreach (var method in methods) + { + if (method.Parameters.Any(parameter => IsFunc(parameter.Type, func))) forms[method] = advice; + } + } + + return forms; + } + private static void Analyze(OperationAnalysisContext context, TypeKitSymbols symbols, Dictionary forms) { var invocation = (IInvocationOperation)context.Operation; diff --git a/src/CodoMetis.TypeKit.Analyzers/IgnoredOutcomeAnalyzer.cs b/src/CodoMetis.TypeKit.Analyzers/IgnoredOutcomeAnalyzer.cs index af95757e..230f9ab5 100644 --- a/src/CodoMetis.TypeKit.Analyzers/IgnoredOutcomeAnalyzer.cs +++ b/src/CodoMetis.TypeKit.Analyzers/IgnoredOutcomeAnalyzer.cs @@ -147,12 +147,178 @@ private static void Analyze(OperationAnalysisContext context, TypeKitSymbols sym // pass-through stands between them; a branch's call keeps its own. var own = ReferenceEquals(invocation, passedOn); var type = own && ReferenceEquals(invocation, dropped) ? expression.Type : awaited is null ? invocation.Type : tasks.Awaited(invocation.Type); - if (DroppedOutcome(type, invocation, symbols, tasks) is not { } outcome) continue; + + var outcome = DroppedOutcome(type, invocation, symbols, tasks) ?? DroppedAssertion(invocation, awaited is not null, tasks); + if (outcome is null) continue; context.Report(Diagnostic.Create(Rule, (own ? expression : invocation).Syntax.GetLocation(), outcome, invocation.TargetMethod.Name)); } } + /// + /// The task that fails a test, dropped, or awaited where says so (the caller + /// of a conversion to a plain Task and Wait wait for it too): pending.ShouldBeSuccess(); + /// asserts nothing, since the assertion fails the test only through its task (). + /// Nor does Task.WhenAll(first.ShouldBeNone(), second.ShouldBeNone()); or Task.Run(() => + /// pending.ShouldBeNone());, whose task faults when an assertion fails, or Task.WhenAny of them, + /// awaited or not, whose task completes with the first to finish and leaves every failure unobserved, when + /// the statement made an assertion it hands on (). Awaited, a WhenAll + /// or a Run observes only a failure it was handed as a fault: await Task.WhenAll(Task.WhenAny(…)) + /// completes with the faulted assertion as its value ('s depth). + /// + private static string? DroppedAssertion(IInvocationOperation invocation, bool awaited, Tasks tasks) + { + var method = invocation.TargetMethod; + + var dropped = IsPendingAssertion(method, tasks) ? !awaited + : tasks.PassesOnFailure(method) is { } faults + && invocation.Arguments.Any(argument => MadeAssertions(argument.Value, tasks).Any(made => !awaited || !faults || made.Depth > 0)); + + if (!dropped) return null; + + // Task.WhenEach(first.ShouldBeNone(), second.ShouldBeNone()) yields them unobserved. + return tasks.ElementOfSequence(invocation.Type) is { } element ? $"sequence of {element.Name}s" : invocation.Type?.Name ?? "Task"; + } + + /// + /// The assertions makes and hands on the task of, ' way, as + /// an element of an array or a collection, or from a lambda or a method group a call that returns tasks was + /// given: new[] { a.ShouldBeNone(), b.ShouldBeNone() }, ids.Select(id => Find(id).ShouldBeNone()), + /// pending.Select(OptionAssertions.ShouldBeNone), ids.Select(async id => await Find(id).ShouldBeNone()). + /// An assertion assigned or discarded in it keeps its own task, and one awaited fails only the + /// async lambda it is in, which is handed on in its place. Each comes with the depth + /// finds it at. + /// + private static IEnumerable<(IOperation Assertion, string Name, int Depth)> MadeAssertions(IOperation value, Tasks tasks) + { + foreach (var operation in value.DescendantsAndSelf()) + { + var method = operation switch + { + IInvocationOperation call => call.TargetMethod, + IMethodReferenceOperation reference => reference.Method, + _ => null, + }; + + if (method is not null && IsPendingAssertion(method, tasks) && HandsOn(operation, value, method, tasks) is { } depth) yield return (operation, method.Name, depth); + } + } + + /// + /// How deep holds the failure of 's assertion, or + /// where it is not handed on, or observed on the way: 0 where root's task faults with + /// it, as a WhenAll's or a Run's does; one more for each task that completes with it as a value + /// instead, a WhenAny's or a WhenEach's, or an async lambda's that returns it; one fewer + /// for each await on the way that unwraps such a task. An await of the faulting task fails the + /// async lambda it is in, which is followed in its place. + /// + private static int? HandsOn(IOperation operation, IOperation root, IMethodSymbol assertion, Tasks tasks) + { + var depth = 0; + + for (var current = operation; !ReferenceEquals(current, root);) + { + var parent = current.Parent!; + + // await Task.WhenAny(pending.ShouldBeNone(), …) completes with the assertion's task, still unobserved; + // await pending.ShouldBeNone() fails the async lambda it is in, unless a catch on the way keeps the failure. + if (parent is IAwaitOperation) + { + if (depth > 0) + { + depth--; + current = parent; + } + else if (EnclosingAsyncFunction(current, root, assertion) is { } function) current = function; + else return null; + + continue; + } + + var handsOn = parent switch + { + IConversionOperation or IConditionalOperation or ICoalesceOperation or ISwitchExpressionArmOperation or ISwitchExpressionOperation => true, + IArrayInitializerOperation or IArrayCreationOperation or ICollectionExpressionOperation or IArgumentOperation => true, + IReturnOperation or IBlockOperation or IAnonymousFunctionOperation or IDelegateCreationOperation or ITranslatedQueryOperation => true, + IInvocationOperation call when ReferenceEquals(call.Instance, current) => tasks.PassesOn(call), + IInvocationOperation call => tasks.HoldsTasks(call.Type), + _ => false, + }; + + if (!handsOn) return null; + + // async () => Task.WhenAny(…) and Task.WhenAny(…) complete with the tasks they were given. + if (parent is IAnonymousFunctionOperation { Symbol.IsAsync: true } + || parent is IInvocationOperation combinator && !ReferenceEquals(combinator.Instance, current) && tasks.PassesOnFailure(combinator.TargetMethod) == false) + depth++; + + current = parent; + } + + return depth; + } + + /// + /// The async lambda or anonymous method inside that + /// fails, with no catch between them that keeps the failure of (). + /// A catch keeps only what its try block throws, not what its handlers or its finally do. + /// + private static IOperation? EnclosingAsyncFunction(IOperation operation, IOperation root, IMethodSymbol assertion) + { + for (IOperation? child = operation, current = operation.Parent; current is not null && !ReferenceEquals(current, root); child = current, current = current.Parent) + { + switch (current) + { + case ITryOperation attempt when ReferenceEquals(child, attempt.Body) && attempt.Catches.Any(clause => Keeps(clause, assertion)): + return null; + case IAnonymousFunctionOperation function: + return function.Symbol.IsAsync ? function : null; + } + } + + return null; + } + + /// + /// Whether takes the failure of and keeps it: its type can + /// hold the TypeKitAssertionException the assertion throws, and its handler can complete, where + /// catch { throw; }, a log and throw;, or a throw new … hand the failure on. A constant + /// false filter leaves the handler unreachable, so it never completes either. A filter that is no + /// constant, when (Expected(e)), may take it and counts as taking it: an Error reports only a failure that + /// is lost whatever the filter answers. + /// + private static bool Keeps(ICatchClauseOperation clause, IMethodSymbol assertion) + { + if (TypeKitSymbols.AssertionException(assertion.ContainingAssembly) is { } thrown && !IsOrDerivesFrom(thrown, clause.ExceptionType)) return false; + + return clause.Handler is not { Syntax: BlockSyntax handler, SemanticModel: { } model } + || model.AnalyzeControlFlow(handler) is not { Succeeded: true, EndPointIsReachable: false, ExitPoints.IsEmpty: true }; + } + + private static bool IsOrDerivesFrom(ITypeSymbol? type, ITypeSymbol? ancestor) + { + for (; type is not null; type = type.BaseType) + { + if (SymbolEqualityComparer.Default.Equals(type, ancestor)) return true; + } + + return false; + } + + /// + /// An assertion of CodoMetis.TypeKit.Testing on a Task or ValueTask of the value it + /// asserts on, await service.FindAsync(id).ShouldBeSome(): it awaits the value and fails the + /// test through the task it returns, so that task dropped unawaited passes whatever the value is. + /// It is judged as every dropped call is, through : passed through + /// ConfigureAwait or WaitAsync, or reached through a branch of a conditional, a + /// ?? or a switch expression. Every such assertion is new in 2.3, so reporting its drop + /// breaks no build that worked before (design/adr/0118). + /// + private static bool IsPendingAssertion(IMethodSymbol method, Tasks tasks) => + TypeKitSymbols.IsTestingAssembly(method.ContainingAssembly) + && (method.ReducedFrom ?? method).Parameters is { Length: > 0 } parameters + && tasks.Awaited(parameters[0].Type) is not null; + /// /// The calls whose values drops as one: the call itself, or each /// branch's call of a conditional or a switch expression, through implicit conversions, @@ -195,6 +361,10 @@ private static IEnumerable Producers(IOperation operation, if ((symbols.OutcomeName(type) ?? tasks.PendingOutcome(type, symbols)) is { } outcome) return ReturnsItsStoredReceiver(invocation, symbols, tasks) ? null : outcome; + // Task.WhenEach(first.CancelAsync(), second.CancelAsync()) yields the tasks it was given, results and all. + if (tasks.PassesOnFailure(invocation.TargetMethod) == false && tasks.SequenceOutcome(type, symbols) is { } element) + return DroppedHolder($"sequence of {element}s", invocation, symbols, tasks); + return DroppedHolder(tasks.HolderOutcome(type, symbols) ?? tasks.HolderOutcome(tasks.Awaited(type), symbols), invocation, symbols, tasks); } @@ -251,6 +421,21 @@ private static void AnalyzeConversion(OperationAnalysisContext context, TypeKitS var toPlainTask = SymbolEqualityComparer.Default.Equals(conversion.Type, tasks.Plain); if (!toPlainTask && !tasks.HoldsPlainTasks(conversion.Type)) return; + // Task.WaitAll(new[] { first.CancelAsync(), second.CancelAsync() }) converts the array of tasks whole: + // each element's call is converted as if alone. + if (!toPlainTask && Unwrap(conversion.Operand) is IArrayCreationOperation { Initializer.ElementValues: var elements }) + { + foreach (var element in elements) + foreach (var invocation in Producers(element, tasks)) + { + if (ReturnsItsStoredReceiver(invocation, symbols, tasks) || tasks.PendingOutcome(invocation.Type, symbols) is not { } pending) continue; + + context.Report(Diagnostic.Create(Rule, invocation.Syntax.GetLocation(), pending, invocation.TargetMethod.Name)); + } + + return; + } + // The call, or each branch's call where a conditional or a switch expression was converted whole. // A pass-through between the conversion and the call, LoadAsync().WaitAsync(t), is reported whole. var passedOn = tasks.PassedOn(conversion.Operand); @@ -258,9 +443,11 @@ private static void AnalyzeConversion(OperationAnalysisContext context, TypeKitS foreach (var invocation in Producers(conversion.Operand, tasks)) { // held.TapAsync(log) returns the task of a result the caller still holds. + // Task Check() => Task.WhenAny(a.ShouldBeNone(), b.ShouldBeNone()); is awaited as a task that never faults. var outcome = toPlainTask ? (ReturnsItsStoredReceiver(invocation, symbols, tasks) ? null : tasks.PendingOutcome(invocation.Type, symbols)) ?? DroppedHolder(tasks.HolderOutcome(tasks.Awaited(invocation.Type), symbols), invocation, symbols, tasks) + ?? DroppedAssertion(invocation, awaited: true, tasks) : DroppedCollection(tasks.PendingOutcome(tasks.ElementOfSequence(invocation.Type), symbols), invocation, symbols, tasks); if (outcome is null) continue; @@ -274,40 +461,70 @@ private static void AnalyzeConversion(OperationAnalysisContext context, TypeKitS /// Wait on a task of a result, or of a collection of results, that the expression made: /// orders.CancelAsync(id).Wait() blocks until the result exists and hands it to nobody, /// whether the statement drops what Wait returns or an if reads it. A task still held, - /// or a tap on one, keeps its result. + /// or a tap on one, keeps its result. Each branch of a conditional, a ?? or a switch + /// expression is its own call, as in a dropped statement. Wait on a Task.WhenAny of + /// assertions waits for the first and never sees a failure, and so does Task.WaitAny. /// private static void AnalyzeWait(OperationAnalysisContext context, TypeKitSymbols symbols, Tasks tasks) { var wait = (IInvocationOperation)context.Operation; - if (wait.TargetMethod is not { Name: "Wait", IsStatic: false } method || !SymbolEqualityComparer.Default.Equals(method.ContainingType, tasks.Plain)) return; - if (wait.Instance is null || tasks.PassedOn(wait.Instance) is not IInvocationOperation invocation) return; + if (wait.TargetMethod is not { Name: "Wait" or "WaitAny" } method || !SymbolEqualityComparer.Default.Equals(method.ContainingType, tasks.Plain)) return; - if (ReturnsItsStoredReceiver(invocation, symbols, tasks)) return; + // Task.WaitAny(a.ShouldBeNone(), b.ShouldBeNone()) returns an index and leaves every failure + // unobserved; Task.WaitAll throws them. Tasks of results are its conversions' (AnalyzeConversion). + if (method.IsStatic) + { + if (method.Name != "WaitAny") return; - var outcome = tasks.PendingOutcome(invocation.Type, symbols) - ?? DroppedHolder(tasks.HolderOutcome(tasks.Awaited(invocation.Type), symbols), invocation, symbols, tasks); - if (outcome is null) return; + foreach (var argument in wait.Arguments.Where(argument => argument.Parameter?.Ordinal == 0)) + foreach (var (assertion, name, _) in MadeAssertions(argument.Value, tasks)) + context.Report(Diagnostic.Create(Rule, assertion.Syntax.GetLocation(), "Task", name)); + + return; + } - context.Report(Diagnostic.Create(Rule, wait.Syntax.GetLocation(), outcome, invocation.TargetMethod.Name)); + if (method.Name != "Wait" || wait.Instance is null) return; + + // The task's own call is reported at the Wait, each branch's call at the branch. + var passedOn = tasks.PassedOn(wait.Instance); + + foreach (var invocation in Producers(wait.Instance, tasks)) + { + if (ReturnsItsStoredReceiver(invocation, symbols, tasks)) continue; + + var outcome = tasks.PendingOutcome(invocation.Type, symbols) + ?? DroppedHolder(tasks.HolderOutcome(tasks.Awaited(invocation.Type), symbols), invocation, symbols, tasks) + ?? DroppedAssertion(invocation, awaited: true, tasks); + if (outcome is null) continue; + + context.Report(Diagnostic.Create(Rule, (ReferenceEquals(invocation, passedOn) ? wait : invocation).Syntax.GetLocation(), outcome, invocation.TargetMethod.Name)); + } } /// /// A foreach or await foreach over results, or tasks of them, that its header made, /// whose body never reads the iteration variable: await foreach (var outcome in - /// orders.CancelEachAsync(ids)) { } runs every cancellation and drops every result. A variable + /// orders.CancelEachAsync(ids)) { } runs every cancellation and drops every result. So does one + /// over assertions the header made, Task.WhenEach of them or a Select, whose failures nobody + /// sees. A variable /// named _ says the drop is intended; a collection held in a variable, a parameter or a field /// keeps its results. /// private static void AnalyzeLoop(OperationAnalysisContext context, TypeKitSymbols symbols, Tasks tasks) { if (context.Operation is not IForEachLoopOperation { LoopControlVariable: IVariableDeclaratorOperation { Symbol: { Name: not "_" } variable } } loop) return; - if ((symbols.OutcomeName(variable.Type) ?? tasks.PendingOutcome(variable.Type, symbols)) is not { } outcome) return; - if (Unwrap(loop.Collection) is not IInvocationOperation invocation || !MadeResults(invocation, symbols, tasks)) return; + if (Unwrap(loop.Collection) is not IInvocationOperation invocation) return; + + // foreach (var check in pending.Select(p => p.ShouldBeNone())) { } makes every assertion and observes none. + var outcome = (symbols.OutcomeName(variable.Type) ?? tasks.PendingOutcome(variable.Type, symbols)) is { } element + ? MadeResults(invocation, symbols, tasks) ? $"sequence of {element}s" : null + : tasks.IsTask(variable.Type) && MadeAssertions(invocation, tasks).Any() ? $"sequence of {variable.Type.Name}s" : null; + if (outcome is null) return; if (loop.Body.Descendants().OfType().Any(reference => SymbolEqualityComparer.Default.Equals(reference.Local, variable))) return; - context.Report(Diagnostic.Create(Rule, invocation.Syntax.GetLocation(), $"sequence of {outcome}s", invocation.TargetMethod.Name)); + context.Report(Diagnostic.Create(Rule, invocation.Syntax.GetLocation(), outcome, invocation.TargetMethod.Name)); } /// @@ -368,7 +585,8 @@ private static IOperation Unwrap(IOperation operation) => /// /// A call that returns its receiver unchanged, on a receiver the caller still holds: the /// outcome's own Tap, TapAsync, TapError and TapErrorAsync, and the - /// Task continuations TapAsync and TapErrorAsync, which return the task's result as it was. + /// Task continuations TapAsync and TapErrorAsync, which return the task's result as it was; + /// and Task.WhenAny of tasks the caller still holds, which completes with one of them. /// private static bool ReturnsItsStoredReceiver(IInvocationOperation invocation, TypeKitSymbols symbols, Tasks tasks) { @@ -382,9 +600,22 @@ private static bool ReturnsItsStoredReceiver(IInvocationOperation invocation, Ty if (method.Name is "TapAsync" or "TapErrorAsync" && symbols.IsDeclaredByResultClass(method)) return IsStored(invocation.Instance ?? invocation.Arguments.FirstOrDefault(argument => argument.Parameter?.Ordinal == 0)?.Value, symbols, tasks); + // Task.WhenAny(first, second) completes with one of the tasks it was given, results and all. + if (tasks.PassesOnFailure(method) == false) + return invocation.Arguments.All(argument => Elements(argument.Value).All(element => IsStored(element, symbols, tasks))); + return false; } + /// The elements of an array or a collection written out, such as a params argument's, or the value itself. + private static IEnumerable Elements(IOperation value) => + Unwrap(value) switch + { + IArrayCreationOperation { Initializer.ElementValues: var elements } => elements, + ICollectionExpressionOperation { Elements: var elements } => elements, + var other => [other], + }; + /// /// Whether the caller still holds : a local, a parameter or a field; a /// tap that returned such a receiver, so that r.Tap(log).TapError(warn) holds at every link; @@ -429,16 +660,40 @@ public IOperation PassedOn(IOperation operation) } /// - /// ConfigureAwait and WaitAsync of Task<T> or ValueTask<T>, - /// every overload: each completes with the task's own result and makes none. + /// ConfigureAwait and WaitAsync of Task, Task<T> or + /// ValueTask<T>, every overload: each completes as the task does, with its own result, + /// and makes none. A plain Task carries no result, but it can be the one an assertion of + /// CodoMetis.TypeKit.Testing returns (). /// public bool PassesOn(IInvocationOperation call) => call.TargetMethod is { IsStatic: false, Name: "ConfigureAwait" or "WaitAsync" } method && (SymbolEqualityComparer.Default.Equals(method.ContainingType.OriginalDefinition, generic) - || SymbolEqualityComparer.Default.Equals(method.ContainingType.OriginalDefinition, genericValueTask)); + || SymbolEqualityComparer.Default.Equals(method.ContainingType.OriginalDefinition, genericValueTask) + || SymbolEqualityComparer.Default.Equals(method.ContainingType, Plain)); + + /// + /// For a method that hands on the tasks it is given: for Task.WhenAll and + /// Task.Run, whose task faults when one of them has; for + /// Task.WhenAny, whose task completes with the first and never faults, and Task.WhenEach, + /// which yields them as they complete, faulted or not; for any other method. + /// + public bool? PassesOnFailure(IMethodSymbol method) => + method is { IsStatic: true, Name: "WhenAll" or "WhenAny" or "WhenEach" or "Run" } && SymbolEqualityComparer.Default.Equals(method.ContainingType, Plain) + ? method.Name is "WhenAll" or "Run" + : null; + + /// Whether a value of is a task, or a collection or a sequence of them. + public bool HoldsTasks(ITypeSymbol? type) => + IsTask(type) || IsTask(ElementOfSequence(type)); - /// A Task or ValueTask of a result, not awaited: the result is dropped with it. - public string? PendingOutcome(ITypeSymbol? type, TypeKitSymbols symbols) => symbols.OutcomeName(Awaited(type)); + public bool IsTask(ITypeSymbol? type) => SymbolEqualityComparer.Default.Equals(type, Plain) || Awaited(type) is not null; + + /// + /// A Task or ValueTask of a result, not awaited: the result is dropped with it. So is a task + /// of one, such as the Task<Task<Result<…>>> of Task.WhenAny, which completes + /// with a task of a result. + /// + public string? PendingOutcome(ITypeSymbol? type, TypeKitSymbols symbols) => symbols.OutcomeName(Awaited(type)) ?? symbols.OutcomeName(Awaited(Awaited(type))); /// What a Task<T> or ValueTask<T> completes with, or for any other type. public ITypeSymbol? Awaited(ITypeSymbol? type) => diff --git a/src/CodoMetis.TypeKit.Analyzers/README.md b/src/CodoMetis.TypeKit.Analyzers/README.md index 88816f9c..b78e309d 100644 --- a/src/CodoMetis.TypeKit.Analyzers/README.md +++ b/src/CodoMetis.TypeKit.Analyzers/README.md @@ -28,13 +28,13 @@ project. The two packages share a version and move together. |---|---|---| | CMTK0001 | Error | `default`, `default(T)`, `new T()`, `new()` and `new T { }` of a value object, an `Option`, a `Result`, or a struct marked `[RequireCustomInitialization]`. Also through a type parameter whose constraints make it one of those. Not reported: a default that is only compared (`id == default`, `id.Equals(default)`, `ThrowIfEqual(id, default)`, `Assert.NotEqual(default, id)`), which is a guard, and one assigned to an `out` parameter of a Try method that returns `bool`. | | CMTK0002 | Error | A type implements `IValue`, `IValidatedValue<,,>` or `IClosedSet`, but the compilation doesn't reference `CodoMetis.TypeKit.Generators`. So nothing is generated for it: no field, no `Value`, no factory, or for a closed set no name and no `Match`. | -| CMTK0003 | Error | A `Result` or `Option` that a call returns, or a collection or a tuple of them that the statement made, dropped. It counts as dropped as a statement, awaited or not; when its `Task` is converted to a plain `Task`, also where a method group is the conversion; through `Wait`; in a loop that never reads it; and in a branch of a conditional, a `switch` expression or a `??` that picked the call. `_` says you mean the drop. | +| CMTK0003 | Error | A `Result` or `Option` that a call returns, or a collection or a tuple of them that the statement made, dropped. It counts as dropped as a statement, awaited or not; when its `Task` is converted to a plain `Task`, also where a method group is the conversion; through `Wait`; in a loop that never reads it; and in a branch of a conditional, a `switch` expression or a `??` that picked the call. Also an assertion of `CodoMetis.TypeKit.Testing` on a task, left without `await`. `_` says you mean the drop. | | CMTK0004 | Error | `Materialize`, which rebuilds a value object without its rules, called or referenced anywhere but the EF Core package. | | CMTK0005 | Warning | An array or span of a value object, `Option`, `Result` or `[RequireCustomInitialization]` struct created with a length, so every slot starts as `default`. A constant length of zero isn't reported. | | CMTK0006 | Warning | A field or auto-property of such a type, in a class, that no initializer, `required` or constructor sets. Also a `required` one that a `[SetsRequiredMembers]` constructor doesn't set. | | CMTK0007 | Info | `FromKnownGood` given a value that comes straight from a parameter. | | CMTK0008 | Warning | The values inside two different value objects compared, `order.CustomerId.Value == product.Id.Value`, or the two value objects themselves compared through `Equals(object)`. | -| CMTK0009 | Warning | A call that returns the `default` of such a type when it finds nothing: `FirstOrDefault()`, `GetValueOrDefault()`, `Option.OrDefault()` and their kind. | +| CMTK0009 | Warning | A call that returns the `default` of such a type when it finds nothing: `FirstOrDefault()`, `GetValueOrDefault()`, `Option.OrDefault()` and their kind. Also `FirstOrNone`, `SingleOrNone` or `FirstOrError` on a query given a delegate, which reads every row. | | CMTK0010 | Error | A type argument that breaks a constraint its parameter declares by attribute: `[Abstract]`, `[Sealed]`, `[Record]`, `[Concrete]`, `[Interface]`, `[Unsealed]`, `[ValueObject]`, `[ClosedSet]`, `[FlagsEnum]` or `[ReadOnlyStruct]`. Also a pair of arguments for two parameters marked `[Distinct]` or `[Unrelated]` that are one type, or related by inheritance, implementation or boxing, reported once at the later argument. Checked at any closed use, written or inferred: a base type, a member, a parameter, a local, a `typeof`, an attribute, a call or a method group, a member of a generic extension block, whose arguments the receiver gives, and a method the compiler calls for a `foreach`, an `await`, a deconstruction, a positional pattern or a collection expression (there, `[Abstract]`, `[Sealed]`, `[Record]` and `[Concrete]` join in 3.0.0). A type parameter passed on without the attribute is a breach too. A method that implements an interface's method, or overrides one, carries the constraint of what it implements, a static one and one a base class declares included. | | CMTK0011 | Error | A concrete class that derives from or implements a type marked `[RequireParameterlessConstructor]`, directly or through another, without a parameterless constructor of the accessibility it asks for. A struct is never reported: its parameterless constructor is public and can be nothing else. The code fix writes or changes the constructor. | | CMTK0012 | Error | A closed set's `Match` given an argument by position. It binds to whichever member is declared at that place, so reordering the members swaps the branches. The code fix names every argument. | @@ -47,6 +47,9 @@ them. Every rule also runs in `.razor` and `.cshtml` files (see [Generated code](#generated-code-and-razor)). +One code fix answers the compiler instead of a rule: [Add Create](#add-create) writes the `Create` a +validated value object is missing. + ## CMTK0001 A `default` value object wraps `default(T)` and passed no rule. A `default` result is neither a @@ -136,7 +139,8 @@ it as they treat a value object. A value object in a project without the generators would compile with nothing in it. This rule turns that into an error. -The generators apply through a transitive project fabric, not an attribute on the interfaces, which +The generators are built on [Metalama](https://postsharp.net/metalama), which generates C# code at +compile time from templates written in C#. They apply through a transitive project fabric, not an attribute on the interfaces, which keeps the base package free of Metalama. The price is that a project that sees the interfaces but not the generators compiles a value object with nothing in it, silently. A closed set fails to compile there anyway, on the interface members nobody implemented, and the rule says why. @@ -182,6 +186,29 @@ call's: `await orders.CancelAsync(id).ConfigureAwait(false);` is reported, namin explicit cast, to `Task` or to the delegate type, says you mean the drop. A `ValueTask` has no conversion to `ValueTask`. +**Assertions on a task.** An assertion of `CodoMetis.TypeKit.Testing` on a `Task` or `ValueTask`, +such as `service.FindAsync(id).ShouldBeSome()`, fails the test only through the task it returns. +Left without `await`, the test passes whatever the task holds, and C# warns only in an `async` +method. CMTK0003 reports the line, naming the assertion. It also reports an assertion passed through +`ConfigureAwait` or `WaitAsync`, and one in a branch of a `?:`, `??` or `switch` expression, as it does +for a dropped result. The rule knows these assertions by the package's own assembly, so an extension of +yours on a pending result is left alone. + +Handing assertions on doesn't hide them either. You get the report when you hand assertions to +`Task.WhenAll`, `Task.Run` or `Task.WhenEach` and drop what it returns. You get it for `Task.WhenAny` and +`Task.WaitAny` of them even when you wait, because those finish with the first task and never show a +failure. It covers an array of them, a `Select`, a query, an `async` lambda that awaits one, and a method +group such as `pending.Select(OptionAssertions.ShouldBeNone)`. `Task.WaitAll` throws the failures, so it is +silent. So is a loop over assertions that reads each one. + +Nesting doesn't hide them. A `Task.WhenAny` inside a `Task.WhenAll` or a `Task.Run` is reported even when you +await the outer task, because that task succeeds with the failed assertion as its value: +`await Task.WhenAll(Task.WhenAny(first.ShouldBeNone(), second.ShouldBeNone()))` passes while both assertions +fail. `await await Task.WhenAny(…)` throws the first failure, so it is silent. A `catch` around an awaited +assertion in an `async` lambda hides the failure only when it can keep it. A `catch` that throws again, one for +another exception, such as `catch (OperationCanceledException)`, and one with a `when (false)` filter are +reported. A filter the rule can't work out, such as `when (Expected(e))`, counts as catching the failure. + **Collections.** A collection of results is dropped as silently as one result: ```csharp @@ -226,10 +253,16 @@ await foreach (var _ in orders.CancelEachAsync(ids)) { } // a variable n ``` - **`Wait`** is reported on a task the expression made, whether the statement drops the `bool` a - timeout overload returns or an `if` reads it. + timeout overload returns or an `if` reads it. On a `?:`, `??` or `switch` expression, each branch's + call is reported, as when you drop it. - **`Task.WaitAll` and `Task.WaitAny`** take plain tasks. So tasks of results that the statement made - are converted and their results dropped, as one task's conversion is. An explicit cast to the plain - tasks' type says you mean that drop. + are converted and their results dropped, as one task's conversion is. That includes an array you + write in the call, `Task.WaitAll(new[] { orders.CancelAsync(first), orders.CancelAsync(second) })`. An + explicit cast to the plain tasks' type says you mean that drop. +- **`Task.WhenAny`** finishes with one of the tasks you give it, so every other result is lost. A + `WhenAny` of tasks the statement made is reported whether you drop it, await it, wait for it or return + it as a plain `Task`. Of tasks you still hold, it hands back one of yours, and is silent. +- **`Task.WhenEach`** of tasks the statement made, dropped, is reported as a sequence of results. - **A `foreach` or `await foreach`** is reported when its header made the results, by the rule above, and its body never reads the iteration variable. @@ -399,6 +432,44 @@ ordinary value. EF Core's forms are recognised in any project that references EF It's a warning, because code that checks the sequence isn't empty first is correct, and the rule can't tell. +### A condition that reads every row + +CMTK0009 also warns when a lookup on a query would read the whole table: + +```csharp +Func isOpen = o => o.Open; +var first = db.Orders.FirstOrNone(isOpen); // CMTK0009: binds to the IEnumerable overload and reads every row +var found = db.Orders.FirstOrNone(o => o.Open); // the database runs the condition and returns one row +``` + +`FirstOrNone`, `SingleOrNone` and `FirstOrError` on an `IQueryable` take the condition as an +`Expression>`, which the database runs. A delegate is no expression. So the call binds to +the overload on `IEnumerable`, which reads every row and runs the condition in memory. The rule +reports that call, and its message names the overload it fell to. + +Reported, on any receiver that is a query (`IQueryable`, a `DbSet`, a type parameter constrained +to one), when the condition is: + +- a `Func` in a variable, a parameter, a field or a property; +- a method group, such as `IsOpen`; +- an anonymous method, `delegate (Order o) { ... }`; +- a call that returns a `Func`, or a lambda cast to one. + +`LastOrNone` and `LastOrError` are reported as well. On a query they don't compile with a lambda, and a +delegate there reads every row in the same way. + +To fix it, write the condition as a lambda at the call, or hold it as an `Expression>`. +To read every row on purpose, call `AsEnumerable()` first, and the rule stays silent. + +**Good to know** + +- **A query held as an `IEnumerable` is a sequence to the rule.** Once your code converts it, the + rule can't tell that a query is underneath, so `IEnumerable all = db.Orders;` followed by + `all.FirstOrNone(isOpen)` isn't reported. +- **There is no code fix.** The fix depends on your condition. A method group wrapped in a lambda + binds to the query's overload, but the database can't run your method, so EF Core throws when the + query runs. + ## CMTK0010 Put an attribute on a type parameter to ask for what C# can't, such as an abstract or a sealed type. @@ -661,6 +732,23 @@ The members are read from the enum itself, so a member that a newer base package here too. An enum of your application's own isn't this rule's job. A fault that no member names is rejected where `ToHttpResult()` turns it into a response, and `WithRule` throws for one. +## Add Create + +A type that declares `IValidatedValue` and has no `Create` fails to build with CS0535. +The package's fix for that error, **Add Create**, writes the method for you, as the [Generators +README](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.Generators/README.md#declare-a-value-object) +shows. It accepts every value until you add your rules. The types are spelled as your file names them, +so a type from a namespace the file doesn't import is qualified. `Create` goes in as the type's first +member, indented as the other members are. A declaration that ends in `;` gets a body. + +The fix isn't offered in these cases: + +- **`TSelf` is another type.** The value to return isn't this type's to build. +- **The type already has a `Create` that takes a `T`**, static or not. A second one wouldn't compile, so + correct the one that's there. +- **The type's body is on one line and already holds members.** Put the body on lines of its own, and the + fix is offered. + ## Configuration You set a rule's severity in `.editorconfig`, as usual: `dotnet_diagnostic.CMTK0001.severity = warning`. @@ -691,11 +779,28 @@ but a suppression to say so: - a sequence known not to be empty (CMTK0009); - a comparison of two identities that really are shared (CMTK0008). -**What a new version may change.** Within a major version, an Error never reports more than it did. -A new Error rule, or an Error that reports more, waits for the next major version. A Warning or an -Info may learn more forms in a minor version, such as another method that returns a default instance -for CMTK0009. In a build that treats warnings as errors, such an update can report code that built -before, as a newer compiler's warnings can. +**What a new version may change.** Within a major version, an update never makes an Error report code +that built before. A new Error rule, or an Error that reports more of the code you could already write, +waits for the next major version. A minor version may teach an Error a form that only its own new API can +write. In 2.3.0, CMTK0003 reports a `CodoMetis.TypeKit.Testing` assertion on a `Task` or `ValueTask` left +without `await`, and those assertions are new in 2.3.0. Your code from before the update can't contain +one, so it builds as before. + +2.3.0 makes an exception for a few forms that code from before 2.3.0 can contain. Each one loses every +result, so the build points at a real bug. CMTK0003 now also reports these, for tasks of results: + +- `Task.WhenAny` of tasks you don't hold, dropped, awaited, waited for or returned as a plain `Task`; +- a `Task.WhenEach` of them that you drop; +- `Wait()` on a `?:`, `??` or `switch` expression of them; +- an array of them that you write in a `Task.WaitAll` or `Task.WaitAny` call. + +Handle the results, or discard what you mean to drop with `_ =`. Only a gap found before a version's tag +is closed this way. After the tag, an Error that reports more of the code you could already write waits +for the next major version. + +A Warning or an Info may learn more forms in a minor version, such as another method that returns a +default instance for CMTK0009. In a build that treats warnings as errors, such an update can report code +that built before, as a newer compiler's warnings can. ## Generated code and Razor diff --git a/src/CodoMetis.TypeKit.Analyzers/TypeKitSymbols.cs b/src/CodoMetis.TypeKit.Analyzers/TypeKitSymbols.cs index 93f02155..6952b38d 100644 --- a/src/CodoMetis.TypeKit.Analyzers/TypeKitSymbols.cs +++ b/src/CodoMetis.TypeKit.Analyzers/TypeKitSymbols.cs @@ -52,6 +52,12 @@ internal sealed class TypeKitSymbols /// private const string GeneratorsAssembly = "CodoMetis.TypeKit.Generators"; + /// + /// The assembly of the test assertions, whose assertions on a pending value fail a test only through + /// the task they return. This is the package's own identity, which a consumer cannot rename. + /// + private const string TestingAssembly = "CodoMetis.TypeKit.Testing"; + private const string Materialize = "Materialize"; private readonly ImmutableArray _valueMarkers; @@ -108,6 +114,16 @@ private TypeKitSymbols( _computeRestriction = ComputeRestriction; } + /// The exception every assertion of CodoMetis.TypeKit.Testing fails with, by its metadata name in that assembly. + private const string AssertionExceptionName = "CodoMetis.TypeKit.Testing.TypeKitAssertionException"; + + /// Whether is CodoMetis.TypeKit.Testing. + public static bool IsTestingAssembly(IAssemblySymbol? assembly) => + assembly is not null && string.Equals(assembly.Name, TestingAssembly, StringComparison.Ordinal); + + /// The exception the assertions of , CodoMetis.TypeKit.Testing, fail with. + public static INamedTypeSymbol? AssertionException(IAssemblySymbol testing) => testing.GetTypeByMetadataName(AssertionExceptionName); + /// Whether the compilation references CodoMetis.TypeKit.Generators. public bool ReferencesGenerators { get; } @@ -256,10 +272,15 @@ private static IEnumerable UsingsOf(SyntaxNode node) => /// declares, in both forms a call can bind to: the extension block's member, which /// option.OrDefault() binds to, and its implementation, which Option.OrDefault(option) binds to. /// - public IEnumerable OptionClassMethods(string name) => - _optionClasses.SelectMany(type => type.GetMembers(name) - .Concat(type.GetTypeMembers().Where(nested => nested.IsExtension).SelectMany(block => block.GetMembers(name)))) - .OfType(); + public IEnumerable OptionClassMethods(string name) => ClassMethods(_optionClasses, name); + + /// The methods named that the package's Result static class declares, as finds them. + public IEnumerable ResultClassMethods(string name) => ClassMethods(_resultClasses, name); + + private static IEnumerable ClassMethods(ImmutableArray classes, string name) => + classes.SelectMany(type => type.GetMembers(name) + .Concat(type.GetTypeMembers().Where(nested => nested.IsExtension).SelectMany(block => block.GetMembers(name)))) + .OfType(); private static bool IsDeclaredBy(IMethodSymbol method, ImmutableArray classes) { diff --git a/src/CodoMetis.TypeKit.AspNetCore/FaultProblem.cs b/src/CodoMetis.TypeKit.AspNetCore/FaultProblem.cs index f9919a21..1aa6711f 100644 --- a/src/CodoMetis.TypeKit.AspNetCore/FaultProblem.cs +++ b/src/CodoMetis.TypeKit.AspNetCore/FaultProblem.cs @@ -31,11 +31,12 @@ namespace CodoMetis.TypeKit.AspNetCore; public sealed class FaultProblem : IResult, IEndpointMetadataProvider where TFault : notnull { - internal FaultProblem(TFault fault, string? detail = null, string? rule = null) + internal FaultProblem(TFault fault, string? detail = null, string? rule = null, Exception? cause = null) { Fault = fault; Detail = detail; Rule = rule; + Cause = cause; } /// The fault this response reports. @@ -47,6 +48,12 @@ internal FaultProblem(TFault fault, string? detail = null, string? rule = null) /// The rule that refused, EmailFault.NoAt, where the result carried a naming one. public string? Rule { get; } + /// + /// The exception that caused the fault, where the result carried a with one + /// (): for the log, and never written into the problem. + /// + public Exception? Cause { get; } + /// public async Task ExecuteAsync(HttpContext httpContext) { @@ -64,15 +71,17 @@ public async Task ExecuteAsync(HttpContext httpContext) var status = endpoint is not null ? FaultStatuses.Override(endpoint.Metadata, Fault, responses.Type) ?? answer.Status : answer.Status; var name = httpContext.RequestServices.GetService>()?.Value.QualifiedFaultNames == true ? answer.QualifiedName : answer.Name; - var detail = Detail ?? answer.Description; + // A withheld name takes the member's description with it, which names the member as surely. + var detail = answer.Named ? Detail ?? answer.Description : Detail; // Present only while this problem is written, so a CustomizeProblemDetails that reads it sees it // for this problem and for no other the request writes. var features = httpContext.Features; var previous = features.Get(); - features.Set(new FaultFeature(typeof(TFault), Fault, name, detail, Rule)); - var extensions = new Dictionary { [FaultResponsesOperationTransformer.FaultMember] = name }; - if (Rule is not null) extensions[FaultResponsesOperationTransformer.RuleMember] = Rule; + features.Set(new FaultFeature(typeof(TFault), Fault, name, detail, Rule, !answer.Named, Cause)); + var extensions = new Dictionary(); + if (answer.Named) extensions[FaultResponsesOperationTransformer.FaultMember] = name; + if (answer.Named && Rule is not null) extensions[FaultResponsesOperationTransformer.RuleMember] = Rule; try { await TypedResults.Problem( @@ -103,7 +112,7 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi builder.Metadata.Add(FaultResponsesMetadata.Instance); } - private sealed record FaultFeature(Type FaultType, object Fault, string Name, string? Detail, string? Rule) : IFaultFeature; + private sealed record FaultFeature(Type FaultType, object Fault, string Name, string? Detail, string? Rule, bool NameWithheld, Exception? Cause) : IFaultFeature; } /// @@ -120,7 +129,10 @@ public interface IFaultFeature /// The fault: the enum member or the closed set's member. object Fault { get; } - /// The name the problem's fault member carries, qualified where the host asks for it. + /// + /// The name the problem's fault member carries, qualified where the host asks for it, or would carry + /// where the mapping withholds it (). + /// string Name { get; } /// The problem's detail, or . @@ -129,6 +141,22 @@ public interface IFaultFeature /// The problem's rule, the rule that refused (), or . /// Implemented by default as , so an implementation of the host's own, a test's, still builds. string? Rule => null; + + /// + /// Whether the mapping withholds the fault's name from the problem (FaultResponse.WithoutFaultName()): + /// the problem then has no fault and no rule, and a CustomizeProblemDetails that writes + /// or into it tells the client what the mapping keeps from it. + /// + /// Implemented by default as , so an implementation of the host's own still builds. + bool NameWithheld => false; + + /// + /// The exception that caused the fault (), or : for + /// the host's log. The problem never carries it, and a CustomizeProblemDetails that copies it into + /// one tells the client what failed underneath. + /// + /// Implemented by default as , so an implementation of the host's own, a test's, still builds. + Exception? Cause => null; } /// How ToHttpResult() writes a problem: services.Configure<FaultResponseOptions>(options => …). diff --git a/src/CodoMetis.TypeKit.AspNetCore/FaultResponse.cs b/src/CodoMetis.TypeKit.AspNetCore/FaultResponse.cs index fac2b51d..a6779f05 100644 --- a/src/CodoMetis.TypeKit.AspNetCore/FaultResponse.cs +++ b/src/CodoMetis.TypeKit.AspNetCore/FaultResponse.cs @@ -20,11 +20,12 @@ namespace CodoMetis.TypeKit.AspNetCore; /// public sealed class FaultResponse { - private FaultResponse(int statusCode, Uri? type, string? title) + private FaultResponse(int statusCode, Uri? type, string? title, bool namesFault = true) { StatusCode = statusCode; Type = type; Title = title; + NamesFault = namesFault; } /// The HTTP status. @@ -36,6 +37,12 @@ private FaultResponse(int statusCode, Uri? type, string? title) /// The problem's title, or for the status's own. public string? Title { get; } + /// + /// Whether the problem names the fault in its fault member, and the rule that refused in its + /// rule. True unless withholds them. + /// + public bool NamesFault { get; } + /// A response with , and the status's own type and title. /// The HTTP status, 400 to 599, checked where the faults are mapped. /// The response. @@ -76,7 +83,7 @@ private FaultResponse(int statusCode, Uri? type, string? title) public FaultResponse WithType(Uri type) { ArgumentNullException.ThrowIfNull(type); - return new(StatusCode, type, Title); + return new(StatusCode, type, Title, NamesFault); } /// @@ -93,9 +100,29 @@ public FaultResponse WithType(Uri type) public FaultResponse WithTitle(string title) { ArgumentException.ThrowIfNullOrWhiteSpace(title); - return new(StatusCode, Type, title); + return new(StatusCode, Type, title, NamesFault); } + /// + /// The same response without the fault's name, for faults a client must not tell apart: a sign-in's + /// fault => FaultResponse.Status(401).WithoutFaultName() answers an unknown user and a wrong + /// password alike. + /// + /// + /// + /// The problem has no fault and no rule member, and its detail is only what the + /// occurrence says (): a fault set member's description, which + /// otherwise stands in for it, names the member as surely as its name does. Two members answered so, + /// with the same status, type and title, write the same problem but for the detail the + /// application wrote. The host still sees the fault, through IFaultFeature, to log it. + /// + /// + /// The document says it: a response whose every fault withholds its name is described as naming none. + /// + /// + /// A new response. + public FaultResponse WithoutFaultName() => new(StatusCode, Type, Title, namesFault: false); + /// A bare status as a response, so a mapping can return 404 where it needs nothing else. /// The HTTP status. public static implicit operator FaultResponse(int statusCode) => Status(statusCode); diff --git a/src/CodoMetis.TypeKit.AspNetCore/FaultResponses.cs b/src/CodoMetis.TypeKit.AspNetCore/FaultResponses.cs index 45c20b08..5c6ba8d7 100644 --- a/src/CodoMetis.TypeKit.AspNetCore/FaultResponses.cs +++ b/src/CodoMetis.TypeKit.AspNetCore/FaultResponses.cs @@ -124,7 +124,7 @@ public static FaultResponses Map(FaultType type, Func.DescriptionOf(fault)); + answers[fault] = new Answer(response.StatusCode, response.Type is { } problemType ? FaultResponse.Written(problemType) : null, response.Title, type.NameOf(fault), type.QualifiedNameOf(fault), FaultType.DescriptionOf(fault), response.NamesFault); } return new FaultResponses(type, answers); @@ -155,6 +155,14 @@ [.. AnsweredBy(endpointMetadata).Where(fault => StatusOf(fault, endpointMetadata .OfType() .Distinct()]; + /// + /// Whether a member the endpoint answers with names its fault: + /// where one does, where every one withholds it, where none + /// is answered with it. + /// + public bool? NamesAt(int status, IReadOnlyList endpointMetadata) => + AnsweredBy(endpointMetadata).Where(fault => StatusOf(fault, endpointMetadata) == status).Select(fault => (bool?)_answers[fault].Named).Max(); + /// The members an endpoint answers with: every member, but those its metadata leaves out. public IEnumerable AnsweredBy(IReadOnlyList endpointMetadata) => Type.Members.Where(fault => FaultExclusions.Excluding(endpointMetadata, fault, Type) is null); @@ -178,7 +186,7 @@ public static InvalidOperationException Unmapped() => /// The mapped answer for one member: what the problem says, before an endpoint changes its status. Its /// Type is the problem's type as the response and the document write it (). /// - internal sealed record Answer(int Status, string? Type, string? Title, string Name, string QualifiedName, string? Description); + internal sealed record Answer(int Status, string? Type, string? Title, string Name, string QualifiedName, string? Description, bool Named); } /// @@ -197,6 +205,9 @@ internal abstract class FaultResponsesMetadata /// The problem types listed for . public abstract IReadOnlyList Types(IServiceProvider services, int status, IReadOnlyList endpointMetadata); + /// Whether a member answered with names its fault (). + public abstract bool? NamesAt(IServiceProvider services, int status, IReadOnlyList endpointMetadata); + /// /// What is wrong with the endpoint's declarations for this fault type: a status or a member left out that /// names no member. @@ -211,8 +222,8 @@ internal abstract class FaultResponsesMetadata public abstract IEnumerable<(IFaultDeclaration Status, string Member, string Message)> StatusesLeftOut(IServiceProvider services, IReadOnlyList endpointMetadata); /// - /// The name and the qualified name of each member the endpoint answers with, those it leaves out aside, or - /// none where the host mapped no status. + /// The name and the qualified name of each member the endpoint answers with and names in the problem, those it + /// leaves out and those whose name it withholds aside, or none where the host mapped no status. /// public abstract IEnumerable<(string Name, string QualifiedName)> Names(IServiceProvider services, IReadOnlyList endpointMetadata); } @@ -233,6 +244,9 @@ private FaultResponsesMetadata() { } public override IReadOnlyList Types(IServiceProvider services, int status, IReadOnlyList endpointMetadata) => Responses(services)?.TypesOf(status, endpointMetadata) ?? []; + public override bool? NamesAt(IServiceProvider services, int status, IReadOnlyList endpointMetadata) => + Responses(services)?.NamesAt(status, endpointMetadata); + public override IEnumerable Problems(IServiceProvider services, IReadOnlyList endpointMetadata) { if (Responses(services) is not { } responses) yield break; @@ -264,7 +278,7 @@ public override IEnumerable Problems(IServiceProvider services, IReadOnl public override IEnumerable<(string Name, string QualifiedName)> Names(IServiceProvider services, IReadOnlyList endpointMetadata) => Responses(services) is { } responses - ? responses.AnsweredBy(endpointMetadata).Select(fault => (responses.Type.NameOf(fault), responses.Type.QualifiedNameOf(fault))) + ? responses.AnsweredBy(endpointMetadata).Where(fault => responses.Of(fault).Named).Select(fault => (responses.Type.NameOf(fault), responses.Type.QualifiedNameOf(fault))) : []; private static FaultResponses? Responses(IServiceProvider services) => FaultResponses.Of(services); diff --git a/src/CodoMetis.TypeKit.AspNetCore/FaultResponsesOperationTransformer.cs b/src/CodoMetis.TypeKit.AspNetCore/FaultResponsesOperationTransformer.cs index 8e936ee6..50f033f1 100644 --- a/src/CodoMetis.TypeKit.AspNetCore/FaultResponsesOperationTransformer.cs +++ b/src/CodoMetis.TypeKit.AspNetCore/FaultResponsesOperationTransformer.cs @@ -89,6 +89,7 @@ public async Task TransformAsync(OpenApiOperation operation, OpenApiOperationTra // Every fault type the endpoint answers with, a Fault's each: a status two of them share is one // response, which lists the problem types of both, and the statuses come in ascending order. var answered = new SortedDictionary>(); + var named = new HashSet(); foreach (var metadata in endpointMetadata.OfType().Distinct()) { var statuses = metadata.Statuses(context.ApplicationServices, endpointMetadata) @@ -100,6 +101,7 @@ public async Task TransformAsync(OpenApiOperation operation, OpenApiOperationTra { if (!answered.TryGetValue(status, out var types)) answered[status] = types = []; types.AddRange(metadata.Types(context.ApplicationServices, status, endpointMetadata).Except(types, StringComparer.Ordinal).ToList()); + if (metadata.NamesAt(context.ApplicationServices, status, endpointMetadata) == true) named.Add(status); } } @@ -122,7 +124,7 @@ public async Task TransformAsync(OpenApiOperation operation, OpenApiOperationTra touched = true; foreach (var schema in schemas) WithFault(schema, Qualified(context)); if (string.IsNullOrEmpty(response.Description) || response.Description == ReasonPhrases.GetReasonPhrase(status)) - response.Description = Description(status, types); + response.Description = Description(status, types, named.Contains(status)); } continue; @@ -131,7 +133,7 @@ public async Task TransformAsync(OpenApiOperation operation, OpenApiOperationTra problem ??= await ProblemSchemaAsync(context, cancellationToken); operation.Responses[key] = new OpenApiResponse { - Description = Description(status, types), + Description = Description(status, types, named.Contains(status)), Content = new Dictionary { ["application/problem+json"] = new() { Schema = problem } }, }; touched = true; @@ -195,10 +197,19 @@ private static (int Status, int Range) Rank(string key) => return schemas; } - private static string Description(int status, List types) => - types.Count == 0 + /// + /// The status's reason phrase, the problem types where the mapping gives any, and, where every fault + /// answered with the status withholds its name, that the problem names none: the shared schema's + /// fault is optional, and the description says which responses never carry it. + /// + private static string Description(int status, List types, bool named) => + (types.Count == 0 ? ReasonPhrases.GetReasonPhrase(status) - : $"{ReasonPhrases.GetReasonPhrase(status)}. Problem types: {string.Join(", ", types)}"; + : $"{ReasonPhrases.GetReasonPhrase(status)}. Problem types: {string.Join(", ", types)}") + + (named ? "" : $". {WithheldDescription}"); + + /// What a response's description says where every fault answered with its status withholds its name. + internal const string WithheldDescription = "The problem names no fault."; private static bool Qualified(OpenApiOperationTransformerContext context) => context.ApplicationServices.GetService>()?.Value.QualifiedFaultNames == true; diff --git a/src/CodoMetis.TypeKit.AspNetCore/README.md b/src/CodoMetis.TypeKit.AspNetCore/README.md index 0428cc4a..231481f0 100644 --- a/src/CodoMetis.TypeKit.AspNetCore/README.md +++ b/src/CodoMetis.TypeKit.AspNetCore/README.md @@ -98,29 +98,20 @@ element schema, so `Quantity` gets no component at all, and both parameters are name, and describes both as the first it meets. A name your own `CreateSchemaReferenceId` gives, set before `AddTypeKit()`, is kept. So give one of them its own name there, or rename one. - To have value objects inlined instead, return `null` for them, the nullable ones included, after - `AddTypeKit()`: + To have value objects described in place instead, with no component of their own, turn on + `InlineValueObjects`: `options.AddTypeKit(typeKit => typeKit.InlineValueObjects = true)`. + [Describe value objects as plain types](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/docs/aspnet-core.md#describe-value-objects-as-plain-types) + shows the document you get. - ```csharp - using System.Reflection; - using CodoMetis.TypeKit.CompilerServices; // GeneratedValueObjectAttribute, on every value object - - options.AddTypeKit(); - options.CreateSchemaReferenceId = type => - (Nullable.GetUnderlyingType(type.Type) ?? type.Type).GetCustomAttribute() is not null - ? null - : OpenApiOptions.CreateDefaultSchemaReferenceId(type); - ``` - - Inlined, a nullable value object or closed set allows null in its own schema, wherever a - referenced one allows it around its reference. A closed set's property is then the enum of its - names plus null. + Inlined, a nullable value object allows null in its own schema, wherever a referenced one allows it + around its reference. Closed sets keep their components with the setting. A host that inlines them + with its own `CreateSchemaReferenceId` gets a closed set's property as the enum of its names plus null. OpenAPI 2.0 can't put null around a reference. ASP.NET writes a nullable property of any component as `{"allOf":[{"x-nullable":true}]}` there, without the reference. So under 2.0 a nullable value-object property loses its type, where the underlying type's nullable property keeps it. A nullable element of - a list or a dictionary loses its type the same way, since it's the component or null too. Inline value - objects if your 2.0 document needs it. + a list or a dictionary loses its type the same way, since it's the component or null too. Turn on + `InlineValueObjects` if your 2.0 document needs it. - **Put rules in `Create`, not on the property.** A validation attribute on a value-object property (`[MaxLength]`, `[Range]`) ends up in the value object's shared component, as ASP.NET does for any @@ -439,6 +430,35 @@ document stay the error's. error's name, the rule is your application's vocabulary. So give rule types distinct names, for instance by nesting a rule in its value object: `Email.Rule.Blank`. +### What failed underneath + +When an exception causes the error, keep it in the `Fault` as its cause. Your log gets the +exception, and the client gets the same problem as before: + +```csharp +Result> saved = Result.Catch(() => store.Save(account)) + .MapError(e => Fault.Of(AccountFault.EmailTaken, "Another account took the address a moment ago.", e)); +``` + +`IFaultFeature.Cause` hands the exception to your `CustomizeProblemDetails`, so you can log it with +the response it led to: + +```csharp +builder.Services.AddProblemDetails(options => options.CustomizeProblemDetails = context => +{ + if (context.HttpContext.Features.Get() is { Cause: { } cause } fault) + context.HttpContext.RequestServices.GetRequiredService().CreateLogger("Faults") + .LogWarning(cause, "Answered {Fault} for an exception", fault.Name); // your log gets the exception, the client doesn't +}); +``` + +- **The cause stays out of the response.** The problem has the same members with a cause as without + one, and names neither the exception's type nor its message. If your `CustomizeProblemDetails` + copies it into the problem, the client learns what failed underneath. +- **`FaultProblem.Cause`** holds it too, for a test or a filter that reads the result before + it's written. +- **It's one exception, not a chain.** An `AggregateException` is fine as the cause. + ### Faults of several types Some rejections mean the same on every route: a change that raced the request, or a version that @@ -601,7 +621,7 @@ works in the same place. A `Result` takes a factory without a value: ### What the host adds to a problem While `ToHttpResult()` writes a problem, the request carries an `IFaultFeature`. It holds the error -type, the error, the name the problem gives it, its detail and its rule. Your +type, the error, the name the problem gives it, its detail, its rule and its cause. Your `CustomizeProblemDetails` reads it to add what the package doesn't name. No other problem the request writes has it. @@ -655,6 +675,24 @@ Both causes then get the same answer: status, `type`, `title`, `fault` and `deta its two errors for its own use, such as a log or a lockout count. A `Fault`'s detail stays behind with them. +Or keep the domain's errors all the way, and leave their name out of the response. +`WithoutFaultName()` in the mapping does that, for the members you choose or for every one: + +```csharp +builder.Services.AddFaultResponses(_ => FaultResponse.Status(StatusCodes.Status401Unauthorized).WithoutFaultName()); + +app.MapPost("/login", (Credentials credentials) => accounts.SignIn(credentials).ToHttpResult()); // no fault, no rule +``` + +- **The problem says nothing of the member**: no `fault`, no `rule`, and no fault set member's + description as its `detail`. A detail you write on the `Fault` still goes out, so keep it + the same for both. +- **Give them the same `type` and `title`**, or those tell them apart instead. +- **Your host still sees the fault.** `IFaultFeature` carries its name, and `NameWithheld` says the + mapping keeps it from the client, so don't copy it into the problem in `CustomizeProblemDetails`. +- **The document says it.** A response whose every fault withholds its name ends its description with + "The problem names no fault." + ## NodaTime ASP.NET can't see through NodaTime's JSON converters. Once they're on the host's JSON options, it diff --git a/src/CodoMetis.TypeKit.AspNetCore/TypeKitHttpResultExtensions.cs b/src/CodoMetis.TypeKit.AspNetCore/TypeKitHttpResultExtensions.cs index 6df36fb1..efbe6b09 100644 --- a/src/CodoMetis.TypeKit.AspNetCore/TypeKitHttpResultExtensions.cs +++ b/src/CodoMetis.TypeKit.AspNetCore/TypeKitHttpResultExtensions.cs @@ -97,7 +97,7 @@ public static Results, FaultProblem> ToHttpResult(this where TFault : notnull => result.Match, FaultProblem>>( static value => TypedResults.Ok(value), - static fault => new FaultProblem(fault.Reason, fault.Detail, fault.Rule)); + static fault => new FaultProblem(fault.Reason, fault.Detail, fault.Rule, fault.Cause)); /// A success as 204 No Content; a fault as problem details for its reason, with its detail. /// The outcome of the operation the endpoint ran. @@ -106,7 +106,7 @@ public static Results> ToHttpResult(this where TFault : notnull => result.Match>>( static () => TypedResults.NoContent(), - static fault => new FaultProblem(fault.Reason, fault.Detail, fault.Rule)); + static fault => new FaultProblem(fault.Reason, fault.Detail, fault.Rule, fault.Cause)); /// A success as the response makes of its value; a fault as problem details for its reason, with its detail. /// The outcome of the operation the endpoint ran. @@ -118,7 +118,7 @@ public static Results> ToHttpResult(this where TSuccess : IResult { ArgumentNullException.ThrowIfNull(success); - return result.Match>>(value => success(value), static fault => new FaultProblem(fault.Reason, fault.Detail, fault.Rule)); + return result.Match>>(value => success(value), static fault => new FaultProblem(fault.Reason, fault.Detail, fault.Rule, fault.Cause)); } /// A success as the response makes; a fault as problem details for its reason, with its detail. @@ -130,7 +130,7 @@ public static Results> ToHttpResult(this where TSuccess : IResult { ArgumentNullException.ThrowIfNull(success); - return result.Match>>(() => success(), static fault => new FaultProblem(fault.Reason, fault.Detail, fault.Rule)); + return result.Match>>(() => success(), static fault => new FaultProblem(fault.Reason, fault.Detail, fault.Rule, fault.Cause)); } /// A success as 200 OK with its value; a fault of either type as problem details for its reason, with its detail. @@ -249,7 +249,7 @@ public static Results, FaultProblem, FaultProble static fault => fault.Match, FaultProblem, FaultProblem>>(static f => Problem(f), static f => Problem(f), static f => Problem(f))); } - private static FaultProblem Problem(Fault fault) where TFault : notnull => new(fault.Reason, fault.Detail, fault.Rule); + private static FaultProblem Problem(Fault fault) where TFault : notnull => new(fault.Reason, fault.Detail, fault.Rule, fault.Cause); } /// Turns a Result whose fault is a closed set into the HTTP response an endpoint returns, as does for an enum. diff --git a/src/CodoMetis.TypeKit.AspNetCore/TypeKitOpenApiOptions.cs b/src/CodoMetis.TypeKit.AspNetCore/TypeKitOpenApiOptions.cs new file mode 100644 index 00000000..0d545900 --- /dev/null +++ b/src/CodoMetis.TypeKit.AspNetCore/TypeKitOpenApiOptions.cs @@ -0,0 +1,19 @@ +namespace CodoMetis.TypeKit.AspNetCore; + +/// How AddTypeKit() describes value objects: options.AddTypeKit(typeKit => …). +public sealed class TypeKitOpenApiOptions +{ + /// + /// Whether every value object is described in place, by its wrapped type's schema, rather than as a component + /// named after it: as a property, a request or response body, a list's or a dictionary's elements, a parameter, + /// and below any of them. The document then holds no component of a value object, and a nullable one admits null + /// in its own schema, as a nullable property of the wrapped type does. Off by default: each value object keeps its + /// component. + /// + /// + /// A closed set and the type a value object wraps (an enum, a record) keep the component ASP.NET gives them. A + /// of the host's own set before + /// AddTypeKit still names every other type; one set after it replaces this, as it replaces the naming. + /// + public bool InlineValueObjects { get; set; } +} diff --git a/src/CodoMetis.TypeKit.AspNetCore/TypeKitOpenApiOptionsExtensions.cs b/src/CodoMetis.TypeKit.AspNetCore/TypeKitOpenApiOptionsExtensions.cs index 3fefa91c..cfc39515 100644 --- a/src/CodoMetis.TypeKit.AspNetCore/TypeKitOpenApiOptionsExtensions.cs +++ b/src/CodoMetis.TypeKit.AspNetCore/TypeKitOpenApiOptionsExtensions.cs @@ -45,11 +45,29 @@ public static class TypeKitOpenApiOptionsExtensions /// /// The options of one OpenAPI document. /// The same options. - public static OpenApiOptions AddTypeKit(this OpenApiOptions options) + public static OpenApiOptions AddTypeKit(this OpenApiOptions options) => AddTypeKit(options, static _ => { }); + + /// + /// Describes every value object in the document with the schema of the type it wraps, as + /// does, with the settings chooses. + /// + /// + /// With , a value object is written in place wherever it + /// appears, and no component is named after one. Without a setting, the document is the one + /// writes. + /// + /// The options of one OpenAPI document. + /// Chooses the settings. + /// The same options. + public static OpenApiOptions AddTypeKit(this OpenApiOptions options, Action configure) { ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(configure); + + var settings = new TypeKitOpenApiOptions(); + configure(settings); - ValueObjectSchemaReferenceIds.Apply(options); + ValueObjectSchemaReferenceIds.Apply(options, settings.InlineValueObjects); PatchSchemaTransformer.Apply(options); return options.AddSchemaTransformer() diff --git a/src/CodoMetis.TypeKit.AspNetCore/ValueObjectSchemaReferenceIds.cs b/src/CodoMetis.TypeKit.AspNetCore/ValueObjectSchemaReferenceIds.cs index c26af7c0..f129a3fb 100644 --- a/src/CodoMetis.TypeKit.AspNetCore/ValueObjectSchemaReferenceIds.cs +++ b/src/CodoMetis.TypeKit.AspNetCore/ValueObjectSchemaReferenceIds.cs @@ -7,7 +7,7 @@ namespace CodoMetis.TypeKit.AspNetCore; /// Names a nested value object's or closed set's component after its whole nesting chain: /// Shop.Id is ShopId, as its generated companion class is ShopIdExtensions, and /// Order.Status is OrderStatus. Refuses a document in which such a name is also another -/// type's. +/// type's. With , gives a value object no component. /// /// /// @@ -33,19 +33,27 @@ namespace CodoMetis.TypeKit.AspNetCore; /// documents are two sets of components. A type's name is the same on every call, so a document /// generated again, or two generated at once, claim the same names. /// +/// +/// Inlined, a value object, or the of one, has no name whatever the host's own delegate +/// says, so it claims none: two value objects of one name can no longer share a component, and a closed set or any +/// other type keeps its name and its claim as before +/// ([decision 115](design/adr/0115-value-objects-can-be-inlined-everywhere.md)). +/// /// internal static class ValueObjectSchemaReferenceIds { - public static void Apply(OpenApiOptions options) + public static void Apply(OpenApiOptions options, bool inlineValueObjects) { var inner = options.CreateSchemaReferenceId; var claims = new ConcurrentDictionary(StringComparer.Ordinal); options.CreateSchemaReferenceId = type => { + var claimant = Nullable.GetUnderlyingType(type.Type) ?? type.Type; + if (inlineValueObjects && ValueObjectTypes.IsValueObject(claimant)) return null; + if (inner(type) is not { } id) return null; - var claimant = Nullable.GetUnderlyingType(type.Type) ?? type.Type; var renamed = claimant.IsNested && (ValueObjectTypes.IsValueObject(claimant) || ClosedSetSchemaTransformer.Describe(claimant) is not null) && id == OpenApiOptions.CreateDefaultSchemaReferenceId(type); diff --git a/src/CodoMetis.TypeKit.EntityFrameworkCore/README.md b/src/CodoMetis.TypeKit.EntityFrameworkCore/README.md index 1e56ef6f..47641fc3 100644 --- a/src/CodoMetis.TypeKit.EntityFrameworkCore/README.md +++ b/src/CodoMetis.TypeKit.EntityFrameworkCore/README.md @@ -87,8 +87,9 @@ Server it's an identity column, so switching an `int` key to a value object chan Where the provider has no identity for the type, the key isn't generated, just as a plain one isn't. On PostgreSQL, for example, a `ulong` is a `numeric(20,0)`, which can't be an identity. -There are two exceptions. EF picks the generator that fills a value object's key at `Add` by the -column's type, not by the underlying type: +At `Add`, a key that is generated gets the same temporary value a plain key of the underlying type +gets, also where the column is of another type, such as the `decimal` that holds a `ulong`. HiLo and a +value generator of your own still come first. There are two exceptions: - **A key over `char` is yours to assign.** It's never generated, on any provider, because none generates a `char` key. PostgreSQL rejects an identity on `character(1)`, SQLite an `AUTOINCREMENT` @@ -97,10 +98,13 @@ column's type, not by the underlying type: value, but it doesn't check that you set a meaningful one. A default key, `'\0'`, is stored as it is, and a second such row fails on the primary key. `ValueGeneratedOnAdd()` or a value generator of your own on the property still applies. -- **A default key over `ulong` throws at `Add` where the column is a `decimal`** (SQL Server, - PostgreSQL). EF's temporary value for the column is a negative `decimal`, and converting it back - throws `OverflowException`. A plain `ulong` key gets a temporary `ulong` instead. Configure - `ValueGeneratedNever()` on such a key and assign it. +- **A default key over `ulong` can still throw at `Add`** where the column is a `decimal` (SQL + Server, PostgreSQL), in two cases. One is a compiled model made with `dotnet ef dbcontext optimize + --nativeaot`, which writes each column's mapping out as code and loses what gives the key a + temporary `ulong`. The other is a key you configure with `ValueObjectConverter` yourself. EF's + temporary value for the column is then a negative `decimal`, and converting it back throws + `OverflowException`. Otherwise the key gets a temporary `ulong`, as a plain one does. In those two + cases, configure `ValueGeneratedNever()` on the key and assign it. A key over a `Guid` is yours to assign, `Id = OrderId.New()` (a version 7 Guid, which sorts by creation time), and EF inserts it as given. To have EF generate it instead, configure @@ -212,9 +216,34 @@ instead, which works everywhere and may add a cast: and stays allowed. A query filter (`HasQueryFilter`, named or not) that does the same fails when the model is built, naming the entity type, since it applies to every query of that type. -The mapping is tested on SQLite and PostgreSQL. The SQL of each translation is pinned on PostgreSQL, -every family of underlying types makes a round trip through a real PostgreSQL, and key generation is -also checked in SQL Server's model. +### On SQL Server + +A value object behaves on SQL Server as the type inside it does there. That isn't always how it +behaves in memory: + +- **Text compares ignoring case.** Under SQL Server's default collation, `o.Note == note`, + `notes.Contains(o.Note)` and `StartsWith` also match text that differs in case or in trailing + spaces. `OrderBy` sorts ignoring case too. In memory, a value object over text equals only the same + text, exactly. Where case matters, give the column a binary collation: + `UseCollation("Latin1_General_BIN2")`. +- **Guids sort by their last group.** SQL Server orders a `uniqueidentifier` by its last six bytes + first. So `OrderBy`, `<` and `Min` over Guids come out in another order than in memory. +- **A `decimal` without a precision is a `decimal(18,2)`.** It reads back with two decimal places: + `10m` comes back as `10.00m`, which is equal but prints `10.00`. More decimal places are rounded + when the value is written. Configure the precision you need, on the property with `HasPrecision` + or for every such value object with `configurationBuilder.Properties().HavePrecision(18, 4)`. +- **A `TimeSpan` must be shorter than a day.** It's stored as `time`. A longer one fails + `SaveChanges` in a column. In a JSON column it's stored, but a query that reads it gets another + value, `01:02:00` for `1.02:00:00`, without an error. Store a longer duration as a number instead, + such as a value object over the seconds as a `long`. +- **`EF.Functions.Greatest` and `Least` need compatibility level 160.** Configure it with + `UseSqlServer(connectionString, sql => sql.UseCompatibilityLevel(160))`. Below it, EF refuses them. +- **A `DateTime` comes back without its kind.** `datetime2` keeps every tick, and a UTC value reads + back with `Kind` `Unspecified`. It's still equal to the value you wrote. + +The mapping is tested on SQLite, PostgreSQL and SQL Server. The SQL of each translation is pinned on +PostgreSQL. Every family of underlying types makes a round trip through a real PostgreSQL and a real +SQL Server, and every query and key shape of the test catalogue runs on the real SQL Server too. ## An Option for nothing found @@ -237,8 +266,10 @@ These ask the database for one row and let it run the condition: - **The condition must be an expression.** A lambda written at the call is one. A `Func` held in a variable, or a method group, binds `FirstOrNone`, `SingleOrNone` and `FirstOrError` to their overloads on a sequence. Then every row is read and the condition runs in - memory, without a warning. Hold an `Expression>` instead. `FirstOrNoneAsync` - takes only an expression, so a `Func` there doesn't compile. + memory, and the analyzer warns at the call + ([CMTK0009](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.Analyzers/README.md#a-condition-that-reads-every-row)). + Hold an `Expression>` instead. `FirstOrNoneAsync` takes only an expression, so a + `Func` there doesn't compile. - **`LastOrNone` and `LastOrError` on a query don't compile.** A query has no last row until it's ordered. So order it the other way and take the first, or call `AsEnumerable()` to read every row on purpose. diff --git a/src/CodoMetis.TypeKit.EntityFrameworkCore/ValueObjectKeyValueGenerationConvention.cs b/src/CodoMetis.TypeKit.EntityFrameworkCore/ValueObjectKeyValueGenerationConvention.cs index c64d73ee..68def304 100644 --- a/src/CodoMetis.TypeKit.EntityFrameworkCore/ValueObjectKeyValueGenerationConvention.cs +++ b/src/CodoMetis.TypeKit.EntityFrameworkCore/ValueObjectKeyValueGenerationConvention.cs @@ -30,10 +30,10 @@ namespace CodoMetis.TypeKit.EntityFrameworkCore; /// it is: nothing checks that the application set a meaningful character. /// /// -/// A key over stays declared generated, as a plain one is, and fails loudly where the -/// column is a decimal (SQL Server, PostgreSQL): a default key throws -/// at Add, from EF's negative temporary decimal. ValueGeneratedNever() and an -/// assigned key are the workaround. +/// A key over stays declared generated, as a plain one is. Where the column is a +/// decimal (SQL Server, PostgreSQL), its temporary value at Add is a , +/// from the factory its converter's mapping hints carry (, decision +/// 114), except in a --nativeaot compiled model, where it still throws . /// /// /// diff --git a/src/CodoMetis.TypeKit.EntityFrameworkCore/ValueObjectMapping.cs b/src/CodoMetis.TypeKit.EntityFrameworkCore/ValueObjectMapping.cs index 6bf15311..01950dd1 100644 --- a/src/CodoMetis.TypeKit.EntityFrameworkCore/ValueObjectMapping.cs +++ b/src/CodoMetis.TypeKit.EntityFrameworkCore/ValueObjectMapping.cs @@ -3,7 +3,9 @@ using System.Reflection; using CodoMetis.TypeKit.CompilerServices; using CodoMetis.TypeKit.ValueObjects; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.ValueGeneration; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage.Json; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -68,8 +70,35 @@ private sealed class Of : ValueObjectMapping where TValueObject : IValueObject, IValueObjectMaterializer where T : notnull { - public override ValueConverter Converter { get; } = - new ValueConverter(ValueObjectConverter.ToProvider, ValueObjectConverter.FromProvider); + public Of() => + Converter = new ValueConverter( + ValueObjectConverter.ToProvider, + ValueObjectConverter.FromProvider, + IsTemporaryNumber ? new ConverterMappingHints(valueGeneratorFactory: TemporaryNumber) : null); + + public override ValueConverter Converter { get; } + + /// + /// The types EF gives a generated property a temporary number of (RelationalValueGeneratorSelector.FindForType + /// in EF Core 10), chosen by the property's type mapping: for a value object that is its own, so EF fell back to + /// the column's type, and a key over got a negative temporary where the + /// column is one, which threw converted back. + /// + private static readonly bool IsTemporaryNumber = + typeof(T) == typeof(int) || typeof(T) == typeof(long) || typeof(T) == typeof(short) || typeof(T) == typeof(byte) + || typeof(T) == typeof(char) || typeof(T) == typeof(ulong) || typeof(T) == typeof(uint) || typeof(T) == typeof(ushort) + || typeof(T) == typeof(sbyte) || typeof(T) == typeof(decimal) || typeof(T) == typeof(float) || typeof(T) == typeof(double); + + /// + /// The temporary values a property of the wrapped type gets, as the value object: EF's own where the column is of + /// the wrapped type, as before, and the provider's for a key of the wrapped type where it is stored as another. + /// A type mapping's factory, which EF consults after a generator configured on the property and after the + /// providers' HiLo, and which a mapping built on the converter takes from its hints (decision 114). + /// + private ValueGenerator TemporaryNumber(IProperty property, IEntityType entityType) => + property.GetTypeMapping().Converter?.ProviderClrType == typeof(T) + ? new WrappedTypeValueGenerator(Converter, new TemporaryNumberValueGeneratorFactory().Create(property, entityType)) + : new WrappedTypeValueGenerator(Converter, WrappedTypeValueGenerator.ProviderGenerator); private static readonly MethodInfo ProviderValue = ((MethodCallExpression)ValueObjectConverter.ToProvider.Body).Method; diff --git a/src/CodoMetis.TypeKit.EntityFrameworkCore/WrappedTypeValueGeneratorFactory.cs b/src/CodoMetis.TypeKit.EntityFrameworkCore/WrappedTypeValueGeneratorFactory.cs index c97f3bef..c6e564d1 100644 --- a/src/CodoMetis.TypeKit.EntityFrameworkCore/WrappedTypeValueGeneratorFactory.cs +++ b/src/CodoMetis.TypeKit.EntityFrameworkCore/WrappedTypeValueGeneratorFactory.cs @@ -41,7 +41,9 @@ public override ValueGenerator Create(IProperty property, ITypeBase typeBase) return ValueObjectTypes.Describe(Nullable.GetUnderlyingType(property.ClrType) ?? property.ClrType) is { } valueObject && WrappedTypeValueGenerator.Generates(valueObject.WrappedType) - ? new WrappedTypeValueGenerator(ValueObjectMapping.For(valueObject).Converter, valueObject.WrappedType) + ? new WrappedTypeValueGenerator( + ValueObjectMapping.For(valueObject).Converter, + valueObject.WrappedType == typeof(Guid) ? WrappedTypeValueGenerator.ProviderGenerator : WrappedTypeValueGenerator.ProviderGenerator) : throw new InvalidOperationException( $"{property.DeclaringType.DisplayName()}.{property.Name} is no value object over a Guid or a string, so it has no generator of its wrapped type's."); } @@ -52,10 +54,15 @@ public override ValueGenerator Create(IProperty property, ITypeBase typeBase) /// The provider is asked once, at the first value, about a key of the wrapped type in a model of its own, /// built with the context's conventions. /// -internal sealed class WrappedTypeValueGenerator(ValueConverter converter, Type wrapped) : ValueGenerator +internal sealed class WrappedTypeValueGenerator(ValueConverter converter, Func providerGenerator) : ValueGenerator { private ValueGenerator? _provider; + /// A generator of the wrapped type's values already chosen, its values converted to the value object. + public WrappedTypeValueGenerator(ValueConverter converter, ValueGenerator provider) + : this(converter, _ => provider) => + _provider = provider; + /// The wrapped types whose generator the providers choose by the property's CLR type. public static bool Generates(Type wrapped) => wrapped == typeof(Guid) || wrapped == typeof(string); @@ -64,35 +71,28 @@ internal sealed class WrappedTypeValueGenerator(ValueConverter converter, Type w _provider?.GeneratesTemporaryValues ?? throw new InvalidOperationException("Whether a value object's generated value is temporary is known only once one is generated."); - /// None of EF's or the providers' generators of a Guid or a string makes stable values. + /// None of EF's or the providers' generators of a Guid, a string or a temporary number makes stable values. public override bool GeneratesStableValues => false; protected override object? NextValue(EntityEntry entry) => - converter.ConvertFromProvider((_provider ??= ProviderGenerator(entry.Context, wrapped)).Next(entry)); + converter.ConvertFromProvider((_provider ??= providerGenerator(entry.Context)).Next(entry)); - private static ValueGenerator ProviderGenerator(DbContext context, Type wrapped) + /// The generator the provider gives a key of generated on add. + public static ValueGenerator ProviderGenerator(DbContext context) { var modelBuilder = new ModelBuilder(context.GetService().CreateConventionSet(), context.GetService()); - if (wrapped == typeof(Guid)) - modelBuilder.Entity().Property(key => key.Id).ValueGeneratedOnAdd(); - else - modelBuilder.Entity().Property(key => key.Id).ValueGeneratedOnAdd(); + modelBuilder.Entity>().Property(key => key.Id).ValueGeneratedOnAdd(); var model = context.GetService().Initialize(modelBuilder.FinalizeModel(), designTime: false, validationLogger: null); - var entityType = model.FindEntityType(wrapped == typeof(Guid) ? typeof(GuidKey) : typeof(StringKey))!; + var entityType = model.FindEntityType(typeof(Key))!; return context.GetService().TrySelect(entityType.FindPrimaryKey()!.Properties[0], entityType, out var generator) && generator is not null ? generator - : throw new InvalidOperationException($"The provider has no generator for a key of {wrapped.Name} generated on add."); - } - - private sealed class GuidKey - { - public Guid Id { get; set; } + : throw new InvalidOperationException($"The provider has no generator for a key of {typeof(T).Name} generated on add."); } - private sealed class StringKey + private sealed class Key { - public string Id { get; set; } = ""; + public T Id { get; set; } = default!; } } diff --git a/src/CodoMetis.TypeKit.Generators/CodeAnnotations.cs b/src/CodoMetis.TypeKit.Generators/CodeAnnotations.cs index 65d74cbf..3e9ea062 100644 --- a/src/CodoMetis.TypeKit.Generators/CodeAnnotations.cs +++ b/src/CodoMetis.TypeKit.Generators/CodeAnnotations.cs @@ -8,31 +8,36 @@ namespace CodoMetis.TypeKit.Generators; /// The attributes the aspects put on what they generate. +/// +/// Each is built where it is used, never kept in a static field: an +/// belongs to one compilation, and a static field would keep every compilation the IDE ever made alive +/// (CompileTimeStaticStateTests). +/// [CompileTime] internal static class CodeAnnotations { - public static readonly AttributeConstruction AggressiveInlining = + public static AttributeConstruction AggressiveInlining => AttributeConstruction.Create(typeof(MethodImplAttribute), [MethodImplOptions.AggressiveInlining]); /// /// On the source parameter of the generated FromKnownGood, whose first parameter is /// always named value. /// - public static readonly AttributeConstruction CallerArgumentExpressionOfValue = + public static AttributeConstruction CallerArgumentExpressionOfValue => AttributeConstruction.Create(typeof(CallerArgumentExpressionAttribute), ["value"]); - public static readonly AttributeConstruction CompilerGenerated = + public static AttributeConstruction CompilerGenerated => AttributeConstruction.Create(typeof(CompilerGeneratedAttribute)); - public static readonly AttributeConstruction DebuggerHidden = + public static AttributeConstruction DebuggerHidden => AttributeConstruction.Create(typeof(DebuggerHiddenAttribute)); - public static readonly AttributeConstruction DebuggerStepThrough = + public static AttributeConstruction DebuggerStepThrough => AttributeConstruction.Create(typeof(DebuggerStepThroughAttribute)); - public static readonly AttributeConstruction EditorNonBrowsable = + public static AttributeConstruction EditorNonBrowsable => AttributeConstruction.Create(typeof(EditorBrowsableAttribute), [EditorBrowsableState.Never]); - public static readonly AttributeConstruction TranslatedAsWrappedValue = + public static AttributeConstruction TranslatedAsWrappedValue => AttributeConstruction.Create(typeof(TranslatedAsWrappedValueAttribute)); } diff --git a/src/CodoMetis.TypeKit.Generators/README.md b/src/CodoMetis.TypeKit.Generators/README.md index 7037b355..8d3818be 100644 --- a/src/CodoMetis.TypeKit.Generators/README.md +++ b/src/CodoMetis.TypeKit.Generators/README.md @@ -2,8 +2,9 @@ Declare a value object in one line, and get a complete type at compile time. This package generates value objects and closed sets (smart enums) for -[CodoMetis.TypeKit](https://www.nuget.org/packages/CodoMetis.TypeKit), built on -[Metalama](https://www.postsharp.net/metalama). Each one comes with its JSON converter, parsing, +[CodoMetis.TypeKit](https://www.nuget.org/packages/CodoMetis.TypeKit). It is built on +[Metalama](https://postsharp.net/metalama), a framework that generates C# code at compile time from +templates written in C#. Each value object comes with its JSON converter, parsing, formatting, comparison, type converter and query helpers, and every one of them runs the one `Create` you wrote. @@ -25,7 +26,7 @@ one of them is the better choice, read ## Built with Metalama -This package exists in this form because of [Metalama](https://www.postsharp.net/metalama). A +This package exists in this form because of [Metalama](https://postsharp.net/metalama). A transitive fabric finds every type that implements a TypeKit interface, in the project that references this package and in every project that references that one. So you declare a value object by its interface and nothing else. @@ -68,6 +69,23 @@ to be a reference type. There are two interfaces: - **`IValidatedValue`** adds `Create`, the one method you write. Every way a value gets created runs it. +You don't have to type `Create` out. Declare the interface first. The build then fails because +`Create` is missing (CS0535), and your editor offers a fix for that error: **Add Create**. It writes +a `Create` that accepts every value, ready for your rules: + +```csharp +public enum NameFault { Blank, TooLong } + +public readonly partial record struct Name : IValidatedValue +{ + public static Result Create(string value) + { + // Add your rules: if (...) return Result.Error(NameFault.Blank); + return new Name(value); + } +} +``` + A validated value object has no `From`. You create one with `Create`, `TryFrom` or `FromKnownGood`, so nothing harmless-looking throws on input. The one factory that throws says in its name that you vouch for the value, and its exception names the call site, never the value. @@ -75,6 +93,48 @@ vouch for the value, and its exception names the call site, never the value. The error `Create` returns (TypeKit calls it a fault) is what every rejection reports: in that exception, in the JSON converter's `JsonException`, and in `Parse`'s `FormatException`. +
+What the generators write for Email, trimmed + +Every factory below calls the `Create` you wrote: + + +```csharp +public readonly partial record struct Email : IValidatedValue, IValueObject +{ + private readonly string _value; + + private Email(string value) + { + _value = value; + } + + public string Value + { + get { return _value; } + } + + public static Option TryFrom(string value) + { + return Result.ToOption(Email.Create(value)); + } + + public static Email FromKnownGood(string value, [CallerArgumentExpression("value")] string? source = null) + { + return GeneratedFactories.OrInvalidOperationException(Email.Create(value), source); + } + + public Result Revalidate() + { + return Email.Create(Value); + } + + // Also: Parse and TryParse, formatting, comparison, a JSON converter and a type converter. +} +``` + +
+ You can declare a value object inside another type. `Order.Id` is generated like any other. Its companion class is `OrderIdExtensions`, named after the whole nesting chain, so `Order.Id` and `Customer.Id` don't collide. @@ -297,6 +357,52 @@ The generators add: Members are singletons, so equality is reference equality. A member added later fails every `Match` until it's handled. +
+What the generators write for OrderStatus, trimmed + + +```csharp +public sealed partial class OrderStatus : IClosedSet, IParsable +{ + private static readonly IReadOnlyList _all = null!; + + private string _name = null!; + + static OrderStatus() + { + OrderStatus.Open._name = "Open"; + OrderStatus.Shipped._name = "Shipped"; + OrderStatus.Cancelled._name = "Cancelled"; + OrderStatus._all = Array.AsReadOnly(new OrderStatus[] { OrderStatus.Open, OrderStatus.Shipped, OrderStatus.Cancelled }); + } + + private OrderStatus() { } + + public string Name + { + get { return _name ?? throw GeneratedClosedSets.NotAMember("OrderStatus"); } + } + + public TResult Match(Func open, Func shipped, Func cancelled) + { + ArgumentNullException.ThrowIfNull(open, "open"); + ArgumentNullException.ThrowIfNull(shipped, "shipped"); + ArgumentNullException.ThrowIfNull(cancelled, "cancelled"); + if (object.ReferenceEquals(this, OrderStatus.Open)) + return open.Invoke(); + if (object.ReferenceEquals(this, OrderStatus.Shipped)) + return shipped.Invoke(); + if (object.ReferenceEquals(this, OrderStatus.Cancelled)) + return cancelled.Invoke(); + throw GeneratedClosedSets.NotAMember("OrderStatus"); + } + + // Also: All, QualifiedName, Parse and TryParse, ToString and a JSON converter. +} +``` + +
+ ### Matching, parsing and binding - **`Match` arguments must be named (CMTK0012).** Every parameter of `Match` has the same type, so an diff --git a/src/CodoMetis.TypeKit.Generators/ValueObjectParsableAspect.Templates.cs b/src/CodoMetis.TypeKit.Generators/ValueObjectParsableAspect.Templates.cs index 93c0e436..84fb9265 100644 --- a/src/CodoMetis.TypeKit.Generators/ValueObjectParsableAspect.Templates.cs +++ b/src/CodoMetis.TypeKit.Generators/ValueObjectParsableAspect.Templates.cs @@ -34,25 +34,31 @@ public static bool TryParseTemplate( result = meta.Default(tag.ValueObjectType); + // The branches are an if-else chain, not ifs that return: a return in a compile-time branch ends + // the run-time body, but the compile-time calls after it still run, and the InsertStatements + // below wrote an unreachable tail into every other TryParse (WovenReachabilityTests). if (meta.CompileTime(tag.InnerTryParse() is not null)) + { return (bool)ExpressionFactory.Parse(tag.InnerTryParse()!, TypeFactory.GetType(SpecialType.Boolean), false).Value!; - - if (meta.CompileTime(tag.Strategy == ValueParseStrategy.String)) + } + else if (meta.CompileTime(tag.Strategy == ValueParseStrategy.String)) { return (bool)ExpressionFactory.Parse($"(s is not null && {tag.TryFromText}(s, out result))", TypeFactory.GetType(SpecialType.Boolean), false).Value!; } + else + { + // The remaining strategies have no TryParse of their own: their inner parse throws, and what it + // throws for text the wrapped type cannot read is GeneratedParsing.Unreadable, a FormatException. + // Only that is caught, around the inner parse alone: a bare catch around the wrapping as well + // made an exception from Create a false, where Parse, and TryParse over a type with a TryParse + // of its own, let it through. + if (s is null) return false; - // The remaining strategies have no TryParse of their own: their inner parse throws, and what it - // throws for text the wrapped type cannot read is GeneratedParsing.Unreadable, a FormatException. - // Only that is caught, around the inner parse alone: a bare catch around the wrapping as well - // made an exception from Create a false, where Parse, and TryParse over a type with a TryParse - // of its own, let it through. - if (s is null) return false; - - meta.InsertStatement($"{ValueObjectTypes.SourceName(tag.ValueType)} __inner;"); - meta.InsertStatement($"try {{ __inner = {tag.InnerParse()}; }} catch (global::System.FormatException) {{ return false; }}"); + meta.InsertStatement($"{ValueObjectTypes.SourceName(tag.ValueType)} __inner;"); + meta.InsertStatement($"try {{ __inner = {tag.InnerParse()}; }} catch (global::System.FormatException) {{ return false; }}"); - return (bool)ExpressionFactory.Parse($"{tag.TryFromText}(__inner, out result)", TypeFactory.GetType(SpecialType.Boolean), false).Value!; + return (bool)ExpressionFactory.Parse($"{tag.TryFromText}(__inner, out result)", TypeFactory.GetType(SpecialType.Boolean), false).Value!; + } } [Template] diff --git a/src/CodoMetis.TypeKit.Testing/FaultAssertions.cs b/src/CodoMetis.TypeKit.Testing/FaultAssertions.cs index 6b825b3d..f7b676f5 100644 --- a/src/CodoMetis.TypeKit.Testing/FaultAssertions.cs +++ b/src/CodoMetis.TypeKit.Testing/FaultAssertions.cs @@ -49,6 +49,76 @@ public static Fault ShouldHold<[Distinct] T1, [Distinct] T2, [Distinct] T3>( public static Fault ShouldHold<[Distinct] T1, [Distinct] T2, [Distinct] T3>(this Fault fault, T3 reason) where T1 : notnull where T2 : notnull where T3 : notnull => HeldFaults.Expect, T3>(reason, Uninitialized(fault), () => fault.Match(held => held, held => held, held => held)); + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task> ShouldHold<[Distinct] T1, [Distinct] T2>(this Task> fault, T1 reason) where T1 : notnull where T2 : notnull + { + return (await Outcomes.Pending(fault, nameof(fault)).ConfigureAwait(false)).ShouldHold(reason); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task> ShouldHold<[Distinct] T1, [Distinct] T2>(this ValueTask> fault, T1 reason) where T1 : notnull where T2 : notnull + { + return (await fault.ConfigureAwait(false)).ShouldHold(reason); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task> ShouldHold<[Distinct] T1, [Distinct] T2>(this Task> fault, T2 reason) where T1 : notnull where T2 : notnull + { + return (await Outcomes.Pending(fault, nameof(fault)).ConfigureAwait(false)).ShouldHold(reason); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task> ShouldHold<[Distinct] T1, [Distinct] T2>(this ValueTask> fault, T2 reason) where T1 : notnull where T2 : notnull + { + return (await fault.ConfigureAwait(false)).ShouldHold(reason); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task> ShouldHold<[Distinct] T1, [Distinct] T2, [Distinct] T3>(this Task> fault, T1 reason) where T1 : notnull where T2 : notnull where T3 : notnull + { + return (await Outcomes.Pending(fault, nameof(fault)).ConfigureAwait(false)).ShouldHold(reason); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task> ShouldHold<[Distinct] T1, [Distinct] T2, [Distinct] T3>(this ValueTask> fault, T1 reason) where T1 : notnull where T2 : notnull where T3 : notnull + { + return (await fault.ConfigureAwait(false)).ShouldHold(reason); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task> ShouldHold<[Distinct] T1, [Distinct] T2, [Distinct] T3>(this Task> fault, T2 reason) where T1 : notnull where T2 : notnull where T3 : notnull + { + return (await Outcomes.Pending(fault, nameof(fault)).ConfigureAwait(false)).ShouldHold(reason); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task> ShouldHold<[Distinct] T1, [Distinct] T2, [Distinct] T3>(this ValueTask> fault, T2 reason) where T1 : notnull where T2 : notnull where T3 : notnull + { + return (await fault.ConfigureAwait(false)).ShouldHold(reason); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task> ShouldHold<[Distinct] T1, [Distinct] T2, [Distinct] T3>(this Task> fault, T3 reason) where T1 : notnull where T2 : notnull where T3 : notnull + { + return (await Outcomes.Pending(fault, nameof(fault)).ConfigureAwait(false)).ShouldHold(reason); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task> ShouldHold<[Distinct] T1, [Distinct] T2, [Distinct] T3>(this ValueTask> fault, T3 reason) where T1 : notnull where T2 : notnull where T3 : notnull + { + return (await fault.ConfigureAwait(false)).ShouldHold(reason); + } + private static bool Uninitialized(TComposed fault) where TComposed : struct => EqualityComparer.Default.Equals(fault, default); } diff --git a/src/CodoMetis.TypeKit.Testing/OptionAssertions.cs b/src/CodoMetis.TypeKit.Testing/OptionAssertions.cs index 5eaa2d40..e7db1a8f 100644 --- a/src/CodoMetis.TypeKit.Testing/OptionAssertions.cs +++ b/src/CodoMetis.TypeKit.Testing/OptionAssertions.cs @@ -24,4 +24,32 @@ public static void ShouldBeNone(this Option option) where T : notnull if (option.IsSome()) throw new TypeKitAssertionException($"Expected None, but the {Outcomes.Name>()} holds a value (not printed)."); } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task ShouldBeSome(this Task> option) where T : notnull + { + return (await Outcomes.Pending(option, nameof(option)).ConfigureAwait(false)).ShouldBeSome(); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task ShouldBeSome(this ValueTask> option) where T : notnull + { + return (await option.ConfigureAwait(false)).ShouldBeSome(); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task ShouldBeNone(this Task> option) where T : notnull + { + (await Outcomes.Pending(option, nameof(option)).ConfigureAwait(false)).ShouldBeNone(); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task ShouldBeNone(this ValueTask> option) where T : notnull + { + (await option.ConfigureAwait(false)).ShouldBeNone(); + } } diff --git a/src/CodoMetis.TypeKit.Testing/Outcomes.cs b/src/CodoMetis.TypeKit.Testing/Outcomes.cs index 9bda0d7e..60fb8b24 100644 --- a/src/CodoMetis.TypeKit.Testing/Outcomes.cs +++ b/src/CodoMetis.TypeKit.Testing/Outcomes.cs @@ -16,6 +16,9 @@ public static void ThrowIfOtherError(TError actual, TError exp throw new TypeKitAssertionException($"Expected the error {expected}, but the {Name()} is the error {actual}."); } + /// The task an asynchronous assertion awaits, refused when null: awaited, it was a naming nothing. + public static Task Pending(Task? task, string paramName) => task ?? throw new ArgumentNullException(paramName, "The assertion was given no task to await."); + /// /// Result<Order, OrderFault>, as the type is written in C# from outside its containing /// types: Billing.Fault, Outer<Int32>.Inner. diff --git a/src/CodoMetis.TypeKit.Testing/PatchAssertions.cs b/src/CodoMetis.TypeKit.Testing/PatchAssertions.cs index 5a131c6c..769b45ad 100644 --- a/src/CodoMetis.TypeKit.Testing/PatchAssertions.cs +++ b/src/CodoMetis.TypeKit.Testing/PatchAssertions.cs @@ -34,6 +34,48 @@ public static void ShouldBeAbsent(this Patch patch) where T : notnull throw new TypeKitAssertionException($"Expected Absent, but the {Outcomes.Name>()} {Was(patch.State)}."); } + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task ShouldBeValue(this Task> patch) where T : notnull + { + return (await Outcomes.Pending(patch, nameof(patch)).ConfigureAwait(false)).ShouldBeValue(); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task ShouldBeValue(this ValueTask> patch) where T : notnull + { + return (await patch.ConfigureAwait(false)).ShouldBeValue(); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task ShouldBeNull(this Task> patch) where T : notnull + { + (await Outcomes.Pending(patch, nameof(patch)).ConfigureAwait(false)).ShouldBeNull(); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task ShouldBeNull(this ValueTask> patch) where T : notnull + { + (await patch.ConfigureAwait(false)).ShouldBeNull(); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task ShouldBeAbsent(this Task> patch) where T : notnull + { + (await Outcomes.Pending(patch, nameof(patch)).ConfigureAwait(false)).ShouldBeAbsent(); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task ShouldBeAbsent(this ValueTask> patch) where T : notnull + { + (await patch.ConfigureAwait(false)).ShouldBeAbsent(); + } + private static string Was(PatchState state) => state == PatchState.Value ? "holds a value (not printed)" : $"is {Describe(state)}"; private static string Describe(PatchState state) => state switch diff --git a/src/CodoMetis.TypeKit.Testing/README.md b/src/CodoMetis.TypeKit.Testing/README.md index 46558db9..4b7fb84b 100644 --- a/src/CodoMetis.TypeKit.Testing/README.md +++ b/src/CodoMetis.TypeKit.Testing/README.md @@ -43,6 +43,15 @@ Fault stale = refusal.ShouldHold(EditFault.Stale); // this fault, o string? detail = stale.Detail; // for the test's own assertions ``` +Every assertion also works on a `Task` or a `ValueTask` of what it checks. Put `await` in front, and +it waits for the task, then checks what the task returned: + +```csharp +Email email = await signups.RegisterAsync("ops@example.com").ShouldBeSuccess(); // awaited, then asserted +await signups.RegisterAsync("nobody").ShouldBeError(EmailFault.NoAt); +await signups.FindAsync("ops@example.com").ShouldBeNone(); // a ValueTask too +``` + - **The content comes back.** `ShouldBeSome()`, `ShouldBeSuccess()`, `ShouldBeError()` and `ShouldBeValue()` return the value or the error. Compare it with whatever assertions your tests already use: xUnit's `Assert`, Shouldly, or another. @@ -54,3 +63,8 @@ string? detail = stale.Detail; // for the test' `Option`, `Result` and `Patch` themselves never do. - **An uninitialized result fails every assertion.** The message says it is a default instance, neither a success nor an error. +- **Await an assertion on a task.** It fails your test only through the task it returns, so without + `await` the test passes whatever the task holds. The analyzer reports that line (CMTK0003), in a + test method that isn't `async` too, where C# itself says nothing. +- **A task that throws fails your test with its own exception**, as it was thrown, and not as an + assertion failure. The assertion never sees an outcome. diff --git a/src/CodoMetis.TypeKit.Testing/ResultAssertions.cs b/src/CodoMetis.TypeKit.Testing/ResultAssertions.cs index d8e29452..b46572b3 100644 --- a/src/CodoMetis.TypeKit.Testing/ResultAssertions.cs +++ b/src/CodoMetis.TypeKit.Testing/ResultAssertions.cs @@ -83,4 +83,100 @@ public static TError ShouldBeError(this Result result) where TEr /// The result is a success, another error, or uninitialized. public static void ShouldBeError(this Result result, TError expected) where TError : notnull => Outcomes.ThrowIfOtherError, TError>(result.ShouldBeError(), expected); + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task ShouldBeSuccess(this Task> result) + where T : notnull + where TError : notnull + { + return (await Outcomes.Pending(result, nameof(result)).ConfigureAwait(false)).ShouldBeSuccess(); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task ShouldBeSuccess(this ValueTask> result) + where T : notnull + where TError : notnull + { + return (await result.ConfigureAwait(false)).ShouldBeSuccess(); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task ShouldBeError(this Task> result) + where T : notnull + where TError : notnull + { + return (await Outcomes.Pending(result, nameof(result)).ConfigureAwait(false)).ShouldBeError(); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task ShouldBeError(this ValueTask> result) + where T : notnull + where TError : notnull + { + return (await result.ConfigureAwait(false)).ShouldBeError(); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task ShouldBeError(this Task> result, TError expected) + where T : notnull + where TError : notnull + { + (await Outcomes.Pending(result, nameof(result)).ConfigureAwait(false)).ShouldBeError(expected); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task ShouldBeError(this ValueTask> result, TError expected) + where T : notnull + where TError : notnull + { + (await result.ConfigureAwait(false)).ShouldBeError(expected); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task ShouldBeSuccess(this Task> result) where TError : notnull + { + (await Outcomes.Pending(result, nameof(result)).ConfigureAwait(false)).ShouldBeSuccess(); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task ShouldBeSuccess(this ValueTask> result) where TError : notnull + { + (await result.ConfigureAwait(false)).ShouldBeSuccess(); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task ShouldBeError(this Task> result) where TError : notnull + { + return (await Outcomes.Pending(result, nameof(result)).ConfigureAwait(false)).ShouldBeError(); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task ShouldBeError(this ValueTask> result) where TError : notnull + { + return (await result.ConfigureAwait(false)).ShouldBeError(); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task ShouldBeError(this Task> result, TError expected) where TError : notnull + { + (await Outcomes.Pending(result, nameof(result)).ConfigureAwait(false)).ShouldBeError(expected); + } + + /// + /// Awaits first: an exception the task ends with propagates as it is. + public static async Task ShouldBeError(this ValueTask> result, TError expected) where TError : notnull + { + (await result.ConfigureAwait(false)).ShouldBeError(expected); + } } diff --git a/src/CodoMetis.TypeKit/Fault.cs b/src/CodoMetis.TypeKit/Fault.cs index e2f33a88..f44ca270 100644 --- a/src/CodoMetis.TypeKit/Fault.cs +++ b/src/CodoMetis.TypeKit/Fault.cs @@ -15,7 +15,7 @@ namespace CodoMetis.TypeKit; /// it can say anything, so whether it quotes the request is the application's to decide. /// /// -/// makes one and infers the type, with a detail or without: +/// makes one and infers the type, with a detail or without: /// Result.Error(Fault.Of(OrderFault.NotAmendable, "The order shipped on 3 May.")). Where the reason /// answers for several rules, WithRule names the one that refused, an enum's or a closed set's /// member, as a key a client branches on: Fault.Of(AccountFault.InvalidEmail).WithRule(EmailFault.NoAt). A bare reason @@ -42,6 +42,16 @@ public Fault(TFault reason, string? detail = null) : detail; } + /// Creates the fault, keeping the exception that caused it. + /// Which fault. Never null. + /// A sentence about this occurrence, or for none. Never blank. + /// The exception underneath, kept for the log as . Never null. + /// or is null. + /// is empty or white space: say for no detail. + public Fault(TFault reason, string? detail, Exception cause) + : this(reason, detail) => + Cause = cause ?? throw new ArgumentNullException(nameof(cause), "A fault's cause is the exception underneath; leave it out for none."); + /// Which fault. public TFault Reason { get; } @@ -64,6 +74,27 @@ public Fault(TFault reason, string? detail = null) /// The rule's type, which equality compares beside : two types' rules can read alike. private Type? RuleType { get; init; } + /// The exception that caused this occurrence, or : for the log, never for the client. + /// + /// Set by , kept by WithRule and by a fault of + /// several types. CodoMetis.TypeKit.AspNetCore hands it to the host beside the problem and never writes it into + /// one. Equality leaves it out, so a fault with a cause equals the same fault without one, and the fault prints + /// without it: log it as the exception it is. + /// + public Exception? Cause { get; } + + /// Equal where the reason, the detail and the rule are; the takes no part. + /// The other fault. + /// Whether the two are the same fault. + public bool Equals(Fault other) => + EqualityComparer.Default.Equals(Reason, other.Reason) + && string.Equals(Detail, other.Detail, StringComparison.Ordinal) + && string.Equals(Rule, other.Rule, StringComparison.Ordinal) + && RuleType == other.RuleType; + + /// + public override int GetHashCode() => HashCode.Combine(Reason, Detail, Rule, RuleType); + /// The same fault with , a member of , as its . internal Fault Naming(string rule, Type type) => this with { Rule = rule, RuleType = type }; @@ -197,4 +228,18 @@ public static class Fault /// is null. /// is empty or white space. public static Fault Of(TFault reason, string? detail = null) where TFault : notnull => new(reason, detail); + + /// A fault and the exception that caused it, kept for the log and never written to a response. + /// + /// The pattern is a MapError from a caught exception: + /// Result.Catch(read).MapError(e => Fault.Of(ImportFault.Unreadable, "The file could not be read.", e)). + /// + /// Which fault. Never null. + /// A sentence about this occurrence, or for none. Never blank. + /// The exception underneath. Never null. + /// The reason's type, inferred. + /// The fault, with as its . + /// or is null. + /// is empty or white space. + public static Fault Of(TFault reason, string? detail, Exception cause) where TFault : notnull => new(reason, detail, cause); } diff --git a/src/CodoMetis.TypeKit/FaultOfSets.cs b/src/CodoMetis.TypeKit/FaultOfSets.cs index c62ac641..f48fcf18 100644 --- a/src/CodoMetis.TypeKit/FaultOfSets.cs +++ b/src/CodoMetis.TypeKit/FaultOfSets.cs @@ -54,12 +54,26 @@ namespace CodoMetis.TypeKit; /// The fault. public static Fault Of(T1 reason, string? detail = null) => Fault.Of(reason, detail); + /// A fault of the first type, with the exception that caused it. + /// Which fault. Never null. + /// A sentence about this occurrence, or for none. Never blank. + /// The exception underneath, kept as the fault's Cause. Never null. + /// The fault. + public static Fault Of(T1 reason, string? detail, Exception cause) => Fault.Of(reason, detail, cause); + /// A fault of the second type, with a sentence about this occurrence or none. /// Which fault. Never null. /// A sentence about this occurrence, or for none. Never blank. /// The fault. public static Fault Of(T2 reason, string? detail = null) => Fault.Of(reason, detail); + /// A fault of the second type, with the exception that caused it. + /// Which fault. Never null. + /// A sentence about this occurrence, or for none. Never blank. + /// The exception underneath, kept as the fault's Cause. Never null. + /// The fault. + public static Fault Of(T2 reason, string? detail, Exception cause) => Fault.Of(reason, detail, cause); + /// The fault is, of the first type: what the conversion does, by name. /// The fault. /// The fault. @@ -163,18 +177,39 @@ private static Fault Checked(Fault fault) where T : notnull => /// The fault. public static Fault Of(T1 reason, string? detail = null) => Fault.Of(reason, detail); + /// A fault of the first type, with the exception that caused it. + /// Which fault. Never null. + /// A sentence about this occurrence, or for none. Never blank. + /// The exception underneath, kept as the fault's Cause. Never null. + /// The fault. + public static Fault Of(T1 reason, string? detail, Exception cause) => Fault.Of(reason, detail, cause); + /// A fault of the second type, with a sentence about this occurrence or none. /// Which fault. Never null. /// A sentence about this occurrence, or for none. Never blank. /// The fault. public static Fault Of(T2 reason, string? detail = null) => Fault.Of(reason, detail); + /// A fault of the second type, with the exception that caused it. + /// Which fault. Never null. + /// A sentence about this occurrence, or for none. Never blank. + /// The exception underneath, kept as the fault's Cause. Never null. + /// The fault. + public static Fault Of(T2 reason, string? detail, Exception cause) => Fault.Of(reason, detail, cause); + /// A fault of the third type, with a sentence about this occurrence or none. /// Which fault. Never null. /// A sentence about this occurrence, or for none. Never blank. /// The fault. public static Fault Of(T3 reason, string? detail = null) => Fault.Of(reason, detail); + /// A fault of the third type, with the exception that caused it. + /// Which fault. Never null. + /// A sentence about this occurrence, or for none. Never blank. + /// The exception underneath, kept as the fault's Cause. Never null. + /// The fault. + public static Fault Of(T3 reason, string? detail, Exception cause) => Fault.Of(reason, detail, cause); + /// The fault is, of the first type: what the conversion does, by name. /// The fault. /// The fault. diff --git a/src/CodoMetis.TypeKit/OptionExtensions.cs b/src/CodoMetis.TypeKit/OptionExtensions.cs index 369d57a6..6895c310 100644 --- a/src/CodoMetis.TypeKit/OptionExtensions.cs +++ b/src/CodoMetis.TypeKit/OptionExtensions.cs @@ -651,6 +651,24 @@ public IEnumerable Choose() } } + /// The stream of options. + /// The type of the values. + extension(IAsyncEnumerable> options) where T : notnull + { + /// + /// The values of the options that hold one, in order, as they arrive: a stream of lookups to what + /// was found. Lazy, as an async LINQ operator is, and the token given to WithCancellation or + /// GetAsyncEnumerator reaches the stream it reads. + /// + /// The values, in order. + public IAsyncEnumerable Choose() + { + ArgumentNullException.ThrowIfNull(options); + + return Chosen(options, static option => option); + } + } + /// The nullable value. /// The underlying value type. extension(in T? instance) where T : struct @@ -679,4 +697,13 @@ internal static IEnumerable Chosen(IEnumerable source, F if (selector(element).TryGetValue(out var value)) yield return value; } } + + /// The iterator behind both Choose over a stream; the caller's token is the stream's. + internal static async IAsyncEnumerable Chosen(IAsyncEnumerable source, Func> selector, [EnumeratorCancellation] CancellationToken cancellationToken = default) where TResult : notnull + { + await foreach (var element in source.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + if (selector(element).TryGetValue(out var value)) yield return value; + } + } } diff --git a/src/CodoMetis.TypeKit/README.md b/src/CodoMetis.TypeKit/README.md index 53be9727..6f7adf7a 100644 --- a/src/CodoMetis.TypeKit/README.md +++ b/src/CodoMetis.TypeKit/README.md @@ -2,7 +2,10 @@ The base package of TypeKit. It gives you `Option` and `Result` that you can't misuse, and the interfaces that value objects and closed sets (smart enums) are generated from. It brings no -code generator and no Metalama, only the types and the analyzers that check them. +code generator, only the types and the analyzers that check them. TypeKit's generators are built on +[Metalama](https://postsharp.net/metalama), which generates C# code at compile time from templates +written in C#. They live in `CodoMetis.TypeKit.Generators`, so this package never brings Metalama into +your project. What it does for you: @@ -76,6 +79,7 @@ Option discount = // a later from - **`Choose(selector)`** keeps the values a selector returns `Some` for, in order and without the `None`s. It filters and maps in one step, over elements that may be null (`nicknames.Choose(n => n.ToOption())`). `Choose()` keeps the values a sequence of options holds. + Both read an async stream too, as its elements arrive. On a database query, `FirstOrNone`, `FirstOrError`, `SingleOrNone` and `ElementAtOrNone` ask the provider for only the element they need, and let it run the predicate (a `WHERE` and a `LIMIT` in @@ -84,7 +88,8 @@ no last element until it is ordered. Write the predicate as a lambda at the call, which makes it an expression. A `Func` variable or a method group is no expression. It binds to the sequence's overload, which reads every row and -runs the condition in memory. Hold an `Expression>` instead. +runs the condition in memory, and the analyzer warns at the call (CMTK0009). Hold an +`Expression>` instead. ### Get the value out @@ -95,6 +100,7 @@ runs the condition in memory. Hold an `Expression>` instead. - **`ToResult(error)`** turns absence into an error. - **`Traverse`** validates an optional input when it is present. `dto.Phone.ToOption().Traverse(Phone.Create)` is a `Result, PhoneFault>`. + `TraverseAsync` does the same with a check you await, such as a lookup. - **`Transpose()`** turns an `Option>` inside out, and a `Result>` back. `ToString()` never prints the content, so an option is safe to log. The debugger shows it. @@ -221,6 +227,20 @@ sequence order. `Traverse` calls nothing after it. None of them collects errors. cancellation token you give it is cancelled. For operations that may all run at once, use `Task.WhenAll` and `Sequence()`. +An async stream (`IAsyncEnumerable`), such as a paged API or a query read with +`AsAsyncEnumerable()`, needs no list first: + +```csharp +IAsyncEnumerable known = lines.Choose(line => Sku.TryFrom(line)); // the lines that are SKUs, as they arrive +Result, SkuFault> imported = await rows.SequenceAsync(cancellationToken); // every SKU, or the first fault +Result, OrderFault> found = await ids.TraverseAsync(orders.FindAsync); // one lookup at a time, in order +``` + +`SequenceAsync` and `TraverseAsync` stop at the first error and read nothing after it. All three read +the stream in order, and dispose its enumerator however they stop: at an error, at a `break`, or when +your selector throws. The token you give `SequenceAsync` or `TraverseAsync` reaches the stream. For +`Choose`, pass yours to `WithCancellation` on what it returns. + The asynchronous steps take callbacks that return a `Task`, and wait for them. `.TapErrorAsync(fault => audit.RecordAsync(fault))` has recorded the fault when the chain's `await` returns, and what it throws comes out of that `await`. @@ -271,14 +291,16 @@ Result fetched = await Result.CatchAsync(() => client.GetByte - **A task goes to `CatchAsync`.** A delegate that returns a `Task`, an `async` lambda among them, given to `Catch` or its command form doesn't compile, and the error names `CatchAsync`. The task's exception would be thrown after `Catch` had returned. -- **A `ValueTask`, or a task spelled as the type argument** (`Catch, ...>`), is rejected - when called, before the delegate runs. `CatchAsync` takes a `ValueTask` through `.AsTask()` or an - `async` lambda, as above. +- **`CatchAsync` takes a `ValueTask` too**, as in `Result.CatchAsync(() => stream.ReadAsync(buffer))`. + An `async` lambda still goes to the `Task` form. +- **A `ValueTask` given to `Catch`, or a task spelled as the type argument** (`Catch, ...>`), + is rejected when called, before the delegate runs. Use `CatchAsync`. - **A delegate that returns null throws**, as `Result.Success(null)` does. - **The other members don't catch.** `Map`, `Bind` and the rest are unchanged: a selector that throws propagates. - **An exception is for the code that called, not for a client.** `ToHttpResult()` doesn't accept a `Result`. Use `MapError` to turn the exception into a fault that it does accept. + `MapError(e => Fault.Of(reason, detail, e))` keeps the exception as the fault's cause, for your log. ### A fault with a sentence @@ -288,6 +310,9 @@ beside it, in a `Fault`: the fault as its `Reason`, and an optional `Det - **`Fault.Of(OrderFault.NotAmendable, "The order shipped on 3 May.")`** infers the type. `Fault.Of(OrderFault.NotFound)` makes one without a detail. A blank detail throws `ArgumentException`. Leave the detail out instead, and it is `null`. +- **`Fault.Of(reason, detail, exception)` keeps what failed underneath** as the fault's `Cause`. + It's for your log. A fault with a cause equals the same fault without one, and prints the same. + CodoMetis.TypeKit.AspNetCore hands it to your host and never writes it into a response. - **A bare fault doesn't convert to one**, so comparing the two doesn't compile. Write `fault.Reason == OrderFault.NotFound`. - **`WithRule` names the rule that failed**, where the reason covers several rules. The rule is an @@ -582,7 +607,8 @@ A value object inside a patch keeps its own rule. `Patch` given a value t is rejected as anywhere else, without quoting the input. Where the rejection should be the endpoint's own fault, the request carries a `Patch`, and `Traverse` validates it: `request.Email.Traverse(Email.Create)` is a `Result, EmailFault>`, with a clear and an -absence passed through. `Map` changes a value the same way. +absence passed through. `TraverseAsync` does the same with a check you await, and `Map` changes a +value the same way. **Good to know** @@ -830,13 +856,13 @@ In a field initializer, refer to a member by its field, `Open`, not by its name. Nothing has run: the delegate is rejected before it is called. **Why:** the task's exception is thrown after `Catch` has returned, so the result would be a success -whatever the call does. A delegate that returns a `Task` is rejected at compile time. A `ValueTask` -can't be: it converts to no task type that an overload could name without making every `async` -lambda ambiguous. And a spelled type argument chooses its overload itself. +whatever the call does. A delegate that returns a `Task` is rejected at compile time. One that +returns a `ValueTask` is rejected when called, since a compile error there would break a build that +works today. And a spelled type argument chooses its overload itself. -**Instead:** use `CatchAsync`, which awaits the task: -`await Result.CatchAsync(() => stream.ReadAsync(buffer).AsTask())`, or an `async` lambda. In -`CatchAsync`, await the inner task in the delegate. +**Instead:** use `CatchAsync`, which awaits the task, a `ValueTask` too: +`await Result.CatchAsync(() => stream.ReadAsync(buffer))`. In `CatchAsync`, await the inner task in +the delegate. ### AsText in globalization-invariant mode diff --git a/src/CodoMetis.TypeKit/ResultExtensions.cs b/src/CodoMetis.TypeKit/ResultExtensions.cs index 5e7b56b6..eb0a0978 100644 --- a/src/CodoMetis.TypeKit/ResultExtensions.cs +++ b/src/CodoMetis.TypeKit/ResultExtensions.cs @@ -1,3 +1,5 @@ +using System.Runtime.CompilerServices; + namespace CodoMetis.TypeKit; /// @@ -283,8 +285,8 @@ private static bool IsTimeout(OperationCanceledException cancellation) /// Refuses a value that is itself a task, before the delegate runs: its exception would be thrown /// after Catch had returned a success. The overloads over a task-returning delegate refuse /// it at compile time; this is the type argument spelled as a task, Catch<Task<int>, …>, - /// and a ValueTask, which no overload can name beside Task without making every - /// async lambda ambiguous. + /// and a ValueTask, whose refusal at compile time would break a build that works today + /// (decision 110). /// private static void RefuseTask(string method) { @@ -306,8 +308,7 @@ private static void RefuseTask(string method) /// cancellation, and so does an of cancellations alone; a timeout, /// which throws as a cancellation over a /// , is the error. A delegate returning a ValueTask - /// needs .AsTask() or an async lambda: a ValueTask overload beside this one is - /// ambiguous for every async lambda. + /// binds to , and an async lambda to this one. /// See for cancellations, timeouts and null. /// /// @@ -468,6 +469,164 @@ public static async Task> CatchAsync(Func a return Result.Success(); } + /// + /// Awaits , a call that returns a , and + /// returns its value as a success, or the exception it threw as the error, whether before or after + /// its first await: Result.CatchAsync(() => stream.ReadAsync(buffer)). + /// See . + /// + /// + /// An async lambda converts to either delegate, so this overload has the lower priority and such + /// a lambda binds to the form, as it did before this one existed. A lambda or + /// a method group that returns a ValueTask converts to this one alone. + /// + /// The asynchronous call whose outcome is wanted. + /// The type of the value. + /// A success holding the value, or an error holding the exception thrown. + /// is null, or its task produced null. + /// is itself a task; await it in the delegate. + [OverloadResolutionPriority(-1)] + public static async Task> CatchAsync(Func> action) where T : notnull + { + ArgumentNullException.ThrowIfNull(action); + RefuseTask(nameof(CatchAsync)); + + ValueTask task; + try + { + task = action(); + } + catch (Exception exception) when (!IsCancellation(exception)) + { + return Result.Error(exception); + } + + T value; + try + { + value = await task.ConfigureAwait(false); + } + catch (Exception exception) when (!IsCancellation(exception)) + { + return Result.Error(exception); + } + + return Result.Success(value); + } + + /// + /// Awaits , a call that returns a , and returns a + /// success, or the exception it threw as the error. See . + /// + /// The asynchronous call whose outcome is wanted. + /// A success, or an error holding the exception thrown. + /// is null. + [OverloadResolutionPriority(-1)] + public static async Task> CatchAsync(Func action) + { + ArgumentNullException.ThrowIfNull(action); + + ValueTask task; + try + { + task = action(); + } + catch (Exception exception) when (!IsCancellation(exception)) + { + return Result.Error(exception); + } + + try + { + await task.ConfigureAwait(false); + } + catch (Exception exception) when (!IsCancellation(exception)) + { + return Result.Error(exception); + } + + return Result.Success(); + } + + /// + /// Awaits , a call that returns a , and + /// returns its value as a success, or the it threw as the error. Any + /// other exception propagates. See . + /// + /// The asynchronous call whose outcome is wanted. + /// The type of the value. + /// The exception that is an outcome; any other propagates. + /// A success holding the value, or an error holding the exception thrown. + /// is null, or its task produced null. + /// is itself a task; await it in the delegate. + [OverloadResolutionPriority(-1)] + public static async Task> CatchAsync(Func> action) + where T : notnull + where TException : Exception + { + ArgumentNullException.ThrowIfNull(action); + RefuseTask(nameof(CatchAsync)); + + ValueTask task; + try + { + task = action(); + } + catch (TException exception) when (!IsCancellation(exception)) + { + return Result.Error(exception); + } + + T value; + try + { + value = await task.ConfigureAwait(false); + } + catch (TException exception) when (!IsCancellation(exception)) + { + return Result.Error(exception); + } + + return Result.Success(value); + } + + /// + /// Awaits , a call that returns a , and returns a + /// success, or the it threw as the error. Any other exception + /// propagates. See . + /// + /// The asynchronous call whose outcome is wanted. + /// The exception that is an outcome; any other propagates. + /// A success, or an error holding the exception thrown. + /// is null. + [OverloadResolutionPriority(-1)] + public static async Task> CatchAsync(Func action) + where TException : Exception + { + ArgumentNullException.ThrowIfNull(action); + + ValueTask task; + try + { + task = action(); + } + catch (TException exception) when (!IsCancellation(exception)) + { + return Result.Error(exception); + } + + try + { + await task.ConfigureAwait(false); + } + catch (TException exception) when (!IsCancellation(exception)) + { + return Result.Error(exception); + } + + return Result.Success(); + } + /// The result. /// The type of the value. /// The type of the error. @@ -1226,6 +1385,138 @@ public async Task> TraverseAsync(FuncThe stream. + /// The type of the elements. + extension(IAsyncEnumerable source) + { + /// + /// Applies an asynchronous operation that may fail to every element of a stream, one at a time + /// and in order, stopping at the first error: the next element is not read, nor its operation + /// started, after one failed, and the stream's enumerator is disposed. + /// + /// Called with each element and awaited, until one fails. + /// The type of each produced value. + /// The type of the error. + /// A success with every produced value, in order, or the first error. + /// returned an uninitialized result. + /// returned no task. + public async Task, TError>> TraverseAsync(Func>> selector) + where TResult : notnull + where TError : notnull + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(selector); + + var values = new List(); + + await foreach (var element in source.ConfigureAwait(false)) + { + var result = await ThrowHelper.Awaitable(selector(element), nameof(TraverseAsync), nameof(selector)).ConfigureAwait(false); + if (!result.TryGetValue(out var value, out var error)) return Result, TError>.Error(error); + + values.Add(value); + } + + return Result, TError>.Success(values); + } + + /// + /// Applies an asynchronous operation that may fail to every element of a stream, one at a time + /// and in order, stopping at the first error, with a cancellation token handed to the stream and + /// to each operation, and checked before each element is read and before each operation: once + /// cancelled, nothing more is read or started and the + /// propagates, since a cancellation is not an error. + /// + /// Called with each element and the token, and awaited, until one fails. + /// Handed to the stream's enumerator and to each operation, and checked before each. + /// The type of each produced value. + /// The type of the error. + /// A success with every produced value, in order, or the first error. + /// returned an uninitialized result. + /// returned no task. + /// was cancelled before an element was read or an operation started. + public async Task, TError>> TraverseAsync(Func>> selector, CancellationToken cancellationToken) + where TResult : notnull + where TError : notnull + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(selector); + + var values = new List(); + + cancellationToken.ThrowIfCancellationRequested(); + await foreach (var element in source.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + cancellationToken.ThrowIfCancellationRequested(); + + var result = await ThrowHelper.Awaitable(selector(element, cancellationToken), nameof(TraverseAsync), nameof(selector)).ConfigureAwait(false); + if (!result.TryGetValue(out var value, out var error)) return Result, TError>.Error(error); + + values.Add(value); + cancellationToken.ThrowIfCancellationRequested(); + } + + return Result, TError>.Success(values); + } + + /// + /// Applies an asynchronous command that may fail to every element of a stream, one at a time and + /// in order, stopping at the first error: the next element is not read, nor its command started, + /// after one failed, and the stream's enumerator is disposed. + /// + /// Called with each element and awaited, until one fails. + /// The type of the error. + /// A success if every command succeeded, or the first error. + /// returned an uninitialized result. + /// returned no task. + public async Task> TraverseAsync(Func>> selector) where TError : notnull + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(selector); + + await foreach (var element in source.ConfigureAwait(false)) + { + var result = await ThrowHelper.Awaitable(selector(element), nameof(TraverseAsync), nameof(selector)).ConfigureAwait(false); + if (result.TryGetError(out var error)) return Result.Error(error); + } + + return Result.Success(); + } + + /// + /// Applies an asynchronous command that may fail to every element of a stream, one at a time and + /// in order, stopping at the first error, with a cancellation token handed to the stream and to + /// each command, and checked before each element is read and before each command: once + /// cancelled, nothing more is read or started and the + /// propagates, since a cancellation is not an error. + /// + /// Called with each element and the token, and awaited, until one fails. + /// Handed to the stream's enumerator and to each command, and checked before each. + /// The type of the error. + /// A success if every command succeeded, or the first error. + /// returned an uninitialized result. + /// returned no task. + /// was cancelled before an element was read or a command started. + public async Task> TraverseAsync(Func>> selector, CancellationToken cancellationToken) where TError : notnull + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(selector); + + cancellationToken.ThrowIfCancellationRequested(); + await foreach (var element in source.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + cancellationToken.ThrowIfCancellationRequested(); + + var result = await ThrowHelper.Awaitable(selector(element, cancellationToken), nameof(TraverseAsync), nameof(selector)).ConfigureAwait(false); + if (result.TryGetError(out var error)) return Result.Error(error); + + cancellationToken.ThrowIfCancellationRequested(); + } + + return Result.Success(); + } + } + /// The results. /// The type of the values. /// The type of the error. @@ -1276,6 +1567,66 @@ public Result Sequence() } } + /// The stream of results. + /// The type of the values. + /// The type of the error. + extension(IAsyncEnumerable> results) where T : notnull where TError : notnull + { + /// Every value of a stream, if every result in it is a success. + /// Handed to the stream's enumerator, and checked before each result is read. + /// + /// A success with every value, in order, or the first error. After an error nothing more is read + /// and the stream's enumerator is disposed. + /// + /// A result up to the first error is uninitialized. + /// was cancelled before a result was read. + public async Task, TError>> SequenceAsync(CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(results); + + var values = new List(); + + cancellationToken.ThrowIfCancellationRequested(); + await foreach (var result in results.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + if (!result.TryGetValue(out var value, out var error)) return Result, TError>.Error(error); + + values.Add(value); + cancellationToken.ThrowIfCancellationRequested(); + } + + return Result, TError>.Success(values); + } + } + + /// The stream of results. + /// The type of the error. + extension(IAsyncEnumerable> results) where TError : notnull + { + /// A success if every result of a stream is one. + /// Handed to the stream's enumerator, and checked before each result is read. + /// + /// A success, or the first error. After an error nothing more is read and the stream's enumerator + /// is disposed. + /// + /// A result up to the first error is uninitialized. + /// was cancelled before a result was read. + public async Task> SequenceAsync(CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(results); + + cancellationToken.ThrowIfCancellationRequested(); + await foreach (var result in results.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + if (result.TryGetError(out var error)) return Result.Error(error); + + cancellationToken.ThrowIfCancellationRequested(); + } + + return Result.Success(); + } + } + /// An optional input that is validated when present. /// The type of the value. /// The type of the error. @@ -1337,6 +1688,57 @@ public Result, TError> Traverse(Func + /// Applies an asynchronous operation that may fail to the value, if there is one: an optional + /// input checked against a store when present, await request.Coupon.TraverseAsync(coupons.FindAsync). + /// For None the selector is not called. + /// + /// Called with the value if there is one, and awaited. + /// The type of the produced value. + /// The type of the error. + /// A success with no value for None, a success with the produced value, or its error. + /// returned an uninitialized result. + /// returned no task. + public async Task, TError>> TraverseAsync(Func>> selector) + where TResult : notnull + where TError : notnull + { + ArgumentNullException.ThrowIfNull(selector); + + if (!option.TryGetValue(out var value)) return Result, TError>.Success(Option.None()); + + var result = await ThrowHelper.Awaitable(selector(value), nameof(TraverseAsync), nameof(selector)).ConfigureAwait(false); + return result.Map(static produced => Option.Some(produced)); + } + + /// + /// Applies an asynchronous operation that may fail to the value, if there is one, with a + /// cancellation token handed to the operation and checked before it: once cancelled, the + /// operation is not started and the propagates, since a + /// cancellation is not an error. For None the selector is not called and the token not checked. + /// + /// Called with the value and the token if there is a value, and awaited. + /// Checked before the operation, and handed to it. + /// The type of the produced value. + /// The type of the error. + /// A success with no value for None, a success with the produced value, or its error. + /// returned an uninitialized result. + /// returned no task. + /// was cancelled before the operation started. + public async Task, TError>> TraverseAsync(Func>> selector, CancellationToken cancellationToken) + where TResult : notnull + where TError : notnull + { + ArgumentNullException.ThrowIfNull(selector); + + if (!option.TryGetValue(out var value)) return Result, TError>.Success(Option.None()); + + cancellationToken.ThrowIfCancellationRequested(); + + var result = await ThrowHelper.Awaitable(selector(value, cancellationToken), nameof(TraverseAsync), nameof(selector)).ConfigureAwait(false); + return result.Map(static produced => Option.Some(produced)); + } } /// A field of a merge patch, validated when it carries a value. @@ -1365,6 +1767,61 @@ public Result, TError> Traverse(Func Patch.Value(produced)); } + + /// + /// Applies an asynchronous operation that may fail to the value, if the field carried one: a + /// patch over what the request sent is checked against a store into a patch over the domain's + /// type, await request.Manager.TraverseAsync(people.FindAsync). A clear or an absent field + /// passes through as it is, and the selector is not called. + /// + /// Called with the value when the field carried one, and awaited. + /// The type of the produced value. + /// The type of the error. + /// A success holding a patch in the same state, with the produced value when the field carried one, or the error. + /// returned an uninitialized result. + /// returned no task. + public async Task, TError>> TraverseAsync(Func>> selector) + where TResult : notnull + where TError : notnull + { + ArgumentNullException.ThrowIfNull(selector); + + if (!patch.TryGetValue(out var value)) + return Result, TError>.Success(patch.State == PatchState.Null ? Patch.Null() : Patch.Absent()); + + var result = await ThrowHelper.Awaitable(selector(value), nameof(TraverseAsync), nameof(selector)).ConfigureAwait(false); + return result.Map(static produced => Patch.Value(produced)); + } + + /// + /// Applies an asynchronous operation that may fail to the value, if the field carried one, with a + /// cancellation token handed to the operation and checked before it: once cancelled, the + /// operation is not started and the propagates, since a + /// cancellation is not an error. A clear or an absent field passes through as it is, and neither the + /// selector is called nor the token checked. + /// + /// Called with the value and the token when the field carried one, and awaited. + /// Checked before the operation, and handed to it. + /// The type of the produced value. + /// The type of the error. + /// A success holding a patch in the same state, with the produced value when the field carried one, or the error. + /// returned an uninitialized result. + /// returned no task. + /// was cancelled before the operation started. + public async Task, TError>> TraverseAsync(Func>> selector, CancellationToken cancellationToken) + where TResult : notnull + where TError : notnull + { + ArgumentNullException.ThrowIfNull(selector); + + if (!patch.TryGetValue(out var value)) + return Result, TError>.Success(patch.State == PatchState.Null ? Patch.Null() : Patch.Absent()); + + cancellationToken.ThrowIfCancellationRequested(); + + var result = await ThrowHelper.Awaitable(selector(value, cancellationToken), nameof(TraverseAsync), nameof(selector)).ConfigureAwait(false); + return result.Map(static produced => Patch.Value(produced)); + } } /// diff --git a/src/CodoMetis.TypeKit/SequenceOptionExtensions.cs b/src/CodoMetis.TypeKit/SequenceOptionExtensions.cs index d74e9935..7f10c7dd 100644 --- a/src/CodoMetis.TypeKit/SequenceOptionExtensions.cs +++ b/src/CodoMetis.TypeKit/SequenceOptionExtensions.cs @@ -1,7 +1,7 @@ namespace CodoMetis.TypeKit; /// -/// Choose(selector) over a sequence whose elements may be null: only what the selector finds +/// Choose(selector) over a sequence or a stream whose elements may be null: only what the selector finds /// must be non-null, so a sequence of string? or int? is chosen from without a warning, /// its nulls the selector's to answer. The other sequence members, Choose() over options among /// them, are on . @@ -35,4 +35,26 @@ public IEnumerable Choose(Func> selector) w return Option.Chosen(source, selector); } } + + /// The stream, whose elements may be null. + /// The type of the elements, nullable or not. + extension(IAsyncEnumerable source) + { + /// + /// The values finds in a stream, as they arrive: for each element it + /// answers with Some, that value, in order, and nothing for a None. Lazy, as an async + /// LINQ operator is, and the token given to WithCancellation or GetAsyncEnumerator + /// reaches the stream it reads. + /// + /// Called with each element as the stream is read. + /// The type of the values. + /// The values found, in order. + public IAsyncEnumerable Choose(Func> selector) where TResult : notnull + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(selector); + + return Option.Chosen(source, selector); + } + } } diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 0ba5aac4..ed2ddcbe 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -17,4 +17,63 @@ + + + + + $([MSBuild]::GetDirectoryNameOfFileAbove('$(MSBuildThisFileDirectory)', 'CodoMetis.TypeKit.slnx'))/CHANGELOG.md + + + + + + <_TypeKitChangelogText>$([System.Text.RegularExpressions.Regex]::Replace($([System.IO.File]::ReadAllText('$(ReleaseNotesChangelog)')), '\r', '')) + <_TypeKitHeading>^## $([System.Text.RegularExpressions.Regex]::Escape('$(Version)'))(?: [^\n]*)?\n + <_TypeKitHasSection>$([System.Text.RegularExpressions.Regex]::IsMatch('$(_TypeKitChangelogText)', '(?m)$(_TypeKitHeading)')) + + + + + + <_TypeKitSection>$([System.Text.RegularExpressions.Regex]::Replace('$(_TypeKitChangelogText)', '(?sm)\A.*?$(_TypeKitHeading)(.*?)(?=^## |\z).*\z', '$1').Trim()) + <_TypeKitRelativeLink>$([System.Text.RegularExpressions.Regex]::IsMatch('$(_TypeKitSection)', '\]\((?!https?://)')) + + + + + + + <_TypeKitNotes>$([System.Text.RegularExpressions.Regex]::Replace('$(_TypeKitSection)', '\[([^\]\n]+)\]\(([^)\s]+)\)', '$1 ($2)')) + <_TypeKitNotes>$([System.Text.RegularExpressions.Regex]::Replace('$(_TypeKitNotes)', '(?m)^#{3,6} +', '')) + <_TypeKitNotes>$(_TypeKitNotes.Replace('`', '').Replace('**', '')) + $(_TypeKitNotes)%0A%0AFull changelog: $(PackageProjectUrl)/blob/main/CHANGELOG.md + <_TypeKitNotesLength>$([MSBuild]::Unescape('$(PackageReleaseNotes)').Length) + + + + diff --git a/test/CodoMetis.TypeKit.Analyzers.Tests/AnalyzerTest.cs b/test/CodoMetis.TypeKit.Analyzers.Tests/AnalyzerTest.cs index 712ba855..061fe502 100644 --- a/test/CodoMetis.TypeKit.Analyzers.Tests/AnalyzerTest.cs +++ b/test/CodoMetis.TypeKit.Analyzers.Tests/AnalyzerTest.cs @@ -57,6 +57,13 @@ internal static class RealEntityFrameworkCore ]; } +/// The real test assertions, whose assertions on a pending value CMTK0003 knows by the assembly's identity. +internal static class RealTesting +{ + public static MetadataReference Reference { get; } = + MetadataReference.CreateFromFile(typeof(global::CodoMetis.TypeKit.Testing.TypeKitAssertionException).Assembly.Location); +} + internal static class RealTypeKit { public static Assembly Assembly { get; } = typeof(Option<>).Assembly; diff --git a/test/CodoMetis.TypeKit.Analyzers.Tests/CodoMetis.TypeKit.Analyzers.Tests.csproj b/test/CodoMetis.TypeKit.Analyzers.Tests/CodoMetis.TypeKit.Analyzers.Tests.csproj index c6103ac4..a6513c17 100644 --- a/test/CodoMetis.TypeKit.Analyzers.Tests/CodoMetis.TypeKit.Analyzers.Tests.csproj +++ b/test/CodoMetis.TypeKit.Analyzers.Tests/CodoMetis.TypeKit.Analyzers.Tests.csproj @@ -12,6 +12,7 @@ + diff --git a/test/CodoMetis.TypeKit.Analyzers.Tests/DefaultProducingCallAnalyzerTests.cs b/test/CodoMetis.TypeKit.Analyzers.Tests/DefaultProducingCallAnalyzerTests.cs index 8189a2ba..8158a56b 100644 --- a/test/CodoMetis.TypeKit.Analyzers.Tests/DefaultProducingCallAnalyzerTests.cs +++ b/test/CodoMetis.TypeKit.Analyzers.Tests/DefaultProducingCallAnalyzerTests.cs @@ -80,15 +80,17 @@ private static AnalyzerTest TestWithEntityFramewor private static DiagnosticResult Cmtk0009(int location, string method, string type, string circumstance, string advice) => new DiagnosticResult("CMTK0009", DiagnosticSeverity.Warning).WithLocation(location).WithArguments(method, type, circumstance, advice); + /// Both descriptors, the default instance's and the query condition's, are the one shipped rule. [Fact] public void The_descriptor_is_the_published_contract() { - var rule = new DefaultProducingCallAnalyzer().SupportedDiagnostics.ShouldHaveSingleItem(); + var rules = new DefaultProducingCallAnalyzer().SupportedDiagnostics; - rule.Id.ShouldBe("CMTK0009"); - rule.DefaultSeverity.ShouldBe(DiagnosticSeverity.Warning); - rule.Category.ShouldBe("Usage"); - rule.IsEnabledByDefault.ShouldBeTrue(); + rules.Length.ShouldBe(2); + rules.ShouldAllBe(rule => rule.Id == "CMTK0009" + && rule.DefaultSeverity == DiagnosticSeverity.Warning + && rule.Category == "Usage" + && rule.IsEnabledByDefault); } [Fact] diff --git a/test/CodoMetis.TypeKit.Analyzers.Tests/DroppedTaskVariantTests.cs b/test/CodoMetis.TypeKit.Analyzers.Tests/DroppedTaskVariantTests.cs new file mode 100644 index 00000000..4f3544e4 --- /dev/null +++ b/test/CodoMetis.TypeKit.Analyzers.Tests/DroppedTaskVariantTests.cs @@ -0,0 +1,70 @@ +using CodoMetis.TypeKit.Analyzers.Tests.Variants; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; + +namespace CodoMetis.TypeKit.Analyzers.Tests; + +/// +/// CMTK0003 over : a call's task dropped in every form and through every +/// pass-through is reported, once per call that made it, and kept in any other use is silent. +/// +/// +/// The oracle is the rule's own statement, which each shape holds to: the statement's own call is reported +/// whole, a branch's call at the branch, a combination of assertions at the combinator, and an awaited task +/// only where what it completes with is a result, or a WhenAny that leaves every failure unobserved. +/// An assertion of CodoMetis.TypeKit.Testing and a call that returns a task of a Result run through +/// the same shapes, so the two forms of the rule agree on each (#152). +/// +public sealed class DroppedTaskVariantTests +{ + public static TheoryData Cases() + { + var cases = new TheoryData(); + foreach (var call in DroppedTaskShapes.Calls) + foreach (var form in DroppedTaskShapes.Forms.Where(form => DroppedTaskShapes.Applies(form, call))) + cases.Add(call.Name, form.Name); + + return cases; + } + + [Theory] + [MemberData(nameof(Cases))] + public Task A_dropped_task_is_reported_in_every_form_and_a_kept_one_never(string name, string shape) + { + var call = DroppedTaskShapes.Calls.Single(candidate => candidate.Name == name); + var form = DroppedTaskShapes.Forms.Single(candidate => candidate.Name == shape); + var (source, expected) = DroppedTaskShapes.Use(call, form); + + var test = new AnalyzerTest(source); + test.TestState.Sources.Add(("Declarations.cs", DroppedTaskShapes.Declarations)); + test.TestState.AdditionalReferences.Add(RealTesting.Reference); + test.ExpectedDiagnostics.AddRange( + expected.Select((message, location) => + new DiagnosticResult("CMTK0003", DiagnosticSeverity.Error).WithLocation(location).WithArguments(message.Outcome, message.Method))); + + return test.RunAsync(TestContext.Current.CancellationToken); + } + + /// + /// Every axis of the catalogue reaches the sweep, and each call has a form that reports and one that keeps: + /// a shape that compiles to no case would pass as silent. + /// + [Fact] + public void Every_shape_makes_a_case() + { + foreach (var call in DroppedTaskShapes.Calls) + { + var uses = DroppedTaskShapes.Forms.Where(form => DroppedTaskShapes.Applies(form, call)).Select(form => DroppedTaskShapes.Use(call, form)).ToList(); + var source = string.Concat(uses.Select(use => use.Source)); + + foreach (var passThrough in DroppedTaskShapes.PassThroughs.Where(passThrough => passThrough.Suffix.Length > 0)) + source.ShouldContain(passThrough.Suffix, customMessage: $"{call}: no case passes through {passThrough}."); + + foreach (var use in DroppedTaskShapes.Uses) + source.ShouldContain($", {use.Name}\n", customMessage: $"{call}: no case is {use}."); + + uses.ShouldContain(use => use.Expected.Count > 0, $"{call}: nothing is expected to be reported."); + uses.Where(use => use.Source.Contains("{|#", StringComparison.Ordinal)).Count().ShouldBeLessThan(uses.Count, $"{call}: every form reports, so no form keeps the task."); + } + } +} diff --git a/test/CodoMetis.TypeKit.Analyzers.Tests/IgnoredOutcomeAnalyzerTests.cs b/test/CodoMetis.TypeKit.Analyzers.Tests/IgnoredOutcomeAnalyzerTests.cs index a6961367..ca71528d 100644 --- a/test/CodoMetis.TypeKit.Analyzers.Tests/IgnoredOutcomeAnalyzerTests.cs +++ b/test/CodoMetis.TypeKit.Analyzers.Tests/IgnoredOutcomeAnalyzerTests.cs @@ -94,6 +94,166 @@ private static AnalyzerTest TestWithFile(string file, pa return test; } + /// The subjects, with the real test assertions referenced and imported. + private static AnalyzerTest TestWithAssertions(string code, params DiagnosticResult[] expected) + { + var test = Test("", expected); + test.TestState.AdditionalReferences.Add(RealTesting.Reference); + test.TestState.Sources.Add(("Caller.cs", "using System;\nusing System.Threading.Tasks;\nusing CodoMetis.TypeKit;\nusing CodoMetis.TypeKit.Testing;\n\n" + code)); + return test; + } + + /// + /// An assertion on a pending value fails the test only through the task it returns: dropped + /// unawaited, in a test method that is not async (where C# warns of nothing) or in one that is, + /// it passes whatever the value is. + /// + [Fact] + public Task A_dropped_assertion_on_a_pending_value_reports() => + TestWithAssertions( + """ + public static class Caller + { + public static void Run(Service service) + { + {|#0:service.LoadAsync().ShouldBeSuccess()|}; + {|#1:service.FindAsync().ShouldBeNone()|}; + {|#2:service.SaveAsync().ShouldBeError(Fault.Refused)|}; + {|#3:service.LoadAsync().ShouldBeSuccess().ConfigureAwait(false)|}; + Action check = () => {|#4:service.FindAsync().ShouldBeSome()|}; + } + + public static async Task RunAsync(Service service) + { + #pragma warning disable CS4014 + {|#5:service.LoadAsync().ShouldBeError()|}; + #pragma warning restore CS4014 + await Task.Yield(); + } + } + """, + new DiagnosticResult("CMTK0003", DiagnosticSeverity.Error) + .WithLocation(0) + .WithMessage("The Task that 'ShouldBeSuccess' returns is ignored, so its outcome is never seen. Handle it, or discard it with '_ =' if nothing depends on it."), + Cmtk0003(1, "Task", "ShouldBeNone"), + Cmtk0003(2, "Task", "ShouldBeError"), + Cmtk0003(3, "Task", "ShouldBeSuccess"), + Cmtk0003(4, "Task", "ShouldBeSome"), + Cmtk0003(5, "Task", "ShouldBeError")) + .RunAsync(TestContext.Current.CancellationToken); + + [Fact] + public Task An_awaited_held_or_discarded_assertion_is_silent() => + TestWithAssertions( + """ + public static class Caller + { + public static async Task RunAsync(Service service) + { + await service.LoadAsync().ShouldBeSuccess(); + await service.FindAsync().ShouldBeNone().ConfigureAwait(false); + var pending = service.LoadAsync().ShouldBeSuccess(); + await pending; + _ = service.SaveAsync().ShouldBeSuccess(); + service.Load().ShouldBeSuccess(); + } + + public static Task Returned(Service service) => service.FindAsync().ShouldBeNone(); + + public static Func Deferred(Service service) => () => service.FindAsync().ShouldBeNone(); + } + """) + .RunAsync(TestContext.Current.CancellationToken); + + /// + /// Only the package's own assertions are reported: the same shape from any other assembly, a + /// consumer's own extension on a pending result among them, built before 2.3 and builds still. + /// + [Fact] + public Task A_task_of_another_assembly_s_extension_on_a_pending_value_is_silent() => + TestWithAssertions( + """ + public static class Logging + { + public static Task LogAsync(this Task> pending) where T : notnull => pending; + + public static Task ShouldBeSuccess(this Task> pending) where T : notnull => pending; + } + + public static class Caller + { + public static void Run(Service service) + { + service.LoadAsync().LogAsync(); + Logging.ShouldBeSuccess(service.FindAsync()); + } + } + """) + .RunAsync(TestContext.Current.CancellationToken); + + /// + /// A Task.WhenAny inside a Task.WhenAll or a Task.Run completes with the faulted assertion + /// as its value, so awaiting the outer task sees no failure (#165). Awaiting what the WhenAny completes + /// with sees it. + /// + [Fact] + public Task An_assertion_a_nested_WhenAny_completes_with_reports_and_awaited_twice_is_silent() => + TestWithAssertions( + """ + public static class Caller + { + public static async Task Check(Task> first, Task> second) + { + {|#0:await Task.WhenAll(Task.WhenAny(first.ShouldBeNone(), second.ShouldBeNone()))|}; + {|#1:await Task.Run(() => Task.WhenAny(first.ShouldBeNone(), second.ShouldBeNone()))|}; + {|#2:Task.WhenAll(Task.WhenAny(first.ShouldBeNone(), second.ShouldBeNone())).Wait()|}; + Func check = () => {|#3:Task.WhenAll(Task.WhenAny(first.ShouldBeNone(), second.ShouldBeNone()))|}; + await await Task.WhenAny(first.ShouldBeNone(), second.ShouldBeNone()); + await Task.WhenAll(Task.WhenAll(first.ShouldBeNone(), second.ShouldBeNone())); + } + } + """, + Cmtk0003(0, "Task", "WhenAll"), + Cmtk0003(1, "Task", "Run"), + Cmtk0003(2, "Task", "WhenAll"), + Cmtk0003(3, "Task", "WhenAll")) + .RunAsync(TestContext.Current.CancellationToken); + + /// + /// A catch around an awaited assertion in an async lambda keeps its failure only when it can: a + /// catch that throws again, one of a type that cannot hold a TypeKitAssertionException, a look-alike + /// of that name among them, or one whose filter is a constant false hands it on to the dropped task (#166). + /// + [Fact] + public Task An_assertion_a_catch_cannot_keep_reports_and_one_it_keeps_is_silent() => + TestWithAssertions( + """ + using System.Linq; + + public static class Lookalike + { + public sealed class TypeKitAssertionException : Exception { } + } + + public static class Caller + { + public static void Check(Task> pending) + { + {|#0:Task.WhenAll(new[] { pending }.Select(async item => { try { await item.ShouldBeNone(); } catch { throw; } }))|}; + {|#1:Task.WhenAll(new[] { pending }.Select(async item => { try { await item.ShouldBeNone(); } catch (OperationCanceledException) { } }))|}; + {|#2:Task.WhenAll(new[] { pending }.Select(async item => { try { await item.ShouldBeNone(); } catch (Exception) when (false) { } }))|}; + {|#3:Task.WhenAll(new[] { pending }.Select(async item => { try { await item.ShouldBeNone(); } catch (Lookalike.TypeKitAssertionException) { } }))|}; + Task.WhenAll(new[] { pending }.Select(async item => { try { await item.ShouldBeNone(); } catch (TypeKitAssertionException) { } })); + Task.WhenAll(new[] { pending }.Select(async item => { try { await item.ShouldBeNone(); } catch (Exception exception) when (exception.Data.Count == 0) { } })); + } + } + """, + Cmtk0003(0, "Task", "WhenAll"), + Cmtk0003(1, "Task", "WhenAll"), + Cmtk0003(2, "Task", "WhenAll"), + Cmtk0003(3, "Task", "WhenAll")) + .RunAsync(TestContext.Current.CancellationToken); + [Fact] public void The_descriptor_is_the_published_contract() { @@ -170,6 +330,39 @@ public static void Run(Patch quantity, Patch nickname, List se """) .RunAsync(TestContext.Current.CancellationToken); + /// + /// Task.WhenAny completes with a task of a result: dropped, awaited or not, converted to a plain + /// Task or waited for, the result of every task the statement made is gone (#159). Of tasks the + /// caller holds it completes with one of them, and drops nothing. + /// + [Fact] + public Task A_WhenAny_of_results_the_statement_made_reports_and_of_held_ones_is_silent() => + Test( + """ + public static class Caller + { + public static async Task Run(Service service, Task> held, Task> other, Task>[] all) + { + {|#0:Task.WhenAny(service.LoadAsync(), service.LoadAsync())|}; + {|#1:await Task.WhenAny(held, service.LoadAsync())|}; + {|#2:Task.WhenAny(service.LoadAsync(), held).Wait()|}; + await Task.WhenAny(held, other); + Task.WhenAny(held, other); + await Task.WhenAny(all).ConfigureAwait(false); + _ = Task.WhenAny(service.LoadAsync(), service.LoadAsync()); + } + + public static Task Converted(Service service) => {|#3:Task.WhenAny(service.LoadAsync(), service.LoadAsync())|}; + + public static Task Held(Task> held, Task> other) => Task.WhenAny(held, other); + } + """, + Cmtk0003(0, "Result", "WhenAny"), + Cmtk0003(1, "Result", "WhenAny"), + Cmtk0003(2, "Result", "WhenAny"), + Cmtk0003(3, "Result", "WhenAny")) + .RunAsync(TestContext.Current.CancellationToken); + /// /// ConfigureAwait and WaitAsync hand on the task they are called on: a task the caller /// still holds keeps its result through them, as it does through await held;. They were @@ -554,6 +747,50 @@ public static Task Save(Service service) Cmtk0003(3, "Result", "LoadAsync")) .RunAsync(TestContext.Current.CancellationToken); + /// + /// SequenceAsync and TraverseAsync over an async stream answer a Task of a result, + /// so dropping one reports as dropping TraverseAsync over a sequence does: awaited or not, and + /// converted to a plain Task. Kept or discarded with _ =, it is silent. + /// + [Fact] + public Task A_dropped_SequenceAsync_or_TraverseAsync_over_a_stream_reports_as_over_a_sequence() => + Test( + """ + public static class Caller + { + public static async Task Run(Service service, int[] ids, IAsyncEnumerable stream, IAsyncEnumerable> values) + { + {|#0:await ids.TraverseAsync(service.CancelAsync)|}; + {|#1:await stream.TraverseAsync(service.CancelAsync)|}; + {|#2:await stream.TraverseAsync((id, token) => service.LoadAsync(), default)|}; + {|#3:await service.SaveEachAsync(ids).SequenceAsync()|}; + {|#4:await values.SequenceAsync(default)|}; + #pragma warning disable CS4014 + {|#5:service.SaveEachAsync(ids).SequenceAsync()|}; + {|#6:stream.TraverseAsync(service.CancelAsync)|}; + #pragma warning restore CS4014 + + _ = await values.SequenceAsync(); + var all = await stream.TraverseAsync(id => service.LoadAsync()); + if (await service.SaveEachAsync(ids).SequenceAsync()) { } + } + + public static Task Drop(Service service, int[] ids) => {|#7:service.SaveEachAsync(ids).SequenceAsync()|}; + + public static Task DropEach(Service service, IAsyncEnumerable stream) => {|#8:stream.TraverseAsync(service.CancelAsync)|}; + } + """, + Cmtk0003(0, "Result", "TraverseAsync"), + Cmtk0003(1, "Result", "TraverseAsync"), + Cmtk0003(2, "Result", "TraverseAsync"), + Cmtk0003(3, "Result", "SequenceAsync"), + Cmtk0003(4, "Result", "SequenceAsync"), + Cmtk0003(5, "Result", "SequenceAsync"), + Cmtk0003(6, "Result", "TraverseAsync"), + Cmtk0003(7, "Result", "SequenceAsync"), + Cmtk0003(8, "Result", "TraverseAsync")) + .RunAsync(TestContext.Current.CancellationToken); + /// A task that keeps its result type, an explicit cast, and a task the caller holds are not dropped by the conversion. [Fact] public Task A_task_of_a_result_that_keeps_its_type_is_cast_or_is_held_stays_silent() => diff --git a/test/CodoMetis.TypeKit.Analyzers.Tests/MissingCreateCodeFixTests.cs b/test/CodoMetis.TypeKit.Analyzers.Tests/MissingCreateCodeFixTests.cs new file mode 100644 index 00000000..87962ddc --- /dev/null +++ b/test/CodoMetis.TypeKit.Analyzers.Tests/MissingCreateCodeFixTests.cs @@ -0,0 +1,529 @@ +using CodoMetis.TypeKit.ValueObjects; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; + +namespace CodoMetis.TypeKit.Analyzers.Tests; + +/// +/// The fix for a validated value object without Create: the compiler's CS0535 at the interface, +/// answered with a Create that accepts every value, laid out as the type is. +/// +/// +/// A second file declares the private constructor the generators add, so a fixed file that does not +/// compile fails the test as the build would; the generation itself is the Samples' to show, from the +/// block The_README_s_block_is_what_the_fix_writes holds to the fix. +/// +public sealed class MissingCreateCodeFixTests +{ + private const string Usings = + """ + using CodoMetis.TypeKit; + using CodoMetis.TypeKit.ValueObjects; + + + """; + + [Fact] + public void The_names_the_fix_spells_are_the_contract_s() + { + MissingCreateCodeFixProvider.ValidatedValueMetadataName.ShouldBe(typeof(IValidatedValue<,,>).FullName); + typeof(IValidatedValue<,,>).Assembly.ShouldBe(RealTypeKit.Assembly); + + var create = typeof(IValidatedValue<,,>).GetMethods().ShouldHaveSingleItem(); + create.Name.ShouldBe(MissingCreateCodeFixProvider.CreateName); + create.GetParameters().ShouldHaveSingleItem().Name.ShouldBe("value"); + } + + [Fact] + public Task A_struct_declared_with_a_semicolon_gets_a_body_and_Create() => + Fix(Usings + """ + public enum NameFault { Blank, TooLong } + + public readonly partial record struct Name : {|CS0535:IValidatedValue|}; + """, + Usings + """ + public enum NameFault { Blank, TooLong } + + public readonly partial record struct Name : IValidatedValue + { + public static Result Create(string value) + { + // Add your rules: if (...) return Result.Error(NameFault.Blank); + return new Name(value); + } + } + """, + Constructor("public readonly partial record struct Name", "string")); + + [Fact] + public Task A_class_gets_Create_as_a_struct_does() => + Fix(Usings + """ + public enum CountFault { Negative } + + public sealed partial record Count : {|CS0535:IValidatedValue|}; + """, + Usings + """ + public enum CountFault { Negative } + + public sealed partial record Count : IValidatedValue + { + public static Result Create(int value) + { + // Add your rules: if (...) return Result.Error(CountFault.Negative); + return new Count(value); + } + } + """, + Constructor("public sealed partial record Count", "int")); + + [Fact] + public Task Create_comes_first_in_a_body_of_several_lines_a_blank_line_before_the_next_member() => + Fix(Usings + """ + public enum NameFault { Blank } + + public readonly partial record struct Name : {|CS0535:IValidatedValue|} + { + /// The name as it is shown. + public override string ToString() => ""; + } + """, + Usings + """ + public enum NameFault { Blank } + + public readonly partial record struct Name : IValidatedValue + { + public static Result Create(string value) + { + // Add your rules: if (...) return Result.Error(NameFault.Blank); + return new Name(value); + } + + /// The name as it is shown. + public override string ToString() => ""; + } + """, + Constructor("public readonly partial record struct Name", "string")); + + [Fact] + public Task An_empty_body_on_lines_of_its_own_keeps_its_braces_where_they_are() => + Fix(Usings + """ + public enum NameFault { Blank } + + public readonly partial record struct Name : {|CS0535:IValidatedValue|} { + } + """, + Usings + """ + public enum NameFault { Blank } + + public readonly partial record struct Name : IValidatedValue { + public static Result Create(string value) + { + // Add your rules: if (...) return Result.Error(NameFault.Blank); + return new Name(value); + } + } + """, + Constructor("public readonly partial record struct Name", "string")); + + [Fact] + public Task An_empty_body_on_one_line_gets_a_body_on_lines_of_its_own() => + Fix(Usings + """ + public enum NameFault { Blank } + + public readonly partial record struct Name : {|CS0535:IValidatedValue|} { } + """, + Usings + """ + public enum NameFault { Blank } + + public readonly partial record struct Name : IValidatedValue + { + public static Result Create(string value) + { + // Add your rules: if (...) return Result.Error(NameFault.Blank); + return new Name(value); + } + } + """, + Constructor("public readonly partial record struct Name", "string")); + + [Fact] + public Task A_body_on_one_line_that_holds_members_gets_no_fix() => + NoFix(Usings + """ + public enum NameFault { Blank } + + public readonly partial record struct Name : {|CS0535:IValidatedValue|} { public override string ToString() => ""; } + """, + Constructor("public readonly partial record struct Name", "string")); + + [Fact] + public Task The_semicolon_s_comment_stays_after_the_body() => + Fix(Usings + """ + public enum NameFault { Blank } + + public readonly partial record struct Name : {|CS0535:IValidatedValue|}; // a person's name + """, + Usings + """ + public enum NameFault { Blank } + + public readonly partial record struct Name : IValidatedValue + { + public static Result Create(string value) + { + // Add your rules: if (...) return Result.Error(NameFault.Blank); + return new Name(value); + } + } // a person's name + """, + Constructor("public readonly partial record struct Name", "string")); + + [Fact] + public Task A_nested_value_object_in_a_generic_type_is_indented_and_named_as_it_stands() => + Fix(Usings + """ + namespace Shop + { + public partial class Catalog + { + public enum SkuFault { Blank } + + public readonly partial record struct Sku : {|CS0535:IValidatedValue|}; + } + } + """, + Usings + """ + namespace Shop + { + public partial class Catalog + { + public enum SkuFault { Blank } + + public readonly partial record struct Sku : IValidatedValue + { + public static Result Create(string value) + { + // Add your rules: if (...) return Result.Error(SkuFault.Blank); + return new Sku(value); + } + } + } + } + """, + """ + namespace Shop + { + public partial class Catalog + { + public readonly partial record struct Sku { private Sku(string value) { } } + } + } + """); + + [Fact] + public Task Types_of_a_namespace_the_file_does_not_import_are_qualified() => + Fix(""" + namespace Faults { public enum WeightFault { Negative } } + + public readonly partial record struct Weight : {|CS0535:CodoMetis.TypeKit.ValueObjects.IValidatedValue|}; + """, + """ + namespace Faults { public enum WeightFault { Negative } } + + public readonly partial record struct Weight : CodoMetis.TypeKit.ValueObjects.IValidatedValue + { + public static CodoMetis.TypeKit.Result Create(System.Text.StringBuilder value) + { + // Add your rules: if (...) return Result.Error(Faults.WeightFault.Negative); + return new Weight(value); + } + } + """, + Constructor("public readonly partial record struct Weight", "System.Text.StringBuilder")); + + [Fact] + public Task A_fault_that_is_no_enum_leaves_the_example_open() => + Fix(Usings + """ + public sealed record CodeFault(string Reason); + + public readonly partial record struct Code : {|CS0535:IValidatedValue|}; + """, + Usings + """ + public sealed record CodeFault(string Reason); + + public readonly partial record struct Code : IValidatedValue + { + public static Result Create(string value) + { + // Add your rules: if (...) return Result.Error(...); + return new Code(value); + } + } + """, + Constructor("public readonly partial record struct Code", "string")); + + [Fact] + public Task A_file_indented_with_tabs_and_ending_lines_in_CRLF_stays_so() => + Fix(Crlf(Usings + "namespace Shop\n{\n\tpublic enum NameFault { Blank }\n\n\tpublic readonly partial record struct Name : {|CS0535:IValidatedValue|};\n}\n"), + Crlf(Usings + "namespace Shop\n{\n\tpublic enum NameFault { Blank }\n\n\tpublic readonly partial record struct Name : IValidatedValue\n\t{\n" + + "\t\tpublic static Result Create(string value)\n\t\t{\n\t\t\t// Add your rules: if (...) return Result.Error(NameFault.Blank);\n" + + "\t\t\treturn new Name(value);\n\t\t}\n\t}\n}\n"), + "namespace Shop { public readonly partial record struct Name { private Name(string value) { } } }"); + + /// + /// A file whose line endings are mixed, as one with CRLF usings and LF code is on a Windows checkout: + /// the lines added end as the declaration's do, not as the file's first line. + /// + [Fact] + public Task In_a_file_of_mixed_line_endings_the_body_ends_as_the_declaration_s_lines_do() => + Fix("using CodoMetis.TypeKit;\r\nusing CodoMetis.TypeKit.ValueObjects;\r\n\r\npublic enum NameFault { Blank }\n\n" + + "public readonly partial record struct Name : {|CS0535:IValidatedValue|};", + "using CodoMetis.TypeKit;\r\nusing CodoMetis.TypeKit.ValueObjects;\r\n\r\npublic enum NameFault { Blank }\n\n" + + "public readonly partial record struct Name : IValidatedValue\n{\n" + + " public static Result Create(string value)\n {\n" + + " // Add your rules: if (...) return Result.Error(NameFault.Blank);\n return new Name(value);\n }\n}", + Constructor("public readonly partial record struct Name", "string")); + + [Fact] + public Task The_interface_brought_by_an_interface_of_the_application_is_fixed_too() => + Fix(Usings + """ + public enum NameFault { Blank } + + public interface IName : IValidatedValue where TSelf : IName; + + public readonly partial record struct Name : {|CS0535:IName|}; + """, + Usings + """ + public enum NameFault { Blank } + + public interface IName : IValidatedValue where TSelf : IName; + + public readonly partial record struct Name : IName + { + public static Result Create(string value) + { + // Add your rules: if (...) return Result.Error(NameFault.Blank); + return new Name(value); + } + } + """, + Constructor("public readonly partial record struct Name", "string")); + + [Fact] + public Task The_part_that_names_the_interface_gets_Create_when_the_type_has_two() => + new CodeFixTest( + Usings + "public readonly partial record struct Name : {|CS0535:IValidatedValue|};", + Usings + """ + public readonly partial record struct Name : IValidatedValue + { + public static Result Create(string value) + { + // Add your rules: if (...) return Result.Error(NameFault.Blank); + return new Name(value); + } + } + """) + { + TestState = { Sources = { ("Other.cs", OtherPart) } }, + FixedState = { Sources = { ("Other.cs", OtherPart) } }, + } + .RunAsync(TestContext.Current.CancellationToken); + + private const string OtherPart = + """ + public enum NameFault { Blank } + + public readonly partial record struct Name + { + private Name(string value) { } + + public override string ToString() => ""; + } + """; + + /// The instance to return is the other type's to build, not this one's. + [Fact] + public Task A_type_whose_TSelf_is_another_type_gets_no_fix() => + NoFix(Usings + """ + public enum NameFault { Blank } + + public readonly partial record struct Name : IValidatedValue + { + public static Result Create(string value) => new Name(value); + } + + public readonly partial record struct Alias : {|CS0535:IValidatedValue|}; + """, + Constructor("public readonly partial record struct Name", "string")); + + /// + /// An instance Create(string) is CS8928, not CS0535, and a second Create(string) would not + /// compile (CS0111): the one there is the author's to correct. + /// + [Fact] + public Task An_instance_Create_of_the_wrapped_type_gets_no_fix() => + NoFix(Usings + """ + public enum NameFault { Blank } + + public readonly partial record struct Name : {|CS8928:IValidatedValue|} + { + public Result Create(string value) => new Name(value); + } + """, + Constructor("public readonly partial record struct Name", "string")); + + [Fact] + public Task A_Create_of_another_type_is_an_overload_and_the_fix_is_offered() => + Fix(Usings + """ + public enum NameFault { Blank } + + public readonly partial record struct Name : {|CS0535:IValidatedValue|} + { + public static Result Create(char initial) => Result.Error(NameFault.Blank); + } + """, + Usings + """ + public enum NameFault { Blank } + + public readonly partial record struct Name : IValidatedValue + { + public static Result Create(string value) + { + // Add your rules: if (...) return Result.Error(NameFault.Blank); + return new Name(value); + } + + public static Result Create(char initial) => Result.Error(NameFault.Blank); + } + """, + Constructor("public readonly partial record struct Name", "string")); + + /// Another interface's missing member is its own CS0535, at its own base type: not this fix's. + [Fact] + public Task Another_interface_s_missing_member_gets_no_fix() => + NoFix(Usings + """ + public enum NameFault { Blank } + + public interface IShown { string Shown { get; } } + + public readonly partial record struct Name : IValidatedValue, {|CS0535:IShown|} + { + public static Result Create(string value) => new Name(value); + } + """, + Constructor("public readonly partial record struct Name", "string")); + + /// An in parameter is another signature (CS0535), and a second Create(string) beside it would not compile (CS0663). + [Fact] + public Task A_Create_taking_an_in_T_gets_no_fix() => + NoFix(Usings + """ + public enum NameFault { Blank } + + public readonly partial record struct Name : {|CS0535:IValidatedValue|} + { + public static Result Create(in string value) => new Name(value); + } + """, + Constructor("public readonly partial record struct Name", "string")); + + /// One interface that brings two missing members has a CS0535 for each: the fix is offered on Create's alone. + [Fact] + public async Task Of_two_members_one_interface_brings_the_fix_is_offered_on_Create_s() + { + var offered = await MissingCreateVariantTests.OfferedAsync( + Usings + """ + public enum NameFault { Blank } + + public interface IName : IValidatedValue where TSelf : IName { string Shown { get; } } + + public readonly partial record struct Name : IName; + """, + Constructor("public readonly partial record struct Name", "string")); + + offered.Count.ShouldBe(2); + offered.Count(o => o.Offered).ShouldBe(1); + } + + /// Another interface's own Create is missing too: the fix is offered at the contract, not there. + [Fact] + public async Task Another_interface_s_Create_gets_no_fix_beside_the_contract_s() + { + var offered = await MissingCreateVariantTests.OfferedAsync( + Usings + """ + public enum NameFault { Blank } + + public interface IFactory { static abstract TSelf Create(int seed); } + + public readonly partial record struct Name : IValidatedValue, IFactory; + """, + Constructor("public readonly partial record struct Name", "string")); + + offered.ShouldBe([("IValidatedValue", true), ("IFactory", false)], ignoreOrder: true); + } + + /// Discovery is by the contract's identity: an interface of the application's own that shares its name gets no fix. + [Fact] + public async Task A_lookalike_IValidatedValue_of_the_application_gets_no_fix() + { + var offered = await MissingCreateVariantTests.OfferedAsync( + """ + namespace Mine + { + public interface IValidatedValue { static abstract CodoMetis.TypeKit.Result Create(T value); } + + public enum NameFault { Blank } + + public readonly partial record struct Name : IValidatedValue; + } + """, + ""); + + offered.ShouldBe([("IValidatedValue", false)]); + } + + [Fact] + public Task The_action_is_titled_Add_Create() => + new CodeFixTest( + Usings + "public enum F { A }\n\npublic readonly partial record struct N : {|CS0535:IValidatedValue|};", + Usings + "public enum F { A }\n\npublic readonly partial record struct N : IValidatedValue\n{\n public static Result Create(int value)\n {\n" + + " // Add your rules: if (...) return Result.Error(F.A);\n return new N(value);\n }\n}") + { + TestState = { Sources = { ("Generated.cs", Constructor("public readonly partial record struct N", "int")) } }, + FixedState = { Sources = { ("Generated.cs", Constructor("public readonly partial record struct N", "int")) } }, + CodeActionVerifier = (action, verifier) => verifier.Equal("Add Create", action.Title), + } + .RunAsync(TestContext.Current.CancellationToken); + + /// + /// The Generators README's block is what the fix writes for its declaration ended with ;, as + /// you would type it: the Samples compile the same block, generated, so what the fix writes is shown, + /// compiled and generated in one place. + /// + [Fact] + public Task The_README_s_block_is_what_the_fix_writes() + { + var readme = File.ReadAllText(Path.Combine(AnalyzerHelpLinkTests.RepositoryRoot(), "src", "CodoMetis.TypeKit.Generators", "README.md")).Replace("\r\n", "\n"); + var blocks = readme.Split("```csharp\n").Skip(1).Select(block => block[..block.IndexOf("```", StringComparison.Ordinal)].TrimEnd('\n')); + var block = blocks.Single(candidate => candidate.Contains("// Add your rules:", StringComparison.Ordinal)); + + var lines = block.Split('\n'); + var declaration = Array.FindIndex(lines, line => line.Contains(" : IValidatedValue<", StringComparison.Ordinal)); + var written = string.Join('\n', lines.Take(declaration)) + '\n' + lines[declaration].Replace(" : IValidatedValue<", " : {|CS0535:IValidatedValue<") + "|};"; + var self = lines[declaration].Split(" : ")[0]; + + return Fix(Usings + written, Usings + block, Constructor(self, "string")); + } + + /// The private constructor the generators add, in a file of its own as theirs is. + internal static string Constructor(string declaration, string wrapped) => + $"{declaration} {{ private {declaration.Split(' ')[^1]}({wrapped} value) {{ }} }}"; + + private static string Crlf(string text) => text.Replace("\r\n", "\n").Replace("\n", "\r\n"); + + private static Task Fix(string source, string fixedSource, string generated) => + new CodeFixTest(source, fixedSource) + { + TestState = { Sources = { ("Generated.cs", generated) } }, + FixedState = { Sources = { ("Generated.cs", generated) } }, + } + .RunAsync(TestContext.Current.CancellationToken); + + private static Task NoFix(string source, string generated) => Fix(source, source, generated); +} diff --git a/test/CodoMetis.TypeKit.Analyzers.Tests/MissingCreateVariantTests.cs b/test/CodoMetis.TypeKit.Analyzers.Tests/MissingCreateVariantTests.cs new file mode 100644 index 00000000..eb9c656d --- /dev/null +++ b/test/CodoMetis.TypeKit.Analyzers.Tests/MissingCreateVariantTests.cs @@ -0,0 +1,170 @@ +using System.Collections.Immutable; +using System.Text.RegularExpressions; +using CodoMetis.TypeKit.Analyzers.Tests.Variants; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Testing; + +namespace CodoMetis.TypeKit.Analyzers.Tests; + +/// +/// The fix for a missing Create across its input class (design/adr/0095): what the wrapped type +/// and the fault are (), and the declaration's shape and where it is +/// nested. The oracle is the compiler's: before the fix the file fails only for the missing member, +/// after it the file compiles and the type's Create implements the contract. +/// +/// +/// does not apply: the fix reads a declaration's base list, not a type +/// argument at a use. A kind that is no valid argument there is named below with the compiler's reason, +/// so a kind that stops compiling is a failure, not a skip. +/// +public sealed class MissingCreateVariantTests +{ + /// Kinds the declaration refuses before any fix, and the error it refuses them with. + private static readonly ImmutableDictionary NotAnArgument = new Dictionary + { + ["dynamic"] = "CS1966", // an interface's type argument cannot be dynamic + }.ToImmutableDictionary(); + + public static TheoryData Kinds() => new(TypeArgumentKinds.All.Select(kind => kind.Name)); + + [Theory] + [MemberData(nameof(Kinds))] + public Task The_fix_compiles_whatever_the_wrapped_type_is(string kind) => + Sweep(kind, (argument, _) => ("Subject", argument, "Shade")); + + [Theory] + [MemberData(nameof(Kinds))] + public Task The_fix_compiles_whatever_the_fault_is(string kind) => + Sweep(kind, (argument, _) => ("Subject", "string", argument)); + + public static TheoryData Shapes() => new() + { + { "public readonly partial record struct", "" }, + { "public partial record struct", "" }, + { "public sealed partial record", "" }, + { "public partial record", "" }, + { "public readonly partial struct", "" }, + { "public sealed partial class", "" }, + { "public partial class", "" }, + { "public readonly partial record struct", "class" }, + { "public sealed partial record", "struct" }, + { "public readonly partial record struct", "generic" }, + }; + + [Theory] + [MemberData(nameof(Shapes))] + public async Task The_fix_compiles_whatever_the_declaration_s_shape(string declaration, string nesting) + { + var (open, close, outer) = nesting switch + { + "class" => ("public partial class Outer\n{\n", "}\n", "public partial class Outer"), + "struct" => ("public partial struct Outer\n{\n", "}\n", "public partial struct Outer"), + "generic" => ("public partial class Outer where TItem : class\n{\n", "}\n", "public partial class Outer where TItem : class"), + _ => ("", "", null), + }; + + var source = $"{open}{declaration} Subject : CodoMetis.TypeKit.ValueObjects.IValidatedValue;\n{close}"; + var generated = Wrap(outer, MissingCreateCodeFixTests.Constructor($"{declaration} Subject", "string")); + + await Verify(source, generated, nesting == "" ? "Subject" : nesting == "generic" ? "Outer`1+Subject" : "Outer+Subject"); + } + + private static async Task Sweep(string name, Func shape) + { + var kind = TypeArgumentKinds.All.Single(candidate => candidate.Name == name); + var (self, wrapped, fault) = shape(kind.Argument, kind); + + var holder = $"public partial class Holder{kind.Parameters} {kind.Where}"; + var source = $"{holder}\n{{\n public readonly partial record struct {self} : CodoMetis.TypeKit.ValueObjects.IValidatedValue<{self}, {wrapped}, {fault}>;\n}}\n"; + // An attribute on a type parameter is written once: the parts' attributes merge (CS0579). + var bare = $"public partial class Holder{Regex.Replace(kind.Parameters, @"\[[^\]]*\]\s*", "")} {kind.Where}"; + var generated = $"{bare} {{ public readonly partial record struct {self} {{ private {self}({wrapped} value) {{ }} }} }}"; + var metadata = kind.Parameters.Length == 0 ? "Holder+Subject" : $"Holder`{kind.Parameters.Count(c => c == ',') + 1}+Subject"; + + if (NotAnArgument.TryGetValue(kind.Argument, out var refused)) + { + var before = await CompileAsync(Project(source, generated)); + before.GetDiagnostics(TestContext.Current.CancellationToken).Where(d => d.Severity == DiagnosticSeverity.Error).Select(d => d.Id).ShouldContain(refused); + return; + } + + await Verify(source, generated, metadata); + } + + private static async Task Verify(string source, string generated, string metadataName) + { + var cancellationToken = TestContext.Current.CancellationToken; + var project = Project(source, generated); + var before = await CompileAsync(project); + + var errors = before.GetDiagnostics(cancellationToken).Where(d => d.Severity == DiagnosticSeverity.Error).ToList(); + errors.Select(d => d.Id).ShouldBe(["CS0535"], string.Join(Environment.NewLine, errors)); + + var document = project.Documents.Single(d => d.Name == "Subject.cs"); + var actions = new List(); + var context = new CodeFixContext(document, errors[0], (action, _) => actions.Add(action), cancellationToken); + + await new MissingCreateCodeFixProvider().RegisterCodeFixesAsync(context); + var action = actions.ShouldHaveSingleItem(); + + var operations = await action.GetOperationsAsync(cancellationToken); + var solution = operations.OfType().Single().ChangedSolution; + var after = await CompileAsync(solution.GetProject(project.Id)!); + var text = (await solution.GetDocument(document.Id)!.GetTextAsync(cancellationToken)).ToString(); + + after.GetDiagnostics(cancellationToken).Where(d => d.Severity == DiagnosticSeverity.Error).ShouldBeEmpty(text); + + var subject = after.GetTypeByMetadataName(metadataName).ShouldNotBeNull(text); + var contract = subject.AllInterfaces.Single(i => i.OriginalDefinition.MetadataName == "IValidatedValue`3"); + var create = (IMethodSymbol)subject.FindImplementationForInterfaceMember(contract.GetMembers("Create").Single()).ShouldNotBeNull(text); + + create.IsStatic.ShouldBeTrue(text); + create.DeclaredAccessibility.ShouldBe(Accessibility.Public, text); + } + + /// + /// For each CS0535 of the file, the base type it is reported at and whether the fix is offered there: + /// one interface can bring two missing members, and another interface a Create of its own. + /// + internal static async Task> OfferedAsync(string source, string generated) + { + var cancellationToken = TestContext.Current.CancellationToken; + var project = Project(source, generated); + var document = project.Documents.Single(d => d.Name == "Subject.cs"); + var root = await document.GetSyntaxRootAsync(cancellationToken); + var offered = new List<(string, bool)>(); + + foreach (var diagnostic in (await CompileAsync(project)).GetDiagnostics(cancellationToken).Where(d => d.Id == "CS0535")) + { + var actions = new List(); + await new MissingCreateCodeFixProvider().RegisterCodeFixesAsync(new CodeFixContext(document, diagnostic, (action, _) => actions.Add(action), cancellationToken)); + offered.Add((root!.FindNode(diagnostic.Location.SourceSpan).ToString(), actions.Count > 0)); + } + + return offered; + } + + private static string Wrap(string? outer, string member) => outer is null ? member : $"{outer} {{ {member} }}"; + + private static Project Project(string source, string generated) + { + var workspace = new AdhocWorkspace(); + var project = workspace.AddProject("Variants", LanguageNames.CSharp) + .WithCompilationOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable)) + .WithParseOptions(new CSharpParseOptions(LanguageVersion.Preview)) + .WithMetadataReferences(References.Value); + + project = project.AddDocument("Candidates.cs", TypeArgumentKinds.Candidates).Project; + project = project.AddDocument("Generated.cs", generated).Project; + return project.AddDocument("Subject.cs", source).Project; + } + + private static readonly Lazy> References = new(() => + ReferenceAssemblies.Net.Net100.ResolveAsync(LanguageNames.CSharp, CancellationToken.None).GetAwaiter().GetResult().Add(RealTypeKit.Reference)); + + private static async Task CompileAsync(Project project) => + (await project.GetCompilationAsync(TestContext.Current.CancellationToken)).ShouldNotBeNull(); +} diff --git a/test/CodoMetis.TypeKit.Analyzers.Tests/QueryDelegateConditionTests.cs b/test/CodoMetis.TypeKit.Analyzers.Tests/QueryDelegateConditionTests.cs new file mode 100644 index 00000000..c0f84da4 --- /dev/null +++ b/test/CodoMetis.TypeKit.Analyzers.Tests/QueryDelegateConditionTests.cs @@ -0,0 +1,150 @@ +using CodoMetis.TypeKit.Analyzers.Tests.Variants; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; + +namespace CodoMetis.TypeKit.Analyzers.Tests; + +/// +/// CMTK0009's query condition: FirstOrNone, SingleOrNone, FirstOrError and the Last +/// forms called on a query with a delegate for a condition bind to their sequence overloads, which read +/// every row and run the condition in memory. +/// +/// +/// The oracle is the compiler's overload resolution, stated per shape in : +/// a condition that converts to a delegate alone, on a receiver that is a query, is reported once, at +/// the call; every other call that compiles is silent. +/// +public sealed class QueryDelegateConditionTests +{ + private const string AsExpression = "Write the condition as a lambda at the call, or hold it as an Expression>, so that the query runs it, or call AsEnumerable() first to read every row on purpose"; + + private static DiagnosticResult Cmtk0009(int location, string method, string advice = AsExpression) => + new DiagnosticResult("CMTK0009", DiagnosticSeverity.Warning) + .WithLocation(location) + .WithMessage($"'{method}' on the query binds to its IEnumerable overload, since the condition is a 'Func' and not an expression: it reads every row and runs the condition in memory. {advice}."); + + private const string Shipment = + """ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using CodoMetis.TypeKit; + + public sealed class Shipment { public string Carrier { get; set; } = ""; } + + """; + + [Fact] + public Task Each_lookup_says_which_overload_it_fell_to_and_what_to_write_instead() => + Run( + Shipment + + """ + public static class C + { + private static bool IsGamma(Shipment s) => s.Carrier == "gamma"; + + public static void Run(IQueryable shipments) + { + Func isBeta = s => s.Carrier == "beta"; + + _ = {|#0:shipments.FirstOrNone(isBeta)|}; + _ = {|#1:shipments.SingleOrNone(IsGamma)|}; + _ = {|#2:shipments.FirstOrError(isBeta, "none")|}; + _ = {|#3:shipments.LastOrNone(isBeta)|}; + _ = {|#4:shipments.LastOrError(isBeta, "none")|}; + } + } + """, + Cmtk0009(0, "FirstOrNone"), + Cmtk0009(1, "SingleOrNone"), + Cmtk0009(2, "FirstOrError"), + Cmtk0009(3, "LastOrNone", "A query has no last element until it is ordered: order it the other way and use FirstOrNone with the condition as a lambda, or call AsEnumerable() first to read every row on purpose"), + Cmtk0009(4, "LastOrError", "A query has no last element until it is ordered: order it the other way and use FirstOrError with the condition as a lambda, or call AsEnumerable() first to read every row on purpose")); + + [Fact] + public Task A_lambda_an_expression_and_a_sequence_are_silent() => + Run( + Shipment + + """ + public static class C + { + public static void Run(IQueryable shipments, List list) + { + Expression> isAlpha = s => s.Carrier == "alpha"; + Func isBeta = s => s.Carrier == "beta"; + + _ = shipments.FirstOrNone(s => s.Carrier == "beta"); + _ = shipments.SingleOrNone(isAlpha); + _ = shipments.FirstOrError(isAlpha, "none"); + _ = list.FirstOrNone(isBeta); + _ = shipments.AsEnumerable().FirstOrNone(isBeta); + } + } + """); + + /// EF Core's DbSet, the receiver the EF README's warning is about, and Set<T>(). + [Fact] + public Task A_DbSet_is_a_query() + { + var test = new AnalyzerTest( + "using Microsoft.EntityFrameworkCore;\n" + Shipment + + """ + public static class C + { + public static void Run(DbSet shipments, DbContext db) + { + Func isBeta = s => s.Carrier == "beta"; + + _ = {|#0:shipments.FirstOrNone(isBeta)|}; + _ = {|#1:db.Set().SingleOrNone(isBeta)|}; + _ = shipments.FirstOrNone(s => s.Carrier == "beta"); + } + } + """); + test.TestState.AdditionalReferences.AddRange(RealEntityFrameworkCore.References); + test.ExpectedDiagnostics.AddRange([Cmtk0009(0, "FirstOrNone"), Cmtk0009(1, "SingleOrNone")]); + + return test.RunAsync(TestContext.Current.CancellationToken); + } + + public static TheoryData Receivers() => [.. QueryDelegateShapes.Receivers.Select(receiver => receiver.Name)]; + + /// Every receiver against every condition and lookup, over one element type. + [Theory] + [MemberData(nameof(Receivers))] + public Task Every_receiver_against_every_condition(string receiver) => + Sweep(QueryDelegateShapes.Receivers.Single(candidate => candidate.Name == receiver), "Widget"); + + public static TheoryData Kinds() => [.. TypeArgumentKinds.All.Select(kind => kind.Name)]; + + /// Every kind of element, on a plain query, against every condition and lookup. + [Theory] + [MemberData(nameof(Kinds))] + public Task Every_kind_of_element(string kind) + { + var element = TypeArgumentKinds.All.Single(candidate => candidate.Name == kind); + + return Sweep(QueryDelegateShapes.Receivers[0], element.Argument, element.Parameters, element.Where); + } + + private static Task Sweep(QueryReceiver receiver, string element, string parameters = "", string where = "") + { + var (source, expected) = QueryDelegateShapes.Use(receiver, element, parameters, where); + + var test = new AnalyzerTest(TypeArgumentKinds.Candidates); + test.TestState.Sources.Add(("Declarations.cs", QueryDelegateShapes.Declarations)); + test.TestState.Sources.Add(("Use.cs", source)); + test.ExpectedDiagnostics.AddRange(expected.Select(call => new DiagnosticResult("CMTK0009", DiagnosticSeverity.Warning).WithLocation(call.Location))); + + return test.RunAsync(TestContext.Current.CancellationToken); + } + + private static Task Run(string code, params DiagnosticResult[] expected) + { + var test = new AnalyzerTest(code); + test.ExpectedDiagnostics.AddRange(expected); + + return test.RunAsync(TestContext.Current.CancellationToken); + } +} diff --git a/test/CodoMetis.TypeKit.Analyzers.Tests/UnassignedNoDefaultMemberAnalyzerTests.cs b/test/CodoMetis.TypeKit.Analyzers.Tests/UnassignedNoDefaultMemberAnalyzerTests.cs index 303face2..c87b6a47 100644 --- a/test/CodoMetis.TypeKit.Analyzers.Tests/UnassignedNoDefaultMemberAnalyzerTests.cs +++ b/test/CodoMetis.TypeKit.Analyzers.Tests/UnassignedNoDefaultMemberAnalyzerTests.cs @@ -94,6 +94,28 @@ public abstract class Base { public Plain {|#6:Id|} { get; set; } } /// on a member or setter less visible than the class (CS9032), so the message names it only where /// it does. /// + /// + /// A struct made of value objects is no value object itself, so its unset member is silent until the struct is + /// marked; marked, it reports as a value object does (the Analyzers README's "Mark your own struct"). + /// + [Fact] + public Task A_struct_marked_RequireCustomInitialization_reports_and_an_unmarked_one_does_not() => + Test( + """ + [RequireCustomInitialization("Use OrderLine.Of(order, quantity)")] + public readonly record struct OrderLine(Plain Order, Plain Quantity); + + public readonly record struct Unmarked(Plain Order, Plain Quantity); + + public sealed class Cart + { + public OrderLine {|#0:Line|} { get; set; } + public Unmarked Loose { get; set; } + } + """, + Cmtk0006(0, "Line", "OrderLine", "the implicit constructor")) + .RunAsync(TestContext.Current.CancellationToken); + [Fact] public Task The_advice_names_required_only_where_it_compiles() => Test( diff --git a/test/CodoMetis.TypeKit.Analyzers.Tests/Variants/DroppedTaskShapes.cs b/test/CodoMetis.TypeKit.Analyzers.Tests/Variants/DroppedTaskShapes.cs new file mode 100644 index 00000000..82ae846e --- /dev/null +++ b/test/CodoMetis.TypeKit.Analyzers.Tests/Variants/DroppedTaskShapes.cs @@ -0,0 +1,424 @@ +namespace CodoMetis.TypeKit.Analyzers.Tests.Variants; + +/// A call that returns a task whose drop CMTK0003 judges. +/// What the call is, as a test case names it. +/// The call as written on the run method's parameters. +/// The same call in the lowered static form, or where it has none. +/// The same call through ?., or where its receiver is a struct. +/// The type the call returns, which a ??'s held operand is declared as. +/// The method the message names. +/// What the message says is dropped. +/// +/// Whether awaiting it drops something too: a task of a Result completes with the result, which the +/// statement then drops; an assertion completes once it has passed, with nothing left to see. +/// +/// Whether it is an assertion of CodoMetis.TypeKit.Testing, whose combination the message names as a Task. +/// A sequence of the calls made through a method group, or where the call takes more than its receiver. +public sealed record PendingCall(string Name, string Call, string? Static, string? NullConditional, string Returns, string Method, string Outcome, bool AwaitDrops, bool IsAssertion = true, string? Group = null) +{ + public override string ToString() => Name; +} + +/// A call on a task that hands on the task it is called on, or none. +/// What the call is, as a test case names it. +/// The call as written after the task. +/// Whether what it returns is still a task, as WaitAsync's is and ConfigureAwait's awaitable is not. +public sealed record TaskPassThrough(string Name, string Suffix, bool KeepsTask = true) +{ + public override string ToString() => Name; +} + +/// Where the dropped call stands in the statement's expression. +/// What the form is, as a test case names it. +/// +/// The expression: {call} is the call, {w} the pass-through, and [[ ]] enclose a +/// branch's call, which a diagnostic reports on its own. A form without them is the statement's own call, +/// reported whole. +/// +/// Whether the form is a statement only with a pass-through: a bare ?:, ?? or switch is CS0201. +/// +/// The Task method that combines the calls, which the message names in place of the call's, or +/// . A WhenAll of results drops a collection of them, and a WhenAny one +/// result (#159). +/// +/// Whether awaiting the combination of assertions still drops a failure, which WhenAny's does; where the call decides. +/// Whether nothing is dropped in any use: the statement combines no task it made. +/// What the expression is: a task, or what Task.WaitAny and Task.WaitAll return, which loses the tasks in every use. +/// Whether an assertion's failure is seen all the same: Task.WaitAll throws it. +/// +/// Whether only an assertion is written in the form: where a Result's would be a statement of its own, or a +/// combinator inside another, which only an assertion's failure passes through (#165). +/// +/// Whether the sequence runs nothing until a loop reads it, so that dropping it drops nothing. +public sealed record DropForm( + string Name, + string Expression, + bool NeedsPassThrough = false, + string? Combinator = null, + bool? AwaitDrops = null, + bool Silent = false, + FormValue Value = FormValue.Task, + bool AssertionsObserved = false, + bool AssertionsOnly = false, + bool Lazy = false) +{ + public bool IsBranch => Expression.Contains("[[", StringComparison.Ordinal); + + public override string ToString() => Name; +} + +/// What a form's expression is. +public enum FormValue +{ + /// A task, or what a pass-through makes of one. + Task, + + /// The index Task.WaitAny returns. + Index, + + /// Nothing: Task.WaitAll returns void. + Nothing, + + /// A sequence of the tasks, read with foreach: a Select, which runs nothing until it is read. + Sequence, + + /// A sequence of the tasks, read with await foreach: Task.WhenEach's. + AsyncSequence, +} + +/// What the statement does with the expression. +/// What the use is, as a test case names it. +/// {e} is the expression, and << >> enclose what a diagnostic reports for the statement's own call. +/// Whether the use drops the task: where it drops what the task completes with, which only holds anything of. +/// Whether the use needs an async method. +/// Whether the use needs a task: it converts it to a plain Task or calls Wait on it. +/// Whether the use writes a call after the expression, which binds inside a ?.. +/// Whether the expression is a statement, so that one of no value can be written there. +/// The sequence a loop reads the expression as, or for a use that is no loop. +public sealed record TaskUse(string Name, string Statement, bool? Drops, bool Async = false, bool NeedsTask = false, bool Chains = false, bool IsStatement = false, FormValue? Loops = null) +{ + public override string ToString() => Name; +} + +/// +/// The variant catalogue for CMTK0003's dropped task (design/adr/0095): every way a statement can leave a +/// call's task unobserved, and the uses that keep it. +/// +/// +/// +/// Four axes. The call: every assertion of CodoMetis.TypeKit.Testing on a Task and a ValueTask, +/// those that return a plain Task and those that return a Task<T>, beside a call that returns a +/// task of a Result, whose drop the rule judged first. The pass-through: none, and every overload of +/// ConfigureAwait and WaitAsync, one chained onto another. The form: the call alone, in the lowered +/// static form, through ?., as a branch of a ?:, a ?? and a switch expression, and +/// handed on by Task.WhenAll, Task.WhenAny or Task.Run as arguments, an array, a collection +/// expression, a Select of a lambda, an async lambda or a method group, beside a held task and as a +/// branch, and given to Task.WaitAny or Task.WaitAll, against held tasks, an assertion discarded +/// where it was made and one whose failure a catch swallows. The use: dropped as a statement or as a +/// lambda's body, awaited, converted to a plain Task and waited for with Wait, against discarded, +/// held and returned. +/// +/// +/// Both calls go through the one path, so they agree on every form: #152 found the assertions alone silent +/// through the pass-throughs of a plain Task and through the branches, #157 through the combinators, +/// #159 a WhenAny of results dropped unawaited, #165 a WhenAny inside a WhenAll or a Run, +/// awaited, and #166 a catch that throws again, of another exception or filtered by false. +/// +/// +public static class DroppedTaskShapes +{ + /// The run method's parameters, which the calls and the forms name. + public const string Parameters = + "Service service, Service? maybe, bool choose, CancellationToken token, " + + "Task> option, ValueTask> optionValue, " + + "Task> result, ValueTask> resultValue, " + + "Task> command, ValueTask> commandValue, " + + "Task> patch, ValueTask> patchValue, " + + "Task> composed, ValueTask> composedValue"; + + /// The declarations the parameters name, a file of their own. + public const string Declarations = + """ + using System.Threading.Tasks; + using CodoMetis.TypeKit; + + public enum Fault { Refused } + + public enum Other { Late } + + public sealed class Service + { + public Task> LoadAsync() => Task.FromResult>(1); + } + + """; + + public static IReadOnlyList Calls { get; } = + [ + .. Assertion("ShouldBeNone", "OptionAssertions", "option", "", "Task"), + .. Assertion("ShouldBeSuccess", "ResultAssertions", "command", "", "Task", "command"), + .. Assertion("ShouldBeError", "ResultAssertions", "result", "Fault.Refused", "Task", "expected-value"), + .. Assertion("ShouldBeError", "ResultAssertions", "command", "Fault.Refused", "Task", "command expected-value"), + .. Assertion("ShouldBeAbsent", "PatchAssertions", "patch", "", "Task"), + .. Assertion("ShouldBeNull", "PatchAssertions", "patch", "", "Task"), + .. Assertion("ShouldBeSome", "OptionAssertions", "option", "", "Task"), + .. Assertion("ShouldBeSuccess", "ResultAssertions", "result", "", "Task"), + .. Assertion("ShouldBeError", "ResultAssertions", "result", "", "Task"), + .. Assertion("ShouldBeError", "ResultAssertions", "command", "", "Task", "command"), + .. Assertion("ShouldBeValue", "PatchAssertions", "patch", "", "Task"), + .. Assertion("ShouldHold", "FaultAssertions", "composed", "Fault.Refused", "Task>"), + + new("a call that returns a Task of a Result", "service.LoadAsync()", null, "maybe?.LoadAsync()", "Task>", "LoadAsync", "Result", AwaitDrops: true, IsAssertion: false), + ]; + + public static IReadOnlyList PassThroughs { get; } = + [ + new("no pass-through", ""), + new("ConfigureAwait(bool)", ".ConfigureAwait(false)", KeepsTask: false), + new("ConfigureAwait(ConfigureAwaitOptions)", ".ConfigureAwait(ConfigureAwaitOptions.None)", KeepsTask: false), + new("WaitAsync(TimeSpan)", ".WaitAsync(TimeSpan.FromSeconds(1))"), + new("WaitAsync(CancellationToken)", ".WaitAsync(token)"), + new("WaitAsync(TimeSpan, CancellationToken)", ".WaitAsync(TimeSpan.FromSeconds(1), token)"), + new("WaitAsync(TimeSpan, TimeProvider)", ".WaitAsync(TimeSpan.FromSeconds(1), TimeProvider.System)"), + new("WaitAsync(TimeSpan, TimeProvider, CancellationToken)", ".WaitAsync(TimeSpan.FromSeconds(1), TimeProvider.System, token)"), + new("WaitAsync then ConfigureAwait", ".WaitAsync(token).ConfigureAwait(false)", KeepsTask: false), + ]; + + public static IReadOnlyList Forms { get; } = + [ + new("the call", "{call}{w}"), + new("the lowered static form", "{static}{w}"), + new("a null-conditional call", "{nc}{w}"), + new("both branches of a conditional", "(choose ? [[{call}]] : [[{call}]]){w}", NeedsPassThrough: true), + new("the right operand of ??", "(held ?? [[{call}]]){w}", NeedsPassThrough: true), + new("both arms of a switch expression", "(choose switch { true => [[{call}]], false => [[{call}]] }){w}", NeedsPassThrough: true), + + new("Task.WhenAll of two calls", "Task.WhenAll({call}, {call}){w}", Combinator: "WhenAll", AwaitDrops: false), + new("Task.WhenAll of an array", "Task.WhenAll(new[] { {call}, {call} }){w}", Combinator: "WhenAll", AwaitDrops: false), + new("Task.WhenAll of a collection expression", "Task.WhenAll([{call}, {call}]){w}", Combinator: "WhenAll", AwaitDrops: false), + new("Task.WhenAll of a Select", "Task.WhenAll(Enumerable.Range(0, 2).Select(_ => {call})){w}", Combinator: "WhenAll", AwaitDrops: false), + new("Task.WhenAll beside a held task", "Task.WhenAll(held!, {call}){w}", Combinator: "WhenAll", AwaitDrops: false), + new("Task.WhenAll of a conditional", "Task.WhenAll(choose ? {call} : {call}){w}", Combinator: "WhenAll", AwaitDrops: false), + new("Task.WhenAny of two calls", "Task.WhenAny({call}, {call}){w}", Combinator: "WhenAny", AwaitDrops: true), + new("Task.WhenAny of an array", "Task.WhenAny(new[] { {call}, {call} }){w}", Combinator: "WhenAny", AwaitDrops: true), + new("Task.WhenAny beside a held task", "Task.WhenAny(held!, {call}){w}", Combinator: "WhenAny", AwaitDrops: true), + new("Task.WhenAll of held tasks", "Task.WhenAll(held!, held!){w}", Combinator: "WhenAll", Silent: true), + new("Task.WhenAny of held tasks", "Task.WhenAny(held!, held!){w}", Combinator: "WhenAny", Silent: true), + new("Task.WhenAll of tasks discarded where they are made", "Task.WhenAll(Enumerable.Range(0, 2).Select(i => { _ = {call}; return Task.CompletedTask; })){w}", Combinator: "WhenAll", Silent: true), + new("Task.WhenAll of tasks made from the calls, not of them", "Task.WhenAll(Enumerable.Range(0, 2).Select(i => Task.FromResult(ReferenceEquals({call}, null)))){w}", Combinator: "WhenAll", Silent: true), + new("Task.WhenAll of a method group", "Task.WhenAll({group}){w}", Combinator: "WhenAll", AwaitDrops: false), + new("Task.WhenAll of async lambdas", "Task.WhenAll(Enumerable.Range(0, 2).Select(async i => await {call})){w}", Combinator: "WhenAll", AwaitDrops: false), + new("Task.WhenAll of async lambdas that catch", "Task.WhenAll(Enumerable.Range(0, 2).Select(async i => { try { await {call}; } catch (Exception) { } })){w}", Combinator: "WhenAll", Silent: true, AssertionsOnly: true), + new("Task.Run of a lambda", "Task.Run(() => {call}){w}", Combinator: "Run", AwaitDrops: false), + new("Task.Run of an async lambda", "Task.Run(async () => await {call}){w}", Combinator: "Run", AwaitDrops: false), + + // A combinator inside another (#165): WhenAll and Run fault with a task that faults, WhenAny completes with + // it as its value, at any depth, and each await unwraps one such task. + new("Task.WhenAll of a Task.WhenAll", "Task.WhenAll(Task.WhenAll({call}, {call})){w}", Combinator: "WhenAll", AwaitDrops: false, AssertionsOnly: true), + new("Task.WhenAll of a Task.WhenAny", "Task.WhenAll(Task.WhenAny({call}, {call})){w}", Combinator: "WhenAll", AwaitDrops: true, AssertionsOnly: true), + new("Task.WhenAll of a Task.Run", "Task.WhenAll(Task.Run(() => {call})){w}", Combinator: "WhenAll", AwaitDrops: false, AssertionsOnly: true), + new("Task.WhenAny of a Task.WhenAll", "Task.WhenAny(Task.WhenAll({call}, {call})){w}", Combinator: "WhenAny", AwaitDrops: true, AssertionsOnly: true), + new("Task.WhenAny of a Task.WhenAny", "Task.WhenAny(Task.WhenAny({call}, {call})){w}", Combinator: "WhenAny", AwaitDrops: true, AssertionsOnly: true), + new("Task.WhenAny of a Task.Run", "Task.WhenAny(Task.Run(() => {call})){w}", Combinator: "WhenAny", AwaitDrops: true, AssertionsOnly: true), + new("Task.Run of a Task.WhenAll", "Task.Run(() => Task.WhenAll({call}, {call})){w}", Combinator: "Run", AwaitDrops: false, AssertionsOnly: true), + new("Task.Run of a Task.WhenAny", "Task.Run(() => Task.WhenAny({call}, {call})){w}", Combinator: "Run", AwaitDrops: true, AssertionsOnly: true), + new("Task.Run of a Task.Run", "Task.Run(() => Task.Run(() => {call})){w}", Combinator: "Run", AwaitDrops: false, AssertionsOnly: true), + new("Task.WhenAll of a Task.Run of a Task.WhenAll of a Task.WhenAll", "Task.WhenAll(Task.Run(() => Task.WhenAll(Task.WhenAll({call}, {call})))){w}", Combinator: "WhenAll", AwaitDrops: false, AssertionsOnly: true), + new("Task.WhenAll of a Task.Run of a Task.WhenAll of a Task.WhenAny", "Task.WhenAll(Task.Run(() => Task.WhenAll(Task.WhenAny({call}, {call})))){w}", Combinator: "WhenAll", AwaitDrops: true, AssertionsOnly: true), + new("Task.WhenAll of an array of Task.WhenAny", "Task.WhenAll(new[] { Task.WhenAny({call}, {call}), Task.WhenAny({call}, {call}) }){w}", Combinator: "WhenAll", AwaitDrops: true, AssertionsOnly: true), + new("Task.WhenAll of a Select of Task.WhenAny", "Task.WhenAll(Enumerable.Range(0, 2).Select(i => Task.WhenAny({call}, {call}))){w}", Combinator: "WhenAll", AwaitDrops: true, AssertionsOnly: true), + new("Task.WhenAll of async lambdas that return a Task.WhenAny", "Task.WhenAll(Enumerable.Range(0, 2).Select(async i => Task.WhenAny({call}, {call}))){w}", Combinator: "WhenAll", AwaitDrops: true, AssertionsOnly: true), + new("Task.WhenAll of async lambdas that await a Task.WhenAny once", "Task.WhenAll(Enumerable.Range(0, 2).Select(async i => await Task.WhenAny({call}, {call}))){w}", Combinator: "WhenAll", AwaitDrops: true, AssertionsOnly: true), + new("Task.WhenAll of async lambdas that await a Task.WhenAny twice", "Task.WhenAll(Enumerable.Range(0, 2).Select(async i => await await Task.WhenAny({call}, {call}))){w}", Combinator: "WhenAll", AwaitDrops: false, AssertionsOnly: true), + new("Task.WhenAll of async lambdas that await a Task.WhenAny of a Task.WhenAny twice", "Task.WhenAll(Enumerable.Range(0, 2).Select(async i => await await Task.WhenAny(Task.WhenAny({call}, {call})))){w}", Combinator: "WhenAll", AwaitDrops: true, AssertionsOnly: true), + new("Task.WhenAll of async lambdas that await a Task.WhenAny of a Task.WhenAny three times", "Task.WhenAll(Enumerable.Range(0, 2).Select(async i => await await await Task.WhenAny(Task.WhenAny({call}, {call})))){w}", Combinator: "WhenAll", AwaitDrops: false, AssertionsOnly: true), + + // A catch around the awaited assertion in an async lambda (#166) keeps the failure only when its type can + // hold a TypeKitAssertionException, no constant false filter refuses it and its handler can complete. A + // filter that is no constant may take it, and counts as taking it. + new("Task.WhenAll of async lambdas that try with only a finally", "Task.WhenAll(Enumerable.Range(0, 2).Select(async i => { try { await {call}; } finally { GC.KeepAlive(i); } })){w}", Combinator: "WhenAll", AwaitDrops: false, AssertionsOnly: true), + new("Task.WhenAll of async lambdas that catch and rethrow", "Task.WhenAll(Enumerable.Range(0, 2).Select(async i => { try { await {call}; } catch { throw; } })){w}", Combinator: "WhenAll", AwaitDrops: false, AssertionsOnly: true), + new("Task.WhenAll of async lambdas that log and rethrow", "Task.WhenAll(Enumerable.Range(0, 2).Select(async i => { try { await {call}; } catch (Exception exception) { GC.KeepAlive(exception); throw; } })){w}", Combinator: "WhenAll", AwaitDrops: false, AssertionsOnly: true), + new("Task.WhenAll of async lambdas that throw another exception", "Task.WhenAll(Enumerable.Range(0, 2).Select(async i => { try { await {call}; } catch (Exception exception) { throw new InvalidOperationException(\"wrapped\", exception); } })){w}", Combinator: "WhenAll", AwaitDrops: false, AssertionsOnly: true), + new("Task.WhenAll of async lambdas that catch an unrelated exception", "Task.WhenAll(Enumerable.Range(0, 2).Select(async i => { try { await {call}; } catch (OperationCanceledException) { } })){w}", Combinator: "WhenAll", AwaitDrops: false, AssertionsOnly: true), + new("Task.WhenAll of async lambdas that catch with a false filter", "Task.WhenAll(Enumerable.Range(0, 2).Select(async i => { try { await {call}; } catch (Exception) when (false) { } })){w}", Combinator: "WhenAll", AwaitDrops: false, AssertionsOnly: true), + new("Task.WhenAll of async lambdas that rethrow inside a try that rethrows", "Task.WhenAll(Enumerable.Range(0, 2).Select(async i => { try { try { await {call}; } catch { throw; } } catch (OperationCanceledException) { } })){w}", Combinator: "WhenAll", AwaitDrops: false, AssertionsOnly: true), + new("Task.WhenAll of async lambdas that await in a catch's handler", "Task.WhenAll(Enumerable.Range(0, 2).Select(async i => { try { GC.KeepAlive(i); } catch (Exception) { await {call}; } })){w}", Combinator: "WhenAll", AwaitDrops: false, AssertionsOnly: true), + new("Task.WhenAll of async lambdas that await in a finally", "Task.WhenAll(Enumerable.Range(0, 2).Select(async i => { try { GC.KeepAlive(i); } catch (Exception) { } finally { await {call}; } })){w}", Combinator: "WhenAll", AwaitDrops: false, AssertionsOnly: true), + new("Task.Run of an async lambda that catches and rethrows", "Task.Run(async () => { try { await {call}; } catch { throw; } }){w}", Combinator: "Run", AwaitDrops: false, AssertionsOnly: true), + new("Task.WhenAny of async lambdas that catch and rethrow", "Task.WhenAny(Enumerable.Range(0, 2).Select(async i => { try { await {call}; } catch { throw; } })){w}", Combinator: "WhenAny", AwaitDrops: true, AssertionsOnly: true), + new("Task.WhenAll of async lambdas that catch everything", "Task.WhenAll(Enumerable.Range(0, 2).Select(async i => { try { await {call}; } catch { } })){w}", Combinator: "WhenAll", Silent: true, AssertionsOnly: true), + new("Task.WhenAll of async lambdas that catch the assertion's exception", "Task.WhenAll(Enumerable.Range(0, 2).Select(async i => { try { await {call}; } catch (TypeKitAssertionException) { } })){w}", Combinator: "WhenAll", Silent: true, AssertionsOnly: true), + new("Task.WhenAll of async lambdas that catch with a true filter", "Task.WhenAll(Enumerable.Range(0, 2).Select(async i => { try { await {call}; } catch (Exception) when (true) { } })){w}", Combinator: "WhenAll", Silent: true, AssertionsOnly: true), + new("Task.WhenAll of async lambdas that catch with a filter that is no constant", "Task.WhenAll(Enumerable.Range(0, 2).Select(async i => { try { await {call}; } catch (Exception exception) when (exception.Data.Count == 0) { } })){w}", Combinator: "WhenAll", Silent: true, AssertionsOnly: true), + new("Task.WhenAll of async lambdas that rethrow only sometimes", "Task.WhenAll(Enumerable.Range(0, 2).Select(async i => { try { await {call}; } catch (Exception) { if (choose) throw; } })){w}", Combinator: "WhenAll", Silent: true, AssertionsOnly: true), + new("Task.WhenAll of async lambdas that return from the catch", "Task.WhenAll(Enumerable.Range(0, 2).Select(async i => { try { await {call}; } catch (Exception) { if (choose) return; throw; } })){w}", Combinator: "WhenAll", Silent: true, AssertionsOnly: true), + new("Task.WhenAll of async lambdas that catch in a later clause", "Task.WhenAll(Enumerable.Range(0, 2).Select(async i => { try { await {call}; } catch (OperationCanceledException) { throw; } catch (Exception) { } })){w}", Combinator: "WhenAll", Silent: true, AssertionsOnly: true), + new("Task.WhenAll of async lambdas that rethrow inside a try that catches", "Task.WhenAll(Enumerable.Range(0, 2).Select(async i => { try { try { await {call}; } catch { throw; } } catch (Exception) { } })){w}", Combinator: "WhenAll", Silent: true, AssertionsOnly: true), + new("Task.WhenAll of async lambdas that catch inside a try that rethrows", "Task.WhenAll(Enumerable.Range(0, 2).Select(async i => { try { try { await {call}; } catch (Exception) { } } catch { throw; } })){w}", Combinator: "WhenAll", Silent: true, AssertionsOnly: true), + new("Task.Run of an async lambda that catches", "Task.Run(async () => { try { await {call}; } catch (Exception) { } }){w}", Combinator: "Run", Silent: true, AssertionsOnly: true), + + new("Task.WaitAny of two calls", "Task.WaitAny([[{call}]], [[{call}]])", Value: FormValue.Index), + new("Task.WaitAny of held tasks", "Task.WaitAny(held!, held!)", Value: FormValue.Index, Silent: true), + new("Task.WaitAll of two calls", "Task.WaitAll([[{call}]], [[{call}]])", Value: FormValue.Nothing, AssertionsObserved: true), + new("Task.WaitAny of an array", "Task.WaitAny(new[] { [[{call}]], [[{call}]] })", Value: FormValue.Index), + new("Task.WaitAll of an array", "Task.WaitAll(new[] { [[{call}]], [[{call}]] })", Value: FormValue.Nothing, AssertionsObserved: true), + new("Task.WaitAll of a Task.WhenAll", "Task.WaitAll(Task.WhenAll([[{call}]], [[{call}]]))", Value: FormValue.Nothing, AssertionsObserved: true, AssertionsOnly: true), + // Task.WaitAll waits for the WhenAny as a plain Task, a task that never faults, reported where it converts. + new("Task.WaitAll of a Task.WhenAny", "Task.WaitAll([[Task.WhenAny({call}, {call})]])", Combinator: "WhenAny", Value: FormValue.Nothing, AssertionsOnly: true), + new("Task.WaitAny of a Task.WhenAll", "Task.WaitAny(Task.WhenAll([[{call}]], [[{call}]]))", Value: FormValue.Index, AssertionsOnly: true), + + new("Task.WhenAll of a query", "Task.WhenAll(from i in Enumerable.Range(0, 2) select {call}){w}", Combinator: "WhenAll", AwaitDrops: false), + new("Task.WhenAll of a list", "Task.WhenAll(Enumerable.Range(0, 2).Select(i => {call}).ToList()){w}", Combinator: "WhenAll", AwaitDrops: false), + new("Task.WhenEach of two calls", "Task.WhenEach({call}, {call})", Combinator: "WhenEach", Value: FormValue.AsyncSequence), + new("Task.WhenEach of held tasks", "Task.WhenEach(held!, held!)", Combinator: "WhenEach", Value: FormValue.AsyncSequence, Silent: true), + new("a Select of the calls", "Enumerable.Range(0, 2).Select(i => {call})", Combinator: "Select", Value: FormValue.Sequence, Lazy: true), + ]; + + public static IReadOnlyList Uses { get; } = + [ + new("dropped as a statement", "<<{e}>>;", Drops: true, IsStatement: true), + new("dropped as a lambda's body", "Action dropped = () => <<{e}>>;", Drops: true, IsStatement: true), + new("awaited", "<>;", Drops: null, Async: true), + new("converted to a plain Task", "Func converted = () => <<{e}>>;", Drops: null, NeedsTask: true), + new("waited for with Wait()", "<<{e}.Wait()>>;", Drops: null, NeedsTask: true, Chains: true), + new("the body of a method that returns nothing", "void Check() => <<{e}>>;", Drops: true, IsStatement: true), + new("drained by a foreach that never reads it", "foreach (var each in <<{e}>>) { }", Drops: true, Loops: FormValue.Sequence), + new("read by a foreach", "foreach (var each in {e}) { GC.KeepAlive(each); }", Drops: false, Loops: FormValue.Sequence), + new("drained by an await foreach that never reads it", "await foreach (var each in <<{e}>>) { }", Drops: true, Async: true, Loops: FormValue.AsyncSequence), + new("read by an await foreach", "await foreach (var each in {e}) { GC.KeepAlive(each); }", Drops: false, Async: true, Loops: FormValue.AsyncSequence), + new("discarded with _ =", "_ = {e};", Drops: false), + new("held in a local", "var kept = {e};", Drops: false), + new("returned from a lambda", "Func returned = () => {e};", Drops: false), + ]; + + /// + /// A file whose two run methods use in , through every + /// pass-through, in every use, and the spans a diagnostic is expected at, by markup location. One file per + /// form keeps a failure to the form, and the verifier's matching, which recurses once per diagnostic, + /// within its stack. + /// + public static (string Source, IReadOnlyList<(string Outcome, string Method)> Expected) Use(PendingCall call, DropForm form) + { + var expected = new List<(string Outcome, string Method)>(); + var body = new System.Text.StringBuilder(); + var awaited = new System.Text.StringBuilder(); + + foreach (var passThrough in PassThroughs) + foreach (var use in Uses) + { + // A statement that does not compile is no case. + if (form.NeedsPassThrough && passThrough.Suffix.Length == 0) continue; + + // await x?.M().ConfigureAwait(false) awaits a nullable awaiter, which has no GetAwaiter, and await x?.M() + // a task that may be null (CS8602); x?.M().Wait() waits inside the ?.. + if ((use.Async || use.Chains) && form.Expression.Contains("{nc}", StringComparison.Ordinal)) continue; + if (use.NeedsTask && !passThrough.KeepsTask) continue; + + // An index, nothing or a sequence takes no await, no pass-through and no conversion, nothing is no + // value, and a loop reads only the sequence it was written for. + if (use.Loops is { } loops ? loops != form.Value : form.Value != FormValue.Task && (use.Async || use.NeedsTask)) continue; + if (form.Value != FormValue.Task && passThrough.Suffix.Length > 0) continue; + if (form.Value == FormValue.Nothing && !use.IsStatement) continue; + + // Task.WaitAny and Task.WaitAll lose or keep the tasks at the call, whatever the statement does next. + var reported = !form.Silent + && !(form.AssertionsObserved && call.IsAssertion) + && !(form.Lazy && use.Loops is null) + && (form.Value is FormValue.Index or FormValue.Nothing || (use.Drops ?? (call.AwaitDrops || (form.AwaitDrops ?? false)))); + var expression = form.Expression + .Replace("{call}", call.Call) + .Replace("{static}", call.Static) + .Replace("{nc}", call.NullConditional) + .Replace("{group}", call.Group) + .Replace("{w}", passThrough.Suffix); + var statement = use.Statement.Replace("{e}", expression); + + var message = form.Combinator switch + { + null => (call.Outcome, call.Method), + "WhenEach" or "Select" => ($"sequence of {(call.IsAssertion ? "Task" : call.Outcome)}s", form.Combinator), + var all when call.IsAssertion => ("Task", all), + "WhenAll" => ($"collection of {call.Outcome}s", "WhenAll"), + var any => (call.Outcome, any), + }; + + statement = Mark(statement, "<<", ">>", reported && !form.IsBranch, message, expected); + statement = Mark(statement, "[[", "]]", reported, message, expected); + + // A task the caller holds, declared where the form names it. + var held = expression.Contains("held", StringComparison.Ordinal) ? $"{call.Returns}? held = choose ? null : {call.Call};\n " : ""; + + (use.Async ? awaited : body).Append($" {{ // {form.Name}, {passThrough.Name}, {use.Name}\n ") + .Append(held) + .Append(statement) + .Append("\n }\n"); + } + + var source = + $$""" + #nullable enable + using System; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using CodoMetis.TypeKit; + using CodoMetis.TypeKit.Testing; + + public static class Use + { + public static void Run({{Parameters}}) + { + {{body}} + } + + public static async Task RunAsync({{Parameters}}) + { + {{awaited}} + } + } + """; + + return (source, expected); + } + + /// + /// Whether can be written in : a call has no static form, a + /// ValueTask no ?., an assertion with arguments no method group, and a Result stays out of + /// a form written for assertions. + /// + public static bool Applies(DropForm form, PendingCall call) => + !(form.AssertionsOnly && !call.IsAssertion) + && !(form.Expression.Contains("{group}", StringComparison.Ordinal) && call.Group is null) + && !(form.Expression.Contains("{static}", StringComparison.Ordinal) && call.Static is null) + && !(form.Expression.Contains("{nc}", StringComparison.Ordinal) && call.NullConditional is null); + + /// Each span between and becomes the next markup location, or plain text where nothing is reported. + private static string Mark(string text, string open, string close, bool reported, (string Outcome, string Method) message, List<(string Outcome, string Method)> expected) + { + var marked = new System.Text.StringBuilder(); + var at = 0; + + for (var start = text.IndexOf(open, StringComparison.Ordinal); start >= 0; start = text.IndexOf(open, at, StringComparison.Ordinal)) + { + var end = text.IndexOf(close, start, StringComparison.Ordinal); + marked.Append(text, at, start - at).Append(reported ? $"{{|#{expected.Count}:" : "").Append(text, start + open.Length, end - start - open.Length).Append(reported ? "|}" : ""); + if (reported) expected.Add(message); + at = end + close.Length; + } + + return marked.Append(text, at, text.Length - at).ToString(); + } + + /// An assertion, on a Task of what it checks and on a ValueTask. + private static IEnumerable Assertion(string method, string @class, string receiver, string arguments, string returns, string? kind = null) + { + var name = kind is null ? method : $"{kind} {method}"; + var tail = arguments.Length == 0 ? "" : ", " + arguments; + + string? Group(string held) => arguments.Length == 0 ? $"new[] {{ {held}, {held} }}.Select({@class}.{method})" : null; + + yield return new($"{name} on a Task", $"{receiver}.{method}({arguments})", $"{@class}.{method}({receiver}{tail})", $"{receiver}?.{method}({arguments})", returns, method, "Task", AwaitDrops: false, Group: Group(receiver)); + yield return new($"{name} on a ValueTask", $"{receiver}Value.{method}({arguments})", $"{@class}.{method}({receiver}Value{tail})", null, returns, method, "Task", AwaitDrops: false, Group: Group(receiver + "Value")); + } +} diff --git a/test/CodoMetis.TypeKit.Analyzers.Tests/Variants/QueryDelegateShapes.cs b/test/CodoMetis.TypeKit.Analyzers.Tests/Variants/QueryDelegateShapes.cs new file mode 100644 index 00000000..1c27bacd --- /dev/null +++ b/test/CodoMetis.TypeKit.Analyzers.Tests/Variants/QueryDelegateShapes.cs @@ -0,0 +1,229 @@ +namespace CodoMetis.TypeKit.Analyzers.Tests.Variants; + +/// +/// What a call to a TypeKit lookup is made on: how the receiver is typed, and how the call is written. +/// +/// What the receiver is, as a test case names it. +/// The run method's parameter that holds it, {A} the element type. +/// +/// The call as written: {Class} is Option or Result, {M} the method, +/// {Args} its arguments, and [[ ]] enclose the invocation a diagnostic reports. +/// +/// Whether the receiver is an IQueryable<T> before the compiler converts it. +/// The run method's type parameters, empty for none. +/// Their constraint clauses. +public sealed record QueryReceiver(string Name, string Parameter, string Call, bool IsQuery, string TypeParameters = "", string Where = "") +{ + public override string ToString() => Name; +} + +/// What a condition converts to. +public enum Converts +{ + /// A Func alone, so a query's overload cannot take it. + ToDelegate, + + /// A Func or an expression: a lambda, which binds to the query's overload where there is one. + ToEither, + + /// An expression alone, which a sequence's overload cannot take. + ToExpression, +} + +/// A condition passed to a lookup, written in the run method's body. +/// What the condition is, as a test case names it. +/// The argument as written; the members it names are . +/// What it converts to. +public sealed record QueryCondition(string Name, string Argument, Converts Converts) +{ + public bool IsDelegate => Converts == Converts.ToDelegate; + + public override string ToString() => Name; +} + +/// One of the lookups that has an overload on a sequence, taking a Func, and one on a query. +/// The method. +/// The static class that declares it. +/// The arguments after the condition. +/// +/// Whether its query overload compiles: LastOrNone's and LastOrError's refuse to, so a +/// lambda at the call on a query is no case. +/// +public sealed record QueryLookup(string Name, string Class, string After, bool HasQueryOverload) +{ + public override string ToString() => Name; +} + +/// +/// The variant catalogue for CMTK0009's query condition (design/adr/0095): a lookup called on a query +/// with a condition that is a delegate binds to its sequence overload, and reads every row. +/// +/// +/// +/// Three axes. The receiver: how a query can be typed (an interface, one deriving from it, a class +/// implementing it, a type parameter constrained to it, EF Core's DbSet in its own test) and the +/// call shapes of that move the receiver (a null-conditional call, the +/// lowered static form, a call in a lambda), against sequences that are no query. The condition: every +/// way C# writes a delegate, against the forms that convert to an expression. The element type is +/// . +/// +/// +/// A receiver typed as a sequence is no query to the rule, even where it holds one: the call cannot +/// bind otherwise, and AsEnumerable() or a cast says the full read is meant. +/// +/// +public static class QueryDelegateShapes +{ + /// The members the conditions name, declared in the class that holds the run method. + public const string Members = + """ + private static Func<{A}, bool> field = x => true; + private static Func<{A}, bool> Property => x => true; + private static readonly Expression> expressionField = x => true; + private static bool Test({A} x) => true; + private bool InstanceTest({A} x) => true; + private static Func<{A}, bool> Make() => x => true; + """; + + /// The locals the conditions name, declared at the top of the run method. + public const string Locals = + """ + Func<{A}, bool> local = x => true; + Expression> expressionLocal = x => true; + static bool LocalTest({A} x) => true; + """; + + /// The declarations the receivers name, a file of their own. + public const string Declarations = + """ + using System.Collections; + using System.Linq; + using System.Linq.Expressions; + + public abstract class Table : IQueryable + { + public abstract Type ElementType { get; } + public abstract Expression Expression { get; } + public abstract IQueryProvider Provider { get; } + public abstract IEnumerator GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + public interface IRepository : IQueryable { } + + """; + + public static IReadOnlyList Lookups { get; } = + [ + new("FirstOrNone", "Option", "", HasQueryOverload: true), + new("SingleOrNone", "Option", "", HasQueryOverload: true), + new("FirstOrError", "Result", ", \"none\"", HasQueryOverload: true), + new("LastOrNone", "Option", "", HasQueryOverload: false), + new("LastOrError", "Result", ", \"none\"", HasQueryOverload: false), + ]; + + private const string Instance = "_ = [[source.{M}({Args})]];"; + + public static IReadOnlyList Receivers { get; } = + [ + // A query, as it can be typed. + new("an IQueryable", "IQueryable<{A}> source", Instance, IsQuery: true), + new("an IOrderedQueryable", "IOrderedQueryable<{A}> source", Instance, IsQuery: true), + new("an EnumerableQuery, a class from metadata", "EnumerableQuery<{A}> source", Instance, IsQuery: true), + new("a class that implements IQueryable", "Table<{A}> source", Instance, IsQuery: true), + new("an interface that derives from IQueryable", "IRepository<{A}> source", Instance, IsQuery: true), + new("a type parameter constrained to IQueryable", "TQuery source", Instance, IsQuery: true, "", "where TQuery : IQueryable<{A}>"), + new("a type parameter bound by one constrained to IQueryable", "TQuery source", Instance, IsQuery: true, "", "where TBase : IQueryable<{A}> where TQuery : TBase"), + + // A query, as the call is written. + new("a composed query", "IQueryable<{A}> source", "_ = [[source.Where(x => true).{M}({Args})]];", IsQuery: true), + new("a list made a query", "List<{A}> source", "_ = [[source.AsQueryable().{M}({Args})]];", IsQuery: true), + new("a parenthesized receiver", "IQueryable<{A}> source", "_ = [[(source).{M}({Args})]];", IsQuery: true), + new("a null-conditional call", "IQueryable<{A}> source", "_ = source?[[.{M}({Args})]];", IsQuery: true), + new("the lowered static form", "IQueryable<{A}> source", "_ = [[{Class}.{M}(source, {Args})]];", IsQuery: true), + new("a call in a lambda", "IQueryable<{A}> source", "Action act = () => _ = [[source.{M}({Args})]];", IsQuery: true), + new("a query held in a local", "IQueryable<{A}> source", "var held = source.Where(x => true);\n_ = [[held.{M}({Args})]];", IsQuery: true), + + // No query. + new("an IEnumerable", "IEnumerable<{A}> source", Instance, IsQuery: false), + new("a List", "List<{A}> source", Instance, IsQuery: false), + new("an array", "{A}[] source", Instance, IsQuery: false), + new("a type parameter constrained to IEnumerable", "TSequence source", Instance, IsQuery: false, "", "where TSequence : IEnumerable<{A}>"), + new("a query read through AsEnumerable()", "IQueryable<{A}> source", "_ = [[source.AsEnumerable().{M}({Args})]];", IsQuery: false), + new("a query cast to IEnumerable", "IQueryable<{A}> source", "_ = [[((IEnumerable<{A}>)source).{M}({Args})]];", IsQuery: false), + new("a query held as IEnumerable", "IQueryable<{A}> source", "IEnumerable<{A}> held = source;\n_ = [[held.{M}({Args})]];", IsQuery: false), + new("a sequence in the lowered static form", "IEnumerable<{A}> source", "_ = [[{Class}.{M}(source, {Args})]];", IsQuery: false), + ]; + + public static IReadOnlyList Conditions { get; } = + [ + new("a Func local", "local", Converts.ToDelegate), + new("a Func parameter", "parameter", Converts.ToDelegate), + new("a Func field", "field", Converts.ToDelegate), + new("a Func property", "Property", Converts.ToDelegate), + new("a static method group", "Test", Converts.ToDelegate), + new("an instance method group", "InstanceTest", Converts.ToDelegate), + new("a local function's method group", "LocalTest", Converts.ToDelegate), + new("an anonymous method", "delegate ({A} x) { return true; }", Converts.ToDelegate), + new("a call that returns a Func", "Make()", Converts.ToDelegate), + new("a conditional of two Funcs", "flag ? local : field", Converts.ToDelegate), + new("a lambda cast to a Func", "(Func<{A}, bool>)(x => true)", Converts.ToDelegate), + new("an expression compiled", "expressionField.Compile()", Converts.ToDelegate), + + new("a lambda at the call", "x => true", Converts.ToEither), + new("a static lambda", "static x => true", Converts.ToEither), + new("a lambda with a typed parameter", "({A} x) => true", Converts.ToEither), + new("an expression local", "expressionLocal", Converts.ToExpression), + new("an expression field", "expressionField", Converts.ToExpression), + ]; + + /// + /// A file whose run method calls every lookup on with every condition, over + /// elements written , and the calls a diagnostic is expected at, by markup location. + /// + public static (string Source, IReadOnlyList<(int Location, QueryLookup Lookup, QueryCondition Condition)> Expected) Use( + QueryReceiver receiver, string element, string parameters = "", string where = "") + { + var expected = new List<(int, QueryLookup, QueryCondition)>(); + var body = new System.Text.StringBuilder(); + + foreach (var lookup in Lookups) + foreach (var condition in Conditions) + { + // A call that does not compile is no case: an expression on a sequence, and on a query a lambda + // or an expression given to a lookup whose query overload refuses to compile. + if (condition.Converts == Converts.ToExpression && !receiver.IsQuery) continue; + if (!condition.IsDelegate && receiver.IsQuery && !lookup.HasQueryOverload) continue; + + var reported = receiver.IsQuery && condition.IsDelegate; + var call = receiver.Call.Replace("{Class}", lookup.Class).Replace("{M}", lookup.Name).Replace("{Args}", condition.Argument + lookup.After); + + call = reported + ? call.Replace("[[", $"{{|#{expected.Count}:").Replace("]]", "|}") + : call.Replace("[[", "").Replace("]]", ""); + + if (reported) expected.Add((expected.Count, lookup, condition)); + + body.Append(" {\n ").Append(call.Replace("\n", "\n ")).Append("\n }\n"); + } + + var source = + $$""" + using System.Linq; + using System.Linq.Expressions; + + public sealed class Use{{parameters}} {{where}} + { + {{Members}} + + public void Run{{receiver.TypeParameters}}({{receiver.Parameter}}, Func<{A}, bool> parameter, bool flag) {{receiver.Where}} + { + {{Locals}} + {{body}} + } + } + """; + + return (source.Replace("{A}", element), expected); + } +} diff --git a/test/CodoMetis.TypeKit.AspNetCore.Tests/ComposedFaultResponseTests.cs b/test/CodoMetis.TypeKit.AspNetCore.Tests/ComposedFaultResponseTests.cs index ac859afb..d6c493da 100644 --- a/test/CodoMetis.TypeKit.AspNetCore.Tests/ComposedFaultResponseTests.cs +++ b/test/CodoMetis.TypeKit.AspNetCore.Tests/ComposedFaultResponseTests.cs @@ -224,6 +224,21 @@ public async Task Two_types_sharing_a_member_s_bare_name_are_a_warning_and_the_h logs.Warnings.ShouldHaveSingleItem().ShouldContain("HTTP: PUT /orders/{id:int} answers ComposedFaultResponseTests.OrderFault.NameTaken and ComposedFaultResponseTests.ClashingFault.NameTaken alike, as the fault \"NameTaken\""); } + /// A member whose name the mapping withholds writes no name, so it shares none with another type's (decision 111). + [Fact] + public async Task A_shared_name_one_type_withholds_is_no_warning() + { + var logs = new ListedLogs(); + await using var app = await StartAsync( + logs: logs, + clashWithheld: true, + endpoints: app => app.MapPut("/orders/{id:int}", Results, FaultProblem> (int id) => + id == 1 ? TypedResults.NoContent() : NameTakenAsIn20())); + + (await ProblemAsync(app, "PUT", "/orders/2", HttpStatusCode.Conflict))["fault"]!.GetValue().ShouldBe("NameTaken"); + logs.Warnings.ShouldBeEmpty(); + } + [Fact] public async Task Qualified_names_keep_a_shared_member_name_apart() { @@ -313,7 +328,8 @@ private static async Task StartAsync( Action? services = null, ILoggerProvider? logs = null, bool typeKit = true, - Action? openApi = null) + Action? openApi = null, + bool clashWithheld = false) { var builder = WebApplication.CreateBuilder(new WebApplicationOptions { EnvironmentName = Environments.Production }); builder.WebHost.UseTestServer(); @@ -350,7 +366,7 @@ private static async Task StartAsync( } builder.Services.AddFaultResponses(fault => fault switch { - ClashingFault.NameTaken => StatusCodes.Status409Conflict, + ClashingFault.NameTaken => clashWithheld ? FaultResponse.Status(StatusCodes.Status409Conflict).WithoutFaultName() : StatusCodes.Status409Conflict, ClashingFault.Other => StatusCodes.Status422UnprocessableEntity, _ => throw new ArgumentOutOfRangeException(nameof(fault)), }); diff --git a/test/CodoMetis.TypeKit.AspNetCore.Tests/DescribedParameterTests.cs b/test/CodoMetis.TypeKit.AspNetCore.Tests/DescribedParameterTests.cs index 05252090..9560df30 100644 --- a/test/CodoMetis.TypeKit.AspNetCore.Tests/DescribedParameterTests.cs +++ b/test/CodoMetis.TypeKit.AspNetCore.Tests/DescribedParameterTests.cs @@ -148,7 +148,7 @@ private static Task DocumentAsync(OpenApiSpecVersion version, strin if (key.Item2 == "inline-all") openApi.CreateSchemaReferenceId = _ => null; if (key.Described) openApi.AddTypeKit(); - // The README's way to inline value objects alone. + // A host's own way to inline value objects alone, which needs no AddTypeKit(). if (key.Item2 == "inline-value-objects") openApi.CreateSchemaReferenceId = type => (Nullable.GetUnderlyingType(type.Type) ?? type.Type).GetCustomAttribute() is not null diff --git a/test/CodoMetis.TypeKit.AspNetCore.Tests/FaultResponsePartsTests.cs b/test/CodoMetis.TypeKit.AspNetCore.Tests/FaultResponsePartsTests.cs index 0ac7324a..edc6ec85 100644 --- a/test/CodoMetis.TypeKit.AspNetCore.Tests/FaultResponsePartsTests.cs +++ b/test/CodoMetis.TypeKit.AspNetCore.Tests/FaultResponsePartsTests.cs @@ -50,6 +50,18 @@ private static Result> ArchiveWithRule(int id) => private static Result> ArchiveWithDetail(int id) => id == 1 ? Result.Success() : Result.Error(Fault.Of(OrderFault.NameTaken, "Order 7 is called that.")); + /// What failed underneath a refusal, which names a host the client must not learn of. + private static readonly TimeoutException Underneath = new("db-host-7 timed out after 30 s"); + + private static Result> RenameWithCause(int id) => + id == 1 ? new Order(1, "renamed") : Result.Error(Fault.Of(OrderFault.NameTaken, "Order 7 is called that.", Underneath)); + + private static Result> ArchiveWithCause(int id) => + id == 1 ? Result.Success() : Result.Error(Fault.Of(OrderFault.NameTaken, "Order 7 is called that.", Underneath)); + + private static Result> RenameComposedWithCause(int id) => + id == 1 ? new Order(1, "renamed") : Result.Error(Fault.Of(OrderFault.NameTaken, "Order 7 is called that.", Underneath)); + private static Result Find(int id) => id switch { 1 => new Order(1, "book"), @@ -183,6 +195,45 @@ public void ToHttpResult_resolves_for_an_enum_a_fault_and_a_closed_set_without_t Rename(1).ToHttpResult(order => TypedResults.Created($"/orders/{order.Id}", order)).Result.ShouldBeOfType>(); } + /// + /// The Result page: ToHttpResult() accepts an enum, a closed set and a fault of one or several types, + /// never a Result<T, Exception>, whose exception is for the code that called. Every overload's + /// error is a Fault, or a type parameter whose constraints an exception does not meet. + /// + [Fact] + public void ToHttpResult_accepts_no_result_whose_error_is_an_exception() + { + var overloads = new[] { typeof(TypeKitHttpResultExtensions), typeof(TypeKitClosedSetHttpResultExtensions) } + .SelectMany(type => type.GetMethods(BindingFlags.Public | BindingFlags.Static)) + .Where(method => method.Name == nameof(TypeKitHttpResultExtensions.ToHttpResult)) + .ToList(); + + overloads.Count.ShouldBeGreaterThanOrEqualTo(20); + overloads.Where(method => Admits(method.GetParameters()[0].ParameterType.GetGenericArguments()[^1], typeof(Exception))) + .Select(method => method.ToString()) + .ShouldBeEmpty(); + + static bool Admits(Type error, Type candidate) + { + if (!error.IsGenericParameter) return error.IsAssignableFrom(candidate); + if (error.GenericParameterAttributes.HasFlag(GenericParameterAttributes.NotNullableValueTypeConstraint) && !candidate.IsValueType) return false; + + try + { + return error.GetGenericParameterConstraints().All(constraint => Substituted(constraint, error, candidate).IsAssignableFrom(candidate)); + } + catch (ArgumentException) + { + return false; // the constraint cannot even be formed over the candidate + } + } + + static Type Substituted(Type constraint, Type parameter, Type candidate) => + constraint.IsGenericType && constraint.ContainsGenericParameters + ? constraint.GetGenericTypeDefinition().MakeGenericType([.. constraint.GetGenericArguments().Select(argument => argument == parameter ? candidate : argument)]) + : constraint; + } + // 4: a detail reaches the body only when set, and the default response is unchanged. [Fact] public async Task A_detail_reaches_the_problem_only_when_there_is_one() @@ -412,11 +463,67 @@ public async Task CustomizeProblemDetails_sees_the_rule() (await ProblemAsync(app, "/detailed/orders/2", HttpStatusCode.Conflict))["code"]!.GetValue().ShouldBe("-"); } + /// + /// A fault's cause reaches the host through the feature, for its log, through every overload over a + /// and over a fault of several types, and never the client: the problem + /// has the members the same fault without a cause writes, and names neither the exception's type nor + /// its message (decision 52). + /// + [Theory] + [InlineData("PUT", "/caused/orders/2")] // Result> + [InlineData("DELETE", "/caused/orders/2")] // Result> + [InlineData("POST", "/caused/orders/2/copies")] // Result>, with the endpoint's success + [InlineData("POST", "/caused/orders/2/archive")] // Result>, with the endpoint's success + [InlineData("PATCH", "/caused/orders/2")] // Result> + public async Task A_cause_reaches_the_host_through_the_feature_and_never_the_problem(string method, string path) + { + var causes = new List(); + await using var app = await StartAsync( + services: services => + { + Responses(services); + services.AddProblemDetails(options => options.CustomizeProblemDetails = context => + causes.Add(context.HttpContext.Features.Get()?.Cause)); + }, + endpoints: app => + { + app.MapPut("/caused/orders/{id:int}", (int id) => RenameWithCause(id).ToHttpResult()); + app.MapDelete("/caused/orders/{id:int}", (int id) => ArchiveWithCause(id).ToHttpResult()); + app.MapPost("/caused/orders/{id:int}/copies", (int id) => RenameWithCause(id).ToHttpResult(order => TypedResults.Created($"/orders/{order.Id}", order))); + app.MapPost("/caused/orders/{id:int}/archive", (int id) => ArchiveWithCause(id).ToHttpResult(() => TypedResults.Accepted("/jobs/7"))); + app.MapPatch("/caused/orders/{id:int}", (int id) => RenameComposedWithCause(id).ToHttpResult()); + }); + + using var request = new HttpRequestMessage(new HttpMethod(method), path); + using var response = await app.GetTestClient().SendAsync(request, TestContext.Current.CancellationToken); + var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + + response.StatusCode.ShouldBe(HttpStatusCode.Conflict, $"{method} {path}"); + causes.ShouldHaveSingleItem().ShouldBeSameAs(Underneath, $"{method} {path}"); + JsonNode.Parse(body)!.AsObject().Select(member => member.Key).Order().ToArray().ShouldBe(["detail", "fault", "status", "title", "traceId", "type"], customMessage: body); + body.ShouldNotContain(nameof(TimeoutException), Case.Insensitive); + body.ShouldNotContain("db-host-7", Case.Insensitive); + } + + /// The response an endpoint returns carries the cause as well, for a test or a filter that reads it before it is written. + [Fact] + public void The_problem_carries_the_cause_and_a_fault_without_one_none() + { + RenameWithCause(2).ToHttpResult().Result.ShouldBeOfType>().Cause.ShouldBeSameAs(Underneath); + RenameWithDetail(2).ToHttpResult().Result.ShouldBeOfType>().Cause.ShouldBeNull(); + RenameWithCause(2).Map(order => order).ToHttpResult().Result.ShouldBeOfType>().Rule.ShouldBeNull(); + } + /// The rule joined the feature with a default, so a host's or a test's own implementation still builds, and names none. [Fact] public void A_fault_feature_of_the_host_s_own_names_no_rule() => ((IFaultFeature)new HostFeature()).Rule.ShouldBeNull(); + /// The cause joined the feature with a default too. + [Fact] + public void A_fault_feature_of_the_host_s_own_has_no_cause() => + ((IFaultFeature)new HostFeature()).Cause.ShouldBeNull(); + private sealed class HostFeature : IFaultFeature { public Type FaultType => typeof(OrderFault); diff --git a/test/CodoMetis.TypeKit.AspNetCore.Tests/FaultSetDefaultTests.cs b/test/CodoMetis.TypeKit.AspNetCore.Tests/FaultSetDefaultTests.cs index 872f0ab9..0b4b551f 100644 --- a/test/CodoMetis.TypeKit.AspNetCore.Tests/FaultSetDefaultTests.cs +++ b/test/CodoMetis.TypeKit.AspNetCore.Tests/FaultSetDefaultTests.cs @@ -99,6 +99,24 @@ public async Task A_closed_set_that_is_no_fault_set_still_needs_a_mapping_of_its refusal.Message.ShouldStartWith("No HTTP status is mapped for the fault type ProbeStatus, which these endpoints answer with: HTTP: DELETE /status."); } + /// + /// The README's "Any closed set can be an error type": a set that is no fault set, mapped on its own, is answered + /// through ToHttpResult() with the status its mapping gives and its member's qualified name as the fault. + /// + [Fact] + public async Task A_closed_set_that_is_no_fault_set_is_answered_with_a_mapping_of_its_own() + { + await using var app = await StartAsync( + services => + { + services.AddFaultResponses(); // the fault sets the other endpoints answer with + services.AddFaultResponses(status => status == ProbeStatus.Open ? StatusCodes.Status409Conflict : StatusCodes.Status422UnprocessableEntity); + }, + app => app.MapPut("/status", () => Opened().ToHttpResult())); + + (await ProblemAsync(app, "/status", HttpStatusCode.Conflict))["fault"]!.GetValue().ShouldBe("ProbeStatus.Open"); + } + [Fact] public async Task A_member_of_no_kind_stops_the_host() { diff --git a/test/CodoMetis.TypeKit.AspNetCore.Tests/HostConfigurationTests.cs b/test/CodoMetis.TypeKit.AspNetCore.Tests/HostConfigurationTests.cs index 3d5360f3..41cc369c 100644 --- a/test/CodoMetis.TypeKit.AspNetCore.Tests/HostConfigurationTests.cs +++ b/test/CodoMetis.TypeKit.AspNetCore.Tests/HostConfigurationTests.cs @@ -120,7 +120,7 @@ public async Task A_container_another_transformer_already_described_is_left_alon } } - /// The predicate the package README shows, a nullable value object included. + /// A host's own predicate, a nullable value object included. [Fact] public async Task A_host_that_inlines_value_objects_gets_the_wrapped_types_schema_in_place() { @@ -239,12 +239,15 @@ private static async Task SourceGeneratedDocumentAsync(JsonSerializerC /// /// What the README's "Binding" says of a minimal API refusing a body: an empty 400 with /// AddProblemDetails() too, a generic problem only with UseStatusCodePages(), and the - /// input in neither (spikes/ValidationFaults). + /// input in neither (spikes/ValidationFaults). A value object inside a refuses alike, as + /// the merge-patches page says. /// [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task A_minimal_API_answers_a_refused_body_with_a_bare_400(bool statusCodePages) + [InlineData(false, "/codes")] + [InlineData(true, "/codes")] + [InlineData(false, "/patches")] + [InlineData(true, "/patches")] + public async Task A_minimal_API_answers_a_refused_body_with_a_bare_400(bool statusCodePages, string path) { var builder = WebApplication.CreateBuilder(new WebApplicationOptions { EnvironmentName = Environments.Production }); builder.WebHost.UseTestServer(); @@ -254,10 +257,11 @@ public async Task A_minimal_API_answers_a_refused_body_with_a_bare_400(bool stat await using var app = builder.Build(); if (statusCodePages) app.UseStatusCodePages(); app.MapPost("/codes", (CodeHolder holder) => holder.Code.Value); + app.MapPost("/patches", (CodePatch patch) => patch.Code.State.ToString()); // the merge-patches page: a patch refuses as the value object does await app.StartAsync(TestContext.Current.CancellationToken); using var response = await app.GetTestClient().PostAsync( - "/codes", new StringContent("""{"code":"lower"}""", System.Text.Encoding.UTF8, "application/json"), TestContext.Current.CancellationToken); + path, new StringContent("""{"code":"lower"}""", System.Text.Encoding.UTF8, "application/json"), TestContext.Current.CancellationToken); var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); response.StatusCode.ShouldBe(HttpStatusCode.BadRequest); @@ -276,6 +280,8 @@ public async Task A_minimal_API_answers_a_refused_body_with_a_bare_400(bool stat private sealed record CodeHolder(ProbeCode Code); + private sealed record CodePatch(Patch Code); + private sealed class InstantSchemaTransformer : IOpenApiSchemaTransformer { public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken) diff --git a/test/CodoMetis.TypeKit.AspNetCore.Tests/InlineValueObjectsSettingTests.cs b/test/CodoMetis.TypeKit.AspNetCore.Tests/InlineValueObjectsSettingTests.cs new file mode 100644 index 00000000..d4fd05a7 --- /dev/null +++ b/test/CodoMetis.TypeKit.AspNetCore.Tests/InlineValueObjectsSettingTests.cs @@ -0,0 +1,110 @@ +using System.Reflection; +using CodoMetis.TypeKit.AspNetCore.Tests.Collisions; +using CodoMetis.TypeKit.CompilerServices; +using CodoMetis.TypeKit.Generators.Probes; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OpenApi; + +namespace CodoMetis.TypeKit.AspNetCore.Tests; + +/// +/// writes every value object in place and names no component +/// after one, and the document without it is the one AddTypeKit() writes (decision 115). The schemas it writes +/// are held to the wrapped type's by SchemaTests and NullablePositionTests, under both settings. +/// +public sealed class InlineValueObjectsSettingTests +{ + private static readonly OpenApiSpecVersion[] Versions = [OpenApiSpecVersion.OpenApi3_1, OpenApiSpecVersion.OpenApi3_0, OpenApiSpecVersion.OpenApi2_0]; + + public static TheoryData EveryVersion() => [.. Versions]; + + /// The names a value object of these assemblies would have as a component, after its nesting chain. + private static readonly HashSet ValueObjectNames = + [ + .. new[] { typeof(ProbeId).Assembly, typeof(InlineValueObjectsSettingTests).Assembly } + .SelectMany(assembly => assembly.GetTypes()) + .Where(type => type.GetCustomAttribute(inherit: false) is { } valueObject && valueObject.ValueObjectType == type) + .Select(Chain), + ]; + + [Theory] + [MemberData(nameof(EveryVersion))] + public async Task No_component_is_a_value_object(OpenApiSpecVersion version) + { + var inlined = await ProbeHost.DocumentAsync(openApi => openApi.AddTypeKit(typeKit => typeKit.InlineValueObjects = true), version: version); + var components = await ProbeHost.DefaultDocumentAsync(version); + + // The control: without the setting, the same endpoints name components after value objects. + components.Components.Select(component => component.Key).Where(ValueObjectNames.Contains).ShouldNotBeEmpty(); + + inlined.Components.Select(component => component.Key).Where(ValueObjectNames.Contains).ShouldBeEmpty(); + var json = inlined.Root.ToJsonString(); + foreach (var name in ValueObjectNames) json.ShouldNotContain($"/{name}\"", customMessage: $"a reference to '{name}' is left"); + } + + [Theory] + [MemberData(nameof(EveryVersion))] + public async Task Without_the_setting_the_document_is_the_one_AddTypeKit_writes(OpenApiSpecVersion version) + { + var plain = await ProbeHost.DocumentAsync(openApi => openApi.AddTypeKit(), version: version); + var unset = await ProbeHost.DocumentAsync(openApi => openApi.AddTypeKit(_ => { }), version: version); + var off = await ProbeHost.DocumentAsync(openApi => openApi.AddTypeKit(typeKit => typeKit.InlineValueObjects = false), version: version); + + unset.Root.ShouldDescribeTheSameAs(plain.Root, "no setting chosen"); + off.Root.ShouldDescribeTheSameAs(plain.Root, "InlineValueObjects = false"); + } + + /// A second, plain AddTypeKit() asks the first for the name, which is none. + [Fact] + public async Task A_later_AddTypeKit_keeps_them_inlined() + { + var once = await ProbeHost.DocumentAsync(openApi => openApi.AddTypeKit(typeKit => typeKit.InlineValueObjects = true)); + var twice = await ProbeHost.DocumentAsync(openApi => openApi.AddTypeKit(typeKit => typeKit.InlineValueObjects = true).AddTypeKit()); + + twice.Root.ShouldDescribeTheSameAs(once.Root); + } + + /// A name the host's own delegate gives a value object does not make it a component; every other type keeps its name. + [Fact] + public async Task The_host_s_own_names_are_kept_for_every_other_type() + { + var document = await ProbeHost.DocumentAsync(openApi => + { + openApi.CreateSchemaReferenceId = type => + type.Type == typeof(ProbeDocument) ? "Probes" : type.Type == typeof(ProbeId) ? "Identifier" : OpenApiOptions.CreateDefaultSchemaReferenceId(type); + openApi.AddTypeKit(typeKit => typeKit.InlineValueObjects = true); + }); + + document.Components.ContainsKey("Identifier").ShouldBeFalse("the host's name made the value object a component"); + document.Property("Probes", "id").ShouldDescribeTheSameAs(new JsonObject { ["type"] = "string", ["format"] = "uuid" }); + } + + /// + /// Decision 55 refuses a name two types claim. Inlined, a value object claims none, so a nested one and a top-level one + /// of its chain's name are two schemas in place; a closed set still claims its name, and is still refused. + /// + [Fact] + public async Task Value_objects_claim_no_name_and_closed_sets_still_do() + { + var carts = await ProbeHost.DocumentAsync( + openApi => openApi.AddTypeKit(typeKit => typeKit.InlineValueObjects = true), + endpoints: app => app.MapPost("/carts", int (Carts carts) => 0)); + + carts.Property(nameof(Carts), "nested").ShouldDescribeTheSameAs(new JsonObject { ["type"] = "string", ["format"] = "uuid" }); + carts.Property(nameof(Carts), "topLevel")["format"]!.GetValue().ShouldBe("int32"); + + var refusal = await Should.ThrowAsync(() => ProbeHost.DocumentAsync( + openApi => openApi.AddTypeKit(typeKit => typeKit.InlineValueObjects = true), + endpoints: app => app.MapPost("/orders", int (OrderStatuses statuses) => 0))); + refusal.Message.ShouldContain("'OrderStatus'"); + } + + [Fact] + public void A_null_configuration_is_refused() => + Should.Throw(() => new OpenApiOptions().AddTypeKit(configure: null!)).ParamName.ShouldBe("configure"); + + private static string Chain(Type type) => type.DeclaringType is { } declaring ? Chain(declaring) + type.Name : type.Name; +} diff --git a/test/CodoMetis.TypeKit.AspNetCore.Tests/InlinedValueObjectTests.cs b/test/CodoMetis.TypeKit.AspNetCore.Tests/InlinedValueObjectTests.cs index 584be04f..4a59196a 100644 --- a/test/CodoMetis.TypeKit.AspNetCore.Tests/InlinedValueObjectTests.cs +++ b/test/CodoMetis.TypeKit.AspNetCore.Tests/InlinedValueObjectTests.cs @@ -9,7 +9,7 @@ namespace CodoMetis.TypeKit.AspNetCore.Tests; /// -/// A host that inlines value objects (the README's CreateSchemaReferenceId returning ) +/// A host that inlines value objects (its own CreateSchemaReferenceId returning ) /// gets, in every position, exactly what ASP.NET publishes for the wrapped type there: a nullable value object /// admits null as a nullable int, string, Guid or enum does, and a non-nullable one does not. /// @@ -119,7 +119,7 @@ public async Task Adding_it_twice_to_an_inlining_host_publishes_the_same_documen _ => schema?.DeepClone(), }; - /// The README's predicate. + /// A host's own predicate, as TypeKitOpenApiOptions.InlineValueObjects does (NullablePositionTests holds the setting). private static void InlineValueObjects(OpenApiOptions openApi) { openApi.AddTypeKit(); diff --git a/test/CodoMetis.TypeKit.AspNetCore.Tests/NullablePositionTests.cs b/test/CodoMetis.TypeKit.AspNetCore.Tests/NullablePositionTests.cs index 6e550fae..67ed9975 100644 --- a/test/CodoMetis.TypeKit.AspNetCore.Tests/NullablePositionTests.cs +++ b/test/CodoMetis.TypeKit.AspNetCore.Tests/NullablePositionTests.cs @@ -1,8 +1,6 @@ using System.Collections.Concurrent; -using System.Reflection; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; -using CodoMetis.TypeKit.CompilerServices; using CodoMetis.TypeKit.Generators.Probes; using CodoMetis.TypeKit.ValueObjects; using Microsoft.AspNetCore.Builder; @@ -21,7 +19,7 @@ namespace CodoMetis.TypeKit.AspNetCore.Tests; /// operation (body, query, header, form, route, response, a list's elements, a dictionary's values, a query or header /// array's items, a property of a non-abstract polymorphic base's derived branch and of its own), in MVC /// and in minimal APIs, with enums written by number or by name, and with everything inlined, value objects alone -/// inlined or components, exactly the schema ASP.NET publishes for the wrapped type in the same position at the same +/// inlined () or components, in OpenAPI 3.1, 3.0 and 2.0, exactly the schema ASP.NET publishes for the wrapped type in the same position at the same /// nullability. /// /// @@ -113,14 +111,9 @@ public async Task A_value_object_is_described_as_its_wrapped_type_in_the_same_po openApi => { if (key.Item2 == "inline-all") openApi.CreateSchemaReferenceId = _ => null; - openApi.AddTypeKit(); - - // The README's way to inline value objects alone: the wrapped enum stays ASP.NET's component. - if (key.Item2 == "inline-value-objects") - openApi.CreateSchemaReferenceId = type => - (Nullable.GetUnderlyingType(type.Type) ?? type.Type).GetCustomAttribute() is not null - ? null - : OpenApiOptions.CreateDefaultSchemaReferenceId(type); + + // The setting that inlines value objects alone (decision 115): the wrapped enum stays ASP.NET's component. + openApi.AddTypeKit(typeKit => typeKit.InlineValueObjects = key.Item2 == "inline-value-objects"); }, json: json => { diff --git a/test/CodoMetis.TypeKit.AspNetCore.Tests/PatchSchemaTests.cs b/test/CodoMetis.TypeKit.AspNetCore.Tests/PatchSchemaTests.cs index f69470fc..7fe4c2f2 100644 --- a/test/CodoMetis.TypeKit.AspNetCore.Tests/PatchSchemaTests.cs +++ b/test/CodoMetis.TypeKit.AspNetCore.Tests/PatchSchemaTests.cs @@ -28,6 +28,9 @@ public sealed record StrictPatch(Patch Name) public sealed record Control(string? Name, int Quantity, ProbeCode Code, ProbeId Id, Lines? Lines); + /// A patch parameter with = default, which the merge-patches page says breaks the document. + public sealed record DefaultedPatch(Patch Quantity = default); + [JsonConverter(typeof(JsonStringEnumConverter))] public enum Colour { Red, Green } @@ -234,6 +237,20 @@ static IEnumerable Required(OpenApiJson document) => document.Component(nameof(StrictPatch))["required"]?.AsArray().Select(node => node!.GetValue()) ?? []; } + /// + /// The merge-patches page's warning: = default on a patch parameter satisfies the serializer, but + /// System.Text.Json's schema exporter throws on a struct parameter whose default is default, so the + /// endpoint's document fails. Pinned, so the page is corrected when the exporter learns to describe it. + /// + [Fact] + public async Task A_patch_parameter_defaulted_with_default_breaks_the_document() + { + var refusal = await Should.ThrowAsync(() => ProbeHost.DocumentAsync( + openApi => openApi.AddTypeKit(), endpoints: app => app.MapPatch("/defaulted", int (DefaultedPatch patch) => 0))); + + refusal.StackTrace.ShouldNotBeNull().ShouldContain(nameof(System.Text.Json.Schema.JsonSchemaExporter), customMessage: "the exporter throws, not the converter"); + } + private static bool AdmitsNull(JsonNode schema, OpenApiSpecVersion version) => version == OpenApiSpecVersion.OpenApi3_1 ? schema["type"] is JsonArray types && types.Any(type => type!.GetValue() == "null") diff --git a/test/CodoMetis.TypeKit.AspNetCore.Tests/ProbeApi.cs b/test/CodoMetis.TypeKit.AspNetCore.Tests/ProbeApi.cs index 061030ca..fe723b1a 100644 --- a/test/CodoMetis.TypeKit.AspNetCore.Tests/ProbeApi.cs +++ b/test/CodoMetis.TypeKit.AspNetCore.Tests/ProbeApi.cs @@ -39,7 +39,8 @@ public sealed record ProbeDocument( ProbeName Name, ProbeName? MaybeName, ProbeId? MaybeId, - ProbeWeekday? MaybeWeekday); + ProbeWeekday? MaybeWeekday, + Dictionary CountsById); /// The same names as , with the wrapped types. public sealed record ControlDocument( @@ -68,7 +69,13 @@ public sealed record ControlDocument( string Name, string? MaybeName, Guid? MaybeId, - DayOfWeek? MaybeWeekday); + DayOfWeek? MaybeWeekday, + Dictionary CountsById, + Guid[] IdArray, + IReadOnlyList ReadOnlyIds, + HashSet IdSet, + Dictionary> NestedIds, + List> ListOfLists); public sealed record ProbeSearch(ProbeId Customer, ProbeCount? Limit); diff --git a/test/CodoMetis.TypeKit.AspNetCore.Tests/ProbeHost.cs b/test/CodoMetis.TypeKit.AspNetCore.Tests/ProbeHost.cs index 35fb13f5..4f3d96af 100644 --- a/test/CodoMetis.TypeKit.AspNetCore.Tests/ProbeHost.cs +++ b/test/CodoMetis.TypeKit.AspNetCore.Tests/ProbeHost.cs @@ -145,7 +145,8 @@ internal sealed class OpenApiJson(JsonNode root) { public JsonNode Root => root; - public JsonObject Components => root["components"]?["schemas"]?.AsObject() ?? []; + /// The components, or under OpenAPI 2.0 the definitions. + public JsonObject Components => (root["components"]?["schemas"] ?? root["definitions"])?.AsObject() ?? []; public JsonNode Component(string name) => Components[name] ?? throw new InvalidOperationException($"No component '{name}'. Components: {string.Join(", ", Components.Select(c => c.Key))}"); @@ -157,6 +158,15 @@ public JsonNode Property(string component, string property) => public JsonNode Resolve(JsonNode schema) => schema["$ref"] is { } reference ? Component(reference.GetValue().Split('/')[^1]) : schema; + /// The schema with every reference replaced by what it refers to. + public JsonNode? Expand(JsonNode? schema) => schema switch + { + JsonObject reference when reference["$ref"] is { } id => Expand(Component(id.GetValue().Split('/')[^1])), + JsonObject keywords => new JsonObject(keywords.Select(keyword => KeyValuePair.Create(keyword.Key, Expand(keyword.Value)))), + JsonArray items => new JsonArray([.. items.Select(Expand)]), + _ => schema?.DeepClone(), + }; + public JsonNode Parameter(string path, string method, string name) => Operation(path, method)["parameters"]?.AsArray().SingleOrDefault(p => p!["name"]!.GetValue() == name)?["schema"] ?? throw new InvalidOperationException($"{method} {path} has no parameter '{name}'."); diff --git a/test/CodoMetis.TypeKit.AspNetCore.Tests/SchemaTests.cs b/test/CodoMetis.TypeKit.AspNetCore.Tests/SchemaTests.cs index 35b7bb24..6b365a65 100644 --- a/test/CodoMetis.TypeKit.AspNetCore.Tests/SchemaTests.cs +++ b/test/CodoMetis.TypeKit.AspNetCore.Tests/SchemaTests.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi; @@ -5,35 +6,69 @@ namespace CodoMetis.TypeKit.AspNetCore.Tests; /// /// A value object in a JSON position (property, body, element) publishes the schema of the type it -/// wraps, under a component of its own. +/// wraps, under a component of its own, or in place with . /// public sealed class SchemaTests { - public static TheoryData WrappedTypeFamilies() + private static readonly ConcurrentDictionary> InlineDocuments = new(); + + /// The properties held to their control in both settings: each a value object beside the wrapped type. + private static readonly string[] Properties = + [ + "id", "count", "amount", "flag", "timestamp", "date", "moment", "time", "uri", "weekday", + "label", "code", "percentage", "customerId", "nested", "name", "countsById", + ]; + + /// + /// Held in place only, where the schema is the position's own: a component cannot admit null (decision 36), so + /// the components document holds these to the component or null (the tests below, and KB-026 for 2.0). + /// + private static readonly string[] InlineOnlyProperties = + [ + "maybeCount", "maybeLabel", "maybeName", "maybeId", "maybeWeekday", + "ids", "idArray", "readOnlyIds", "idSet", "counts", "nestedIds", "listOfLists", + "maybeCounts", "maybeCountArray", "maybeCountsByKey", + ]; + + public static TheoryData WrappedTypeFamilies() { - var data = new TheoryData(); - foreach (var version in new[] { OpenApiSpecVersion.OpenApi3_1, OpenApiSpecVersion.OpenApi3_0 }) - foreach (var property in new[] - { - "id", "count", "amount", "flag", "timestamp", "date", "moment", "time", "uri", "weekday", - "label", "code", "percentage", "customerId", "nested", - }) - data.Add(property, version); + var data = new TheoryData(); + foreach (var version in new[] { OpenApiSpecVersion.OpenApi3_1, OpenApiSpecVersion.OpenApi3_0, OpenApiSpecVersion.OpenApi2_0 }) + foreach (var inline in new[] { false, true }) + foreach (var property in inline ? [.. Properties, .. InlineOnlyProperties] : Properties) + data.Add(property, version, inline); return data; } [Theory] [MemberData(nameof(WrappedTypeFamilies))] - public async Task A_value_object_publishes_the_schema_of_the_type_it_wraps(string property, OpenApiSpecVersion version) + public async Task A_value_object_publishes_the_schema_of_the_type_it_wraps(string property, OpenApiSpecVersion version, bool inline) { - var document = await ProbeHost.DefaultDocumentAsync(version); + var document = inline + ? await InlineDocuments.GetOrAdd(version, static v => ProbeHost.DocumentAsync(openApi => openApi.AddTypeKit(typeKit => typeKit.InlineValueObjects = true), version: v)) + : await ProbeHost.DefaultDocumentAsync(version); var valueObject = document.Property("ProbeDocument", property); var control = document.Property("ControlDocument", property); - valueObject["$ref"].ShouldNotBeNull($"'{property}' should refer to the value object's own component, but was {valueObject.ToJsonString()}"); - document.Resolve(valueObject).ShouldDescribeTheSameAs(document.Resolve(control), property); + if (inline) + { + // In place, the value object holds what the wrapped type refers to (an enum's component), byte for byte. + valueObject.ToJsonString().ShouldNotContain("Probe", customMessage: $"'{property}' refers to a value object's component: {valueObject.ToJsonString()}"); + document.Expand(valueObject)!.ShouldDescribeTheSameAs(document.Expand(control)!, property); + } + else if (property == "countsById") + { + // ASP.NET describes no key, of a Guid or of a value object over one: the two dictionaries are one object. + valueObject["propertyNames"].ShouldBeNull(); + valueObject.ShouldDescribeTheSameAs(control, property); + } + else + { + valueObject["$ref"].ShouldNotBeNull($"'{property}' should refer to the value object's own component, but was {valueObject.ToJsonString()}"); + document.Resolve(valueObject).ShouldDescribeTheSameAs(document.Resolve(control), property); + } } [Fact] diff --git a/test/CodoMetis.TypeKit.AspNetCore.Tests/WithheldFaultNameTests.cs b/test/CodoMetis.TypeKit.AspNetCore.Tests/WithheldFaultNameTests.cs new file mode 100644 index 00000000..a6adfb0e --- /dev/null +++ b/test/CodoMetis.TypeKit.AspNetCore.Tests/WithheldFaultNameTests.cs @@ -0,0 +1,168 @@ +using CodoMetis.TypeKit.AspNetCore; +using CodoMetis.TypeKit.Generators.Probes; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace CodoMetis.TypeKit.AspNetCore.Tests; + +/// +/// A mapping that withholds a member's name, FaultResponse.Status(401).WithoutFaultName(): the problem +/// has no fault, no rule and no member description, so two causes on one status answer alike +/// but for the detail the application wrote, and the document says that status names no fault (decision 111). +/// +public sealed class WithheldFaultNameTests +{ + public enum SignInFault { UnknownUser, WrongPassword, Locked } + + public enum PasswordRule { Expired } + + private static Result> SignIn(string user) => user switch + { + "nobody" => Result.Error(Fault.Of(SignInFault.UnknownUser)), + "ann" => Result.Error(Fault.Of(SignInFault.WrongPassword).WithRule(PasswordRule.Expired)), + "bob" => Result.Error(Fault.Of(SignInFault.WrongPassword, "Try again in a minute.")), + _ => Result.Error(Fault.Of(SignInFault.Locked)), + }; + + private static Result> Archive(int id) => + id == 1 ? Fault.Of(ProbeOrderFault.NotFound) : Fault.Of(ProbeOrderFault.NotFound, "Order 2 was archived."); + + private static FaultResponse Mapping(SignInFault fault) => fault switch + { + SignInFault.Locked => StatusCodes.Status403Forbidden, + _ => FaultResponse.Status(StatusCodes.Status401Unauthorized).WithType(new Uri("https://problems.example/sign-in")).WithoutFaultName(), + }; + + [Fact] + public async Task Two_withheld_members_on_one_status_answer_alike_with_no_fault_and_no_rule() + { + await using var app = await StartAsync(); + + var unknownUser = await ProblemAsync(app, "/login/nobody"); + var wrongPassword = await ProblemAsync(app, "/login/ann"); + + unknownUser.ContainsKey("fault").ShouldBeFalse(unknownUser.ToJsonString()); + wrongPassword.ContainsKey("rule").ShouldBeFalse("the rule names the cause as surely as the member does"); + JsonNode.DeepEquals(unknownUser, wrongPassword).ShouldBeTrue($"{wrongPassword.ToJsonString()} tells itself apart from {unknownUser.ToJsonString()}"); + unknownUser["status"]!.GetValue().ShouldBe(401); + unknownUser["type"]!.GetValue().ShouldBe("https://problems.example/sign-in"); + } + + [Fact] + public async Task The_occurrence_s_own_detail_is_kept_and_a_named_member_still_names_itself() + { + await using var app = await StartAsync(); + + var withDetail = await ProblemAsync(app, "/login/bob"); + withDetail["detail"]!.GetValue().ShouldBe("Try again in a minute."); + withDetail.ContainsKey("fault").ShouldBeFalse(); + + (await ProblemAsync(app, "/login/carol"))["fault"]!.GetValue().ShouldBe("Locked"); + } + + [Fact] + public async Task The_host_still_sees_the_fault_and_that_its_name_is_withheld() + { + var seen = new List<(string Name, bool Withheld, string? Rule)>(); + await using var app = await StartAsync(services => services.AddProblemDetails(options => options.CustomizeProblemDetails = context => + { + if (context.HttpContext.Features.Get() is { } feature) seen.Add((feature.Name, feature.NameWithheld, feature.Rule)); + })); + + await ProblemAsync(app, "/login/ann"); + await ProblemAsync(app, "/login/carol"); + + seen.ShouldBe([("WrongPassword", true, "WithheldFaultNameTests.PasswordRule.Expired"), ("Locked", false, null)]); + } + + [Fact] + public async Task The_document_says_a_status_whose_every_fault_is_withheld_names_none() + { + await using var app = await StartAsync(endpoints: app => + app.MapPost("/admin/login/{user}", (string user) => SignIn(user).ToHttpResult()).WithFaultStatus(SignInFault.Locked, StatusCodes.Status401Unauthorized)); + + var paths = (await DocumentAsync(app))["paths"]!; + + Description(paths, "/login/{user}", "401").ShouldBe("Unauthorized. Problem types: https://problems.example/sign-in. The problem names no fault."); + Description(paths, "/login/{user}", "403").ShouldBe("Forbidden"); + Description(paths, "/admin/login/{user}", "401").ShouldBe("Unauthorized. Problem types: https://problems.example/sign-in", "Locked shares the status and names itself"); + } + + /// A fault set member's description stands in for a missing detail, and names the member: withheld, it goes too. + [Fact] + public async Task A_whole_fault_set_withheld_writes_no_member_description() + { + await using var app = await StartAsync( + services => services.AddFaultResponses(fault => FaultResponse.For(fault.Kind).WithoutFaultName()), + app => app.MapGet("/orders/{id:int}", (int id) => Archive(id).ToHttpResult())); + + var described = await ProblemAsync(app, "/orders/1"); + described.ContainsKey("detail").ShouldBeFalse(described.ToJsonString()); + described.ContainsKey("fault").ShouldBeFalse(); + + (await ProblemAsync(app, "/orders/2"))["detail"]!.GetValue().ShouldBe("Order 2 was archived."); + } + + [Fact] + public void Withholding_the_name_survives_a_type_and_a_title_in_either_order() + { + FaultResponse.Status(401).NamesFault.ShouldBeTrue(); + FaultResponse.Status(401).WithoutFaultName().WithType(new Uri("https://problems.example/a")).WithTitle("Refused").NamesFault.ShouldBeFalse(); + + var response = FaultResponse.Status(401).WithType(new Uri("https://problems.example/a")).WithTitle("Refused").WithoutFaultName(); + response.NamesFault.ShouldBeFalse(); + response.Type.ShouldBe(new Uri("https://problems.example/a")); + response.Title.ShouldBe("Refused"); + response.StatusCode.ShouldBe(401); + } + + private static async Task StartAsync(Action? services = null, Action? endpoints = null) + { + var builder = WebApplication.CreateBuilder(new WebApplicationOptions { EnvironmentName = Environments.Production }); + builder.WebHost.UseTestServer(); + builder.Logging.ClearProviders(); + builder.Services.AddOpenApi(options => options.AddTypeKit()); + builder.Services.AddFaultResponses(Mapping); + services?.Invoke(builder.Services); + + var app = builder.Build(); + app.MapOpenApi(); + app.MapPost("/login/{user}", (string user) => SignIn(user).ToHttpResult()); + endpoints?.Invoke(app); + + try + { + await app.StartAsync(TestContext.Current.CancellationToken); + } + catch + { + await app.DisposeAsync(); + throw; + } + + return app; + } + + private static async Task ProblemAsync(WebApplication app, string path) + { + var method = path.StartsWith("/login", StringComparison.Ordinal) ? HttpMethod.Post : HttpMethod.Get; + using var response = await app.GetTestClient().SendAsync(new HttpRequestMessage(method, path), TestContext.Current.CancellationToken); + + response.Content.Headers.ContentType!.MediaType.ShouldBe("application/problem+json"); + var body = JsonNode.Parse(await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken))!.AsObject(); + body.Remove("traceId"); + return body; + } + + private static async Task DocumentAsync(WebApplication app) => + JsonNode.Parse(await app.GetTestClient().GetStringAsync("/openapi/v1.json", TestContext.Current.CancellationToken))!; + + private static string Description(JsonNode paths, string path, string status) => + paths[path]!["post"]?["responses"]?[status]?["description"]?.GetValue() + ?? paths[path]!["get"]!["responses"]![status]!["description"]!.GetValue(); +} diff --git a/test/CodoMetis.TypeKit.CiLanes.Tests/CodoMetis.TypeKit.CiLanes.Tests.csproj b/test/CodoMetis.TypeKit.CiLanes.Tests/CodoMetis.TypeKit.CiLanes.Tests.csproj new file mode 100644 index 00000000..c978265a --- /dev/null +++ b/test/CodoMetis.TypeKit.CiLanes.Tests/CodoMetis.TypeKit.CiLanes.Tests.csproj @@ -0,0 +1,18 @@ + + + + + + + <_Parameter1>ToolOutput + <_Parameter2>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)../../tools/CiLanes/bin/$(Configuration)/$(TargetFramework)/')) + + + + + + + diff --git a/test/CodoMetis.TypeKit.CiLanes.Tests/GateTests.cs b/test/CodoMetis.TypeKit.CiLanes.Tests/GateTests.cs new file mode 100644 index 00000000..d7886970 --- /dev/null +++ b/test/CodoMetis.TypeKit.CiLanes.Tests/GateTests.cs @@ -0,0 +1,171 @@ +using System.Text.Json; + +namespace CodoMetis.TypeKit.CiLanes.Tests; + +/// +/// The gate's verdict on a run, from the toJSON(needs) the CI job writes. Each failing case starts from a +/// run that passes and breaks one thing: a failed selection, a cancelled leg, an answer the selection does not +/// write, a selected job that was skipped, and a skipped job that ran (#71, design/adr/0113). +/// +public sealed class GateTests : IDisposable +{ + private const string LinuxOnly = """["ubuntu-latest"]"""; + private const string LinuxAndWindows = """["ubuntu-latest","windows-latest"]"""; + private const string EveryLeg = """["ubuntu-latest","windows-latest","macos-latest"]"""; + + private readonly ToolSandbox _tool = new("CiLanes", "ci-lanes"); + + public void Dispose() => _tool.Dispose(); + + private sealed record Run( + string Changes = "success", + string? Legs = LinuxOnly, + string? Pack = "false", + string? Suite = "prose", + string Test = "success", + string PackJob = "skipped", + string Consumer = "skipped"); + + private static readonly Run Prose = new(); + + private static readonly Run Package = new(Pack: "true", Suite: "full", PackJob: "success", Consumer: "success"); + + private static readonly Run Full = Package with { Legs = EveryLeg }; + + private (int Code, string Output) Judge(Run run, Action>? edit = null) + { + var outputs = new Dictionary(); + if (run.Legs is not null) outputs["legs"] = run.Legs; + if (run.Pack is not null) outputs["pack"] = run.Pack; + if (run.Suite is not null) outputs["suite"] = run.Suite; + + var needs = new Dictionary + { + ["changes"] = new { result = run.Changes, outputs }, + ["test"] = new { result = run.Test, outputs = new { } }, + ["pack"] = new { result = run.PackJob, outputs = new { } }, + ["consumer"] = new { result = run.Consumer, outputs = new { } }, + }; + edit?.Invoke(needs); + + var file = Path.Combine(_tool.Work, $"{Guid.NewGuid():N}.json"); + File.WriteAllText(file, JsonSerializer.Serialize(needs)); + var (code, output, _) = _tool.Run(arguments: ["gate", file]); + return (code, output); + } + + private void Passes(Run run) + { + var (code, output) = Judge(run); + code.ShouldBe(0, output); + output.ShouldNotContain("::error::"); + } + + private void Fails(Run run, string problem, Action>? edit = null) + { + var (code, output) = Judge(run, edit); + code.ShouldBe(1, output); + output.ShouldContain($"::error::{problem}"); + } + + [Fact] + public void A_prose_run_passes_with_the_pack_skipped() => Passes(Prose); + + [Fact] + public void A_package_run_passes_with_the_pack_run() => Passes(Package); + + [Fact] + public void An_analyzer_run_passes_with_Windows_and_the_pack() => Passes(Package with { Legs = LinuxAndWindows }); + + [Fact] + public void A_test_run_passes_with_Windows_and_no_pack() => Passes(Prose with { Legs = LinuxAndWindows, Suite = "full" }); + + [Theory] + [InlineData("Prose")] + [InlineData("all")] + [InlineData("")] + public void A_suite_answer_the_selection_does_not_write_fails(string suite) => + Fails(Prose with { Suite = suite }, $"The changes job's suite output is '{suite}'"); + + [Fact] + public void The_prose_suite_beside_another_leg_fails() => + Fails(Prose with { Legs = LinuxAndWindows }, "The changes job chose the prose suite for a run with more than the Linux leg"); + + [Fact] + public void The_prose_suite_beside_the_pack_fails() => + Fails(Package with { Suite = "prose" }, "The changes job chose the prose suite for a run with more than the Linux leg"); + + [Fact] + public void A_run_that_is_not_selected_passes_with_every_leg() => Passes(Full); + + [Theory] + [InlineData("failure")] + [InlineData("cancelled")] + [InlineData("skipped")] + public void A_selection_that_did_not_succeed_fails(string result) => + Fails(Prose with { Changes = result, Legs = null, Pack = null, Suite = null }, $"The changes job ended '{result}'"); + + [Theory] + [InlineData("cancelled")] + [InlineData("failure")] + [InlineData("skipped")] + public void A_test_matrix_that_did_not_succeed_fails(string result) => + Fails(Package with { Test = result }, $"The job 'test' was selected and ended '{result}'."); + + [Fact] + public void A_selected_pack_that_was_skipped_fails() => + Fails(Package with { PackJob = "skipped" }, "The job 'pack' was selected and ended 'skipped'."); + + [Fact] + public void A_cancelled_consumer_fails() => + Fails(Package with { Consumer = "cancelled" }, "The job 'consumer' was selected and ended 'cancelled'."); + + [Fact] + public void An_unselected_job_that_ran_fails() => + Fails(Prose with { Consumer = "success" }, "The job 'consumer' was not selected and ended 'success', not skipped"); + + [Theory] + [InlineData("True")] + [InlineData("yes")] + [InlineData("")] + public void A_pack_answer_the_selection_does_not_write_fails(string pack) => + Fails(Prose with { Pack = pack }, $"The changes job's pack output is '{pack}'"); + + [Fact] + public void A_missing_answer_fails() => Fails(Prose with { Pack = null }, "The changes job's pack output is missing"); + + [Theory] + [InlineData("""["ubuntu-latest","macos-latest"]""")] + [InlineData("""["windows-latest","ubuntu-latest"]""")] + [InlineData("""["ubuntu"]""")] + [InlineData("ubuntu-latest")] + [InlineData("[]")] + public void A_matrix_the_selection_does_not_write_fails(string legs) => + Fails(Prose with { Legs = legs }, "The changes job's legs output is"); + + [Fact] + public void A_job_the_gate_does_not_know_fails() => + Fails(Prose, "The gate does not know the job 'docs'", needs => needs["docs"] = new { result = "success", outputs = new { } }); + + [Fact] + public void A_job_the_gate_does_not_need_fails() => + Fails(Prose, "The gate does not need the job 'consumer'", needs => needs.Remove("consumer")); + + [Fact] + public void The_summary_tables_each_job() + { + var file = Path.Combine(_tool.Work, "needs.json"); + File.WriteAllText(file, JsonSerializer.Serialize(new Dictionary + { + ["changes"] = new { result = "success", outputs = new { legs = LinuxOnly, pack = "false", suite = "prose" } }, + ["test"] = new { result = "success" }, + ["pack"] = new { result = "skipped" }, + ["consumer"] = new { result = "skipped" }, + })); + var summary = Path.Combine(_tool.Work, "summary.md"); + + _tool.Run(arguments: ["gate", file, "--summary", summary]).Code.ShouldBe(0); + + File.ReadAllText(summary).ShouldContain("| pack | no | skipped |"); + } +} diff --git a/test/CodoMetis.TypeKit.CiLanes.Tests/SelectionTests.cs b/test/CodoMetis.TypeKit.CiLanes.Tests/SelectionTests.cs new file mode 100644 index 00000000..8ecf0cad --- /dev/null +++ b/test/CodoMetis.TypeKit.CiLanes.Tests/SelectionTests.cs @@ -0,0 +1,263 @@ +namespace CodoMetis.TypeKit.CiLanes.Tests; + +/// +/// The lanes a run gets, from the event and the paths it changes. Each case runs the built tool as its own process +/// against a throwaway git repository, the way the changes job runs it against the checkout, and reads the answers +/// it writes to the step's output file (design/adr/0113). +/// +public sealed class SelectionTests : IDisposable +{ + private const string LinuxOnly = """legs=["ubuntu-latest"]"""; + private const string LinuxAndWindows = """legs=["ubuntu-latest","windows-latest"]"""; + private const string EveryLeg = """legs=["ubuntu-latest","windows-latest","macos-latest"]"""; + + private readonly ToolSandbox _tool = new("CiLanes", "ci-lanes"); + + public SelectionTests() + { + Write("src/CodoMetis.TypeKit/Option.cs", "// a package's source\n"); + Write("src/CodoMetis.TypeKit.Analyzers/CodeFixes/AddCreate.cs", "// a code fix's source\n"); + Write("docs/guide.md", "# A guide\n"); + Git("init", "-q", "-b", "release/9.9.0"); + Git("add", "-A"); + Git("commit", "-q", "-m", "base"); + Git("update-ref", "refs/remotes/origin/release/9.9.0", "HEAD"); + Git("update-ref", "refs/remotes/origin/main", "HEAD"); + } + + public void Dispose() => _tool.Dispose(); + + public static TheoryData Paths => new() + { + { "docs/guide.md", "prose", "linux" }, + { "design/adr/0113-ci-runs-the-lanes-a-change-can-affect.md", "prose", "linux" }, + { "README.md", "prose", "linux" }, + { "test/oracles/README.md", "prose", "linux" }, + { ".github/ISSUE_TEMPLATE/bug.yml", "prose", "linux" }, + { "site/docfx.json", "prose", "linux" }, + { "site/toc.yml", "prose", "linux" }, + { "site/template/conceptual.extension.js", "prose", "linux" }, + { "CHANGELOG.md", "changelog", "linux, pack" }, + { "src/CodoMetis.TypeKit.Analyzers/CodeFixes/AddCreateCodeFix.cs", "source text", "linux, windows, pack" }, + { "src/CodoMetis.TypeKit.Analyzers/AnalyzerReleases.Unshipped.md", "source text", "linux, windows, pack" }, + { "src/CodoMetis.TypeKit.Generators/README.md", "source text", "linux, windows, pack" }, + { "src/CodoMetis.TypeKit/README.md", "package", "linux, pack" }, + { "src/CodoMetis.TypeKit.EntityFrameworkCore/ValueObjectConverter.cs", "package", "linux, pack" }, + { "src/CodoMetis.TypeKit.AnalyzersAndMore/X.cs", "package", "linux, pack" }, + { "src/Directory.Build.targets", "package", "linux, pack" }, + { "tools/ConsumerSmokeTest/Consumers/Aot/Program.cs", "smoke test", "linux, windows, pack" }, + { "tools/ConsumerSmokeTest/README.md", "smoke test", "linux, windows, pack" }, + { "tools/Shared/SharedFile.cs", "smoke test", "linux, windows, pack" }, + { "tools/Directory.Build.props", "smoke test", "linux, windows, pack" }, + { "tools/CiLanes/Program.cs", "smoke test", "linux, windows, pack" }, + { "tools/PrHygiene/Program.cs", "code no package reads", "linux, windows" }, + { "test/CodoMetis.TypeKit.Tests/OptionTests.cs", "code no package reads", "linux, windows" }, + { "test/Directory.Build.props", "code no package reads", "linux, windows" }, + { "spikes/FabricSpike/Program.cs", "code no package reads", "linux, windows" }, + { "global.json", "unlisted", "linux, windows, pack" }, + { "Directory.Build.props", "unlisted", "linux, windows, pack" }, + { "Directory.Packages.props", "unlisted", "linux, windows, pack" }, + { ".config/dotnet-tools.json", "unlisted", "linux, windows, pack" }, + { ".github/workflows/dotnet.yml", "unlisted", "linux, windows, pack" }, + { ".github/workflows/docs.yml", "unlisted", "linux, windows, pack" }, + { "benchmarks/CodoMetis.TypeKit.Benchmarks/Program.cs", "unlisted", "linux, windows, pack" }, + { "a-file-nobody-listed", "unlisted", "linux, windows, pack" }, + }; + + [Theory, MemberData(nameof(Paths))] + public void A_path_selects_the_lanes_of_its_class(string path, string @class, string lanes) + { + var (code, output, _) = _tool.Run(arguments: ["classify", path]); + + code.ShouldBe(0, output); + output.ShouldBe($"{path}\t{@class}\t{lanes}\n"); + } + + [Fact] + public void A_prose_change_runs_the_Linux_leg_alone() + { + Commit("docs/guide.md", "# A guide, reworded\n"); + + Answers(PullRequestInto("release/9.9.0")).ShouldBe([LinuxOnly, "pack=false", "suite=prose"]); + } + + /// The prose suite runs only for a change that is prose throughout. + [Fact] + public void Prose_beside_any_other_change_runs_the_whole_suite() + { + Commit("docs/guide.md", "# A guide, reworded\n"); + Commit("test/CodoMetis.TypeKit.Tests/OptionTests.cs", "// a test\n"); + + Answers(PullRequestInto("release/9.9.0")).ShouldBe([LinuxAndWindows, "pack=false", "suite=full"]); + } + + [Fact] + public void A_changelog_change_runs_the_whole_suite() + { + Commit("CHANGELOG.md", "## 9.9.0\n"); + + Answers(PullRequestInto("release/9.9.0")).ShouldBe([LinuxOnly, "pack=true", "suite=full"]); + } + + [Fact] + public void A_package_change_runs_Linux_and_the_pack() + { + Commit("docs/guide.md", "# A guide, reworded\n"); + Commit("src/CodoMetis.TypeKit/Option.cs", "// changed\n"); + + Answers(PullRequestInto("release/9.9.0")).ShouldBe([LinuxOnly, "pack=true", "suite=full"]); + } + + /// The one src/ defect a Windows leg alone caught in 150 runs was a code fix's line endings (f9f64e5). + [Fact] + public void An_analyzer_change_adds_the_Windows_leg() + { + Commit("src/CodoMetis.TypeKit/Option.cs", "// changed\n"); + Commit("src/CodoMetis.TypeKit.Analyzers/CodeFixes/AddCreate.cs", "// changed\n"); + + Answers(PullRequestInto("release/9.9.0")).ShouldBe([LinuxAndWindows, "pack=true", "suite=full"]); + } + + [Fact] + public void A_test_change_runs_Linux_and_Windows_without_the_pack() + { + Commit("test/CodoMetis.TypeKit.Tests/OptionTests.cs", "// a test\n"); + + Answers(PullRequestInto("release/9.9.0")).ShouldBe([LinuxAndWindows, "pack=false", "suite=full"]); + } + + /// A selection never runs macOS: a path no class names gets every lane but that one. + [Fact] + public void An_unlisted_change_runs_every_lane_but_macOS() + { + Commit("global.json", "{}\n"); + + Answers(PullRequestInto("release/9.9.0")).ShouldBe([LinuxAndWindows, "pack=true", "suite=full"]); + } + + /// Without --no-renames, git lists a rename's new path alone, and a move out of src/ reads as prose (#70). + [Fact] + public void A_move_out_of_a_package_counts_its_old_path() + { + Git("mv", "src/CodoMetis.TypeKit/Option.cs", "docs/Option.cs.md"); + Git("commit", "-q", "-m", "move"); + + Answers(PullRequestInto("release/9.9.0")).ShouldBe([LinuxOnly, "pack=true", "suite=full"]); + } + + [Fact] + public void The_release_pull_request_into_main_runs_every_lane() + { + Commit("docs/guide.md", "# A guide, reworded\n"); + + Answers(PullRequestInto("main")).ShouldBe([EveryLeg, "pack=true", "suite=full"]); + } + + [Fact] + public void A_push_to_a_release_branch_is_selected_by_what_it_replaced() + { + var before = Git("rev-parse", "HEAD").Trim(); + Commit("docs/guide.md", "# A guide, reworded\n"); + + Answers("--event", "push", "--ref", "refs/heads/release/9.9.0", "--before", before).ShouldBe([LinuxOnly, "pack=false", "suite=prose"]); + } + + [Theory] + [InlineData("0000000000000000000000000000000000000000")] + [InlineData("1234567890abcdef1234567890abcdef12345678")] + [InlineData("")] + public void A_push_without_a_commit_to_diff_against_runs_every_lane(string before) + { + Commit("docs/guide.md", "# A guide, reworded\n"); + + Answers("--event", "push", "--ref", "refs/heads/release/9.9.0", "--before", before).ShouldBe([EveryLeg, "pack=true", "suite=full"]); + } + + [Theory] + [InlineData("push", "refs/heads/main")] + [InlineData("push", "refs/tags/v9.9.0")] + [InlineData("workflow_dispatch", "refs/heads/release/9.9.0")] + [InlineData("schedule", "refs/heads/main")] + public void Every_other_run_gets_every_lane(string @event, string @ref) + { + Commit("docs/guide.md", "# A guide, reworded\n"); + + Answers("--event", @event, "--ref", @ref, "--before", Git("rev-parse", "HEAD~1").Trim()).ShouldBe([EveryLeg, "pack=true", "suite=full"]); + } + + [Fact] + public void A_diff_that_names_no_path_runs_every_lane() => + Answers(PullRequestInto("release/9.9.0")).ShouldBe([EveryLeg, "pack=true", "suite=full"]); + + /// A diff that cannot be taken fails the job, never answers "nothing changed" (#70). + [Fact] + public void A_diff_that_fails_fails_the_selection() + { + var output = Path.Combine(_tool.Work, "output"); + + var (code, text, _) = _tool.Run(arguments: ["select", .. PullRequestInto("release/9.9.1"), "--output", output]); + + code.ShouldBe(1, text); + text.ShouldContain("::error::git diff origin/release/9.9.1...HEAD failed"); + File.Exists(output).ShouldBeFalse("a failed selection writes no answer"); + } + + [Fact] + public void The_summary_names_each_path_and_its_class() + { + Commit("CHANGELOG.md", "## 9.9.0\n"); + var summary = Path.Combine(_tool.Work, "summary.md"); + + _tool.Run(arguments: ["select", .. PullRequestInto("release/9.9.0"), "--summary", summary]).Code.ShouldBe(0); + + File.ReadAllText(summary).ShouldContain("| `CHANGELOG.md` | changelog | linux, pack |"); + } + + [Theory] + [InlineData] + [InlineData("select")] + [InlineData("select", "--event", "push")] + [InlineData("select", "--event", "push", "--ref", "refs/heads/main", "--colour", "red")] + [InlineData("classify")] + [InlineData("gate")] + [InlineData("lanes")] + public void A_usage_error_is_exit_2(params string[] arguments) => + _tool.Run(arguments: arguments).Code.ShouldBe(2); + + private static string[] PullRequestInto(string target) => ["--event", "pull_request", "--ref", "refs/pull/1/merge", "--base", target]; + + private string[] Answers(params string[] options) + { + var output = Path.Combine(_tool.Work, $"{Guid.NewGuid():N}.output"); + var (code, text, _) = _tool.Run(arguments: ["select", .. options, "--output", output]); + + code.ShouldBe(0, text); + return File.ReadAllLines(output); + } + + private void Commit(string path, string content) + { + Write(path, content); + Git("add", "-A"); + Git("commit", "-q", "-m", $"change {path}"); + } + + private void Write(string path, string content) + { + var file = Path.Combine(_tool.Repo, path); + Directory.CreateDirectory(Path.GetDirectoryName(file)!); + File.WriteAllText(file, content); + } + + // git in the sandbox's repository, with no configuration of the machine's: its user, signing and hooks stay out. + private string Git(params string[] arguments) + { + var empty = Path.Combine(_tool.Work, "gitconfig"); + File.WriteAllText(empty, ""); + var (code, output, error) = ToolSandbox.RunProcess("git", ["-c", "user.name=CI Lanes", "-c", "user.email=ci@example.invalid", "-c", "commit.gpgsign=false", .. arguments], + _tool.Repo, new Dictionary { ["GIT_CONFIG_GLOBAL"] = empty, ["GIT_CONFIG_NOSYSTEM"] = "1" }); + + code.ShouldBe(0, $"git {string.Join(' ', arguments)}: {error}"); + return output; + } +} diff --git a/test/CodoMetis.TypeKit.CiLanes.Tests/StaleToolTests.cs b/test/CodoMetis.TypeKit.CiLanes.Tests/StaleToolTests.cs new file mode 100644 index 00000000..2c563896 --- /dev/null +++ b/test/CodoMetis.TypeKit.CiLanes.Tests/StaleToolTests.cs @@ -0,0 +1,30 @@ +namespace CodoMetis.TypeKit.CiLanes.Tests; + +/// +/// A selection built from other sources than the checkout holds says so and selects nothing: exit 3, the +/// difference, and the command that rebuilds it. A stale selection would skip or run lanes by rules +/// nobody can read in the checkout. +/// +public sealed class StaleToolTests +{ + [Fact] + public void A_tool_built_from_its_sources_runs() + { + using var sandbox = new ToolSandbox("CiLanes", "ci-lanes"); + + sandbox.Run().Code.ShouldBe(2, "a usage error, so the tool ran"); + } + + [Fact] + public void A_changed_source_stops_it() + { + using var sandbox = new ToolSandbox("CiLanes", "ci-lanes"); + File.AppendAllText(Path.Combine(sandbox.Repo, "tools", "CiLanes", "Program.cs"), "\n// edited\n"); + + var (code, output, _) = sandbox.Run(); + + code.ShouldBe(3); + output.ShouldStartWith("TOOL STALE: tools/CiLanes was built from other sources than the checkout holds (tools/CiLanes/Program.cs differs)"); + output.ShouldContain("rebuild it: dotnet build tools/CiLanes -c "); + } +} diff --git a/test/CodoMetis.TypeKit.Conventions.Tests/ChangelogTests.cs b/test/CodoMetis.TypeKit.Conventions.Tests/ChangelogTests.cs index e293252c..de586664 100644 --- a/test/CodoMetis.TypeKit.Conventions.Tests/ChangelogTests.cs +++ b/test/CodoMetis.TypeKit.Conventions.Tests/ChangelogTests.cs @@ -51,6 +51,26 @@ public void A_released_version_has_shipped_its_analyzer_rules() $"CHANGELOG.md dates {Version} as released, but AnalyzerReleases.Shipped.md has no '## Release {Version}'."); } + /// + /// A dated section is its packages' release notes on nuget.org and its GitHub release's notes, both + /// frozen with the version, so it links the repository at its own tag. A link at main opens + /// that day's page, where the section may say something else or the anchor may be gone (#76). A + /// page written after the tag, such as the 2.1 migration guide, is linked at a full commit id, + /// which is as frozen. The Unreleased section has no tag yet and links main. + /// + [Fact] + public void A_released_version_links_the_repository_at_its_tag() + { + var links = DatedSections().SelectMany(section => RepositoryLink().Matches(section.Text) + .Select(link => (section.Version, Ref: link.Groups["ref"].Value, Link: link.Value))) + .ToList(); + + links.Count.ShouldBeGreaterThanOrEqualTo(30, "Too few links read from the dated sections: the reader no longer finds them."); + links.Where(link => link.Ref != $"v{link.Version}" && !CommitId().IsMatch(link.Ref)) + .Select(link => $"{link.Version}: {link.Link}") + .ShouldBeEmpty("These links in a released version's section don't name its tag. Write blob/v/ in place of blob/main/ when you date the section (CONTRIBUTING.md, Releasing)."); + } + /// /// Package validation compares every pack with the baseline, so ApiCompat refuses a break of the /// public surface. A baseline left behind at an older release, or dropped, lets a break of the last @@ -82,9 +102,27 @@ public void The_package_validation_baseline_is_the_last_release() private static Dictionary Sections() => VersionHeading().Matches(Changelog).ToDictionary(match => match.Groups["version"].Value, match => match.Groups["date"].Value); + /// Each dated version's section: its heading and everything up to the next one. + private static IEnumerable<(string Version, string Text)> DatedSections() + { + var headings = VersionHeading().Matches(Changelog); + + return headings.Select((heading, index) => + (heading, end: index + 1 < headings.Count ? headings[index + 1].Index : Changelog.Length)) + .Where(section => section.heading.Groups["date"].Value != "Unreleased") + .Select(section => (section.heading.Groups["version"].Value, + Changelog[section.heading.Index..section.end])); + } + [GeneratedRegex(@"(?m)^## (?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?) — (?Unreleased|\d{4}-\d{2}-\d{2})\s*$")] private static partial Regex VersionHeading(); + [GeneratedRegex(@"https://github\.com/CaffeinatedCoder/CodoMetis\.TypeKit/(?:blob|tree)/(?[^/\s)]+)/[^\s)]*")] + private static partial Regex RepositoryLink(); + + [GeneratedRegex(@"^[0-9a-f]{40}$")] + private static partial Regex CommitId(); + [GeneratedRegex(@"^CMTK\d{4}\s*\|")] private static partial Regex RuleRow(); } diff --git a/test/CodoMetis.TypeKit.Conventions.Tests/CiLaneTests.cs b/test/CodoMetis.TypeKit.Conventions.Tests/CiLaneTests.cs new file mode 100644 index 00000000..0fe055d0 --- /dev/null +++ b/test/CodoMetis.TypeKit.Conventions.Tests/CiLaneTests.cs @@ -0,0 +1,140 @@ +using System.Diagnostics; +using System.Reflection; +using System.Text.Json; + +namespace CodoMetis.TypeKit.Conventions.Tests; + +/// +/// The selection of tools/CiLanes never lets a change to a package skip the pack: every file MSBuild evaluates +/// for a shipping project or the consumer smoke test, items and imports both, is classified into a class that +/// selects the pack lane (design/adr/0113, #70). +/// +/// +/// Read from MSBuild's evaluation, not from a list: before decision 113 the changes job listed its paths by hand, +/// and missed tools/Shared/*.cs, which tools/Directory.Build.props compiles into the smoke test. The +/// imports are read from the preprocessed project (-pp), since MSBuildAllProjects lists only the +/// files that append themselves to it. A file a target reads by a property function is in no evaluation: the one +/// such file, the changelog the release notes come from, is held by name. +/// +public sealed class CiLaneTests +{ + /// The shipping projects, with the ones they reference, and the smoke test, which packs them. + public static TheoryData Projects => + [ + .. Repository.ShippingProjects().Select(project => $"src/{project}/{project}.csproj"), + "tools/ConsumerSmokeTest/ConsumerSmokeTest.csproj", + ]; + + [Theory, MemberData(nameof(Projects))] + public async Task Every_file_a_package_or_the_smoke_test_reads_selects_the_pack(string project) + { + var inputs = new SortedSet(StringComparer.Ordinal); + var pending = new Queue([Path.GetFullPath(Path.Combine(Repository.Root, project))]); + var seen = new HashSet(StringComparer.Ordinal); + + while (pending.TryDequeue(out var path)) + { + if (!seen.Add(path)) continue; + inputs.Add(path); + + foreach (var import in await Imports(path)) inputs.Add(import); + foreach (var (item, full) in await Items(path)) + { + inputs.Add(full); + if (item == "ProjectReference") pending.Enqueue(full); + } + } + + var relative = inputs.Where(InRepository).Select(path => Path.GetRelativePath(Repository.Root, path).Replace('\\', '/')).ToList(); + relative.ShouldContain(path => path.StartsWith("tools/Directory.Build.props", StringComparison.Ordinal) || path.StartsWith("src/", StringComparison.Ordinal), + "the evaluation found none of the project's own inputs, so the discovery is broken"); + + var unpacked = Classify(relative).Where(row => !row.Lanes.Contains("pack", StringComparer.Ordinal)).ToList(); + + unpacked.ShouldBeEmpty( + $"{project} reads these files, and a pull request changing one alone would skip the pack: " + + string.Join(", ", unpacked.Select(row => $"{row.Path} ({row.Class})")) + + ". Give them a class that selects pack in tools/CiLanes (decision 113)."); + } + + /// + /// src/Directory.Build.targets reads the changelog into the release notes with a property function inside a + /// target, which no evaluation lists (#75). Held by name, beside the evidence that the target still reads it. + /// + [Fact] + public void The_changelog_the_release_notes_come_from_selects_the_pack() + { + File.ReadAllText(Path.Combine(Repository.Root, "src", "Directory.Build.targets")) + .ShouldContain("'CodoMetis.TypeKit.slnx'))/CHANGELOG.md", Case.Sensitive, + "src/Directory.Build.targets no longer reads CHANGELOG.md at the root; find what it reads and hold that here"); + + Classify(["CHANGELOG.md"]).Single().Lanes.ShouldContain("pack"); + } + + private static bool InRepository(string path) => + path.StartsWith(Repository.Root + Path.DirectorySeparatorChar, StringComparison.Ordinal) + && !Path.GetRelativePath(Repository.Root, path).Split(Path.DirectorySeparatorChar).Any(part => part is "bin" or "obj"); + + private static List<(string Path, string Class, string[] Lanes)> Classify(IReadOnlyList paths) + { + var output = typeof(CiLaneTests).Assembly.GetCustomAttributes().Single(a => a.Key == "CiLanesOutput").Value!; + var (code, text) = Run(Path.Combine(output, "ci-lanes.dll"), ["classify", .. paths]); + + code.ShouldBe(0, text); + return + [ + .. text.Split('\n', StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Split('\t')) + .Select(parts => (parts[0], parts[1], parts[2].Split(", "))) + ]; + } + + // The project and every file it imports, from the preprocessed project: each import is named there by its full path, on a line of its own. + private static async Task> Imports(string project) + { + var file = Path.Combine(Path.GetTempPath(), $"cilanes-{Guid.NewGuid():N}.xml"); + try + { + var (code, text) = await RunAsync(["msbuild", project, $"-pp:{file}", "-nodeReuse:false"]); + code.ShouldBe(0, $"dotnet msbuild -pp {project} failed: {text}"); + return [.. File.ReadAllLines(file).Select(line => line.Trim()).Where(line => Path.IsPathFullyQualified(line) && File.Exists(line))]; + } + finally + { + File.Delete(file); + } + } + + // Every item that names a file: what is compiled, embedded, packed or analysed, and the projects referenced. + private static async Task> Items(string project) + { + string[] kinds = ["Compile", "None", "Content", "EmbeddedResource", "AdditionalFiles", "ProjectReference"]; + var (code, text) = await RunAsync(["msbuild", project, .. kinds.Select(kind => $"-getItem:{kind}"), "-nodeReuse:false"]); + code.ShouldBe(0, $"dotnet msbuild {project} failed: {text}"); + + using var evaluation = JsonDocument.Parse(text); + return + [ + .. evaluation.RootElement.GetProperty("Items").EnumerateObject() + .SelectMany(kind => kind.Value.EnumerateArray().Select(item => (kind.Name, item.GetProperty("FullPath").GetString()!))) + .Where(item => File.Exists(item.Item2)) + ]; + } + + private static Task<(int Code, string Output)> RunAsync(string[] arguments) => Task.Run(() => Run(null, arguments)); + + // dotnet with the arguments, or the dll with them when one is given; stdout, and stderr after it on a failure. + private static (int Code, string Output) Run(string? dll, string[] arguments) + { + using var process = Process.Start(new ProcessStartInfo("dotnet", dll is null ? arguments : [dll, .. arguments]) + { + RedirectStandardOutput = true, + RedirectStandardError = true, + })!; + + var errors = process.StandardError.ReadToEndAsync(); + var output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + return (process.ExitCode, process.ExitCode == 0 ? output : output + errors.Result); + } +} diff --git a/test/CodoMetis.TypeKit.Conventions.Tests/CodoMetis.TypeKit.Conventions.Tests.csproj b/test/CodoMetis.TypeKit.Conventions.Tests/CodoMetis.TypeKit.Conventions.Tests.csproj index d79888dc..e05e52e3 100644 --- a/test/CodoMetis.TypeKit.Conventions.Tests/CodoMetis.TypeKit.Conventions.Tests.csproj +++ b/test/CodoMetis.TypeKit.Conventions.Tests/CodoMetis.TypeKit.Conventions.Tests.csproj @@ -10,6 +10,18 @@ + + + + + <_Parameter1>CiLanesOutput + <_Parameter2>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)../../tools/CiLanes/bin/$(Configuration)/$(TargetFramework)/')) + + + "); + } + + [GeneratedRegex(@"^generated code for (?\S+), trimmed \(GeneratedCodeBlockTests\)$")] + private static partial Regex Marker(); + + /// The namespaces of every type the samples can name, so a qualified name loses only those. + private static readonly Lazy> KnownNamespaces = new(() => + { + var names = new HashSet(StringComparer.Ordinal); + + // TypeKit's and the samples' own, read from their sources: this project loads neither assembly. + foreach (var folder in new[] { "src", ReadmeSamples.SamplesProject }) + foreach (var path in Directory.EnumerateFiles(Path.Combine(Repository.Root, folder), "*.cs", SearchOption.AllDirectories) + .Where(path => !path.Split(Path.DirectorySeparatorChar).Any(part => part is "bin" or "obj"))) + foreach (var declaration in Parse(File.ReadAllText(path)).DescendantNodes().OfType()) + AddWithParents(names, declaration.Name.ToString()); + + // The framework's, read from the metadata of every assembly of the runtime and of this test's output. + var folders = new[] { Path.GetDirectoryName(typeof(object).Assembly.Location)!, AppContext.BaseDirectory }; + foreach (var path in folders.SelectMany(folder => Directory.EnumerateFiles(folder, "*.dll"))) + { + try + { + using var stream = File.OpenRead(path); + using var reader = new PEReader(stream); + if (!reader.HasMetadata) continue; + + var metadata = reader.GetMetadataReader(); + foreach (var handle in metadata.TypeDefinitions) + if (metadata.GetString(metadata.GetTypeDefinition(handle).Namespace) is { Length: > 0 } ns) AddWithParents(names, ns); + } + catch (BadImageFormatException) + { + // A native library beside the managed ones. + } + } + + return names; + }); + + private static void AddWithParents(HashSet names, string ns) + { + for (var name = ns; name.Length > 0; name = name.Contains('.') ? name[..name.LastIndexOf('.')] : "") names.Add(name); + } + + /// Takes out what the woven code adds for the compiler and not for a reader. + private sealed class ReadingForm(IReadOnlySet namespaces) : CSharpSyntaxRewriter + { + private static readonly Dictionary Keywords = new(StringComparer.Ordinal) + { + ["Boolean"] = SyntaxKind.BoolKeyword, ["String"] = SyntaxKind.StringKeyword, ["Int32"] = SyntaxKind.IntKeyword, + ["Int64"] = SyntaxKind.LongKeyword, ["Object"] = SyntaxKind.ObjectKeyword, ["Char"] = SyntaxKind.CharKeyword, + ["Byte"] = SyntaxKind.ByteKeyword, ["Double"] = SyntaxKind.DoubleKeyword, ["Decimal"] = SyntaxKind.DecimalKeyword, + }; + + public override SyntaxNode? VisitAttributeList(AttributeListSyntax node) => null; + + public override SyntaxTrivia VisitTrivia(SyntaxTrivia trivia) => + trivia.IsKind(SyntaxKind.SingleLineCommentTrivia) || trivia.IsKind(SyntaxKind.MultiLineCommentTrivia) ? default : trivia; + + /// + /// A cast goes, and so do the parentheses around its operand where the cast stood alone: a returned + /// value, an initializer, an argument or another cast. + /// + public override SyntaxNode? VisitCastExpression(CastExpressionSyntax node) => + node.Expression is ParenthesizedExpressionSyntax parenthesized + && node.Parent is ReturnStatementSyntax or ArrowExpressionClauseSyntax or EqualsValueClauseSyntax or ArgumentSyntax or CastExpressionSyntax + ? Visit(parenthesized.Expression) + : Visit(node.Expression); + + public override SyntaxNode? VisitMemberAccessExpression(MemberAccessExpressionSyntax node) + { + if (node.Expression is ThisExpressionSyntax) return Visit(node.Name); + + var qualifier = Qualifier(node.Expression); + if (qualifier is not null && namespaces.Contains(qualifier)) return Unqualified(node.Name); + + return base.VisitMemberAccessExpression(node); + } + + public override SyntaxNode? VisitQualifiedName(QualifiedNameSyntax node) + { + var qualifier = Qualifier(node.Left); + return qualifier is not null && namespaces.Contains(qualifier) ? Unqualified(node.Right) : base.VisitQualifiedName(node); + } + + public override SyntaxNode? VisitAliasQualifiedName(AliasQualifiedNameSyntax node) => Unqualified(node.Name); + + /// A name whose namespace was taken off: System.Object is read as object. + private SyntaxNode? Unqualified(SimpleNameSyntax name) => + name is IdentifierNameSyntax identifier && Keywords.TryGetValue(identifier.Identifier.ValueText, out var keyword) + ? Keyword(identifier, keyword) + : Visit(name); + + public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node) => + Keywords.TryGetValue(node.Identifier.ValueText, out var keyword) && !(node.Parent is MemberAccessExpressionSyntax access && access.Name == node) + ? Keyword(node, keyword) + : base.VisitIdentifierName(node); + + private static SyntaxNode Keyword(IdentifierNameSyntax node, SyntaxKind keyword) => + SyntaxFactory.PredefinedType(SyntaxFactory.Token(keyword)).WithTriviaFrom(node); + + /// The dotted text of a name or member-access chain, without global::; null for anything else. + private static string? Qualifier(ExpressionSyntax expression) => + expression switch + { + AliasQualifiedNameSyntax alias => alias.Name.Identifier.ValueText, + IdentifierNameSyntax identifier => identifier.Identifier.ValueText, + QualifiedNameSyntax qualified => Qualifier(qualified.Left) is { } left ? $"{left}.{qualified.Right.Identifier.ValueText}" : null, + MemberAccessExpressionSyntax access => Qualifier(access.Expression) is { } left ? $"{left}.{access.Name.Identifier.ValueText}" : null, + _ => null, + }; + } +} diff --git a/test/CodoMetis.TypeKit.Conventions.Tests/GeneratedHarnessTests.cs b/test/CodoMetis.TypeKit.Conventions.Tests/GeneratedHarnessTests.cs deleted file mode 100644 index 4737a5bb..00000000 --- a/test/CodoMetis.TypeKit.Conventions.Tests/GeneratedHarnessTests.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.Diagnostics; - -namespace CodoMetis.TypeKit.Conventions.Tests; - -/// -/// Every generated file of the probe harness (tools/probe/corpus/harness/*.g.cs) is what its generator writes -/// from the tracked inputs today: the generator is run into a temporary directory and its output compared with the -/// committed file. -/// -/// -/// The generators are file-based apps under tools/probe/generators/ (design/adr/0092, form 1), which have no -/// tests of their own; this is their guard. Without it a hand edit of a .g.cs, or a rows file changed by -/// prober rows without regenerating the declarations, would leave the harness built from something nothing can -/// reproduce, as the Python originals did when they left the repository. The comparison ignores line endings only, -/// since a Windows checkout may write the committed file with CRLF and the generators write LF, as git stores it. -/// -public sealed class GeneratedHarnessTests -{ - private const string Harness = "tools/probe/corpus/harness"; - - private const string Rows = "tools/probe/corpus/rows"; - - private const string Axes = ".claude/probe-axes.md"; - - /// - /// The one place the mapping lives: each generated file, the app that writes it and its arguments, the paths from - /// the repository root. The test adds --out. The EF catalogue is E01's and E02's first round of rows (the - /// rows appended since run on EfShapes3's and EfShapes4's complete grids), so it reads only those. - /// - private static readonly (string File, string App, string[] Arguments)[] Generators = - [ - ("Shapes.g.cs", "shapes.cs", ["--axes", Axes, "--l06", $"{Rows}/L06OffTheWireProbe.json.gz"]), - ("Containers.g.cs", "containers.cs", ["--axes", Axes]), - ("EfShapes.g.cs", "ef.cs", ["catalogue", .. EfCatalogue]), - ("EfShapes3.g.cs", "ef.cs", ["top-level", .. EfCatalogue]), - ("EfShapes4.g.cs", "ef.cs", ["nested", .. EfCatalogue]), - ("KeyShapes.g.cs", "ef.cs", ["keys", "--rows", $"{Rows}/E04IntegerKeyProbe.json.gz"]), - ("JsonFamilyShapes.g.cs", "json-family.cs", ["stand-ins"]), - ("L05Shapes.g.cs", "json-family.cs", ["l05", "--rows", $"{Rows}/L05EqualityProbe.json.gz"]), - ("C09Shapes.g.cs", "decls.cs", ["C09", "--rows", $"{Rows}/C09BaseRecordEqualityProbe.json.gz"]), - ("C11Shapes.g.cs", "decls.cs", ["C11", "--rows", $"{Rows}/C11ParseShapesBuildProbe.json.gz"]), - ("FaultClaimControllers.g.cs", "faults.cs", []), - ("RuleControllers.g.cs", "rules.cs", []), - ]; - - private static string[] EfCatalogue => - [ - "--rows", $"{Rows}/E01ColumnParityProbe.json.gz", "--first", "90", - "--rows", $"{Rows}/E02ValueTranslatesProbe.json.gz", "--first", "108", - ]; - - public static TheoryData Files => [.. Generators.Select(generator => generator.File)]; - - [Fact] - public void Every_generated_file_of_the_harness_has_its_generator() - { - var committed = Directory.GetFiles(Path.Combine(Repository.Root, Harness), "*.g.cs") - .Select(Path.GetFileName).OfType().Order(StringComparer.Ordinal).ToList(); - - committed.Count.ShouldBeGreaterThanOrEqualTo(12, $"{Harness} holds {committed.Count} generated files; it held twelve when this test was written."); - committed.ShouldBe( - Generators.Select(generator => generator.File).Order(StringComparer.Ordinal).ToList(), ignoreOrder: false, - $"Every {Harness}/*.g.cs is written by an app of tools/probe/generators, listed in {nameof(Generators)} with its arguments, and every listed file exists."); - } - - [Theory] - [MemberData(nameof(Files))] - public async Task The_committed_file_is_its_generators_output(string file) - { - var (_, app, arguments) = Generators.Single(generator => generator.File == file); - var directory = Directory.CreateTempSubdirectory("generated-harness-"); - try - { - var output = Path.Combine(directory.FullName, file); - string[] command = ["run", $"tools/probe/generators/{app}", "--", .. arguments, "--out", output]; - using var process = Process.Start(new ProcessStartInfo("dotnet", command) - { - WorkingDirectory = Repository.Root, - RedirectStandardOutput = true, - RedirectStandardError = true, - })!; - - var standardOutput = process.StandardOutput.ReadToEndAsync(TestContext.Current.CancellationToken); - var errors = process.StandardError.ReadToEndAsync(TestContext.Current.CancellationToken); - await process.WaitForExitAsync(TestContext.Current.CancellationToken); - - var invocation = $"dotnet {string.Join(' ', command[..^1])} {Harness}/{file}"; - process.ExitCode.ShouldBe(0, $"`{invocation}` failed:{Environment.NewLine}{await standardOutput}{await errors}"); - - var expected = (await File.ReadAllTextAsync(output, TestContext.Current.CancellationToken)).ReplaceLineEndings("\n"); - var actual = (await File.ReadAllTextAsync(Path.Combine(Repository.Root, Harness, file), TestContext.Current.CancellationToken)).ReplaceLineEndings("\n"); - (actual == expected).ShouldBeTrue( - $"{Harness}/{file} is not what {app} writes from the tracked inputs ({FirstDifference(actual, expected)}). " + - $"Regenerate it with `{invocation}` and commit the result; a hand edit belongs in the generator."); - } - finally - { - directory.Delete(recursive: true); - } - } - - private static string FirstDifference(string committed, string generated) - { - var (a, b) = (committed.Split('\n'), generated.Split('\n')); - for (var line = 0; line < Math.Min(a.Length, b.Length); line++) - { - if (a[line] != b[line]) return $"first difference at line {line + 1}: committed `{Cut(a[line])}`, generated `{Cut(b[line])}`"; - } - return $"the committed file has {a.Length} lines, the generated one {b.Length}"; - } - - private static string Cut(string line) => line.Length > 160 ? line[..160] + "..." : line; -} diff --git a/test/CodoMetis.TypeKit.Conventions.Tests/PackageReleaseNotesTests.cs b/test/CodoMetis.TypeKit.Conventions.Tests/PackageReleaseNotesTests.cs new file mode 100644 index 00000000..74d9386b --- /dev/null +++ b/test/CodoMetis.TypeKit.Conventions.Tests/PackageReleaseNotesTests.cs @@ -0,0 +1,176 @@ +using System.Diagnostics; +using System.Text.RegularExpressions; +using System.Xml.Linq; + +namespace CodoMetis.TypeKit.Conventions.Tests; + +/// +/// Every package's release notes are its version's section of CHANGELOG.md, in the plain text +/// nuget.org shows, followed by one line linking the whole changelog (#75). +/// +/// +/// nuget.org's Release Notes tab and Visual Studio's Updates tab show the nuspec's +/// releaseNotes, frozen with the version once pushed. The ReleaseNotesFromChangelog +/// target in src/Directory.Build.targets writes it at pack. These read the packed nuspecs and +/// derive the expected text again from the changelog, line by line rather than by the target's +/// expressions, so a target that reads another version's section or stops running fails here. A +/// missing or empty section fails the pack itself, and with it the fixture. The version's section +/// may hold no Markdown at all, which would let a target that leaves Markdown in pass, so the target +/// also runs on a sample changelog of every shape, and on samples it must refuse. +/// +/// Plain text, measured on nuget.org on 2026-10-20: release notes are escaped, each line break becomes +/// <br />, and a bare URL is linked; the brackets of [text](url), backticks, ** +/// and # reach the page literally (Bogus 35.6.5, Asp.Versioning.Http 10.2.3), and a URL in +/// parentheses is linked without its closing one (RazorEngine 3.10.0). +/// +/// +[Collection(PacksCollection.Name)] +public sealed partial class PackageReleaseNotesTests(AnalyzerPackagingTests.Packs packs) +{ + public static TheoryData ShippingPackages => [.. Repository.ShippingProjects()]; + + private static XElement Props => XDocument.Load(Path.Combine(Repository.Root, "Directory.Build.props")).Root!; + + private static string Version => Props.Descendants("Version").Single().Value.Trim(); + + private static string Footer => $"Full changelog: {Props.Descendants("PackageProjectUrl").Single().Value.Trim()}/blob/main/CHANGELOG.md"; + + [Theory] + [MemberData(nameof(ShippingPackages))] + public void The_release_notes_are_the_versions_changelog_section(string package) => + ReleaseNotes(package).ShouldBe(Expected(File.ReadAllText(Path.Combine(Repository.Root, "CHANGELOG.md")), Version), + $"{package}'s release notes are not CHANGELOG.md's '## {Version}' section in plain text. The ReleaseNotesFromChangelog target (src/Directory.Build.targets) writes them at pack."); + + [Theory] + [MemberData(nameof(ShippingPackages))] + public void The_release_notes_carry_no_markdown(string package) => + Markdown().Matches(ReleaseNotes(package)).Select(match => match.Value).ShouldBeEmpty( + $"{package}'s release notes contain Markdown, which nuget.org shows literally."); + + /// A section between two others, with every shape the changelog writes, and text MSBuild would expand or unescape. + private const string Sample = """ + # Changelog + + ## 9.9.10 — Unreleased + + - Later: not this version. + + ## 9.9.9 — 2026-01-02 + + ### Fixed + + - Generators: `Parse` on a **span** ([What is generated](https://example.org/g#what)). + - [`Code` in a link](https://example.org/c?a=1&b=2) and 50%3B; $(Foo) @(Bar) %(Baz) 'q' "d" & b — ü. + + ## 9.9.8 — 2026-01-01 + + - Earlier: not this version. + """; + + private const string SamplePlain = """ + Fixed + + - Generators: Parse on a span (What is generated (https://example.org/g#what)). + - Code in a link (https://example.org/c?a=1&b=2) and 50%3B; $(Foo) @(Bar) %(Baz) 'q' "d" & b — ü. + """; + + [Theory] + [InlineData("\n")] + [InlineData("\r\n")] + public async Task The_target_writes_every_shape_as_plain_text(string lineEnding) + { + var changelog = Sample.ReplaceLineEndings(lineEnding); + var expected = $"{SamplePlain.ReplaceLineEndings("\n")}\n\n{Footer}"; + + Expected(changelog, "9.9.9").ShouldBe(expected, "This test's own reading of a section disagrees with the sample's plain text."); + + var (exitCode, output) = await RunTarget(changelog, "9.9.9"); + + exitCode.ShouldBe(0, output); + output.ReplaceLineEndings("\n").Trim().ShouldBe(expected); + } + + public static TheoryData Refused => new() + { + { "no section", "## 9.9.8 — 2026-01-01\n\n- Earlier.\n", "has no '## 9.9.9' section" }, + { "empty section", "## 9.9.9 — Unreleased\n\n## 9.9.8 — 2026-01-01\n\n- Earlier.\n", "'## 9.9.9' section is empty" }, + { "relative link", "## 9.9.9 — Unreleased\n\n- See [the docs](docs/json.md).\n", "relative link" }, + { "too long", $"## 9.9.9 — Unreleased\n\n- {new string('x', 35_000)}\n", "over nuget.org's 35,000" }, + }; + + [Theory] + [MemberData(nameof(Refused))] + public async Task The_target_refuses_a_section_nuget_org_cannot_show(string what, string changelog, string message) + { + var (exitCode, output) = await RunTarget(changelog, "9.9.9"); + + exitCode.ShouldNotBe(0, $"The target accepted a changelog with {what}:{Environment.NewLine}{output}"); + output.ShouldContain(message, Case.Sensitive, $"The target refused a changelog with {what}, but not for that reason:{Environment.NewLine}{output}"); + output.ShouldContain("CHANGELOG.md", Case.Sensitive, "The refusal does not name the changelog."); + } + + /// The section, read line by line from the line after its heading up to the next ## , in plain text, with the footer. + private static string Expected(string changelog, string version) + { + var lines = changelog.ReplaceLineEndings("\n").Split('\n'); + var heading = Array.FindIndex(lines, line => line == $"## {version}" || line.StartsWith($"## {version} ", StringComparison.Ordinal)); + + heading.ShouldBeGreaterThanOrEqualTo(0, $"CHANGELOG.md has no '## {version}' section."); + + var section = string.Join('\n', lines.Skip(heading + 1).TakeWhile(line => !line.StartsWith("## ", StringComparison.Ordinal))).Trim(); + section.ShouldNotBeEmpty($"CHANGELOG.md's '## {version}' section is empty."); + + var plain = section.Split('\n').Select(line => + Link().Replace(line.StartsWith("### ", StringComparison.Ordinal) ? line[4..] : line, "$1 ($2)").Replace("`", "").Replace("**", "")); + + return $"{string.Join('\n', plain)}\n\n{Footer}"; + } + + private string ReleaseNotes(string package) + { + var notes = packs.Nuspec(package).Descendants() + .SingleOrDefault(element => element.Name.LocalName == "releaseNotes" && element.Parent?.Name.LocalName == "metadata"); + + notes.ShouldNotBeNull($"{package} has no in its nuspec."); + return notes.Value; + } + + /// + /// The target alone, on one shipping project, against a changelog of the test's own: what it sets + /// PackageReleaseNotes to, or its refusal. Nothing is built or written beside the project. + /// + private static async Task<(int ExitCode, string Output)> RunTarget(string changelog, string version) + { + var directory = Directory.CreateTempSubdirectory("codometis-typekit-release-notes-"); + try + { + var path = Path.Combine(directory.FullName, "CHANGELOG.md"); + await File.WriteAllTextAsync(path, changelog); + + var project = Path.Combine(Repository.Root, "src", "CodoMetis.TypeKit.Serilog", "CodoMetis.TypeKit.Serilog.csproj"); + using var process = Process.Start(new ProcessStartInfo("dotnet", + ["msbuild", project, "-t:ReleaseNotesFromChangelog", "-getProperty:PackageReleaseNotes", $"-p:ReleaseNotesChangelog={path}", $"-p:Version={version}", "-nodeReuse:false"]) + { + RedirectStandardOutput = true, + RedirectStandardError = true, + })!; + + var output = process.StandardOutput.ReadToEndAsync(); + var errors = process.StandardError.ReadToEndAsync(); + await process.WaitForExitAsync(); + + return (process.ExitCode, await output + await errors); + } + finally + { + directory.Delete(recursive: true); + } + } + + [GeneratedRegex(@"\[([^\]\n]+)\]\((https?://[^)\s]+)\)")] + private static partial Regex Link(); + + /// Inline code, emphasis, a link, or a heading marker at the start of a line. + [GeneratedRegex(@"(?m)`|\*\*|\]\(|^#")] + private static partial Regex Markdown(); +} diff --git a/test/CodoMetis.TypeKit.Conventions.Tests/PortableFileNameTests.cs b/test/CodoMetis.TypeKit.Conventions.Tests/PortableFileNameTests.cs new file mode 100644 index 00000000..a2378df4 --- /dev/null +++ b/test/CodoMetis.TypeKit.Conventions.Tests/PortableFileNameTests.cs @@ -0,0 +1,126 @@ +using System.Diagnostics; +using System.Text; +using System.Text.RegularExpressions; + +namespace CodoMetis.TypeKit.Conventions.Tests; + +/// +/// Every tracked path checks out on Windows and macOS as it does on Linux. A prose-only change runs the Linux leg +/// alone (design/adr/0113), so a file name that Windows refuses, or two names a case-insensitive file system takes +/// for one, would otherwise first fail on the release's pull request (#73). +/// +/// +/// Read from git's index (git ls-files), which can hold names this file system cannot: a test that walked the +/// directory would never see the second of two names that differ only in case. +/// +public sealed partial class PortableFileNameTests +{ + /// + /// The longest repository-relative path allowed. Windows' MAX_PATH is 260; a checkout's own directory takes + /// about 40 characters on a hosted runner and up to about 90 in a worktree under .claude/worktrees/, and + /// the longest tracked path was 102 on 2026-10-29. + /// + private const int LongestPath = 160; + + [Fact] + public void Every_tracked_path_is_a_name_Windows_and_macOS_accept() => + Problems(TrackedPaths()).ShouldBeEmpty("These tracked paths do not check out on every platform the tests run on."); + + [Fact] + public void The_index_is_read() => TrackedPaths().ShouldContain("CodoMetis.TypeKit.slnx"); + + [Theory] + [InlineData("docs/a:b.md", "a character Windows refuses")] + [InlineData("docs/what?.md", "a character Windows refuses")] + [InlineData("docs/tab\there.md", "a character Windows refuses")] + [InlineData("docs/back\\slash.md", "a character Windows refuses")] + [InlineData("docs/trailing.", "ends in a dot or a space")] + [InlineData("docs/trailing /x.md", "ends in a dot or a space")] + [InlineData("docs/con.md", "a device name Windows reserves")] + [InlineData("src/LPT1/x.cs", "a device name Windows reserves")] + [InlineData("docs/Aux", "a device name Windows reserves")] + public void A_name_Windows_refuses_is_found(string path, string problem) => + Problems([path]).ShouldContain(found => found.Contains(problem, StringComparison.Ordinal)); + + [Fact] + public void Two_names_that_differ_only_in_case_are_found() => + Problems(["docs/Guide.md", "docs/guide.md"]).ShouldContain(found => found.Contains("the same name to a case-insensitive", StringComparison.Ordinal)); + + [Fact] + public void Two_directories_that_differ_only_in_case_are_found() => + Problems(["Docs/a.md", "docs/b.md"]).ShouldContain(found => found.Contains("Docs", StringComparison.Ordinal) && found.Contains("docs", StringComparison.Ordinal)); + + [Fact] + public void Two_names_that_differ_only_in_normalization_are_found() => + Problems(["docs/caf\u00e9.md", "docs/cafe\u0301.md"]).ShouldNotBeEmpty(); + + [Fact] + public void A_path_too_long_is_found() => + Problems([$"docs/{new string('a', LongestPath)}.md"]).ShouldContain(found => found.Contains("characters long", StringComparison.Ordinal)); + + [Theory] + [InlineData("docs/console.md")] + [InlineData("docs/a.b.c.md")] + [InlineData(".github/workflows/dotnet.yml")] + [InlineData("design/adr/0113-ci-runs-the-lanes-a-change-can-affect.md")] + public void An_ordinary_name_passes(string path) => Problems([path]).ShouldBeEmpty(); + + private static List Problems(IReadOnlyList paths) + { + var problems = new List(); + foreach (var path in paths) + { + if (path.Length > LongestPath) + problems.Add($"{path} is {path.Length} characters long, over {LongestPath}: with a checkout's own directory it passes Windows' MAX_PATH"); + + foreach (var segment in path.Split('/')) + { + if (segment.Any(c => c < ' ' || "<>:\"|?*\\".Contains(c))) + problems.Add($"{path} holds a character Windows refuses in a file name (<>:\"|?*\\ or a control character)"); + if (segment.EndsWith('.') || segment.EndsWith(' ')) + problems.Add($"{path}: '{segment}' ends in a dot or a space, which Windows drops"); + if (DeviceName().IsMatch(segment)) + problems.Add($"{path}: '{segment}' is a device name Windows reserves, with any extension"); + } + } + + var names = new Dictionary(StringComparer.Ordinal); + foreach (var name in paths.SelectMany(Prefixes).Distinct(StringComparer.Ordinal)) + { + var folded = name.Normalize(NormalizationForm.FormC).ToUpperInvariant(); + if (names.TryGetValue(folded, out var other)) + problems.Add($"{other} and {name} are the same name to a case-insensitive or normalizing file system (Windows, macOS)"); + else + names[folded] = name; + } + + return problems; + } + + // The path and each directory above it: two directories that differ in case collide as two files do. + private static IEnumerable Prefixes(string path) + { + for (var slash = path.IndexOf('/'); slash > 0; slash = path.IndexOf('/', slash + 1)) yield return path[..slash]; + yield return path; + } + + private static List TrackedPaths() + { + using var process = Process.Start(new ProcessStartInfo("git", ["-C", Repository.Root, "-c", "core.quotePath=false", "ls-files", "-z"]) + { + RedirectStandardOutput = true, + RedirectStandardError = true, + StandardOutputEncoding = Encoding.UTF8, + })!; + + var errors = process.StandardError.ReadToEndAsync(); + var output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + + process.ExitCode.ShouldBe(0, $"git ls-files failed: {errors.Result}"); + return [.. output.Split('\0', StringSplitOptions.RemoveEmptyEntries)]; + } + + [GeneratedRegex(@"^(CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])(\..*)?$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + private static partial Regex DeviceName(); +} diff --git a/test/CodoMetis.TypeKit.Conventions.Tests/ProbeCiteTests.cs b/test/CodoMetis.TypeKit.Conventions.Tests/ProbeCiteTests.cs deleted file mode 100644 index de7969a7..00000000 --- a/test/CodoMetis.TypeKit.Conventions.Tests/ProbeCiteTests.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System.Text.Json.Nodes; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace CodoMetis.TypeKit.Conventions.Tests; - -/// -/// Every reason a check gives for not applying or for meeting a refusal is cited, and what it cites exists: in the -/// probe corpus, the first argument of a check's Inapplicable or Refused is a Cite factory -/// (NotPromised, Docs, Fact, Record) called with a string literal, and a -/// NotPromised id is a not-promised entry of the probe's claim (of any claim, for a harness source no one -/// probe owns), a Docs source names a file of the repository, a Fact an entry under design/knowledge/ -/// and a Record a file under design/adr/. -/// -/// -/// The factories check the form when a check runs; this holds, before any build, that an excuse names a reason a -/// reader can find. `prober register` and `publish` hold the same for the staged sources (SourceLint). It reads -/// syntax, so a call in a comment or a string does not count, and the positive control finds each rule's defect. No -/// floor yet: no probe calls a check's Inapplicable or Refused until the conversion that starts -/// with the per-check evidence. -/// -public sealed class ProbeCiteTests -{ - private static string Corpus => Path.Combine(Repository.Root, "tools", "probe", "corpus"); - - [Fact] - public void Every_citation_in_the_corpus_is_a_factory_with_a_literal_naming_what_exists() - { - var context = Context.Of(Repository.Root); - Directory.EnumerateFiles(Corpus, "*.cs", SearchOption.AllDirectories).Order(StringComparer.Ordinal) - .SelectMany(path => Problems(Path.GetRelativePath(Repository.Root, path).Replace('\\', '/'), File.ReadAllText(path), context)) - .ShouldBeEmpty(); - } - - [Fact] - public void A_citation_built_from_text_or_naming_nothing_is_found() - { - const string source = - """ - class P1Probe - { - string? Check(Check c, string x, Entry e) - { - var refused = e.Refused(); - c.Inapplicable(Cite.NotPromised("c1-limit"), x); - c.Refused(Cite.Docs("README.md#what-typekit-guarantees")); - c.Refused(Cite.Fact("KB-001")); - c.Inapplicable(Cite.Record("0094"), "detail"); - var cite = Cite.NotPromised("c1-limit"); - c.Inapplicable(cite); - c.Refused(Cite.NotPromised("c1-" + x)); - c.Inapplicable(Cite.NotPromised("c2-limit")); - c.Refused(Cite.Docs("docs/gone.md#a")); - c.Refused(Cite.Fact("KB-999")); - c.Inapplicable(Cite.Record("9999")); - // c.Inapplicable(cite); - var text = "c.Inapplicable(cite)"; - return null; - } - } - """; - var context = new Context(Repository.Root, - new Dictionary(StringComparer.Ordinal) { ["P1Probe"] = "C1" }, - new Dictionary>(StringComparer.Ordinal) { ["C1"] = ["c1-limit"], ["C2"] = ["c2-limit"] }); - Problems("probes/P1Probe.cs", source, context).ShouldBe( - [ - "probes/P1Probe.cs:11: Inapplicable(cite, ...) does not cite by a Cite factory with a string literal", - "probes/P1Probe.cs:12: Refused(Cite.NotPromised(\"c1-\" + x), ...) does not cite by a Cite factory with a string literal", - "probes/P1Probe.cs:13: Inapplicable(Cite.NotPromised(\"c2-limit\"), ...) names no not-promised entry of its claim C1", - "probes/P1Probe.cs:14: Refused(Cite.Docs(\"docs/gone.md#a\"), ...) names no file docs/gone.md in the repository", - "probes/P1Probe.cs:15: Refused(Cite.Fact(\"KB-999\"), ...) names no entry under design/knowledge/", - "probes/P1Probe.cs:16: Inapplicable(Cite.Record(\"9999\"), ...) names no record under design/adr/", - ]); - Problems("harness/Shared.cs", source.Replace("P1Probe", "Shared"), context) - .ShouldNotContain(p => p.Contains("c2-limit"), "a harness source no one probe owns may cite any claim's entry"); - } - - /// The repository's files, each claim's not-promised ids, and the claim of each probe by its name and by the class its filter selects. - private sealed record Context(string Root, Dictionary ClaimOf, Dictionary> NotPromised) - { - public static Context Of(string root) - { - var corpus = Path.Combine(root, "tools", "probe", "corpus"); - var notPromised = new Dictionary>(StringComparer.Ordinal); - foreach (var claim in JsonNode.Parse(File.ReadAllText(Path.Combine(corpus, "claims.json")))!.AsArray().OfType()) - notPromised[claim["id"]!.GetValue()] = [.. (claim["not-promised"] as JsonArray ?? []).Select(n => n!["id"]!.GetValue())]; - var claimOf = new Dictionary(StringComparer.Ordinal); - foreach (var (name, entry) in JsonNode.Parse(File.ReadAllText(Path.Combine(corpus, "registry.json")))!["probes"]!.AsObject()) - { - var claim = entry!["claim"]!.GetValue(); - claimOf[name] = claim; - if (entry["filter"]?.GetValue().TrimStart('*') is { Length: > 0 } cls) claimOf.TryAdd(cls, claim); - } - return new Context(root, claimOf, notPromised); - } - } - - private static IEnumerable Problems(string path, string text, Context context) - { - var claim = context.ClaimOf.GetValueOrDefault(Path.GetFileNameWithoutExtension(path)); - foreach (var call in CSharpSyntaxTree.ParseText(text).GetRoot().DescendantNodes().OfType()) - { - if (call.Expression is not MemberAccessExpressionSyntax { Name.Identifier.ValueText: var method and ("Inapplicable" or "Refused") }) continue; - var arguments = call.ArgumentList.Arguments; - if (arguments.Count == 0) continue; // an entry's own Refused() of the harness, not a check's - var line = call.GetLocation().GetLineSpan().StartLinePosition.Line + 1; - var shown = $"{path}:{line}: {method}({arguments[0]}, ...)"; - if (arguments[0].Expression is not InvocationExpressionSyntax - { - Expression: MemberAccessExpressionSyntax { Expression: IdentifierNameSyntax { Identifier.ValueText: "Cite" }, Name.Identifier.ValueText: var factory and ("NotPromised" or "Docs" or "Fact" or "Record") }, - ArgumentList.Arguments: [{ Expression: LiteralExpressionSyntax literal }], - } || !literal.IsKind(SyntaxKind.StringLiteralExpression)) - { - yield return $"{shown} does not cite by a Cite factory with a string literal"; - continue; - } - var value = literal.Token.ValueText; - var missing = factory switch - { - "NotPromised" when claim is not null => context.NotPromised.GetValueOrDefault(claim)?.Contains(value) ?? false ? null : $"names no not-promised entry of its claim {claim}", - "NotPromised" => context.NotPromised.Values.Any(ids => ids.Contains(value)) ? null : "names no not-promised entry of any claim", - "Docs" => value.Split('#')[0] is var file && file.Length > 0 && !file.Contains("..", StringComparison.Ordinal) && File.Exists(Path.Combine(context.Root, file)) - ? null : $"names no file {value.Split('#')[0]} in the repository", - "Fact" => Named(Path.Combine(context.Root, "design", "knowledge"), value) ? null : "names no entry under design/knowledge/", - _ => Named(Path.Combine(context.Root, "design", "adr"), value) ? null : "names no record under design/adr/", - }; - if (missing is not null) yield return $"{shown} {missing}"; - } - } - - private static bool Named(string directory, string id) => - id.Length > 0 && Directory.Exists(directory) - && Directory.EnumerateFiles(directory, "*.md").Any(f => Path.GetFileName(f).StartsWith(id + "-", StringComparison.Ordinal) || Path.GetFileName(f) == id + ".md"); -} diff --git a/test/CodoMetis.TypeKit.Conventions.Tests/ProbeMessageTextTests.cs b/test/CodoMetis.TypeKit.Conventions.Tests/ProbeMessageTextTests.cs deleted file mode 100644 index c0e67120..00000000 --- a/test/CodoMetis.TypeKit.Conventions.Tests/ProbeMessageTextTests.cs +++ /dev/null @@ -1,148 +0,0 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace CodoMetis.TypeKit.Conventions.Tests; - -/// -/// No probe decides how a row ends by the words of a message, except to fail it: in the probe corpus (harness and probes), a -/// .Message.Contains(, StartsWith(, EndsWith( or IndexOf( stands only in the condition of a failure text, -/// an if whose branch returns one or raises a harness error, or a ?: between null and one. A refusal the framework -/// makes on its own is shown by side (1) of the three-sided differential (the harness's Sides), compared by its type and stage; -/// an intended difference by a citation. A message read to excuse a row, the substring excuse decision 99 retired, is refused here. -/// -/// -/// It reads syntax, as does: a lambda inside the condition counts as the condition (errors.Any(e => -/// e.Message.StartsWith(...))), an assignment, a catch ... when or a branch that passes does not. The EF family stays -/// two-sided in WS2's last step and is not changed by it: its one excuse by a message is listed in with its reason, -/// and a listed site that no longer reads a message fails, so the list cannot outlive it. -/// -public sealed class ProbeMessageTextTests -{ - private static string Corpus => Path.Combine(Repository.Root, "tools", "probe", "corpus"); - - private static readonly string[] Reads = ["Contains", "StartsWith", "EndsWith", "IndexOf"]; - - /// The sites kept, by file, each with why: the EF family is two-sided by decision of WS2's last step. - private static readonly Dictionary Kept = new() - { - ["tools/probe/corpus/probes/E06OwnConverterNoValueProbe.cs"] = - "EF stays two-sided in WS2's last step: EF's 'could not be translated' refusal is matched by its words until the EF family gets its side (1)", - }; - - [Fact] - public void The_corpus_reads_of_a_message_are_found() => - Sources().Sum(source => Sites(source.Text).Count()).ShouldBeGreaterThanOrEqualTo(30, - "the corpus read a message's words at 33 places when this rule was written (WS2's three-sided differential), all but the one EF site a failure's condition"); - - [Fact] - public void No_message_text_decides_a_row_but_a_failure() => - Sources().Where(source => !Kept.ContainsKey(source.Path)).SelectMany(source => Problems(source.Path, source.Text)).ShouldBeEmpty(); - - [Fact] - public void Each_kept_site_still_reads_a_message_outside_a_failure() => - Kept.Keys.Where(path => !Problems(path, File.ReadAllText(Path.Combine(Repository.Root, path))).Any()).ShouldBeEmpty(); - - [Fact] - public void A_message_read_to_excuse_a_row_is_found() - { - const string source = - """ - class P - { - string? Check(Check c, Exception x, Exception? y, string[] names) - { - if (x.Message.Contains("maximum allowed depth")) { c.Note(NoteKeys.StjMaxdepth); return null; } - if (!x.Message.Contains("Refused")) return $"does not name the fault: {x.Message}"; - if (x.InnerException is { } i && i.Message.Contains("secret")) return "inner quotes the input"; - if (!x.Message.StartsWith("V") || x.Message.EndsWith(".")) return "not named: " + x.Message; - if (names.Any(n => !x.Message.Contains(n))) { r.Outcome("harness-error", "not named"); return null; } - var named = x.Message.Contains("V"); - try { throw x; } catch (Exception e) when (e.Message.Contains("could not be translated")) { return null; } - var missing = names.Where(n => !x.Message.Contains(n)).ToList(); - return x.Message.Contains("V") ? null : $"not named: {x.Message}"; - // x.Message.Contains("in a comment") is no call - } - } - """; - Problems("P.cs", source).ShouldBe( - [ - "P.cs:5: x.Message.Contains(\"maximum allowed depth\") decides a row by a message's words outside a failure", - "P.cs:10: x.Message.Contains(\"V\") decides a row by a message's words outside a failure", - "P.cs:11: e.Message.Contains(\"could not be translated\") decides a row by a message's words outside a failure", - "P.cs:12: x.Message.Contains(n) decides a row by a message's words outside a failure", - ]); - } - - private static List<(string Path, string Text)> Sources() => - [ - .. Directory.EnumerateFiles(Corpus, "*.cs", SearchOption.AllDirectories) - .Order(StringComparer.Ordinal) - .Select(path => (Path.GetRelativePath(Repository.Root, path).Replace('\\', '/'), File.ReadAllText(path))) - ]; - - /// Every X.Message.Contains(...) and its kin in a source. - private static IEnumerable Sites(string text) => - CSharpSyntaxTree.ParseText(text).GetRoot().DescendantNodes().OfType() - .Where(call => call.Expression is MemberAccessExpressionSyntax - { - Name.Identifier.ValueText: var read, - Expression: MemberAccessExpressionSyntax { Name.Identifier.ValueText: "Message" }, - } && Reads.Contains(read)); - - private static IEnumerable Problems(string path, string text) - { - foreach (var call in Sites(text)) - if (!InAFailuresCondition(call)) - yield return $"{path}:{call.GetLocation().GetLineSpan().StartLinePosition.Line + 1}: {call} decides a row by a message's words outside a failure"; - } - - /// - /// The read stands in the condition of an if whose branch fails the row (returns a failure text, or raises a harness error), or - /// of a ?: between null and a failure text. A lambda on the way up is part of that condition. - /// - private static bool InAFailuresCondition(SyntaxNode read) - { - for (var node = read; node.Parent is { } parent; node = parent) - { - switch (parent) - { - case IfStatementSyntax branch when branch.Condition == node: - return Fails(branch.Statement); - case ConditionalExpressionSyntax choice when choice.Condition == node: - return (IsNull(choice.WhenTrue) && IsFailure(choice.WhenFalse)) || (IsNull(choice.WhenFalse) && IsFailure(choice.WhenTrue)); - case StatementSyntax: - case CatchFilterClauseSyntax: - return false; - } - } - - return false; - } - - /// A branch that fails the row: it returns a failure text, or raises a harness error, first. - private static bool Fails(StatementSyntax statement) => statement switch - { - ReturnStatementSyntax { Expression: { } returned } => IsFailure(returned), - BlockSyntax { Statements: [var first, ..] } => Fails(first), - ExpressionStatementSyntax { Expression: InvocationExpressionSyntax raise } => RaisesAHarnessError(raise), - _ => false, - }; - - private static bool RaisesAHarnessError(InvocationExpressionSyntax call) => - call.Expression is MemberAccessExpressionSyntax { Name.Identifier.ValueText: "Outcome" } - && call.ArgumentList.Arguments is [{ Expression: LiteralExpressionSyntax { Token.ValueText: "harness-error" } }, ..]; - - /// A failure text: a string, an interpolated one, a concatenation with one, or a ?: one of whose branches is one. - private static bool IsFailure(ExpressionSyntax expression) => expression switch - { - LiteralExpressionSyntax literal => literal.IsKind(SyntaxKind.StringLiteralExpression), - InterpolatedStringExpressionSyntax => true, - BinaryExpressionSyntax add when add.IsKind(SyntaxKind.AddExpression) => IsFailure(add.Left) || IsFailure(add.Right), - ConditionalExpressionSyntax choice => IsFailure(choice.WhenTrue) || IsFailure(choice.WhenFalse), - ParenthesizedExpressionSyntax inner => IsFailure(inner.Expression), - _ => false, - }; - - private static bool IsNull(ExpressionSyntax expression) => expression.IsKind(SyntaxKind.NullLiteralExpression); -} diff --git a/test/CodoMetis.TypeKit.Conventions.Tests/ProbeNoteKeyTests.cs b/test/CodoMetis.TypeKit.Conventions.Tests/ProbeNoteKeyTests.cs deleted file mode 100644 index 8876ae13..00000000 --- a/test/CodoMetis.TypeKit.Conventions.Tests/ProbeNoteKeyTests.cs +++ /dev/null @@ -1,94 +0,0 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace CodoMetis.TypeKit.Conventions.Tests; - -/// -/// Every note a probe makes names a key of the catalogue, and every skip names its finding as written -/// (decision 103): in the probe corpus, the first argument of a Note call is a member of -/// NoteKeys, and the first argument of a two-argument Skip (the harness's, not LINQ's) is a -/// string literal. -/// -/// -/// The compiler already refuses a note key outside the catalogue, since NoteKeys has a private -/// constructor; this holds the shape for the sources the replay overlays, before any build, and for -/// Skip, whose finding id stays a string. It reads syntax, so a call in a comment or a string does -/// not count, and the positive control and the floor keep it from passing because the walker stopped -/// finding anything. -/// -public sealed class ProbeNoteKeyTests -{ - private static string Corpus => Path.Combine(Repository.Root, "tools", "probe", "corpus"); - - [Fact] - public void The_corpus_notes_are_found() => - Calls(Sources()).Count(c => c.Method == "Note").ShouldBeGreaterThanOrEqualTo(100, - "the corpus made 136 notes when this rule was written, 127 once the http family's outcome notes became checks' citations (WS2 step 4), " + - "and 119 at WS2's close, where every note is a pass key or framework-refuses"); - - [Fact] - public void Every_note_and_skip_in_the_corpus_names_its_key_as_written() => - Sources().SelectMany(source => Problems(source.Path, source.Text)).ShouldBeEmpty(); - - [Fact] - public void A_key_or_a_finding_built_from_text_is_found() - { - const string source = - """ - class P - { - void Check(Row r, string x) - { - r.Note("msg:" + x, "detail"); - r.Note(NoteKeys.Compared, "fine"); - r.Skip("F" + x, "why"); - r.Skip("F7", "fine"); - var rest = new[] { 1, 2 }.Skip(x.Length); - // r.Note("in a comment", x); - } - } - """; - Problems("P.cs", source).ShouldBe( - [ - "P.cs:5: Note(\"msg:\" + x, ...) does not name a key of NoteKeys", - "P.cs:7: Skip(\"F\" + x, ...) does not name its finding as a string literal", - ]); - } - - private static List<(string Path, string Text)> Sources() => - [ - .. Directory.EnumerateFiles(Corpus, "*.cs", SearchOption.AllDirectories) - .Order(StringComparer.Ordinal) - .Select(path => (Path.GetRelativePath(Repository.Root, path).Replace('\\', '/'), File.ReadAllText(path))) - ]; - - private static IEnumerable<(string Method, InvocationExpressionSyntax Call)> Calls(IEnumerable<(string Path, string Text)> sources) => - sources.SelectMany(source => Calls(source.Text)); - - private static IEnumerable<(string Method, InvocationExpressionSyntax Call)> Calls(string text) => - CSharpSyntaxTree.ParseText(text).GetRoot().DescendantNodes().OfType() - .Select(call => (Method: call.Expression switch - { - MemberAccessExpressionSyntax member => member.Name.Identifier.ValueText, - IdentifierNameSyntax name => name.Identifier.ValueText, - _ => "", - }, Call: call)) - .Where(c => c.Method is "Note" or "Skip"); - - private static IEnumerable Problems(string path, string text) - { - foreach (var (method, call) in Calls(text)) - { - var arguments = call.ArgumentList.Arguments; - var line = call.GetLocation().GetLineSpan().StartLinePosition.Line + 1; - if (method == "Note" && arguments.Count > 0 && !IsCatalogueKey(arguments[0].Expression)) - yield return $"{path}:{line}: Note({arguments[0]}, ...) does not name a key of NoteKeys"; - if (method == "Skip" && arguments.Count == 2 && !arguments[0].Expression.IsKind(SyntaxKind.StringLiteralExpression)) - yield return $"{path}:{line}: Skip({arguments[0]}, ...) does not name its finding as a string literal"; - } - } - - private static bool IsCatalogueKey(ExpressionSyntax expression) => - expression is MemberAccessExpressionSyntax { Expression: IdentifierNameSyntax { Identifier.ValueText: "NoteKeys" } }; -} diff --git a/test/CodoMetis.TypeKit.Conventions.Tests/ProbeRowsTests.cs b/test/CodoMetis.TypeKit.Conventions.Tests/ProbeRowsTests.cs deleted file mode 100644 index 4d424afd..00000000 --- a/test/CodoMetis.TypeKit.Conventions.Tests/ProbeRowsTests.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System.IO.Compression; -using System.Text.Json.Nodes; - -namespace CodoMetis.TypeKit.Conventions.Tests; - -/// -/// The probe corpus keeps its rows per probe (tools/probe/corpus/rows/<probe>.json.gz): every registered -/// probe has a rows file named for it, each file names its probe's claim, a claim of claims.json, and no two rows of -/// one file have the same axis map. -/// -/// -/// A shared rows file tied two probes of a claim together, so rows appended for one changed the other's identity, and -/// appending instead of regenerating left duplicates (C02 42 of 88, C20 90 of 351), each a row run again for nothing. A -/// rows file named for no registered probe is a leftover no replay runs. The positive control holds each rule to finding -/// its defect, and the floor keeps the walker from passing by finding no file. -/// -public sealed class ProbeRowsTests -{ - private static string Corpus => Path.Combine(Repository.Root, "tools", "probe", "corpus"); - - [Fact] - public void The_rows_files_are_found() => - RowsFiles().Count.ShouldBeGreaterThanOrEqualTo(43, "the corpus had a rows file for each of its 43 probes when this rule was written"); - - [Fact] - public void Every_probe_has_rows_of_its_own_each_axis_map_once() => - Problems(RowsFiles(), Claims(), Registry()).ShouldBeEmpty(); - - [Fact] - public void A_duplicate_row_a_wrong_claim_a_leftover_file_and_a_probe_without_rows_are_found() - { - static Dictionary Row(string x, string y) => new(StringComparer.Ordinal) { ["x"] = x, ["y"] = y }; - var files = new Dictionary> Rows)>(StringComparer.Ordinal) - { - ["P"] = ("C1", [Row("1", "1"), Row("1", "2"), new(StringComparer.Ordinal) { ["y"] = "1", ["x"] = "1" }]), - ["Q"] = ("C9", [Row("1", "1")]), - ["R"] = ("C1", [Row("1", "1")]), - ["Old"] = ("C1", [Row("2", "2")]), - ["N"] = (null, [Row("2", "2")]), - }; - var registry = new Dictionary(StringComparer.Ordinal) { ["P"] = "C1", ["Q"] = "C9", ["R"] = "C2", ["N"] = "C1", ["S"] = "C1" }; - Problems(files, new HashSet(StringComparer.Ordinal) { "C1", "C2" }, registry).ShouldBe( - [ - "rows/N.json.gz names no claim", - "rows/Old.json.gz names no registered probe", - "rows/P.json.gz: row 2 has the axis map of row 0", - "rows/Q.json.gz names claim C9, which claims.json does not have", - "rows/R.json.gz names claim C1, but its probe R is a probe of C2", - "S is a registered probe without a rows file (rows/S.json.gz)", - ]); - } - - /// Every rule's problems, in ordinal order: files by name, then the probes without a file. - private static List Problems(IReadOnlyDictionary> Rows)> files, - IReadOnlySet claims, IReadOnlyDictionary registry) - { - var problems = new List(); - foreach (var (probe, (claim, rows)) in files.OrderBy(f => f.Key, StringComparer.Ordinal)) - { - var name = $"rows/{probe}.json.gz"; - if (claim is null) problems.Add($"{name} names no claim"); - else if (!claims.Contains(claim)) problems.Add($"{name} names claim {claim}, which claims.json does not have"); - if (!registry.TryGetValue(probe, out var registered)) problems.Add($"{name} names no registered probe"); - else if (claim is not null && claims.Contains(claim) && registered != claim) problems.Add($"{name} names claim {claim}, but its probe {probe} is a probe of {registered}"); - var first = new Dictionary(StringComparer.Ordinal); - for (var i = 0; i < rows.Count; i++) - { - var key = string.Join("\n", rows[i].OrderBy(kv => kv.Key, StringComparer.Ordinal).Select(kv => kv.Key + "=" + kv.Value)); - if (first.TryGetValue(key, out var earlier)) problems.Add($"{name}: row {i} has the axis map of row {earlier}"); - else first[key] = i; - } - } - foreach (var probe in registry.Keys.Where(p => !files.ContainsKey(p)).Order(StringComparer.Ordinal)) - problems.Add($"{probe} is a registered probe without a rows file (rows/{probe}.json.gz)"); - return problems; - } - - /// Every rows file of the corpus by its probe's name, with the claim it names and its rows' axis maps. - private static Dictionary> Rows)> RowsFiles() - { - var files = new Dictionary>)>(StringComparer.Ordinal); - foreach (var path in Directory.EnumerateFiles(Path.Combine(Corpus, "rows")).Order(StringComparer.Ordinal)) - { - var name = Path.GetFileName(path); - name.ShouldEndWith(".json.gz", Case.Sensitive, $"rows/{name}: the corpus keeps its rows gzipped, one file per probe"); - using var gz = new GZipStream(File.OpenRead(path), CompressionMode.Decompress); - var document = JsonNode.Parse(gz)!; - var claim = document["claim"] is JsonValue c && c.TryGetValue(out var s) ? s : null; - var rows = document["rows"]!.AsArray() - .Select(r => r!["row"]!.AsObject().ToDictionary(kv => kv.Key, kv => kv.Value!.GetValue(), StringComparer.Ordinal)) - .ToList(); - files[name[..^".json.gz".Length]] = (claim, rows); - } - return files; - } - - private static HashSet Claims() => - [.. JsonNode.Parse(File.ReadAllText(Path.Combine(Corpus, "claims.json")))!.AsArray().Select(c => c!["id"]!.GetValue())]; - - private static Dictionary Registry() => - JsonNode.Parse(File.ReadAllText(Path.Combine(Corpus, "registry.json")))!["probes"]!.AsObject() - .ToDictionary(kv => kv.Key, kv => kv.Value!["claim"]!.GetValue(), StringComparer.Ordinal); -} diff --git a/test/CodoMetis.TypeKit.Conventions.Tests/ProseReaderTests.cs b/test/CodoMetis.TypeKit.Conventions.Tests/ProseReaderTests.cs new file mode 100644 index 00000000..97f85eef --- /dev/null +++ b/test/CodoMetis.TypeKit.Conventions.Tests/ProseReaderTests.cs @@ -0,0 +1,119 @@ +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace CodoMetis.TypeKit.Conventions.Tests; + +/// +/// A prose-only change builds and runs only the test projects of test/Prose.slnf (design/adr/0113). So no test +/// project outside it may read prose: a file under docs/, design/, site/ or .github/, or a +/// Markdown file outside src/. One that did would go untested by the very change that breaks it, silently. +/// +/// +/// Read from the sources, by the string literals that name such a path: "docs", "design", +/// "site", ".github", a literal starting with one of them and a slash, and one ending in .md. A +/// package's README is not prose (the selection classes it with its package), so "README.md" passes in a file +/// that also names "src", as Path.Combine(root, "src", package, "README.md") does, and a literal holding src/ passes. +/// A literal that names such a path without reading it is waived below, with the reason. +/// +public sealed partial class ProseReaderTests +{ + private const string Filter = "test/Prose.slnf"; + + /// Files whose prose-shaped literals are not reads, each with the reason. + private static readonly Dictionary Waived = new(StringComparer.Ordinal) + { + ["test/CodoMetis.TypeKit.CiLanes.Tests/SelectionTests.cs"] = "paths handed to the classifier and written into a throwaway repository", + ["test/CodoMetis.TypeKit.BashGuard.Tests/GuardTests.cs"] = "command lines the guard judges, never run", + ["test/CodoMetis.TypeKit.CiLanes.Tests/GateTests.cs"] = "a job named docs the gate must refuse, and a summary file in the sandbox", + ["test/CodoMetis.TypeKit.PrHygiene.Tests/HygieneTests.cs"] = "branch names such as docs/plain-voice, which name no file", + }; + + private static IReadOnlyList ProseProjects() + { + using var filter = JsonDocument.Parse(File.ReadAllText(Path.Combine(Repository.Root, Filter))); + return + [ + .. filter.RootElement.GetProperty("solution").GetProperty("projects").EnumerateArray() + .Select(project => Path.GetDirectoryName(project.GetString()!)!.Replace('\\', '/')) + ]; + } + + [Fact] + public void The_prose_suite_holds_the_projects_that_read_the_documents() + { + var projects = ProseProjects(); + + foreach (var project in projects) + Directory.Exists(Path.Combine(Repository.Root, project)).ShouldBeTrue($"{Filter} names {project}, which does not exist."); + projects.ShouldContain("test/CodoMetis.TypeKit.Conventions.Tests", $"{Filter} lost the conventions tests, which read every document's links."); + projects.ShouldContain("test/CodoMetis.TypeKit.Samples", $"{Filter} lost the samples, which compile the documents' code blocks."); + } + + [Fact] + public void No_test_project_outside_the_prose_suite_reads_prose() + { + var prose = ProseProjects().ToHashSet(StringComparer.Ordinal); + var readers = new List(); + + foreach (var project in Directory.GetDirectories(Path.Combine(Repository.Root, "test"))) + { + var relative = Path.GetRelativePath(Repository.Root, project).Replace('\\', '/'); + if (prose.Contains(relative) || Directory.GetFiles(project, "*.csproj").Length == 0) continue; + + foreach (var file in SourceFiles(project)) + { + var path = Path.GetRelativePath(Repository.Root, file).Replace('\\', '/'); + if (Waived.ContainsKey(path)) continue; + + var found = Prose(File.ReadAllText(file)).ToList(); + if (found.Count > 0) readers.Add($"{path} ({string.Join(", ", found)})"); + } + } + + readers.ShouldBeEmpty( + $"These test sources name prose, which a prose-only change does not run them for. Add their project to {Filter}, " + + "or waive the file in ProseReaderTests.Waived with the reason it reads nothing."); + } + + [Theory] + [InlineData("""File.ReadAllText(Path.Combine(root, "docs", "json.md"));""", true)] + [InlineData("""var page = "design/adr/README.md";""", true)] + [InlineData("""Path.Combine(root, ".github", "workflows", "dotnet.yml")""", true)] + [InlineData("""Path.Combine(root, "site", "toc.yml")""", true)] + [InlineData("""Path.Combine(root, "CHANGELOG.md")""", true)] + [InlineData("""Path.Combine(root, "README.md")""", true)] + [InlineData("""Path.Combine(root, "src", "CodoMetis.TypeKit", "README.md")""", false)] + [InlineData("""const string Link = "https://github.com/o/r/blob/main/src/CodoMetis.TypeKit.Analyzers/README.md";""", false)] + [InlineData("""rule.HelpLinkUri.ShouldEndWith("README.md#cmtk0014");""", false)] + [InlineData("""var name = "documented";""", false)] + public void A_literal_that_names_prose_is_found(string source, bool reads) => + Prose(source).Any().ShouldBe(reads); + + private static IEnumerable Prose(string source) + { + var literals = Literal().Matches(source).Select(match => match.Groups["text"].Value).ToList(); + var namesSrc = literals.Contains("src"); + + foreach (var literal in literals) + { + var directory = literal is "docs" or "design" or "site" or ".github" + || literal.StartsWith("docs/", StringComparison.Ordinal) + || literal.StartsWith("design/", StringComparison.Ordinal) + || literal.StartsWith("site/", StringComparison.Ordinal) + || literal.StartsWith(".github/", StringComparison.Ordinal); + var markdown = literal.EndsWith(".md", StringComparison.OrdinalIgnoreCase) + && !literal.Contains("src/", StringComparison.Ordinal) + && !(literal == "README.md" && namesSrc); + + if (directory || markdown) yield return $"\"{literal}\""; + } + } + + private static IEnumerable SourceFiles(string project) => + Directory.EnumerateFiles(project, "*.cs", SearchOption.AllDirectories) + .Where(file => !Path.GetRelativePath(project, file).Split(Path.DirectorySeparatorChar).Any(part => part is "bin" or "obj")); + + // A regular string literal's text; a verbatim or raw one is matched as far as its first quote, which is enough for a path. + [GeneratedRegex(@"""(?(?:[^""\\\r\n]|\\.)*)""")] + private static partial Regex Literal(); +} diff --git a/test/CodoMetis.TypeKit.Conventions.Tests/ReadmeSampleTests.cs b/test/CodoMetis.TypeKit.Conventions.Tests/ReadmeSampleTests.cs index 82b5727d..676e5d26 100644 --- a/test/CodoMetis.TypeKit.Conventions.Tests/ReadmeSampleTests.cs +++ b/test/CodoMetis.TypeKit.Conventions.Tests/ReadmeSampleTests.cs @@ -2,8 +2,9 @@ namespace CodoMetis.TypeKit.Conventions.Tests; /// /// Every C# block in the READMEs and the topic pages under docs/ is compiled code: it equals, -/// after indentation, exactly one sample region in test/CodoMetis.TypeKit.Samples, and every -/// region there is shown in the page its name starts with. A README edit that is not mirrored in the samples fails here, and a sample that +/// after indentation, exactly one sample region in test/CodoMetis.TypeKit.Samples, or in the sources a +/// page names for itself (: the walkthrough's sample application), +/// and every region is shown in the page its name starts with. A README edit that is not mirrored in the samples fails here, and a sample that /// stops compiling fails the build. /// /// @@ -44,6 +45,7 @@ public void The_docs_pages_are_discovered() => [InlineData("README.md", "README")] [InlineData("src/CodoMetis.TypeKit.EntityFrameworkCore/README.md", "CodoMetis.TypeKit.EntityFrameworkCore")] [InlineData("docs/ef-core.md", "ef-core")] + [InlineData("docs/walkthrough.md", "walkthrough")] public void A_page_s_samples_are_named_after_it(string page, string prefix) => ReadmeSamples.Prefix(page).ShouldBe(prefix); /// @@ -92,6 +94,36 @@ public void The_csharp_blocks_and_the_samples_are_discovered() Samples.Value.Count.ShouldBeGreaterThanOrEqualTo(30); } + /// + /// The walkthrough's sample application is found: a directory renamed would leave its page's blocks matching + /// nothing, which the theories report, but a page that lost its blocks with it would pass. + /// + [Fact] + public void Each_page_s_own_sources_hold_its_samples() + { + foreach (var (prefix, directory) in ReadmeSamples.RegionSources) + { + Directory.Exists(Path.Combine(Repository.Root, directory)).ShouldBeTrue($"{directory}, where the '{prefix}' samples live, does not exist."); + Samples.Value.Count(sample => sample.ReadmePrefix == prefix).ShouldBeGreaterThanOrEqualTo(10, $"{directory} holds too few '{prefix}/' samples."); + } + } + + /// + /// A page's region lives in the sources its prefix names, and nowhere else: a walkthrough region in the samples + /// project would be compiled where the packed packages never build it, and a samples region among the + /// walkthrough's sources would be shipped to its consumer. + /// + [Theory] + [MemberData(nameof(SampleNames))] + public void A_sample_lives_in_the_sources_of_its_page(string name) + { + var sample = Samples.Value.Single(candidate => candidate.Name == name); + var expected = ReadmeSamples.SourcesOf(sample.ReadmePrefix); + + sample.File.ShouldStartWith(expected + "/", Case.Sensitive, + $"'{name}' ({sample.Location}) belongs to the page '{sample.ReadmePrefix}', whose samples live in {expected}."); + } + [Theory] [MemberData(nameof(CompiledBlocks))] public void A_csharp_block_in_a_README_is_a_compiled_sample(string location) @@ -102,7 +134,7 @@ public void A_csharp_block_in_a_README_is_a_compiled_sample(string location) if (sample is null) { throw new ShouldAssertException( - $"{location} is not compiled: no sample in {ReadmeSamples.SamplesProject} holds this code. Mirror the change in the sample region, " + + $"{location} is not compiled: no sample in {ReadmeSamples.SourcesOf(ReadmeSamples.Prefix(block.Readme))} holds this code. Mirror the change in the sample region, " + $"or mark a block that cannot compile on purpose with above its fence.{Environment.NewLine}" + Closest(block.Code, Samples.Value.Select(candidate => (candidate.Name + " at " + candidate.Location, candidate.Code)), "sample", "README")); } diff --git a/test/CodoMetis.TypeKit.Conventions.Tests/ReadmeSamples.cs b/test/CodoMetis.TypeKit.Conventions.Tests/ReadmeSamples.cs index ab771ab8..f43d92b9 100644 --- a/test/CodoMetis.TypeKit.Conventions.Tests/ReadmeSamples.cs +++ b/test/CodoMetis.TypeKit.Conventions.Tests/ReadmeSamples.cs @@ -42,6 +42,19 @@ internal static partial class ReadmeSamples public const string DocsDirectory = "docs"; + /// + /// The pages whose regions live outside the samples project, each with the directory that holds them. The + /// walkthrough's are the sources of the smoke test's walkthrough consumer: a sample application that + /// the packed packages build and run, and whose answers the page states. + /// + public static readonly IReadOnlyDictionary RegionSources = new Dictionary(StringComparer.Ordinal) + { + ["walkthrough"] = "tools/ConsumerSmokeTest/Consumers/walkthrough", + }; + + /// Where the regions of the page with this prefix live: its own sources, or the samples project. + public static string SourcesOf(string prefix) => RegionSources.GetValueOrDefault(prefix, SamplesProject); + /// /// Every page whose C# is compiled: the root README, every shipping package's, discovered from /// src/, and every topic page in docs/ (). @@ -93,12 +106,13 @@ public static IReadOnlyList DocumentationLinks(string markdown) public static IReadOnlyList CodeBlocks() => [.. Readmes().SelectMany(readme => CodeBlocks(readme, File.ReadAllText(Path.Combine(Repository.Root, readme))))]; + /// Every region of the samples project and of each page's own sources (). public static IReadOnlyList Regions() { - var project = Path.Combine(Repository.Root, SamplesProject); - - var regions = Directory.EnumerateFiles(project, "*.cs", SearchOption.AllDirectories) - .Where(path => !Path.GetRelativePath(project, path).Split(Path.DirectorySeparatorChar).Any(part => part is "bin" or "obj")) + var regions = new[] { SamplesProject }.Concat(RegionSources.Values) + .Select(directory => Path.Combine(Repository.Root, directory)) + .SelectMany(directory => Directory.EnumerateFiles(directory, "*.cs", SearchOption.AllDirectories) + .Where(path => !Path.GetRelativePath(directory, path).Split(Path.DirectorySeparatorChar).Any(part => part is "bin" or "obj"))) .Order(StringComparer.Ordinal) .SelectMany(path => Regions(Path.GetRelativePath(Repository.Root, path).Replace('\\', '/'), File.ReadAllText(path))) .ToArray(); diff --git a/test/CodoMetis.TypeKit.Conventions.Tests/ReleaseWiringTests.cs b/test/CodoMetis.TypeKit.Conventions.Tests/ReleaseWiringTests.cs index 28441a1f..ad7277de 100644 --- a/test/CodoMetis.TypeKit.Conventions.Tests/ReleaseWiringTests.cs +++ b/test/CodoMetis.TypeKit.Conventions.Tests/ReleaseWiringTests.cs @@ -25,9 +25,6 @@ public sealed partial class ReleaseWiringTests private const string SmokeTestProject = "tools/ConsumerSmokeTest/ConsumerSmokeTest.csproj"; private const string SmokeTest = "dotnet tools/ConsumerSmokeTest/bin/Release/net10.0/consumer-smoke-test.dll"; - /// The prober, a tool project run as its built dll (design/adr/0092), which the workflows run with no model (design/adr/0096). - private const string Prober = "dotnet tools/Prober/bin/Release/net10.0/prober.dll"; - private static string ReleaseWorkflow => Workflow(ReleaseWorkflowFileName); private static string BuildWorkflow => Workflow("dotnet.yml"); @@ -49,7 +46,7 @@ public void Only_the_publish_job_may_mint_a_token() { var jobs = Jobs(ReleaseWorkflow); - jobs.Keys.ShouldBe(["verify", "publish", "release"], ignoreOrder: true, "The release workflow no longer has the verify/publish/release split these tests assume."); + jobs.Keys.ShouldBe(["ci", "verify", "publish", "release"], ignoreOrder: true, "The release workflow no longer has the ci/verify/publish/release split these tests assume."); foreach (var (name, job) in jobs) { @@ -160,214 +157,147 @@ public void Every_job_that_runs_the_smoke_test_builds_it_first() } /// - /// Measured on PR #22's run, 2026-10-16: on a pull request that changed no package, the pack job took four - /// minutes and the consumer smoke test four more after a test job of five to eight, a thirteen-minute run. So on - /// a pull request the build workflow packs and smoke-tests only when the diff against the base branch touches a - /// path that can change a package or a trimmed consumer, as its changes job decides, and on a push always. - /// A job that loses the condition runs on every pull request again, which fails nothing but the maintainer's - /// time; a path the step no longer lists lets a change to a package merge unpacked and untested, silently. Both - /// are held here, by structure: the job-level keys and the case labels of the deciding step. + /// The changes job asks tools/CiLanes which lanes the run needs (design/adr/0113), and hands its two answers to the + /// jobs that read them: the test matrix and the pack lane. Before decision 113 the job decided in a shell `case` + /// whose list missed inputs and failed open (#70). The checkout is the whole history, since the diff is against + /// the base branch or the commit a push replaced. /// [Fact] - public void The_build_workflow_packs_a_pull_request_only_when_the_diff_can_change_a_package() + public void The_changes_job_selects_the_lanes_with_ci_lanes() { var changes = Job(BuildWorkflow, "changes"); - var output = Regex.Match(changes, @"(?m)^ product:\s*\$\{\{ steps\.(?[A-Za-z0-9_-]+)\.outputs\.product \}\}\s*$"); - output.Success.ShouldBeTrue("dotnet.yml:changes has no `product` output taken from a step's output, so the pack and consumer jobs have nothing to read."); + foreach (var output in new[] { "legs", "pack", "suite" }) + changes.ShouldMatch($@"(?m)^ {output}:\s*\$\{{\{{ steps\.select\.outputs\.{output} \}}\}}\s*$", + $"dotnet.yml:changes has no `{output}` output taken from the select step's, so the jobs it selects have nothing to read."); + + var select = Steps(changes).SingleOrDefault(step => Regex.IsMatch(step, @"(?m)^\s+id:\s*select\s*$")); + select.ShouldNotBeNull("dotnet.yml:changes has no step with id select."); + select.ShouldContain("dotnet tools/CiLanes/bin/Release/net10.0/ci-lanes.dll select", Case.Sensitive, "dotnet.yml:changes does not run the selection's built dll."); + foreach (var argument in new[] { @"--event ""$GITHUB_EVENT_NAME""", @"--ref ""$GITHUB_REF""", @"--base ""$BASE_REF""", @"--before ""$BEFORE""", @"--output ""$GITHUB_OUTPUT""" }) + select.ShouldContain(argument, Case.Sensitive, $"dotnet.yml:changes does not pass {argument} to the selection."); + select.ShouldContain("BASE_REF: ${{ github.base_ref }}", Case.Sensitive); + select.ShouldContain("BEFORE: ${{ github.event.before }}", Case.Sensitive); + + changes.ShouldMatch(@"(?m)^\s+run:\s*dotnet build tools/CiLanes -c Release\s*$", "dotnet.yml:changes runs the selection without building it in Release first."); + changes.ShouldMatch(@"(?m)^\s+fetch-depth:\s*0\s*$", "dotnet.yml:changes checks out a shallow clone, which has neither the base branch nor the commit a push replaced."); + } - var decide = Steps(changes).SingleOrDefault(step => Regex.IsMatch(step, $@"(?m)^\s+id:\s*{Regex.Escape(output.Groups["id"].Value)}\s*$")); + [Fact] + public void The_test_matrix_is_the_selection() + { + var test = Job(BuildWorkflow, "test"); - decide.ShouldNotBeNull($"dotnet.yml:changes has no step with id {output.Groups["id"].Value}, which its `product` output names."); - decide.ShouldMatch(@"(?m)^\s+if \[ ""\$GITHUB_EVENT_NAME"" != pull_request \]; then\s*$", - "dotnet.yml:changes does not answer true for every event but a pull request, so a push or the dispatch would skip the pack."); - decide.ShouldMatch(@"(?m)^\s+echo ""product=true"" >> ""\$GITHUB_OUTPUT""\s*$", "dotnet.yml:changes never writes product=true to the step's output."); - decide.ShouldMatch(@"(?m)^\s+git diff --name-only ""origin/\$GITHUB_BASE_REF\.\.\.HEAD"" > \S+\s*$", - "dotnet.yml:changes does not diff the pull request against its base branch (origin/$GITHUB_BASE_REF...HEAD) as a command of its own, whose failure fails the job."); - decide.ShouldMatch(@"(?m)^\s+echo ""product=\$product"" >> ""\$GITHUB_OUTPUT""\s*$", "dotnet.yml:changes never writes the diff's answer to the step's output."); + test.ShouldMatch(JobKey("needs", @"(?:changes|\[\s*changes\s*\])"), "dotnet.yml:test does not need changes, so it cannot read the selected legs."); + test.ShouldMatch(@"(?m)^\s+os:\s*\$\{\{ fromJSON\(needs\.changes\.outputs\.legs\) \}\}\s*$", + "dotnet.yml:test's matrix is not the changes job's legs, so the selection no longer decides which legs run."); - foreach (var pattern in ProductPaths) - decide.ShouldMatch(CaseLabel(pattern), - $"dotnet.yml:changes no longer lists {pattern} as a case label of the deciding step, so a pull request changing it alone would merge unpacked and untested."); + // A prose-only change runs test/Prose.slnf; any other answer, a missing one included, the whole solution. + test.ShouldMatch(@"(?m)^ SOLUTION:\s*\$\{\{ needs\.changes\.outputs\.suite == 'prose' && 'test/Prose\.slnf' \|\| 'CodoMetis\.TypeKit\.slnx' \}\}\s*$", + "dotnet.yml:test does not choose its solution from the changes job's suite, falling back to the whole solution."); + foreach (var command in new[] { @"dotnet restore ""$SOLUTION""", @"dotnet build ""$SOLUTION"" -c Release", @"dotnet test --solution ""$SOLUTION"" -c Release" }) + test.ShouldContain(command, Case.Sensitive, $"dotnet.yml:test does not run `{command}`, so the suite the changes job chose is not what it builds or tests."); + } + /// Pack and Consumer start beside the test legs (#117) and run when the pack lane is selected. + [Fact] + public void Pack_and_Consumer_run_when_the_pack_lane_is_selected() + { foreach (var name in new[] { "pack", "consumer" }) { var job = Job(BuildWorkflow, name); - job.ShouldMatch(JobKey("needs", @"\[\s*test,\s*changes\s*\]"), $"dotnet.yml:{name} does not need [ test, changes ], so it cannot read the changes job's output."); - job.ShouldMatch(JobKey("if", Regex.Escape("needs.changes.outputs.product == 'true'")), - $"dotnet.yml:{name} is not conditioned on `needs.changes.outputs.product == 'true'`, so it runs on every pull request again."); + job.ShouldMatch(JobKey("needs", @"\[\s*changes\s*\]"), + $"dotnet.yml:{name} does not need [ changes ] alone: it reads the selection, and waits for nothing else."); + job.ShouldMatch(JobKey("if", Regex.Escape("needs.changes.outputs.pack == 'true'")), + $"dotnet.yml:{name} is not conditioned on `needs.changes.outputs.pack == 'true'`, so it runs on every pull request again or never."); } } /// - /// The release packs and smoke-tests always: the artifacts it publishes are the artifacts it tested, whatever - /// the tag's diff. Neither its verify job nor the pack or smoke test step carries a condition. + /// The CI job is the required build check: it judges every other job, so it needs each of them, and it runs under + /// always(), since a skipped job reads as a passing required check (KB-043). A job added without joining its + /// needs would go unjudged; the gate itself fails a job it does not know. /// [Fact] - public void The_release_packs_and_smoke_tests_unconditionally() + public void The_CI_job_judges_every_other_job() { - var job = Job(ReleaseWorkflow, "verify"); - var steps = Steps(job).ToList(); - - job.ShouldNotMatch(JobKey("if", ".*"), "release.yml:verify carries a job-level condition; the release packs and smoke-tests every tag."); - - foreach (var (what, step) in new[] { ("pack", steps.Single(candidate => candidate.Contains("dotnet pack", StringComparison.Ordinal))), ("smoke test", steps.Single(candidate => SmokeTestStep().IsMatch(candidate))) }) - step.ShouldNotMatch(@"(?m)^\s+if:", $"release.yml:verify's {what} step carries a condition; the release packs and smoke-tests every tag."); + var jobs = Jobs(BuildWorkflow); + var ci = Job(BuildWorkflow, "ci"); + + var needs = Regex.Match(ci, @"(?m)^ needs:\s*\[(?[^\]]*)\]\s*$"); + needs.Success.ShouldBeTrue("dotnet.yml:ci has no `needs: [ ... ]` list."); + needs.Groups["jobs"].Value.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) + .ShouldBe(jobs.Keys.Where(name => name != "ci"), ignoreOrder: true, "dotnet.yml:ci does not need exactly every other job, so it cannot judge them all."); + + ci.ShouldMatch(JobKey("if", Regex.Escape("always()")), "dotnet.yml:ci does not run under always(), so a failed or cancelled job skips it, and a skipped CI reads as passing."); + ci.ShouldMatch(@"(?m)^\s+NEEDS:\s*\$\{\{ toJSON\(needs\) \}\}\s*$", "dotnet.yml:ci does not hand the gate toJSON(needs)."); + ci.ShouldContain("dotnet tools/CiLanes/bin/Release/net10.0/ci-lanes.dll gate", Case.Sensitive, "dotnet.yml:ci does not run the gate."); + ci.ShouldMatch(@"(?m)^\s+name:\s*CI\s*$", "dotnet.yml:ci is not named CI, the required check's name."); } /// - /// The paths a pull request must touch for its diff to change a package or a trimmed consumer, each a case label - /// of the changes job's deciding step: the shipping projects, the build property files at any depth, any project - /// file, the SDK pin, the package versions, the workflows and the smoke test itself. - /// - private static readonly string[] ProductPaths = - ["src/", "Directory.*.props", "Directory.*.targets", "*.csproj", "global.json", "Directory.Packages.props", ".github/workflows/", "tools/ConsumerSmokeTest/"]; - - /// A bash case label naming the path pattern, alone or among alternatives, a trailing * allowed for a directory. - private static string CaseLabel(string pattern) => $@"(?m)^\s+(?:[^\s)|]+\|)*{Regex.Escape(pattern)}\*?(?:\|[^\s)|]+)*\)"; - - /// - /// CI replays the probe corpus with no model (design/adr/0096), split by family into legs that run at once - /// and a job that merges their states (design/adr/0101): on a push every probe, with coverage, and the merged - /// state saved for the branch's pull requests; on a pull request the base branch's state restored and only - /// the probes it stales replayed, without a footprint. The replay had once been documented as running in CI - /// while no workflow called it, and a dropped step fails nothing: a regression the corpus would catch merges - /// instead. So a dropped leg, a family in no leg, a replay that cannot fail its job, a merge that cannot fail - /// its job, a state saved from a pull request, or a job with no time limit each fail here. + /// The release runs every lane of the build workflow before it verifies (#74): the Windows and macOS legs and the + /// smoke test's own-feed mode, which a pull request may have skipped. /// [Fact] - public void The_build_workflow_replays_every_probe_by_family_and_merges_the_legs() + public void The_release_runs_the_build_workflow_before_it_verifies() { - var (name, job, steps) = ProberJob("dotnet.yml", BuildWorkflow); - var jobName = name[(name.IndexOf(':') + 1)..]; - - var listed = Regex.Match(job, @"(?m)^\s+family:\s*\[(?[^\]]*)\]").Groups["families"].Value - .Split(',').Select(f => f.Trim()).Where(f => f.Length > 0).ToList(); - listed.ShouldBe(ReplayFamilies(), ignoreOrder: true, - $"{name}'s matrix does not list exactly the families of the axes file's replay configuration: a family in no leg is never replayed (`prober merge` then refuses), a leg of no family runs nothing."); - job.ShouldMatch(JobKey("timeout-minutes", @"\d+"), $"{name} has no timeout-minutes, so a hung replay runs to the six-hour default."); - job.ShouldMatch(@"PROBE_BRANCH=\$\{\{ github\.event_name == 'pull_request' && github\.base_ref \|\| github\.ref_name \}\}", - $"{name} does not take the base branch's state on a pull request (PROBE_BRANCH), so the base's footprints would not decide what the branch stales."); - - var replay = steps.FindIndex(step => ProberStep("replay").IsMatch(step)); - var build = steps.FindIndex(step => ProberBuild().IsMatch(step)); - replay.ShouldBeGreaterThanOrEqualTo(0, $"{name} does not run `prober replay`."); - build.ShouldBeInRange(0, replay - 1, $"{name} runs the prober without building it in Release first: build the solution or tools/Prober with -c Release."); - var lines = steps[replay].Split('\n').Where(line => ProberStep("replay").IsMatch(line)).Select(line => line.Trim()).ToList(); - lines.Count.ShouldBe(2, $"{name} does not run the replay one way on a push and another on a pull request."); - lines.Single(line => line.Contains(" --all", StringComparison.Ordinal)) - .ShouldMatch($@"^{Regex.Escape(Prober)} replay --all --branch ""\$PROBE_BRANCH"" --family ""\$\{{\{{ matrix\.family \}}\}}""$", - $"{name}'s push replay is not `replay --all` of the branch's family leg: every probe, with a footprint, for the state the branch saves."); - lines.Single(line => !line.Contains(" --all", StringComparison.Ordinal)) - .ShouldMatch($@"^{Regex.Escape(Prober)} replay --branch ""\$PROBE_BRANCH"" --family ""\$\{{\{{ matrix\.family \}}\}}"" --no-footprint$", - $"{name}'s pull request replay is not the family leg's stale probes without a footprint: coverage would be spent on a state nobody keeps."); - steps[replay].ShouldNotMatch(@"\|\|\s*true", $"{name} lets the replay fail without failing the job."); - steps[replay].ShouldNotMatch(@"(?m)^\s+continue-on-error:", $"{name} lets the replay fail without failing the job."); - - var restore = steps.FindIndex(step => step.Contains("uses: actions/cache/restore@", StringComparison.Ordinal)); - restore.ShouldBeInRange(0, replay - 1, $"{name} does not restore the base branch's probe state before the replay, so a pull request replays every probe."); - steps[restore].ShouldMatch(@"(?m)^\s+if:\s*github\.event_name == 'pull_request'\s*$", $"{name} restores a saved state on a push too, where decision 0101 keeps the replay from an empty state."); - steps[restore].ShouldMatch(@"restore-keys:\s*probe-state-\$\{\{ github\.base_ref \}\}-", $"{name} does not restore the base branch's newest saved state (restore-keys by base_ref)."); - - var merge = Jobs(BuildWorkflow).Single(candidate => Steps(candidate.Value).Any(step => ProberStep("merge").IsMatch(step))); - var mergeName = $"dotnet.yml:{merge.Key}"; - merge.Value.ShouldMatch(JobKey("needs", Regex.Escape(jobName)), $"{mergeName} does not follow the {jobName} legs."); - merge.Value.ShouldMatch(JobKey("if", @"always\(\)"), $"{mergeName} is skipped when a leg fails, so a failing run reports no status."); - merge.Value.ShouldMatch(JobKey("timeout-minutes", @"\d+"), $"{mergeName} has no timeout-minutes."); - merge.Value.ShouldMatch(StateDirectory, $"{mergeName} does not set PROBE_STATE_DIR, so the prober writes into a developer's state directory."); - var mergeSteps = Steps(merge.Value).ToList(); - var merging = mergeSteps.FindIndex(step => ProberStep("merge").IsMatch(step)); - var download = mergeSteps.FindIndex(step => step.Contains("uses: actions/download-artifact@", StringComparison.Ordinal)); - var status = mergeSteps.FindIndex(step => ProberStep("status").IsMatch(step)); - var save = mergeSteps.FindIndex(step => step.Contains("uses: actions/cache/save@", StringComparison.Ordinal)); - download.ShouldBeInRange(0, merging - 1, $"{mergeName} does not download the legs' states before merging them."); - mergeSteps[merging].ShouldMatch($@"(?m)^\s+run:\s*{Regex.Escape(Prober)} merge --branch ""\$PROBE_BRANCH"" --from \S", $"{mergeName} does not merge the legs' states with `prober merge --branch --from` alone on its run line."); - mergeSteps[merging].ShouldNotMatch(@"\|\|\s*true", $"{mergeName} lets the merge fail without failing the job, so a probe in no leg or a failed leg would pass."); - mergeSteps[merging].ShouldNotMatch(@"(?m)^\s+continue-on-error:", $"{mergeName} lets the merge fail without failing the job, so a probe in no leg or a failed leg would pass."); - status.ShouldBeGreaterThan(merging, $"{mergeName} does not run `prober status` after the merge, so the run reports no coverage."); - save.ShouldBeGreaterThan(merging, $"{mergeName} does not save the merged state for the branch's pull requests."); - mergeSteps[save].ShouldMatch(@"(?m)^\s+if:\s*github\.event_name == 'push'\s*$", $"{mergeName} saves the state from a pull request too, whose replay was partial and without a footprint."); - mergeSteps[save].ShouldMatch(@"key:\s*probe-state-\$\{\{ github\.ref_name \}\}-", $"{mergeName} does not save the state under the branch's key, which its pull requests restore by prefix."); - } - - /// A key of the job itself, at the job's indentation (four spaces), never a step's key of the same name. - private static string JobKey(string key, string value) => $@"(?m)^ {key}:\s*{value}\s*$"; - - /// The families of the axes file's replay configuration, which the matrix lists in full. - private static List ReplayFamilies() - { - var text = File.ReadAllText(Path.Combine(Repository.Root, ".claude", "probe-axes.md")); - var start = text.IndexOf("```json probe-axes", StringComparison.Ordinal); - start.ShouldBeGreaterThanOrEqualTo(0, "No `json probe-axes` block in .claude/probe-axes.md."); - start = text.IndexOf('\n', start) + 1; - var end = text.IndexOf("\n```", start, StringComparison.Ordinal); - using var block = System.Text.Json.JsonDocument.Parse(text[start..end]); - var families = block.RootElement.GetProperty("replay").GetProperty("families").EnumerateObject().Select(property => property.Name).ToList(); - families.ShouldNotBeEmpty("The replay configuration of .claude/probe-axes.md names no families."); - return families; + BuildWorkflow.ShouldMatch(@"(?m)^ workflow_call:\s*$", "dotnet.yml cannot be called: it has no workflow_call trigger."); + Job(ReleaseWorkflow, "ci").ShouldMatch(JobKey("uses", Regex.Escape("./.github/workflows/dotnet.yml")), "release.yml:ci does not call dotnet.yml."); + Job(ReleaseWorkflow, "verify").ShouldMatch(JobKey("needs", @"(?:ci|\[\s*ci\s*\])"), "release.yml:verify does not need ci, so it packs without the full matrix."); } /// - /// The release replays the corpus and runs the gate before it packs (design/adr/0096), against the previous - /// release: on a tag push, git describe alone names the tag being released, and the gate would compare - /// the commit with itself and find nothing touched. The gate binds from 2.2.0 (design/adr/0097), so a refusal - /// fails the job: the step's exit code is the gate's own, either because nothing follows the gate's - /// || or because the step ends with exit "$code", and it has no continue-on-error. + /// A called workflow's github.workflow is its caller's name, and a concurrency group ignores case (KB-043). + /// The build workflow's group under the release's call must differ from the release's own, or the call waits for its + /// caller and the release never finishes. /// [Fact] - public void The_release_replays_every_probe_and_runs_the_gate_before_packing() + public void A_call_from_the_release_never_shares_its_callers_concurrency_group() { - var job = Job(ReleaseWorkflow, "verify"); - var steps = Steps(job).ToList(); + var caller = Regex.Match(ReleaseWorkflow, @"(?m)^name:\s*(?.+?)\s*$").Groups["name"].Value; + string Group(string workflow) => + Regex.Match(workflow, @"(?m)^concurrency:\s*\n\s+group:\s*(?.+?)\s*$").Groups["group"].Value.Replace("${{ github.workflow }}", caller, StringComparison.Ordinal); + + var release = Group(ReleaseWorkflow); + var called = Group(BuildWorkflow); - var replay = StrictReplay("release.yml:verify", steps); - var gate = steps.FindIndex(step => ProberStep("gate").IsMatch(step)); - var pack = steps.FindIndex(step => step.Contains("dotnet pack", StringComparison.Ordinal)); - - gate.ShouldBeGreaterThan(replay, "release.yml:verify does not run `prober gate` after `prober replay --all`; the gate reads the replay's evidence."); - pack.ShouldBeGreaterThan(gate, "release.yml:verify packs before the probe gate has run."); - steps[gate].ShouldMatch(@"\bgate --since ""?\$", "The gate runs without --since, so on a tag push it compares the release with itself."); - steps[gate].ShouldNotMatch(@"(?m)^\s+continue-on-error:", "release.yml:verify lets the probe gate fail without failing the job; it binds from 2.2.0."); - (!Regex.IsMatch(steps[gate], $@"{Regex.Escape(Prober)} gate\b[^\n]*\|\|") || Regex.IsMatch(steps[gate], @"\n\s*exit ""?\$code""?\s*$")) - .ShouldBeTrue("release.yml:verify catches the probe gate's exit code and does not end the step with it, so a refusal does not fail the release; it binds from 2.2.0."); - job.ShouldMatch(StateDirectory, "release.yml:verify does not set PROBE_STATE_DIR, so the prober writes into a developer's state directory."); + release.ShouldNotBeEmpty("release.yml has no workflow-level concurrency group."); + called.ShouldNotBeEmpty("dotnet.yml has no workflow-level concurrency group."); + string.Equals(release, called, StringComparison.OrdinalIgnoreCase).ShouldBeFalse( + $"Under release.yml's call, dotnet.yml's concurrency group is {called}, the release's own ({release}) as GitHub compares groups: the call would wait for its caller."); } - /// The one job of a workflow whose steps run `prober replay`: its name, its text and its steps. - private static (string Name, string Job, List Steps) ProberJob(string workflow, string text) + [Fact] + public void The_nightly_run_dispatches_the_build_workflow() { - var jobs = Jobs(text).Where(job => Steps(job.Value).Any(step => ProberStep("replay").IsMatch(step))).ToList(); - - jobs.Count.ShouldBe(1, $"{workflow} has no job that runs `prober replay`, or more than one."); - jobs[0].Value.ShouldMatch(StateDirectory, $"{workflow}:{jobs[0].Key} does not set PROBE_STATE_DIR, so the prober writes into a developer's state directory."); + var nightly = Workflow("nightly.yml"); - return ($"{workflow}:{jobs[0].Key}", jobs[0].Value, Steps(jobs[0].Value).ToList()); + nightly.ShouldMatch(@"(?m)^\s+- cron:", "nightly.yml has no schedule."); + nightly.ShouldContain("gh workflow run dotnet.yml --ref", Case.Sensitive, "nightly.yml does not dispatch dotnet.yml."); + nightly.ShouldMatch(@"(?m)^\s+actions:\s*write\b", "nightly.yml cannot dispatch without actions: write."); } /// - /// The replay of every probe, built first, and able to fail the job: its exit code is the step's own, with no - /// || true and no continue-on-error. + /// The release packs and smoke-tests always: the artifacts it publishes are the artifacts it tested, whatever + /// the tag's diff. Neither its verify job nor the pack or smoke test step carries a condition. /// - private static int StrictReplay(string name, List steps) + [Fact] + public void The_release_packs_and_smoke_tests_unconditionally() { - var replay = steps.FindIndex(step => ProberStep("replay").IsMatch(step)); - var build = steps.FindIndex(step => ProberBuild().IsMatch(step)); + var job = Job(ReleaseWorkflow, "verify"); + var steps = Steps(job).ToList(); - replay.ShouldBeGreaterThanOrEqualTo(0, $"{name} does not run `prober replay`."); - steps[replay].ShouldMatch($@"(?m)^\s+run:\s*{Regex.Escape(Prober)} replay --all\s*$", - $"{name} replays the probes some other way than `{Prober} replay --all` alone on its run line, so a failing probe may not fail the job."); - steps[replay].ShouldNotMatch(@"(?m)^\s+continue-on-error:", $"{name} lets the replay fail without failing the job."); - build.ShouldBeInRange(0, replay - 1, $"{name} runs the prober without building it in Release first: build the solution or tools/Prober with -c Release."); + job.ShouldNotMatch(JobKey("if", ".*"), "release.yml:verify carries a job-level condition; the release packs and smoke-tests every tag."); - return replay; + foreach (var (what, step) in new[] { ("pack", steps.Single(candidate => candidate.Contains("dotnet pack", StringComparison.Ordinal))), ("smoke test", steps.Single(candidate => SmokeTestStep().IsMatch(candidate))) }) + step.ShouldNotMatch(@"(?m)^\s+if:", $"release.yml:verify's {what} step carries a condition; the release packs and smoke-tests every tag."); } - /// PROBE_STATE_DIR set for the job: as an env key, or written to $GITHUB_ENV (the runner context is not available to a job's env). - private const string StateDirectory = @"(?m)^\s+(PROBE_STATE_DIR:\s*\S|echo ""PROBE_STATE_DIR=\S+"" >> ""\$GITHUB_ENV"")"; + /// A key of the job itself, at the job's indentation (four spaces), never a step's key of the same name. + private static string JobKey(string key, string value) => $@"(?m)^ {key}:\s*{value}\s*$"; - /// A step that runs a prober command; has already dropped the comment lines. - private static Regex ProberStep(string command) => new($@"{Regex.Escape(Prober)} {command}\b"); /// /// Measured 2026-09-27: without the filter, the analyzer package's SBOM lists 13 Roslyn @@ -478,9 +408,6 @@ private static IEnumerable Steps(string job) => [GeneratedRegex(@"(?m)^\s+run:\s*dotnet build (CodoMetis\.TypeKit\.slnx|tools/ConsumerSmokeTest)\s+-c Release\s*$")] private static partial Regex SmokeTestBuild(); - [GeneratedRegex(@"(?m)^\s+run:\s*dotnet build (CodoMetis\.TypeKit\.slnx|tools/Prober)\s+-c Release\s*$")] - private static partial Regex ProberBuild(); - [GeneratedRegex(@"(?m)^\s+run:.*\bapt-get install\b.*\bclang\b.*\bzlib1g-dev\b")] private static partial Regex AotPrerequisites(); diff --git a/test/CodoMetis.TypeKit.Conventions.Tests/SessionStartHookTests.cs b/test/CodoMetis.TypeKit.Conventions.Tests/SessionStartHookTests.cs index 73ea4959..9660cedd 100644 --- a/test/CodoMetis.TypeKit.Conventions.Tests/SessionStartHookTests.cs +++ b/test/CodoMetis.TypeKit.Conventions.Tests/SessionStartHookTests.cs @@ -36,7 +36,7 @@ .. Directory.GetDirectories(Path.Combine(Repository.Root, "tools")) ]; [Fact] - public void The_tools_are_found() => Tools.Select(row => row.Data).ShouldContain("Prober"); + public void The_tools_are_found() => Tools.Select(row => row.Data).ShouldContain("Oracles"); [Theory, MemberData(nameof(Tools))] public void Every_tool_is_in_the_session_start_filter(string tool) diff --git a/test/CodoMetis.TypeKit.Conventions.Tests/WovenReachabilityTests.cs b/test/CodoMetis.TypeKit.Conventions.Tests/WovenReachabilityTests.cs new file mode 100644 index 00000000..a2fe7db9 --- /dev/null +++ b/test/CodoMetis.TypeKit.Conventions.Tests/WovenReachabilityTests.cs @@ -0,0 +1,126 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace CodoMetis.TypeKit.Conventions.Tests; + +/// +/// The woven code has no statement after a return or a throw of the same block: what the +/// generators write is all reachable. +/// +/// +/// +/// A template that returns from a compile-time branch ends the run-time body there, but compile-time +/// calls after it still run. TryParseTemplate called meta.InsertStatement after such a +/// branch, so every generated TryParse(string?, IFormatProvider?, out …) ended in a declaration and +/// a try/catch nothing reached (issue #100). The compiler keeps quiet about unreachable code +/// in generated code, and no test of behaviour can see it. +/// +/// +/// This reads the woven files the probes emit (MetalamaEmitCompilerTransformedFiles) for the build +/// of its own configuration. The probes hold a value object of every parse strategy. +/// +/// +public sealed class WovenReachabilityTests +{ + private const string Probes = "test/CodoMetis.TypeKit.Generators.Probes"; + + [Fact] + public void A_statement_after_a_return_is_found() + { + const string source = + """ + class C + { + bool M(string? s) + { + if (s is null) return false; + return true; + int dead; + } + + int N() + { + throw new System.Exception(); + return 1; + } + + int O() + { + return Local(); + int Local() => 1; + } + } + """; + + Unreachable(source, Path.Combine(Repository.Root, "C.cs")).ShouldBe(["C.cs:7: C.M: int dead;", "C.cs:13: C.N: return 1;"]); + } + + [Fact] + public void The_woven_probes_are_read() + { + var methods = WovenFiles().SelectMany(path => Methods(File.ReadAllText(path), path)).ToArray(); + + // Every value object of the probes that parses gets a TryParse over a string. + methods.Count(method => method.Identifier.ValueText == "TryParse" && method.ParameterList.Parameters.FirstOrDefault()?.Type?.ToString() is "global::System.String?") + .ShouldBeGreaterThanOrEqualTo(20); + } + + [Fact] + public void The_woven_code_has_no_unreachable_statement() + { + var unreachable = WovenFiles().SelectMany(path => Unreachable(File.ReadAllText(path), path)).ToArray(); + + unreachable.ShouldBeEmpty( + "A template wrote a statement after a return or a throw. A compile-time call after a return in a compile-time branch " + + $"still runs: put it in an else branch.{Environment.NewLine}{string.Join(Environment.NewLine, unreachable.Take(20))}"); + } + + /// The woven sources of the probes, written by the build of this configuration. + private static IReadOnlyList WovenFiles() + { + // bin/{configuration}/{framework}/ of this test project. + var output = new DirectoryInfo(AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar)); + var framework = output.Name; + var configuration = output.Parent!.Name; + var folder = Path.Combine(Repository.Root, Probes, "obj", configuration, framework, "metalama"); + + return Directory.Exists(folder) + ? [.. Directory.EnumerateFiles(folder, "*.cs", SearchOption.AllDirectories).Order(StringComparer.Ordinal)] + : throw new DirectoryNotFoundException($"No woven probes at {folder}. Build {Probes} in {configuration} first; dotnet test --solution does."); + } + + private static IEnumerable Methods(string source, string path) => + CSharpSyntaxTree.ParseText(source, path: path, cancellationToken: TestContext.Current.CancellationToken) + .GetRoot(TestContext.Current.CancellationToken) + .DescendantNodes() + .OfType(); + + /// Each statement that follows a return or a throw in the same block, a local function excepted. + private static IEnumerable Unreachable(string source, string path) + { + var cancellationToken = TestContext.Current.CancellationToken; + var tree = CSharpSyntaxTree.ParseText(source, path: path, cancellationToken: cancellationToken); + var lines = tree.GetText(cancellationToken).Lines; + + foreach (var block in tree.GetRoot(cancellationToken).DescendantNodes().OfType()) + { + var exit = block.Statements.IndexOf(statement => statement is ReturnStatementSyntax or ThrowStatementSyntax); + if (exit < 0) continue; + + var after = block.Statements.Skip(exit + 1).FirstOrDefault(statement => statement is not LocalFunctionStatementSyntax); + if (after is null) continue; + + var member = after.Ancestors().OfType().First(); + var type = after.Ancestors().OfType().First().Identifier.ValueText; + var name = member switch + { + MethodDeclarationSyntax method => method.Identifier.ValueText, + PropertyDeclarationSyntax property => property.Identifier.ValueText, + _ => member.Kind().ToString(), + }; + + yield return $"{Path.GetRelativePath(Repository.Root, path)}:{lines.GetLineFromPosition(after.SpanStart).LineNumber + 1}: {type}.{name}: {after.ToString().Split('\n')[0].Trim()}"; + } + } +} diff --git a/test/CodoMetis.TypeKit.DocsQuery.Tests/FactCitationTests.cs b/test/CodoMetis.TypeKit.DocsQuery.Tests/FactCitationTests.cs deleted file mode 100644 index 6ca113d4..00000000 --- a/test/CodoMetis.TypeKit.DocsQuery.Tests/FactCitationTests.cs +++ /dev/null @@ -1,216 +0,0 @@ -using DocsQuery; - -namespace CodoMetis.TypeKit.DocsQuery.Tests; - -/// -/// A fact is executable or it explains only (design/adr/0107): check fails a fact the probe corpus cites to -/// prune or excuse a row (a refused entry of the axes file, a not-promised entry or an obligation's declaration in -/// claims.json, a Cite.Fact or Baseline.Fact of the harness) when it has no test, and a fixture: -/// whose file is missing or not beside the test. -/// -public sealed class FactCitationTests : IDisposable -{ - private readonly TempRepo _repo = Store(new TempRepo()); - - public void Dispose() => _repo.Dispose(); - - private static string Fact(int number, string test, string fixture = "") => - $""" - --- - verified: 2026-10-15 - pins: - - sdk 10.0.4xx - source: the framework's own output, measured - turned-on: decision 1 - test: {test} - {fixture} - --- - - # KB-{number:D3}. A fact - """; - - /// KB-001 tested, with a fixture; KB-002 prose only. - private static TempRepo Store(TempRepo repo) => repo - .With("global.json", """{ "sdk": { "version": "10.0.402", "rollForward": "latestPatch" } }""") - .With("design/adr/0001-first.md", "# 0001. First\n\nDate: 2026-10-01\nStatus: accepted\n") - .With("design/knowledge/KB-001-tested.md", Fact(1, "test/CodoMetis.TypeKit.Facts/KB001.cs", "fixture: test/CodoMetis.TypeKit.Facts/KB001.fixture.json")) - .With("test/CodoMetis.TypeKit.Facts/KB001.cs", "public sealed class KB001 { }\n") - .With("test/CodoMetis.TypeKit.Facts/KB001.fixture.json", """{ "fact": "KB-001", "answers": [] }""") - .With("design/knowledge/KB-002-prose.md", Fact(2, "none, a sentence of the framework's documentation")); - - /// The axes file, its refused entries citing in their reason. - private TempRepo Axes(string fact) => _repo.With(".claude/probe-axes.md", - $$""" - # Axes - - ```json probe-axes - { - "axes": {}, - "refused": { - "json.ReadCommentHandling": { - "Allow": "System.Text.Json refuses it on the options object" - }, - "mvc.MaxValidationDepth": { - "0": "MVC refuses it, fact:{{fact}}" - } - } - } - ``` - """); - - /// claims.json with a not-promised entry whose source is and an obligation declaration citing . - private TempRepo Claims(string source, string cite) => _repo.With("tools/probe/corpus/claims.json", - $$""" - [ - { - "id": "C01", - "source": "README.md#a", - "obligations": [ - { - "id": "binds", - "sentence": "It binds.", - "inapplicable": [ - { "axes": { "x": ["1"] }, "cite": "not-promised:np" }, - { "axes": { "x": ["2"] }, "cite": "{{cite}}" } - ] - } - ], - "not-promised": [ - { "id": "np", "source": "{{source}}", "sentence": "So.", "axes": { "x": ["1"] } } - ] - } - ] - """); - - private List Problems() => - [.. _repo.Queries().Check().Rows.Select(row => $"{row.Fields[0].Value} {row.Fields[1].Value} {row.Fields[2].Value}")]; - - [Fact] - public void Citations_of_a_tested_fact_check_clean_and_are_counted_by_kind() - { - Axes("KB-001"); - Claims("design/knowledge/KB-001-tested.md#kb-001-a-fact", "fact:KB-001"); - _repo.With("tools/probe/corpus/harness/Http.cs", "class Http { void M() { c.Inapplicable(Cite.Fact(\"KB-001\"), \"why\"); } }\n") - .With("tools/probe/corpus/probes/P.cs", "class P { Baseline b = Baseline.Fact( \"KB-001\" ); }\n"); - - var answer = _repo.Queries().Check(); - - answer.Rows.ShouldBeEmpty(answer.Text()); - answer.Summary.ShouldContain("5 fact citations (1 refused, 1 not-promised, 1 obligation, 2 harness)"); - var scan = FactCitations.Find(_repo.Root); - scan.Read.ShouldBe(new Dictionary { ["refused"] = 2, ["not-promised"] = 1, ["obligation"] = 2, ["harness"] = 2 }); - scan.Citations.Select(c => $"{c.Kind} {c.At} {c.Fact} {c.Baseline}").ShouldBe( - [ - "refused .claude/probe-axes.md:11 KB-001 False", - "obligation tools/probe/corpus/claims.json:11 KB-001 False", - "not-promised tools/probe/corpus/claims.json:16 KB-001 False", - "harness tools/probe/corpus/harness/Http.cs:1 KB-001 False", - "harness tools/probe/corpus/probes/P.cs:1 KB-001 True", - ]); - } - - [Fact] - public void A_refused_entry_citing_a_fact_without_a_test_fails_where_it_is_written() - { - Axes("KB-002"); - - Problems().ShouldBe([".claude/probe-axes.md:11 fact-not-executable-cited KB-002"]); - _repo.Queries().Check().Rows.Single().Fields.Single(f => f.Name == "detail").Value - .ShouldBe("the refused entry mvc.MaxValidationDepth=0 cites KB-002, and its test: is 'none, a sentence of the framework's documentation': " + - "a fact without its test explains only, never prunes or excuses a row; write its test in test/CodoMetis.TypeKit.Facts/, or cite a docs sentence"); - } - - [Fact] - public void A_not_promised_entry_whose_source_is_a_fact_without_a_test_fails() - { - Claims("design/knowledge/KB-002-prose.md#kb-002-a-fact", "fact:KB-001"); - - Problems().ShouldBe(["tools/probe/corpus/claims.json:16 fact-not-executable-cited KB-002"]); - } - - [Fact] - public void An_obligation_s_declaration_citing_a_fact_without_a_test_fails() - { - Claims("README.md#a", "fact:KB-002"); - - Problems().ShouldBe(["tools/probe/corpus/claims.json:11 fact-not-executable-cited KB-002"]); - } - - [Fact] - public void A_harness_citation_of_a_fact_without_a_test_fails_and_one_in_a_comment_does_not_count() - { - _repo.With("tools/probe/corpus/harness/Http.cs", - """ - class Http - { - // Cite.Fact("KB-002") is only mentioned here - /* and Cite.Fact("KB-002") here */ - string url = "http://example"; object o = Cite.Fact("KB-002"); - void M() => c.Inapplicable(Cite.Fact("KB-002"), "why"); - } - """); - - Problems().ShouldBe(["tools/probe/corpus/harness/Http.cs:5 fact-not-executable-cited KB-002", "tools/probe/corpus/harness/Http.cs:6 fact-not-executable-cited KB-002"]); - } - - [Fact] - public void A_cited_fact_whose_test_file_is_missing_fails_twice() - { - _repo.With("design/knowledge/KB-003-test-gone.md", Fact(3, "test/CodoMetis.TypeKit.Facts/KB003.cs")) - .With("tools/probe/corpus/probes/P.cs", "class P { object o = Cite.Fact(\"KB-003\"); }\n"); - - Problems().ShouldBe( - [ - "design/knowledge/KB-003-test-gone.md:7 fact-untested test/CodoMetis.TypeKit.Facts/KB003.cs", - "tools/probe/corpus/probes/P.cs:1 fact-not-executable-cited KB-003", - ]); - } - - [Fact] - public void A_cited_fact_that_does_not_exist_fails() - { - _repo.With("tools/probe/corpus/probes/P.cs", "class P { object o = Cite.Fact(\"KB-099\"); }\n"); - - Problems().ShouldBe(["tools/probe/corpus/probes/P.cs:1 fact-not-executable-cited KB-099"]); - } - - /// A baseline is the fact's measured answer, so it takes the fixture as well as the test. - [Fact] - public void A_baseline_of_a_tested_fact_without_a_fixture_fails() - { - _repo.With("design/knowledge/KB-004-no-fixture.md", Fact(4, "test/CodoMetis.TypeKit.Facts/KB004.cs")) - .With("test/CodoMetis.TypeKit.Facts/KB004.cs", "public sealed class KB004 { }\n") - .With("tools/probe/corpus/harness/Http.cs", "class Http { Baseline a = Baseline.Fact(\"KB-001\"), b = Baseline.Fact(\"KB-004\"); object c = Cite.Fact(\"KB-004\"); }\n"); - - Problems().ShouldBe(["tools/probe/corpus/harness/Http.cs:1 fact-not-executable-cited KB-004"]); - } - - [Fact] - public void A_fixture_must_exist_beside_the_test_of_a_tested_fact() - { - _repo.With("design/knowledge/KB-005-fixture-gone.md", Fact(5, "test/CodoMetis.TypeKit.Facts/KB005.cs", "fixture: test/CodoMetis.TypeKit.Facts/KB005.fixture.json")) - .With("test/CodoMetis.TypeKit.Facts/KB005.cs", "public sealed class KB005 { }\n") - .With("design/knowledge/KB-006-fixture-elsewhere.md", Fact(6, "test/CodoMetis.TypeKit.Facts/KB006.cs", "fixture: test/fixtures/KB006.json")) - .With("test/CodoMetis.TypeKit.Facts/KB006.cs", "public sealed class KB006 { }\n") - .With("test/fixtures/KB006.json", "{}") - .With("design/knowledge/KB-007-fixture-untested.md", Fact(7, "none, prose", "fixture: test/CodoMetis.TypeKit.Facts/KB007.fixture.json")) - .With("test/CodoMetis.TypeKit.Facts/KB007.fixture.json", "{}") - .With("test/CodoMetis.TypeKit.Facts/KB008.fixture.json", "{}"); - - Problems().ShouldBe( - [ - "design/knowledge/KB-005-fixture-gone.md:8 fact-fixture-missing test/CodoMetis.TypeKit.Facts/KB005.fixture.json", - "design/knowledge/KB-006-fixture-elsewhere.md:8 fact-fixture test/fixtures/KB006.json", - "design/knowledge/KB-007-fixture-untested.md:8 fact-fixture test/CodoMetis.TypeKit.Facts/KB007.fixture.json", - "test/CodoMetis.TypeKit.Facts/KB008.fixture.json:1 fact-orphan-fixture test/CodoMetis.TypeKit.Facts/KB008.fixture.json", - ]); - } - - [Fact] - public void Comments_are_blanked_and_strings_kept() - { - var source = "a // b\nc /* d\ne */ f \"// g\" @\"h \"\" // i\" \"\"\"raw // j\"\"\" '\"' k"; - - FactCitations.WithoutComments(source).ShouldBe("a \nc \n f \"// g\" @\"h \"\" // i\" \"\"\"raw // j\"\"\" '\"' k"); - } -} diff --git a/test/CodoMetis.TypeKit.DocsQuery.Tests/FactTests.cs b/test/CodoMetis.TypeKit.DocsQuery.Tests/FactTests.cs index 360c25ed..2b5b1536 100644 --- a/test/CodoMetis.TypeKit.DocsQuery.Tests/FactTests.cs +++ b/test/CodoMetis.TypeKit.DocsQuery.Tests/FactTests.cs @@ -168,4 +168,82 @@ public void What_is_installed_is_read_from_the_two_files() installed.Packages.ShouldContainKeyAndValue("Microsoft.AspNetCore.OpenApi", "10.0.12"); installed.Packages.Count.ShouldBe(2); } + + /// + /// A fact with an external pin is due once its verified: date is more than the given days old, and not a day + /// before; one pinning only what is installed never is, however old, since check compares its pins. + /// + [Fact] + public void Due_lists_an_external_fact_verified_more_than_the_given_days_ago() + { + _repo.With("design/knowledge/KB-002-installed-only.md", Fact(2, " - sdk 10.0.4xx", "none, prose").Replace("2026-10-15", "2025-01-01", StringComparison.Ordinal)); + + var today = new DateOnly(2026, 10, 29); + var due = _repo.Queries().Due(13, today, feed: null); + + due.ExitCode.ShouldBe(0); + due.Rows.Select(row => row.ToString()).ShouldBe(["at=design/knowledge/KB-001-a-fact.md:2 kind=fact-old id=KB-001 target=2026-10-15 detail=verified 14 days ago, more than 13: re-read it at its latest release"]); + due.Summary.ShouldStartWith("1 fact due of 1 fact pinned external"); + + var notYet = _repo.Queries().Due(14, today, feed: null); + notYet.Rows.ShouldBeEmpty(notYet.Text()); + notYet.ExitCode.ShouldBe(1); + } + + /// + /// Given a feed, a pin is due when NuGet lists a later release on its track: stable for a stable pin, any for a + /// pre-release, labels ordered as SemVer orders them (beta.10 after beta.2). A pin of a release still the latest, of + /// a name NuGet has no package of, or with no version (even where NuGet has a package of the name), is not. + /// + [Fact] + public void Due_with_a_feed_lists_a_pin_nuget_has_a_later_release_of() + { + _repo.With("design/knowledge/KB-002-competitors.md", Fact(2, + " - external Vogen 8.0.7\n - external Vogen 9.0.0-beta.2\n - external OneOf 3.0.271\n - external PostgreSQL 17.11\n - external NuGet registration data, 2026-10-29", + "none, prose")); + + var asked = new List(); + IReadOnlyList? Feed(string id) + { + asked.Add(id); + return id switch + { + "Vogen" => ["8.0.7", "9.0.0-beta.2", "9.0.0-beta.10"], + "OneOf" => ["3.0.255", "3.0.271"], + "NuGet" => ["0.0.1"], + _ => null, + }; + } + + var due = _repo.Queries().Due(1000, new DateOnly(2026, 10, 29), Feed); + + due.Rows.Select(row => $"{row.Fields[0].Value} {row.Fields[1].Value} {row.Fields[3].Value}: {row.Fields[4].Value}").ShouldBe( + [ + "design/knowledge/KB-002-competitors.md:5 fact-released external Vogen 9.0.0-beta.2: NuGet's latest Vogen is 9.0.0-beta.10: re-read it there", + ], due.Text()); + asked.ShouldBe(["Vogen", "Vogen", "Vogen", "OneOf", "PostgreSQL"], "KB-001's Vogen pin, then KB-002's; the pin with no version is never looked up"); + due.Summary.ShouldEndWith("or a later release on NuGet, against which 4 pins compared"); + + var released = _repo.Queries().Due(1000, new DateOnly(2026, 10, 29), id => id == "Vogen" ? ["8.0.7", "8.0.8", "9.0.0-beta.2"] : null); + released.Rows.Select(row => $"{row.Fields[0].Value} {row.Fields[4].Value}").ShouldBe( + [ + "design/knowledge/KB-001-a-fact.md:6 NuGet's latest stable Vogen is 8.0.8: re-read it there", + "design/knowledge/KB-002-competitors.md:4 NuGet's latest stable Vogen is 8.0.8: re-read it there", + ], released.Text()); + } + + [Theory] + [InlineData("1.0.0-beta.2", "1.0.0-beta.10", -1)] + [InlineData("1.0.0-beta-77", "1.0.0-beta-78", -1)] + [InlineData("1.0.0-beta", "1.0.0-beta.1", -1)] + [InlineData("1.0.0-alpha.9", "1.0.0-beta", -1)] + [InlineData("1.0.0-1", "1.0.0-alpha", -1)] + [InlineData("1.0.0-Beta08", "1.0.0-beta08", 0)] + [InlineData("1.0.0-rc.1", "1.0.0", -1)] + [InlineData("10.6.0", "10.5.1", 1)] + public void Versions_are_ordered_as_nuget_orders_them(string left, string right, int expected) + { + Pins.CompareVersions(left, right).ShouldBe(expected); + Pins.CompareVersions(right, left).ShouldBe(-expected); + } } diff --git a/test/CodoMetis.TypeKit.DocsQuery.Tests/RepositoryTests.cs b/test/CodoMetis.TypeKit.DocsQuery.Tests/RepositoryTests.cs index fd61d078..87616598 100644 --- a/test/CodoMetis.TypeKit.DocsQuery.Tests/RepositoryTests.cs +++ b/test/CodoMetis.TypeKit.DocsQuery.Tests/RepositoryTests.cs @@ -58,25 +58,6 @@ public void The_repository_checks_clean() answer.Rows.ShouldBeEmpty(answer.Text()); } - /// - /// Floors on the places the citation rule reads and on the citations it finds there (design/adr/0107), so that a - /// scan that found nothing cannot let the check above pass. On 2026-10-17: 3 refused entries, 41 not-promised - /// entries, 11 obligation declarations and 87 harness and probe sources were read, and KB-001 and KB-027 were - /// cited by not-promised entries, KB-037 twice by the harness. - /// - [Theory] - [InlineData(FactCitations.Refused, 3, 0)] - [InlineData(FactCitations.NotPromised, 30, 2)] - [InlineData(FactCitations.Obligation, 10, 0)] - [InlineData(FactCitations.Harness, 40, 2)] - public void The_corpus_s_citations_of_facts_are_found(string kind, int read, int cited) - { - var scan = FactCitations.Find(RepositoryRoot.Path); - - scan.Read[kind].ShouldBeGreaterThanOrEqualTo(read); - scan.Of(kind).ShouldBeGreaterThanOrEqualTo(cited); - } - [Fact] public void Every_record_is_listed_with_its_status_and_date() => Queries.List(["kind=adr"]).Rows.ShouldAllBe(row => row.Fields.Single(f => f.Name == "status").Value.Length > 0 && row.Fields.Single(f => f.Name == "date").Value.Length == 10); diff --git a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/CodoMetis.TypeKit.EntityFrameworkCore.Tests.csproj b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/CodoMetis.TypeKit.EntityFrameworkCore.Tests.csproj index 7782386b..ce8a11d3 100644 --- a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/CodoMetis.TypeKit.EntityFrameworkCore.Tests.csproj +++ b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/CodoMetis.TypeKit.EntityFrameworkCore.Tests.csproj @@ -2,8 +2,8 @@ @@ -20,6 +20,8 @@ + + diff --git a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/DesignTime.cs b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/DesignTime.cs index 704da534..73eb1faf 100644 --- a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/DesignTime.cs +++ b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/DesignTime.cs @@ -30,8 +30,11 @@ public static ServiceProvider Services(DbContext db) return services.BuildServiceProvider(); } - /// The context's compiled model, as dotnet ef dbcontext optimize writes it, compiled and loaded. - public static IModel CompiledModel(DbContext db) + /// + /// The context's compiled model, as dotnet ef dbcontext optimize writes it, compiled and loaded; with + /// , as --nativeaot writes it, type mappings included. + /// + public static IModel CompiledModel(DbContext db, bool nativeAot = false) { using var designTime = Services(db); var options = new CompiledModelCodeGenerationOptions @@ -40,6 +43,7 @@ public static IModel CompiledModel(DbContext db) ModelNamespace = $"CompiledModel{Guid.NewGuid():N}", Language = "C#", UseNullableReferenceTypes = true, + ForNativeAot = nativeAot, }; var files = designTime.GetRequiredService().Select(options) .GenerateModel(db.GetService().Model, options); diff --git a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/KeyGenerationTests.cs b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/KeyGenerationTests.cs index 1bc61232..df2783e3 100644 --- a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/KeyGenerationTests.cs +++ b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/KeyGenerationTests.cs @@ -1,6 +1,7 @@ using CodoMetis.TypeKit.ValueObjects; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; namespace CodoMetis.TypeKit.EntityFrameworkCore.Tests; @@ -180,19 +181,26 @@ public void A_char_key_the_application_configures_keeps_its_configuration(string } /// - /// Pinned as documented, not as wanted: where the provider stores a ulong as a decimal - /// (SQL Server, PostgreSQL), EF gives a value object's default key a temporary decimal, which is - /// negative, and converting it back throws at Add; a plain ulong key gets a temporary - /// ulong. Configured ValueGeneratedNever(), the key the application assigns is added. + /// A default key over ulong gets a temporary ulong at Add, as a plain key does, also where the + /// provider stores it as a decimal (SQL Server, PostgreSQL). EF chooses a temporary number by the property's + /// type mapping, which for a value object is its own, so it fell back to the column's type, and the negative + /// temporary decimal threw converted back (decision 63, now 114). + /// Configured ValueGeneratedNever(), the key the application assigns is added. /// [Theory] - [InlineData("sqlserver")] - [InlineData("npgsql")] - public void A_default_key_over_ulong_throws_at_Add_where_the_column_is_a_decimal(string provider) + [MemberData(nameof(AllProviders))] + public void A_default_key_over_ulong_gets_a_temporary_value_at_Add_as_a_plain_one_does(string provider) { using var db = new CharAndULongKeysDb(Options(provider)); - Should.Throw(() => db.Add(new KeyedByUInt64())); + KeyedByUInt64[] keyed = [new(), new()]; + KeyedByUlong[] plain = [new(), new()]; + db.AddRange([.. keyed, .. plain]); + + keyed.Select(e => db.Entry(e).Property(p => p.Id).IsTemporary) + .ShouldBe(plain.Select(e => db.Entry(e).Property(p => p.Id).IsTemporary), $"on {provider}"); + keyed.Select(e => db.Entry(e).Property(p => p.Id).CurrentValue.Value) + .ShouldBe(plain.Select(e => db.Entry(e).Property(p => p.Id).CurrentValue), $"on {provider}"); var assigned = new AssignedUInt64Key { Id = UInt64Key.From(42) }; db.Add(assigned); @@ -202,6 +210,106 @@ public void A_default_key_over_ulong_throws_at_Add_where_the_column_is_a_decimal Key(db, typeof(AssignedUInt64Key)).ValueGenerated.ShouldBe(ValueGenerated.Never); } + public static TheoryData DecimalColumnProviders => ["sqlserver", "npgsql"]; + + /// + /// SQL Server's HiLo still comes before a temporary value, for a ulong key as for a plain one, and hands out + /// values that are not temporary. A generator configured on the property would have come before HiLo, so the + /// temporary value is the type mapping's (decision 114). No database is opened: this is the generator EF chooses, + /// not its values. PostgreSQL's provider refuses HiLo on a ulong key, plain or value object, when it validates the model. + /// + [Fact] + public void SQL_Server_s_HiLo_still_comes_first_for_a_key_over_ulong() + { + using var db = new ULongHiLoKeysDb(Options("sqlserver")); + var selector = db.GetService(); + + bool Temporary(Type entity) + { + var entityType = db.Model.FindEntityType(entity)!; + selector.TrySelect(entityType.FindPrimaryKey()!.Properties.Single(), entityType, out var generator).ShouldBeTrue(entity.Name); + return generator!.GeneratesTemporaryValues; + } + + Temporary(typeof(HiLoUlongKey)).ShouldBeFalse(); + Temporary(typeof(HiLoUInt64Key)).ShouldBeFalse("HiLo was not chosen"); + } + + /// A generator the application configures on a ulong key still comes before a temporary value. + [Theory] + [MemberData(nameof(DecimalColumnProviders))] + public void The_application_s_own_generator_still_comes_first_for_a_key_over_ulong(string provider) + { + using var db = new ULongHiLoKeysDb(Options(provider)); + + var own = new OwnGeneratorUInt64Key(); + db.Add(own); + + own.Id.Value.ShouldBe(7ul, $"on {provider}"); + } + + /// + /// A compiled model gives a default ulong key the same temporary value: dbcontext optimize leaves the + /// type mappings to be found when the model is first used, where UseTypeKit() composes them. + /// + [Theory] + [MemberData(nameof(DecimalColumnProviders))] + public void In_a_compiled_model_a_default_key_over_ulong_gets_a_temporary_value(string provider) + { + using var db = Compiled(provider, nativeAot: false); + + KeyedByUInt64[] keyed = [new(), new()]; + KeyedByUlong[] plain = [new(), new()]; + db.AddRange([.. keyed, .. plain]); + + keyed.Select(e => db.Entry(e).Property(p => p.Id).IsTemporary).ShouldBe([true, true], $"on {provider}"); + keyed.Select(e => db.Entry(e).Property(p => p.Id).CurrentValue.Value) + .ShouldBe(plain.Select(e => db.Entry(e).Property(p => p.Id).CurrentValue), $"on {provider}"); + } + + /// + /// Pinned as documented, not as wanted: --nativeaot writes each type mapping out as code, and the factory a + /// value object's mapping carries is a delegate it cannot write, so a default ulong key still throws at + /// Add where the column is a decimal (decision 114). ValueGeneratedNever() is the workaround. + /// + [Theory] + [MemberData(nameof(DecimalColumnProviders))] + public void In_a_Native_AOT_compiled_model_a_default_key_over_ulong_still_throws_at_Add(string provider) + { + using var db = Compiled(provider, nativeAot: true); + + Should.Throw(() => db.Add(new KeyedByUInt64())); + } + + /// + /// Pinned as documented, not as wanted: a key configured with by hand + /// is mapped on that converter, which carries no hints, so a default ulong key still throws at Add where + /// the column is a decimal (decision 114). + /// + [Theory] + [MemberData(nameof(DecimalColumnProviders))] + public void A_default_key_over_ulong_with_the_converter_named_by_hand_still_throws_at_Add(string provider) + { + using var db = new NamedConverterULongKeyDb(Options(provider)); + + Should.Throw(() => db.Add(new KeyedByUInt64())); + } + + /// + /// A context on the compiled model, with an internal service provider of its own: EF caches a property's generator + /// by the model's id, which a compiled model keeps from the model it was written from. + /// + private CompiledULongKeysDb Compiled(string provider, bool nativeAot) + { + using var design = new CompiledULongKeysDb(Options(provider)); + var compiled = DesignTime.CompiledModel(design, nativeAot); + var db = new CompiledULongKeysDb( + new DbContextOptionsBuilder(Options(provider)).UseModel(compiled).EnableServiceProviderCaching(false).Options); + + db.Model.ShouldBeSameAs(compiled); + return db; + } + /// /// Each integer type SQLite fills a key of, added twice. Not char: EF's own char key asks /// SQLite for a TEXT column with AUTOINCREMENT, which it refuses. @@ -549,10 +657,57 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().Property(e => e.Id).ValueGeneratedOnAdd(); modelBuilder.Entity().Property(e => e.Id).HasValueGenerator(); modelBuilder.Entity(); + modelBuilder.Entity(); modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever(); } } +public class HiLoUInt64Key { public UInt64Key Id { get; set; } } + +public class HiLoUlongKey { public ulong Id { get; set; } } + +public class OwnGeneratorUInt64Key { public UInt64Key Id { get; set; } } + +public sealed class SevenGenerator : Microsoft.EntityFrameworkCore.ValueGeneration.ValueGenerator +{ + public override bool GeneratesTemporaryValues => false; + + public override UInt64Key Next(Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry entry) => UInt64Key.From(7); +} + +/// A key over ulong configured with the public converter by hand. +public sealed class NamedConverterULongKeyDb(DbContextOptions options) : DbContext(options) +{ + protected override void OnModelCreating(ModelBuilder modelBuilder) => + modelBuilder.Entity().Property(e => e.Id).HasConversion>(); +} + +/// A default key over ulong and a plain one, for a compiled model, which refuses a generator type. +public sealed class CompiledULongKeysDb(DbContextOptions options) : DbContext(options) +{ + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(); + modelBuilder.Entity(); + } +} + +/// Keys over ulong numbered by SQL Server's HiLo, and one numbered by the application's own generator. +public sealed class ULongHiLoKeysDb(DbContextOptions options) : DbContext(options) +{ + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // Npgsql refuses HiLo on a ulong key, plain or value object: "Sequences can only be used with integer properties". + if (Database.IsSqlServer()) + { + SqlServerPropertyBuilderExtensions.UseHiLo(modelBuilder.Entity().Property(e => e.Id)); + SqlServerPropertyBuilderExtensions.UseHiLo(modelBuilder.Entity().Property(e => e.Id)); + } + + modelBuilder.Entity().Property(e => e.Id).HasValueGenerator(); + } +} + /// The keys over the integer types SQLite fills in. public sealed class SqliteIntegerKeysDb(DbContextOptions options) : DbContext(options) { diff --git a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/KeyGenerationVariantTests.cs b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/KeyGenerationVariantTests.cs index df1fda2c..dd58a4c2 100644 --- a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/KeyGenerationVariantTests.cs +++ b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/KeyGenerationVariantTests.cs @@ -88,22 +88,9 @@ public void A_value_object_key_generated_on_add_gets_values_as_a_key_of_the_wrap var plain = Added(provider, wrapped.Wrapped, typeKit: false); if (plain is null) Assert.Skip("the wrapped type's key is not generated at Add"); - Added(provider, wrapped.ValueObject, typeKit: true).ShouldBe(AddedExpected(provider, wrapped, plain), $"{kind} configured OnAdd on {provider}"); + Added(provider, wrapped.ValueObject, typeKit: true).ShouldBe(plain, $"{kind} configured OnAdd on {provider}"); } - /// - /// The README's departures at Add: "A default key over ulong throws at Add where the column - /// is a decimal (SQL Server, PostgreSQL)", and on SQL Server a key over char configured - /// ValueGeneratedOnAdd() gets "one character of a GUID string", since EF reaches the generator through the - /// column's nvarchar(1), so sixteen of them collide. - /// - private static string? AddedExpected(string provider, WrappedKind kind, string plain) => (provider, kind.Name) switch - { - ("npgsql" or "sqlserver", "ulong") => null, - ("sqlserver", "char") => null, - _ => plain, - }; - /// What the values generated at Add are, or null where Add generates none or refuses. private string? Added(string provider, Type key, bool typeKit) { diff --git a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/PostgreSqlRoundTripTests.cs b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/PostgreSqlRoundTripTests.cs index d65a26b5..562d7368 100644 --- a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/PostgreSqlRoundTripTests.cs +++ b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/PostgreSqlRoundTripTests.cs @@ -9,6 +9,7 @@ namespace CodoMetis.TypeKit.EntityFrameworkCore.Tests; /// A real PostgreSQL: every wrapped-type family survives a round trip, stored values are read back /// without the rules, and the translated queries return the rows they should. /// +[Trait("Needs", "Docker")] public sealed class PostgreSqlRoundTripTests(PostgreSqlRoundTripTests.Database database) : IClassFixture { [Fact] diff --git a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/QueryOptionTests.cs b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/QueryOptionTests.cs index 58099512..cf46fb21 100644 --- a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/QueryOptionTests.cs +++ b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/QueryOptionTests.cs @@ -76,8 +76,8 @@ public async Task A_condition_only_the_database_can_run_is_translated() /// /// What the documentation says of a condition EF cannot read. A lambda written at the call is an /// expression and binds to the query's overload; a Func held in a variable, or a method group, - /// binds to the sequence's, which reads every row and runs the condition in memory. No analyzer - /// tells it yet, so this pins the behaviour the pages describe. + /// binds to the sequence's, which reads every row and runs the condition in memory. CMTK0009 reports + /// such a call (QueryDelegateConditionTests); this pins the behaviour it warns of. /// [Fact] public void A_Func_or_a_method_group_reads_every_row_and_runs_the_condition_in_memory() diff --git a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/QueryTranslationVariantTests.cs b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/QueryTranslationVariantTests.cs index fdfa4e7a..a1600950 100644 --- a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/QueryTranslationVariantTests.cs +++ b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/QueryTranslationVariantTests.cs @@ -68,32 +68,59 @@ public void A_query_over_a_value_object_answers_as_the_same_query_over_the_wrapp if (expectation is Unclaimed unclaimed) Assert.Skip(unclaimed.Reason); if (expectation is OpenFinding finding) Assert.Skip($"open finding: {finding.Reason}"); - var valueObject = Run(@case, valueObject: true); + if (!AnswersAsTheWrappedType(@case, expectation, Run, out _, out var plain)) return; + + // PostgreSQL translates where it would refuse what SQLite runs: an element column re-typed to the + // wrapped type, for one, threw there alone. + if (plain is null || PostgreSql(@case, valueObject: false) is not { Refusal: null } wrapped) return; + var translated = PostgreSql(@case, valueObject: true)!; + if (translated.Refusal is not null) + throw new ShouldAssertException($"PostgreSQL refuses what it translates over the wrapped type: {translated.Refusal}"); + if (ClaimsTheColumn(@case)) + translated.Sql.ShouldBe(wrapped.Sql, "PostgreSQL's SQL of the same query over the wrapped type"); + } + + /// + /// The case's query over the value object, run by , against its expectation: refused as the + /// docs say, or answering the rows (and, on a table's column, the SQL) of the same query over the wrapped type, or + /// memory's rows where that query is refused or not written. False where a refusal settles the case. + /// + /// + /// names the cases a database is known to answer differently from memory, a fact of + /// design/knowledge: where memory is the oracle, such a case is left to the caller. + /// + internal static bool AnswersAsTheWrappedType(QueryCase @case, Expectation expectation, Func run, out Answer valueObject, out Answer? plain, + Func? differs = null) + { + valueObject = run(@case, true); + plain = null; switch (expectation) { case RefusedOrdering: Refusal(valueObject, "its ordering is its own CompareTo").ShouldContain(@case.Kind.ValueObject.Name); - return; + return false; // The same last step through EF.Functions, which EF takes in memory too, where a database's function cannot run. case RefusedForeign when @case is { Op: Op.Greatest or Op.Least, Site: Site.Column or Site.Nullable }: Refusal(valueObject, "is not supported because the query has switched to client-evaluation"); - return; + return false; case RefusedForeign when @case.Op is Op.Select or Op.Distinct: var memory = Memory(@case); if (memory.Refusal is { } thrown) { // r.N!.Value over the null row, read through the converter and then unwrapped in memory, as memory does. valueObject.Refusal.ShouldBeOfType(thrown.GetType(), "the query's last step, which throws in memory too"); - return; + return false; } valueObject.Refusal.ShouldBeNull("the query's last step, which EF evaluates in memory after reading the value object through its converter"); valueObject.Rows.ShouldBe(memory.Rows, "the query's last step, evaluated in memory"); - return; + return false; case RefusedForeign: Refusal(valueObject, "could not be translated"); - return; + return false; + case NullsLeftOut when differs?.Invoke(@case, valueObject, Memory(@case)) == true: + return false; case NullsLeftOut: valueObject.Refusal.ShouldBeNull(); var nulls = NullRows(@case); @@ -102,16 +129,17 @@ public void A_query_over_a_value_object_answers_as_the_same_query_over_the_wrapp "the rows memory keeps are the query's and the null rows, which the generated operators sort first"); // A string has no < to write the wrapped type's query with; memory is no oracle here. - if (!new QueryBuilder(@case, valueObject: false).CanBuild) return; + if (!new QueryBuilder(@case, valueObject: false).CanBuild) return false; break; } - var plain = new QueryBuilder(@case, valueObject: false).CanBuild ? Run(@case, valueObject: false) : null; + plain = new QueryBuilder(@case, valueObject: false).CanBuild ? run(@case, false) : null; + if (plain is not { Refusal: null } && differs?.Invoke(@case, valueObject, Memory(@case)) == true) return false; if (valueObject.Refusal is not null) { if (plain is { Refusal: null }) throw new ShouldAssertException($"refused where the same query over the wrapped type answers [{string.Join(", ", plain.Rows!)}]: {valueObject.Refusal}"); if (plain is null) throw new ShouldAssertException($"refused, where memory answers {Shown(Memory(@case))}: {valueObject.Refusal}"); - return; + return false; } if (plain is { Refusal: null }) @@ -125,14 +153,7 @@ public void A_query_over_a_value_object_answers_as_the_same_query_over_the_wrapp valueObject.Rows.ShouldBe(Memory(@case).Rows, $"the rows memory answers, where the wrapped type's query is {(plain is null ? "not written" : "refused")}"); } - // PostgreSQL translates where it would refuse what SQLite runs: an element column re-typed to the - // wrapped type, for one, threw there alone. - if (plain is null || PostgreSql(@case, valueObject: false) is not { Refusal: null } wrapped) return; - var translated = PostgreSql(@case, valueObject: true)!; - if (translated.Refusal is not null) - throw new ShouldAssertException($"PostgreSQL refuses what it translates over the wrapped type: {translated.Refusal}"); - if (ClaimsTheColumn(@case)) - translated.Sql.ShouldBe(wrapped.Sql, "PostgreSQL's SQL of the same query over the wrapped type"); + return true; } /// A guard against a vacuous run: the catalogue's floor, every kind, site and accessor in it, and the refusals it expects. @@ -151,34 +172,53 @@ public void The_catalogue_reaches_every_kind_site_accessor_and_converter() all.Count(@case => Expect(@case) is NullsLeftOut).ShouldBeGreaterThan(5); } - private abstract record Expectation; + /// + /// A guard on the harness: one seeded database read by many cases at once, as this class and + /// read it from two collections, answers each the same rows, never + /// "database is locked" (#148). + /// + [Fact] + public void A_seeded_database_read_by_many_cases_at_once_answers_each_alike() + { + var @case = QueryShapes.All().First(@case => @case is { Op: Op.OrderBy, Site: Site.Nullable } && Run(@case, valueObject: true).Rows is not null); + var first = Run(@case, valueObject: true); + + var answers = new ConcurrentBag(); + Parallel.For(0, 400, new ParallelOptions { MaxDegreeOfParallelism = Math.Max(8, Environment.ProcessorCount * 2) }, + _ => answers.Add(Run(@case, valueObject: true))); + + answers.ShouldAllBe(answer => answer.Refusal == null && answer.Rows!.SequenceEqual(first.Rows!), + answers.FirstOrDefault(answer => answer.Refusal != null) is { } refused ? Shown(refused) : null); + } + + internal abstract record Expectation; /// Answers as the same query over the wrapped type. - private sealed record Same : Expectation; + internal sealed record Same : Expectation; /// Refused, naming the value object whose own CompareTo the database cannot run. - private sealed record RefusedOrdering : Expectation; + internal sealed record RefusedOrdering : Expectation; /// /// Refused by EF, "could not be translated": the column holds what a converter of the application's own /// stores. A final Select or Distinct, which EF evaluates in memory after reading the value /// object through that converter, answers as memory does. /// - private sealed record RefusedForeign : Expectation; + internal sealed record RefusedForeign : Expectation; /// /// Answers as the same query over the wrapped type, where a null passes no comparison, which is fewer rows /// than memory's: a record class's generated operators sort null first (decision 98). /// - private sealed record NullsLeftOut : Expectation; + internal sealed record NullsLeftOut : Expectation; /// A divergence the catalogue found and no decision settles yet: named here, never dropped from the catalogue. - private sealed record OpenFinding(string Reason) : Expectation; + internal sealed record OpenFinding(string Reason) : Expectation; - /// No sentence of the docs says what the case answers. - private sealed record Unclaimed(string Reason) : Expectation; + /// No sentence of the docs says what the case answers; where that is SQLite's doing alone. + internal sealed record Unclaimed(string Reason, bool SqliteOnly = false) : Expectation; - private static Expectation Expect(QueryCase @case) + internal static Expectation Expect(QueryCase @case) { if (@case.IsForeign) { @@ -194,7 +234,7 @@ private static Expectation Expect(QueryCase @case) // JSON text EF stores: "1:2:00:00" before "2:00:00", "09:30:00.0000000" against a parameter '09:30:00'. The // wrapped type's own max over a TimeSpan column answers the same text order (measured on 2026-10-16). if (@case is { Site: Site.CollectionElement, Op: Op.Greatest or Op.Least, ReadsWrapped: true } && (@case.Kind.Wrapped == typeof(TimeSpan) || @case.Kind.Wrapped == typeof(TimeOnly))) - return new Unclaimed("SQLite orders a primitive collection's TimeSpan and TimeOnly elements as the text EF stores, and the wrapped type's query is refused there"); + return new Unclaimed("SQLite orders a primitive collection's TimeSpan and TimeOnly elements as the text EF stores, and the wrapped type's query is refused there", SqliteOnly: true); // The generated operators of a record class sort null first, so in memory null < x is true; SQL's // "N" < @x is unknown for a null N, as it is for the wrapped type's lifted operator. @@ -226,12 +266,16 @@ private static string Refusal(Answer answer, string expected) return answer.Refusal.Message; } - private sealed record Answer(string[]? Rows, string? Sql, Exception? Refusal); + internal sealed record Answer(string[]? Rows, string? Sql, Exception? Refusal); - /// One in-memory SQLite database per model, seeded once and only read. + /// + /// One in-memory SQLite database per model, seeded once and only read, one query at a time: a connection is not + /// thread-safe, and reads the same databases from its own collection, in + /// parallel with this one, where two commands at once are "database is locked" (#148). + /// private static readonly ConcurrentDictionary> Databases = new(); - private static Answer Run(QueryCase @case, bool valueObject) + internal static Answer Run(QueryCase @case, bool valueObject) { var builder = new QueryBuilder(@case, valueObject); var key = @case.IsFilter @@ -241,16 +285,19 @@ private static Answer Run(QueryCase @case, bool valueObject) var (connection, refusal) = Databases.GetOrAdd(key, _ => new Lazy<(SqliteConnection, Exception?)>(() => Seeded(model, builder))).Value; if (refusal is not null) return new Answer(null, null, refusal); - using var db = QueryDb.Create(model, connection); - try - { - var set = (IQueryable)typeof(DbContext).GetMethod(nameof(DbContext.Set), Type.EmptyTypes)!.MakeGenericMethod(builder.Source).Invoke(db, null)!; - var (query, ordered) = builder.Build(set.Expression); - return Execute(set.Provider, query, ordered, sql: true); - } - catch (Exception exception) when (exception is not ShouldAssertException) + lock (connection) { - return new Answer(null, null, Unwrapped(exception)); + using var db = QueryDb.Create(model, connection); + try + { + var set = (IQueryable)typeof(DbContext).GetMethod(nameof(DbContext.Set), Type.EmptyTypes)!.MakeGenericMethod(builder.Source).Invoke(db, null)!; + var (query, ordered) = builder.Build(set.Expression); + return Execute(set.Provider, query, ordered, sql: true); + } + catch (Exception exception) when (exception is not ShouldAssertException) + { + return new Answer(null, null, Unwrapped(exception)); + } } } @@ -281,7 +328,7 @@ private static Answer Run(QueryCase @case, bool valueObject) /// The same query over the value objects in memory, with the query filter applied as EF would, and /// EF.Functions answered as does. /// - private static Answer Memory(QueryCase @case) + internal static Answer Memory(QueryCase @case) { var builder = new QueryBuilder(@case, valueObject: true); var source = Queryable.AsQueryable(Seed.Entities(builder, @case.Kind, valueObject: true)); @@ -302,7 +349,9 @@ private static Answer Memory(QueryCase @case) /// /// EF.Functions.Greatest, Least and Npgsql's row-value comparisons, which throw outside a query, as /// SQLite answers them: by each type's default comparer, which is the wrapped type's order for a value object - /// that declares none of its own, and a null wherever Greatest or Least meets one. + /// that declares none of its own, and a null wherever Greatest or Least meets one. Text compares + /// ordinally, as SQLite's max and a value object's generated comparison do, where a string's default + /// comparer follows the culture. /// private sealed class InMemoryFunctions : ExpressionVisitor { @@ -322,7 +371,9 @@ protected override Expression VisitMethodCall(MethodCallExpression node) public static T Least(T[] values) => Pick(values, order => order < 0); private static T Pick(T[] values, Func keeps) => - values.Any(value => value is null) ? default! : values.Aggregate((kept, value) => keeps(Comparer.Default.Compare(value, kept)) ? value : kept); + values.Any(value => value is null) ? default! : values.Aggregate((kept, value) => keeps(Order().Compare(value, kept)) ? value : kept); + + private static IComparer Order() => typeof(T) == typeof(string) ? (IComparer)StringComparer.Ordinal : Comparer.Default; /// A tuple's own order is its elements', one after the other, each by its default comparer. public static bool RowValue(string comparison, ITuple left, ITuple right) @@ -339,7 +390,7 @@ public static bool RowValue(string comparison, ITuple left, ITuple right) } /// An answer as a message shows it: its rows, or its refusal. - private static string Shown(Answer answer) => + internal static string Shown(Answer answer) => answer.Refusal is { } refusal ? $"refused ({refusal.Message})" : $"[{string.Join(", ", answer.Rows!)}]"; private static (SqliteConnection, Exception?) Seeded(QueryModel model, QueryBuilder builder) @@ -360,7 +411,7 @@ private static (SqliteConnection, Exception?) Seeded(QueryModel model, QueryBuil } } - private static Answer Execute(IQueryProvider provider, Expression query, bool ordered, bool sql) + internal static Answer Execute(IQueryProvider provider, Expression query, bool ordered, bool sql) { if (!typeof(IQueryable).IsAssignableFrom(query.Type)) return new Answer([Text(provider.Execute(query))], null, null); @@ -379,5 +430,5 @@ private static string Text(object? value) => ? $"#{id.GetValue(value)}" : WrappedKind.Text(value); - private static Exception Unwrapped(Exception exception) => exception is TargetInvocationException { InnerException: { } inner } ? Unwrapped(inner) : exception; + internal static Exception Unwrapped(Exception exception) => exception is TargetInvocationException { InnerException: { } inner } ? Unwrapped(inner) : exception; } diff --git a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServer.cs b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServer.cs new file mode 100644 index 00000000..f53ac770 --- /dev/null +++ b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServer.cs @@ -0,0 +1,91 @@ +using System.Collections.Concurrent; +using Microsoft.Data.SqlClient; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; + +namespace CodoMetis.TypeKit.EntityFrameworkCore.Tests; + +/// +/// One SQL Server container for every class of the , started when the first of +/// them runs: the server takes seconds to start, and a class that needs it never starts one of its own. +/// +/// +/// SQL Server 2022's Linux image, which CI runs and the design/knowledge entries pin. It is amd64 only: on an +/// arm64 machine Docker must emulate it with Rosetta, since under QEMU it stops at start ("Unable to create a new +/// asynchronous I/O context"). TYPEKIT_MSSQL_IMAGE names another image, such as the native arm64 +/// mcr.microsoft.com/azure-sql-edge, whose older engine lacks some of 2022's functions. A database is made +/// for each class (), under the server's default collation, +/// SQL_Latin1_General_CP1_CI_AS, which is case-insensitive. +/// +public sealed class SqlServer : IAsyncLifetime +{ + public const string ImageVariable = "TYPEKIT_MSSQL_IMAGE"; + + private const string Password = "TypeKit-2026!"; + + /// Started by its log line: Testcontainers' module waits through sqlcmd, which Azure SQL Edge's image has not. + private readonly IContainer _container = + new ContainerBuilder(Environment.GetEnvironmentVariable(ImageVariable) is { Length: > 0 } image ? image : "mcr.microsoft.com/mssql/server:2022-latest") + .WithEnvironment("ACCEPT_EULA", "Y") + .WithEnvironment("MSSQL_SA_PASSWORD", Password) + .WithPortBinding(1433, assignRandomHostPort: true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("SQL Server is now ready for client connections")) + .Build(); + + private readonly ConcurrentDictionary> _databases = new(); + + private string ConnectionString(string database) => new SqlConnectionStringBuilder + { + DataSource = $"{_container.Hostname},{_container.GetMappedPublicPort(1433)}", + UserID = "sa", + Password = Password, + InitialCatalog = database, + TrustServerCertificate = true, + }.ConnectionString; + + /// A database of its own on the server, created empty the first time it is asked for, and its connection string. + public Task DatabaseAsync(string name) => _databases.GetOrAdd(name, CreateAsync); + + private async Task CreateAsync(string name) + { + await using var connection = await OpenAsync(ConnectionString("master")); + await using var create = connection.CreateCommand(); + create.CommandText = $"IF DB_ID(N'{name}') IS NOT NULL DROP DATABASE [{name}]; CREATE DATABASE [{name}];"; + await create.ExecuteNonQueryAsync(); + + return ConnectionString(name); + } + + /// + /// The server logs that it is ready before the login works, the first time it starts (a "Login failed for user 'sa'" + /// for some seconds after), so the first connection is tried again for a minute. + /// + private static async Task OpenAsync(string connectionString) + { + for (var attempt = 1; ; attempt++) + { + var connection = new SqlConnection(connectionString); + try + { + await connection.OpenAsync(); + return connection; + } + catch (SqlException) when (attempt < 60) + { + await connection.DisposeAsync(); + await Task.Delay(TimeSpan.FromSeconds(1)); + } + } + } + + public async ValueTask InitializeAsync() => await _container.StartAsync(); + + public async ValueTask DisposeAsync() => await _container.DisposeAsync(); +} + +/// The classes that share the SQL Server container; CI's Windows and macOS legs, which cannot start it, leave them out. +[CollectionDefinition(Name)] +public sealed class SqlServerCollection : ICollectionFixture +{ + public const string Name = "SQL Server"; +} diff --git a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServerKeyVariantTests.cs b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServerKeyVariantTests.cs new file mode 100644 index 00000000..ef42e4b1 --- /dev/null +++ b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServerKeyVariantTests.cs @@ -0,0 +1,188 @@ +using System.Globalization; +using CodoMetis.TypeKit.EntityFrameworkCore.Tests.Variants; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage; + +namespace CodoMetis.TypeKit.EntityFrameworkCore.Tests; + +/// +/// The key-generation catalogue () inserted into a real SQL Server and read back: every shape +/// keyed by every kind of , under UseTypeKit() and keyed by the wrapped type +/// under plain EF, each in a schema of its own. +/// +/// +/// holds the model: a value-object key is generated as a key of the wrapped +/// type is. Here the database answers. A key the model generates is left unset, one it does not is assigned, and +/// every key reads back as it was after SaveChanges. Where the database numbers the keys (an identity column, +/// TPC's sequence), the value object gets the numbers the wrapped type gets. +/// +[Collection(SqlServerCollection.Name)] +[Trait("Needs", "Docker")] +public sealed class SqlServerKeyVariantTests(SqlServer server) +{ + public static TheoryData Cases + { + get + { + var cases = new TheoryData(); + foreach (var (_, shape, kind) in KeyShapes.All().Where(@case => @case.Provider == "sqlserver")) cases.Add(shape, kind.Name); + return cases; + } + } + + [Theory] + [MemberData(nameof(Cases))] + public async Task A_value_object_key_is_stored_and_read_back_as_a_key_of_the_wrapped_type_is(KeyShape shape, string kind) + { + var wrapped = WrappedKinds.Named(kind); + var database = await server.DatabaseAsync("typekit_keys"); + + var plain = Inserted(shape, wrapped, typeKit: false, database); + var valueObject = Inserted(shape, wrapped, typeKit: true, database); + var keys = $"{shape} keyed by {kind}: value object [{string.Join(", ", valueObject.Saved)}], wrapped type [{string.Join(", ", plain.Saved)}]"; + + if (plain.Refusal is not null) + { + if (valueObject.Refusal is not null) Assert.Skip($"refused for the wrapped type and the value object alike: {plain.Refusal}"); + + // A key over char, which the application assigns (the README's exception), where EF generates the plain one as NULL. + wrapped.Wrapped.ShouldBe(typeof(char), $"stored where the wrapped type's keys are refused ({plain.Refusal}); {keys}"); + ReadBack(valueObject, wrapped).ShouldBe(Saved(valueObject, wrapped), ignoreOrder: true, $"the keys read back; {keys}"); + return; + } + + valueObject.Refusal.ShouldBeNull(keys); + ReadBack(valueObject, wrapped).ShouldBe(Saved(valueObject, wrapped), ignoreOrder: true, $"the keys read back; {keys}"); + ReadBack(plain, wrapped).ShouldBe(Saved(plain, wrapped), ignoreOrder: true, $"the wrapped type's keys read back; {keys}"); + + // The README's two exceptions: a key over char or Guid is the application's to assign unless configured. + var assigned = (wrapped.Wrapped == typeof(char) || wrapped.Wrapped == typeof(Guid)) && shape is not (KeyShape.ConfiguredOnAdd or KeyShape.ConfiguredNever); + valueObject.Generated.ShouldBe(plain.Generated && !assigned, keys); + valueObject.Numbered.ShouldBe(plain.Numbered && !assigned, keys); + if (plain.Numbered && IsInteger(wrapped.Wrapped)) + valueObject.Saved.ShouldBe(plain.Saved, $"the keys SQL Server numbered; {keys}"); + } + + /// A guard against a vacuous run: every shape crossed with every key kind, and the database numbering some. + [Fact] + public void The_catalogue_crosses_every_shape_with_every_key_kind() => + Cases.Count.ShouldBe(Enum.GetValues().Length * WrappedKinds.Keys.Length); + + /// + /// A decimal reads back at its column's scale, 1.50 for 1.5 in SQL Server's default decimal(18,2) + /// (KB-050), and is the same number: decimal keys are compared as numbers. + /// + private static string[] ReadBack(Keys keys, WrappedKind kind) => kind.Wrapped == typeof(decimal) ? [.. keys.Read.Select(Number)] : keys.Read; + + private static string[] Saved(Keys keys, WrappedKind kind) => kind.Wrapped == typeof(decimal) ? [.. keys.Saved.Select(Number)] : keys.Saved; + + private static string Number(string key) => + string.Join("/", key.Split('/').Select(part => decimal.Parse(part, CultureInfo.InvariantCulture).ToString("0.############", CultureInfo.InvariantCulture))); + + private static int _schemas; + + /// The keys saved and read back, whether the subject's key is generated, and whether the database left any key it saved to the database. + private sealed record Keys(string[] Saved, string[] Read, bool Generated, bool Numbered, string? Refusal); + + /// Two entities of the shape saved, the keys they have after SaveChanges, and the keys a fresh context reads. + private static Keys Inserted(KeyShape shape, WrappedKind kind, bool typeKit, string database) + { + var key = typeKit ? kind.ValueObject : kind.Wrapped; + var schema = $"k{Interlocked.Increment(ref _schemas)}"; + try + { + using (var create = KeyShapeDb.OnSqlServer(shape, key, typeKit, database, schema)) + create.GetService().CreateTables(); + + string[] saved; + bool generated, numbered; + using (var db = KeyShapeDb.OnSqlServer(shape, key, typeKit, database, schema)) + { + generated = KeyShapes.KeyOf(db, shape, key) is [{ ValueGenerated: ValueGenerated.OnAdd }]; + object Value(int index) => typeKit ? kind.Wrap(kind.Values[index]) : kind.Values[index]; + (saved, numbered) = Save(db, shape, key, Value); + } + + using var read = KeyShapeDb.OnSqlServer(shape, key, typeKit, database, schema); + return new Keys(saved, Read(read, shape, key), generated, numbered, null); + } + catch (Exception exception) when (exception is not ShouldAssertException) + { + return new Keys([], [], false, false, $"{exception.GetType().Name}: {exception.GetBaseException().Message}"); + } + } + + /// The shape's entities saved, each key left to the model's generation or assigned, and whether any was left. + private static (string[] Saved, bool Numbered) Save(KeyShapeDb db, KeyShape shape, Type key, Func value) + { + var numbered = false; + Type Closed(Type definition) => definition.MakeGenericType(key); + object New(Type type, int index) + { + var entity = Activator.CreateInstance(type)!; + if (db.Model.FindEntityType(type)!.FindPrimaryKey()!.Properties is [{ ValueGenerated: ValueGenerated.OnAdd }]) numbered = true; + else type.GetProperty("Id")!.SetValue(entity, value(index)); + return entity; + } + + object[] entities = shape switch + { + KeyShape.TphRoot or KeyShape.TptRoot or KeyShape.TpcRoot => [New(Closed(typeof(Root<>)), 0), New(Closed(typeof(Leaf<>)), 1)], + KeyShape.AbstractTphRoot or KeyShape.AbstractTptRoot or KeyShape.AbstractTpcRoot => [New(Closed(typeof(LeafA<>)), 0), New(Closed(typeof(LeafB<>)), 1)], + KeyShape.OwnedDependent => [New(Closed(typeof(Owner<>)), 0), New(Closed(typeof(Owner<>)), 1)], + KeyShape.SharedKeyDependent or KeyShape.TableSplitDependent => [New(Closed(typeof(Principal<>)), 0), New(Closed(typeof(Principal<>)), 1)], + KeyShape.Composite => [Composite(key, value(0), value(1)), Composite(key, value(1), value(0))], + _ => [New(Closed(typeof(Alone<>)), 0), New(Closed(typeof(Alone<>)), 1)], + }; + db.AddRange(entities); + db.SaveChanges(); + + // A dependent's key is its principal's, so the dependents follow the principals they share it with. + if (shape is KeyShape.SharedKeyDependent or KeyShape.TableSplitDependent) + { + object[] dependents = [.. entities.Select(principal => + { + var dependent = Activator.CreateInstance(Closed(typeof(Dependent<>)))!; + dependent.GetType().GetProperty("Id")!.SetValue(dependent, principal.GetType().GetProperty("Id")!.GetValue(principal)); + return dependent; + })]; + db.AddRange(dependents); + db.SaveChanges(); + entities = dependents; + } + + return ([.. entities.Select(Text)], numbered); + } + + private static object Composite(Type key, object id, object line) + { + var entity = Activator.CreateInstance(typeof(Composite<>).MakeGenericType(key))!; + entity.GetType().GetProperty("Id")!.SetValue(entity, id); + entity.GetType().GetProperty("Line")!.SetValue(entity, line); + return entity; + } + + /// The keys of the entities the shape is about, as a fresh context reads them. + private static string[] Read(KeyShapeDb db, KeyShape shape, Type key) + { + var type = shape switch + { + KeyShape.OwnedDependent => typeof(Owner<>).MakeGenericType(key), + _ => KeyShapes.Subject(shape, key), + }; + var set = (IQueryable)typeof(DbContext).GetMethod(nameof(DbContext.Set), Type.EmptyTypes)!.MakeGenericMethod(type).Invoke(db, null)!; + return [.. set.Cast().AsEnumerable().Select(Text)]; + } + + /// An entity's key as text, both properties of a composite one. + private static string Text(object entity) => + entity.GetType().GetProperty("Line") is { } line + ? $"{WrappedKind.Text(entity.GetType().GetProperty("Id")!.GetValue(entity))}/{WrappedKind.Text(line.GetValue(entity))}" + : WrappedKind.Text(entity.GetType().GetProperty("Id")!.GetValue(entity)); + + private static bool IsInteger(Type type) => + type == typeof(int) || type == typeof(long) || type == typeof(short) || type == typeof(sbyte) || type == typeof(byte) + || type == typeof(uint) || type == typeof(ulong) || type == typeof(ushort); +} diff --git a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServerQueryVariantTests.cs b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServerQueryVariantTests.cs new file mode 100644 index 00000000..aafe945f --- /dev/null +++ b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServerQueryVariantTests.cs @@ -0,0 +1,197 @@ +using System.Collections.Concurrent; +using System.Globalization; +using CodoMetis.TypeKit.EntityFrameworkCore.Tests.Variants; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; +using static CodoMetis.TypeKit.EntityFrameworkCore.Tests.QueryTranslationVariantTests; + +namespace CodoMetis.TypeKit.EntityFrameworkCore.Tests; + +/// +/// The query-translation catalogue () run on a real SQL Server: each query over a value +/// object against the same query over the wrapped type there, as runs +/// it on SQLite, and then against the same query over the objects in memory. +/// +/// +/// An answer that differs from memory's is SQL Server's own only where SQLite answers as memory does: a null +/// .Value that throws in memory and is a NULL in SQL, or a query EF translates for neither, is the relational +/// providers' alike. Each difference that is SQL Server's alone is a fact of design/knowledge, named in +/// ; any other fails. +/// +[Collection(SqlServerCollection.Name)] +[Trait("Needs", "Docker")] +public sealed class SqlServerQueryVariantTests(SqlServer server) +{ + public static TheoryData Cases => QueryTranslationVariantTests.Cases; + + [Theory] + [MemberData(nameof(Cases))] + public async Task On_SQL_Server_a_query_over_a_value_object_answers_as_the_same_query_over_the_wrapped_type_and_as_memory( + string kind, Conversion conversion, Site site, Accessor accessor, Op op) + { + var @case = QueryShapes.Find(new QueryCase(WrappedKinds.Named(kind), conversion, site, accessor, op).Id); + var expectation = Expect(@case); + if (expectation is Unclaimed { SqliteOnly: false } unclaimed) Assert.Skip(unclaimed.Reason); + if (expectation is OpenFinding finding) Assert.Skip($"open finding: {finding.Reason}"); + if (expectation is Unclaimed) expectation = new Same(); + + var database = await server.DatabaseAsync("typekit_queries"); + AnswersAsTheWrappedType(@case, expectation, (c, valueObject) => Run(c, valueObject, database), out var answer, out _, + differs: (c, onSqlServer, memory) => Known(c, onSqlServer, memory) is not null); + if (expectation is not (Same or NullsLeftOut)) return; + + // EF.Functions.Greatest and Least, which EF translates on SQL Server from compatibility level 160 alone (KB-053): + // there the SQL is the wrapped type's, translated without a server. + if (@case.Op is Op.Greatest or Op.Least && answer.Refusal is not null) + { + // Over a primitive collection's element EF refuses them on SQLite too, and a TimeSpan table is refused its rows (KB-051). + if (Refusal(answer) is not DbUpdateException) + answer.Refusal!.Message.ShouldMatch("is not supported because the query has switched to client-evaluation|could not be translated"); + AtLevel160(@case, database); + return; + } + + var memory = Memory(@case); + if (Agree(answer, memory)) return; + + var sqlite = QueryTranslationVariantTests.Run(@case, valueObject: true); + if (Agree(answer, sqlite)) return; + + // r.N!.Value.Value throws in memory on the row without one, where SQL reads a NULL, as ValueOrNull() does. + if (memory.Refusal is not null && @case is { Site: Site.Nullable, Accessor: Accessor.ValueThroughTypeParameter } + && QueryShapes.All().FirstOrDefault(other => other == @case with { Accessor = Accessor.ValueOrNull }) is { } orNull) + { + answer.Rows.ShouldBe(Run(orNull, valueObject: true, database).Rows, $"the same query through ValueOrNull(), where memory throws: {memory.Refusal.Message}"); + return; + } + + if (Known(@case, answer, memory) is null) + throw new ShouldAssertException($"SQL Server answers {Shown(answer)}, where memory answers {Shown(memory)} and SQLite {Shown(sqlite)}"); + } + + /// + /// A guard against a vacuous run: the catalogue holds the cases each fact is about, and the facts that a case's + /// answer decides (a decimal's scale, a refused aggregate) excuse no other answer. + /// + [Fact] + public void Every_known_difference_is_one_the_catalogue_reaches() + { + Answer Rows(params string[] rows) => new(rows, null, null); + + QueryShapes.All().Select(@case => Known(@case, Rows("0"), Rows("1"))).OfType().Distinct().ShouldBe([TextCollation, GuidOrder, TimeUnderADay], ignoreOrder: true); + var decimals = QueryShapes.All().Where(@case => @case.Kind == WrappedKinds.Decimal).ToList(); + decimals.Any(@case => Known(@case, Rows("10.00"), Rows("10")) == DecimalScale).ShouldBeTrue("a decimal read back at another scale"); + decimals.All(@case => Known(@case, Rows("10.01"), Rows("10")) is null).ShouldBeTrue("a decimal read back as another number is no difference of scale"); + var refused = new Answer(null, null, new InvalidOperationException("Operand data type bit is invalid for max operator.")); + QueryShapes.All().Where(@case => @case.Kind == WrappedKinds.Boolean).Any(@case => Known(@case, refused, Rows("1")) == BitAggregate).ShouldBeTrue("a refused MAX of a bit"); + } + + private const string TextCollation = "KB-048: SQL Server compares and orders text by the database's collation, case-insensitive by default"; + + private const string GuidOrder = "KB-049: SQL Server orders a uniqueidentifier by its last group first"; + + private const string DecimalScale = "KB-050: a decimal reads back at its column's scale, decimal(18,2) unless configured"; + + private const string TimeUnderADay = "KB-051: SQL Server's time holds less than a day, so a TimeSpan of a day or more is refused in a column and misread in JSON"; + + private const string BitAggregate = "KB-052: SQL Server takes neither MAX nor MIN of a bit"; + + /// + /// The differences from memory that are SQL Server's own, each pinned in design/knowledge with a test of plain + /// EF Core there (test/CodoMetis.TypeKit.Facts); null for any other. + /// + private static string? Known(QueryCase @case, Answer answer, Answer memory) + { + var wrapped = @case.Kind.Wrapped; + if ((wrapped == typeof(string) || wrapped == typeof(char)) && @case.Orders && answer.Refusal is null) return TextCollation; + if (wrapped == typeof(Guid) && @case.Orders && answer.Refusal is null) return GuidOrder; + if (wrapped == typeof(decimal) && answer.Rows is { } rows && memory.Rows is { } expected && rows.Length == expected.Length + && rows.Zip(expected).All(pair => pair.First == pair.Second || decimal.TryParse(pair.First, CultureInfo.InvariantCulture, out var read) && read == decimal.Parse(pair.Second, CultureInfo.InvariantCulture))) + return DecimalScale; + if (wrapped == typeof(TimeSpan) && (answer.Refusal is DbUpdateException || @case.Site is Site.JsonProperty or Site.CollectionElement)) return TimeUnderADay; + if (wrapped == typeof(bool) && answer.Refusal?.Message.Contains("Operand data type bit is invalid", StringComparison.Ordinal) == true) return BitAggregate; + return null; + } + + /// The SQL of the case at compatibility level 160, over the value object and over the wrapped type, written without a server. + private static void AtLevel160(QueryCase @case, string database) + { + Answer Translated(bool valueObject) + { + var builder = new QueryBuilder(@case, valueObject); + using var db = QueryDb.OnSqlServer(new QueryModel($"{@case.Id} | {valueObject} | 160", @case, valueObject), database, compatibilityLevel: 160); + try + { + var set = (IQueryable)typeof(DbContext).GetMethod(nameof(DbContext.Set), Type.EmptyTypes)!.MakeGenericMethod(builder.Source).Invoke(db, null)!; + return new Answer(null, set.Provider.CreateQuery(builder.Build(set.Expression).Query).ToQueryString(), null); + } + catch (Exception exception) when (exception is not ShouldAssertException) + { + return new Answer(null, null, Unwrapped(exception)); + } + } + + if (!new QueryBuilder(@case, valueObject: false).CanBuild || Translated(valueObject: false) is not { Refusal: null } plain) return; + + var translated = Translated(valueObject: true); + translated.Refusal.ShouldBeNull("refused at compatibility level 160, where the same query over the wrapped type translates"); + translated.Sql.ShouldBe(plain.Sql, "the SQL of the same query over the wrapped type at compatibility level 160"); + } + + private static Exception Refusal(Answer answer) => answer.Refusal ?? throw new ShouldAssertException($"answered {Shown(answer)}"); + + /// The same rows, or both refused. + private static bool Agree(Answer left, Answer right) => + left.Refusal is not null && right.Refusal is not null || left.Rows is { } rows && right.Rows is { } other && rows.SequenceEqual(other); + + /// The schema each model was seeded into, all in one database, seeded once and only read. + private static readonly ConcurrentDictionary> Schemas = new(); + + private static int _schemas; + + private static Answer Run(QueryCase @case, bool valueObject, string database) + { + var builder = new QueryBuilder(@case, valueObject); + var key = @case.IsFilter + ? $"{@case.Id} | {valueObject}" + : $"{@case.Kind.Name} | {(valueObject ? @case.Conversion : Conversion.TypeKit)} | {builder.Source.Name} | {valueObject}"; + var (model, refusal) = Schemas.GetOrAdd(key, _ => new Lazy<(QueryModel, Exception?)>(() => + { + var seeded = new QueryModel(key, @case, valueObject, $"m{Interlocked.Increment(ref _schemas)}"); + return (seeded, Seeded(seeded, builder, database)); + })).Value; + if (refusal is not null) return new Answer(null, null, refusal); + + using var db = QueryDb.OnSqlServer(model, database); + try + { + var set = (IQueryable)typeof(DbContext).GetMethod(nameof(DbContext.Set), Type.EmptyTypes)!.MakeGenericMethod(builder.Source).Invoke(db, null)!; + var (query, ordered) = builder.Build(set.Expression); + var answer = Execute(set.Provider, query, ordered, sql: true); + + // Each model has a schema of its own; the SQL compared is the rest. + return answer with { Sql = answer.Sql?.Replace($"[{model.Schema}].", "", StringComparison.Ordinal) }; + } + catch (Exception exception) when (exception is not ShouldAssertException) + { + return new Answer(null, null, Unwrapped(exception)); + } + } + + private static Exception? Seeded(QueryModel model, QueryBuilder builder, string database) + { + try + { + using var db = QueryDb.OnSqlServer(model, database); + db.GetService().CreateTables(); + db.AddRange(Seed.Stored(builder, model.Case.Kind, model.ValueObject)); + db.SaveChanges(); + return null; + } + catch (Exception exception) when (exception is not ShouldAssertException) + { + return Unwrapped(exception); + } + } +} diff --git a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServerRoundTripTests.cs b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServerRoundTripTests.cs new file mode 100644 index 00000000..52cd744f --- /dev/null +++ b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/SqlServerRoundTripTests.cs @@ -0,0 +1,176 @@ +using System.Globalization; +using CodoMetis.TypeKit.Generators.Probes; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace CodoMetis.TypeKit.EntityFrameworkCore.Tests; + +/// +/// A real SQL Server, as is a real PostgreSQL: every wrapped-type family survives +/// a round trip, stored values are read back without the rules, the translated queries return the rows they should, +/// and what SQL Server does differently from memory (design/knowledge KB-048 to KB-054) a value object does as the +/// type it wraps does. +/// +[Collection(SqlServerCollection.Name)] +[Trait("Needs", "Docker")] +public sealed class SqlServerRoundTripTests(SqlServer server) +{ + private async Task CreateAsync() + { + var options = new DbContextOptionsBuilder().UseSqlServer(await server.DatabaseAsync("typekit_round_trips")).UseTypeKit() + .ConfigureWarnings(warnings => warnings.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning)) + .Options; + var db = new TestDb(options); + await db.Database.EnsureCreatedAsync(TestContext.Current.CancellationToken); + return db; + } + + private async Task<(Customer Customer, Order Order)> SeedAsync(Action? adjust = null) + { + var customer = new Customer { Id = CustomerId.New(), Name = $"customer {Guid.NewGuid():N}" }; + var order = Order.Sample(customer.Id); + adjust?.Invoke(order); + + await using var db = await CreateAsync(); + db.AddRange(customer, order); + await db.SaveChangesAsync(TestContext.Current.CancellationToken); + + return (customer, order); + } + + [Fact] + public async Task Every_value_object_round_trips_and_the_navigation_loads() + { + var (customer, saved) = await SeedAsync(); + + await using var db = await CreateAsync(); + var loaded = await db.Orders.Include(o => o.Customer).SingleAsync(o => o.Id == saved.Id, TestContext.Current.CancellationToken); + + loaded.CustomerId.ShouldBe(customer.Id); + loaded.Customer!.Name.ShouldBe(customer.Name); + loaded.Code.ShouldBe(saved.Code); + loaded.Discount.ShouldBe(saved.Discount); + loaded.Quantity.ShouldBe(saved.Quantity); + loaded.Amount.ShouldBe(saved.Amount); + loaded.Urgent.ShouldBe(saved.Urgent); + loaded.PlacedAt.ShouldBe(saved.PlacedAt); + loaded.DueOn.ShouldBe(saved.DueOn); + loaded.ConfirmedAt.ShouldBe(saved.ConfirmedAt); + loaded.Slot.ShouldBe(saved.Slot); + loaded.Note.ShouldBe(saved.Note); + loaded.Tags.ShouldBe(saved.Tags); + + // KB-054: datetime2 keeps the ticks, and reads a DateTime back without its kind, as a plain DateTime column does. + loaded.PlacedAt.Value.Ticks.ShouldBe(saved.PlacedAt.Value.Ticks); + loaded.PlacedAt.Value.Kind.ShouldBe(DateTimeKind.Unspecified); + } + + [Fact] + public async Task An_absent_optional_value_object_round_trips_as_null() + { + var (_, saved) = await SeedAsync(order => { order.Discount = null; order.Note = null; }); + + await using var db = await CreateAsync(); + var loaded = await db.Orders.SingleAsync(o => o.Id == saved.Id, TestContext.Current.CancellationToken); + + loaded.Discount.ShouldBeNull(); + loaded.Note.ShouldBeNull(); + } + + /// A rule added after a row was written must not make it unreadable: the column is read through the materializer. + [Fact] + public async Task A_stored_value_the_rules_now_refuse_is_read_back_as_stored() + { + var (_, saved) = await SeedAsync(); + + await using (var db = await CreateAsync()) + await db.Database.ExecuteSqlAsync($"UPDATE [Orders] SET [Code] = 'lower', [Discount] = 101 WHERE [Id] = {saved.Id.Value}", TestContext.Current.CancellationToken); + + await using var read = await CreateAsync(); + var loaded = await read.Orders.AsNoTracking().SingleAsync(o => o.Id == saved.Id, TestContext.Current.CancellationToken); + + loaded.Code.Value.ShouldBe("lower"); + loaded.Discount!.Value.Value.ShouldBe(101); + loaded.Code.Revalidate().TryGetValue(out _, out var fault).ShouldBeFalse(); + fault.ShouldBe(ProbeCodeFault.NotUpperCase); + } + + [Fact] + public async Task The_translated_queries_find_the_row() + { + var (_, saved) = await SeedAsync(order => order.Code = ProbeCode.FromKnownGood("QUERY")); + List ids = [saved.Id]; + var tag = saved.Tags[0]; + var cancellation = TestContext.Current.CancellationToken; + + await using var db = await CreateAsync(); + + (await db.Orders.CountAsync(o => ids.Contains(o.Id), cancellation)).ShouldBe(1); + (await db.Orders.CountAsync(o => o.Id == saved.Id && o.Code.Value.StartsWith("QUE"), cancellation)).ShouldBe(1); + (await db.Orders.CountAsync(o => o.Id == saved.Id && o.Discount.ValueOrNull() > 10, cancellation)).ShouldBe(1); + (await db.Orders.CountAsync(o => o.Id == saved.Id && o.Tags.Contains(tag), cancellation)).ShouldBe(1); + (await db.Orders.CountAsync(o => o.Id == saved.Id && o.Tags.Any(t => t.Value == "x"), cancellation)).ShouldBe(1); + (await db.Orders.CountAsync(o => ids.Any(id => id.Value == o.Id.Value), cancellation)).ShouldBe(1); + } + + /// A key over an integer is an identity column, as an int key is, and SQL Server numbers it. + [Fact] + public async Task An_integer_key_is_an_identity_column() + { + await using var db = await CreateAsync(); + + SqlServerPropertyExtensions.GetValueGenerationStrategy(db.Model.FindEntityType(typeof(Shipment))!.FindProperty(nameof(Shipment.Id))!) + .ShouldBe(SqlServerValueGenerationStrategy.IdentityColumn); + + var first = new Shipment { Carrier = "a" }; + var second = new Shipment { Carrier = "b" }; + db.AddRange(first, second); + await db.SaveChangesAsync(TestContext.Current.CancellationToken); + + first.Id.Value.ShouldBeGreaterThan(0); + second.Id.ShouldNotBe(first.Id); + } + + /// + /// KB-048: under the default collation a value object over text is compared and matched ignoring case and trailing + /// spaces, as the plain string column beside it is, where in memory its equality is ordinal. + /// + [Fact] + public async Task Text_is_compared_by_the_collation_as_a_plain_string_column_is() + { + var marker = Guid.NewGuid().ToString("N")[..8]; + var (customer, saved) = await SeedAsync(order => { order.Note = ProbeLabel.From($"Fragile {marker}"); order.Tags = [Tag.From($"Needle {marker}")]; }); + var cancellation = TestContext.Current.CancellationToken; + var shouted = ProbeLabel.From($"FRAGILE {marker}"); + List notes = [shouted]; + + shouted.ShouldNotBe(saved.Note, "in memory, a value object over text is equal ordinally"); + + await using var db = await CreateAsync(); + var mine = db.Orders.Where(o => o.Id == saved.Id); + + (await mine.CountAsync(o => o.Note == shouted, cancellation)).ShouldBe(1); + (await mine.CountAsync(o => o.Note == ProbeLabel.From($"Fragile {marker} "), cancellation)).ShouldBe(1, "trailing spaces"); + (await mine.CountAsync(o => notes.Contains(o.Note!), cancellation)).ShouldBe(1); + (await mine.CountAsync(o => o.Code.Value.StartsWith("ab"), cancellation)).ShouldBe(1); + (await mine.CountAsync(o => o.Tags.Any(t => t.Value == $"needle {marker}"), cancellation)).ShouldBe(1); + (await db.Customers.CountAsync(c => c.Id == customer.Id && c.Name == customer.Name.ToUpperInvariant(), cancellation)).ShouldBe(1, "the plain string column"); + } + + /// + /// KB-050: a decimal value object without a configured precision is a decimal(18,2) column, as a plain + /// decimal is: more fractional digits are rounded on write, and the value reads back at scale 2. + /// + [Fact] + public async Task A_decimal_value_object_is_rounded_to_the_column_s_scale_as_a_plain_decimal_is() + { + var (_, saved) = await SeedAsync(order => order.Amount = ProbeAmount.From(12.505m)); + + await using var db = await CreateAsync(); + db.Model.FindEntityType(typeof(Order))!.FindProperty(nameof(Order.Amount))!.GetColumnType().ShouldBe("decimal(18,2)"); + var loaded = await db.Orders.AsNoTracking().SingleAsync(o => o.Id == saved.Id, TestContext.Current.CancellationToken); + + loaded.Amount.Value.ToString(CultureInfo.InvariantCulture).ShouldBe("12.51"); + } +} diff --git a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/Variants/KeyShapes.cs b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/Variants/KeyShapes.cs index 61135a9a..aa781900 100644 --- a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/Variants/KeyShapes.cs +++ b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/Variants/KeyShapes.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -118,14 +119,30 @@ public static IReadOnlyList KeyOf(DbContext db, KeyShape shape, Type db.GetService().Model.FindEntityType(Subject(shape, key))!.FindPrimaryKey()!.Properties; } -/// A model of one key shape, cached per shape, key type and side. -public sealed class KeyShapeDb(DbContextOptions options, KeyShape shape, Type key) : DbContext(options) +/// A model of one key shape, cached per shape, key type and side, in a schema of its own where many share one database. +public sealed class KeyShapeDb(DbContextOptions options, KeyShape shape, Type key, string? schema = null) : DbContext(options) { public KeyShape Shape => shape; public Type Key => key; - protected override void OnModelCreating(ModelBuilder modelBuilder) => KeyShapes.Configure(modelBuilder, shape, key); + public string? Schema => schema; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + if (schema is not null) modelBuilder.HasDefaultSchema(schema); + KeyShapes.Configure(modelBuilder, shape, key); + } + + /// On a real SQL Server, the shape's tables in . + public static KeyShapeDb OnSqlServer(KeyShape shape, Type key, bool typeKit, string connectionString, string schema) + { + var builder = new DbContextOptionsBuilder().ReplaceService().UseSqlServer(connectionString) + .ConfigureWarnings(warnings => warnings.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning)); + if (typeKit) builder = builder.UseTypeKit(); + + return new KeyShapeDb(builder.Options, shape, key, schema); + } public static KeyShapeDb Create(string provider, KeyShape shape, Type key, bool typeKit, Microsoft.Data.Sqlite.SqliteConnection connection) { @@ -146,7 +163,7 @@ public static KeyShapeDb Create(string provider, KeyShape shape, Type key, bool public sealed class KeyShapeModelCacheKey : IModelCacheKeyFactory { public object Create(DbContext context, bool designTime) => - context is KeyShapeDb db ? (db.GetType(), db.Shape, db.Key, designTime) : (context.GetType(), designTime); + context is KeyShapeDb db ? (db.GetType(), db.Shape, db.Key, db.Schema, designTime) : (context.GetType(), designTime); } public class Alone diff --git a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/Variants/QueryShapes.cs b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/Variants/QueryShapes.cs index a99b6e6f..55c4fdd1 100644 --- a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/Variants/QueryShapes.cs +++ b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/Variants/QueryShapes.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; using CodoMetis.TypeKit.ValueObjects; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -657,8 +658,11 @@ private static List Rows(QueryBuilder builder, WrappedKind kind, bool va private static void Set(object target, string property, object? value) => target.GetType().GetProperty(property)!.SetValue(target, value); } -/// A model of one kind's entities for the catalogue, cached by . -public sealed record QueryModel(string Key, QueryCase Case, bool ValueObject); +/// +/// A model of one kind's entities for the catalogue, cached by , in a schema of its own where +/// many models share one database. +/// +public sealed record QueryModel(string Key, QueryCase Case, bool ValueObject, string? Schema = null); public sealed class QueryDb(DbContextOptions options, QueryModel model) : DbContext(options) { @@ -673,6 +677,7 @@ protected override void ConfigureConventions(ModelConfigurationBuilder configura protected override void OnModelCreating(ModelBuilder modelBuilder) { var builder = new QueryBuilder(model.Case, model.ValueObject); + if (model.Schema is { } schema) modelBuilder.HasDefaultSchema(schema); if (builder.Source == builder.ElementRow) { @@ -725,6 +730,16 @@ public static QueryDb Create(QueryModel model, Microsoft.Data.Sqlite.SqliteConne public static QueryDb OnPostgreSql(QueryModel model) => Create(model, new DbContextOptionsBuilder().UseNpgsql("Host=localhost;Database=none")); + /// + /// On SQL Server, at EF's default compatibility level or at . The suite's + /// models need more than the twenty internal service providers EF warns about, all in one process. + /// + public static QueryDb OnSqlServer(QueryModel model, string connectionString, int? compatibilityLevel = null) => + Create(model, new DbContextOptionsBuilder().UseSqlServer(connectionString, sql => + { + if (compatibilityLevel is { } level) sql.UseCompatibilityLevel(level); + }).ConfigureWarnings(warnings => warnings.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning))); + private static QueryDb Create(QueryModel model, DbContextOptionsBuilder provider) { var builder = provider.ReplaceService(); diff --git a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/Variants/WrappedKinds.cs b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/Variants/WrappedKinds.cs index 63583190..50795eaa 100644 --- a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/Variants/WrappedKinds.cs +++ b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/Variants/WrappedKinds.cs @@ -20,7 +20,10 @@ public enum Ordering /// /// The values are chosen so that a column read as text orders differently from the type (10 /// before 2.25, 1.02:00:00 before 10:00:00): a converter that stores another -/// representation, or a provider that orders the stored text, cannot answer like memory by accident. +/// representation, or a provider that orders the stored text, cannot answer like memory by accident. Text and +/// char mix case ("Bravo" before "alpha" ordinally), and the Guids ascend by their first group +/// and not by their last, so a database that orders text ignoring case, or a uniqueidentifier by its last +/// group (SQL Server), cannot either. /// public sealed record WrappedKind(string Name, Type ValueObject, Type Wrapped, Ordering Ordering, object[] Values) { @@ -83,11 +86,11 @@ public static class WrappedKinds public static readonly WrappedKind UInt16 = new("ushort", typeof(WrapsUInt16), typeof(ushort), Ordering.Wrapped, [(ushort)2, (ushort)7, (ushort)10]); public static readonly WrappedKind UInt32 = new("uint", typeof(WrapsUInt32), typeof(uint), Ordering.Wrapped, [2u, 7u, 10u]); public static readonly WrappedKind UInt64 = new("ulong", typeof(WrapsUInt64), typeof(ulong), Ordering.Wrapped, [2ul, 7ul, 10ul]); - public static readonly WrappedKind Char = new("char", typeof(WrapsChar), typeof(char), Ordering.Wrapped, ['a', 'b', 'c']); + public static readonly WrappedKind Char = new("char", typeof(WrapsChar), typeof(char), Ordering.Wrapped, ['B', 'a', 'c']); public static readonly WrappedKind Guid = new("Guid", typeof(WrapsGuid), typeof(Guid), Ordering.Wrapped, - [System.Guid.Parse("00000000-0000-0000-0000-000000000002"), System.Guid.Parse("00000000-0000-0000-0000-000000000007"), System.Guid.Parse("00000000-0000-0000-0000-000000000010")]); - public static readonly WrappedKind String = new("string", typeof(WrapsString), typeof(string), Ordering.Wrapped, ["alpha", "bravo", "charlie"]); - public static readonly WrappedKind StringRecordClass = new("string, record class", typeof(WrapsStringRecord), typeof(string), Ordering.Wrapped, ["alpha", "bravo", "charlie"]); + [System.Guid.Parse("00000001-0000-0000-0000-000000000010"), System.Guid.Parse("00000002-0000-0000-0000-000000000002"), System.Guid.Parse("00000003-0000-0000-0000-000000000007")]); + public static readonly WrappedKind String = new("string", typeof(WrapsString), typeof(string), Ordering.Wrapped, ["Bravo", "alpha", "charlie"]); + public static readonly WrappedKind StringRecordClass = new("string, record class", typeof(WrapsStringRecord), typeof(string), Ordering.Wrapped, ["Bravo", "alpha", "charlie"]); public static readonly WrappedKind Int32RecordClass = new("int, record class", typeof(WrapsInt32Record), typeof(int), Ordering.Wrapped, [2, 7, 10]); public static readonly WrappedKind Decimal = new("decimal", typeof(WrapsDecimal), typeof(decimal), Ordering.Wrapped, [1.5m, 2.25m, 10m]); public static readonly WrappedKind Double = new("double", typeof(WrapsDouble), typeof(double), Ordering.Wrapped, [1.5, 2.25, 10.0]); diff --git a/test/CodoMetis.TypeKit.Facts/CodoMetis.TypeKit.Facts.csproj b/test/CodoMetis.TypeKit.Facts/CodoMetis.TypeKit.Facts.csproj index 20de5a52..e725ba98 100644 --- a/test/CodoMetis.TypeKit.Facts/CodoMetis.TypeKit.Facts.csproj +++ b/test/CodoMetis.TypeKit.Facts/CodoMetis.TypeKit.Facts.csproj @@ -25,6 +25,13 @@ + + + + diff --git a/test/CodoMetis.TypeKit.Facts/KB047.cs b/test/CodoMetis.TypeKit.Facts/KB047.cs new file mode 100644 index 00000000..245d66d1 --- /dev/null +++ b/test/CodoMetis.TypeKit.Facts/KB047.cs @@ -0,0 +1,109 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Microsoft.EntityFrameworkCore.ValueGeneration; + +namespace CodoMetis.TypeKit.Facts; + +/// +/// design/knowledge/KB-047: EF Core gives a key generated on add whose type is no number, converted to a +/// column, a negative temporary , which a conversion to refuses. A value generator +/// factory in the converter's mapping hints is consulted instead, after a generator configured on the property and after +/// Npgsql's HiLo. No database is opened. +/// +public sealed class KB047 +{ + public readonly record struct Code(ulong Value); + + public readonly record struct Number(long Value); + + public sealed class ByCode + { + public Code Id { get; set; } + } + + public sealed class ByNumber + { + public Number Id { get; set; } + } + + private static readonly Code Hinted = new(7); + + /// A stored as a , as SQL Server and PostgreSQL store a . + private static ValueConverter CodeConverter(bool hinted) => + new(code => (decimal)code.Value, value => new Code((ulong)value), hinted ? new ConverterMappingHints(valueGeneratorFactory: (_, _) => new Fixed(Hinted)) : null); + + private sealed class Fixed(T value) : ValueGenerator + { + public override bool GeneratesTemporaryValues => true; + + public override T Next(EntityEntry entry) => value; + } + + private sealed class Own : ValueGenerator + { + public override bool GeneratesTemporaryValues => false; + + public override Code Next(EntityEntry entry) => new(42); + } + + private sealed class Db(DbContextOptions options, Action configure) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) => configure(modelBuilder); + } + + private sealed class Uncached : IModelCacheKeyFactory + { + private static int _next; + + public object Create(DbContext context, bool designTime) => Interlocked.Increment(ref _next); + } + + private static Db Create(Action configure) => + new(new DbContextOptionsBuilder().UseNpgsql("Host=localhost;Database=none").ReplaceService().Options, configure); + + [Fact] + public void Without_hints_the_temporary_decimal_is_refused_converted_back() + { + using var db = Create(model => model.Entity().Property(e => e.Id).HasConversion(CodeConverter(hinted: false)).ValueGeneratedOnAdd()); + + Should.Throw(() => db.Add(new ByCode())); + } + + [Fact] + public void The_hints_factory_fills_the_key() + { + using var db = Create(model => model.Entity().Property(e => e.Id).HasConversion(CodeConverter(hinted: true)).ValueGeneratedOnAdd()); + + var entity = new ByCode(); + db.Add(entity); + + db.Entry(entity).Property(e => e.Id).CurrentValue.ShouldBe(Hinted); + } + + [Fact] + public void A_generator_on_the_property_comes_before_the_hints_factory() + { + using var db = Create(model => model.Entity().Property(e => e.Id).HasConversion(CodeConverter(hinted: true)).HasValueGenerator()); + + var entity = new ByCode(); + db.Add(entity); + + entity.Id.ShouldBe(new Code(42)); + } + + [Fact] + public void Npgsql_s_HiLo_comes_before_the_hints_factory() + { + var converter = new ValueConverter( + number => number.Value, value => new Number(value), new ConverterMappingHints(valueGeneratorFactory: (_, _) => new Fixed(new Number(7)))); + using var db = Create(model => NpgsqlPropertyBuilderExtensions.UseHiLo(model.Entity().Property(e => e.Id).HasConversion(converter))); + + var entityType = db.Model.FindEntityType(typeof(ByNumber))!; + db.GetService().TrySelect(entityType.FindPrimaryKey()!.Properties[0], entityType, out var generator).ShouldBeTrue(); + + generator.ShouldNotBeOfType>(); + generator!.GeneratesTemporaryValues.ShouldBeFalse(); + } +} diff --git a/test/CodoMetis.TypeKit.Facts/KB048.cs b/test/CodoMetis.TypeKit.Facts/KB048.cs new file mode 100644 index 00000000..69efd812 --- /dev/null +++ b/test/CodoMetis.TypeKit.Facts/KB048.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore; + +namespace CodoMetis.TypeKit.Facts; + +/// +/// design/knowledge/KB-048: under SQL Server's default collation, SQL_Latin1_General_CP1_CI_AS, text compares +/// ignoring case and trailing spaces: ==, Contains over a list (IN) and StartsWith +/// (LIKE) match rows that differ in case, and ORDER BY sorts ignoring case. A binary collation +/// (Latin1_General_BIN2) gives the ordinal order. +/// +[Trait("Needs", "Docker")] +public sealed class KB048 +{ + public sealed class Named + { + public int Id { get; set; } + + public string Name { get; set; } = ""; + } + + [Fact] + public async Task Text_compares_and_orders_ignoring_case() + { + var cancellation = TestContext.Current.CancellationToken; + await using var db = await SqlServer.ContextAsync(model => model.Entity()); + db.AddRange(new Named { Name = "alpha" }, new Named { Name = "Bravo" }, new Named { Name = "charlie" }); + await db.SaveChangesAsync(cancellation); + List upper = ["ALPHA"]; + + (await db.Set().Where(n => n.Name == "BRAVO").Select(n => n.Name).ToListAsync(cancellation)).ShouldBe(["Bravo"]); + (await db.Set().Where(n => n.Name == "alpha ").Select(n => n.Name).ToListAsync(cancellation)).ShouldBe(["alpha"]); + (await db.Set().Where(n => upper.Contains(n.Name)).Select(n => n.Name).ToListAsync(cancellation)).ShouldBe(["alpha"]); + (await db.Set().Where(n => n.Name.StartsWith("b")).Select(n => n.Name).ToListAsync(cancellation)).ShouldBe(["Bravo"]); + (await db.Set().OrderBy(n => n.Name).Select(n => n.Name).ToListAsync(cancellation)).ShouldBe(["alpha", "Bravo", "charlie"]); + (await db.Set().OrderBy(n => EF.Functions.Collate(n.Name, "Latin1_General_BIN2")).Select(n => n.Name).ToListAsync(cancellation)) + .ShouldBe(["Bravo", "alpha", "charlie"]); + } +} diff --git a/test/CodoMetis.TypeKit.Facts/KB049.cs b/test/CodoMetis.TypeKit.Facts/KB049.cs new file mode 100644 index 00000000..9148e0db --- /dev/null +++ b/test/CodoMetis.TypeKit.Facts/KB049.cs @@ -0,0 +1,34 @@ +using Microsoft.EntityFrameworkCore; + +namespace CodoMetis.TypeKit.Facts; + +/// +/// design/knowledge/KB-049: SQL Server orders a uniqueidentifier by its last group first (bytes 10 to 15), then +/// the fourth, third, second and first, where starts at the first group. So +/// ORDER BY, < and MIN over Guids answer in another order than memory does. +/// +[Trait("Needs", "Docker")] +public sealed class KB049 +{ + public sealed class Keyed + { + public int Id { get; set; } + + public Guid Key { get; set; } + } + + [Fact] + public async Task A_uniqueidentifier_orders_by_its_last_group_first() + { + var cancellation = TestContext.Current.CancellationToken; + Guid first = Guid.Parse("00000001-0000-0000-0000-000000000003"), second = Guid.Parse("00000002-0000-0000-0000-000000000001"), third = Guid.Parse("00000003-0000-0000-0000-000000000002"); + await using var db = await SqlServer.ContextAsync(model => model.Entity()); + db.AddRange(new Keyed { Key = first }, new Keyed { Key = second }, new Keyed { Key = third }); + await db.SaveChangesAsync(cancellation); + + new[] { third, first, second }.Order().ShouldBe([first, second, third]); + (await db.Set().OrderBy(k => k.Key).Select(k => k.Key).ToListAsync(cancellation)).ShouldBe([second, third, first]); + (await db.Set().MinAsync(k => k.Key, cancellation)).ShouldBe(second); + (await db.Set().Where(k => k.Key < first).Select(k => k.Key).OrderBy(k => k).ToListAsync(cancellation)).ShouldBe([second, third]); + } +} diff --git a/test/CodoMetis.TypeKit.Facts/KB050.cs b/test/CodoMetis.TypeKit.Facts/KB050.cs new file mode 100644 index 00000000..8f90703f --- /dev/null +++ b/test/CodoMetis.TypeKit.Facts/KB050.cs @@ -0,0 +1,34 @@ +using Microsoft.EntityFrameworkCore; + +namespace CodoMetis.TypeKit.Facts; + +/// +/// design/knowledge/KB-050: a property without a configured precision is a decimal(18,2) +/// column on SQL Server. Every value reads back at scale 2 (10m as 10.00m, equal but printed +/// "10.00"), and a value with more fractional digits is rounded when it is written. +/// +[Trait("Needs", "Docker")] +public sealed class KB050 +{ + public sealed class Money + { + public int Id { get; set; } + + public decimal Amount { get; set; } + } + + [Fact] + public async Task A_decimal_reads_back_at_scale_2_rounded() + { + var cancellation = TestContext.Current.CancellationToken; + await using var db = await SqlServer.ContextAsync(model => model.Entity()); + db.Model.FindEntityType(typeof(Money))!.FindProperty(nameof(Money.Amount))!.GetColumnType().ShouldBe("decimal(18,2)"); + db.AddRange(new Money { Amount = 10m }, new Money { Amount = 1.005m }, new Money { Amount = -0.004m }); + await db.SaveChangesAsync(cancellation); + db.ChangeTracker.Clear(); + + var read = await db.Set().OrderBy(m => m.Id).Select(m => m.Amount).ToListAsync(cancellation); + read.Select(amount => amount.ToString(System.Globalization.CultureInfo.InvariantCulture)).ShouldBe(["10.00", "1.01", "0.00"]); + read[0].ShouldBe(10m); + } +} diff --git a/test/CodoMetis.TypeKit.Facts/KB051.cs b/test/CodoMetis.TypeKit.Facts/KB051.cs new file mode 100644 index 00000000..defad0a0 --- /dev/null +++ b/test/CodoMetis.TypeKit.Facts/KB051.cs @@ -0,0 +1,56 @@ +using Microsoft.EntityFrameworkCore; + +namespace CodoMetis.TypeKit.Facts; + +/// +/// design/knowledge/KB-051: EF maps a to SQL Server's time, which holds less than a day. +/// In a column, SaveChanges refuses a value of a day or more. In a JSON column the value is written as its text, +/// "1.02:00:00", and a query that reads the property converts that text to time: it answers +/// 01:02:00, without an error, where the entity materialized whole reads 1.02:00:00. +/// +[Trait("Needs", "Docker")] +public sealed class KB051 +{ + public sealed class Span + { + public int Id { get; set; } + + public TimeSpan Length { get; set; } + } + + public sealed class Holder + { + public int Id { get; set; } + + public Spans Doc { get; set; } = new(); + } + + public sealed class Spans + { + public TimeSpan Length { get; set; } + } + + [Fact] + public async Task A_time_column_refuses_a_day_or_more() + { + var cancellation = TestContext.Current.CancellationToken; + await using var db = await SqlServer.ContextAsync(model => model.Entity()); + db.Model.FindEntityType(typeof(Span))!.FindProperty(nameof(Span.Length))!.GetColumnType().ShouldBe("time"); + + db.Add(new Span { Length = TimeSpan.FromHours(26) }); + await Should.ThrowAsync(() => db.SaveChangesAsync(cancellation)); + } + + [Fact] + public async Task A_day_or_more_in_a_JSON_column_is_misread_by_a_query() + { + var cancellation = TestContext.Current.CancellationToken; + await using var db = await SqlServer.ContextAsync(model => model.Entity().OwnsOne(h => h.Doc, doc => doc.ToJson())); + db.Add(new Holder { Doc = new Spans { Length = TimeSpan.FromHours(26) } }); + await db.SaveChangesAsync(cancellation); + db.ChangeTracker.Clear(); + + (await db.Set().Select(h => h.Doc.Length).SingleAsync(cancellation)).ShouldBe(new TimeSpan(1, 2, 0)); + (await db.Set().SingleAsync(cancellation)).Doc.Length.ShouldBe(TimeSpan.FromHours(26)); + } +} diff --git a/test/CodoMetis.TypeKit.Facts/KB052.cs b/test/CodoMetis.TypeKit.Facts/KB052.cs new file mode 100644 index 00000000..932f4991 --- /dev/null +++ b/test/CodoMetis.TypeKit.Facts/KB052.cs @@ -0,0 +1,31 @@ +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; + +namespace CodoMetis.TypeKit.Facts; + +/// +/// design/knowledge/KB-052: EF translates Max and Min of a column to MAX and +/// MIN of a bit, which SQL Server refuses: "Operand data type bit is invalid for max operator". +/// +[Trait("Needs", "Docker")] +public sealed class KB052 +{ + public sealed class Flagged + { + public int Id { get; set; } + + public bool Flag { get; set; } + } + + [Fact] + public async Task Max_and_min_of_a_bit_are_refused() + { + var cancellation = TestContext.Current.CancellationToken; + await using var db = await SqlServer.ContextAsync(model => model.Entity()); + db.AddRange(new Flagged { Flag = false }, new Flagged { Flag = true }); + await db.SaveChangesAsync(cancellation); + + (await Should.ThrowAsync(() => db.Set().MaxAsync(f => f.Flag, cancellation))).Message.ShouldContain("Operand data type bit is invalid for max operator"); + (await Should.ThrowAsync(() => db.Set().MinAsync(f => f.Flag, cancellation))).Message.ShouldContain("Operand data type bit is invalid for min operator"); + } +} diff --git a/test/CodoMetis.TypeKit.Facts/KB053.cs b/test/CodoMetis.TypeKit.Facts/KB053.cs new file mode 100644 index 00000000..844f4db1 --- /dev/null +++ b/test/CodoMetis.TypeKit.Facts/KB053.cs @@ -0,0 +1,55 @@ +using Microsoft.EntityFrameworkCore; + +namespace CodoMetis.TypeKit.Facts; + +/// +/// design/knowledge/KB-053: EF's SQL Server provider translates EF.Functions.Greatest and Least to +/// GREATEST and LEAST only at compatibility level 160 or later (UseCompatibilityLevel(160)), which +/// UseSqlServer does not assume. Below it a filter that calls one is refused ("could not be translated"), and a +/// final projection reads the arguments' columns and calls the function in memory, where it throws when the query runs. +/// No server is opened; ToQueryString translates without one. +/// +public sealed class KB053 +{ + public sealed class Pair + { + public int Id { get; set; } + + public int Left { get; set; } + + public int Right { get; set; } + } + + private sealed class Db(DbContextOptions options) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) => modelBuilder.Entity(); + } + + private static Db Create(int? level) => + new(new DbContextOptionsBuilder().UseSqlServer("Server=localhost;Database=none", sql => + { + if (level is { } compatibility) sql.UseCompatibilityLevel(compatibility); + }).Options); + + /// EF's default, and 150, the level below. + public static TheoryData Below160 => [0, 150]; + + [Theory, MemberData(nameof(Below160))] + public void Below_compatibility_level_160_Greatest_is_not_translated(int level) + { + using var db = Create(level == 0 ? null : level); + + Should.Throw(() => db.Set().Where(p => EF.Functions.Greatest(p.Left, p.Right) > 2).ToQueryString()).Message.ShouldContain("could not be translated"); + db.Set().Select(p => EF.Functions.Least(p.Left, p.Right)).ToQueryString().ShouldBe("SELECT [p].[Left], [p].[Right]" + Environment.NewLine + "FROM [Pair] AS [p]"); + Should.Throw(() => EF.Functions.Least(1, 2)).Message.ShouldContain("switched to client-evaluation"); + } + + [Fact] + public void At_compatibility_level_160_Greatest_and_Least_are_translated() + { + using var db = Create(160); + + db.Set().Where(p => EF.Functions.Greatest(p.Left, p.Right) > 2).ToQueryString().ShouldContain("WHERE GREATEST([p].[Left], [p].[Right]) > 2"); + db.Set().Select(p => EF.Functions.Least(p.Left, p.Right)).ToQueryString().ShouldContain("SELECT LEAST([p].[Left], [p].[Right])"); + } +} diff --git a/test/CodoMetis.TypeKit.Facts/KB054.cs b/test/CodoMetis.TypeKit.Facts/KB054.cs new file mode 100644 index 00000000..1e526286 --- /dev/null +++ b/test/CodoMetis.TypeKit.Facts/KB054.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore; + +namespace CodoMetis.TypeKit.Facts; + +/// +/// design/knowledge/KB-054: EF maps a to SQL Server's datetime2, of precision 7 unless +/// configured, which keeps every tick: nothing is rounded. A written as UTC reads back with +/// , and equal, since equality ignores the kind. A +/// keeps its offset. +/// +[Trait("Needs", "Docker")] +public sealed class KB054 +{ + public sealed class Stamped + { + public int Id { get; set; } + + public DateTime At { get; set; } + + public DateTimeOffset Offset { get; set; } + } + + [Fact] + public async Task Datetime2_keeps_every_tick_and_reads_back_unspecified() + { + var cancellation = TestContext.Current.CancellationToken; + var at = new DateTime(2026, 10, 29, 12, 0, 0, DateTimeKind.Utc).AddTicks(1234567); + var offset = new DateTimeOffset(2026, 10, 29, 12, 0, 0, TimeSpan.FromHours(2)).AddTicks(7654321); + await using var db = await SqlServer.ContextAsync(model => model.Entity()); + db.Model.FindEntityType(typeof(Stamped))!.FindProperty(nameof(Stamped.At))!.GetColumnType().ShouldBe("datetime2"); + db.Add(new Stamped { At = at, Offset = offset }); + await db.SaveChangesAsync(cancellation); + db.ChangeTracker.Clear(); + + var read = await db.Set().SingleAsync(cancellation); + read.At.Ticks.ShouldBe(at.Ticks); + read.At.Kind.ShouldBe(DateTimeKind.Unspecified); + read.Offset.ShouldBe(offset); + read.Offset.Offset.ShouldBe(TimeSpan.FromHours(2)); + } +} diff --git a/test/CodoMetis.TypeKit.Facts/SqlServerFactHost.cs b/test/CodoMetis.TypeKit.Facts/SqlServerFactHost.cs new file mode 100644 index 00000000..cebb39ef --- /dev/null +++ b/test/CodoMetis.TypeKit.Facts/SqlServerFactHost.cs @@ -0,0 +1,84 @@ +using System.Runtime.CompilerServices; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace CodoMetis.TypeKit.Facts; + +/// +/// A SQL Server database of its own for a fact, with plain EF Core and nothing of TypeKit's, in one container started +/// by the first fact that needs it: SQL Server 2022's Linux image, which CI's Linux leg runs and the entries pin, or the +/// image TYPEKIT_MSSQL_IMAGE names (the native arm64 mcr.microsoft.com/azure-sql-edge, on a machine that +/// would emulate 2022 under QEMU, where it does not start). +/// +/// The facts that need it carry the trait Needs=Docker, which CI's Windows and macOS legs leave out. +internal static class SqlServer +{ + private const string Password = "TypeKit-2026!"; + + private static readonly Lazy> Container = new(async () => + { + var container = new ContainerBuilder(Environment.GetEnvironmentVariable("TYPEKIT_MSSQL_IMAGE") is { Length: > 0 } image ? image : "mcr.microsoft.com/mssql/server:2022-latest") + .WithEnvironment("ACCEPT_EULA", "Y") + .WithEnvironment("MSSQL_SA_PASSWORD", Password) + .WithPortBinding(1433, assignRandomHostPort: true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("SQL Server is now ready for client connections")) + .Build(); + await container.StartAsync(); + return container; + }); + + /// A fresh database for , created, and dropped when disposed; the model is cached per fact and test. + public static async Task ContextAsync(Action model, Action? sql = null, + [CallerFilePath] string fact = "", [CallerMemberName] string test = "") + { + var container = await Container.Value; + var connection = new SqlConnectionStringBuilder + { + DataSource = $"{container.Hostname},{container.GetMappedPublicPort(1433)}", + UserID = "sa", + Password = Password, + InitialCatalog = $"kb_{Guid.NewGuid():N}", + TrustServerCertificate = true, + }.ConnectionString; + var options = new DbContextOptionsBuilder().UseSqlServer(connection, sql) + .ReplaceService() + .Options; + var db = new SqlServerDb(options, model, $"{Path.GetFileNameWithoutExtension(fact)}.{test}"); + + // The server logs that it is ready before its login works, the first time it starts. + for (var attempt = 1; ; attempt++) + { + try + { + await db.Database.EnsureCreatedAsync(TestContext.Current.CancellationToken); + return db; + } + catch (SqlException) when (attempt < 60) + { + await Task.Delay(TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken); + } + } + } +} + +/// A context whose model is the fact's, cached per fact. +internal sealed class SqlServerDb(DbContextOptions options, Action model, string fact) : DbContext(options) +{ + protected override void OnModelCreating(ModelBuilder modelBuilder) => model(modelBuilder); + + public override async ValueTask DisposeAsync() + { + await Database.EnsureDeletedAsync(); + await base.DisposeAsync(); + } + + public sealed class CacheKey : IModelCacheKeyFactory + { + public object Create(DbContext context, bool designTime) => context is SqlServerDb db ? (typeof(SqlServerDb), db.FactName, designTime) : (context.GetType(), designTime); + } + + private string FactName => fact; +} diff --git a/test/CodoMetis.TypeKit.Generators.Tests/FabricTests.cs b/test/CodoMetis.TypeKit.Generators.Tests/FabricTests.cs index e67465ca..a2881620 100644 --- a/test/CodoMetis.TypeKit.Generators.Tests/FabricTests.cs +++ b/test/CodoMetis.TypeKit.Generators.Tests/FabricTests.cs @@ -10,6 +10,13 @@ namespace CodoMetis.TypeKit.Generators.Tests; /// public readonly partial record struct TestLocalId : IValue; +/// +/// Reaches its marker through , an interface of another assembly: the +/// fabric finds it in this compilation's index of derived types, whose chain runs through a type it +/// did not compile. +/// +public readonly partial record struct TestForeignIdentifier : IProbeIdentifier; + /// /// The fabric reaches every value object it should. A type it skips compiles cleanly, and the /// analyzer stays quiet because the generators are referenced, so only a test like this notices. @@ -47,6 +54,7 @@ public void Every_declared_value_object_is_generated(Type type) /// [Theory] [InlineData(typeof(ProbeCustomerId), "a marker reached through another interface")] + [InlineData(typeof(TestForeignIdentifier), "a marker reached through an interface of another assembly")] [InlineData(typeof(ProbeContainer.NestedId), "a nested type")] [InlineData(typeof(ProbeLabel), "a record class")] [InlineData(typeof(ProbeInternalId), "an internal type")] diff --git a/test/CodoMetis.TypeKit.Generators.Tests/GeneratedSurface.verified.txt b/test/CodoMetis.TypeKit.Generators.Tests/GeneratedSurface.verified.txt index c2e1484e..14f02dab 100644 --- a/test/CodoMetis.TypeKit.Generators.Tests/GeneratedSurface.verified.txt +++ b/test/CodoMetis.TypeKit.Generators.Tests/GeneratedSurface.verified.txt @@ -1,3 +1,4 @@ +# CodoMetis.TypeKit.Generators.Probes ProbeAmount CompareTo(Object) : Int32 CompareTo(ProbeAmount) : Int32 @@ -1581,3 +1582,2617 @@ ProbeWeekday static op_LessThanOrEqual(ProbeWeekday, ProbeWeekday) : Boolean extension static ProbeWeekdayExtensions.GetValue(ProbeWeekday) : DayOfWeek extension static ProbeWeekdayExtensions.ValueOrNull(ProbeWeekday?) : DayOfWeek? +# CodoMetis.TypeKit.Generators.Tests +DefaultedAddress + Equals(DefaultedAddress) : Boolean + Equals(Object) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : IPAddress + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested DefaultedAddressJsonConverter : JsonConverter, IStoredJsonConverterSource + nested DefaultedAddressTypeConverter : TypeConverter + new() + static From(IPAddress) : DefaultedAddress + static Parse(ReadOnlySpan, IFormatProvider) : DefaultedAddress + static Parse(ReadOnlySpan, IFormatProvider) : DefaultedAddress + static Parse(String, IFormatProvider) : DefaultedAddress + static TryParse(ReadOnlySpan, IFormatProvider, out DefaultedAddress) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out DefaultedAddress) : Boolean + static TryParse(String, IFormatProvider, out DefaultedAddress) : Boolean + static op_Equality(DefaultedAddress, DefaultedAddress) : Boolean + static op_Inequality(DefaultedAddress, DefaultedAddress) : Boolean + extension static DefaultedAddressExtensions.GetValue(DefaultedAddress) : IPAddress + extension static DefaultedAddressExtensions.ValueOrNull(DefaultedAddress?) : IPAddress +DefaultedVersion + CompareTo(DefaultedVersion) : Int32 + CompareTo(Object) : Int32 + Equals(DefaultedVersion) : Boolean + Equals(Object) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Version + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested DefaultedVersionJsonConverter : JsonConverter, IStoredJsonConverterSource + nested DefaultedVersionTypeConverter : TypeConverter + new() + static From(Version) : DefaultedVersion + static Parse(ReadOnlySpan, IFormatProvider) : DefaultedVersion + static Parse(String, IFormatProvider) : DefaultedVersion + static TryParse(ReadOnlySpan, IFormatProvider, out DefaultedVersion) : Boolean + static TryParse(String, IFormatProvider, out DefaultedVersion) : Boolean + static op_Equality(DefaultedVersion, DefaultedVersion) : Boolean + static op_GreaterThan(DefaultedVersion, DefaultedVersion) : Boolean + static op_GreaterThanOrEqual(DefaultedVersion, DefaultedVersion) : Boolean + static op_Inequality(DefaultedVersion, DefaultedVersion) : Boolean + static op_LessThan(DefaultedVersion, DefaultedVersion) : Boolean + static op_LessThanOrEqual(DefaultedVersion, DefaultedVersion) : Boolean + extension static DefaultedVersionExtensions.GetValue(DefaultedVersion) : Version + extension static DefaultedVersionExtensions.ValueOrNull(DefaultedVersion?) : Version +DocumentKind + Match(Func, Func) : TResult + Name : String + QualifiedName : String + ToString() : String + attribute CompilerGeneratedAttribute + attribute GeneratedClosedSetAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IClosedSet + implements IParsable + nested DocumentKindJsonConverter : JsonConverter + nested DocumentKindTypeConverter : TypeConverter + static All : IReadOnlyList + static Parse(String) : DocumentKind + static Parse(String, Boolean) : DocumentKind + static TryParse(String, Boolean, out DocumentKind) : Boolean + static TryParse(String, out DocumentKind) : Boolean +EarlyAllStatus + Match(Func) : TResult + Name : String + QualifiedName : String + ToString() : String + attribute CompilerGeneratedAttribute + attribute GeneratedClosedSetAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IClosedSet + implements IParsable + nested EarlyAllStatusJsonConverter : JsonConverter + nested EarlyAllStatusTypeConverter : TypeConverter + static All : IReadOnlyList + static Parse(String) : EarlyAllStatus + static Parse(String, Boolean) : EarlyAllStatus + static TryParse(String, Boolean, out EarlyAllStatus) : Boolean + static TryParse(String, out EarlyAllStatus) : Boolean +EarlyIgnoreCaseParseStatus + Match(Func) : TResult + Name : String + QualifiedName : String + ToString() : String + attribute CompilerGeneratedAttribute + attribute GeneratedClosedSetAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IClosedSet + implements IParsable + nested EarlyIgnoreCaseParseStatusJsonConverter : JsonConverter + nested EarlyIgnoreCaseParseStatusTypeConverter : TypeConverter + static All : IReadOnlyList + static Parse(String) : EarlyIgnoreCaseParseStatus + static Parse(String, Boolean) : EarlyIgnoreCaseParseStatus + static TryParse(String, Boolean, out EarlyIgnoreCaseParseStatus) : Boolean + static TryParse(String, out EarlyIgnoreCaseParseStatus) : Boolean +EarlyIgnoreCaseTryParseStatus + Match(Func) : TResult + Name : String + QualifiedName : String + ToString() : String + attribute CompilerGeneratedAttribute + attribute GeneratedClosedSetAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IClosedSet + implements IParsable + nested EarlyIgnoreCaseTryParseStatusJsonConverter : JsonConverter + nested EarlyIgnoreCaseTryParseStatusTypeConverter : TypeConverter + static All : IReadOnlyList + static Parse(String) : EarlyIgnoreCaseTryParseStatus + static Parse(String, Boolean) : EarlyIgnoreCaseTryParseStatus + static TryParse(String, Boolean, out EarlyIgnoreCaseTryParseStatus) : Boolean + static TryParse(String, out EarlyIgnoreCaseTryParseStatus) : Boolean +EarlyJsonKeyStatus + Match(Func) : TResult + Name : String + QualifiedName : String + ToString() : String + attribute CompilerGeneratedAttribute + attribute GeneratedClosedSetAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IClosedSet + implements IParsable + nested EarlyJsonKeyStatusJsonConverter : JsonConverter + nested EarlyJsonKeyStatusTypeConverter : TypeConverter + static All : IReadOnlyList + static Parse(String) : EarlyJsonKeyStatus + static Parse(String, Boolean) : EarlyJsonKeyStatus + static TryParse(String, Boolean, out EarlyJsonKeyStatus) : Boolean + static TryParse(String, out EarlyJsonKeyStatus) : Boolean +EarlyJsonStatus + Match(Func) : TResult + Name : String + QualifiedName : String + ToString() : String + attribute CompilerGeneratedAttribute + attribute GeneratedClosedSetAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IClosedSet + implements IParsable + nested EarlyJsonStatusJsonConverter : JsonConverter + nested EarlyJsonStatusTypeConverter : TypeConverter + static All : IReadOnlyList + static Parse(String) : EarlyJsonStatus + static Parse(String, Boolean) : EarlyJsonStatus + static TryParse(String, Boolean, out EarlyJsonStatus) : Boolean + static TryParse(String, out EarlyJsonStatus) : Boolean +EarlyParsableStatus + Match(Func) : TResult + Name : String + QualifiedName : String + ToString() : String + attribute CompilerGeneratedAttribute + attribute GeneratedClosedSetAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IClosedSet + implements IParsable + nested EarlyParsableStatusJsonConverter : JsonConverter + nested EarlyParsableStatusTypeConverter : TypeConverter + static All : IReadOnlyList + static Parse(String) : EarlyParsableStatus + static Parse(String, Boolean) : EarlyParsableStatus + static TryParse(String, Boolean, out EarlyParsableStatus) : Boolean + static TryParse(String, out EarlyParsableStatus) : Boolean +EarlyParseStatus + Match(Func) : TResult + Name : String + QualifiedName : String + ToString() : String + attribute CompilerGeneratedAttribute + attribute GeneratedClosedSetAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IClosedSet + implements IParsable + nested EarlyParseStatusJsonConverter : JsonConverter + nested EarlyParseStatusTypeConverter : TypeConverter + static All : IReadOnlyList + static Parse(String) : EarlyParseStatus + static Parse(String, Boolean) : EarlyParseStatus + static TryParse(String, Boolean, out EarlyParseStatus) : Boolean + static TryParse(String, out EarlyParseStatus) : Boolean +EarlyTryParseStatus + Match(Func) : TResult + Name : String + QualifiedName : String + ToString() : String + attribute CompilerGeneratedAttribute + attribute GeneratedClosedSetAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IClosedSet + implements IParsable + nested EarlyTryParseStatusJsonConverter : JsonConverter + nested EarlyTryParseStatusTypeConverter : TypeConverter + static All : IReadOnlyList + static Parse(String) : EarlyTryParseStatus + static Parse(String, Boolean) : EarlyTryParseStatus + static TryParse(String, Boolean, out EarlyTryParseStatus) : Boolean + static TryParse(String, out EarlyTryParseStatus) : Boolean +LocalOrder.Stage + Match(Func, Func) : TResult + Name : String + QualifiedName : String + ToString() : String + attribute CompilerGeneratedAttribute + attribute GeneratedClosedSetAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IClosedSet + implements IParsable + nested StageJsonConverter : JsonConverter + nested StageTypeConverter : TypeConverter + static All : IReadOnlyList + static Parse(String) : LocalOrder.Stage + static Parse(String, Boolean) : LocalOrder.Stage + static TryParse(String, Boolean, out LocalOrder.Stage) : Boolean + static TryParse(String, out LocalOrder.Stage) : Boolean +LocalStatus + Match(Func, Func, Func) : TResult + Name : String + QualifiedName : String + ToString() : String + attribute CompilerGeneratedAttribute + attribute GeneratedClosedSetAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IClosedSet + implements IParsable + nested LocalStatusJsonConverter : JsonConverter + nested LocalStatusTypeConverter : TypeConverter + static All : IReadOnlyList + static Parse(String) : LocalStatus + static Parse(String, Boolean) : LocalStatus + static TryParse(String, Boolean, out LocalStatus) : Boolean + static TryParse(String, out LocalStatus) : Boolean +MaskedOverALibraryRecord + $() : MaskedOverALibraryRecord + CompareTo(MaskedOverALibraryRecord) : Int32 + CompareTo(Object) : Int32 + Equals(MaskedOverALibraryRecord) : Boolean + Equals(Object) : Boolean + Equals(ProbeMaskingRecord) : Boolean + GetHashCode() : Int32 + Value : String + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IEquatable + implements IParsable + implements IPlainValueObject + implements IReimplementedEquatable + implements ISpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested MaskedOverALibraryRecordJsonConverter : JsonConverter, IStoredJsonConverterSource + nested MaskedOverALibraryRecordTypeConverter : TypeConverter + static From(String) : MaskedOverALibraryRecord + static Parse(ReadOnlySpan, IFormatProvider) : MaskedOverALibraryRecord + static Parse(String, IFormatProvider) : MaskedOverALibraryRecord + static TryParse(ReadOnlySpan, IFormatProvider, out MaskedOverALibraryRecord) : Boolean + static TryParse(String, IFormatProvider, out MaskedOverALibraryRecord) : Boolean + static op_Equality(MaskedOverALibraryRecord, MaskedOverALibraryRecord) : Boolean + static op_GreaterThan(MaskedOverALibraryRecord, MaskedOverALibraryRecord) : Boolean + static op_GreaterThanOrEqual(MaskedOverALibraryRecord, MaskedOverALibraryRecord) : Boolean + static op_Inequality(MaskedOverALibraryRecord, MaskedOverALibraryRecord) : Boolean + static op_LessThan(MaskedOverALibraryRecord, MaskedOverALibraryRecord) : Boolean + static op_LessThanOrEqual(MaskedOverALibraryRecord, MaskedOverALibraryRecord) : Boolean + extension static MaskedOverALibraryRecordExtensions.GetValue(MaskedOverALibraryRecord) : String + extension static MaskedOverALibraryRecordExtensions.ValueOrNull(MaskedOverALibraryRecord) : String +MaskedOverALocalRecord + $() : MaskedOverALocalRecord + CompareTo(MaskedOverALocalRecord) : Int32 + CompareTo(Object) : Int32 + Equals(LocalMaskingRecord) : Boolean + Equals(MaskedOverALocalRecord) : Boolean + Equals(Object) : Boolean + GetHashCode() : Int32 + Value : String + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IEquatable + implements IParsable + implements IPlainValueObject + implements IReimplementedEquatable + implements ISpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested MaskedOverALocalRecordJsonConverter : JsonConverter, IStoredJsonConverterSource + nested MaskedOverALocalRecordTypeConverter : TypeConverter + static From(String) : MaskedOverALocalRecord + static Parse(ReadOnlySpan, IFormatProvider) : MaskedOverALocalRecord + static Parse(String, IFormatProvider) : MaskedOverALocalRecord + static TryParse(ReadOnlySpan, IFormatProvider, out MaskedOverALocalRecord) : Boolean + static TryParse(String, IFormatProvider, out MaskedOverALocalRecord) : Boolean + static op_Equality(MaskedOverALocalRecord, MaskedOverALocalRecord) : Boolean + static op_GreaterThan(MaskedOverALocalRecord, MaskedOverALocalRecord) : Boolean + static op_GreaterThanOrEqual(MaskedOverALocalRecord, MaskedOverALocalRecord) : Boolean + static op_Inequality(MaskedOverALocalRecord, MaskedOverALocalRecord) : Boolean + static op_LessThan(MaskedOverALocalRecord, MaskedOverALocalRecord) : Boolean + static op_LessThanOrEqual(MaskedOverALocalRecord, MaskedOverALocalRecord) : Boolean + extension static MaskedOverALocalRecordExtensions.GetValue(MaskedOverALocalRecord) : String + extension static MaskedOverALocalRecordExtensions.ValueOrNull(MaskedOverALocalRecord) : String +OverABetweenExplicitEqualityRecord + $() : OverABetweenExplicitEqualityRecord + CompareTo(Object) : Int32 + CompareTo(OverABetweenExplicitEqualityRecord) : Int32 + Equals(LocalEqualityFreeRecord) : Boolean + Equals(LocalReimplementingBetweenRecord) : Boolean + Equals(Object) : Boolean + Equals(OverABetweenExplicitEqualityRecord) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Int32 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IEquatable + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements IReimplementedEquatable + implements IReimplementedEquatable + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverABetweenExplicitEqualityRecordJsonConverter : JsonConverter, IStoredJsonConverterSource + nested OverABetweenExplicitEqualityRecordTypeConverter : TypeConverter + static From(Int32) : OverABetweenExplicitEqualityRecord + static MaxValue : OverABetweenExplicitEqualityRecord + static MinValue : OverABetweenExplicitEqualityRecord + static Parse(ReadOnlySpan, IFormatProvider) : OverABetweenExplicitEqualityRecord + static Parse(ReadOnlySpan, IFormatProvider) : OverABetweenExplicitEqualityRecord + static Parse(String, IFormatProvider) : OverABetweenExplicitEqualityRecord + static TryParse(ReadOnlySpan, IFormatProvider, out OverABetweenExplicitEqualityRecord) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out OverABetweenExplicitEqualityRecord) : Boolean + static TryParse(String, IFormatProvider, out OverABetweenExplicitEqualityRecord) : Boolean + static op_Equality(OverABetweenExplicitEqualityRecord, OverABetweenExplicitEqualityRecord) : Boolean + static op_GreaterThan(OverABetweenExplicitEqualityRecord, OverABetweenExplicitEqualityRecord) : Boolean + static op_GreaterThanOrEqual(OverABetweenExplicitEqualityRecord, OverABetweenExplicitEqualityRecord) : Boolean + static op_Inequality(OverABetweenExplicitEqualityRecord, OverABetweenExplicitEqualityRecord) : Boolean + static op_LessThan(OverABetweenExplicitEqualityRecord, OverABetweenExplicitEqualityRecord) : Boolean + static op_LessThanOrEqual(OverABetweenExplicitEqualityRecord, OverABetweenExplicitEqualityRecord) : Boolean + extension static OverABetweenExplicitEqualityRecordExtensions.GetValue(OverABetweenExplicitEqualityRecord) : Int32 + extension static OverABetweenExplicitEqualityRecordExtensions.ValueOrNull(OverABetweenExplicitEqualityRecord) : Int32? +OverADeeperExplicitEqualityRecord + $() : OverADeeperExplicitEqualityRecord + CompareTo(Object) : Int32 + CompareTo(OverADeeperExplicitEqualityRecord) : Int32 + Equals(LocalBetweenRecord) : Boolean + Equals(LocalExplicitEqualityRecord) : Boolean + Equals(Object) : Boolean + Equals(OverADeeperExplicitEqualityRecord) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Int32 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IEquatable + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements IReimplementedEquatable + implements IReimplementedEquatable + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverADeeperExplicitEqualityRecordJsonConverter : JsonConverter, IStoredJsonConverterSource + nested OverADeeperExplicitEqualityRecordTypeConverter : TypeConverter + static From(Int32) : OverADeeperExplicitEqualityRecord + static MaxValue : OverADeeperExplicitEqualityRecord + static MinValue : OverADeeperExplicitEqualityRecord + static Parse(ReadOnlySpan, IFormatProvider) : OverADeeperExplicitEqualityRecord + static Parse(ReadOnlySpan, IFormatProvider) : OverADeeperExplicitEqualityRecord + static Parse(String, IFormatProvider) : OverADeeperExplicitEqualityRecord + static TryParse(ReadOnlySpan, IFormatProvider, out OverADeeperExplicitEqualityRecord) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out OverADeeperExplicitEqualityRecord) : Boolean + static TryParse(String, IFormatProvider, out OverADeeperExplicitEqualityRecord) : Boolean + static op_Equality(OverADeeperExplicitEqualityRecord, OverADeeperExplicitEqualityRecord) : Boolean + static op_GreaterThan(OverADeeperExplicitEqualityRecord, OverADeeperExplicitEqualityRecord) : Boolean + static op_GreaterThanOrEqual(OverADeeperExplicitEqualityRecord, OverADeeperExplicitEqualityRecord) : Boolean + static op_Inequality(OverADeeperExplicitEqualityRecord, OverADeeperExplicitEqualityRecord) : Boolean + static op_LessThan(OverADeeperExplicitEqualityRecord, OverADeeperExplicitEqualityRecord) : Boolean + static op_LessThanOrEqual(OverADeeperExplicitEqualityRecord, OverADeeperExplicitEqualityRecord) : Boolean + extension static OverADeeperExplicitEqualityRecordExtensions.GetValue(OverADeeperExplicitEqualityRecord) : Int32 + extension static OverADeeperExplicitEqualityRecordExtensions.ValueOrNull(OverADeeperExplicitEqualityRecord) : Int32? +OverALibraryRecord + $() : OverALibraryRecord + CompareTo(Object) : Int32 + CompareTo(OverALibraryRecord) : Int32 + Equals(Object) : Boolean + Equals(OverALibraryRecord) : Boolean + Equals(ProbeRecordBase) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Int32 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements IReimplementedEquatable + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverALibraryRecordJsonConverter : JsonConverter, IStoredJsonConverterSource + nested OverALibraryRecordTypeConverter : TypeConverter + static From(Int32) : OverALibraryRecord + static MaxValue : OverALibraryRecord + static MinValue : OverALibraryRecord + static Parse(ReadOnlySpan, IFormatProvider) : OverALibraryRecord + static Parse(ReadOnlySpan, IFormatProvider) : OverALibraryRecord + static Parse(String, IFormatProvider) : OverALibraryRecord + static TryParse(ReadOnlySpan, IFormatProvider, out OverALibraryRecord) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out OverALibraryRecord) : Boolean + static TryParse(String, IFormatProvider, out OverALibraryRecord) : Boolean + static op_Equality(OverALibraryRecord, OverALibraryRecord) : Boolean + static op_GreaterThan(OverALibraryRecord, OverALibraryRecord) : Boolean + static op_GreaterThanOrEqual(OverALibraryRecord, OverALibraryRecord) : Boolean + static op_Inequality(OverALibraryRecord, OverALibraryRecord) : Boolean + static op_LessThan(OverALibraryRecord, OverALibraryRecord) : Boolean + static op_LessThanOrEqual(OverALibraryRecord, OverALibraryRecord) : Boolean + extension static OverALibraryRecordExtensions.GetValue(OverALibraryRecord) : Int32 + extension static OverALibraryRecordExtensions.ValueOrNull(OverALibraryRecord) : Int32? +OverAReferenceEqualityRecord + $() : OverAReferenceEqualityRecord + CompareTo(Object) : Int32 + CompareTo(OverAReferenceEqualityRecord) : Int32 + Equals(LocalReferenceEqualityRecord) : Boolean + Equals(Object) : Boolean + Equals(OverAReferenceEqualityRecord) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Int32 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements IReimplementedEquatable + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverAReferenceEqualityRecordJsonConverter : JsonConverter, IStoredJsonConverterSource + nested OverAReferenceEqualityRecordTypeConverter : TypeConverter + static From(Int32) : OverAReferenceEqualityRecord + static MaxValue : OverAReferenceEqualityRecord + static MinValue : OverAReferenceEqualityRecord + static Parse(ReadOnlySpan, IFormatProvider) : OverAReferenceEqualityRecord + static Parse(ReadOnlySpan, IFormatProvider) : OverAReferenceEqualityRecord + static Parse(String, IFormatProvider) : OverAReferenceEqualityRecord + static TryParse(ReadOnlySpan, IFormatProvider, out OverAReferenceEqualityRecord) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out OverAReferenceEqualityRecord) : Boolean + static TryParse(String, IFormatProvider, out OverAReferenceEqualityRecord) : Boolean + static op_Equality(OverAReferenceEqualityRecord, OverAReferenceEqualityRecord) : Boolean + static op_GreaterThan(OverAReferenceEqualityRecord, OverAReferenceEqualityRecord) : Boolean + static op_GreaterThanOrEqual(OverAReferenceEqualityRecord, OverAReferenceEqualityRecord) : Boolean + static op_Inequality(OverAReferenceEqualityRecord, OverAReferenceEqualityRecord) : Boolean + static op_LessThan(OverAReferenceEqualityRecord, OverAReferenceEqualityRecord) : Boolean + static op_LessThanOrEqual(OverAReferenceEqualityRecord, OverAReferenceEqualityRecord) : Boolean + extension static OverAReferenceEqualityRecordExtensions.GetValue(OverAReferenceEqualityRecord) : Int32 + extension static OverAReferenceEqualityRecordExtensions.ValueOrNull(OverAReferenceEqualityRecord) : Int32? +OverAccess + CompareTo(Object) : Int32 + CompareTo(OverAccess) : Int32 + Equals(Object) : Boolean + Equals(OverAccess) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : ProbeAccess + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverAccessJsonConverter : JsonConverter, IStoredJsonConverterSource + nested OverAccessTypeConverter : TypeConverter + new() + static From(ProbeAccess) : OverAccess + static Parse(String, IFormatProvider) : OverAccess + static TryParse(String, IFormatProvider, out OverAccess) : Boolean + static op_Equality(OverAccess, OverAccess) : Boolean + static op_GreaterThan(OverAccess, OverAccess) : Boolean + static op_GreaterThanOrEqual(OverAccess, OverAccess) : Boolean + static op_Inequality(OverAccess, OverAccess) : Boolean + static op_LessThan(OverAccess, OverAccess) : Boolean + static op_LessThanOrEqual(OverAccess, OverAccess) : Boolean + extension static OverAccessExtensions.GetValue(OverAccess) : ProbeAccess + extension static OverAccessExtensions.ValueOrNull(OverAccess?) : ProbeAccess? +OverAnExplicitEqualityRecord + $() : OverAnExplicitEqualityRecord + CompareTo(Object) : Int32 + CompareTo(OverAnExplicitEqualityRecord) : Int32 + Equals(LocalExplicitEqualityRecord) : Boolean + Equals(Object) : Boolean + Equals(OverAnExplicitEqualityRecord) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Int32 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements IReimplementedEquatable + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverAnExplicitEqualityRecordJsonConverter : JsonConverter, IStoredJsonConverterSource + nested OverAnExplicitEqualityRecordTypeConverter : TypeConverter + static From(Int32) : OverAnExplicitEqualityRecord + static MaxValue : OverAnExplicitEqualityRecord + static MinValue : OverAnExplicitEqualityRecord + static Parse(ReadOnlySpan, IFormatProvider) : OverAnExplicitEqualityRecord + static Parse(ReadOnlySpan, IFormatProvider) : OverAnExplicitEqualityRecord + static Parse(String, IFormatProvider) : OverAnExplicitEqualityRecord + static TryParse(ReadOnlySpan, IFormatProvider, out OverAnExplicitEqualityRecord) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out OverAnExplicitEqualityRecord) : Boolean + static TryParse(String, IFormatProvider, out OverAnExplicitEqualityRecord) : Boolean + static op_Equality(OverAnExplicitEqualityRecord, OverAnExplicitEqualityRecord) : Boolean + static op_GreaterThan(OverAnExplicitEqualityRecord, OverAnExplicitEqualityRecord) : Boolean + static op_GreaterThanOrEqual(OverAnExplicitEqualityRecord, OverAnExplicitEqualityRecord) : Boolean + static op_Inequality(OverAnExplicitEqualityRecord, OverAnExplicitEqualityRecord) : Boolean + static op_LessThan(OverAnExplicitEqualityRecord, OverAnExplicitEqualityRecord) : Boolean + static op_LessThanOrEqual(OverAnExplicitEqualityRecord, OverAnExplicitEqualityRecord) : Boolean + extension static OverAnExplicitEqualityRecordExtensions.GetValue(OverAnExplicitEqualityRecord) : Int32 + extension static OverAnExplicitEqualityRecordExtensions.ValueOrNull(OverAnExplicitEqualityRecord) : Int32? +OverAnIdentityRecord + $() : OverAnIdentityRecord + CompareTo(Object) : Int32 + CompareTo(OverAnIdentityRecord) : Int32 + Equals(Object) : Boolean + Equals(OverAnIdentityRecord) : Boolean + Equals(ProbeIdentityRecord) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Int32 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements IReimplementedEquatable + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverAnIdentityRecordJsonConverter : JsonConverter, IStoredJsonConverterSource + nested OverAnIdentityRecordTypeConverter : TypeConverter + static From(Int32) : OverAnIdentityRecord + static MaxValue : OverAnIdentityRecord + static MinValue : OverAnIdentityRecord + static Parse(ReadOnlySpan, IFormatProvider) : OverAnIdentityRecord + static Parse(ReadOnlySpan, IFormatProvider) : OverAnIdentityRecord + static Parse(String, IFormatProvider) : OverAnIdentityRecord + static TryParse(ReadOnlySpan, IFormatProvider, out OverAnIdentityRecord) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out OverAnIdentityRecord) : Boolean + static TryParse(String, IFormatProvider, out OverAnIdentityRecord) : Boolean + static op_Equality(OverAnIdentityRecord, OverAnIdentityRecord) : Boolean + static op_GreaterThan(OverAnIdentityRecord, OverAnIdentityRecord) : Boolean + static op_GreaterThanOrEqual(OverAnIdentityRecord, OverAnIdentityRecord) : Boolean + static op_Inequality(OverAnIdentityRecord, OverAnIdentityRecord) : Boolean + static op_LessThan(OverAnIdentityRecord, OverAnIdentityRecord) : Boolean + static op_LessThanOrEqual(OverAnIdentityRecord, OverAnIdentityRecord) : Boolean + extension static OverAnIdentityRecordExtensions.GetValue(OverAnIdentityRecord) : Int32 + extension static OverAnIdentityRecordExtensions.ValueOrNull(OverAnIdentityRecord) : Int32? +OverByte + CompareTo(Object) : Int32 + CompareTo(OverByte) : Int32 + Equals(Object) : Boolean + Equals(OverByte) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Byte + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverByteJsonConverter : JsonConverter, IStoredJsonConverterSource + nested OverByteTypeConverter : TypeConverter + new() + static From(Byte) : OverByte + static MaxValue : OverByte + static MinValue : OverByte + static Parse(ReadOnlySpan, IFormatProvider) : OverByte + static Parse(ReadOnlySpan, IFormatProvider) : OverByte + static Parse(String, IFormatProvider) : OverByte + static TryParse(ReadOnlySpan, IFormatProvider, out OverByte) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out OverByte) : Boolean + static TryParse(String, IFormatProvider, out OverByte) : Boolean + static op_Equality(OverByte, OverByte) : Boolean + static op_GreaterThan(OverByte, OverByte) : Boolean + static op_GreaterThanOrEqual(OverByte, OverByte) : Boolean + static op_Inequality(OverByte, OverByte) : Boolean + static op_LessThan(OverByte, OverByte) : Boolean + static op_LessThanOrEqual(OverByte, OverByte) : Boolean + extension static OverByteExtensions.GetValue(OverByte) : Byte + extension static OverByteExtensions.ValueOrNull(OverByte?) : Byte? +OverChar + CompareTo(Object) : Int32 + CompareTo(OverChar) : Int32 + Equals(Object) : Boolean + Equals(OverChar) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Char + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverCharJsonConverter : JsonConverter, IStoredJsonConverterSource + nested OverCharTypeConverter : TypeConverter + new() + static From(Char) : OverChar + static MaxValue : OverChar + static MinValue : OverChar + static Parse(ReadOnlySpan, IFormatProvider) : OverChar + static Parse(ReadOnlySpan, IFormatProvider) : OverChar + static Parse(String, IFormatProvider) : OverChar + static TryParse(ReadOnlySpan, IFormatProvider, out OverChar) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out OverChar) : Boolean + static TryParse(String, IFormatProvider, out OverChar) : Boolean + static op_Equality(OverChar, OverChar) : Boolean + static op_GreaterThan(OverChar, OverChar) : Boolean + static op_GreaterThanOrEqual(OverChar, OverChar) : Boolean + static op_Inequality(OverChar, OverChar) : Boolean + static op_LessThan(OverChar, OverChar) : Boolean + static op_LessThanOrEqual(OverChar, OverChar) : Boolean + extension static OverCharExtensions.GetValue(OverChar) : Char + extension static OverCharExtensions.ValueOrNull(OverChar?) : Char? +OverCode + Equals(Object) : Boolean + Equals(OverCode) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : Code + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IParsable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverCodeJsonConverter : JsonConverter, IStoredJsonConverterSource + nested OverCodeTypeConverter : TypeConverter + new() + static From(Code) : OverCode + static Parse(String, IFormatProvider) : OverCode + static TryParse(String, IFormatProvider, out OverCode) : Boolean + static op_Equality(OverCode, OverCode) : Boolean + static op_Inequality(OverCode, OverCode) : Boolean + extension static OverCodeExtensions.GetValue(OverCode) : Code + extension static OverCodeExtensions.ValueOrNull(OverCode?) : Code? +OverConstructedAbstract + Equals(Object) : Boolean + Equals(OverConstructedAbstract) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : ConstructedAbstract + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverConstructedAbstractJsonConverter : JsonConverter, IStoredJsonConverterSource + new() + static From(ConstructedAbstract) : OverConstructedAbstract + static op_Equality(OverConstructedAbstract, OverConstructedAbstract) : Boolean + static op_Inequality(OverConstructedAbstract, OverConstructedAbstract) : Boolean + extension static OverConstructedAbstractExtensions.GetValue(OverConstructedAbstract) : ConstructedAbstract + extension static OverConstructedAbstractExtensions.ValueOrNull(OverConstructedAbstract?) : ConstructedAbstract +OverConstructedByRef + Equals(Object) : Boolean + Equals(OverConstructedByRef) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : ConstructedByRef + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverConstructedByRefJsonConverter : JsonConverter, IStoredJsonConverterSource + new() + static From(ConstructedByRef) : OverConstructedByRef + static op_Equality(OverConstructedByRef, OverConstructedByRef) : Boolean + static op_Inequality(OverConstructedByRef, OverConstructedByRef) : Boolean + extension static OverConstructedByRefExtensions.GetValue(OverConstructedByRef) : ConstructedByRef + extension static OverConstructedByRefExtensions.ValueOrNull(OverConstructedByRef?) : ConstructedByRef +OverConstructedFromIn + Equals(Object) : Boolean + Equals(OverConstructedFromIn) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : ConstructedFromIn + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IParsable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverConstructedFromInJsonConverter : JsonConverter, IStoredJsonConverterSource + nested OverConstructedFromInTypeConverter : TypeConverter + new() + static From(ConstructedFromIn) : OverConstructedFromIn + static Parse(String, IFormatProvider) : OverConstructedFromIn + static TryParse(String, IFormatProvider, out OverConstructedFromIn) : Boolean + static op_Equality(OverConstructedFromIn, OverConstructedFromIn) : Boolean + static op_Inequality(OverConstructedFromIn, OverConstructedFromIn) : Boolean + extension static OverConstructedFromInExtensions.GetValue(OverConstructedFromIn) : ConstructedFromIn + extension static OverConstructedFromInExtensions.ValueOrNull(OverConstructedFromIn?) : ConstructedFromIn +OverDisposable + Equals(Object) : Boolean + Equals(OverDisposable) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : IDisposable + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverDisposableJsonConverter : JsonConverter, IStoredJsonConverterSource + new() + static From(IDisposable) : OverDisposable + static op_Equality(OverDisposable, OverDisposable) : Boolean + static op_Inequality(OverDisposable, OverDisposable) : Boolean + extension static OverDisposableExtensions.GetValue(OverDisposable) : IDisposable + extension static OverDisposableExtensions.ValueOrNull(OverDisposable?) : IDisposable +OverGenericParser + Equals(Object) : Boolean + Equals(OverGenericParser) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : GenericParser + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverGenericParserJsonConverter : JsonConverter, IStoredJsonConverterSource + new() + static From(GenericParser) : OverGenericParser + static op_Equality(OverGenericParser, OverGenericParser) : Boolean + static op_Inequality(OverGenericParser, OverGenericParser) : Boolean + extension static OverGenericParserExtensions.GetValue(OverGenericParser) : GenericParser + extension static OverGenericParserExtensions.ValueOrNull(OverGenericParser?) : GenericParser +OverHoldingAnything + Equals(Object) : Boolean + Equals(OverHoldingAnything) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : HoldingAnything + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverHoldingAnythingJsonConverter : JsonConverter, IStoredJsonConverterSource + new() + static From(HoldingAnything) : OverHoldingAnything + static op_Equality(OverHoldingAnything, OverHoldingAnything) : Boolean + static op_Inequality(OverHoldingAnything, OverHoldingAnything) : Boolean + extension static OverHoldingAnythingExtensions.GetValue(OverHoldingAnything) : HoldingAnything + extension static OverHoldingAnythingExtensions.ValueOrNull(OverHoldingAnything?) : HoldingAnything? +OverHoldingIgnored + Equals(Object) : Boolean + Equals(OverHoldingIgnored) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : HoldingIgnored + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverHoldingIgnoredJsonConverter : JsonConverter, IStoredJsonConverterSource + new() + static From(HoldingIgnored) : OverHoldingIgnored + static op_Equality(OverHoldingIgnored, OverHoldingIgnored) : Boolean + static op_Inequality(OverHoldingIgnored, OverHoldingIgnored) : Boolean + extension static OverHoldingIgnoredExtensions.GetValue(OverHoldingIgnored) : HoldingIgnored + extension static OverHoldingIgnoredExtensions.ValueOrNull(OverHoldingIgnored?) : HoldingIgnored? +OverHoldingNamedHolder + Equals(Object) : Boolean + Equals(OverHoldingNamedHolder) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : HoldingNamedHolder + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverHoldingNamedHolderJsonConverter : JsonConverter, IStoredJsonConverterSource + new() + static From(HoldingNamedHolder) : OverHoldingNamedHolder + static op_Equality(OverHoldingNamedHolder, OverHoldingNamedHolder) : Boolean + static op_Inequality(OverHoldingNamedHolder, OverHoldingNamedHolder) : Boolean + extension static OverHoldingNamedHolderExtensions.GetValue(OverHoldingNamedHolder) : HoldingNamedHolder + extension static OverHoldingNamedHolderExtensions.ValueOrNull(OverHoldingNamedHolder?) : HoldingNamedHolder? +OverHoldingNamedShade + Equals(Object) : Boolean + Equals(OverHoldingNamedShade) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : HoldingNamedShade + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverHoldingNamedShadeJsonConverter : JsonConverter, IStoredJsonConverterSource + new() + static From(HoldingNamedShade) : OverHoldingNamedShade + static op_Equality(OverHoldingNamedShade, OverHoldingNamedShade) : Boolean + static op_Inequality(OverHoldingNamedShade, OverHoldingNamedShade) : Boolean + extension static OverHoldingNamedShadeExtensions.GetValue(OverHoldingNamedShade) : HoldingNamedShade + extension static OverHoldingNamedShadeExtensions.ValueOrNull(OverHoldingNamedShade?) : HoldingNamedShade? +OverHoldingNullableHolder + Equals(Object) : Boolean + Equals(OverHoldingNullableHolder) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : HoldingNullableHolder + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverHoldingNullableHolderJsonConverter : JsonConverter, IStoredJsonConverterSource + new() + static From(HoldingNullableHolder) : OverHoldingNullableHolder + static op_Equality(OverHoldingNullableHolder, OverHoldingNullableHolder) : Boolean + static op_Inequality(OverHoldingNullableHolder, OverHoldingNullableHolder) : Boolean + extension static OverHoldingNullableHolderExtensions.GetValue(OverHoldingNullableHolder) : HoldingNullableHolder + extension static OverHoldingNullableHolderExtensions.ValueOrNull(OverHoldingNullableHolder?) : HoldingNullableHolder? +OverHoldingTracked + Equals(Object) : Boolean + Equals(OverHoldingTracked) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : HoldingTracked + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverHoldingTrackedJsonConverter : JsonConverter, IStoredJsonConverterSource + new() + static From(HoldingTracked) : OverHoldingTracked + static op_Equality(OverHoldingTracked, OverHoldingTracked) : Boolean + static op_Inequality(OverHoldingTracked, OverHoldingTracked) : Boolean + extension static OverHoldingTrackedExtensions.GetValue(OverHoldingTracked) : HoldingTracked + extension static OverHoldingTrackedExtensions.ValueOrNull(OverHoldingTracked?) : HoldingTracked? +OverLabels + Equals(Object) : Boolean + Equals(OverLabels) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : Labels + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverLabelsJsonConverter : JsonConverter, IStoredJsonConverterSource + new() + static From(Labels) : OverLabels + static op_Equality(OverLabels, OverLabels) : Boolean + static op_Inequality(OverLabels, OverLabels) : Boolean + extension static OverLabelsExtensions.GetValue(OverLabels) : Labels + extension static OverLabelsExtensions.ValueOrNull(OverLabels?) : Labels? +OverLocalTime + CompareTo(Object) : Int32 + CompareTo(OverLocalTime) : Int32 + Equals(Object) : Boolean + Equals(OverLocalTime) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : LocalTime + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverLocalTimeJsonConverter : JsonConverter, IStoredJsonConverterSource + nested OverLocalTimeTypeConverter : TypeConverter + new() + static From(LocalTime) : OverLocalTime + static MaxValue : OverLocalTime + static MinValue : OverLocalTime + static Parse(String, IFormatProvider) : OverLocalTime + static TryParse(String, IFormatProvider, out OverLocalTime) : Boolean + static op_Equality(OverLocalTime, OverLocalTime) : Boolean + static op_GreaterThan(OverLocalTime, OverLocalTime) : Boolean + static op_GreaterThanOrEqual(OverLocalTime, OverLocalTime) : Boolean + static op_Inequality(OverLocalTime, OverLocalTime) : Boolean + static op_LessThan(OverLocalTime, OverLocalTime) : Boolean + static op_LessThanOrEqual(OverLocalTime, OverLocalTime) : Boolean + extension static OverLocalTimeExtensions.GetValue(OverLocalTime) : LocalTime + extension static OverLocalTimeExtensions.ValueOrNull(OverLocalTime?) : LocalTime? +OverLong + CompareTo(Object) : Int32 + CompareTo(OverLong) : Int32 + Equals(Object) : Boolean + Equals(OverLong) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Int64 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverLongJsonConverter : JsonConverter, IStoredJsonConverterSource + nested OverLongTypeConverter : TypeConverter + new() + static From(Int64) : OverLong + static MaxValue : OverLong + static MinValue : OverLong + static Parse(ReadOnlySpan, IFormatProvider) : OverLong + static Parse(ReadOnlySpan, IFormatProvider) : OverLong + static Parse(String, IFormatProvider) : OverLong + static TryParse(ReadOnlySpan, IFormatProvider, out OverLong) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out OverLong) : Boolean + static TryParse(String, IFormatProvider, out OverLong) : Boolean + static op_Equality(OverLong, OverLong) : Boolean + static op_GreaterThan(OverLong, OverLong) : Boolean + static op_GreaterThanOrEqual(OverLong, OverLong) : Boolean + static op_Inequality(OverLong, OverLong) : Boolean + static op_LessThan(OverLong, OverLong) : Boolean + static op_LessThanOrEqual(OverLong, OverLong) : Boolean + extension static OverLongExtensions.GetValue(OverLong) : Int64 + extension static OverLongExtensions.ValueOrNull(OverLong?) : Int64? +OverParsedFromText + Equals(Object) : Boolean + Equals(OverParsedFromText) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : ParsedFromText + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IParsable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverParsedFromTextJsonConverter : JsonConverter, IStoredJsonConverterSource + nested OverParsedFromTextTypeConverter : TypeConverter + new() + static From(ParsedFromText) : OverParsedFromText + static Parse(String, IFormatProvider) : OverParsedFromText + static TryParse(String, IFormatProvider, out OverParsedFromText) : Boolean + static op_Equality(OverParsedFromText, OverParsedFromText) : Boolean + static op_Inequality(OverParsedFromText, OverParsedFromText) : Boolean + extension static OverParsedFromTextExtensions.GetValue(OverParsedFromText) : ParsedFromText + extension static OverParsedFromTextExtensions.ValueOrNull(OverParsedFromText?) : ParsedFromText +OverParsedShape + Equals(Object) : Boolean + Equals(OverParsedShape) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : ParsedShape + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IParsable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverParsedShapeJsonConverter : JsonConverter, IStoredJsonConverterSource + nested OverParsedShapeTypeConverter : TypeConverter + new() + static From(ParsedShape) : OverParsedShape + static Parse(String, IFormatProvider) : OverParsedShape + static TryParse(String, IFormatProvider, out OverParsedShape) : Boolean + static op_Equality(OverParsedShape, OverParsedShape) : Boolean + static op_Inequality(OverParsedShape, OverParsedShape) : Boolean + extension static OverParsedShapeExtensions.GetValue(OverParsedShape) : ParsedShape + extension static OverParsedShapeExtensions.ValueOrNull(OverParsedShape?) : ParsedShape +OverParsesFromIn + Equals(Object) : Boolean + Equals(OverParsesFromIn) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : ParsesFromIn + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IParsable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverParsesFromInJsonConverter : JsonConverter, IStoredJsonConverterSource + nested OverParsesFromInTypeConverter : TypeConverter + new() + static From(ParsesFromIn) : OverParsesFromIn + static Parse(String, IFormatProvider) : OverParsesFromIn + static TryParse(String, IFormatProvider, out OverParsesFromIn) : Boolean + static op_Equality(OverParsesFromIn, OverParsesFromIn) : Boolean + static op_Inequality(OverParsesFromIn, OverParsesFromIn) : Boolean + extension static OverParsesFromInExtensions.GetValue(OverParsesFromIn) : ParsesFromIn + extension static OverParsesFromInExtensions.ValueOrNull(OverParsesFromIn?) : ParsesFromIn +OverParsesToADerivedType + Equals(Object) : Boolean + Equals(OverParsesToADerivedType) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : ParsesToADerivedType + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IParsable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverParsesToADerivedTypeJsonConverter : JsonConverter, IStoredJsonConverterSource + nested OverParsesToADerivedTypeTypeConverter : TypeConverter + new() + static From(ParsesToADerivedType) : OverParsesToADerivedType + static Parse(String, IFormatProvider) : OverParsesToADerivedType + static TryParse(String, IFormatProvider, out OverParsesToADerivedType) : Boolean + static op_Equality(OverParsesToADerivedType, OverParsesToADerivedType) : Boolean + static op_Inequality(OverParsesToADerivedType, OverParsesToADerivedType) : Boolean + extension static OverParsesToADerivedTypeExtensions.GetValue(OverParsesToADerivedType) : ParsesToADerivedType + extension static OverParsesToADerivedTypeExtensions.ValueOrNull(OverParsesToADerivedType?) : ParsesToADerivedType +OverParsesToAnotherType + Equals(Object) : Boolean + Equals(OverParsesToAnotherType) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : ParsesToAnotherType + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverParsesToAnotherTypeJsonConverter : JsonConverter, IStoredJsonConverterSource + new() + static From(ParsesToAnotherType) : OverParsesToAnotherType + static op_Equality(OverParsesToAnotherType, OverParsesToAnotherType) : Boolean + static op_Inequality(OverParsesToAnotherType, OverParsesToAnotherType) : Boolean + extension static OverParsesToAnotherTypeExtensions.GetValue(OverParsesToAnotherType) : ParsesToAnotherType + extension static OverParsesToAnotherTypeExtensions.ValueOrNull(OverParsesToAnotherType?) : ParsesToAnotherType +OverParsesVirtually + Equals(Object) : Boolean + Equals(OverParsesVirtually) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : IParsesVirtually + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverParsesVirtuallyJsonConverter : JsonConverter, IStoredJsonConverterSource + new() + static From(IParsesVirtually) : OverParsesVirtually + static op_Equality(OverParsesVirtually, OverParsesVirtually) : Boolean + static op_Inequality(OverParsesVirtually, OverParsesVirtually) : Boolean + extension static OverParsesVirtuallyExtensions.GetValue(OverParsesVirtually) : IParsesVirtually + extension static OverParsesVirtuallyExtensions.ValueOrNull(OverParsesVirtually?) : IParsesVirtually +OverProbeStatus + Equals(Object) : Boolean + Equals(OverProbeStatus) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : ProbeStatus + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IParsable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverProbeStatusJsonConverter : JsonConverter, IStoredJsonConverterSource + nested OverProbeStatusTypeConverter : TypeConverter + new() + static From(ProbeStatus) : OverProbeStatus + static Parse(String, IFormatProvider) : OverProbeStatus + static TryParse(String, IFormatProvider, out OverProbeStatus) : Boolean + static op_Equality(OverProbeStatus, OverProbeStatus) : Boolean + static op_Inequality(OverProbeStatus, OverProbeStatus) : Boolean + extension static OverProbeStatusExtensions.GetValue(OverProbeStatus) : ProbeStatus + extension static OverProbeStatusExtensions.ValueOrNull(OverProbeStatus?) : ProbeStatus +OverStream + Equals(Object) : Boolean + Equals(OverStream) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : Stream + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverStreamJsonConverter : JsonConverter, IStoredJsonConverterSource + new() + static From(Stream) : OverStream + static op_Equality(OverStream, OverStream) : Boolean + static op_Inequality(OverStream, OverStream) : Boolean + extension static OverStreamExtensions.GetValue(OverStream) : Stream + extension static OverStreamExtensions.ValueOrNull(OverStream?) : Stream +OverTracked + Equals(Object) : Boolean + Equals(OverTracked) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : Tracked + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverTrackedJsonConverter : JsonConverter, IStoredJsonConverterSource + new() + static From(Tracked) : OverTracked + static op_Equality(OverTracked, OverTracked) : Boolean + static op_Inequality(OverTracked, OverTracked) : Boolean + extension static OverTrackedExtensions.GetValue(OverTracked) : Tracked + extension static OverTrackedExtensions.ValueOrNull(OverTracked?) : Tracked +OverTrackedList + Equals(Object) : Boolean + Equals(OverTrackedList) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : TrackedList + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverTrackedListJsonConverter : JsonConverter, IStoredJsonConverterSource + new() + static From(TrackedList) : OverTrackedList + static op_Equality(OverTrackedList, OverTrackedList) : Boolean + static op_Inequality(OverTrackedList, OverTrackedList) : Boolean + extension static OverTrackedListExtensions.GetValue(OverTrackedList) : TrackedList + extension static OverTrackedListExtensions.ValueOrNull(OverTrackedList?) : TrackedList? +OverUInt64 + CompareTo(Object) : Int32 + CompareTo(OverUInt64) : Int32 + Equals(Object) : Boolean + Equals(OverUInt64) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : UInt64 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverUInt64JsonConverter : JsonConverter, IStoredJsonConverterSource + nested OverUInt64TypeConverter : TypeConverter + new() + static From(UInt64) : OverUInt64 + static MaxValue : OverUInt64 + static MinValue : OverUInt64 + static Parse(ReadOnlySpan, IFormatProvider) : OverUInt64 + static Parse(ReadOnlySpan, IFormatProvider) : OverUInt64 + static Parse(String, IFormatProvider) : OverUInt64 + static TryParse(ReadOnlySpan, IFormatProvider, out OverUInt64) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out OverUInt64) : Boolean + static TryParse(String, IFormatProvider, out OverUInt64) : Boolean + static op_Equality(OverUInt64, OverUInt64) : Boolean + static op_GreaterThan(OverUInt64, OverUInt64) : Boolean + static op_GreaterThanOrEqual(OverUInt64, OverUInt64) : Boolean + static op_Inequality(OverUInt64, OverUInt64) : Boolean + static op_LessThan(OverUInt64, OverUInt64) : Boolean + static op_LessThanOrEqual(OverUInt64, OverUInt64) : Boolean + extension static OverUInt64Extensions.GetValue(OverUInt64) : UInt64 + extension static OverUInt64Extensions.ValueOrNull(OverUInt64?) : UInt64? +OverUntracked + Equals(Object) : Boolean + Equals(OverUntracked) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : Untracked + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverUntrackedJsonConverter : JsonConverter, IStoredJsonConverterSource + new() + static From(Untracked) : OverUntracked + static op_Equality(OverUntracked, OverUntracked) : Boolean + static op_Inequality(OverUntracked, OverUntracked) : Boolean + extension static OverUntrackedExtensions.GetValue(OverUntracked) : Untracked + extension static OverUntrackedExtensions.ValueOrNull(OverUntracked?) : Untracked? +OverUntrackedTwice + Equals(Object) : Boolean + Equals(OverUntrackedTwice) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : UntrackedTwice + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverUntrackedTwiceJsonConverter : JsonConverter, IStoredJsonConverterSource + new() + static From(UntrackedTwice) : OverUntrackedTwice + static op_Equality(OverUntrackedTwice, OverUntrackedTwice) : Boolean + static op_Inequality(OverUntrackedTwice, OverUntrackedTwice) : Boolean + extension static OverUntrackedTwiceExtensions.GetValue(OverUntrackedTwice) : UntrackedTwice + extension static OverUntrackedTwiceExtensions.ValueOrNull(OverUntrackedTwice?) : UntrackedTwice? +OverVersion + CompareTo(Object) : Int32 + CompareTo(OverVersion) : Int32 + Equals(Object) : Boolean + Equals(OverVersion) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Version + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested OverVersionJsonConverter : JsonConverter, IStoredJsonConverterSource + nested OverVersionTypeConverter : TypeConverter + new() + static From(Version) : OverVersion + static Parse(ReadOnlySpan, IFormatProvider) : OverVersion + static Parse(String, IFormatProvider) : OverVersion + static TryParse(ReadOnlySpan, IFormatProvider, out OverVersion) : Boolean + static TryParse(String, IFormatProvider, out OverVersion) : Boolean + static op_Equality(OverVersion, OverVersion) : Boolean + static op_GreaterThan(OverVersion, OverVersion) : Boolean + static op_GreaterThanOrEqual(OverVersion, OverVersion) : Boolean + static op_Inequality(OverVersion, OverVersion) : Boolean + static op_LessThan(OverVersion, OverVersion) : Boolean + static op_LessThanOrEqual(OverVersion, OverVersion) : Boolean + extension static OverVersionExtensions.GetValue(OverVersion) : Version + extension static OverVersionExtensions.ValueOrNull(OverVersion?) : Version +ParityByte + CompareTo(Object) : Int32 + CompareTo(ParityByte) : Int32 + Equals(Object) : Boolean + Equals(ParityByte) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Byte + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested ParityByteJsonConverter : JsonConverter, IStoredJsonConverterSource + nested ParityByteTypeConverter : TypeConverter + new() + static From(Byte) : ParityByte + static MaxValue : ParityByte + static MinValue : ParityByte + static Parse(ReadOnlySpan, IFormatProvider) : ParityByte + static Parse(ReadOnlySpan, IFormatProvider) : ParityByte + static Parse(String, IFormatProvider) : ParityByte + static TryParse(ReadOnlySpan, IFormatProvider, out ParityByte) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out ParityByte) : Boolean + static TryParse(String, IFormatProvider, out ParityByte) : Boolean + static op_Equality(ParityByte, ParityByte) : Boolean + static op_GreaterThan(ParityByte, ParityByte) : Boolean + static op_GreaterThanOrEqual(ParityByte, ParityByte) : Boolean + static op_Inequality(ParityByte, ParityByte) : Boolean + static op_LessThan(ParityByte, ParityByte) : Boolean + static op_LessThanOrEqual(ParityByte, ParityByte) : Boolean + extension static ParityByteExtensions.GetValue(ParityByte) : Byte + extension static ParityByteExtensions.ValueOrNull(ParityByte?) : Byte? +ParityFloat + CompareTo(Object) : Int32 + CompareTo(ParityFloat) : Int32 + Equals(Object) : Boolean + Equals(ParityFloat) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Single + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested ParityFloatJsonConverter : JsonConverter, IStoredJsonConverterSource + nested ParityFloatTypeConverter : TypeConverter + new() + static From(Single) : ParityFloat + static MaxValue : ParityFloat + static MinValue : ParityFloat + static Parse(ReadOnlySpan, IFormatProvider) : ParityFloat + static Parse(ReadOnlySpan, IFormatProvider) : ParityFloat + static Parse(String, IFormatProvider) : ParityFloat + static TryParse(ReadOnlySpan, IFormatProvider, out ParityFloat) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out ParityFloat) : Boolean + static TryParse(String, IFormatProvider, out ParityFloat) : Boolean + static op_Equality(ParityFloat, ParityFloat) : Boolean + static op_GreaterThan(ParityFloat, ParityFloat) : Boolean + static op_GreaterThanOrEqual(ParityFloat, ParityFloat) : Boolean + static op_Inequality(ParityFloat, ParityFloat) : Boolean + static op_LessThan(ParityFloat, ParityFloat) : Boolean + static op_LessThanOrEqual(ParityFloat, ParityFloat) : Boolean + extension static ParityFloatExtensions.GetValue(ParityFloat) : Single + extension static ParityFloatExtensions.ValueOrNull(ParityFloat?) : Single? +ParityHalf + CompareTo(Object) : Int32 + CompareTo(ParityHalf) : Int32 + Equals(Object) : Boolean + Equals(ParityHalf) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Half + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested ParityHalfJsonConverter : JsonConverter, IStoredJsonConverterSource + nested ParityHalfTypeConverter : TypeConverter + new() + static From(Half) : ParityHalf + static MaxValue : ParityHalf + static MinValue : ParityHalf + static Parse(ReadOnlySpan, IFormatProvider) : ParityHalf + static Parse(ReadOnlySpan, IFormatProvider) : ParityHalf + static Parse(String, IFormatProvider) : ParityHalf + static TryParse(ReadOnlySpan, IFormatProvider, out ParityHalf) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out ParityHalf) : Boolean + static TryParse(String, IFormatProvider, out ParityHalf) : Boolean + static op_Equality(ParityHalf, ParityHalf) : Boolean + static op_GreaterThan(ParityHalf, ParityHalf) : Boolean + static op_GreaterThanOrEqual(ParityHalf, ParityHalf) : Boolean + static op_Inequality(ParityHalf, ParityHalf) : Boolean + static op_LessThan(ParityHalf, ParityHalf) : Boolean + static op_LessThanOrEqual(ParityHalf, ParityHalf) : Boolean + extension static ParityHalfExtensions.GetValue(ParityHalf) : Half + extension static ParityHalfExtensions.ValueOrNull(ParityHalf?) : Half? +ParityInt128 + CompareTo(Object) : Int32 + CompareTo(ParityInt128) : Int32 + Equals(Object) : Boolean + Equals(ParityInt128) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Int128 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested ParityInt128JsonConverter : JsonConverter, IStoredJsonConverterSource + nested ParityInt128TypeConverter : TypeConverter + new() + static From(Int128) : ParityInt128 + static MaxValue : ParityInt128 + static MinValue : ParityInt128 + static Parse(ReadOnlySpan, IFormatProvider) : ParityInt128 + static Parse(ReadOnlySpan, IFormatProvider) : ParityInt128 + static Parse(String, IFormatProvider) : ParityInt128 + static TryParse(ReadOnlySpan, IFormatProvider, out ParityInt128) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out ParityInt128) : Boolean + static TryParse(String, IFormatProvider, out ParityInt128) : Boolean + static op_Equality(ParityInt128, ParityInt128) : Boolean + static op_GreaterThan(ParityInt128, ParityInt128) : Boolean + static op_GreaterThanOrEqual(ParityInt128, ParityInt128) : Boolean + static op_Inequality(ParityInt128, ParityInt128) : Boolean + static op_LessThan(ParityInt128, ParityInt128) : Boolean + static op_LessThanOrEqual(ParityInt128, ParityInt128) : Boolean + extension static ParityInt128Extensions.GetValue(ParityInt128) : Int128 + extension static ParityInt128Extensions.ValueOrNull(ParityInt128?) : Int128? +ParityLong + CompareTo(Object) : Int32 + CompareTo(ParityLong) : Int32 + Equals(Object) : Boolean + Equals(ParityLong) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Int64 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested ParityLongJsonConverter : JsonConverter, IStoredJsonConverterSource + nested ParityLongTypeConverter : TypeConverter + new() + static From(Int64) : ParityLong + static MaxValue : ParityLong + static MinValue : ParityLong + static Parse(ReadOnlySpan, IFormatProvider) : ParityLong + static Parse(ReadOnlySpan, IFormatProvider) : ParityLong + static Parse(String, IFormatProvider) : ParityLong + static TryParse(ReadOnlySpan, IFormatProvider, out ParityLong) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out ParityLong) : Boolean + static TryParse(String, IFormatProvider, out ParityLong) : Boolean + static op_Equality(ParityLong, ParityLong) : Boolean + static op_GreaterThan(ParityLong, ParityLong) : Boolean + static op_GreaterThanOrEqual(ParityLong, ParityLong) : Boolean + static op_Inequality(ParityLong, ParityLong) : Boolean + static op_LessThan(ParityLong, ParityLong) : Boolean + static op_LessThanOrEqual(ParityLong, ParityLong) : Boolean + extension static ParityLongExtensions.GetValue(ParityLong) : Int64 + extension static ParityLongExtensions.ValueOrNull(ParityLong?) : Int64? +ParitySByte + CompareTo(Object) : Int32 + CompareTo(ParitySByte) : Int32 + Equals(Object) : Boolean + Equals(ParitySByte) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : SByte + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested ParitySByteJsonConverter : JsonConverter, IStoredJsonConverterSource + nested ParitySByteTypeConverter : TypeConverter + new() + static From(SByte) : ParitySByte + static MaxValue : ParitySByte + static MinValue : ParitySByte + static Parse(ReadOnlySpan, IFormatProvider) : ParitySByte + static Parse(ReadOnlySpan, IFormatProvider) : ParitySByte + static Parse(String, IFormatProvider) : ParitySByte + static TryParse(ReadOnlySpan, IFormatProvider, out ParitySByte) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out ParitySByte) : Boolean + static TryParse(String, IFormatProvider, out ParitySByte) : Boolean + static op_Equality(ParitySByte, ParitySByte) : Boolean + static op_GreaterThan(ParitySByte, ParitySByte) : Boolean + static op_GreaterThanOrEqual(ParitySByte, ParitySByte) : Boolean + static op_Inequality(ParitySByte, ParitySByte) : Boolean + static op_LessThan(ParitySByte, ParitySByte) : Boolean + static op_LessThanOrEqual(ParitySByte, ParitySByte) : Boolean + extension static ParitySByteExtensions.GetValue(ParitySByte) : SByte + extension static ParitySByteExtensions.ValueOrNull(ParitySByte?) : SByte? +ParityShort + CompareTo(Object) : Int32 + CompareTo(ParityShort) : Int32 + Equals(Object) : Boolean + Equals(ParityShort) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Int16 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested ParityShortJsonConverter : JsonConverter, IStoredJsonConverterSource + nested ParityShortTypeConverter : TypeConverter + new() + static From(Int16) : ParityShort + static MaxValue : ParityShort + static MinValue : ParityShort + static Parse(ReadOnlySpan, IFormatProvider) : ParityShort + static Parse(ReadOnlySpan, IFormatProvider) : ParityShort + static Parse(String, IFormatProvider) : ParityShort + static TryParse(ReadOnlySpan, IFormatProvider, out ParityShort) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out ParityShort) : Boolean + static TryParse(String, IFormatProvider, out ParityShort) : Boolean + static op_Equality(ParityShort, ParityShort) : Boolean + static op_GreaterThan(ParityShort, ParityShort) : Boolean + static op_GreaterThanOrEqual(ParityShort, ParityShort) : Boolean + static op_Inequality(ParityShort, ParityShort) : Boolean + static op_LessThan(ParityShort, ParityShort) : Boolean + static op_LessThanOrEqual(ParityShort, ParityShort) : Boolean + extension static ParityShortExtensions.GetValue(ParityShort) : Int16 + extension static ParityShortExtensions.ValueOrNull(ParityShort?) : Int16? +ParityUInt + CompareTo(Object) : Int32 + CompareTo(ParityUInt) : Int32 + Equals(Object) : Boolean + Equals(ParityUInt) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : UInt32 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested ParityUIntJsonConverter : JsonConverter, IStoredJsonConverterSource + nested ParityUIntTypeConverter : TypeConverter + new() + static From(UInt32) : ParityUInt + static MaxValue : ParityUInt + static MinValue : ParityUInt + static Parse(ReadOnlySpan, IFormatProvider) : ParityUInt + static Parse(ReadOnlySpan, IFormatProvider) : ParityUInt + static Parse(String, IFormatProvider) : ParityUInt + static TryParse(ReadOnlySpan, IFormatProvider, out ParityUInt) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out ParityUInt) : Boolean + static TryParse(String, IFormatProvider, out ParityUInt) : Boolean + static op_Equality(ParityUInt, ParityUInt) : Boolean + static op_GreaterThan(ParityUInt, ParityUInt) : Boolean + static op_GreaterThanOrEqual(ParityUInt, ParityUInt) : Boolean + static op_Inequality(ParityUInt, ParityUInt) : Boolean + static op_LessThan(ParityUInt, ParityUInt) : Boolean + static op_LessThanOrEqual(ParityUInt, ParityUInt) : Boolean + extension static ParityUIntExtensions.GetValue(ParityUInt) : UInt32 + extension static ParityUIntExtensions.ValueOrNull(ParityUInt?) : UInt32? +ParityUInt128 + CompareTo(Object) : Int32 + CompareTo(ParityUInt128) : Int32 + Equals(Object) : Boolean + Equals(ParityUInt128) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : UInt128 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested ParityUInt128JsonConverter : JsonConverter, IStoredJsonConverterSource + nested ParityUInt128TypeConverter : TypeConverter + new() + static From(UInt128) : ParityUInt128 + static MaxValue : ParityUInt128 + static MinValue : ParityUInt128 + static Parse(ReadOnlySpan, IFormatProvider) : ParityUInt128 + static Parse(ReadOnlySpan, IFormatProvider) : ParityUInt128 + static Parse(String, IFormatProvider) : ParityUInt128 + static TryParse(ReadOnlySpan, IFormatProvider, out ParityUInt128) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out ParityUInt128) : Boolean + static TryParse(String, IFormatProvider, out ParityUInt128) : Boolean + static op_Equality(ParityUInt128, ParityUInt128) : Boolean + static op_GreaterThan(ParityUInt128, ParityUInt128) : Boolean + static op_GreaterThanOrEqual(ParityUInt128, ParityUInt128) : Boolean + static op_Inequality(ParityUInt128, ParityUInt128) : Boolean + static op_LessThan(ParityUInt128, ParityUInt128) : Boolean + static op_LessThanOrEqual(ParityUInt128, ParityUInt128) : Boolean + extension static ParityUInt128Extensions.GetValue(ParityUInt128) : UInt128 + extension static ParityUInt128Extensions.ValueOrNull(ParityUInt128?) : UInt128? +ParityULong + CompareTo(Object) : Int32 + CompareTo(ParityULong) : Int32 + Equals(Object) : Boolean + Equals(ParityULong) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : UInt64 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested ParityULongJsonConverter : JsonConverter, IStoredJsonConverterSource + nested ParityULongTypeConverter : TypeConverter + new() + static From(UInt64) : ParityULong + static MaxValue : ParityULong + static MinValue : ParityULong + static Parse(ReadOnlySpan, IFormatProvider) : ParityULong + static Parse(ReadOnlySpan, IFormatProvider) : ParityULong + static Parse(String, IFormatProvider) : ParityULong + static TryParse(ReadOnlySpan, IFormatProvider, out ParityULong) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out ParityULong) : Boolean + static TryParse(String, IFormatProvider, out ParityULong) : Boolean + static op_Equality(ParityULong, ParityULong) : Boolean + static op_GreaterThan(ParityULong, ParityULong) : Boolean + static op_GreaterThanOrEqual(ParityULong, ParityULong) : Boolean + static op_Inequality(ParityULong, ParityULong) : Boolean + static op_LessThan(ParityULong, ParityULong) : Boolean + static op_LessThanOrEqual(ParityULong, ParityULong) : Boolean + extension static ParityULongExtensions.GetValue(ParityULong) : UInt64 + extension static ParityULongExtensions.ValueOrNull(ParityULong?) : UInt64? +ParityUShort + CompareTo(Object) : Int32 + CompareTo(ParityUShort) : Int32 + Equals(Object) : Boolean + Equals(ParityUShort) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : UInt16 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested ParityUShortJsonConverter : JsonConverter, IStoredJsonConverterSource + nested ParityUShortTypeConverter : TypeConverter + new() + static From(UInt16) : ParityUShort + static MaxValue : ParityUShort + static MinValue : ParityUShort + static Parse(ReadOnlySpan, IFormatProvider) : ParityUShort + static Parse(ReadOnlySpan, IFormatProvider) : ParityUShort + static Parse(String, IFormatProvider) : ParityUShort + static TryParse(ReadOnlySpan, IFormatProvider, out ParityUShort) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out ParityUShort) : Boolean + static TryParse(String, IFormatProvider, out ParityUShort) : Boolean + static op_Equality(ParityUShort, ParityUShort) : Boolean + static op_GreaterThan(ParityUShort, ParityUShort) : Boolean + static op_GreaterThanOrEqual(ParityUShort, ParityUShort) : Boolean + static op_Inequality(ParityUShort, ParityUShort) : Boolean + static op_LessThan(ParityUShort, ParityUShort) : Boolean + static op_LessThanOrEqual(ParityUShort, ParityUShort) : Boolean + extension static ParityUShortExtensions.GetValue(ParityUShort) : UInt16 + extension static ParityUShortExtensions.ValueOrNull(ParityUShort?) : UInt16? +ShippableStatus + Equals(Object) : Boolean + Equals(ShippableStatus) : Boolean + GetHashCode() : Int32 + Revalidate() : Result + ToString() : String + ToString(String, IFormatProvider) : String + Value : LocalStatus + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IParsable + implements IValidatedValue + implements IValueObject + implements IValueObjectMaterializer + nested ShippableStatusJsonConverter : JsonConverter, IStoredJsonConverterSource + nested ShippableStatusTypeConverter : TypeConverter + new() + static Create(LocalStatus) : Result + static FromKnownGood(LocalStatus, String) : ShippableStatus + static Parse(String, IFormatProvider) : ShippableStatus + static TryFrom(LocalStatus) : Option + static TryParse(String, IFormatProvider, out ShippableStatus) : Boolean + static op_Equality(ShippableStatus, ShippableStatus) : Boolean + static op_Inequality(ShippableStatus, ShippableStatus) : Boolean + extension static ShippableStatusExtensions.GetValue(ShippableStatus) : LocalStatus + extension static ShippableStatusExtensions.ValueOrNull(ShippableStatus?) : LocalStatus +StageOfOrder + Equals(Object) : Boolean + Equals(StageOfOrder) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : LocalOrder.Stage + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IParsable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested StageOfOrderJsonConverter : JsonConverter, IStoredJsonConverterSource + nested StageOfOrderTypeConverter : TypeConverter + new() + static From(LocalOrder.Stage) : StageOfOrder + static Parse(String, IFormatProvider) : StageOfOrder + static TryParse(String, IFormatProvider, out StageOfOrder) : Boolean + static op_Equality(StageOfOrder, StageOfOrder) : Boolean + static op_Inequality(StageOfOrder, StageOfOrder) : Boolean + extension static StageOfOrderExtensions.GetValue(StageOfOrder) : LocalOrder.Stage + extension static StageOfOrderExtensions.ValueOrNull(StageOfOrder?) : LocalOrder.Stage +StatusLabel + $() : StatusLabel + Equals(Object) : Boolean + Equals(StatusLabel) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : LocalStatus + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IParsable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested StatusLabelJsonConverter : JsonConverter, IStoredJsonConverterSource + nested StatusLabelTypeConverter : TypeConverter + static From(LocalStatus) : StatusLabel + static Parse(String, IFormatProvider) : StatusLabel + static TryParse(String, IFormatProvider, out StatusLabel) : Boolean + static op_Equality(StatusLabel, StatusLabel) : Boolean + static op_Inequality(StatusLabel, StatusLabel) : Boolean + extension static StatusLabelExtensions.GetValue(StatusLabel) : LocalStatus + extension static StatusLabelExtensions.ValueOrNull(StatusLabel) : LocalStatus +StatusOfOrder + Equals(Object) : Boolean + Equals(StatusOfOrder) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : LocalStatus + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IParsable + implements IPlainValueObject + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested StatusOfOrderJsonConverter : JsonConverter, IStoredJsonConverterSource + nested StatusOfOrderTypeConverter : TypeConverter + new() + static From(LocalStatus) : StatusOfOrder + static Parse(String, IFormatProvider) : StatusOfOrder + static TryParse(String, IFormatProvider, out StatusOfOrder) : Boolean + static op_Equality(StatusOfOrder, StatusOfOrder) : Boolean + static op_Inequality(StatusOfOrder, StatusOfOrder) : Boolean + extension static StatusOfOrderExtensions.GetValue(StatusOfOrder) : LocalStatus + extension static StatusOfOrderExtensions.ValueOrNull(StatusOfOrder?) : LocalStatus +TestForeignIdentifier + CompareTo(Object) : Int32 + CompareTo(TestForeignIdentifier) : Int32 + Equals(Object) : Boolean + Equals(TestForeignIdentifier) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Guid + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IParsable + implements IPlainValueObject + implements IProbeIdentifier + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested TestForeignIdentifierJsonConverter : JsonConverter, IStoredJsonConverterSource + nested TestForeignIdentifierTypeConverter : TypeConverter + new() + static From(Guid) : TestForeignIdentifier + static Parse(ReadOnlySpan, IFormatProvider) : TestForeignIdentifier + static Parse(ReadOnlySpan, IFormatProvider) : TestForeignIdentifier + static Parse(String, IFormatProvider) : TestForeignIdentifier + static TryParse(ReadOnlySpan, IFormatProvider, out TestForeignIdentifier) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out TestForeignIdentifier) : Boolean + static TryParse(String, IFormatProvider, out TestForeignIdentifier) : Boolean + static op_Equality(TestForeignIdentifier, TestForeignIdentifier) : Boolean + static op_GreaterThan(TestForeignIdentifier, TestForeignIdentifier) : Boolean + static op_GreaterThanOrEqual(TestForeignIdentifier, TestForeignIdentifier) : Boolean + static op_Inequality(TestForeignIdentifier, TestForeignIdentifier) : Boolean + static op_LessThan(TestForeignIdentifier, TestForeignIdentifier) : Boolean + static op_LessThanOrEqual(TestForeignIdentifier, TestForeignIdentifier) : Boolean + extension static TestForeignIdentifierExtensions.GetValue(TestForeignIdentifier) : Guid + extension static TestForeignIdentifierExtensions.ValueOrNull(TestForeignIdentifier?) : Guid? +TestLocalId + CompareTo(Object) : Int32 + CompareTo(TestLocalId) : Int32 + Equals(Object) : Boolean + Equals(TestLocalId) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Int32 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested TestLocalIdJsonConverter : JsonConverter, IStoredJsonConverterSource + nested TestLocalIdTypeConverter : TypeConverter + new() + static From(Int32) : TestLocalId + static MaxValue : TestLocalId + static MinValue : TestLocalId + static Parse(ReadOnlySpan, IFormatProvider) : TestLocalId + static Parse(ReadOnlySpan, IFormatProvider) : TestLocalId + static Parse(String, IFormatProvider) : TestLocalId + static TryParse(ReadOnlySpan, IFormatProvider, out TestLocalId) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out TestLocalId) : Boolean + static TryParse(String, IFormatProvider, out TestLocalId) : Boolean + static op_Equality(TestLocalId, TestLocalId) : Boolean + static op_GreaterThan(TestLocalId, TestLocalId) : Boolean + static op_GreaterThanOrEqual(TestLocalId, TestLocalId) : Boolean + static op_Inequality(TestLocalId, TestLocalId) : Boolean + static op_LessThan(TestLocalId, TestLocalId) : Boolean + static op_LessThanOrEqual(TestLocalId, TestLocalId) : Boolean + extension static TestLocalIdExtensions.GetValue(TestLocalId) : Int32 + extension static TestLocalIdExtensions.ValueOrNull(TestLocalId?) : Int32? +ThrowingCount + CompareTo(Object) : Int32 + CompareTo(ThrowingCount) : Int32 + Equals(Object) : Boolean + Equals(ThrowingCount) : Boolean + GetHashCode() : Int32 + Revalidate() : Result + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Int32 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IParsable + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValidatedValue + implements IValueObject + implements IValueObjectMaterializer + nested ThrowingCountJsonConverter : JsonConverter, IStoredJsonConverterSource + nested ThrowingCountTypeConverter : TypeConverter + new() + static Create(Int32) : Result + static FromKnownGood(Int32, String) : ThrowingCount + static Parse(ReadOnlySpan, IFormatProvider) : ThrowingCount + static Parse(ReadOnlySpan, IFormatProvider) : ThrowingCount + static Parse(String, IFormatProvider) : ThrowingCount + static TryFrom(Int32) : Option + static TryParse(ReadOnlySpan, IFormatProvider, out ThrowingCount) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out ThrowingCount) : Boolean + static TryParse(String, IFormatProvider, out ThrowingCount) : Boolean + static op_Equality(ThrowingCount, ThrowingCount) : Boolean + static op_GreaterThan(ThrowingCount, ThrowingCount) : Boolean + static op_GreaterThanOrEqual(ThrowingCount, ThrowingCount) : Boolean + static op_Inequality(ThrowingCount, ThrowingCount) : Boolean + static op_LessThan(ThrowingCount, ThrowingCount) : Boolean + static op_LessThanOrEqual(ThrowingCount, ThrowingCount) : Boolean + extension static ThrowingCountExtensions.GetValue(ThrowingCount) : Int32 + extension static ThrowingCountExtensions.ValueOrNull(ThrowingCount?) : Int32? +ThrowingDate + CompareTo(Object) : Int32 + CompareTo(ThrowingDate) : Int32 + Equals(Object) : Boolean + Equals(ThrowingDate) : Boolean + GetHashCode() : Int32 + Revalidate() : Result + ToString() : String + ToString(String, IFormatProvider) : String + Value : LocalDate + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IComparable + implements IComparable + implements IComparisonOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IParsable + implements IValidatedValue + implements IValueObject + implements IValueObjectMaterializer + nested ThrowingDateJsonConverter : JsonConverter, IStoredJsonConverterSource + nested ThrowingDateTypeConverter : TypeConverter + new() + static Create(LocalDate) : Result + static FromKnownGood(LocalDate, String) : ThrowingDate + static Parse(String, IFormatProvider) : ThrowingDate + static TryFrom(LocalDate) : Option + static TryParse(String, IFormatProvider, out ThrowingDate) : Boolean + static op_Equality(ThrowingDate, ThrowingDate) : Boolean + static op_GreaterThan(ThrowingDate, ThrowingDate) : Boolean + static op_GreaterThanOrEqual(ThrowingDate, ThrowingDate) : Boolean + static op_Inequality(ThrowingDate, ThrowingDate) : Boolean + static op_LessThan(ThrowingDate, ThrowingDate) : Boolean + static op_LessThanOrEqual(ThrowingDate, ThrowingDate) : Boolean + extension static ThrowingDateExtensions.GetValue(ThrowingDate) : LocalDate + extension static ThrowingDateExtensions.ValueOrNull(ThrowingDate?) : LocalDate? +ThrowingIsbn + Equals(Object) : Boolean + Equals(ThrowingIsbn) : Boolean + GetHashCode() : Int32 + Revalidate() : Result + ToString() : String + ToString(String, IFormatProvider) : String + Value : ProbeIsbnText + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IParsable + implements IValidatedValue + implements IValueObject + implements IValueObjectMaterializer + nested ThrowingIsbnJsonConverter : JsonConverter, IStoredJsonConverterSource + nested ThrowingIsbnTypeConverter : TypeConverter + new() + static Create(ProbeIsbnText) : Result + static FromKnownGood(ProbeIsbnText, String) : ThrowingIsbn + static Parse(String, IFormatProvider) : ThrowingIsbn + static TryFrom(ProbeIsbnText) : Option + static TryParse(String, IFormatProvider, out ThrowingIsbn) : Boolean + static op_Equality(ThrowingIsbn, ThrowingIsbn) : Boolean + static op_Inequality(ThrowingIsbn, ThrowingIsbn) : Boolean + extension static ThrowingIsbnExtensions.GetValue(ThrowingIsbn) : ProbeIsbnText + extension static ThrowingIsbnExtensions.ValueOrNull(ThrowingIsbn?) : ProbeIsbnText +ThrowingSku + Equals(Object) : Boolean + Equals(ThrowingSku) : Boolean + GetHashCode() : Int32 + Revalidate() : Result + ToString() : String + ToString(String, IFormatProvider) : String + Value : ProbeSkuText + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IParsable + implements IValidatedValue + implements IValueObject + implements IValueObjectMaterializer + nested ThrowingSkuJsonConverter : JsonConverter, IStoredJsonConverterSource + nested ThrowingSkuTypeConverter : TypeConverter + new() + static Create(ProbeSkuText) : Result + static FromKnownGood(ProbeSkuText, String) : ThrowingSku + static Parse(String, IFormatProvider) : ThrowingSku + static TryFrom(ProbeSkuText) : Option + static TryParse(String, IFormatProvider, out ThrowingSku) : Boolean + static op_Equality(ThrowingSku, ThrowingSku) : Boolean + static op_Inequality(ThrowingSku, ThrowingSku) : Boolean + extension static ThrowingSkuExtensions.GetValue(ThrowingSku) : ProbeSkuText + extension static ThrowingSkuExtensions.ValueOrNull(ThrowingSku?) : ProbeSkuText diff --git a/test/CodoMetis.TypeKit.Generators.Tests/GeneratedSurfaceTests.cs b/test/CodoMetis.TypeKit.Generators.Tests/GeneratedSurfaceTests.cs index cc39a700..391ccc88 100644 --- a/test/CodoMetis.TypeKit.Generators.Tests/GeneratedSurfaceTests.cs +++ b/test/CodoMetis.TypeKit.Generators.Tests/GeneratedSurfaceTests.cs @@ -7,13 +7,17 @@ namespace CodoMetis.TypeKit.Generators.Tests; /// -/// The public surface the generators give each probe, compared with GeneratedSurface.verified.txt. +/// The public surface the generators give each probe, and each value object these tests declare for +/// themselves, compared with GeneratedSurface.verified.txt. /// /// /// /// The behaviour tests check what they know to look for. This catches the rest: an aspect change /// that adds, drops or renames a member, an interface or an attribute shows up as a diff in review -/// instead of shipping unnoticed. +/// instead of shipping unnoticed. The tests' own value objects are in it because the shapes a fix was +/// found for live there: 2.1.0's prerelease stopped generating Parse for a wrapped Parse +/// whose result converts implicitly (OverParsedShape), which ApiCompat, seeing only the packages' +/// own surface, let through. /// /// /// On a mismatch the actual surface is written next to the snapshot as @@ -29,7 +33,7 @@ public sealed class GeneratedSurfaceTests public void The_generated_surface_matches_the_snapshot() { var directory = Path.Combine(RepositoryRoot(), "test", "CodoMetis.TypeKit.Generators.Tests"); - var actual = Render(typeof(ProbeId).Assembly); + var actual = Render(typeof(ProbeId).Assembly, 19) + Render(typeof(GeneratedSurfaceTests).Assembly, 70); var expected = File.Exists(Path.Combine(directory, Verified)) ? File.ReadAllText(Path.Combine(directory, Verified)) : ""; if (actual == expected.ReplaceLineEndings("\n")) @@ -42,16 +46,16 @@ public void The_generated_surface_matches_the_snapshot() actual.ShouldBe(expected, $"The generated surface changed. Review {Received} and, if the change is intended, replace {Verified} with it."); } - private static string Render(Assembly assembly) + private static string Render(Assembly assembly, int floor) { var valueObjects = assembly.GetTypes() .Where(type => type.GetInterfaces().Any(i => i.IsGenericType && (i.GetGenericTypeDefinition() == typeof(IValueObject<,>) || i.GetGenericTypeDefinition() == typeof(CodoMetis.TypeKit.ClosedSets.IClosedSet<>)))) .OrderBy(type => type.FullName, StringComparer.Ordinal) .ToList(); - valueObjects.Count.ShouldBeGreaterThanOrEqualTo(19); + valueObjects.Count.ShouldBeGreaterThanOrEqualTo(floor, assembly.GetName().Name); - var text = new StringBuilder(); + var text = new StringBuilder().Append("# ").Append(assembly.GetName().Name).Append('\n'); foreach (var type in valueObjects) { diff --git a/test/CodoMetis.TypeKit.Generators.Tests/SourceGeneratedJsonTests.cs b/test/CodoMetis.TypeKit.Generators.Tests/SourceGeneratedJsonTests.cs index 59edb06e..728d2ce8 100644 --- a/test/CodoMetis.TypeKit.Generators.Tests/SourceGeneratedJsonTests.cs +++ b/test/CodoMetis.TypeKit.Generators.Tests/SourceGeneratedJsonTests.cs @@ -131,8 +131,33 @@ public void A_converter_on_the_options_comes_before_the_wrapped_type_s_own() public void Stored_JSON_is_read_without_the_rules() => JsonSerializer.Deserialize("101", new JsonSerializerOptions(ContextOnly) { Converters = { new StoredJsonConverterFactory() } }) .Value.ShouldBe(101); + + /// + /// The merge-patches page: a source-generated context never sees through a patch, and a value object inside one + /// is read with its own converter, so it keeps its rule where neither it nor its wrapped type is listed. + /// + [Fact] + public void A_value_object_inside_a_patch_is_read_with_its_own_converter() + { + var options = new JsonSerializerOptions { TypeInfoResolver = PatchesOnly.Default }; + PatchesOnly.Default.GetTypeInfo(typeof(ProbeCode)).ShouldBeNull("the control: the context never saw the value object"); + + var read = JsonSerializer.Deserialize("""{"Code":"ABC","Amount":1.5}""", options).ShouldNotBeNull(); + read.Code.Match(code => code.Value, () => "cleared", () => "absent").ShouldBe("ABC"); + read.Amount.Match(amount => amount.Value, () => -1m, () => -2m).ShouldBe(1.5m); + + var refusal = Should.Throw(() => JsonSerializer.Deserialize("""{"Code":"lower"}""", options)); + refusal.Path.ShouldBe("$.Code"); + refusal.Message.ShouldContain(nameof(ProbeCodeFault.NotUpperCase)); + refusal.Message.ShouldNotContain("lower"); + } } +public sealed record CodePatch(Patch Code, Patch Amount); + +[JsonSerializable(typeof(CodePatch))] +internal sealed partial class PatchesOnly : JsonSerializerContext; + /// A wrapped type with a converter of its own, which writes it in lower case. [JsonConverter(typeof(CodeAsLowerConverter))] public readonly struct Code(string text) diff --git a/test/CodoMetis.TypeKit.Generators.Tests/SpanParsingTests.cs b/test/CodoMetis.TypeKit.Generators.Tests/SpanParsingTests.cs index f966ae2d..cc8146b9 100644 --- a/test/CodoMetis.TypeKit.Generators.Tests/SpanParsingTests.cs +++ b/test/CodoMetis.TypeKit.Generators.Tests/SpanParsingTests.cs @@ -30,7 +30,7 @@ public enum ProbeAccess /// /// Span and UTF-8 parsing are generated exactly where the wrapped type has them, and read and refuse /// what Parse(string) reads and refuses, one row per wrapped type of the shape catalogue -/// (shape.wrapped in .claude/probe-axes.md) and per parse strategy. +/// (the wrapped types of the generation shapes, test/oracles/axes.md) and per parse strategy. /// /// /// A value object over got no ISpanParsable, although diff --git a/test/CodoMetis.TypeKit.Oracles.Tests/ApiSurface.cs b/test/CodoMetis.TypeKit.Oracles.Tests/ApiSurface.cs new file mode 100644 index 00000000..8bb093ae --- /dev/null +++ b/test/CodoMetis.TypeKit.Oracles.Tests/ApiSurface.cs @@ -0,0 +1,87 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using CodoMetis.TypeKit.AspNetCore; +using CodoMetis.TypeKit.EntityFrameworkCore; +using CodoMetis.TypeKit.Testing; + +namespace CodoMetis.TypeKit.Oracles.Tests; + +/// +/// The public surface of the shipping libraries, enumerated by reflection: one key per public member declared on a +/// public type, TypeName.Member, nested types joined with + and generic arity dropped +/// (Result.CatchAsync, FaultResponse.WithoutFaultName). +/// +/// +/// +/// Every key must be named by a claim's surfaces or by a waiver in test/oracles/surface-waivers.json, +/// so a public member cannot be added without deciding which oracle holds it, or saying why none should. +/// +/// +/// Overloads collapse into one key. The compiler's grouping types of C# 14 extension blocks are skipped, their +/// members are listed on the static class, as are explicit interface implementations and accessors. The generators +/// and the analyzers are left out: their public surface is what they generate and the diagnostics they report. +/// +/// +internal static class ApiSurface +{ + /// One type per shipping library, so a satellite that is split off is still found. + private static readonly Type[] AssemblyMarkers = + [ + typeof(Option<>), + typeof(FaultResponse), + typeof(ValueObjectConverter<,>), + typeof(global::Serilog.TypeKitSerilogExtensions), + typeof(TypeKitAssertionException), + ]; + + /// Every public member key, sorted ordinally. + internal static IReadOnlyList Members { get; } = Enumerate(); + + /// Compiles a surface pattern: * matches any run of characters, everything else is literal. + internal static Regex Pattern(string glob) => + new("^" + string.Join(".*", glob.Split('*').Select(Regex.Escape)) + "$", RegexOptions.CultureInvariant); + + /// The members a pattern names. + internal static IReadOnlyList Matching(string glob) + { + var pattern = Pattern(glob); + return [.. Members.Where(member => pattern.IsMatch(member))]; + } + + private static List Enumerate() + { + var keys = new SortedSet(StringComparer.Ordinal); + + foreach (var assembly in AssemblyMarkers.Select(marker => marker.Assembly).Distinct()) + foreach (var type in assembly.GetExportedTypes()) + { + if (type.Name.Contains('<') || type.GetCustomAttribute() is not null) continue; + + var typeName = Name(type); + foreach (var member in type.GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)) + { + switch (member) + { + case Type: + continue; // listed as its own type + case MethodInfo { IsSpecialName: true } accessor when !accessor.Name.StartsWith("op_", StringComparison.Ordinal): + continue; // property and event accessors; the property is the member + case MethodInfo { Name: var name } when name.Contains('.') || name.Contains('<'): + continue; // explicit interface implementations and compiler-generated members + } + + keys.Add($"{typeName}.{(member is ConstructorInfo ? ".ctor" : member.Name)}"); + } + } + + return [.. keys]; + } + + private static string Name(Type type) + { + var tick = type.Name.IndexOf('`', StringComparison.Ordinal); + var bare = tick < 0 ? type.Name : type.Name[..tick]; + return type.DeclaringType is { } outer ? Name(outer) + "+" + bare : bare; + } +} diff --git a/test/CodoMetis.TypeKit.Oracles.Tests/CodoMetis.TypeKit.Oracles.Tests.csproj b/test/CodoMetis.TypeKit.Oracles.Tests/CodoMetis.TypeKit.Oracles.Tests.csproj new file mode 100644 index 00000000..d9d8cd6c --- /dev/null +++ b/test/CodoMetis.TypeKit.Oracles.Tests/CodoMetis.TypeKit.Oracles.Tests.csproj @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + diff --git a/test/CodoMetis.TypeKit.Oracles.Tests/OracleCandidateTests.cs b/test/CodoMetis.TypeKit.Oracles.Tests/OracleCandidateTests.cs new file mode 100644 index 00000000..035555af --- /dev/null +++ b/test/CodoMetis.TypeKit.Oracles.Tests/OracleCandidateTests.cs @@ -0,0 +1,41 @@ +using System.Text.RegularExpressions; + +namespace CodoMetis.TypeKit.Oracles.Tests; + +/// +/// The candidate registry (test/oracles/candidates.json) names only registered oracles and tracks every entry +/// to an issue or to a labelled failure (design/adr/0112). A misspelt oracle name would match nothing, and the row +/// would silently keep failing as unknown. +/// +public sealed class OracleCandidateTests +{ + [Fact] + public void Every_candidate_names_a_registered_oracle_and_a_row() + { + var oracles = OracleCatalogue.Oracles.Select(oracle => oracle.Class).ToHashSet(StringComparer.Ordinal); + var problems = new List(); + + foreach (var candidate in Candidates.All) + { + if (!oracles.Contains(candidate.Oracle)) problems.Add($"'{candidate.Row}' names oracle '{candidate.Oracle}', which oracles.json does not register"); + if (candidate.Row.Trim().Length < 3) problems.Add($"a candidate of {candidate.Oracle} has no row text"); + if (candidate.Family.Trim().Length == 0) problems.Add($"'{candidate.Row}' has no family label"); + if (candidate.Method is { } method && !Regex.IsMatch(File.ReadAllText(SourceOf(candidate.Oracle)), $@"\b{Regex.Escape(method)}\s*\(")) + problems.Add($"'{candidate.Row}' is scoped to method '{method}', which {candidate.Oracle} does not declare"); + if (candidate.Issue.Length > 0 && !candidate.Issue.StartsWith('#') && !candidate.Issue.StartsWith("http", StringComparison.Ordinal)) + problems.Add($"'{candidate.Row}' has an issue that is neither '#' nor a URL: '{candidate.Issue}'"); + } + + problems.ShouldBeEmpty(); + } + + [Fact] + public void Candidates_are_unique_per_oracle() => + Candidates.All.GroupBy(candidate => (candidate.Oracle, candidate.Row)).Where(group => group.Count() > 1) + .Select(group => $"{group.Key.Oracle}: '{group.Key.Row}'") + .ShouldBeEmpty("Duplicate candidates."); + + private static string SourceOf(string oracle) => + Directory.EnumerateFiles(Path.Combine(OracleCatalogue.Root, "test"), oracle + ".cs", SearchOption.AllDirectories) + .First(path => !path.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.Ordinal)); +} diff --git a/test/CodoMetis.TypeKit.Oracles.Tests/OracleCatalogue.cs b/test/CodoMetis.TypeKit.Oracles.Tests/OracleCatalogue.cs new file mode 100644 index 00000000..74d0aea7 --- /dev/null +++ b/test/CodoMetis.TypeKit.Oracles.Tests/OracleCatalogue.cs @@ -0,0 +1,191 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; + +namespace CodoMetis.TypeKit.Oracles.Tests; + +/// +/// The files under test/oracles/, loaded once: the claims the documents make, the oracle classes that hold +/// them, the seeds that prove the oracles can fail, the waivers for public members no claim covers, and the known +/// disagreements (design/adr/0112). +/// +/// +/// Everything here is data a maintainer edits by hand, and the Oracle*Tests hold it consistent with the +/// repository: a claim's quote must still be in its document, a surface must still exist, an oracle must still be a +/// test class with a seed. The tests are what make the catalogue a worklist rather than a wish list. +/// +internal static partial class OracleCatalogue +{ + // Declared first: static initializers run in textual order, and the loaders below use it. + private static readonly JsonSerializerOptions Json = new() + { + PropertyNameCaseInsensitive = true, + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true, + }; + + internal static string Root { get; } = FindRoot(); + + internal static string Directory { get; } = Path.Combine(Root, "test", "oracles"); + + internal static IReadOnlyList Claims { get; } = Load>("claims.json"); + + internal static IReadOnlyList Oracles { get; } = Load>("oracles.json"); + + /// Surface patterns no claim names, each with the reason it needs no oracle. + internal static IReadOnlyDictionary SurfaceWaivers { get; } = Load>("surface-waivers.json"); + + internal static IReadOnlyList Seeds { get; } = + [.. new DirectoryInfo(Path.Combine(Directory, "seeds")).EnumerateFiles("*.patch").OrderBy(file => file.Name, StringComparer.Ordinal).Select(Seed.Read)]; + + /// The documents that make promises to consumers; every one must carry at least one claim. + internal static readonly string[] PromiseDocuments = + [ + "README.md", + "docs/json.md", + "docs/text.md", + "docs/merge-patches.md", + "docs/option.md", + "docs/result.md", + "docs/ef-core.md", + "docs/type-constraints.md", + "docs/walkthrough.md", + "src/CodoMetis.TypeKit.AspNetCore/README.md", + "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md", + "src/CodoMetis.TypeKit.Generators/README.md", + "src/CodoMetis.TypeKit.Analyzers/README.md", + "src/CodoMetis.TypeKit.Testing/README.md", + "src/CodoMetis.TypeKit.Serilog/README.md", + ]; + + /// Every test class under test/, a class whose file holds a [Fact] or a [Theory], with its project. + internal static IReadOnlyDictionary TestClasses { get; } = DiscoverTestClasses(); + + /// GitHub's heading slug: lower case, punctuation dropped, spaces to hyphens. + internal static string Slug(string heading) => + Regex.Replace(Regex.Replace(heading.Trim().ToLowerInvariant(), @"[^\p{L}\p{N}\s-]", ""), @"\s", "-"); + + /// Headings of a Markdown file, as slugs, in order. + internal static IReadOnlyList HeadingSlugs(string relativePath) + { + var inCode = false; + var slugs = new List(); + foreach (var line in File.ReadLines(Path.Combine(Root, relativePath))) + { + if (line.StartsWith("```", StringComparison.Ordinal)) inCode = !inCode; + if (inCode) continue; + var match = Regex.Match(line, @"^#{1,6}\s+(.*?)\s*#*\s*$"); + if (match.Success) slugs.Add(Slug(match.Groups[1].Value)); + } + + return slugs; + } + + /// A file's text with every run of whitespace collapsed to one space, for quote matching across line wraps. + internal static string Flattened(string relativePath) => Regex.Replace(File.ReadAllText(Path.Combine(Root, relativePath)), @"\s+", " "); + + private static T Load(string file) + { + var path = Path.Combine(Directory, file); + using var stream = File.OpenRead(path); + return JsonSerializer.Deserialize(stream, Json) ?? throw new InvalidOperationException($"'{path}' is empty."); + } + + private static Dictionary DiscoverTestClasses() + { + var classes = new Dictionary(StringComparer.Ordinal); + var testRoot = Path.Combine(Root, "test"); + var separator = Path.DirectorySeparatorChar; + + foreach (var file in System.IO.Directory.EnumerateFiles(testRoot, "*.cs", SearchOption.AllDirectories)) + { + if (file.Contains($"{separator}obj{separator}", StringComparison.Ordinal) || file.Contains($"{separator}bin{separator}", StringComparison.Ordinal)) continue; + + var text = File.ReadAllText(file); + if (!TestAttribute().IsMatch(text)) continue; + + var project = Path.GetRelativePath(testRoot, file).Split(separator)[0]; + foreach (Match match in ClassDeclaration().Matches(text)) + classes.TryAdd(match.Groups["name"].Value, project); + } + + return classes; + } + + private static string FindRoot() + { + const string marker = "CodoMetis.TypeKit.slnx"; + for (var directory = new DirectoryInfo(AppContext.BaseDirectory); directory is not null; directory = directory.Parent) + { + if (File.Exists(Path.Combine(directory.FullName, marker))) return directory.FullName; + } + + throw new InvalidOperationException($"Could not find '{marker}' above '{AppContext.BaseDirectory}'."); + } + + [GeneratedRegex(@"\[(Fact|Theory)\b")] + private static partial Regex TestAttribute(); + + [GeneratedRegex(@"(?m)^(?:public|internal)\s+(?:sealed\s+|static\s+|partial\s+|abstract\s+)*class\s+(?\w+)")] + private static partial Regex ClassDeclaration(); + + /// A sentence a document states about the library's behaviour, and what holds it. + internal sealed class Claim + { + public string Id { get; init; } = ""; + + /// file#heading-slug: where the sentence is. + public string Source { get; init; } = ""; + + /// The sentence, verbatim, so the catalogue test can find it in the file. + public string Quote { get; init; } = ""; + + /// The restatement as "under conditions, observable result". + [JsonPropertyName("claim")] + public string Statement { get; init; } = ""; + + /// claim, differential, metamorphic, exception or silent-default. + public string Oracle { get; init; } = ""; + + /// The public members a test of the claim calls, as patterns. + public IReadOnlyList Surfaces { get; init; } = []; + + /// The test classes that assert the claim; empty is the worklist. + public IReadOnlyList HeldBy { get; init; } = []; + + /// The source file, before the #. + internal string File => Source.Split('#')[0]; + + /// The heading slug after the #, or null for a claim about the whole file. + internal string? Heading => Source.Contains('#') ? Source[(Source.IndexOf('#') + 1)..] : null; + } + + /// A test class that checks the library against an independent answer over many cases. + internal sealed record Oracle(string Class, string Project, string Kind, string Against); + + /// A patch under seeds/: its header lines, the oracles it names and the files it changes. + internal sealed record Seed(FileInfo File, string Defect, IReadOnlyList Oracles, string Body) + { + internal static Seed Read(FileInfo file) + { + var text = System.IO.File.ReadAllText(file.FullName); + var diffAt = text.IndexOf("diff --git", StringComparison.Ordinal); + var header = diffAt < 0 ? text : text[..diffAt]; + return new Seed( + file, + HeaderValue(header, "Seed"), + HeaderValue(header, "Oracles").Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries), + diffAt < 0 ? "" : text[diffAt..]); + } + + /// The a/ paths the diff changes. + internal IReadOnlyList ChangedFiles => + [.. Regex.Matches(Body, @"^diff --git a/(?\S+) b/", RegexOptions.Multiline).Select(match => match.Groups["path"].Value)]; + + private static string HeaderValue(string header, string key) + { + var match = Regex.Match(header, $@"^{key}:\s*(?.+?)\s*$", RegexOptions.Multiline); + return match.Success ? match.Groups["value"].Value : ""; + } + } +} diff --git a/test/CodoMetis.TypeKit.Oracles.Tests/OracleCatalogueTests.cs b/test/CodoMetis.TypeKit.Oracles.Tests/OracleCatalogueTests.cs new file mode 100644 index 00000000..3fe3a0b8 --- /dev/null +++ b/test/CodoMetis.TypeKit.Oracles.Tests/OracleCatalogueTests.cs @@ -0,0 +1,118 @@ +using System.Text.RegularExpressions; + +namespace CodoMetis.TypeKit.Oracles.Tests; + +/// +/// The claim catalogue (test/oracles/claims.json) stays true to the documents it quotes and to the tests it +/// names (design/adr/0112). +/// +/// +/// A claim is a sentence a consumer-facing document states about the library, in a form a test can decide, with the +/// oracle kind, the public members a test of it calls and the test classes that hold it. The catalogue is only worth +/// keeping if it cannot drift: a reworded sentence stales its claim, a renamed test class is found, a surface that no +/// longer exists is noticed. A claim with an empty heldBy is allowed: that is the worklist. +/// +public sealed class OracleCatalogueTests +{ + /// 150 at the first catalogue (2026-10-28). A floor, not a target: discovery would otherwise pass on an empty array. + private const int KnownClaims = 150; + + private static readonly string[] Kinds = ["claim", "differential", "metamorphic", "exception", "silent-default"]; + + [Fact] + public void The_catalogue_is_not_empty_and_its_ids_are_unique_and_sorted() + { + var ids = OracleCatalogue.Claims.Select(claim => claim.Id).ToList(); + + ids.Count.ShouldBeGreaterThanOrEqualTo(KnownClaims); + ids.Distinct(StringComparer.Ordinal).Count().ShouldBe(ids.Count, "Duplicate claim ids."); + ids.ShouldBe(ids.Order(StringComparer.Ordinal).ToList(), "Claims are sorted by id."); + ids.Where(id => !Regex.IsMatch(id, @"^[A-Z]\d{2}-[a-z0-9-]+$")) + .ShouldBeEmpty("Every id is a letter, two digits, a hyphen and a kebab-case slug."); + } + + [Fact] + public void Every_claim_names_an_oracle_kind() => + OracleCatalogue.Claims.Where(claim => !Kinds.Contains(claim.Oracle)).Select(claim => $"{claim.Id}: '{claim.Oracle}'") + .ShouldBeEmpty("The oracle kind is one of " + string.Join("|", Kinds) + "."); + + [Fact] + public void Every_claim_quotes_its_source_verbatim() + { + var problems = new List(); + var flattened = new Dictionary(StringComparer.Ordinal); + var headings = new Dictionary>(StringComparer.Ordinal); + + foreach (var claim in OracleCatalogue.Claims) + { + if (!File.Exists(Path.Combine(OracleCatalogue.Root, claim.File))) + { + problems.Add($"{claim.Id}: source file '{claim.File}' does not exist"); + continue; + } + + if (claim.Heading is { } heading) + { + if (!headings.TryGetValue(claim.File, out var slugs)) headings[claim.File] = slugs = OracleCatalogue.HeadingSlugs(claim.File); + if (!slugs.Contains(heading)) problems.Add($"{claim.Id}: '{claim.File}' has no heading '#{heading}'"); + } + + if (!flattened.TryGetValue(claim.File, out var text)) flattened[claim.File] = text = OracleCatalogue.Flattened(claim.File); + + var quote = Regex.Replace(claim.Quote, @"\s+", " ").Trim(); + if (quote.Length < 20) + problems.Add($"{claim.Id}: the quote is too short to pin a sentence: '{quote}'"); + else if (!text.Contains(quote, StringComparison.Ordinal)) + problems.Add($"{claim.Id}: '{claim.File}' no longer contains the quoted sentence; reword the claim or retire it: '{quote}'"); + } + + problems.ShouldBeEmpty("Claims out of step with their documents."); + } + + [Fact] + public void Every_claim_names_test_classes_that_exist() + { + OracleCatalogue.TestClasses.Count.ShouldBeGreaterThanOrEqualTo(200, "Too few test classes were discovered under test/."); + + OracleCatalogue.Claims.SelectMany(claim => claim.HeldBy.Select(held => (claim.Id, held))) + .Where(pair => !OracleCatalogue.TestClasses.ContainsKey(pair.held)) + .Select(pair => $"{pair.Id} -> {pair.held}") + .ShouldBeEmpty("heldBy names a test class that does not exist."); + } + + [Fact] + public void Every_claim_names_surfaces_that_exist() => + OracleCatalogue.Claims.SelectMany(claim => claim.Surfaces.Select(surface => (claim.Id, surface))) + .Where(pair => ApiSurface.Matching(pair.surface).Count == 0) + .Select(pair => $"{pair.Id} -> '{pair.surface}'") + .ShouldBeEmpty("A claim names a public member that does not exist (keys are TypeName.Member, nested types with '+', '*' is a wildcard)."); + + [Fact] + public void Every_claim_names_at_least_one_surface() => + OracleCatalogue.Claims.Where(claim => claim.Surfaces.Count == 0).Select(claim => claim.Id) + .ShouldBeEmpty("A claim with no surface cannot be tested."); + + [Fact] + public void Every_promise_document_carries_a_claim() + { + var claimed = OracleCatalogue.Claims.Select(claim => claim.File).ToHashSet(StringComparer.Ordinal); + + OracleCatalogue.PromiseDocuments.Where(document => !claimed.Contains(document)) + .ShouldBeEmpty("A consumer-facing document has no claim in the catalogue."); + } + + /// + /// heldBy may name any test class, but a class named like an oracle, a sweep over generated cases or a probe over + /// the product of consumer settings (test/oracles/axes.md), is registered, so that it has a seed. + /// + [Fact] + public void A_class_named_like_an_oracle_that_holds_a_claim_is_registered() + { + var oracles = OracleCatalogue.Oracles.Select(oracle => oracle.Class).ToHashSet(StringComparer.Ordinal); + + OracleCatalogue.Claims.SelectMany(claim => claim.HeldBy).Distinct() + .Where(held => held.EndsWith("OracleTests", StringComparison.Ordinal) || held.EndsWith("ParityTests", StringComparison.Ordinal) || held.EndsWith("ProbeTests", StringComparison.Ordinal)) + .Where(held => !oracles.Contains(held)) + .ShouldBeEmpty("A class named like an oracle holds a claim but is not in oracles.json, so no seed is required of it."); + } +} diff --git a/test/CodoMetis.TypeKit.Oracles.Tests/OracleSeedTests.cs b/test/CodoMetis.TypeKit.Oracles.Tests/OracleSeedTests.cs new file mode 100644 index 00000000..40070919 --- /dev/null +++ b/test/CodoMetis.TypeKit.Oracles.Tests/OracleSeedTests.cs @@ -0,0 +1,94 @@ +using System.Diagnostics; + +namespace CodoMetis.TypeKit.Oracles.Tests; + +/// +/// Every registered oracle ships a seed, and every seed still plants its defect (design/adr/0112). +/// +/// +/// +/// A test that passes with and without a defect is not a guard. The rule has always been to prove a guard by +/// reverting its fix (.claude/skills/verify-the-guard); a seed is that revert kept as a file. +/// test/oracles/seeds/<defect>.patch changes src/ only, names the oracle classes it fails in an +/// Oracles: header line, and was shown to fail them by oracles seeds. +/// +/// +/// These tests build nothing: an oracle without a seed, a seed that names no oracle, a seed that touches a test, and a +/// seed a refactor has stranded all fail here. Whether a seed is still caught is the local tool's job, a copy, a build +/// of one project and a run of one class per seed, before a release. +/// +/// +public sealed class OracleSeedTests +{ + private const int KnownOracles = 5; + + [Fact] + public void Every_oracle_is_a_test_class_in_its_project() + { + OracleCatalogue.Oracles.Count.ShouldBeGreaterThanOrEqualTo(KnownOracles); + + OracleCatalogue.Oracles + .Where(oracle => !OracleCatalogue.TestClasses.TryGetValue(oracle.Class, out var project) || project != oracle.Project) + .Select(oracle => $"{oracle.Class} (registered in {oracle.Project}, found in {(OracleCatalogue.TestClasses.TryGetValue(oracle.Class, out var found) ? found : "no project")})") + .ShouldBeEmpty("oracles.json names a class that is not a test class in that project."); + } + + [Fact] + public void Every_oracle_has_a_seed() + { + var seeded = OracleCatalogue.Seeds.SelectMany(seed => seed.Oracles).ToHashSet(StringComparer.Ordinal); + + OracleCatalogue.Oracles.Select(oracle => oracle.Class).Where(oracle => !seeded.Contains(oracle)) + .ShouldBeEmpty("An oracle has no seed: add test/oracles/seeds/.patch with an 'Oracles:' line naming it, and show it caught with `oracles seeds`."); + } + + [Fact] + public void Every_seed_names_a_defect_and_registered_oracles() + { + OracleCatalogue.Seeds.ShouldNotBeEmpty("No seeds under test/oracles/seeds/."); + + var oracles = OracleCatalogue.Oracles.Select(oracle => oracle.Class).ToHashSet(StringComparer.Ordinal); + var problems = new List(); + foreach (var seed in OracleCatalogue.Seeds) + { + if (seed.Defect.Length == 0) problems.Add($"{seed.File.Name}: no 'Seed:' header line describing the defect"); + if (seed.Oracles.Count == 0) problems.Add($"{seed.File.Name}: no 'Oracles:' header line naming the classes it fails"); + problems.AddRange(seed.Oracles.Where(oracle => !oracles.Contains(oracle)).Select(oracle => $"{seed.File.Name}: names '{oracle}', which oracles.json does not register")); + if (seed.Body.Length == 0) problems.Add($"{seed.File.Name}: holds no diff"); + } + + problems.ShouldBeEmpty(); + } + + [Fact] + public void Every_seed_touches_only_source() => + OracleCatalogue.Seeds.SelectMany(seed => seed.ChangedFiles.Select(file => (seed.File.Name, file))) + .Where(pair => !pair.file.StartsWith("src/", StringComparison.Ordinal)) + .Select(pair => $"{pair.Name} changes '{pair.file}'") + .ShouldBeEmpty("A seed plants a defect in the library, never in a test."); + + [Fact] + public void Every_seed_applies_to_the_current_tree() + { + var problems = new List(); + foreach (var seed in OracleCatalogue.Seeds) + { + var (exit, output) = Git("apply", "--check", seed.File.FullName); + if (exit != 0) problems.Add($"{seed.File.Name} no longer applies; refresh it against src/: {output.Trim()}"); + } + + problems.ShouldBeEmpty(); + } + + private static (int Exit, string Output) Git(params string[] arguments) + { + var info = new ProcessStartInfo("git") { WorkingDirectory = OracleCatalogue.Root, RedirectStandardOutput = true, RedirectStandardError = true }; + foreach (var argument in arguments) info.ArgumentList.Add(argument); + + using var git = Process.Start(info) ?? throw new InvalidOperationException("git could not be started; the seed checks need it on PATH."); + var error = git.StandardError.ReadToEndAsync(); + var output = git.StandardOutput.ReadToEnd(); + git.WaitForExit(); + return (git.ExitCode, output + error.Result); + } +} diff --git a/test/CodoMetis.TypeKit.Oracles.Tests/OracleSurfaceCoverageTests.cs b/test/CodoMetis.TypeKit.Oracles.Tests/OracleSurfaceCoverageTests.cs new file mode 100644 index 00000000..cc46ca96 --- /dev/null +++ b/test/CodoMetis.TypeKit.Oracles.Tests/OracleSurfaceCoverageTests.cs @@ -0,0 +1,59 @@ +namespace CodoMetis.TypeKit.Oracles.Tests; + +/// +/// Every public member of the shipping libraries is named by a claim's surfaces or waived with a reason, so the API +/// cannot grow without deciding which oracle holds it (design/adr/0112). +/// +/// +/// enumerates the members by reflection, test/oracles/claims.json says which a claim +/// covers, and test/oracles/surface-waivers.json says which need no oracle and why. A waiver that no longer +/// matches an unclaimed member is stale and fails too, so the list shrinks as the claims grow. +/// +public sealed class OracleSurfaceCoverageTests +{ + /// A floor against the enumeration going quiet. + private const int KnownMembers = 300; + + [Fact] + public void The_surface_is_enumerated() + { + ApiSurface.Members.Count.ShouldBeGreaterThanOrEqualTo(KnownMembers); + ApiSurface.Members.ShouldContain("Result.CatchAsync"); + ApiSurface.Members.ShouldContain("Option.Some"); + ApiSurface.Members.ShouldContain("FaultResponse.WithType"); + } + + [Fact] + public void Every_public_member_is_claimed_or_waived() + { + var claimed = Claimed(); + var waived = OracleCatalogue.SurfaceWaivers.Keys.SelectMany(ApiSurface.Matching).ToHashSet(StringComparer.Ordinal); + + ApiSurface.Members.Where(member => !claimed.Contains(member) && !waived.Contains(member)) + .ShouldBeEmpty("These public members are named by no claim and no waiver. Either a document promises something about " + + "them, and the claim goes into test/oracles/claims.json, or they need no oracle, and test/oracles/surface-waivers.json says why."); + } + + [Fact] + public void No_waiver_is_stale() + { + var claimed = Claimed(); + var stale = new List(); + + foreach (var (glob, reason) in OracleCatalogue.SurfaceWaivers) + { + var members = ApiSurface.Matching(glob); + if (members.Count == 0) + stale.Add($"'{glob}' matches no public member"); + else if (members.All(claimed.Contains)) + stale.Add($"'{glob}' matches only members a claim already covers ({members.Count}): remove the waiver"); + if (reason.Trim().Length < 10) + stale.Add($"'{glob}' needs a reason"); + } + + stale.ShouldBeEmpty("Waivers to clean up."); + } + + private static HashSet Claimed() => + OracleCatalogue.Claims.SelectMany(claim => claim.Surfaces).Distinct().SelectMany(ApiSurface.Matching).ToHashSet(StringComparer.Ordinal); +} diff --git a/test/CodoMetis.TypeKit.Oracles.Tests/SeedVerdictTests.cs b/test/CodoMetis.TypeKit.Oracles.Tests/SeedVerdictTests.cs new file mode 100644 index 00000000..aa420722 --- /dev/null +++ b/test/CodoMetis.TypeKit.Oracles.Tests/SeedVerdictTests.cs @@ -0,0 +1,194 @@ +using System.Xml.Linq; + +namespace CodoMetis.TypeKit.Oracles.Tests; + +/// +/// The rule oracles seeds judges a seed by (): a seed is caught when its oracle passes +/// without it and one of its tests fails with it, with a row that is not a known candidate, whatever else shares its +/// message, and never by a failure xunit wrote for a fixture it could not set up or by a run that did not reach the +/// tests (design/adr/0117). +/// +/// +/// The TRX documents here have the shape xunit.v3 4.0.2 writes through --report-xunit-trx, measured on +/// 2026-10-29 with one class per kind of failure: the messages are xunit's own, a fixture's Dispose that throws +/// is a counter and no result, a crashed host writes no file. +/// +public sealed class SeedVerdictTests +{ + private static readonly Candidates.Candidate Unfiled = new("AnyOracle", "row 1:", "a known disagreement", Issue: ""); + + [Fact] + public void A_message_with_a_known_row_and_another_row_catches() => + SeedVerdict.Catches(MessageOf([Unfiled], ["row 1: expected 1, got 2", "row 2: expected 1, got 3"])).ShouldBeTrue(); + + [Fact] + public void A_message_whose_every_row_is_known_does_not_catch() => + SeedVerdict.Catches(MessageOf([Unfiled], ["row 1: expected 1, got 2"])).ShouldBeFalse(); + + /// The other rows come first, so a message cut short still shows one; known rows are what is cut. + [Fact] + public void A_message_cut_short_still_shows_the_row_that_catches() + { + var message = MessageOf([Unfiled], [.. Enumerable.Range(0, 30).Select(i => $"row 1: known variant {i}"), "row 2: expected 1, got 3"], shown: 5); + + message.ShouldContain("… and 26 more"); + SeedVerdict.Catches(message).ShouldBeTrue(); + } + + [Fact] + public void A_message_cut_short_whose_every_row_is_known_does_not_catch() + { + var message = MessageOf([Unfiled], [.. Enumerable.Range(0, 30).Select(i => $"row 1: known variant {i}")], shown: 5); + + message.ShouldContain("… and 25 more"); + SeedVerdict.Catches(message).ShouldBeFalse(); + } + + /// A known row whose text spans lines is still one known row: its continuation has no marker and read as a caught row. + [Fact] + public void A_known_row_spanning_lines_does_not_catch() + { + var unfiled = new Candidates.Candidate("AnyOracle", "text {1,", "a known disagreement", Issue: ""); + var message = MessageOf([unfiled], ["text {1,\n2}: expected {1,2}, refused"]); + + SeedVerdict.Catches(message).ShouldBeFalse(message); + message.ShouldContain("{1,⏎2}"); + } + + /// A seed that makes a known row agree changes what the sweep answers, which is a catch. + [Fact] + public void A_candidate_fixed_catches() => SeedVerdict.Catches(MessageOf([Unfiled], [])).ShouldBeTrue(); + + [Theory] + [InlineData("Shouldly.ShouldAssertException: value should be 1 but was 2")] + [InlineData("Assert.Fail: the text '[known candidate: x]' appears inside a line, not as a row")] + public void Any_other_assertion_catches(string message) => SeedVerdict.Catches(message).ShouldBeTrue(); + + [Fact] + public void A_sweep_with_no_failure_and_no_candidate_passes() => + Should.NotThrow(() => Candidates.Judge([], [], "a sweep")); + + /// What SQL Server 2022 under QEMU wrote for every test of a SQL Server oracle (#151). + private const string DeadSqlServer = + "Collection fixture type 'CodoMetis.TypeKit.EntityFrameworkCore.Tests.SqlServer' threw in InitializeAsync\n" + + "---- DotNet.Testcontainers.Containers.ContainerNotRunningException : Container is not running."; + + // Before Passing: static fields initialize in the order they are written. + private static readonly XNamespace Ns = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010"; + + private static readonly SeedVerdict.Run Passing = Read(Result("Passed"), Result("Passed")); + + /// The defect of #151: a dead fixture failed every test with and without the seed, and the seed read as caught. + [Fact] + public void A_fixture_that_fails_to_initialize_is_an_error_with_or_without_the_seed() + { + var dead = Read(Result("Failed", DeadSqlServer), Result("Failed", DeadSqlServer)); + + dead.Error.ShouldNotBeNull().ShouldContain("2 of 2 tests failed in setup"); + SeedVerdict.Judge(dead, dead).ShouldStartWith("ERROR without the seed"); + SeedVerdict.Judge(Passing, dead).ShouldStartWith("ERROR with the seed"); + } + + /// A seed that breaks a fixture is not credited either: only a test of the oracle's own catches. + [Fact] + public void A_fixture_failure_beside_a_caught_test_is_an_error() + { + var seeded = Read(Result("Failed", "Shouldly.ShouldAssertException : value should be 1 but was 2"), Result("Failed", DeadSqlServer)); + + SeedVerdict.Judge(Passing, seeded).ShouldStartWith("ERROR with the seed"); + } + + /// Every message xunit.v3.core 4.0.2 holds for a test it could not set up. + [Theory] + [InlineData(DeadSqlServer)] + [InlineData("Class fixture type 'Probe.ThrowingCtorFixture' threw in its constructor\n---- System.InvalidOperationException : fixture ctor threw")] + [InlineData("Class fixture type 'Probe.ThrowingInitFixture' threw in InitializeAsync\n---- System.InvalidOperationException : threw")] + [InlineData("Assembly fixture type 'Probe.Fixture' threw in InitializeAsync\n---- System.Exception : threw")] + [InlineData("Collection fixture type 'Probe.Fixture' had one or more unresolved constructor arguments: Probe.Other other")] + [InlineData("Class fixture type 'Probe.Fixture' may only define a single public constructor.")] + [InlineData("The following constructor parameters did not have matching fixture data: SqlServer server")] + [InlineData("Cannot create type 'Probe.Fixture' due to missing constructor arguments: Probe.Other")] + public void A_message_xunit_writes_for_a_fixture_it_could_not_set_up_is_setup(string message) => + SeedVerdict.IsSetupFailure(message).ShouldBeTrue(); + + /// An assertion, an exception from the code a test calls, and a sweep's verdict are the test's own failures. + [Theory] + [InlineData("Shouldly.ShouldAssertException : [Fact] public void Shouldly_fails1\n should be\n2\n but was\n1")] + [InlineData("Assert.Equal() Failure: Values differ\nExpected: 1\nActual: 2")] + [InlineData("System.InvalidOperationException : product threw")] + [InlineData("a sweep: 3 of 170 rows disagree\n(shape: TptRoot, kind: \"ushort\"): the key reads back as 0")] + [InlineData("row 1: the Collection fixture type 'X' threw in InitializeAsync, quoted by a test")] + public void A_failure_in_the_test_itself_is_not_setup(string message) => SeedVerdict.IsSetupFailure(message).ShouldBeFalse(); + + [Fact] + public void An_oracle_that_fails_without_the_seed_is_an_error() + { + var failing = Read(Result("Passed"), Result("Failed", "Assert.Equal() Failure: Values differ")); + + SeedVerdict.Judge(failing, failing).ShouldBe("ERROR 1 of 2 tests fail without the seed, e.g. Probe.Oracle.T1"); + } + + [Fact] + public void A_test_failing_only_with_the_seed_catches() => + SeedVerdict.Judge(Passing, Read(Result("Passed"), Result("Failed", "Assert.Equal() Failure: Values differ"))) + .ShouldBe("CAUGHT 1 of 2 tests fail, e.g. Probe.Oracle.T1"); + + [Fact] + public void An_oracle_that_passes_with_the_seed_misses_it() => + SeedVerdict.Judge(Passing, Passing).ShouldBe("MISSED all 2 tests pass with the defect planted"); + + /// An unfiled candidate fails its sweep without a seed; the baseline passes, and the seed is judged by its other rows. + [Fact] + public void A_baseline_failing_only_on_known_candidates_passes() + { + var known = Read(Result("Passed"), Result("Failed", MessageOf([Unfiled], ["row 1: expected 1, got 2"]))); + var seeded = Read(Result("Passed"), Result("Failed", MessageOf([Unfiled], ["row 1: expected 1, got 2", "row 2: expected 1, got 3"]))); + + known.Error.ShouldBeNull(); + SeedVerdict.Judge(known, known).ShouldBe("MISSED 1 of 2 tests fail, every row a known candidate"); + SeedVerdict.Judge(known, seeded).ShouldStartWith("CAUGHT"); + } + + /// A fixture whose Dispose throws fails no test; the TRX counts it as an error outside any result. + [Fact] + public void An_error_counted_outside_any_test_is_an_error() => + SeedVerdict.Read(2, Trx([Result("Passed")], error: 1), "Probe.Oracle").Error.ShouldNotBeNull().ShouldContain("1 error outside any test"); + + /// A host that crashes writes no TRX; one that exits otherwise than 0 or 2 did not finish its tests. + [Theory] + [InlineData(134, false)] + [InlineData(7, true)] + [InlineData(3, true)] + public void A_host_that_did_not_finish_is_an_error(int exitCode, bool wroteResults) => + SeedVerdict.Read(exitCode, wroteResults ? Trx([Result("Failed", "Assert.Equal() Failure: Values differ")]) : null, "Probe.Oracle") + .Error.ShouldNotBeNull().ShouldContain($"exited {exitCode}"); + + [Fact] + public void A_run_whose_every_test_is_skipped_is_an_error() => + SeedVerdict.Read(0, Trx([Result("NotExecuted"), Result("NotExecuted")]), "Probe.Oracle").Error.ShouldBe("no test of Probe.Oracle ran"); + + private static SeedVerdict.Run Read(params XElement[] results) => + SeedVerdict.Read(results.Any(result => (string?)result.Attribute("outcome") == "Failed") ? 2 : 0, Trx(results), "Probe.Oracle"); + + private static XElement Result(string outcome, string? message = null) => + new(Ns + "UnitTestResult", new XAttribute("testName", "?"), new XAttribute("outcome", outcome), + message is null ? null : new XElement(Ns + "Output", new XElement(Ns + "ErrorInfo", new XElement(Ns + "Message", message), new XElement(Ns + "StackTrace", " at Probe.Oracle.T()")))); + + private static XDocument Trx(IReadOnlyList results, int error = 0) + { + var named = results.Select(result => new XElement(result)).ToList(); + for (var i = 0; i < named.Count; i++) named[i].SetAttributeValue("testName", $"Probe.Oracle.T{i}"); + + int Count(string outcome) => named.Count(result => (string?)result.Attribute("outcome") == outcome); + return new XDocument(new XElement(Ns + "TestRun", + new XElement(Ns + "Results", named), + new XElement(Ns + "ResultSummary", new XAttribute("outcome", Count("Failed") > 0 || error > 0 ? "Failed" : "Completed"), + new XElement(Ns + "Counters", + new XAttribute("total", named.Count), new XAttribute("executed", Count("Passed") + Count("Failed")), + new XAttribute("passed", Count("Passed")), new XAttribute("failed", Count("Failed")), new XAttribute("error", error), + new XAttribute("timeout", 0), new XAttribute("aborted", 0), new XAttribute("notExecuted", Count("NotExecuted")), new XAttribute("disconnected", 0))))); + } + + private static string MessageOf(IReadOnlyList known, IReadOnlyList failures, int shown = 25) => + Should.Throw(() => Candidates.Judge(known, failures, "a sweep", shown)).Message; +} diff --git a/test/CodoMetis.TypeKit.PrHygiene.Tests/CodoMetis.TypeKit.PrHygiene.Tests.csproj b/test/CodoMetis.TypeKit.PrHygiene.Tests/CodoMetis.TypeKit.PrHygiene.Tests.csproj new file mode 100644 index 00000000..a0a4c5c4 --- /dev/null +++ b/test/CodoMetis.TypeKit.PrHygiene.Tests/CodoMetis.TypeKit.PrHygiene.Tests.csproj @@ -0,0 +1,18 @@ + + + + + + + <_Parameter1>ToolOutput + <_Parameter2>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)../../tools/PrHygiene/bin/$(Configuration)/$(TargetFramework)/')) + + + + + + + diff --git a/test/CodoMetis.TypeKit.PrHygiene.Tests/HygieneTests.cs b/test/CodoMetis.TypeKit.PrHygiene.Tests/HygieneTests.cs new file mode 100644 index 00000000..eb0fac60 --- /dev/null +++ b/test/CodoMetis.TypeKit.PrHygiene.Tests/HygieneTests.cs @@ -0,0 +1,179 @@ +using System.Text.Json; + +namespace CodoMetis.TypeKit.PrHygiene.Tests; + +/// +/// The pull requests the check must pass and the ones it must fail, one rule at a time. Each case writes the +/// event payload GitHub would and runs the built tool on it as its own process, the way the workflow does. +/// Each failing case starts from , which passes, and breaks one thing, so a case that +/// fails proves the rule it names; each rule was also proved by deleting it from the tool and watching its +/// cases fail. +/// +public sealed class HygieneTests(HygieneTests.Sandbox sandbox) : IClassFixture +{ + public sealed class Sandbox : IDisposable + { + public ToolSandbox Tool { get; } = new("PrHygiene", "pr-hygiene"); + + public void Dispose() => Tool.Dispose(); + } + + private sealed record Pr( + string[] Labels, + string? Body = "Closes #12", + string Head = "fix/serilog-max-length", + string Base = "release/2.3.0", + string? Milestone = "2.3.0", + string Author = "CaffeinatedCoder"); + + private static readonly Pr InOrder = new(["bug", "severity/high", "package/serilog"]); + + private (int Code, string Output) Check(Pr pr) + { + var payload = new + { + action = "opened", + pull_request = new + { + user = new { login = pr.Author }, + labels = pr.Labels.Select(name => new { name }).ToArray(), + body = pr.Body, + head = new { @ref = pr.Head }, + @base = new { @ref = pr.Base }, + milestone = pr.Milestone is null ? null : new { title = pr.Milestone }, + }, + }; + var file = Path.Combine(sandbox.Tool.Work, $"{Guid.NewGuid():N}.json"); + File.WriteAllText(file, JsonSerializer.Serialize(payload)); + var (code, output, _) = sandbox.Tool.Run(arguments: file); + return (code, output); + } + + private void Passes(Pr pr) + { + var (code, output) = Check(pr); + code.ShouldBe(0, output); + output.ShouldNotContain("::error::"); + } + + private void Fails(Pr pr, string problem) + { + var (code, output) = Check(pr); + code.ShouldBe(1, output); + output.ShouldContain($"::error::{problem}", customMessage: output); + } + + [Fact] + public void A_pull_request_in_order_passes() => Passes(InOrder); + + [Theory] + [InlineData("feat/cause-on-fault", "enhancement")] + [InlineData("docs/plain-voice", "documentation")] + [InlineData("test/variant-catalogue", "tests")] + [InlineData("build/pr-hygiene", "chore")] + [InlineData("refactor/split-plan", "chore")] + [InlineData("spike/mutation-base", "chore")] + public void Each_branch_type_passes_with_its_label(string head, string type) => Passes(InOrder with { Head = head, Labels = [type] }); + + [Fact] + public void A_release_into_main_passes_with_its_milestone() => + Passes(InOrder with { Head = "release/2.3.0", Base = "main", Labels = ["chore", "area/release"], Body = "Closes #70" }); + + [Fact] + public void Dependabot_is_not_checked() => + Passes(new Pr([], Body: null, Head: "dependabot/github_actions/actions-abc", Base: "main", Milestone: null, Author: "dependabot[bot]")); + + [Fact] + public void No_type_label_fails() => Fails(InOrder with { Labels = ["package/serilog"] }, "Needs exactly one type label"); + + [Fact] + public void Two_type_labels_fail() => Fails(InOrder with { Labels = ["bug", "chore", "severity/low"] }, "Needs exactly one type label (bug, enhancement, documentation, tests, chore); it has 2: bug, chore."); + + [Fact] + public void A_bug_without_a_severity_fails() => Fails(InOrder with { Labels = ["bug"] }, "A bug needs exactly one severity/* label"); + + [Fact] + public void A_bug_with_two_severities_fails() => Fails(InOrder with { Labels = ["bug", "severity/high", "severity/low"] }, "A bug needs exactly one severity/* label (critical, high, medium, low); it has 2."); + + [Fact] + public void Silent_on_no_bug_fails() => Fails(InOrder with { Head = "feat/x", Labels = ["enhancement", "silent"] }, "Only a bug is silent"); + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("Refs #12")] + [InlineData("Closes the gap in #12")] + [InlineData("Closes #")] + [InlineData("Discloses #12")] + public void A_description_without_a_closing_reference_fails(string? body) => + Fails(InOrder with { Body = body }, "The description needs a closing reference to its issue"); + + [Theory] + [InlineData("closes #12")] + [InlineData("Fixes: #3")] + [InlineData("Problem first.\n\nResolved CaffeinatedCoder/CodoMetis.TypeKit#4")] + [InlineData("FIXED #5, close #6")] + public void Each_closing_keyword_passes(string body) => Passes(InOrder with { Body = body }); + + [Theory] + [InlineData("claude/quirky-panini")] + [InlineData("pr-hygiene")] + [InlineData("fix/")] + [InlineData("/x")] + public void A_branch_not_named_type_slash_slug_fails(string head) => Fails(InOrder with { Head = head }, $"The branch '{head}' is not named /"); + + [Fact] + public void A_branch_whose_type_disagrees_with_the_label_fails() => + Fails(InOrder with { Head = "feat/cause-on-fault" }, "The branch 'feat/cause-on-fault' is a feat branch, which is labelled enhancement, not bug."); + + [Fact] + public void A_pull_request_into_a_release_without_a_milestone_fails() => + Fails(InOrder with { Milestone = null }, "A pull request into 2.3.0 has the milestone 2.3.0; it has none."); + + [Fact] + public void A_pull_request_into_a_release_with_another_milestone_fails() => + Fails(InOrder with { Milestone = "2.4.0" }, "A pull request into 2.3.0 has the milestone 2.3.0; it has '2.4.0'."); + + [Fact] + public void A_release_into_main_without_its_milestone_fails() => + Fails(InOrder with { Head = "release/2.3.0", Base = "main", Labels = ["chore"], Milestone = null }, "A pull request into 2.3.0 has the milestone 2.3.0; it has none."); + + [Fact] + public void A_topic_into_main_needs_no_milestone() => Passes(InOrder with { Base = "main", Milestone = null }); + + [Theory] + [InlineData("2.3.0")] + [InlineData("2.2.1")] + public void A_breaking_change_into_a_minor_or_a_patch_fails(string version) => + Fails(InOrder with { Base = $"release/{version}", Milestone = version, Labels = ["bug", "severity/high", "breaking"] }, + $"A breaking change needs a major, and {version} is not one"); + + [Fact] + public void A_breaking_change_into_a_major_passes() => + Passes(InOrder with { Base = "release/3.0.0", Milestone = "3.0.0", Labels = ["bug", "severity/high", "breaking"] }); + + [Fact] + public void Every_problem_is_reported_at_once() + { + var (code, output) = Check(new Pr([], Body: null, Head: "claude/x", Milestone: null)); + + code.ShouldBe(1); + output.Split('\n').Count(line => line.StartsWith("::error::", StringComparison.Ordinal)).ShouldBe(4, output); + } + + [Fact] + public void A_payload_without_a_pull_request_is_a_usage_error() + { + var file = Path.Combine(sandbox.Tool.Work, "push.json"); + File.WriteAllText(file, """{"ref":"refs/heads/main"}"""); + + sandbox.Tool.Run(arguments: file).Code.ShouldBe(2); + } + + [Fact] + public void A_missing_payload_is_a_usage_error() + { + sandbox.Tool.Run(arguments: Path.Combine(sandbox.Tool.Work, "absent.json")).Code.ShouldBe(2); + sandbox.Tool.Run().Code.ShouldBe(2); + } +} diff --git a/test/CodoMetis.TypeKit.PrHygiene.Tests/StaleToolTests.cs b/test/CodoMetis.TypeKit.PrHygiene.Tests/StaleToolTests.cs new file mode 100644 index 00000000..9b542b78 --- /dev/null +++ b/test/CodoMetis.TypeKit.PrHygiene.Tests/StaleToolTests.cs @@ -0,0 +1,30 @@ +namespace CodoMetis.TypeKit.PrHygiene.Tests; + +/// +/// A check built from other sources than the checkout holds says so and checks nothing: exit 3, the +/// difference, and the command that rebuilds it. A stale check would pass or fail pull requests by rules +/// nobody can read in the checkout. +/// +public sealed class StaleToolTests +{ + [Fact] + public void A_tool_built_from_its_sources_runs() + { + using var sandbox = new ToolSandbox("PrHygiene", "pr-hygiene"); + + sandbox.Run().Code.ShouldBe(2, "a usage error, so the tool ran"); + } + + [Fact] + public void A_changed_source_stops_it() + { + using var sandbox = new ToolSandbox("PrHygiene", "pr-hygiene"); + File.AppendAllText(Path.Combine(sandbox.Repo, "tools", "PrHygiene", "Program.cs"), "\n// edited\n"); + + var (code, output, _) = sandbox.Run(); + + code.ShouldBe(3); + output.ShouldStartWith("TOOL STALE: tools/PrHygiene was built from other sources than the checkout holds (tools/PrHygiene/Program.cs differs)"); + output.ShouldContain("rebuild it: dotnet build tools/PrHygiene -c "); + } +} diff --git a/test/CodoMetis.TypeKit.Prober.Tests/ClaimsFileTests.cs b/test/CodoMetis.TypeKit.Prober.Tests/ClaimsFileTests.cs deleted file mode 100644 index c7fa558b..00000000 --- a/test/CodoMetis.TypeKit.Prober.Tests/ClaimsFileTests.cs +++ /dev/null @@ -1,159 +0,0 @@ -using System.Text.Json.Nodes; -using Prober; - -namespace CodoMetis.TypeKit.Prober.Tests; - -/// -/// claims.json is read against its schema, and each exclusion carries the reason a reviewer reads (decision 102): -/// a field nobody reads is refused, not kept, and a publish rewrites the file only where it changed. -/// -public sealed class ClaimsFileTests : IDisposable -{ - private readonly Fixture fx = new(); - public void Dispose() => fx.Dispose(); - - private void Claims(JsonObject claim) - { - var c = new JsonObject { ["id"] = "C1", ["source"] = "README.md#a", ["surfaces"] = new JsonArray("s") }; - foreach (var (key, value) in claim) c[key] = value?.DeepClone(); - fx.Write("tools/probe/corpus/claims.json", new JsonArray(c).ToJsonString()); - } - - private static JsonArray Pairs(params (string A, string B)[] pairs) => new([.. pairs.Select(p => (JsonNode)new JsonArray(p.A, p.B))]); - - [Theory] - [InlineData(null, "claim C1: exclude[0].reason is required")] - [InlineData("", "claim C1: exclude[0].reason does not match")] - [InlineData(" ", "claim C1: exclude[0].reason does not match")] - public void An_exclusion_without_a_reason_is_refused_naming_the_claim(string? reason, string expected) - { - var exclusion = new JsonObject { ["pairs"] = Pairs(("x=1", "y=2")) }; - if (reason is not null) exclusion["reason"] = reason; - Claims(new JsonObject { ["exclude"] = new JsonArray(exclusion) }); - var (exit, _, error) = fx.Ledger("universe", "--base", "HEAD"); - exit.ShouldBe(1); - error.ShouldContain(expected); - } - - [Fact] - public void A_bare_pair_is_refused_as_an_exclusion_without_its_reason() - { - Claims(new JsonObject { ["exclude"] = Pairs(("x=1", "y=2")) }); - var (exit, _, error) = fx.Ledger("universe", "--base", "HEAD"); - exit.ShouldBe(1); - error.ShouldContain("claim C1: exclude[0] is not an object"); - } - - [Theory] - [InlineData("excludeReasons")] - [InlineData("excludeWhy")] - public void A_reason_field_of_its_own_is_refused(string field) - { - Claims(new JsonObject - { - ["exclude"] = new JsonArray(new JsonObject { ["reason"] = "No row holds both.", ["pairs"] = Pairs(("x=1", "y=2")) }), - [field] = new JsonArray(new JsonObject { ["match"] = "x=1 x y=2", ["reason"] = "No row holds both." }), - }); - var (exit, _, error) = fx.Ledger("universe", "--base", "HEAD"); - exit.ShouldBe(1); - error.ShouldContain($"claim C1: {field} is not a field the schema has"); - } - - [Fact] - public void A_field_of_its_own_in_an_exclusion_is_refused() - { - Claims(new JsonObject - { - ["exclude"] = new JsonArray(new JsonObject { ["reason"] = "No row holds both.", ["why"] = "twice", ["pairs"] = Pairs(("x=1", "y=2")) }), - }); - var (exit, _, error) = fx.Ledger("universe", "--base", "HEAD"); - exit.ShouldBe(1); - error.ShouldContain("claim C1: exclude[0].why is not a field the schema has"); - } - - [Theory] - [InlineData("\n")] - [InlineData("\r\n")] - public void The_universe_shows_each_exclusion_with_its_reason(string newLine) - { - Claims(new JsonObject - { - ["exclude"] = new JsonArray(new JsonObject { ["reason"] = "No row holds x=1 with y=2.", ["pairs"] = Pairs(("y=2", "x=1")) }), - }); - var (exit, output, error) = fx.LedgerEnding([], newLine, "universe", "--base", "HEAD"); - exit.ShouldBe(0, error); - output.ReplaceLineEndings("\n").ShouldContain("C1: 2 axes, 3 pairs\n exclude 1 pair(s), first x=1 y=2: No row holds x=1 with y=2.\n"); - } - - [Theory] - [InlineData("\n")] - [InlineData("\r\n")] - public void The_universe_shows_each_narrowing_with_its_quote(string newLine) - { - Claims(new JsonObject { ["narrow"] = new JsonObject { ["y"] = "The claim." } }); - var (exit, output, error) = fx.LedgerEnding([], newLine, "universe", "--base", "HEAD"); - exit.ShouldBe(0, error); - output.ReplaceLineEndings("\n").ShouldContain("C1: 1 axes, 0 pairs\n narrow y: \"The claim.\"\n"); - } - - [Theory] - [InlineData("\n")] - [InlineData("\r\n")] - public void A_publish_that_changes_nothing_keeps_the_file_as_it_is(string staged) - { - // The form publish writes, by hand: a reason with a backtick, a plus and angle brackets, all literal, and \n line - // ends, whatever this source file was checked out with. The staged copy may come with either line end. - var tracked = """ - [ - { - "id": "C1", - "source": "README.md#a", - "surfaces": [ - "s" - ], - "exclude": [ - { - "reason": "A `+` row and an Option row cannot meet.", - "pairs": [ - [ - "x=1", - "y=2" - ] - ] - } - ] - } - ] - - """.ReplaceLineEndings("\n"); - fx.Write("tools/probe/corpus/claims.json", tracked); - fx.Commit("claims"); - fx.Write(".claude/agent-memory-local/prober/branches/main/staging/claims.json", tracked.ReplaceLineEndings(staged)); - var (exit, _, error) = fx.Ledger("publish"); - exit.ShouldBe(0, error); - fx.Sh("git", "diff", "--exit-code", "--", "tools/probe/corpus/claims.json"); - File.ReadAllText(Path.Combine(fx.Root, "tools/probe/corpus/claims.json")).ShouldBe(tracked, "byte for byte: the escapes and the line ends are the ones publish writes"); - } - - [Theory] - [InlineData("\n")] - [InlineData("\r\n")] - public void The_corpus_claims_match_their_schema_in_the_form_publish_writes(string checkedOut) - { - var root = AppContext.BaseDirectory; - while (!File.Exists(Path.Combine(root, "CodoMetis.TypeKit.slnx"))) root = Path.GetDirectoryName(root) ?? throw new InvalidOperationException("no CodoMetis.TypeKit.slnx above the test output"); - var path = Path.Combine(root, "tools/probe/corpus/claims.json"); - var claims = Ledger.ReadClaims(path, "tools/probe/corpus/claims.json"); - claims.ShouldNotBeEmpty(); - // Git may check the file out with either line end (core.autocrlf on Windows); what it stores, and diffs, has \n. - var text = File.ReadAllText(path).ReplaceLineEndings(checkedOut); - Files.Format(JsonNode.Parse(text)!).ShouldBe(text.ReplaceLineEndings("\n"), "claims.json is in the form `prober publish` writes, so a publish diffs only what it changes"); - } - - [Fact] - public void A_schema_keyword_the_check_does_not_read_is_an_error_not_a_pass() - { - var schema = new JsonObject { ["type"] = "string", ["maxLength"] = 3 }; - Should.Throw(() => SchemaCheck.Problems("abcd", schema)).Message.ShouldContain("maxLength"); - } -} diff --git a/test/CodoMetis.TypeKit.Prober.Tests/CodoMetis.TypeKit.Prober.Tests.csproj b/test/CodoMetis.TypeKit.Prober.Tests/CodoMetis.TypeKit.Prober.Tests.csproj deleted file mode 100644 index 2126c5f2..00000000 --- a/test/CodoMetis.TypeKit.Prober.Tests/CodoMetis.TypeKit.Prober.Tests.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - <_Parameter1>ToolOutput - <_Parameter2>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)../../tools/Prober/bin/$(Configuration)/$(TargetFramework)/')) - - - - diff --git a/test/CodoMetis.TypeKit.Prober.Tests/CoveringTests.cs b/test/CodoMetis.TypeKit.Prober.Tests/CoveringTests.cs deleted file mode 100644 index 0a824ae1..00000000 --- a/test/CodoMetis.TypeKit.Prober.Tests/CoveringTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Prober; - -namespace CodoMetis.TypeKit.Prober.Tests; - -/// -/// The covering array never puts an excluded pair or one no obligation wants in a row, not even to fill an axis whose every -/// value would bring one (#54 and #59 saw `rows` place them): such an axis is left out of that row, and an axis with no wanted -/// pair at all is left out of every row. -/// -public sealed class CoveringTests -{ - private static Inapplicability Declared(string cite, params (string Axis, string[] Values)[] axes) => - new(axes.ToDictionary(a => a.Axis, a => a.Values.ToList(), StringComparer.Ordinal), cite); - - private static Claim Over(string[] axes, IEnumerable? excluded = null, params Inapplicability[] declared) => new() - { - Id = "C1", Source = "README.md#a", Surfaces = ["s"], Watched = [], Excluded = [.. excluded ?? []], - Obligations = declared.Length == 0 ? [] : [new("keys", "The claim.", new SortedDictionary>(StringComparer.Ordinal)) { Inapplicable = declared }], - Axes = new(axes.ToDictionary(a => a, _ => new List { "1", "2" }), StringComparer.Ordinal), - }; - - private static bool Holds(IReadOnlyDictionary row, Pair pair) => - new[] { pair.A, pair.B }.All(item => row.TryGetValue(item[..item.IndexOf('=', StringComparison.Ordinal)], out var v) && v == item[(item.IndexOf('=', StringComparison.Ordinal) + 1)..]); - - private static IEnumerable Shown(IEnumerable pairs) => pairs.Select(p => p.ToString()).Order(StringComparer.Ordinal); - - [Fact] - public void An_axis_whose_every_value_brings_an_excluded_pair_is_left_out_of_the_row() - { - // x=1 goes with no y (as shape.containing goes with no shape.position=root): a row holding x=1 holds no y. - Pair[] excluded = [new("x=1", "y=1"), new("x=1", "y=2")]; - var claim = Over(["x", "y", "z"], excluded); - for (var seed = 1; seed <= 20; seed++) - { - var rows = Covering.Rows(claim, claim.Pairs(), 2000, seed); - rows.ShouldNotContain(r => excluded.Any(p => Holds(r.Row, p)), $"seed {seed}"); - rows.Where(r => r.Row["x"] == "1").ShouldAllBe(r => !r.Row.ContainsKey("y"), $"seed {seed}"); - Shown(rows.SelectMany(r => r.Pairs)).ShouldBe(Shown(claim.Pairs()), $"seed {seed}: every pair once, x=1 with z among them"); - } - } - - [Fact] - public void An_axis_whose_every_value_brings_a_pair_no_obligation_wants_is_left_out_of_the_row() - { - var claim = Over(["x", "y", "z"], null, Declared("record:0001", ("x", ["1"]), ("y", ["1", "2"]))); - var dead = claim.DeclaredInapplicable().Keys.ToList(); - dead.Count.ShouldBe(2); - for (var seed = 1; seed <= 20; seed++) - { - var rows = Covering.Rows(claim, claim.PairsWanted(), 2000, seed); - rows.ShouldNotContain(r => dead.Any(p => Holds(r.Row, p)), $"seed {seed}"); - Shown(rows.SelectMany(r => r.Pairs)).ShouldBe(Shown(claim.PairsWanted()), $"seed {seed}"); - } - } - - [Fact] - public void An_axis_with_no_wanted_pair_is_in_no_row() - { - // every pair of z is excluded (as shape.refused-kind is where the refusal is not the claim): rows over x and y alone. - var claim = Over(["x", "y", "z"], [.. from x in new[] { "x=1", "x=2", "y=1", "y=2" } from z in new[] { "z=1", "z=2" } select new Pair(x, z)]); - for (var seed = 1; seed <= 5; seed++) - { - var rows = Covering.Rows(claim, claim.Pairs(), 2000, seed); - rows.ShouldAllBe(r => !r.Row.ContainsKey("z"), $"seed {seed}"); - Shown(rows.SelectMany(r => r.Pairs)).ShouldBe(["x=1 y=1", "x=1 y=2", "x=2 y=1", "x=2 y=2"], $"seed {seed}"); - } - } - - [Fact] - public void A_seeded_row_leaves_out_an_axis_no_value_of_which_can_join_it() - { - // c=1 goes with no d: the row seeded from a=1 c=1 is a, b and c. - var claim = Over(["a", "b", "c", "d"], [new("c=1", "d=1"), new("c=1", "d=2")]); - Covering.Seeded(claim, new Pair("a=1", "c=1"), claim.DeclaredInapplicable()) - .ShouldBe(new SortedDictionary(StringComparer.Ordinal) { ["a"] = "1", ["b"] = "1", ["c"] = "1" }); - Covering.Seeded(claim, new Pair("a=1", "b=2"), claim.DeclaredInapplicable()) - .ShouldBe(new SortedDictionary(StringComparer.Ordinal) { ["a"] = "1", ["b"] = "2", ["c"] = "1" }, - "c=1 is c's first value, and d then has none"); - } -} diff --git a/test/CodoMetis.TypeKit.Prober.Tests/DeclaredApplicabilityTests.cs b/test/CodoMetis.TypeKit.Prober.Tests/DeclaredApplicabilityTests.cs deleted file mode 100644 index aae5e585..00000000 --- a/test/CodoMetis.TypeKit.Prober.Tests/DeclaredApplicabilityTests.cs +++ /dev/null @@ -1,439 +0,0 @@ -using System.Text.Json.Nodes; -using Prober; -using Probes.Shapes; - -namespace CodoMetis.TypeKit.Prober.Tests; - -/// -/// An obligation declares where it does not apply, as axis values with a citation: Axes.Build holds the -/// declaration to the claim's axes, values and not-promised list; a pair is wanted by an obligation unless one of -/// its declarations matches it, and a pair no obligation wants is declared inapplicable, which `universe`, `gaps` -/// and `status` show with its citations, the gate leaves out as it leaves out an exclusion, and the covering -/// generator never puts in a row. Fixture: C1 over x and y, each 1 and 2, so four pairs. -/// -public sealed class DeclaredApplicabilityTests : IDisposable -{ - private readonly Fixture fx = new(); - public void Dispose() => fx.Dispose(); - - private static JsonObject Declared(string cite, params (string Axis, string[] Values)[] axes) => new() - { - ["axes"] = new JsonObject(axes.Select(a => KeyValuePair.Create(a.Axis, (JsonNode?)new JsonArray([.. a.Values.Select(v => (JsonNode)v)])))), - ["cite"] = cite, - }; - - private static JsonObject Obligation(string id, params JsonObject[] inapplicable) - { - var o = new JsonObject { ["id"] = id, ["sentence"] = "The claim." }; - if (inapplicable.Length > 0) o["inapplicable"] = new JsonArray([.. inapplicable]); - return o; - } - - /// C1 with the obligations given and one not-promised entry, no-one. - private void Claim(params JsonObject[] obligations) => - fx.Write("tools/probe/corpus/claims.json", new JsonArray(new JsonObject - { - ["id"] = "C1", ["source"] = "README.md#a", ["surfaces"] = new JsonArray("s"), - ["obligations"] = new JsonArray([.. obligations]), - ["not-promised"] = new JsonArray(new JsonObject { ["id"] = "no-one", ["source"] = "README.md#a", ["sentence"] = "The claim." }), - }).ToJsonString()); - - /// - /// keys does not apply where x=1 and y=1 (one declaration over both axes), nulls nowhere x=1: the pair x=1 y=1 is - /// wanted by neither, x=1 y=2 by keys alone. - /// - private void OnePairWantedByNone() => Claim( - Obligation("keys", Declared("docs:README.md#a", ("x", ["1"]), ("y", ["1"]))), - Obligation("nulls", Declared("not-promised:no-one", ("x", ["1"])))); - - private string Refused() - { - var (exit, _, error) = fx.Ledger("universe", "--base", "HEAD"); - exit.ShouldBe(1); - return error; - } - - [Fact] - public void A_declaration_naming_an_axis_the_claim_does_not_have_is_refused() - { - Claim(Obligation("keys", Declared("not-promised:no-one", ("z", ["1"])))); - Refused().ShouldContain("claim C1: obligation keys: inapplicable[0] names axis z, which the claim does not have"); - } - - [Fact] - public void A_declaration_naming_a_value_the_claim_does_not_list_is_refused() - { - Claim(Obligation("keys", Declared("not-promised:no-one", ("x", ["1"]))), Obligation("nulls", Declared("not-promised:no-one", ("x", ["3"])))); - Refused().ShouldContain("claim C1: obligation nulls: inapplicable[0] names x=3, a value the claim does not list for x"); - } - - [Theory] - [InlineData("fact:KB-31", "is not a knowledge entry's id (fact:KB-031)")] - [InlineData("record:94", "is not a decision record's number (record:0094)")] - [InlineData("docs:../README.md#a", "is not a docs source (docs:docs/json.md#decimals-in-one-spelling)")] - [InlineData("not-promised:No-one", "is not a not-promised id (not-promised:closed-set-has-no-text)")] - public void A_malformed_cite_is_refused(string cite, string why) - { - Claim(Obligation("keys", Declared(cite, ("x", ["1"])))); - Refused().ShouldContain($"claim C1: obligation keys: inapplicable[0]: cite {cite} {why}"); - } - - [Fact] - public void A_cite_of_a_not_promised_id_the_claim_lacks_is_refused() - { - Claim(Obligation("keys", Declared("not-promised:no-such-entry", ("x", ["1"])))); - Refused().ShouldContain("claim C1: obligation keys: inapplicable[0] cites not-promised:no-such-entry, which the claim's not-promised list does not hold"); - } - - [Fact] - public void A_field_a_declaration_does_not_have_is_refused_by_the_schema() - { - var declaration = Declared("not-promised:no-one", ("x", ["1"])); - declaration["reason"] = "x=1 has no key"; - Claim(Obligation("keys", declaration)); - Refused().ShouldContain("claim C1: obligations[0].inapplicable[0].reason is not a field the schema has"); - } - - [Fact] - public void A_pair_wanted_by_no_obligation_is_listed_by_universe_with_its_citations_and_the_claim_keeps_its_pairs() - { - OnePairWantedByNone(); - var (exit, output, error) = fx.Ledger("universe", "--base", "HEAD"); - exit.ShouldBe(0, error); - output.ReplaceLineEndings("\n").ShouldContain( - "C1: 2 axes, 4 pairs\n inapplicable 1 pair(s), wanted by no obligation, first x=1 y=1: docs:README.md#a, not-promised:no-one\n"); - } - - [Fact] - public void Gaps_lists_the_pairs_no_obligation_wants_under_their_own_heading() - { - OnePairWantedByNone(); - var (exit, output, error) = fx.Ledger("gaps", "--base", "HEAD"); - exit.ShouldBe(0, error); - output.ReplaceLineEndings("\n").ShouldContain( - "pairs no obligation wants (declared inapplicable): 1\n C1: inapplicable 1 pair(s), wanted by no obligation, first x=1 y=1: docs:README.md#a, not-promised:no-one\n"); - } - - private static Dictionary R(string x, string y) => new(StringComparer.Ordinal) { ["x"] = x, ["y"] = y }; - - /// P passes rows over the three pairs some obligation wants, never x=1 y=1. - private void RecordTheWantedPairs() - { - fx.Rows([("1", "2"), ("2", "1"), ("2", "2")]); - fx.Commit("claims and rows"); - fx.Ledger("universe", "--base", "HEAD").Exit.ShouldBe(0); - fx.Footprint(); - fx.Ledger("record-run", "--probe", "P", "--result", "pass", "--evidence", fx.Evidence([R("1", "2"), R("2", "1"), R("2", "2")])).Exit.ShouldBe(0); - } - - [Fact] - public void Status_reports_the_inapplicable_pairs_and_is_done_once_every_wanted_pair_is_current() - { - OnePairWantedByNone(); - RecordTheWantedPairs(); - var (exit, output, error) = fx.Ledger("status", "--json"); - exit.ShouldBe(0, error); - var status = JsonNode.Parse(output)!; - var c1 = status["claims"]!["C1"]!; - c1["pairs"]!.GetValue().ShouldBe(4, "the claim's pairs stay its pairs"); - c1["current"]!.GetValue().ShouldBe(3); - c1["inapplicable_pairs"]!.GetValue().ShouldBe(1); - status["inapplicable_pairs"]!.GetValue().ShouldBe(1); - status["done"]!.GetValue().ShouldBeTrue("the one pair not current is wanted by no obligation"); - fx.Ledger("status").Out.ShouldContain(" C1: 3/4, 1 inapplicable"); - } - - [Fact] - public void Status_is_not_done_while_a_wanted_pair_is_not_current() - { - Claim(Obligation("keys"), Obligation("nulls")); - RecordTheWantedPairs(); - var status = JsonNode.Parse(fx.Ledger("status", "--json").Out)!; - status["claims"]!["C1"]!["inapplicable_pairs"]!.GetValue().ShouldBe(0); - status["done"]!.GetValue().ShouldBeFalse("x=1 y=1 is wanted and not current"); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void The_gate_leaves_out_a_pair_wanted_by_no_obligation_as_it_leaves_out_an_exclusion(bool declared) - { - fx.Sh("git", "tag", "v1"); - if (declared) OnePairWantedByNone(); else Claim(Obligation("keys"), Obligation("nulls")); - RecordTheWantedPairs(); - fx.Write("README.md", "# a\n\nThe claim.\n\nMore words.\n"); - fx.Commit("touch the claim"); - - var (exit, output, _) = fx.Ledger("gate", "--since", "v1", "--json"); - var gate = JsonNode.Parse(output)!; - gate["touched"]!.AsArray().Select(c => c!.GetValue()).ShouldBe(["C1"]); - if (declared) - { - exit.ShouldBe(0, output); - gate["uncovered_pairs"]!.GetValue().ShouldBe(0); - gate["inapplicable_pairs"]!.GetValue().ShouldBe(1); - } - else - { - exit.ShouldBe(1); - gate["reasons"]!.AsArray().Select(r => r!.GetValue()).ShouldContain("C1: 1 of 4 pairs uncovered (touched since v1)"); - } - } - - [Fact] - public void Rows_for_a_claim_ask_for_no_pair_no_obligation_wants() - { - OnePairWantedByNone(); - fx.Commit("claims"); - fx.Ledger("universe", "--base", "HEAD").Exit.ShouldBe(0); - var rows = JsonNode.Parse(fx.Ledger("rows", "--claim", "C1", "--full").Out)!; - rows["rows"]!.AsArray().Select(r => $"{r!["row"]!["x"]} {r["row"]!["y"]}").ShouldNotContain("1 1"); - rows["uncovered_after"]!.GetValue().ShouldBe(0, "a pair wanted by no obligation is not left uncovered"); - } - - [Fact] - public void A_declaration_changes_the_identity_of_its_claim_s_probes() - { - Claim(Obligation("keys"), Obligation("nulls")); - fx.Commit("claims"); - var before = fx.Ledger("identity", "--probe", "P").Out; - OnePairWantedByNone(); - fx.Commit("declared"); - fx.Ledger("identity", "--probe", "P").Out.ShouldNotBe(before, "Row.Check applies the declarations, so a result under others is not this one"); - } - - [Fact] - public void The_overlay_stages_the_claims_beside_the_rows_with_the_staging_s_over_the_corpus_s() - { - Claim(Obligation("keys"), Obligation("nulls")); - fx.Commit("claims"); - var ledger = new Ledger(new Context(fx.Root, new Dictionary(), TextWriter.Null, TextWriter.Null), "main"); - var staged = new JsonArray(new JsonObject - { - ["id"] = "C1", ["source"] = "README.md#a", ["surfaces"] = new JsonArray("s"), - ["obligations"] = new JsonArray(Obligation("keys", Declared("docs:README.md#a", ("x", ["1"])))), - }); - fx.Write(Path.GetRelativePath(fx.Root, Path.Combine(ledger.StagingDir, "claims.json")), staged.ToJsonString()); - var worktree = fx.Root + "-overlay"; - try - { - var probeDir = global::Prober.Replay.Overlay(ledger, new JsonObject { ["probeDir"] = "probes" }, worktree); - var claims = JsonNode.Parse(File.ReadAllText(Path.Combine(probeDir, "claims.json")))!.AsArray(); - JsonNode.DeepEquals(claims, staged).ShouldBeTrue(claims.ToJsonString()); - global::Prober.Replay.Overlay(ledger, new JsonObject { ["probeDir"] = "probes" }, worktree); - File.Exists(Path.Combine(probeDir, "claims.json")).ShouldBeTrue("a second overlay keeps it"); - } - finally - { - try { Directory.Delete(worktree, recursive: true); } catch (IOException) { } - } - } -} - -/// The wanted pairs of a claim, computed on the claim itself, and the covering generator over them. -public sealed class WantedPairsTests -{ - private static Inapplicability Declared(string cite, params (string Axis, string[] Values)[] axes) => - new(axes.ToDictionary(a => a.Axis, a => a.Values.ToList(), StringComparer.Ordinal), cite); - - private static global::Prober.Obligation Obligation(string id, params Inapplicability[] inapplicable) => - new(id, "The claim.", new SortedDictionary>(StringComparer.Ordinal)) { Inapplicable = inapplicable }; - - private static Claim Over(int axes, params global::Prober.Obligation[] obligations) => new() - { - Id = "C1", Source = "README.md#a", Surfaces = ["s"], Watched = [], Excluded = [], Obligations = obligations, - Axes = new(new[] { "x", "y", "z" }.Take(axes).ToDictionary(a => a, _ => new List { "1", "2" }), StringComparer.Ordinal), - }; - - private static IEnumerable Shown(IEnumerable pairs) => pairs.Select(p => p.ToString()).Order(StringComparer.Ordinal); - - [Fact] - public void An_obligation_wants_the_pairs_none_of_its_declarations_matches_on_either_side() - { - // keys: not where x=1 (the first of a pair, ordinally); nulls: not where y=1 (the second); counts: nowhere declared. - var claim = Over(2, Obligation("keys", Declared("record:0001", ("x", ["1"]))), Obligation("nulls", Declared("record:0002", ("y", ["1"]))), Obligation("counts")); - Shown(claim.PairsWantedBy("keys")).ShouldBe(["x=2 y=1", "x=2 y=2"]); - Shown(claim.PairsWantedBy("nulls")).ShouldBe(["x=1 y=2", "x=2 y=2"]); - Shown(claim.PairsWantedBy("counts")).ShouldBe(["x=1 y=1", "x=1 y=2", "x=2 y=1", "x=2 y=2"]); - claim.Pairs().Count.ShouldBe(4, "the claim's pairs stay its pairs"); - claim.DeclaredInapplicable().ShouldBeEmpty("counts wants every pair"); - } - - [Fact] - public void A_pair_no_obligation_wants_is_declared_inapplicable_with_the_citations_that_match_it() - { - // keys wants x=2 y=1 alone, nulls x=1 y=2 alone. - var claim = Over(2, - Obligation("keys", Declared("record:0001", ("x", ["1"])), Declared("record:0003", ("y", ["2"]))), - Obligation("nulls", Declared("record:0002", ("y", ["1"])), Declared("record:0004", ("x", ["2"])))); - Shown(claim.PairsWanted()).ShouldBe(["x=1 y=2", "x=2 y=1"]); - var dead = claim.DeclaredInapplicable(); - Shown(dead.Keys).ShouldBe(["x=1 y=1", "x=2 y=2"]); - dead[new Pair("x=1", "y=1")].ShouldBe(["record:0001", "record:0002"]); - dead[new Pair("x=2", "y=2")].ShouldBe(["record:0003", "record:0004"]); - } - - [Fact] - public void A_declaration_over_two_axes_matches_only_the_pair_holding_both() - { - var claim = Over(3, Obligation("keys", Declared("record:0001", ("x", ["1"]), ("y", ["1"])))); - Shown(claim.DeclaredInapplicable().Keys).ShouldBe(["x=1 y=1"], "x=1 with z, or y=1 with z, is in rows where the other axis is 2"); - } - - [Fact] - public void The_covering_rows_never_hold_a_pair_no_obligation_wants_and_count_each_wanted_pair_once() - { - // Both obligations want the same eleven pairs: none of x=1 with y=1. - var claim = Over(3, Obligation("keys", Declared("record:0001", ("x", ["1"]), ("y", ["1"]))), Obligation("nulls", Declared("record:0002", ("x", ["1"]), ("y", ["1"])))); - for (var seed = 1; seed <= 20; seed++) - { - var rows = Covering.Rows(claim, claim.Pairs(), 2000, seed); - rows.ShouldNotContain(r => r.Row["x"] == "1" && r.Row["y"] == "1", $"seed {seed}"); - var gained = rows.SelectMany(r => r.Pairs).ToList(); - gained.Count.ShouldBe(11, $"seed {seed}: each wanted pair once"); - Shown(gained).ShouldBe(Shown(claim.PairsWanted()), $"seed {seed}"); - } - } - - [Fact] - public void A_seeded_row_fills_each_other_axis_with_a_value_that_puts_no_unwanted_pair_in_it() - { - var claim = Over(3, Obligation("keys", Declared("record:0001", ("x", ["1"]), ("z", ["1"])))); - var row = Covering.Seeded(claim, new Pair("y=1", "z=1"), claim.DeclaredInapplicable()); - row.ShouldBe(new SortedDictionary(StringComparer.Ordinal) { ["x"] = "2", ["y"] = "1", ["z"] = "1" }); - } - - [Theory] - [InlineData("not-promised:closed-set-has-no-text")] - [InlineData("not-promised:Closed")] - [InlineData("docs:docs/json.md#decimals-in-one-spelling")] - [InlineData("docs:README.md")] - [InlineData("docs:README.md#What TypeKit guarantees")] - [InlineData("docs:/etc/passwd.md")] - [InlineData("docs:../README.md#a")] - [InlineData("fact:KB-031")] - [InlineData("fact:KB-31")] - [InlineData("record:0094")] - [InlineData("record:94")] - [InlineData("README.md#a")] - [InlineData("note:x")] - public void A_cite_the_universe_accepts_is_one_the_harness_can_cite(string cite) - { - var harness = Record.Exception(() => Cite.Parse(cite)) is null; - (Axes.CiteProblem(cite) is null).ShouldBe(harness, harness ? "the harness cites it" : "the harness refuses it"); - } -} - -/// -/// The harness applies the claim's declarations before a check's body runs: where one matches the row's values the -/// check is inapplicable with the declaration's citation, for that check only, and the body does not run. Without a -/// staged claims file, as for a probe these tests run in-process, no claim declares anything. -/// -[Collection(nameof(ProbeOut))] -public sealed class DeclaredCheckTests : IDisposable -{ - private readonly string dir = Path.Combine(Path.GetTempPath(), "prober-tests", Guid.NewGuid().ToString("N")); - private readonly string? before = Rows.ClaimsFile; - - public DeclaredCheckTests() - { - Directory.CreateDirectory(dir); - File.WriteAllText(Path.Combine(dir, "claims.json"), new JsonArray(new JsonObject - { - ["id"] = "C1", ["source"] = "README.md#a", ["surfaces"] = new JsonArray("s"), - ["obligations"] = new JsonArray( - new JsonObject - { - ["id"] = "keys", ["sentence"] = "The claim.", - ["inapplicable"] = new JsonArray( - new JsonObject { ["axes"] = new JsonObject { ["x"] = new JsonArray("1"), ["y"] = new JsonArray("2") }, ["cite"] = "not-promised:no-key-here" }, - new JsonObject { ["axes"] = new JsonObject { ["y"] = new JsonArray("3") }, ["cite"] = "fact:KB-031" }), - }, - new JsonObject { ["id"] = "nulls", ["sentence"] = "The claim." }), - }).ToJsonString()); - Rows.ClaimsFile = Path.Combine(dir, "claims.json"); - } - - public void Dispose() - { - Rows.ClaimsFile = before; - try { Directory.Delete(dir, recursive: true); } catch (IOException) { } catch (UnauthorizedAccessException) { } - } - - /// Runs one row of C1 through Rec.Run with a check of keys and one of nulls, synchronous or not; which bodies ran, and the line written. - private (List Ran, Exception? Ended, JsonObject Line) Run(string x, string y, bool async = false, string? claim = "C1") - { - var ran = new List(); - var outDir = Path.Combine(dir, "out"); - var previous = Environment.GetEnvironmentVariable("PROBE_OUT"); - Environment.SetEnvironmentVariable("PROBE_OUT", outDir); - Exception? ended = null; - try - { - Rec.Run("P", new Row(0, new Dictionary { ["x"] = x, ["y"] = y }, claim), async r => - { - foreach (var obligation in new[] { "keys", "nulls" }) - _ = async - ? await r.Check(obligation, Baseline.Twin, c => { ran.Add(obligation); return Task.FromResult(c["x"] == "0" ? "differs" : null); }) - : r.Check(obligation, Baseline.Twin, c => { ran.Add(obligation); return c["x"] == "0" ? "differs" : null; }); - return null; - }).GetAwaiter().GetResult(); - } - catch (Exception e) { ended = e; } - finally { Environment.SetEnvironmentVariable("PROBE_OUT", previous); } - var line = File.ReadAllLines(Path.Combine(outDir, "P.jsonl")).Select(l => JsonNode.Parse(l)!.AsObject()).Last(); - File.Delete(Path.Combine(outDir, "P.jsonl")); - return (ran, ended, line); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public void A_check_its_claim_declares_inapplicable_for_the_row_s_values_does_not_run_and_records_the_cite(bool async) - { - var (ran, ended, line) = Run("1", "2", async); - ran.ShouldBe(["nulls"], "keys does not apply where x=1 and y=2"); - ended.ShouldBeNull("nulls compared and passed; keys did not apply, which voids nothing"); - var keys = HarnessRun.Check(line, "keys"); - Files.Str(keys["category"]).ShouldBe("inapplicable"); - Files.Strings(keys["reasons"]).ShouldBe(["inapplicable:not-promised:no-key-here(declared x=1 y=2)"]); - Files.StringMap(keys["applied"]).ShouldBe(new Dictionary { ["x"] = "1", ["y"] = "2" }, "the axes the declaration matched"); - Files.Str(HarnessRun.Check(line, "nulls")["category"]).ShouldBe("pass", "the declaration is keys', for that check only"); - } - - [Fact] - public void Each_declaration_of_the_obligation_is_applied_and_one_over_two_axes_needs_both() - { - Run("1", "1").Ran.ShouldBe(["keys", "nulls"], "x=1 with y=1 is not x=1 with y=2"); - Run("2", "2").Ran.ShouldBe(["keys", "nulls"], "x=2 with y=2 is not x=1 with y=2"); - var (ran, _, line) = Run("2", "3"); - ran.ShouldBe(["nulls"]); - Files.Strings(HarnessRun.Check(line, "keys")["reasons"]).ShouldBe(["inapplicable:fact:KB-031(declared y=3)"]); - } - - [Fact] - public void A_row_in_hand_without_a_claim_or_without_a_staged_claims_file_sees_no_declarations() - { - Run("1", "2", claim: null).Ran.ShouldBe(["keys", "nulls"]); - Rows.ClaimsFile = null; - Run("1", "2").Ran.ShouldBe(["keys", "nulls"]); - Rows.ClaimsFile = Path.Combine(dir, "none.json"); - Run("1", "2").Ran.ShouldBe(["keys", "nulls"]); - } - - [Fact] - public void A_copy_of_a_row_keeps_its_claim() - { - var row = new Row(0, new Dictionary { ["x"] = "1", ["y"] = "2" }, "C1"); - row.Fresh().Claim.ShouldBe("C1"); - row.With("y", "1").Claim.ShouldBe("C1"); - } - - [Fact] - public void A_row_of_a_rows_file_holds_the_file_s_claim() - { - var path = Path.Combine(dir, "P.json"); - File.WriteAllText(path, """{"claim": "C1", "rows": [{"row": {"x": "1", "y": "2"}}]}"""); - Rows.Read(path).ShouldHaveSingleItem().Claim.ShouldBe("C1"); - } -} diff --git a/test/CodoMetis.TypeKit.Prober.Tests/DotnetTests.cs b/test/CodoMetis.TypeKit.Prober.Tests/DotnetTests.cs deleted file mode 100644 index 40c07acf..00000000 --- a/test/CodoMetis.TypeKit.Prober.Tests/DotnetTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -namespace CodoMetis.TypeKit.Prober.Tests; - -/// -/// Which dotnet a tool starts (tools/Shared/Dotnet.cs, design/adr/0105): the one on PATH when it satisfies -/// global.json, the SDK installer's when only that one does, and otherwise the one on PATH, whose own error then -/// reaches whoever reads the output. -/// -public sealed class DotnetTests : IDisposable -{ - private readonly string installer = Directory.CreateTempSubdirectory("dotnet-installer-").FullName; - public void Dispose() => Directory.Delete(installer, recursive: true); - - private string Executable => Path.Combine(installer, OperatingSystem.IsWindows() ? "dotnet.exe" : "dotnet"); - - private void Install() => File.WriteAllText(Executable, ""); - - [Fact] - public void The_one_on_path_when_it_satisfies_the_pin() - { - Install(); - var asked = new List(); - Dotnet.Choose(installer, d => { asked.Add(d); return true; }).ShouldBe(("dotnet", null)); - asked.ShouldBe(["dotnet"], "the installer's is not even asked"); - } - - [Fact] - public void The_installers_when_only_it_satisfies_the_pin() - { - Install(); - Dotnet.Choose(installer, d => d != "dotnet").ShouldBe((Executable, installer)); - } - - [Fact] - public void The_one_on_path_when_neither_does() - { - Install(); - Dotnet.Choose(installer, _ => false).ShouldBe(("dotnet", null)); - } - - [Fact] - public void The_one_on_path_when_the_installer_put_none() - { - var asked = new List(); - Dotnet.Choose(installer, d => { asked.Add(d); return d != "dotnet"; }).ShouldBe(("dotnet", null)); - asked.ShouldBe(["dotnet"]); - } - - [Fact] - public void A_dotnet_satisfies_a_directory_whose_pin_it_has_and_not_one_whose_pin_it_lacks() - { - Dotnet.Satisfies("dotnet", installer).ShouldBeTrue("no global.json: any SDK"); - File.WriteAllText(Path.Combine(installer, "global.json"), """{"sdk": {"version": "0.0.1", "rollForward": "disable"}}"""); - Dotnet.Satisfies("dotnet", installer).ShouldBeFalse("an SDK no dotnet has"); - } - - [Fact] - public void A_program_that_does_not_start_satisfies_nothing() => - Dotnet.Satisfies(Path.Combine(installer, "no-such-dotnet"), installer).ShouldBeFalse(); - - [Fact] - public void A_child_is_pointed_at_the_chosen_directory() - { - var environment = new Dictionary { ["PATH"] = "/usr/bin" }; - Dotnet.Apply(installer, environment); - environment["PATH"].ShouldBe(installer + Path.PathSeparator + "/usr/bin"); - environment["DOTNET_ROOT"].ShouldBe(installer); - - var untouched = new Dictionary { ["PATH"] = "/usr/bin" }; - Dotnet.Apply(null, untouched); - untouched.Keys.ShouldBe(["PATH"]); - untouched["PATH"].ShouldBe("/usr/bin"); - } -} diff --git a/test/CodoMetis.TypeKit.Prober.Tests/FixTests.cs b/test/CodoMetis.TypeKit.Prober.Tests/FixTests.cs deleted file mode 100644 index 38cf460c..00000000 --- a/test/CodoMetis.TypeKit.Prober.Tests/FixTests.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System.Text.Json.Nodes; -using Prober; - -namespace CodoMetis.TypeKit.Prober.Tests; - -/// -/// prober fix F --commit SHA writes the fixed line of findings.jsonl as finding-line.schema.json wants it, where a -/// fixed finding was a line written by hand (F3, #45): the open line's claim and probe, the commit's full id. -/// -public sealed class FixTests : IDisposable -{ - private readonly Fixture fx = new(); - public void Dispose() => fx.Dispose(); - - private string Findings => Path.Combine(fx.BranchDir, "findings.jsonl"); - - private void Open(string id) => - File.AppendAllText(Findings, new JsonObject - { - ["schemaVersion"] = 1, ["id"] = id, ["claim"] = "C1", ["status"] = "open", ["probe"] = "P", ["reason"] = "row 3 differs", - }.ToJsonString() + "\n"); - - private JsonObject Last() => JsonNode.Parse(File.ReadAllLines(Findings)[^1])!.AsObject(); - - private Ledger Ledger() => new(new Context(fx.Root, new Dictionary(), TextWriter.Null, TextWriter.Null), null); - - [Fact] - public void It_writes_the_fixed_line_with_the_open_lines_claim_and_probe_and_the_full_commit() - { - Open("F9"); - var head = fx.Sh("git", "rev-parse", "HEAD").Trim(); - var (exit, output, error) = fx.Ledger("fix", "F9", "--commit", head[..7], "--reason", "fixed by the converter's null"); - exit.ShouldBe(0, error); - output.ShouldBe($"F9 fixed at {head[..7]}{Environment.NewLine}"); - var line = Last(); - line.Select(kv => kv.Key).ShouldBe(["schemaVersion", "id", "claim", "status", "commit", "probe", "reason"]); - line["schemaVersion"]!.GetValue().ShouldBe(1); - line["id"]!.GetValue().ShouldBe("F9"); - line["claim"]!.GetValue().ShouldBe("C1"); - line["status"]!.GetValue().ShouldBe("fixed"); - line["commit"]!.GetValue().ShouldBe(head); - line["probe"]!.GetValue().ShouldBe("P"); - line["reason"]!.GetValue().ShouldBe("fixed by the converter's null"); - Ledger().OpenFindings().ShouldBeEmpty(); - Ledger().Findings()["F9"]["status"]!.GetValue().ShouldBe("fixed"); - } - - [Fact] - public void A_commit_outside_heads_history_is_recorded_and_counts_once_it_is_in_it() - { - Open("F9"); - fx.Sh("git", "checkout", "-q", "-b", "fix"); - fx.Write("src/a.cs", "class A { }\n"); - fx.Sh("git", "-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qam", "the fix"); // the state stays untracked - var fix = fx.Sh("git", "rev-parse", "HEAD").Trim(); - fx.Sh("git", "checkout", "-q", "main"); - var (exit, output, error) = fx.Ledger("fix", "F9", "--commit", fix); - exit.ShouldBe(0, error); - output.ShouldContain($"F9 fixed at {fix[..7]}; it counts once that commit is in HEAD's history"); - Ledger().OpenFindings().ShouldBe(["F9"]); - fx.Sh("git", "merge", "-q", "--ff-only", "fix"); - Ledger().OpenFindings().ShouldBeEmpty(); - } - - [Theory] - [InlineData("F8", "HEAD", 1, "F8 is no finding of this ledger")] - [InlineData("F9", "0123456789abcdef0123456789abcdef01234567", 1, "--commit 0123456789abcdef0123456789abcdef01234567 is no commit of this repository")] - [InlineData("F9", "", 2, "--commit is required")] - public void It_refuses_a_finding_the_ledger_does_not_hold_and_a_commit_the_repository_lacks(string id, string commit, int code, string message) - { - Open("F9"); - var before = File.ReadAllText(Findings); - var (exit, _, error) = commit.Length > 0 ? fx.Ledger("fix", id, "--commit", commit) : fx.Ledger("fix", id); - exit.ShouldBe(code); - error.ShouldContain(message); - File.ReadAllText(Findings).ShouldBe(before); - } - - [Fact] - public void It_refuses_a_finding_already_fixed_and_a_call_without_one_id() - { - Open("F9"); - fx.Ledger("fix", "F9", "--commit", "HEAD").Exit.ShouldBe(0); - var (exit, _, error) = fx.Ledger("fix", "F9", "--commit", "HEAD"); - exit.ShouldBe(1); - error.ShouldContain("F9 is fixed, not open"); - fx.Ledger("fix", "--commit", "HEAD").Exit.ShouldBe(2); - fx.Ledger("fix", "F9", "F10", "--commit", "HEAD").Exit.ShouldBe(2); - } -} diff --git a/test/CodoMetis.TypeKit.Prober.Tests/Fixture.cs b/test/CodoMetis.TypeKit.Prober.Tests/Fixture.cs deleted file mode 100644 index b388e557..00000000 --- a/test/CodoMetis.TypeKit.Prober.Tests/Fixture.cs +++ /dev/null @@ -1,167 +0,0 @@ -using System.Diagnostics; -using System.Text.Json.Nodes; -using Prober; - -namespace CodoMetis.TypeKit.Prober.Tests; - -/// A repository with one claim C1 over axes x and y (values 1 and 2) and one probe P covering it. -internal sealed class Fixture : IDisposable -{ - public Fixture() - { - Root = Path.Combine(Path.GetTempPath(), "prober-tests", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(Root); - Memory = Path.Combine(Root, ".claude", "agent-memory-local", "prober"); - BranchDir = Path.Combine(Memory, "branches", "main"); - Sh("git", "init", "-q", "-b", "main"); - Write("README.md", "# a\n\nThe claim.\n"); - Write("src/a.cs", "class A {}\n"); - Write(".claude/probe-axes.md", "```json probe-axes\n" + new JsonObject - { - ["axes"] = new JsonObject { ["x"] = new JsonArray("1", "2"), ["y"] = new JsonArray("1", "2") }, - ["surfaces"] = new JsonObject { ["s"] = new JsonObject { ["axes"] = new JsonArray("x", "y"), ["watched"] = new JsonArray("src") } }, - }.ToJsonString() + "\n```\n"); - Write("tools/probe/corpus/claims.json", """[{"id": "C1", "source": "README.md#a", "surfaces": ["s"]}]"""); - Write("tools/probe/corpus/registry.json", """{"probes": {"P": {"claim": "C1", "filter": "*P"}}}"""); - Write("tools/probe/corpus/harness/P.cs", "// probe P\n"); - Rows([("1", "1"), ("2", "2")]); - Commit("init"); - Directory.CreateDirectory(BranchDir); - File.WriteAllText(Path.Combine(Memory, "axes-generated.json"), """{"axes": {}, "needsValues": []}"""); - Ledger("universe", "--base", "HEAD").Exit.ShouldBe(0); - } - - public string Root { get; } - public string Memory { get; } - public string BranchDir { get; } - - public void Write(string relative, string text) - { - var path = Path.Combine(Root, relative); - Directory.CreateDirectory(Path.GetDirectoryName(path)!); - File.WriteAllText(path, text); - } - - /// P's rows: each probe has a rows file of its own, its claim inside. - public void Rows(IEnumerable<(string X, string Y)> rows) => - Write("tools/probe/corpus/rows/P.json", new JsonObject - { - ["claim"] = "C1", - ["rows"] = new JsonArray([.. rows.Select(r => (JsonNode)new JsonObject { ["row"] = new JsonObject { ["x"] = r.X, ["y"] = r.Y } })]), - }.ToJsonString()); - - public void Commit(string message) - { - Sh("git", "add", "-A"); - Sh("git", "-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", message); - } - - public string Sh(string file, params string[] args) - { - var start = new ProcessStartInfo(file) { WorkingDirectory = Root, RedirectStandardOutput = true, RedirectStandardError = true }; - foreach (var a in args) start.ArgumentList.Add(a); - using var p = Process.Start(start)!; - var output = p.StandardOutput.ReadToEnd(); var error = p.StandardError.ReadToEnd(); - p.WaitForExit(); - p.ExitCode.ShouldBe(0, $"{file} {string.Join(' ', args)}: {output}{error}"); - return output; - } - - /// Runs a prober command in-process, in this repository, with the environment given. - public (int Exit, string Out, string Err) Ledger(params string[] args) => LedgerWith(new Dictionary(), args); - - public (int Exit, string Out, string Err) LedgerWith(Dictionary env, params string[] args) => LedgerEnding(env, Environment.NewLine, args); - - /// As , with the line end the output writers use, so a test sees Windows' output on any OS. - public (int Exit, string Out, string Err) LedgerEnding(Dictionary env, string newLine, params string[] args) - { - var output = new StringWriter { NewLine = newLine }; var error = new StringWriter { NewLine = newLine }; - var exit = Program.Run(new Context(Root, env, output, error), args); - return (exit, output.ToString(), error.ToString()); - } - - /// Runs a prober command in-process in another checkout of this repository (a linked worktree). - public (int Exit, string Out, string Err) LedgerIn(string checkout, params string[] args) - { - var output = new StringWriter(); var error = new StringWriter(); - var exit = Program.Run(new Context(checkout, new Dictionary(), output, error), args); - return (exit, output.ToString(), error.ToString()); - } - - /// A linked worktree of this repository (git worktree add) on a new branch, beside it; removed with the fixture. - public string Linked(string branch) - { - var path = Root + "-" + branch; - Sh("git", "worktree", "add", "-q", "-b", branch, path); - linked.Add(path); - return path; - } - - private readonly List linked = []; - - /// The lock file of a branch's state, wherever the tool puts it. - public string LockFile(string branch = "main") => - RoundLock.LockPath(new Ledger(new Context(Root, new Dictionary(), TextWriter.Null, TextWriter.Null), branch)); - - /// Runs the built dll as its own process: for the tests that race several takers. - public Process Spawn(params string[] args) - { - var start = new ProcessStartInfo("dotnet") { WorkingDirectory = Root, RedirectStandardOutput = true, RedirectStandardError = true }; - start.ArgumentList.Add(typeof(Program).Assembly.Location); - foreach (var a in args) start.ArgumentList.Add(a); - return Process.Start(start)!; - } - - public void Footprint() - { - var blob = Sh("git", "rev-parse", "HEAD:src/a.cs").Trim(); - File.AppendAllText(Path.Combine(BranchDir, "footprints.jsonl"), - new JsonObject { ["probe"] = "P", ["footprint"] = new JsonObject { ["src/a.cs"] = blob }, ["compileTime"] = new JsonObject(), ["toolchain"] = new JsonObject() }.ToJsonString() + "\n"); - } - - /// - /// What a replay of P records: the rows it ran, how each ended and the settings that applied; outcomes, when - /// given, sets each row's category and the finding it skips for. - /// - public string Evidence(IReadOnlyList> applied, string category = "pass", int[]? executed = null, - IReadOnlyList<(string Category, string? Finding)>? outcomes = null) - { - var idx = Enumerable.Range(0, applied.Count).ToArray(); - var path = Path.Combine(Root, "evidence.json"); - File.WriteAllText(path, new JsonObject - { - ["variants"] = new JsonArray(new JsonObject - { - ["env"] = new JsonObject(), ["expected"] = new JsonArray([.. idx.Select(i => (JsonNode)i)]), - ["executed"] = new JsonArray([.. (executed ?? idx).Select(i => (JsonNode)i)]), - }), - ["rows"] = new JsonObject(idx.Select(i => KeyValuePair.Create(i.ToString(System.Globalization.CultureInfo.InvariantCulture), (JsonNode?)new JsonObject - { - ["category"] = outcomes?[i].Category ?? category, ["applied"] = new JsonObject(applied[i].Select(kv => KeyValuePair.Create(kv.Key, (JsonNode?)kv.Value))), - ["findings"] = outcomes?[i].Finding is { } finding ? new JsonArray(finding) : new JsonArray(), - }))), - }.ToJsonString()); - return path; - } - - public int Covered() - { - var (exit, output, error) = Ledger("status", "--json"); - exit.ShouldBe(0, error); - return (int)JsonNode.Parse(output)!["claims"]!["C1"]!["current"]!.GetValue(); - } - - public void AgeTheLock() - { - var path = LockFile(); - var held = JsonNode.Parse(File.ReadAllText(path))!.AsObject(); - held["heartbeat"] = held["heartbeat"]!.GetValue() - 21 * 60; - File.WriteAllText(path, held.ToJsonString()); - } - - public void Dispose() - { - foreach (var dir in linked.Append(Root)) - try { Directory.Delete(dir, recursive: true); } catch (IOException) { } catch (UnauthorizedAccessException) { } - } -} diff --git a/test/CodoMetis.TypeKit.Prober.Tests/FreshnessTests.cs b/test/CodoMetis.TypeKit.Prober.Tests/FreshnessTests.cs deleted file mode 100644 index c71a4a7a..00000000 --- a/test/CodoMetis.TypeKit.Prober.Tests/FreshnessTests.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace CodoMetis.TypeKit.Prober.Tests; - -/// -/// The dll refuses to answer from other sources than the ones it was built from, its schemas among them, -/// as every tool does (tools/Directory.Build.targets, tools/Shared/SourceFingerprint.cs). -/// -public sealed class FreshnessTests -{ - [Fact] - public void The_stamp_covers_the_schemas() => - SourceFingerprint.Embedded.ShouldContain(line => line.StartsWith("tools/Prober/Schemas/", StringComparison.Ordinal) && line.Contains(".json|", StringComparison.Ordinal)); - - [Theory] - [InlineData("tools/Prober/Program.cs")] - [InlineData("tools/Prober/Schemas/round.schema.json")] - public void A_stale_dll_refuses_and_names_the_rebuild(string source) - { - using var sandbox = new ToolSandbox("Prober", "prober"); - File.AppendAllText(Path.Combine(sandbox.Repo, source), "\n"); - - var (code, _, error) = sandbox.Run(arguments: ["status"]); - - code.ShouldBe(3); - error.ShouldContain($"({source} differs)"); - error.ShouldContain("Run `dotnet build tools/Prober -c "); - } -} diff --git a/test/CodoMetis.TypeKit.Prober.Tests/GapsTests.cs b/test/CodoMetis.TypeKit.Prober.Tests/GapsTests.cs deleted file mode 100644 index d9db71e3..00000000 --- a/test/CodoMetis.TypeKit.Prober.Tests/GapsTests.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System.Text.Json.Nodes; - -namespace CodoMetis.TypeKit.Prober.Tests; - -/// -/// `prober gaps` prints what the corpus leaves out, each under its heading with its recorded reason, and says -/// none under a heading with nothing: a report from the tracked corpus and the generated axes that decides -/// nothing (decision 104). -/// -public sealed class GapsTests : IDisposable -{ - private readonly Fixture fx = new(); - public void Dispose() => fx.Dispose(); - - /// The output after its first line, which names the branch and the commit. - private string Gaps(string newLine) - { - var (exit, output, error) = fx.LedgerEnding([], newLine, "gaps", "--base", "HEAD"); - exit.ShouldBe(0, "a report decides nothing: " + error); - var text = output.ReplaceLineEndings("\n"); - text.ShouldStartWith("prober gaps: main at "); - return text[(text.IndexOf('\n', StringComparison.Ordinal) + 1)..]; - } - - [Theory] - [InlineData("\n")] - [InlineData("\r\n")] - public void Each_gap_is_listed_under_its_heading_with_its_reason(string newLine) - { - fx.Write("design/adr/0001-limits.md", "# 1. Limits\n\nAn ordering of keys is not promised.\n\nNo culture but the invariant one is promised.\n"); - fx.Write(".claude/probe-axes.md", "```json probe-axes\n" + new JsonObject - { - ["axes"] = new JsonObject - { - ["x"] = new JsonArray("1", "2", "3"), ["y"] = new JsonArray("1", "2"), - ["shape.refused-kind"] = new JsonArray("in-generic-type"), - }, - ["surfaces"] = new JsonObject { ["s"] = new JsonObject { ["axes"] = new JsonArray("x", "y"), ["watched"] = new JsonArray("src") } }, - ["refused"] = new JsonObject { ["x"] = new JsonObject { ["3"] = "the framework refuses 3 on its own (KB-001)" } }, - ["out-of-reach"] = new JsonObject { ["postgresql"] = "needs a server" }, - }.ToJsonString() + "\n```\n"); - File.WriteAllText(Path.Combine(fx.Memory, "axes-generated.json"), new JsonObject - { - ["axes"] = new JsonObject { ["json.z"] = new JsonArray("true", "false") }, - ["needsValues"] = new JsonArray("json.w"), - ["versions"] = new JsonObject { ["System.Text.Json"] = "10.0.0" }, - }.ToJsonString()); - fx.Write("tools/probe/corpus/claims.json", new JsonArray(new JsonObject - { - ["id"] = "C1", ["source"] = "README.md#a", ["surfaces"] = new JsonArray("s"), - ["narrow"] = new JsonObject { ["y"] = "The claim." }, - ["not-promised"] = new JsonArray( - new JsonObject - { - ["id"] = "order", ["source"] = "design/adr/0001-limits.md#1-limits", ["sentence"] = "An ordering of keys is not promised.", - ["axes"] = new JsonObject { ["x"] = new JsonArray("1", "2") }, - }, - new JsonObject { ["id"] = "culture", ["source"] = "design/adr/0001-limits.md#1-limits", ["sentence"] = "No culture but the invariant one is promised." }), - }).ToJsonString()); - - Gaps(newLine).ShouldBe(""" - settings no surface reaches and nothing ignores (enumerated at System.Text.Json 10.0.0): 1 - json.z (no surface reaches it and it is not ignored) - settings without values: 1 - json.w (no hand values and not ignored) - declarations the generators refuse (shape.refused-kind): 1 - in-generic-type - values the frameworks refuse on their own: 1 - x=3: the framework refuses 3 on its own (KB-001) - not promised, by what it concerns: 3 - the whole claim: C1/culture "No culture but the invariant one is promised." (design/adr/0001-limits.md#1-limits) - x=1|2: C1/order "An ordering of keys is not promised." (design/adr/0001-limits.md#1-limits) - y: C1 narrows it, "The claim." (README.md#a) - pairs no obligation wants (declared inapplicable): none - pairs no passing check read both axes of (the last replay's evidence): none - out of reach: 1 - postgresql: needs a server - - """.ReplaceLineEndings("\n")); - } - - [Theory] - [InlineData("\n")] - [InlineData("\r\n")] - public void A_corpus_without_gaps_says_none_under_each_heading(string newLine) => - Gaps(newLine).ShouldBe(""" - settings no surface reaches and nothing ignores: none - settings without values: none - declarations the generators refuse (shape.refused-kind): none - values the frameworks refuse on their own: none - not promised, by what it concerns: none - pairs no obligation wants (declared inapplicable): none - pairs no passing check read both axes of (the last replay's evidence): none - out of reach: none - - """.ReplaceLineEndings("\n")); - - /// - /// C1 over x, y and z: shape wants every pair and its passing checks read x and y; parse does not apply where x=1 and - /// z=1 and its passing checks read x and z. x=1 z=1 is wanted by shape alone, which never read z, and no passing check - /// read y with z: those five pairs are listed. A check that did not pass reads nothing for the listing. - /// - [Fact] - public void The_pairs_no_obligation_wanting_them_read_both_axes_of_are_listed() - { - fx.Write(".claude/probe-axes.md", "```json probe-axes\n" + new JsonObject - { - ["axes"] = new JsonObject { ["x"] = new JsonArray("1", "2"), ["y"] = new JsonArray("1", "2"), ["z"] = new JsonArray("1", "2") }, - ["surfaces"] = new JsonObject { ["s"] = new JsonObject { ["axes"] = new JsonArray("x", "y", "z"), ["watched"] = new JsonArray("src") } }, - }.ToJsonString() + "\n```\n"); - fx.Write("tools/probe/corpus/claims.json", new JsonArray(new JsonObject - { - ["id"] = "C1", ["source"] = "README.md#a", ["surfaces"] = new JsonArray("s"), - ["obligations"] = new JsonArray( - new JsonObject { ["id"] = "shape", ["sentence"] = "The claim." }, - new JsonObject - { - ["id"] = "parse", ["sentence"] = "The claim.", - ["inapplicable"] = new JsonArray(new JsonObject { ["axes"] = new JsonObject { ["x"] = new JsonArray("1"), ["z"] = new JsonArray("1") }, ["cite"] = "docs:README.md#a" }), - }), - }).ToJsonString()); - fx.Commit("three axes"); - fx.Ledger("universe", "--base", "HEAD").Exit.ShouldBe(0); - Gaps("\n").ShouldContain("pairs no passing check read both axes of (the last replay's evidence): none\n", Case.Sensitive, "no evidence yet: nothing to say"); - - static JsonObject Check(string obligation, string category, params (string Axis, string Value)[] read) => new() - { - ["obligation"] = obligation, ["category"] = category, ["baseline"] = "twin", - ["applied"] = new JsonObject(read.Select(r => KeyValuePair.Create(r.Axis, (JsonNode?)r.Value))), - }; - var evidence = Path.Combine(fx.Root, "evidence.json"); - File.WriteAllText(evidence, new JsonObject - { - ["variants"] = new JsonArray(new JsonObject { ["env"] = new JsonObject(), ["expected"] = new JsonArray(0), ["executed"] = new JsonArray(0) }), - ["rows"] = new JsonObject - { - ["0"] = new JsonObject - { - ["category"] = "pass", ["applied"] = new JsonObject(), ["findings"] = new JsonArray(), - ["checks"] = new JsonArray( - Check("shape", "pass", ("x", "1"), ("y", "1")), - Check("parse", "pass", ("x", "2"), ("z", "1")), - Check("parse", "inapplicable", ("y", "2"), ("z", "2"))), - }, - }, - }.ToJsonString()); - fx.Footprint(); - fx.Ledger("record-run", "--probe", "P", "--result", "pass", "--evidence", evidence).Exit.ShouldBe(0); - - Gaps("\n").ShouldContain(""" - pairs no passing check read both axes of (the last replay's evidence): 5 - C1: x=1 z=1 - C1: y=1 z=1 - C1: y=1 z=2 - C1: y=2 z=1 - C1: y=2 z=2 - - """.ReplaceLineEndings("\n")); - } -} diff --git a/test/CodoMetis.TypeKit.Prober.Tests/HarnessErrorTests.cs b/test/CodoMetis.TypeKit.Prober.Tests/HarnessErrorTests.cs deleted file mode 100644 index be0ec024..00000000 --- a/test/CodoMetis.TypeKit.Prober.Tests/HarnessErrorTests.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System.Text.Json.Nodes; -using Prober; -using Probes.Shapes; - -namespace CodoMetis.TypeKit.Prober.Tests; - -/// The harness writes rows to the process's PROBE_OUT, so its tests run one at a time. -[CollectionDefinition(nameof(ProbeOut), DisableParallelization = true)] -public sealed class ProbeOut; - -/// -/// A harness error is never a catch (decision 103): the probe corpus's own Rec.Run (harness/Row.cs, -/// compiled in) ends a row whose check throws as a harness error, not as a violation, and `seeds`, reading -/// the lines it writes, counts no such row caught, nor a violation whose reasons carry a harness error. -/// -[Collection(nameof(ProbeOut))] -public sealed class HarnessErrorTests : IDisposable -{ - private readonly Fixture fx = new(); - private readonly string cache; - - public HarnessErrorTests() - { - cache = fx.Root + "-cache"; - fx.Sh("git", "config", "core.autocrlf", "false"); - fx.Write(".claude/probe-axes.md", "```json probe-axes\n" + new JsonObject - { - ["axes"] = new JsonObject { ["x"] = new JsonArray("1", "2"), ["y"] = new JsonArray("1", "2") }, - ["surfaces"] = new JsonObject { ["s"] = new JsonObject { ["axes"] = new JsonArray("x", "y"), ["watched"] = new JsonArray("src") } }, - ["replay"] = new JsonObject { ["probeDir"] = "probes", ["include"] = new JsonArray("src") }, - }.ToJsonString() + "\n```\n"); - fx.Write("src/a.cs", "class A { /* DEFECT */ }\n"); - fx.Write("tools/probe/corpus/seeds/C1.patch", fx.Sh("git", "diff", "--", "src/a.cs")); - fx.Write("src/a.cs", "class A {}\n"); - fx.Write("tools/probe/corpus/registry.json", new JsonObject - { - ["probes"] = new JsonObject - { - ["P"] = new JsonObject { ["claim"] = "C1", ["filter"] = "*P", ["seed"] = new JsonObject { ["defect"] = "A holds the defect.", ["patch"] = "seeds/C1.patch" } }, - }, - }.ToJsonString()); - fx.Commit("seed"); - } - - public void Dispose() - { - fx.Dispose(); - try { Directory.Delete(cache, recursive: true); } catch (IOException) { } catch (UnauthorizedAccessException) { } - } - - /// Runs one row through the harness's Rec.Run, writing to ; the exception it ends the test with, if any. - private static Exception? Harness(string outDir, int idx, Func> check) - { - var previous = Environment.GetEnvironmentVariable("PROBE_OUT"); - Environment.SetEnvironmentVariable("PROBE_OUT", outDir); - try - { - Rec.Run("C1", new Row(idx, new Dictionary { ["x"] = "1", ["y"] = "1" }), check).GetAwaiter().GetResult(); - return null; - } - catch (Exception ended) { return ended; } - finally { Environment.SetEnvironmentVariable("PROBE_OUT", previous); } - } - - private static Task Throws(Row _) => throw new InvalidOperationException("the check broke"); - - [Fact] - public void A_check_that_throws_ends_as_a_harness_error_and_fails_the_test_naming_the_exception() - { - var outDir = Path.Combine(fx.Root, "rows-out"); - var ended = Harness(outDir, 1, Throws); - ended.ShouldNotBeNull().Message.ShouldContain("UNEXPECTED InvalidOperationException: the check broke"); - - var line = JsonNode.Parse(File.ReadAllLines(Path.Combine(outDir, "C1.jsonl")).ShouldHaveSingleItem())!; - Files.Str(line["category"]).ShouldBe("harness-error", "an exception is the harness breaking, never the claim's check firing"); - Files.Strings(line["reasons"]).ShouldBe(["harness-error:UNEXPECTED InvalidOperationException: the check broke"]); - Files.Str(line["status"]).ShouldBe("fail"); - } - - [Fact] - public void A_seed_that_only_makes_a_check_throw_is_missed_not_caught() - { - var (exit, output) = Seeds(new HarnessAdapter(Throws)); - exit.ShouldBe(1, output); - output.ShouldContain("P (C1): missed, no row violates that did not already (2 rows: harness-error 1, pass 1; 1 ended in a harness error, which is no catch;"); - output.ShouldNotContain("caught,"); - } - - [Fact] - public void A_violation_whose_reasons_carry_a_harness_error_is_no_catch() - { - var (exit, output) = Seeds(new HarnessAdapter(r => - { - r.Outcome("harness-error", "the check could not tell"); - return Task.FromResult("and failed anyway"); - })); - exit.ShouldBe(1, output); - output.ShouldContain("P (C1): missed, no row violates that did not already (2 rows: pass 1, violation 1; 1 ended in a harness error, which is no catch;"); - } - - [Fact] - public void A_violation_through_the_harness_is_a_catch() - { - var (exit, output) = Seeds(new HarnessAdapter(_ => Task.FromResult("the claim does not hold"))); - exit.ShouldBe(0, output); - output.ShouldContain("P (C1): caught, 1 of 2 rows violate; e.g. #1: violation:the claim does not hold"); - } - - private (int Exit, string Out) Seeds(IProbeAdapter adapter) - { - var output = new StringWriter(); - var context = new Context(fx.Root, new Dictionary { ["PROBE_CACHE_DIR"] = cache }, output, output); - int exit; - try { exit = global::Prober.Seeds.Run(context, new Args([], new HashSet(StringComparer.Ordinal)), adapter); } - catch (Refusal refusal) { output.WriteLine(refusal.Message); exit = refusal.ExitCode; } - return (exit, output.ToString()); - } - - /// - /// The probe P over C1's two rows, each run through the real harness: row 0 passes, and row 1 runs - /// while src/a.cs holds DEFECT. - /// - private sealed class HarnessAdapter(Func> seeded) : IProbeAdapter - { - public Ran Build(string worktree, IReadOnlyDictionary environment) => new(0, "Build: OK", ""); - - public Ran RunProbe(string worktree, string filter, string results, string coverageFile, IReadOnlyDictionary environment) - { - var defect = File.ReadAllText(Path.Combine(worktree, "src", "a.cs")).Contains("DEFECT", StringComparison.Ordinal); - var outDir = environment["PROBE_OUT"]!; - var failed = false; - for (var idx = 0; idx < 2; idx++) - failed |= Harness(outDir, idx, idx == 1 && defect ? seeded : _ => Task.FromResult(null)) is not null; - return new Ran(failed ? 1 : 0, "", ""); - } - - public (JsonObject? Axes, string? Error) DeriveAxes(string checkout, string workDirectory) => (null, "none"); - - public IReadOnlyDictionary Environment(string cacheDirectory) => new Dictionary(); - } -} diff --git a/test/CodoMetis.TypeKit.Prober.Tests/LedgerTests.cs b/test/CodoMetis.TypeKit.Prober.Tests/LedgerTests.cs deleted file mode 100644 index 62ec4ad4..00000000 --- a/test/CodoMetis.TypeKit.Prober.Tests/LedgerTests.cs +++ /dev/null @@ -1,350 +0,0 @@ -using System.Text.Json.Nodes; -using Prober; - -namespace CodoMetis.TypeKit.Prober.Tests; - -/// Coverage is earned only by replayed evidence, bound to the identity it ran under (the review's priority 1). -public sealed class AccountingTests : IDisposable -{ - private readonly Fixture fx = new(); - public void Dispose() => fx.Dispose(); - - private static Dictionary R(string x, string y) => new(StringComparer.Ordinal) { ["x"] = x, ["y"] = y }; - private static readonly IReadOnlyList> TwoRows = [R("1", "1"), R("2", "2")]; - - private void RecordPass(IReadOnlyList> applied, string category = "pass", int[]? executed = null) - { - fx.Footprint(); - fx.Ledger("record-run", "--probe", "P", "--result", "pass", "--evidence", fx.Evidence(applied, category, executed)).Exit.ShouldBe(0); - } - - [Fact] - public void A_pass_credits_the_pairs_of_the_rows_it_ran() - { - RecordPass(TwoRows); - fx.Covered().ShouldBe(2); - } - - [Fact] - public void The_status_line_counts_what_the_json_counts() - { - RecordPass(TwoRows); - fx.Ledger("status").Out.ShouldStartWith("main at "); - fx.Ledger("status").Out.ShouldContain(": 2 of 4 pairs current (50.0%), 0 open finding(s)"); - } - - [Fact] - public void An_old_pass_does_not_credit_a_row_added_since() - { - RecordPass(TwoRows); - fx.Rows([("1", "1"), ("2", "2"), ("1", "2")]); - fx.Commit("a new row"); - fx.Covered().ShouldBe(0, "the pass was for another corpus: nothing of it may count now"); - } - - [Fact] - public void A_changed_harness_voids_the_pass() - { - RecordPass(TwoRows); - fx.Write("tools/probe/corpus/harness/P.cs", "// probe P, changed\n"); - fx.Commit("harness"); - fx.Covered().ShouldBe(0); - } - - [Fact] - public void A_changed_axes_file_voids_the_pass() - { - RecordPass(TwoRows); - var path = Path.Combine(fx.Root, ".claude/probe-axes.md"); - fx.Write(".claude/probe-axes.md", File.ReadAllText(path).Replace("\"watched\":[\"src\"]", "\"watched\":[\"src\",\"README.md\"]", StringComparison.Ordinal)); - fx.Commit("axes"); - fx.Covered().ShouldBe(0); - } - - [Fact] - public void Only_the_settings_that_applied_earn_credit() - { - RecordPass([R("1", "1"), R("1", "2")]); // row 2 asked for x=2 and ran as x=1 - fx.Covered().ShouldBe(2, "x1y1 and the applied x1y2, never the requested x2y2"); - var rows = JsonNode.Parse(fx.Ledger("rows", "--claim", "C1").Out)!["rows"]!.AsArray(); - rows.SelectMany(r => r!["pairs"]!.AsArray()).Select(p => $"{p![0]} {p[1]}").ShouldContain("x=2 y=2"); - } - - [Fact] - public void An_axis_the_harness_never_read_earns_nothing() - { - RecordPass([new(StringComparer.Ordinal) { ["x"] = "1" }, new(StringComparer.Ordinal) { ["x"] = "2" }]); - fx.Covered().ShouldBe(0); - } - - [Fact] - public void A_refusal_is_no_evidence_of_success() - { - RecordPass(TwoRows, category: "expected-refusal"); - fx.Covered().ShouldBe(0); - } - - [Fact] - public void A_row_that_did_not_run_is_never_credited() - { - RecordPass(TwoRows, executed: [0]); - fx.Covered().ShouldBe(1); - } -} - -/// -/// What earned nothing is counted where the coverage is read, and a row that steps around a finding binds: the -/// gate refuses while one does, and the ledger names a skip whose finding is fixed or dismissed. -/// -public sealed class OutcomeTests : IDisposable -{ - private readonly Fixture fx = new(); - public void Dispose() => fx.Dispose(); - - private static Dictionary R(string x, string y) => new(StringComparer.Ordinal) { ["x"] = x, ["y"] = y }; - - private string Record(params (string Category, string? Finding)[] outcomes) - { - fx.Footprint(); - var evidence = fx.Evidence([R("1", "1"), R("2", "2")], outcomes: outcomes); - fx.Ledger("record-run", "--probe", "P", "--result", "pass", "--evidence", evidence).Exit.ShouldBe(0); - return evidence; - } - - private List StaleSkips(string evidence) => - new Ledger(new Context(fx.Root, new Dictionary(), TextWriter.Null, TextWriter.Null), null) - .StaleSkips(JsonNode.Parse(File.ReadAllText(evidence)) as JsonObject); - - private void Finding(string status, string commit) => - File.AppendAllText(Path.Combine(fx.BranchDir, "findings.jsonl"), - new JsonObject { ["id"] = "F9", ["claim"] = "C1", ["status"] = status, ["commit"] = commit }.ToJsonString() + "\n"); - - [Fact] - public void Status_counts_how_the_rows_of_a_claim_ended() - { - Record(("pass", null), ("expected-refusal", null)); - var outcomes = JsonNode.Parse(fx.Ledger("status", "--json").Out)!["claims"]!["C1"]!["outcomes"]!; - outcomes["pass"]!.GetValue().ShouldBe(1); - outcomes["expected-refusal"]!.GetValue().ShouldBe(1); - fx.Ledger("status").Out.ShouldContain("C1: 1/4 (rows: expected-refusal 1, pass 1; checks: expected-refusal 1, pass 1)"); - } - - [Fact] - public void A_skip_for_a_finding_is_shown_and_refused_by_the_gate_without_any_ledger_of_findings() - { - fx.Sh("git", "tag", "v1"); - Record(("pass", null), ("known-finding", "F9")); - fx.Ledger("status").Out.ShouldContain("1 row(s) skip for finding F9"); - var (exit, output, _) = fx.Ledger("gate", "--since", "v1", "--json"); - exit.ShouldNotBe(0); - JsonNode.Parse(output)!["reasons"]!.AsArray().Select(r => r!.GetValue()).ShouldContain("P: 1 row(s) skip for finding F9; fix it and remove the skip, or dismiss it"); - } - - [Fact] - public void A_skip_for_an_open_finding_stands() - { - var evidence = Record(("known-finding", "F9"), ("pass", null)); - Finding("open", ""); - StaleSkips(evidence).ShouldBeEmpty(); - } - - [Fact] - public void A_skip_for_a_fixed_finding_is_stale() - { - var evidence = Record(("known-finding", "F9"), ("pass", null)); - var head = fx.Sh("git", "rev-parse", "HEAD").Trim(); - Finding("open", ""); - Finding("fixed", head); - StaleSkips(evidence).ShouldBe([$"1 row(s) skip for F9, which is fixed at {head[..7]}: remove the skip"]); - } - - [Fact] - public void A_skip_for_a_dismissed_finding_is_stale() - { - var evidence = Record(("known-finding", "F9"), ("pass", null)); - Finding("open", ""); - fx.Write("tools/probe/corpus/dismissed.jsonl", new JsonObject { ["id"] = "F9", ["reason"] = "intended", ["date"] = "2026-10-15" }.ToJsonString() + "\n"); - StaleSkips(evidence).ShouldBe(["1 row(s) skip for F9, which is dismissed: remove the skip"]); - } -} - -/// One round at a time, on every OS: an exclusive create, liveness by heartbeat, a mutex for takeovers. -public sealed class LockTests : IDisposable -{ - private readonly Fixture fx = new(); - public void Dispose() => fx.Dispose(); - - [Fact] - public void A_second_holder_is_refused_while_the_first_is_alive() - { - fx.Ledger("lock", "--holder", "a", "--mode", "explore").Exit.ShouldBe(0); - fx.Ledger("lock", "--holder", "b", "--mode", "replay").Exit.ShouldNotBe(0); - } - - [Fact] - public void A_holder_whose_heartbeat_stopped_is_taken_over() - { - fx.Ledger("lock", "--holder", "a", "--mode", "explore").Exit.ShouldBe(0); - fx.AgeTheLock(); - fx.Ledger("lock", "--holder", "b", "--mode", "replay").Exit.ShouldBe(0); - } - - [Fact] - public void A_heartbeat_keeps_the_holder_alive() - { - fx.Ledger("lock", "--holder", "a", "--mode", "explore").Exit.ShouldBe(0); - fx.AgeTheLock(); - fx.Ledger("heartbeat", "--holder", "a").Exit.ShouldBe(0); - fx.Ledger("lock", "--holder", "b", "--mode", "replay").Exit.ShouldNotBe(0); - } - - [Fact] - public void Two_takers_at_once_get_one_lock() => RaceOf(8).ShouldBe(1); - - [Fact] - public void Racing_takers_of_a_dead_holder_get_one_lock() - { - fx.Ledger("lock", "--holder", "dead", "--mode", "explore").Exit.ShouldBe(0); - fx.AgeTheLock(); - RaceOf(8).ShouldBe(1); - } - - [Fact] - public void Racing_threads_over_a_dead_holder_get_one_lock() - { - // Threads released together by a barrier collide in the critical section far more tightly than - // processes can; ten rounds, each must end with exactly one holder. - for (var round = 0; round < 10; round++) - { - fx.Ledger("lock", "--holder", $"dead{round}", "--mode", "explore").Exit.ShouldBe(0); - fx.AgeTheLock(); - const int takers = 16; - using var barrier = new Barrier(takers); - var exits = new int[takers]; - var threads = Enumerable.Range(0, takers).Select(i => new Thread(() => - { - barrier.SignalAndWait(); - exits[i] = fx.Ledger("lock", "--holder", $"t{round}-{i}", "--mode", "replay").Exit; - })).ToList(); - threads.ForEach(t => t.Start()); - threads.ForEach(t => t.Join()); - exits.Count(e => e == 0).ShouldBe(1, $"round {round}"); - File.Delete(fx.LockFile()); - } - } - - /// - /// What crashed the test host on windows-latest (run 38732628888): a taker creating round.lock.mutex while - /// its last holder's delete was still pending got UnauthorizedAccessException, which Windows throws there - /// instead of IOException, and the mutex loop let it through. Here the OS refuses the create every time, - /// the same exception on every platform: a directory in the mutex's place on Windows, a state directory - /// without write permission elsewhere. A taker waits, then refuses; it never throws. - /// - [Fact] - public void A_mutex_the_OS_refuses_to_create_is_waited_for_and_refused_not_thrown() - { - var directory = Path.GetDirectoryName(fx.LockFile())!; - Directory.CreateDirectory(directory); - var mutex = fx.LockFile() + ".mutex"; - if (OperatingSystem.IsWindows()) Directory.CreateDirectory(mutex); - else File.SetUnixFileMode(directory, UnixFileMode.UserRead | UnixFileMode.UserExecute); - try - { - var (exit, _, error) = fx.Ledger("lock", "--holder", "a", "--mode", "explore"); - - exit.ShouldNotBe(0); - error.ShouldContain("mutex"); - } - finally - { - if (OperatingSystem.IsWindows()) Directory.Delete(mutex); - else File.SetUnixFileMode(directory, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute); - } - } - - [Fact] - public void A_lock_of_one_branch_leaves_another_branchs_lock_free() - { - var other = fx.Linked("other"); - fx.Ledger("lock", "--holder", "a", "--mode", "explore").Exit.ShouldBe(0); - - var (exit, output, error) = fx.LedgerIn(other, "lock", "--holder", "b", "--mode", "explore"); - - exit.ShouldBe(0, error + output); - File.Exists(fx.LockFile("other")).ShouldBeTrue(); - File.Exists(fx.LockFile("main")).ShouldBeTrue("a's lock is still held"); - } - - [Fact] - public void A_second_lock_of_one_branch_is_refused_from_another_checkout() - { - var other = fx.Linked("other"); - fx.Ledger("lock", "--holder", "a", "--mode", "explore").Exit.ShouldBe(0); - - fx.LedgerIn(other, "lock", "--branch", "main", "--holder", "b", "--mode", "replay").Exit.ShouldNotBe(0); - } - - private int RaceOf(int takers) - { - var processes = Enumerable.Range(0, takers).Select(i => fx.Spawn("lock", "--holder", $"h{i}", "--mode", "replay")).ToList(); - foreach (var p in processes) { p.StandardOutput.ReadToEnd(); p.StandardError.ReadToEnd(); p.WaitForExit(); } - return processes.Count(p => p.ExitCode == 0); - } -} - -/// The release gate needs no model: every obligation is a probe or a reviewed waiver. -public sealed class GateTests : IDisposable -{ - private readonly Fixture fx = new(); - public void Dispose() => fx.Dispose(); - - private void TouchTheClaim() - { - fx.Write("README.md", "# a\n\nThe claim, reworded.\n"); - fx.Commit("docs"); - } - - [Fact] - public void An_unprobed_touched_claim_fails_with_what_is_missing() - { - fx.Sh("git", "tag", "v1"); - TouchTheClaim(); - var (exit, output, _) = fx.Ledger("gate", "--since", "v1", "--json"); - exit.ShouldNotBe(0); - JsonNode.Parse(output)!["reasons"]!.AsArray().Select(r => r!.GetValue()).ShouldContain(r => r.StartsWith("C1:", StringComparison.Ordinal)); - } - - [Fact] - public void A_reviewed_waiver_meets_the_obligation_without_a_probe() - { - fx.Sh("git", "tag", "v1"); - fx.Write("tools/probe/corpus/registry.json", """{"probes": {}}"""); - fx.Write("tools/probe/corpus/waivers.json", """[{"claim": "C1", "reason": "covered by the suite's C1Tests", "reviewed_by": "maintainer", "date": "2026-10-15"}]"""); - TouchTheClaim(); - var (exit, _, error) = fx.Ledger("gate", "--since", "v1"); - exit.ShouldBe(0, error); - } - - [Fact] - public void An_unsigned_waiver_meets_nothing() - { - fx.Sh("git", "tag", "v1"); - fx.Write("tools/probe/corpus/registry.json", """{"probes": {}}"""); - fx.Write("tools/probe/corpus/waivers.json", """[{"claim": "C1", "reason": "later"}]"""); - TouchTheClaim(); - fx.Ledger("gate", "--since", "v1").Exit.ShouldNotBe(0); - } - - [Fact] - public void State_goes_where_PROBE_STATE_DIR_says() - { - var state = Path.Combine(Path.GetTempPath(), "prober-state-" + Guid.NewGuid().ToString("N")); - try - { - fx.LedgerWith(new() { ["PROBE_STATE_DIR"] = state }, "lock", "--holder", "ci", "--mode", "replay").Exit.ShouldBe(0); - Directory.EnumerateFiles(state, "round.lock", SearchOption.AllDirectories).ShouldNotBeEmpty(); - File.Exists(fx.LockFile()).ShouldBeFalse(); - } - finally { if (Directory.Exists(state)) Directory.Delete(state, recursive: true); } - } -} diff --git a/test/CodoMetis.TypeKit.Prober.Tests/NewProbeTests.cs b/test/CodoMetis.TypeKit.Prober.Tests/NewProbeTests.cs deleted file mode 100644 index 2d9071e6..00000000 --- a/test/CodoMetis.TypeKit.Prober.Tests/NewProbeTests.cs +++ /dev/null @@ -1,193 +0,0 @@ -using System.Text.Json.Nodes; -using System.Text.RegularExpressions; -using Prober; - -namespace CodoMetis.TypeKit.Prober.Tests; - -/// -/// `new` stages what a probe writer used to do by hand: the skeleton in the harness's form with the claim's -/// obligations quoted and one throwing check each, the claim's rows, and the registry entry with no seed yet, -/// so the seeds every obligation owes are named. C1 lists two obligations; C7-two-words lists none. -/// -public sealed class NewProbeTests : IDisposable -{ - private readonly Fixture fx = new(); - - public NewProbeTests() - { - fx.Write("README.md", "# a\n\nThe claim.\n\nA key is written as the wrapped type writes it.\n\nA null is refused.\n"); - fx.Write(".claude/probe-axes.md", "```json probe-axes\n" + new JsonObject - { - ["axes"] = new JsonObject { ["x"] = new JsonArray("1", "2"), ["y"] = new JsonArray("1", "2") }, - ["surfaces"] = new JsonObject { ["s"] = new JsonObject { ["axes"] = new JsonArray("x", "y"), ["watched"] = new JsonArray("src") } }, - ["replay"] = new JsonObject { ["probeDir"] = "probes", ["include"] = new JsonArray("src"), ["families"] = new JsonObject { ["f"] = new JsonArray("s") } }, - }.ToJsonString() + "\n```\n"); - fx.Write("tools/probe/corpus/claims.json", new JsonArray( - new JsonObject - { - ["id"] = "C1", ["source"] = "README.md#a", ["surfaces"] = new JsonArray("s"), - ["obligations"] = new JsonArray( - new JsonObject { ["id"] = "keys", ["sentence"] = "A key is written as the wrapped type writes it." }, - new JsonObject { ["id"] = "nulls", ["sentence"] = "A null is refused." }), - }, - new JsonObject { ["id"] = "C7-two-words", ["source"] = "README.md#a", ["surfaces"] = new JsonArray("s") }).ToJsonString()); - fx.Commit("claims with obligations"); - fx.Ledger("universe", "--base", "HEAD").Exit.ShouldBe(0); - } - - public void Dispose() => fx.Dispose(); - - private string Staged(string relative) => Path.Combine(fx.BranchDir, "staging", relative); - - [Fact] - public void It_stages_the_skeleton_the_rows_and_the_registry_entry() - { - var (exit, output, error) = fx.Ledger("new", "--claim", "C1", "--probe", "C1KeysProbe"); - exit.ShouldBe(0, output + error); - - var source = File.ReadAllText(Staged("probes/C1KeysProbe.cs")); - source.ShouldContain("// Claim C1 (README.md#a), surfaces s, family f."); - source.ShouldContain("// keys: \"A key is written as the wrapped type writes it.\""); - source.ShouldContain("// nulls: \"A null is refused.\""); - source.ShouldContain("// TODO keys: \"A key is written as the wrapped type writes it.\""); - source.ShouldContain("// TODO nulls: \"A null is refused.\""); - - // The probe's own rows, C1's other probe P's untouched: P has covered nothing, so a covering array over all four pairs. - var staged = JsonNode.Parse(File.ReadAllText(Staged("rows/C1KeysProbe.json")))!; - staged["claim"]!.GetValue().ShouldBe("C1"); - staged["rows"]!.AsArray().Select(r => $"{r!["row"]!["x"]}{r["row"]!["y"]}").Order().ShouldBe(["11", "12", "21", "22"]); - File.Exists(Staged("rows/P.json")).ShouldBeFalse("a new probe leaves its sibling's rows as they are"); - - var entry = JsonNode.Parse(File.ReadAllText(Staged("registry.json")))!["probes"]!["C1KeysProbe"]!.AsObject(); - entry["claim"]!.GetValue().ShouldBe("C1"); - entry["filter"]!.GetValue().ShouldBe("*C1KeysProbe"); - entry["seeds"]!.AsArray().ShouldBeEmpty(); - entry.ContainsKey("seed").ShouldBeFalse(); - - output.ShouldContain("still owed, per obligation:"); - output.ShouldContain(" keys: the check CheckKeys"); - output.ShouldContain(" nulls: the check CheckNulls"); - } - - [Fact] - public void The_seeds_each_obligation_owes_are_named() - { - fx.Ledger("new", "--claim", "C1", "--probe", "C1KeysProbe").Exit.ShouldBe(0); - var problems = global::Prober.Seeds.Problems(new Ledger(new Context(fx.Root, new Dictionary(), TextWriter.Null, TextWriter.Null), null)); - problems.ShouldContain("C1KeysProbe: no seed, its registry entry names none"); - problems.ShouldContain("C1: obligation keys has no seed in any of its probes"); - problems.ShouldContain("C1: obligation nulls has no seed in any of its probes"); - - var (exit, output, _) = fx.Ledger("seeds", "--dry-run"); - exit.ShouldBe(0); - output.ShouldContain(" C1KeysProbe/keys: (none)"); - output.ShouldContain(" C1KeysProbe/nulls: (none)"); - } - - [Fact] - public void A_claim_not_in_the_claims_is_refused_and_nothing_staged() - { - var (exit, _, error) = fx.Ledger("new", "--claim", "C9"); - exit.ShouldBe(1); - error.ShouldContain("no claim C9 in claims.json"); - Directory.Exists(Path.Combine(fx.BranchDir, "staging")).ShouldBeFalse(); - } - - [Fact] - public void A_name_a_probe_has_is_refused_and_nothing_changes() - { - fx.Write("tools/probe/corpus/registry.json", """{"probes": {"P": {"claim": "C1", "filter": "*P"}, "C1OldProbe": {"claim": "C1", "filter": "*C1OldProbe"}}}"""); - fx.Commit("a second probe of C1"); - var (exit, _, error) = fx.Ledger("new", "--claim", "C1", "--probe", "C1OldProbe"); - exit.ShouldBe(1); - error.ShouldContain("a probe C1OldProbe exists already (in the registry)"); - fx.Ledger("new", "--claim", "C1", "--probe", "P").Exit.ShouldBe(2, "a name that does not end in Probe"); - Directory.Exists(Path.Combine(fx.BranchDir, "staging")).ShouldBeFalse(); - - fx.Ledger("new", "--claim", "C1", "--probe", "C1KeysProbe").Exit.ShouldBe(0); - var registry = File.ReadAllText(Staged("registry.json")); - var source = File.ReadAllText(Staged("probes/C1KeysProbe.cs")); - fx.Ledger("new", "--claim", "C7-two-words", "--probe", "C1KeysProbe").Exit.ShouldBe(1, "a second `new` of one name"); - File.ReadAllText(Staged("registry.json")).ShouldBe(registry); - File.ReadAllText(Staged("probes/C1KeysProbe.cs")).ShouldBe(source); - } - - [Theory] - [InlineData("C20-testing-shouldhold", "C20TestingShouldholdProbe")] - [InlineData("E02-value-translates-to-column", "E02ValueTranslatesToColumnProbe")] - [InlineData("L01-json-bytes", "L01JsonBytesProbe")] - public void The_name_defaults_to_the_claim_s_prefix_and_words(string claim, string probe) => NewProbe.DefaultName(claim).ShouldBe(probe); - - [Fact] - public void Without_a_name_the_default_is_staged() - { - fx.Ledger("new", "--claim", "C7-two-words").Exit.ShouldBe(0); - // The registry's key, not the file: a file system that ignores case finds the file under another spelling. - JsonNode.Parse(File.ReadAllText(Staged("registry.json")))!["probes"]!.AsObject().Select(kv => kv.Key).ShouldBe(["C7TwoWordsProbe"]); - File.ReadAllText(Staged("probes/C7TwoWordsProbe.cs")).ShouldContain("// claim: the claim as a whole"); - } - - [Fact] - public void A_family_that_is_not_the_claim_s_is_refused() - { - var (exit, _, error) = fx.Ledger("new", "--claim", "C1", "--probe", "C1KeysProbe", "--family", "g"); - exit.ShouldBe(2); - error.ShouldContain("claim C1 is of family f (its first surface, s), not g"); - fx.Ledger("new", "--claim", "C1", "--probe", "C1KeysProbe", "--family", "f").Exit.ShouldBe(0); - } - - /// - /// The lines that make a probe a probe of this harness, with its own names taken out: the namespace, the - /// class, the rows as member data, the theory and the row's run through Rec.Run with a check of the row. - /// - private static List Form(string source) => - [.. source.Split('\n').Select(line => line.Trim()).Select(line => line switch - { - _ when line.StartsWith("namespace ", StringComparison.Ordinal) => line, - _ when Regex.IsMatch(line, @"^public sealed class \w+Probe$") => "public sealed class {probe}", - _ when line.StartsWith("public static TheoryData Data()", StringComparison.Ordinal) => - Regex.Replace(line, @"Rows\.Data\(""[^""]+""\)", "Rows.Data({probe})"), - _ when line.StartsWith("[Theory", StringComparison.Ordinal) => line, - _ when line.StartsWith("public Task ", StringComparison.Ordinal) => Regex.Replace(line, @"Rec\.Run\(probe, idx, [^;]+\);$", "Rec.Run(probe, idx, {check});"), - _ => null, - }).OfType()]; - - private static readonly string Corpus = Path.Combine(RepositoryRoot(), "tools", "probe", "corpus", "probes"); - - [Theory] - [InlineData("C08SameNameWarningProbe")] - [InlineData("C12DescribedConverterProbe")] - [InlineData("C20ShouldHoldProbe")] - public void The_skeleton_has_the_form_of_the_corpus_s_probes(string probe) - { - fx.Ledger("new", "--claim", "C1", "--probe", "C1KeysProbe").Exit.ShouldBe(0); - var skeleton = File.ReadAllText(Staged("probes/C1KeysProbe.cs")); - var corpus = File.ReadAllText(Path.Combine(Corpus, probe + ".cs")); - Form(skeleton).ShouldBe(Form(corpus)); - Form(skeleton).Count.ShouldBe(5, "namespace, class, rows, theory and run"); - var usings = corpus.Split('\n').Where(l => l.StartsWith("using ", StringComparison.Ordinal)).Select(l => l.Trim()).ToList(); - skeleton.Split('\n').Where(l => l.StartsWith("using ", StringComparison.Ordinal)).ShouldAllBe(l => usings.Contains(l.Trim())); - } - - [Fact] - public void Every_obligation_s_check_throws_until_it_is_written() - { - fx.Ledger("new", "--claim", "C1", "--probe", "C1KeysProbe").Exit.ShouldBe(0); - var skeleton = File.ReadAllText(Staged("probes/C1KeysProbe.cs")); - foreach (var method in new[] { "CheckKeys", "CheckNulls" }) - { - skeleton.ShouldContain($"static Task {method}(Row r) => throw new NotImplementedException("); - Regex.IsMatch(skeleton, $@"static async Task Check\(Row r\) =>[^;]*await {method}\(r\)").ShouldBeTrue($"Check runs {method}"); - } - Regex.Matches(skeleton, @"=> throw new NotImplementedException\(").Count.ShouldBe(2); - skeleton.ShouldNotContain("return null"); - skeleton.ShouldNotContain("FromResult"); - } - - private static string RepositoryRoot() - { - var root = AppContext.BaseDirectory; - while (!File.Exists(Path.Combine(root, "CodoMetis.TypeKit.slnx"))) root = Path.GetDirectoryName(root) ?? throw new InvalidOperationException("no CodoMetis.TypeKit.slnx above the test output"); - return root; - } -} diff --git a/test/CodoMetis.TypeKit.Prober.Tests/ObligationsTests.cs b/test/CodoMetis.TypeKit.Prober.Tests/ObligationsTests.cs deleted file mode 100644 index 94369bb4..00000000 --- a/test/CodoMetis.TypeKit.Prober.Tests/ObligationsTests.cs +++ /dev/null @@ -1,228 +0,0 @@ -using System.Text.Json.Nodes; -using Prober; - -namespace CodoMetis.TypeKit.Prober.Tests; - -/// -/// A claim lists what it promises, each obligation a sentence quoted from its source, and what it does not -/// promise, each with the sentence that makes it intended; every obligation of a claim with probes has a seed -/// that names it (decision 104). The probe here is a fake adapter: row N of C1 violates while SEED_ROW is N. -/// -public sealed class ObligationsTests : IDisposable -{ - private readonly Fixture fx = new(); - private readonly string cache; - private readonly FakeAdapter adapter = new(); - - public ObligationsTests() - { - cache = fx.Root + "-cache"; - fx.Sh("git", "config", "core.autocrlf", "false"); - fx.Write("README.md", "# a\n\nThe claim.\n\nA key is written as the wrapped type writes it.\n\nA null is refused.\n"); - fx.Write("design/adr/0001-limits.md", "# 1. Limits\n\nAn ordering of keys is not promised.\n"); - fx.Write(".claude/probe-axes.md", "```json probe-axes\n" + new JsonObject - { - ["axes"] = new JsonObject { ["x"] = new JsonArray("1", "2"), ["y"] = new JsonArray("1", "2") }, - ["surfaces"] = new JsonObject { ["s"] = new JsonObject { ["axes"] = new JsonArray("x", "y"), ["watched"] = new JsonArray("src") } }, - ["replay"] = new JsonObject { ["probeDir"] = "probes", ["include"] = new JsonArray("src") }, - }.ToJsonString() + "\n```\n"); - fx.Commit("sources and replay configuration"); - } - - public void Dispose() - { - fx.Dispose(); - try { Directory.Delete(cache, recursive: true); } catch (IOException) { } catch (UnauthorizedAccessException) { } - } - - private static JsonObject Obligation(string id, string sentence) => new() { ["id"] = id, ["sentence"] = sentence }; - - private static JsonArray TwoObligations() => new( - Obligation("keys", "A key is written as the wrapped type writes it."), - Obligation("nulls", "A null is refused.")); - - private void Claims(JsonObject claim) - { - var c = new JsonObject { ["id"] = "C1", ["source"] = "README.md#a", ["surfaces"] = new JsonArray("s") }; - foreach (var (key, value) in claim) c[key] = value?.DeepClone(); - fx.Write("tools/probe/corpus/claims.json", new JsonArray(c).ToJsonString()); - } - - /// Registers P for C1 with the seeds given, each an environment toggle that makes the check of its obligation violate in row row. - private void Register(JsonObject? seed = null, params (string Obligation, string Row)[] seeds) - { - var entry = new JsonObject { ["claim"] = "C1", ["filter"] = "*P" }; - if (seed is not null) entry["seed"] = seed; - if (seeds.Length > 0) - entry["seeds"] = new JsonArray([.. seeds.Select(s => (JsonNode)new JsonObject - { - ["obligation"] = s.Obligation, ["env"] = new JsonObject { ["SEED_ROW"] = s.Row, ["SEED_CHECK"] = s.Obligation }, ["defect"] = $"{s.Obligation} is broken.", - })]); - fx.Write("tools/probe/corpus/registry.json", new JsonObject { ["probes"] = new JsonObject { ["P"] = entry } }.ToJsonString()); - fx.Commit("register"); - } - - private static JsonObject EnvSeed() => new() { ["env"] = new JsonObject { ["SEED_ROW"] = "1" }, ["defect"] = "Row 1 is broken." }; - - private List Problems() => Seeds.Problems(new Ledger(new Context(fx.Root, new Dictionary(), TextWriter.Null, TextWriter.Null), null)); - - private JsonObject Status() - { - var (exit, output, error) = fx.Ledger("status", "--json"); - exit.ShouldBe(0, error); - return JsonNode.Parse(output)!["claims"]!["C1"]!.AsObject(); - } - - /// Each obligation of the claim in `status --json`, as "id: the probes whose seeds break it". - private static List ObligationsOf(JsonObject claim) => - [.. claim["obligations"]!.AsArray().Select(o => $"{o!["id"]!.GetValue()}: {string.Join(", ", o["seeded_by"]!.AsArray().Select(p => p!.GetValue()))}")]; - - [Fact] - public void An_obligation_quoting_a_sentence_its_source_does_not_hold_is_refused() - { - Claims(new JsonObject { ["obligations"] = new JsonArray(Obligation("keys", "A key is written by ToString().")) }); - var (exit, _, error) = fx.Ledger("universe", "--base", "HEAD"); - exit.ShouldBe(1); - error.ShouldContain("claim C1: obligation keys quotes a sentence not found in README.md"); - } - - [Fact] - public void A_not_promised_entry_is_checked_against_its_own_source() - { - // The sentence is in the claim's README, not in the record the entry names, so it is refused. - Claims(new JsonObject - { - ["not-promised"] = new JsonArray(new JsonObject { ["id"] = "order", ["source"] = "design/adr/0001-limits.md#1-limits", ["sentence"] = "A null is refused." }), - }); - var (exit, _, error) = fx.Ledger("universe", "--base", "HEAD"); - exit.ShouldBe(1); - error.ShouldContain("claim C1: not-promised order quotes a sentence not found in design/adr/0001-limits.md"); - } - - [Fact] - public void A_not_promised_entry_its_source_holds_is_kept() - { - Claims(new JsonObject - { - ["not-promised"] = new JsonArray(new JsonObject { ["id"] = "order", ["source"] = "design/adr/0001-limits.md#1-limits", ["sentence"] = "An ordering of keys is not promised." }), - }); - var (exit, _, error) = fx.Ledger("universe", "--base", "HEAD"); - exit.ShouldBe(0, error); - } - - [Fact] - public void A_duplicate_obligation_id_is_refused() - { - Claims(new JsonObject - { - ["obligations"] = new JsonArray(Obligation("keys", "A key is written as the wrapped type writes it."), Obligation("keys", "A null is refused.")), - }); - var (exit, _, error) = fx.Ledger("universe", "--base", "HEAD"); - exit.ShouldBe(1); - error.ShouldContain("claim C1: obligation keys is listed twice"); - } - - [Fact] - public void A_claim_without_obligations_has_one_the_claim_seeded_by_a_seed_naming_none() - { - Register(EnvSeed()); - ObligationsOf(Status()).ShouldBe(["claim: P"]); - Problems().ShouldBeEmpty(); - } - - [Theory] - [InlineData("\n")] - [InlineData("\r\n")] - public void Status_shows_the_obligations_no_seed_breaks(string newLine) - { - Claims(new JsonObject { ["obligations"] = TwoObligations() }); - Register(null, ("keys", "1")); - ObligationsOf(Status()).ShouldBe(["keys: P", "nulls: "]); - var (exit, output, error) = fx.LedgerEnding([], newLine, "status"); - exit.ShouldBe(0, error); - output.ReplaceLineEndings("\n").ShouldContain("\n C1: 0/4 obligations 2 (1 without a seed: nulls), 2 uncredited\n"); - - Register(null, ("keys", "1"), ("nulls", "0")); - fx.LedgerEnding([], newLine, "status").Out.ReplaceLineEndings("\n").ShouldContain("\n C1: 0/4 obligations 2, each seeded, 2 uncredited\n"); - } - - [Fact] - public void The_check_without_a_build_names_an_obligation_no_seed_breaks() - { - Claims(new JsonObject { ["obligations"] = TwoObligations() }); - Register(EnvSeed(), ("keys", "1")); - Problems().ShouldBe(["C1: obligation nulls has no seed in any of its probes"], "a seed naming no obligation proves the probe, not an obligation the claim lists"); - Register(null, ("keys", "1"), ("nulls", "0")); - Problems().ShouldBeEmpty(); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void The_check_without_a_build_names_a_seed_naming_an_obligation_the_claim_does_not_list(bool listed) - { - if (listed) Claims(new JsonObject { ["obligations"] = new JsonArray(Obligation("keys", "A key is written as the wrapped type writes it.")) }); - Register(null, ("keys", "1"), ("kyes", "0")); - Problems().ShouldBe(listed - ? ["P/kyes: names obligation kyes, which claim C1 does not list"] - : ["P/keys: names obligation keys, which claim C1 does not list", "P/kyes: names obligation kyes, which claim C1 does not list"]); - } - - [Fact] - public void Seeds_runs_each_seed_under_its_obligation() - { - Claims(new JsonObject { ["obligations"] = TwoObligations() }); - Register(null, ("keys", "1"), ("nulls", "7")); - var output = new StringWriter(); var error = new StringWriter(); - var context = new Context(fx.Root, new Dictionary { ["PROBE_CACHE_DIR"] = cache }, output, error); - var exit = Seeds.Run(context, new Args([], new HashSet(StringComparer.Ordinal) { "dry-run" }), adapter); - - exit.ShouldBe(1, "the nulls seed makes no row violate, so it is missed and fails the command"); - var text = output.ToString().ReplaceLineEndings("\n"); - text.ShouldContain(": 2 seeds of 1 probes, in "); - text.ShouldContain("\n P/keys (C1): caught, 1 of 2 rows violate; e.g. #1 keys: violation:row 1 is broken\n"); - text.ShouldContain("\n P/nulls (C1): missed, no row violates that did not already (2 rows: pass 2;"); - var last = JsonNode.Parse(File.ReadAllText(Path.Combine(fx.BranchDir, "seeds-last.json")))!["seeds"]!.AsArray() - .Select(s => (s!["probe"]!.GetValue(), s["obligation"]!.GetValue(), s["seed"]!.GetValue(), s["result"]!.GetValue())); - last.ShouldBe([("P", "keys", "SEED_ROW=1,SEED_CHECK=keys", "caught"), ("P", "nulls", "SEED_ROW=7,SEED_CHECK=nulls", "missed")]); - } - - /// - /// The probe P over C1's two rows, without a build: row N violates where SEED_ROW is N, by its check of the - /// obligation SEED_CHECK names where it names one (each row then runs that check), else as a row without checks. - /// - private sealed class FakeAdapter : IProbeAdapter - { - public Ran Build(string worktree, IReadOnlyDictionary environment) => new(0, "Build: OK", ""); - - public Ran RunProbe(string worktree, string filter, string results, string coverageFile, IReadOnlyDictionary environment) - { - var broken = environment.GetValueOrDefault("SEED_ROW"); - var check = environment.GetValueOrDefault("SEED_CHECK"); - var outDir = environment["PROBE_OUT"]!; - Directory.CreateDirectory(outDir); - var lines = Enumerable.Range(0, 2).Select(idx => - { - var violates = broken == idx.ToString(System.Globalization.CultureInfo.InvariantCulture); - var line = new JsonObject - { - ["idx"] = idx, ["category"] = violates ? "violation" : "pass", - ["reasons"] = violates && check is null ? new JsonArray($"violation:row {idx} is broken") : new JsonArray(), - }; - if (check is not null) - line["checks"] = new JsonArray(new JsonObject - { - ["obligation"] = check, ["baseline"] = "twin", ["category"] = violates ? "violation" : "pass", - ["reasons"] = violates ? new JsonArray($"violation:row {idx} is broken") : new JsonArray(), - }); - return line.ToJsonString(); - }).ToList(); - File.WriteAllText(Path.Combine(outDir, "C1.jsonl"), string.Join('\n', lines) + "\n"); - return new Ran(lines.Any(l => l.Contains("\"violation\"", StringComparison.Ordinal)) ? 1 : 0, "", ""); - } - - public (JsonObject? Axes, string? Error) DeriveAxes(string checkout, string workDirectory) => (null, "none"); - - public IReadOnlyDictionary Environment(string cacheDirectory) => new Dictionary(); - } -} diff --git a/test/CodoMetis.TypeKit.Prober.Tests/PerCheckTests.cs b/test/CodoMetis.TypeKit.Prober.Tests/PerCheckTests.cs deleted file mode 100644 index 6602c34b..00000000 --- a/test/CodoMetis.TypeKit.Prober.Tests/PerCheckTests.cs +++ /dev/null @@ -1,492 +0,0 @@ -using System.Text.Json.Nodes; -using Prober; -using Probes.Shapes; - -namespace CodoMetis.TypeKit.Prober.Tests; - -/// Runs rows through the probe corpus's own harness (harness/Row.cs, compiled in) and reads the lines it writes. -internal static class HarnessRun -{ - /// - /// Runs one row of axes x and y through Rec.Run, writing to : the exception the - /// test ends with (a failure or a skip), if any, and the line the harness wrote for the row. - /// - public static (Exception? Ended, JsonObject Line) Row(string outDir, int idx, Func> check, string x = "1", string y = "1") - { - var previous = Environment.GetEnvironmentVariable("PROBE_OUT"); - Environment.SetEnvironmentVariable("PROBE_OUT", outDir); - Exception? ended = null; - try { Rec.Run("C1", new Row(idx, new Dictionary { ["x"] = x, ["y"] = y }), check).GetAwaiter().GetResult(); } - catch (Exception e) { ended = e; } - finally { Environment.SetEnvironmentVariable("PROBE_OUT", previous); } - var line = File.ReadAllLines(Path.Combine(outDir, "C1.jsonl")).Select(l => JsonNode.Parse(l)!.AsObject()).Last(l => (int)Files.Num(l["idx"]) == idx); - return (ended, line); - } - - public static JsonObject Check(JsonObject line, string obligation) => - line["checks"]!.AsArray().OfType().Single(c => Files.Str(c["obligation"]) == obligation); -} - -/// -/// A check of a row (Row.Check) has one obligation, one baseline, and reads, notes and an outcome of its own: a -/// failure it returns is a violation, an exception a harness error that the other checks of the row survive, and a -/// pass with no baseline a harness error. The row passes when no check failed and one passed, and is skipped, each -/// check's outcome listed, when none passed. -/// -[Collection(nameof(ProbeOut))] -public sealed class CheckHarnessTests : IDisposable -{ - private readonly string outDir = Path.Combine(Path.GetTempPath(), "prober-tests", Guid.NewGuid().ToString("N")); - - public void Dispose() - { - try { Directory.Delete(outDir, recursive: true); } catch (IOException) { } catch (UnauthorizedAccessException) { } - } - - [Fact] - public void A_failure_a_check_returns_is_its_violation_with_its_own_reads() - { - string? returned = "not run"; - var (ended, line) = HarnessRun.Row(outDir, 0, async r => - { - _ = r["x"]; - returned = await r.Check("keys", Baseline.Twin, c => Task.FromResult(c["y"] == "1" ? "the key differs" : null)); - return returned; - }); - returned.ShouldBe("the key differs", "Check returns the failure, so a probe can stop the row at it"); - ended.ShouldNotBeNull().Message.ShouldContain("keys violation: the key differs"); - Files.Str(line["category"]).ShouldBe("violation"); - Files.StringMap(line["applied"]).ShouldBe(new Dictionary { ["x"] = "1" }, "the row's own reads are those outside any check"); - Files.Strings(line["reasons"]).ShouldBeEmpty("the failure is the check's, not the row's own"); - var check = HarnessRun.Check(line, "keys"); - Files.Str(check["baseline"]).ShouldBe("twin"); - Files.Str(check["category"]).ShouldBe("violation"); - Files.StringMap(check["applied"]).ShouldBe(new Dictionary { ["y"] = "1" }); - Files.Strings(check["reasons"]).ShouldBe(["violation:the key differs"]); - } - - [Fact] - public void An_exception_in_a_check_is_a_harness_error_the_other_checks_survive() - { - string? returned = "not run"; - var (ended, line) = HarnessRun.Row(outDir, 0, async r => - { - returned = await r.Check("keys", Baseline.Twin, (Func>)(_ => throw new InvalidOperationException("the check broke"))); - return r.Check("nulls", Baseline.RoundTrip, c => c.Is("x", "1") ? null : "never"); - }); - returned.ShouldBeNull("an exception never reads as a failure, so a probe returning it makes no violation"); - ended.ShouldNotBeNull().Message.ShouldContain("keys harness-error: UNEXPECTED InvalidOperationException: the check broke"); - Files.Str(line["category"]).ShouldBe("harness-error"); - Files.Str(HarnessRun.Check(line, "keys")["category"]).ShouldBe("harness-error"); - Files.Str(HarnessRun.Check(line, "nulls")["category"]).ShouldBe("pass", "the next check of the row still ran"); - } - - [Fact] - public void The_synchronous_form_keeps_an_exception_from_the_row_too() - { - var (ended, line) = HarnessRun.Row(outDir, 0, r => - { - var returned = r.Check("keys", Baseline.Twin, (Func)(_ => throw new FormatException("bad"))); - return Task.FromResult(returned); - }); - ended.ShouldNotBeNull().Message.ShouldContain("keys harness-error: UNEXPECTED FormatException: bad"); - Files.Strings(line["reasons"]).ShouldBeEmpty("the exception stayed in the check"); - } - - [Fact] - public void A_pass_without_a_baseline_is_a_harness_error() - { - var (ended, line) = HarnessRun.Row(outDir, 0, r => Task.FromResult(r.Check("keys", null, c => c.Is("x", "1") ? null : "differs"))); - ended.ShouldNotBeNull().Message.ShouldContain("keys harness-error: pass-without-baseline"); - Files.Str(HarnessRun.Check(line, "keys")["category"]).ShouldBe("harness-error"); - HarnessRun.Check(line, "keys")["baseline"].ShouldBeNull(); - } - - [Fact] - public void A_row_with_a_passing_check_and_one_that_did_not_apply_passes() - { - var (ended, line) = HarnessRun.Row(outDir, 0, r => - { - r.Check("keys", Baseline.Twin, c => c.Is("x", "1") ? null : "differs"); - r.Check("nulls", Baseline.Twin, c => c.Inapplicable(Cite.NotPromised("closed-set-has-no-null"), "x=1")); - return Task.FromResult(null); - }); - ended.ShouldBeNull("one check compared and passed; the other did not apply, which voids nothing"); - Files.Str(line["status"]).ShouldBe("pass"); - Files.Str(line["category"]).ShouldBe("inapplicable", "the row's category is the highest of its checks'"); - Files.Strings(HarnessRun.Check(line, "nulls")["reasons"]).ShouldBe(["inapplicable:not-promised:closed-set-has-no-null(x=1)"]); - Files.Str(HarnessRun.Check(line, "nulls")["notes"]).ShouldBe("not-promised:closed-set-has-no-null(x=1)"); - } - - [Fact] - public void A_row_where_no_check_passed_is_skipped_listing_each_check() - { - var (ended, line) = HarnessRun.Row(outDir, 0, r => - { - r.Check("smallest-scale", Baseline.Twin, c => c.Inapplicable(Cite.NotPromised("closed-set-has-no-scale"))); - r.Check("number-is-a-number", Baseline.Fact("KB-031"), c => c.Refused(Cite.Fact("KB-031"))); - r.Check("reads-a-number", Baseline.Model, c => c.Inconclusive("both threw, not alike")); - r.Check("covers", Baseline.Docs("docs/json.md#decimals-in-one-spelling"), c => c.Refused(Cite.Docs("docs/json.md#decimals-in-one-spelling"))); - r.Check("keeps", Baseline.Cost, c => c.Inapplicable(Cite.Record("0094"))); - return Task.FromResult(null); - }); - ended.ShouldNotBeNull().GetType().Name.ShouldBe("SkipException"); - ended.Message.ShouldContain("smallest-scale inapplicable: not-promised:closed-set-has-no-scale; number-is-a-number expected-refusal: fact:KB-031; " + - "reads-a-number inconclusive: both threw, not alike; covers expected-refusal: docs:docs/json.md#decimals-in-one-spelling; " + - "keeps inapplicable: record:0094"); - Files.Str(line["status"]).ShouldBe("fail"); - Files.Str(line["category"]).ShouldBe("inconclusive"); - line["checks"]!.AsArray().Select(c => Files.Str(c!["baseline"])).ShouldBe(["twin", "fact:KB-031", "model", "docs:docs/json.md#decimals-in-one-spelling", "cost"]); - } - - [Fact] - public void A_violation_fails_the_row_even_beside_a_passing_check() - { - var (ended, line) = HarnessRun.Row(outDir, 0, r => - { - r.Check("keys", Baseline.Twin, _ => (string?)null); - r.Check("nulls", Baseline.Twin, _ => "a null was written"); - return Task.FromResult(null); - }); - ended.ShouldNotBeNull().GetType().Name.ShouldNotBe("SkipException"); - ended.Message.ShouldContain("nulls violation: a null was written"); - ended.Message.ShouldNotContain("keys pass"); - Files.Str(line["status"]).ShouldBe("fail"); - } - - [Fact] - public void A_row_that_runs_no_check_is_written_as_before() - { - var (ended, line) = HarnessRun.Row(outDir, 0, r => Task.FromResult(r["x"] == "1" ? null : "differs")); - ended.ShouldBeNull(); - line.ContainsKey("checks").ShouldBeFalse(); - line.Select(kv => kv.Key).ShouldBe(["idx", "status", "category", "completed", "applied", "reasons", "findings", "note"]); - } - - [Fact] - public void Reads_notes_and_substitutions_through_the_row_inside_a_check_are_the_checks() - { - var (ended, line) = HarnessRun.Row(outDir, 0, async r => - { - await r.Check("keys", Baseline.Twin, async c => - { - await Task.Yield(); - _ = r["y"]; // a helper handed the row, inside the check - _ = r.Get("x"); - r.Substituted("x", "2", "the options ran as x=2"); - r.Note(NoteKeys.Compared, "write"); - return null; - }); - return null; - }); - ended.ShouldBeNull(); - Files.StringMap(line["applied"]).ShouldBeEmpty("the row read nothing outside the check"); - Files.Str(line["note"]).ShouldBeEmpty(); - var check = HarnessRun.Check(line, "keys"); - Files.StringMap(check["applied"]).OrderBy(kv => kv.Key, StringComparer.Ordinal).Select(kv => $"{kv.Key}={kv.Value}").ShouldBe(["x=2", "y=1"]); - Files.Str(check["notes"]).ShouldBe("substituted(x=2, the options ran as x=2);compared(write)"); - } - - [Fact] - public void A_malformed_citation_or_baseline_is_refused_at_construction() - { - Should.Throw(() => Cite.Fact("KB-31")); - Should.Throw(() => Cite.Record("94")); - Should.Throw(() => Cite.NotPromised("Closed set")); - Should.Throw(() => Cite.Docs("README.md#What TypeKit guarantees")); - Should.Throw(() => Cite.Docs("../outside.md")); - Should.Throw(() => Baseline.Fact("fact:KB-031")); - Should.Throw(() => Baseline.Docs("#anchor-alone")); - new[] { Baseline.Twin, Baseline.Framework, Baseline.RoundTrip, Baseline.Model, Baseline.Refusal, Baseline.Cost, Baseline.Fact("KB-031"), Baseline.Docs("README.md") } - .Select(b => b.Spelling).ShouldBe(["twin", "framework", "round-trip", "model", "refusal", "cost", "fact:KB-031", "docs:README.md"]); - new[] { Cite.NotPromised("a-b"), Cite.Docs("CHANGELOG.md#2.1.0-fixed"), Cite.Fact("KB-001"), Cite.Record("0094") } - .Select(c => c.Spelling).ShouldBe(["not-promised:a-b", "docs:CHANGELOG.md#2.1.0-fixed", "fact:KB-001", "record:0094"]); - - var (ended, line) = HarnessRun.Row(outDir, 0, r => Task.FromResult(r.Check("keys", Baseline.Twin, c => c.Refused(Cite.Fact("KB-31"))))); - ended.ShouldNotBeNull().Message.ShouldContain("keys harness-error: UNEXPECTED ArgumentException:"); - Files.Str(HarnessRun.Check(line, "keys")["category"]).ShouldBe("harness-error"); - } -} - -/// -/// The ledger credits checks, not rows: a passing check earns the pairs of the settings it ran under (the row's, its -/// own laid over them) whatever another check of the row ended as, never a pair put together from two checks' reads, -/// and `status` says per obligation what current checks earned and which no passing check named. The evidence is the -/// harness's own lines, copied as the replay copies them. -/// -[Collection(nameof(ProbeOut))] -public sealed class CheckLedgerTests : IDisposable -{ - private readonly Fixture fx = new(); - public void Dispose() => fx.Dispose(); - - /// Runs P's two rows (x=1 y=1, x=2 y=2) through the harness and records the run as the replay would. - private JsonObject Record(Func> check) - { - var outDir = Path.Combine(fx.Root, "rows-out"); - var rows = new JsonObject(); - foreach (var (idx, v) in new[] { (0, "1"), (1, "2") }) - rows[idx.ToString(System.Globalization.CultureInfo.InvariantCulture)] = Replay.EvidenceRow(HarnessRun.Row(outDir, idx, check, v, v).Line); - var path = Path.Combine(fx.Root, "evidence.json"); - File.WriteAllText(path, new JsonObject - { - ["variants"] = new JsonArray(new JsonObject { ["env"] = new JsonObject(), ["expected"] = new JsonArray(0, 1), ["executed"] = new JsonArray(0, 1) }), - ["rows"] = rows, - }.ToJsonString()); - fx.Footprint(); - fx.Ledger("record-run", "--probe", "P", "--result", "pass", "--evidence", path).Exit.ShouldBe(0); - return JsonNode.Parse(File.ReadAllText(Path.Combine(fx.BranchDir, "evidence", "P.json")))!.AsObject(); - } - - private void Obligations() - { - fx.Write("README.md", "# a\n\nThe claim.\n\nA key is written as the wrapped type writes it.\n\nA null is refused.\n"); - fx.Write("tools/probe/corpus/claims.json", """ - [{"id": "C1", "source": "README.md#a", "surfaces": ["s"], "obligations": [ - {"id": "keys", "sentence": "A key is written as the wrapped type writes it."}, - {"id": "nulls", "sentence": "A null is refused."}]}] - """); - fx.Commit("obligations"); - fx.Ledger("universe", "--base", "HEAD").Exit.ShouldBe(0); - } - - private JsonObject Status() - { - var (exit, output, error) = fx.Ledger("status", "--json"); - exit.ShouldBe(0, error); - return JsonNode.Parse(output)!["claims"]!["C1"]!.AsObject(); - } - - [Fact] - public void A_passing_check_earns_its_pairs_beside_a_check_that_did_not_apply() - { - var evidence = Record(r => - { - r.Check("keys", Baseline.Twin, c => c["x"] == c["y"] ? null : "differs"); - r.Check("nulls", Baseline.Twin, c => c.Inapplicable(Cite.NotPromised("no-null-here"))); - return Task.FromResult(null); - }); - Files.Num(evidence["schemaVersion"]).ShouldBe(2); - fx.Covered().ShouldBe(2, "x1y1 and x2y2, each read by the passing check, though every row ended inapplicable"); - } - - [Fact] - public void A_pair_is_never_put_together_from_two_checks_reads() - { - Record(r => - { - r.Check("keys", Baseline.Twin, c => c["x"] is not null ? null : "never"); - if (r.Index == 0) r.Check("nulls", Baseline.Twin, c => c["y"] is not null ? null : "never"); - else r.Check("nulls", Baseline.Twin, c => c["x"] is not null && c["y"] is not null ? null : "never"); - return Task.FromResult(null); - }); - fx.Covered().ShouldBe(1, "row 0's checks read x and y apart, which credits no pair; row 1's second check read both"); - } - - [Fact] - public void The_rows_own_reads_are_what_every_check_ran_under() - { - Record(r => - { - _ = r["x"]; - r.Check("keys", Baseline.Twin, c => c["y"] is not null ? null : "never"); - return Task.FromResult(null); - }); - fx.Covered().ShouldBe(2); - } - - [Fact] - public void A_row_without_checks_is_one_check_of_the_claim() - { - Record(r => Task.FromResult(r["x"] == r["y"] ? null : "differs")); - fx.Covered().ShouldBe(2); - var obligation = Status()["obligations"]!.AsArray().ShouldHaveSingleItem()!; - Files.Str(obligation["id"]).ShouldBe("claim"); - Files.Num(obligation["current"]).ShouldBe(2); - } - - [Fact] - public void Status_gives_each_obligation_its_current_pairs_and_counts_those_no_passing_check_named() - { - Obligations(); - Record(r => - { - r.Check("keys", Baseline.Twin, c => c["x"] == c["y"] ? null : "differs"); - r.Check("nulls", Baseline.Twin, c => c.Inapplicable(Cite.NotPromised("no-null-here"))); - return Task.FromResult(null); - }); - var claim = Status(); - claim["obligations"]!.AsArray().Select(o => $"{Files.Str(o!["id"])} {Files.Num(o["current"])} {o["passed"]!.GetValue()}").ShouldBe(["keys 2 True", "nulls 0 False"]); - claim["check_outcomes"]!.AsObject().Select(kv => $"{kv.Key} {Files.Num(kv.Value)}").ShouldBe(["inapplicable 2", "pass 2"]); - claim["outcomes"]!.AsObject().Select(kv => $"{kv.Key} {Files.Num(kv.Value)}").ShouldBe(["inapplicable 2"]); - fx.Ledger("status").Out.ReplaceLineEndings("\n") - .ShouldContain("\n C1: 2/4 (rows: inapplicable 2; checks: inapplicable 2, pass 2) obligations 2 (2 without a seed: keys, nulls), 1 uncredited\n"); - } - - [Fact] - public void The_replay_keeps_each_checks_fields_and_six_reasons() - { - var line = JsonNode.Parse(""" - {"idx": 0, "category": "inapplicable", "applied": {"x": "1"}, "reasons": [], "findings": [], "note": "", - "checks": [{"obligation": "keys", "baseline": "twin", "category": "inapplicable", "applied": {"y": "1"}, - "reasons": ["inapplicable:1", "inapplicable:2", "inapplicable:3", "inapplicable:4", "inapplicable:5", "inapplicable:6", "inapplicable:7"], "notes": "n"}, - {"obligation": "nulls", "baseline": null, "category": "harness-error", "applied": {}, "reasons": [], "notes": ""}]} - """)!.AsObject(); - var row = Replay.EvidenceRow(line); - var checks = row["checks"]!.AsArray(); - checks.Count.ShouldBe(2); - Files.Str(checks[0]!["obligation"]).ShouldBe("keys"); - Files.Str(checks[0]!["baseline"]).ShouldBe("twin"); - Files.StringMap(checks[0]!["applied"]).ShouldBe(new Dictionary { ["y"] = "1" }); - Files.Strings(checks[0]!["reasons"]).Count().ShouldBe(6); - Files.Str(checks[0]!["notes"]).ShouldBe("n"); - checks[1]!["baseline"].ShouldBeNull(); - Replay.EvidenceRow(JsonNode.Parse("""{"idx": 0, "category": "pass", "applied": {}}""")!.AsObject()).ContainsKey("checks").ShouldBeFalse(); - } -} - -/// -/// `seeds` judges a seed by the checks it fires: a seed naming obligation O is caught only by a clean violation of a -/// check naming O, in a row whose unseeded run had no violation of O; a seed naming none by any check. P's rows run a -/// check of each obligation through the real harness; SEED names the check the seeded build breaks. -/// -[Collection(nameof(ProbeOut))] -public sealed class CheckSeedsTests : IDisposable -{ - private readonly Fixture fx = new(); - private readonly string cache; - - public CheckSeedsTests() - { - cache = fx.Root + "-cache"; - fx.Sh("git", "config", "core.autocrlf", "false"); - fx.Write("README.md", "# a\n\nThe claim.\n\nA key is written as the wrapped type writes it.\n\nA null is refused.\n"); - fx.Write("tools/probe/corpus/claims.json", """ - [{"id": "C1", "source": "README.md#a", "surfaces": ["s"], "obligations": [ - {"id": "keys", "sentence": "A key is written as the wrapped type writes it."}, - {"id": "nulls", "sentence": "A null is refused."}]}] - """); - fx.Write(".claude/probe-axes.md", "```json probe-axes\n" + new JsonObject - { - ["axes"] = new JsonObject { ["x"] = new JsonArray("1", "2"), ["y"] = new JsonArray("1", "2") }, - ["surfaces"] = new JsonObject { ["s"] = new JsonObject { ["axes"] = new JsonArray("x", "y"), ["watched"] = new JsonArray("src") } }, - ["replay"] = new JsonObject { ["probeDir"] = "probes", ["include"] = new JsonArray("src") }, - }.ToJsonString() + "\n```\n"); - fx.Commit("obligations and the replay configuration"); - } - - public void Dispose() - { - fx.Dispose(); - try { Directory.Delete(cache, recursive: true); } catch (IOException) { } catch (UnauthorizedAccessException) { } - } - - /// Registers P with one seed per (obligation, SEED value): the seed names the obligation, and its build breaks the check SEED names. - private void Register(params (string? Obligation, string Breaks)[] seeds) - { - fx.Write("tools/probe/corpus/registry.json", new JsonObject - { - ["probes"] = new JsonObject - { - ["P"] = new JsonObject - { - ["claim"] = "C1", ["filter"] = "*P", - ["seeds"] = new JsonArray([.. seeds.Select(s => (JsonNode)new JsonObject - { - ["obligation"] = s.Obligation, ["env"] = new JsonObject { ["SEED"] = s.Breaks }, ["defect"] = $"{s.Breaks} breaks.", - })]), - }, - }, - }.ToJsonString()); - fx.Commit("register"); - } - - private (int Exit, string Out) Seeds(HarnessAdapter adapter) - { - var output = new StringWriter(); - var context = new Context(fx.Root, new Dictionary { ["PROBE_CACHE_DIR"] = cache }, output, output); - int exit; - try { exit = global::Prober.Seeds.Run(context, new Args([], new HashSet(StringComparer.Ordinal)), adapter); } - catch (Refusal refusal) { output.WriteLine(refusal.Message); exit = refusal.ExitCode; } - return (exit, output.ToString().ReplaceLineEndings("\n")); - } - - /// Row 1 runs a check of keys and one of nulls; the one SEED names fails. fails a check of row 1 without a seed. - private static Func> TwoChecks(string? unseeded = null) => (r, seed) => - { - foreach (var obligation in new[] { "keys", "nulls" }) - r.Check(obligation, Baseline.Twin, _ => r.Index == 1 && (seed == obligation || unseeded == obligation) ? $"{obligation} broke" : null); - return Task.FromResult(null); - }; - - [Fact] - public void A_seed_naming_an_obligation_is_caught_only_by_a_check_naming_it() - { - Register(("keys", "nulls"), ("nulls", "nulls")); - var (exit, output) = Seeds(new HarnessAdapter(TwoChecks())); - exit.ShouldBe(1, output); - output.ShouldContain("\n P/keys (C1): missed, no row violates that did not already (2 rows: pass 1, violation 1; 1 violate by a check naming nulls, not keys;"); - output.ShouldContain("\n P/nulls (C1): caught, 1 of 2 rows violate; e.g. #1 nulls: violation:nulls broke\n"); - } - - [Fact] - public void The_baseline_is_per_row_and_obligation() - { - Register(("keys", "keys")); - var (exit, output) = Seeds(new HarnessAdapter(TwoChecks(unseeded: "nulls"))); - exit.ShouldBe(0, output); - output.ShouldContain("\n P/keys (C1): caught, 1 of 2 rows violate; e.g. #1 keys: violation:keys broke\n", - Case.Sensitive, "row 1 violated nulls unseeded, which takes nothing from a violation of keys there"); - } - - [Fact] - public void A_seed_naming_none_is_caught_by_any_check() - { - Register((null, "nulls")); - var (exit, output) = Seeds(new HarnessAdapter(TwoChecks())); - exit.ShouldBe(0, output); - output.ShouldContain("\n P (C1): caught, 1 of 2 rows violate; e.g. #1 nulls: violation:nulls broke\n"); - } - - [Fact] - public void A_harness_error_of_the_row_outside_its_checks_makes_no_check_a_catch() - { - Register(("keys", "keys")); - var (exit, output) = Seeds(new HarnessAdapter((r, seed) => - { - if (seed is not null && r.Index == 1) r.Outcome("harness-error", "the host did not start"); - return TwoChecks()(r, seed); - })); - exit.ShouldBe(1, output); - output.ShouldContain("\n P/keys (C1): missed, no row violates that did not already (2 rows: pass 1, violation 1; 1 ended in a harness error, which is no catch;"); - } - - [Fact] - public void A_row_without_checks_catches_no_seed_naming_an_obligation() - { - Register(("keys", "keys")); - var (exit, output) = Seeds(new HarnessAdapter((r, seed) => Task.FromResult(seed is not null && r.Index == 1 ? "row 1 broke" : null))); - exit.ShouldBe(1, output); - output.ShouldContain("1 violate by a check naming claim, not keys;"); - } - - /// The probe P over two rows, each run through the real harness with the value of SEED (null unseeded). - private sealed class HarnessAdapter(Func> check) : IProbeAdapter - { - public Ran Build(string worktree, IReadOnlyDictionary environment) => new(0, "Build: OK", ""); - - public Ran RunProbe(string worktree, string filter, string results, string coverageFile, IReadOnlyDictionary environment) - { - var seed = environment.GetValueOrDefault("SEED"); - var outDir = environment["PROBE_OUT"]!; - var failed = false; - for (var idx = 0; idx < 2; idx++) - failed |= HarnessRun.Row(outDir, idx, r => check(r, seed)).Ended is not null; - return new Ran(failed ? 1 : 0, "", ""); - } - - public (JsonObject? Axes, string? Error) DeriveAxes(string checkout, string workDirectory) => (null, "none"); - - public IReadOnlyDictionary Environment(string cacheDirectory) => new Dictionary(); - } -} diff --git a/test/CodoMetis.TypeKit.Prober.Tests/RegisterTests.cs b/test/CodoMetis.TypeKit.Prober.Tests/RegisterTests.cs deleted file mode 100644 index a8e95229..00000000 --- a/test/CodoMetis.TypeKit.Prober.Tests/RegisterTests.cs +++ /dev/null @@ -1,171 +0,0 @@ -using System.Text.Json.Nodes; -using System.Text.RegularExpressions; -using Prober; - -namespace CodoMetis.TypeKit.Prober.Tests; - -/// -/// `register --obligation O` writes a seed per obligation (decision 104) into the entry's `seeds`, in place of -/// any seed that names O already, and refuses an O the claim does not list; `new` prints that command for each -/// listed obligation, so no seed is written into the registry by hand. C1 lists keys and nulls; C7 lists none. -/// -public sealed class RegisterTests : IDisposable -{ - private readonly Fixture fx = new(); - - public RegisterTests() - { - fx.Write(".claude/probe-axes.md", "```json probe-axes\n" + new JsonObject - { - ["axes"] = new JsonObject { ["x"] = new JsonArray("1", "2"), ["y"] = new JsonArray("1", "2") }, - ["surfaces"] = new JsonObject { ["s"] = new JsonObject { ["axes"] = new JsonArray("x", "y"), ["watched"] = new JsonArray("src") } }, - ["replay"] = new JsonObject { ["probeDir"] = "probes", ["include"] = new JsonArray("src"), ["families"] = new JsonObject { ["f"] = new JsonArray("s") } }, - }.ToJsonString() + "\n```\n"); - fx.Write("tools/probe/corpus/claims.json", new JsonArray( - new JsonObject - { - ["id"] = "C1", ["source"] = "README.md#a", ["surfaces"] = new JsonArray("s"), - ["obligations"] = new JsonArray( - new JsonObject { ["id"] = "keys", ["sentence"] = "The claim." }, - new JsonObject { ["id"] = "nulls", ["sentence"] = "The claim." }), - }, - new JsonObject { ["id"] = "C7", ["source"] = "README.md#a", ["surfaces"] = new JsonArray("s") }).ToJsonString()); - fx.Commit("claims with obligations"); - fx.Ledger("universe", "--base", "HEAD").Exit.ShouldBe(0); - } - - public void Dispose() => fx.Dispose(); - - private string StagedRegistry => Path.Combine(fx.BranchDir, "staging", "registry.json"); - - private JsonObject Entry(string probe = "P") => JsonNode.Parse(File.ReadAllText(StagedRegistry))!["probes"]![probe]!.AsObject(); - - private (int Exit, string Out, string Err) Register(params string[] more) => - fx.Ledger(["register", "--probe", "P", "--claim", "C1", "--filter", "*P", .. more]); - - private static string Seeds(JsonObject entry) => - string.Join("; ", entry["seeds"]!.AsArray().Select(s => $"{s!["obligation"]}={s["patch"]}:{s["defect"]}")); - - [Fact] - public void An_obligation_s_seed_goes_into_seeds() - { - var (exit, output, error) = Register("--obligation", "keys", "--seed", "seeds/C1-keys.patch", "--defect", "A key is written with ToString()."); - exit.ShouldBe(0, output + error); - var entry = Entry(); - entry["seeds"]!.ToJsonString().ShouldBe("""[{"obligation":"keys","patch":"seeds/C1-keys.patch","defect":"A key is written with ToString()."}]"""); - entry.ContainsKey("seed").ShouldBeFalse("a seed naming an obligation is not the probe's one `seed`"); - output.ShouldContain("seed for obligation keys of P"); - } - - [Fact] - public void A_seed_for_an_obligation_that_has_one_replaces_it() - { - Register("--obligation", "keys", "--seed", "seeds/C1-keys.patch", "--defect", "first").Exit.ShouldBe(0); - Register("--obligation", "nulls", "--seed", "seeds/C1-nulls.patch", "--defect", "nulls").Exit.ShouldBe(0); - var (exit, output, _) = Register("--obligation", "keys", "--seed", "seeds/C1-keys-2.patch", "--defect", "second"); - exit.ShouldBe(0); - output.ShouldContain("seed for obligation keys of P, in place of the 1 it had"); - Seeds(Entry()).ShouldBe("nulls=seeds/C1-nulls.patch:nulls; keys=seeds/C1-keys-2.patch:second"); - } - - [Fact] - public void The_one_seed_naming_the_obligation_is_replaced_and_one_naming_another_kept() - { - fx.Write("tools/probe/corpus/registry.json", """ - {"probes": { - "P": {"claim": "C1", "filter": "*P", "seed": {"patch": "seeds/C1.patch", "defect": "old", "obligation": "keys"}}, - "Q": {"claim": "C1", "filter": "*Q", "seed": {"patch": "seeds/C1-q.patch", "defect": "kept", "obligation": "nulls"}}}} - """); - fx.Commit("probes whose one seed names an obligation"); - - Register("--obligation", "keys", "--seed", "seeds/C1-keys.patch", "--defect", "new").Exit.ShouldBe(0); - Entry().ContainsKey("seed").ShouldBeFalse("the seed that named keys is replaced, not kept beside the new one"); - Seeds(Entry()).ShouldBe("keys=seeds/C1-keys.patch:new"); - - fx.Ledger("register", "--probe", "Q", "--claim", "C1", "--filter", "*Q", "--obligation", "keys", "--seed", "seeds/C1-qk.patch", "--defect", "q").Exit.ShouldBe(0); - Entry("Q")["seed"]!["obligation"]!.GetValue().ShouldBe("nulls"); - Seeds(Entry("Q")).ShouldBe("keys=seeds/C1-qk.patch:q"); - } - - [Theory] - [InlineData("C1", "kyes", "claim C1 lists no obligation kyes (it lists keys, nulls)")] - [InlineData("C1", "claim", "claim C1 lists no obligation claim (it lists keys, nulls)")] - [InlineData("C7", "keys", "claim C7 lists no obligation keys (it lists claim)")] - public void An_obligation_the_claim_does_not_list_is_refused_and_nothing_staged(string claim, string obligation, string message) - { - var (exit, _, error) = fx.Ledger("register", "--probe", "P", "--claim", claim, "--filter", "*P", "--obligation", obligation, "--seed", "seeds/x.patch", "--defect", "d"); - exit.ShouldBe(2); - error.ShouldContain(message); - File.Exists(StagedRegistry).ShouldBeFalse(); - } - - [Fact] - public void A_claim_without_obligations_takes_a_seed_for_its_one() - { - fx.Ledger("register", "--probe", "P", "--claim", "C7", "--filter", "*P", "--obligation", "claim", "--seed", "seeds/C7.patch", "--defect", "d").Exit.ShouldBe(0); - Seeds(Entry()).ShouldBe("claim=seeds/C7.patch:d"); - } - - [Fact] - public void A_claim_that_is_not_in_the_claims_is_refused() - { - var (exit, _, error) = fx.Ledger("register", "--probe", "P", "--claim", "C9", "--filter", "*P", "--obligation", "keys", "--seed", "seeds/x.patch", "--defect", "d"); - exit.ShouldBe(1); - error.ShouldContain("no claim C9 in claims.json"); - File.Exists(StagedRegistry).ShouldBeFalse(); - } - - [Fact] - public void An_obligation_without_a_seed_or_a_defect_is_refused() - { - var (exit, _, error) = Register("--obligation", "keys", "--defect", "d"); - exit.ShouldBe(2); - error.ShouldContain("give the seed with --seed PATCH or --seed-env NAME=VALUE"); - Register("--obligation", "keys", "--seed", "seeds/C1-keys.patch").Exit.ShouldBe(2, "a seed without its defect"); - File.Exists(StagedRegistry).ShouldBeFalse(); - } - - [Fact] - public void An_environment_toggle_is_a_seed_too() - { - Register("--obligation", "nulls", "--seed-env", "A=1", "B=x=y", "--defect", "d").Exit.ShouldBe(0); - Entry()["seeds"]!.ToJsonString().ShouldBe("""[{"obligation":"nulls","env":{"A":"1","B":"x=y"},"defect":"d"}]"""); - Register("--seed-env", "C=2", "--defect", "one").Exit.ShouldBe(0); - Entry()["seed"]!.ToJsonString().ShouldBe("""{"env":{"C":"2"},"defect":"one"}"""); - Register("--obligation", "nulls", "--seed-env", "=1", "--defect", "d").Exit.ShouldBe(2, "a toggle without a name"); - } - - [Fact] - public void A_seed_stales_no_result() - { - var before = new Ledger(new Context(fx.Root, new Dictionary(), TextWriter.Null, TextWriter.Null), null).Identity("P"); - Register("--obligation", "keys", "--seed", "seeds/C1-keys.patch", "--defect", "d").Exit.ShouldBe(0); - Register("--seed", "seeds/C1.patch", "--defect", "d").Exit.ShouldBe(0); - Entry().ContainsKey("seeds").ShouldBeTrue(); - new Ledger(new Context(fx.Root, new Dictionary(), TextWriter.Null, TextWriter.Null), null).Identity("P").ShouldBe(before); - } - - [Fact] - public void The_commands_new_prints_register_each_obligation_s_seed() - { - var (exit, output, _) = fx.Ledger("new", "--claim", "C1", "--probe", "C1KeysProbe"); - exit.ShouldBe(0); - var commands = Regex.Matches(output, @"`prober (register [^`]*)`").Select(m => m.Groups[1].Value).ToList(); - commands.Count.ShouldBe(2, output); - output.ShouldNotContain("in the entry's `seeds`"); - foreach (var command in commands) - { - var args = Regex.Matches(command, @"""([^""]*)""|(\S+)").Select(m => m.Groups[1].Success ? m.Groups[1].Value : m.Groups[2].Value) - .Select(arg => arg == "..." ? "What the seed breaks." : arg).ToArray(); - var (registered, said, error) = fx.Ledger(args); - registered.ShouldBe(0, command + ": " + said + error); - } - - var entry = Entry("C1KeysProbe"); - entry["filter"]!.GetValue().ShouldBe("*C1KeysProbe"); - Seeds(entry).ShouldBe("keys=seeds/C1-keys.patch:What the seed breaks.; nulls=seeds/C1-nulls.patch:What the seed breaks."); - var problems = global::Prober.Seeds.Problems(new Ledger(new Context(fx.Root, new Dictionary(), TextWriter.Null, TextWriter.Null), null)); - problems.ShouldNotContain(p => p.StartsWith("C1KeysProbe: no seed", StringComparison.Ordinal)); - problems.ShouldNotContain(p => p.StartsWith("C1: obligation", StringComparison.Ordinal)); - } -} diff --git a/test/CodoMetis.TypeKit.Prober.Tests/ReplayTemplateTests.cs b/test/CodoMetis.TypeKit.Prober.Tests/ReplayTemplateTests.cs deleted file mode 100644 index 6c0b723b..00000000 --- a/test/CodoMetis.TypeKit.Prober.Tests/ReplayTemplateTests.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System.Text.Json.Nodes; -using Prober; - -namespace CodoMetis.TypeKit.Prober.Tests; - -/// -/// The replay's templates name only what the repository has. A tool that moves (a script ported to a tool -/// project) must not leave the replay starting a file that is gone: that fails only on the next replay, far -/// from the commit that caused it. -/// -public sealed class ReplayTemplateTests -{ - private static string RepositoryRoot() - { - for (var dir = new DirectoryInfo(AppContext.BaseDirectory); dir is not null; dir = dir.Parent) - if (File.Exists(Path.Combine(dir.FullName, "CodoMetis.TypeKit.slnx"))) return dir.FullName; - throw new InvalidOperationException("no CodoMetis.TypeKit.slnx above the test output"); - } - - /// - /// Every path a template names: a file must be tracked in the repository; a dll under a project's bin/ must - /// come from a project there that a "prepare" command builds (a fresh worktree has no build output). - /// - internal static List Problems(string root, JsonObject replay) - { - var problems = new List(); - var prepared = (replay["prepare"] as JsonArray ?? []).OfType() - .SelectMany(c => c.Select(a => a?.GetValue() ?? "")).ToHashSet(StringComparer.Ordinal); - var templates = new List<(string Name, IEnumerable Args)>(); - foreach (var name in new[] { "build", "test" }) - templates.Add((name, (replay[name] as JsonArray ?? []).Select(a => a?.GetValue() ?? ""))); - foreach (var (i, command) in (replay["prepare"] as JsonArray ?? []).OfType().Index()) - templates.Add(($"prepare[{i}]", command.Select(a => a?.GetValue() ?? ""))); - if (replay["enumerate"]?["script"]?.GetValue() is { } script) templates.Add(("enumerate", [script])); - - foreach (var (name, args) in templates) - foreach (var arg in args.Where(a => a.Contains('/') && !a.Contains('{'))) - { - var bin = arg.IndexOf("/bin/", StringComparison.Ordinal); - if (bin >= 0) - { - var project = arg[..bin]; - if (!Directory.Exists(Path.Combine(root, project)) || !Directory.EnumerateFiles(Path.Combine(root, project), "*.csproj").Any()) - problems.Add($"{name}: {arg} comes from {project}, which is no project in the repository"); - else if (!prepared.Contains(project)) - problems.Add($"{name}: {arg} is build output of {project}, which no \"prepare\" command builds"); - } - else if (!File.Exists(Path.Combine(root, arg)) && !Directory.Exists(Path.Combine(root, arg))) - problems.Add($"{name}: {arg} is not in the repository"); - } - return problems; - } - - private static JsonObject RepositoryReplay(string root) => - Axes.Block(Path.Combine(root, ".claude", "probe-axes.md"))["replay"] as JsonObject ?? throw new InvalidOperationException("no replay object"); - - [Fact] - public void The_replay_templates_name_only_what_the_repository_has() - { - var root = RepositoryRoot(); - Problems(root, RepositoryReplay(root)).ShouldBeEmpty(); - } - - [Fact] - public void A_template_naming_a_removed_script_is_caught() - { - // The seed: the templates as they were before the verdict script became a tool project. - var root = RepositoryRoot(); - var replay = (JsonObject)RepositoryReplay(root).DeepClone(); - replay["build"] = new JsonArray("tools/build-verdict.sh", "build", "{project}"); - Problems(root, replay).ShouldContain(p => p.Contains("tools/build-verdict.sh is not in the repository", StringComparison.Ordinal)); - } - - [Fact] - public void A_tool_dll_no_prepare_command_builds_is_caught() - { - var root = RepositoryRoot(); - var replay = (JsonObject)RepositoryReplay(root).DeepClone(); - replay.Remove("prepare"); - Problems(root, replay).ShouldContain(p => p.Contains("which no \"prepare\" command builds", StringComparison.Ordinal)); - } - - [Fact] - public void A_bare_command_is_found_on_PATH_and_a_path_must_exist_in_the_worktree() - { - var worktree = Path.Combine(Path.GetTempPath(), "prober-exe-" + Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(Path.Combine(worktree, "tools")); - try - { - DotNetAdapter.Executable(worktree, "dotnet").ShouldBe("dotnet"); - File.WriteAllText(Path.Combine(worktree, "tools", "run.sh"), ""); - DotNetAdapter.Executable(worktree, "tools/run.sh").ShouldBe(Path.Combine(worktree, "tools/run.sh")); - Should.Throw(() => DotNetAdapter.Executable(worktree, "tools/build-verdict.sh")) - .Message.ShouldContain("tools/build-verdict.sh"); - } - finally { Directory.Delete(worktree, recursive: true); } - } -} diff --git a/test/CodoMetis.TypeKit.Prober.Tests/ReplayTests.cs b/test/CodoMetis.TypeKit.Prober.Tests/ReplayTests.cs deleted file mode 100644 index b4502eec..00000000 --- a/test/CodoMetis.TypeKit.Prober.Tests/ReplayTests.cs +++ /dev/null @@ -1,368 +0,0 @@ -using System.Text.Json.Nodes; -using Prober; - -namespace CodoMetis.TypeKit.Prober.Tests; - -/// -/// The replay runs probes in parallel, by family, with or without a footprint, and `merge` joins the states -/// of a replay split by family (decision 101). The probes here are a fake adapter: P covers C1 (surface s) -/// and Q covers C2 (surface t); a run writes its rows and, when asked for one, a coverage report that names -/// src/a.cs. -/// -public sealed class ReplayTests : IDisposable -{ - private readonly Fixture fx = new(); - private readonly string cache; - private readonly FakeAdapter adapter = new(); - private readonly List states = []; - - public ReplayTests() - { - cache = fx.Root + "-cache"; - fx.Sh("git", "config", "core.autocrlf", "false"); - Axes(new JsonObject { ["a"] = new JsonArray("s"), ["b"] = new JsonArray("t") }); - fx.Write("tools/probe/corpus/claims.json", """[{"id": "C1", "source": "README.md#a", "surfaces": ["s"]}, {"id": "C2", "source": "README.md#a", "surfaces": ["t"]}]"""); - fx.Write("tools/probe/corpus/registry.json", """{"probes": {"P": {"claim": "C1", "filter": "*P"}, "Q": {"claim": "C2", "filter": "*Q"}}}"""); - fx.Write("tools/probe/corpus/harness/Q.cs", "// probe Q\n"); - fx.Write("tools/probe/corpus/rows/Q.json", new JsonObject - { - ["claim"] = "C2", ["rows"] = new JsonArray(new JsonObject { ["row"] = new JsonObject { ["x"] = "1", ["y"] = "1" } }), - }.ToJsonString()); - fx.Commit("two probes of two families"); - } - - public void Dispose() - { - fx.Dispose(); - foreach (var dir in states.Append(cache)) - try { Directory.Delete(dir, recursive: true); } catch (IOException) { } catch (UnauthorizedAccessException) { } - } - - /// The axes file with a replay configuration; its families are the given ones, or none. - private void Axes(JsonObject? families) - { - var replay = new JsonObject { ["probeDir"] = "probes", ["include"] = new JsonArray("src") }; - if (families is not null) replay["families"] = families; - fx.Write(".claude/probe-axes.md", "```json probe-axes\n" + new JsonObject - { - ["axes"] = new JsonObject { ["x"] = new JsonArray("1", "2"), ["y"] = new JsonArray("1", "2") }, - ["surfaces"] = new JsonObject - { - ["s"] = new JsonObject { ["axes"] = new JsonArray("x", "y"), ["watched"] = new JsonArray("src") }, - ["t"] = new JsonObject { ["axes"] = new JsonArray("x", "y"), ["watched"] = new JsonArray("src") }, - }, - ["replay"] = replay, - }.ToJsonString() + "\n```\n"); - } - - /// A state directory of its own, as a leg of a matrix or the job that merges them has. - private string State(string name) - { - var dir = fx.Root + "-" + name; - states.Add(dir); - return dir; - } - - private static readonly HashSet Flags = new(StringComparer.Ordinal) { "all", "dry-run", "where", "no-footprint" }; - - private (int Exit, string Out, string Err) Replay(string? state, params string[] args) - { - var output = new StringWriter(); var error = new StringWriter(); - var env = new Dictionary { ["PROBE_CACHE_DIR"] = cache }; - if (state is not null) env["PROBE_STATE_DIR"] = state; - int exit; - try { exit = global::Prober.Replay.Run(new Context(fx.Root, env, output, error), new Args(args, Flags), adapter); } - catch (Refusal refusal) { error.WriteLine(refusal.Message); exit = refusal.ExitCode; } - return (exit, output.ToString(), error.ToString()); - } - - /// A replay in another checkout of the repository, a linked worktree, with the fake adapter. - private (int Exit, string Out, string Err) ReplayIn(string checkout, params string[] args) - { - var output = new StringWriter(); var error = new StringWriter(); - int exit; - try { exit = global::Prober.Replay.Run(new Context(checkout, new Dictionary { ["PROBE_CACHE_DIR"] = cache }, output, error), new Args(args, Flags), adapter); } - catch (Refusal refusal) { error.WriteLine(refusal.Message); exit = refusal.ExitCode; } - return (exit, output.ToString(), error.ToString()); - } - - private string Home(string checkout) => - global::Prober.Replay.Home(new Context(checkout, new Dictionary { ["PROBE_CACHE_DIR"] = cache }, TextWriter.Null, TextWriter.Null), - new global::Prober.Ledger(new Context(checkout, new Dictionary(), TextWriter.Null, TextWriter.Null), null)); - - private (int Exit, string Out, string Err) Ledger(string? state, params string[] args) => - state is null ? fx.Ledger(args) : fx.LedgerWith(new Dictionary { ["PROBE_STATE_DIR"] = state }, args); - - private string BranchDir(string? state) => Path.Combine(state ?? fx.Memory, "branches", "main"); - private bool HasEvidence(string? state, string probe) => File.Exists(Path.Combine(BranchDir(state), "evidence", probe + ".json")); - private JsonObject Last(string? state) => (JsonObject)JsonNode.Parse(File.ReadAllText(Path.Combine(BranchDir(state), "replay-last.json")))!; - private int Lines(string? state, string file) => - File.Exists(Path.Combine(BranchDir(state), file)) ? File.ReadAllLines(Path.Combine(BranchDir(state), file)).Count(l => l.Trim().Length > 0) : 0; - - [Fact] - public void A_family_replays_the_probes_whose_claim_surface_it_holds() - { - var (exit, output, error) = Replay(null, "--all", "--family", "a"); - exit.ShouldBe(0, error + output); - output.ShouldContain("1 of 1 probes to replay (family a)"); - HasEvidence(null, "P").ShouldBeTrue(); - HasEvidence(null, "Q").ShouldBeFalse("Q's claim is of surface t, family b"); - Files.Strings(Last(null)["selected"]).ShouldBe(["P"]); - Files.Str(Last(null)["family"]).ShouldBe("a"); - - Replay(null, "--all", "--family", "b").Exit.ShouldBe(0); - HasEvidence(null, "Q").ShouldBeTrue(); - } - - [Fact] - public void A_probe_whose_surface_is_in_no_family_refuses_the_replay() - { - Axes(new JsonObject { ["a"] = new JsonArray("s") }); - fx.Commit("family b dropped"); - var (exit, _, error) = Replay(null, "--all", "--family", "a"); - exit.ShouldBe(1, "a matrix by family would never run Q"); - error.ShouldContain("Q"); - error.ShouldContain("in no family"); - HasEvidence(null, "P").ShouldBeFalse("nothing ran"); - } - - [Fact] - public void An_unknown_family_and_a_bad_parallel_are_usage_errors() - { - Replay(null, "--all", "--family", "c").Exit.ShouldBe(2); - Replay(null, "--all", "--parallel", "0").Exit.ShouldBe(2); - Replay(null, "--all", "--parallel", "two").Exit.ShouldBe(2); - Axes(null); - fx.Commit("no families"); - var (exit, _, error) = Replay(null, "--all", "--family", "a"); - exit.ShouldBe(2); - error.ShouldContain("names no families"); - } - - [Fact] - public void A_replay_without_a_footprint_records_evidence_and_leaves_the_probes_stale() - { - var (exit, output, error) = Replay(null, "--all", "--no-footprint"); - exit.ShouldBe(0, error + output); - adapter.CoverageAsked.ShouldBeEmpty("no coverage report is asked for without a footprint"); - output.ShouldContain("footprint: not recorded (--no-footprint)"); - HasEvidence(null, "P").ShouldBeTrue(); - HasEvidence(null, "Q").ShouldBeTrue(); - Lines(null, "footprints.jsonl").ShouldBe(0); - Last(null)["footprint"]!.GetValue().ShouldBeFalse(); - Ledger(null, "stale").Out.ShouldStartWith("2 of 2 probes to replay"); - - Replay(null, "--all").Exit.ShouldBe(0); - adapter.CoverageAsked.Count.ShouldBe(2); - Lines(null, "footprints.jsonl").ShouldBe(2); - Ledger(null, "stale").Out.ShouldStartWith("0 of 2 probes to replay"); - } - - [Fact] - public void The_rows_a_probe_is_expected_to_run_are_those_of_its_own_rows_file() - { - Replay(null, "--all", "--no-footprint").Exit.ShouldBe(0); - List Expected(string probe) => - [.. JsonNode.Parse(File.ReadAllText(Path.Combine(BranchDir(null), "evidence", probe + ".json")))!["variants"]![0]!["expected"]!.AsArray().Select(i => i!.GetValue())]; - Expected("P").ShouldBe([0, 1], "rows/P.json holds two rows"); - Expected("Q").ShouldBe([0], "rows/Q.json holds one"); - } - - [Fact] - public void Probes_run_as_many_at_a_time_as_asked_without_a_footprint_and_are_recorded_in_registry_order() - { - var (exit, output, error) = Replay(null, "--all", "--no-footprint", "--parallel", "2"); - exit.ShouldBe(0, error + output); - adapter.MostAtOnce.ShouldBe(2); - output.IndexOf(" P:", StringComparison.Ordinal).ShouldBeLessThan(output.IndexOf(" Q:", StringComparison.Ordinal)); - Last(null)["parallel"]!.GetValue().ShouldBe(2); - - adapter.MostAtOnce = 0; - Replay(null, "--all", "--no-footprint", "--parallel", "1").Exit.ShouldBe(0); - adapter.MostAtOnce.ShouldBe(1); - } - - /// - /// The coverage collector rewrites the assemblies on disk, so two hosts from one output directory corrupt - /// each other (KB-025): a replay that records a footprint runs one host at a time, whatever --parallel says. - /// - [Fact] - public void A_replay_with_a_footprint_runs_one_host_at_a_time() - { - var (exit, output, error) = Replay(null, "--all", "--parallel", "2"); - exit.ShouldBe(0, error + output); - adapter.MostAtOnce.ShouldBe(1); - output.ShouldContain("one at a time (the coverage collector rewrites the assemblies on disk)"); - Last(null)["parallel"]!.GetValue().ShouldBe(1); - } - - [Fact] - public void Merge_joins_the_legs_and_refuses_a_probe_no_leg_saw() - { - var legA = State("leg-a"); var legB = State("leg-b"); var merged = State("merged"); - Replay(legA, "--all", "--family", "a").Exit.ShouldBe(0); - Replay(legB, "--all", "--family", "b").Exit.ShouldBe(0); - - var (exit, output, error) = Ledger(merged, "merge", "--from", legA, legB); - exit.ShouldBe(0, error + output); - output.ShouldContain("2 leg(s)"); - HasEvidence(merged, "P").ShouldBeTrue(); - HasEvidence(merged, "Q").ShouldBeTrue(); - Lines(merged, "runs.jsonl").ShouldBe(2); - Lines(merged, "footprints.jsonl").ShouldBe(2); - Files.Strings(Last(merged)["selected"]).ShouldBe(["P", "Q"]); - Files.Strings(Last(merged)["legs"]).ShouldBe(["a", "b"]); - Ledger(merged, "stale").Out.ShouldStartWith("0 of 2 probes to replay"); - - Ledger(merged, "merge", "--from", legA, legB).Exit.ShouldBe(0); - Lines(merged, "runs.jsonl").ShouldBe(2, "a line a leg carried is added once"); - - var (shortExit, _, shortError) = Ledger(State("short"), "merge", "--from", legA); - shortExit.ShouldBe(1, "Q was in no leg"); - shortError.ShouldContain("Q"); - - var (noneExit, _, noneError) = Ledger(State("none"), "merge", "--from", State("empty")); - noneExit.ShouldBe(1); - noneError.ShouldContain("no replay of main"); - } - - [Fact] - public void Merge_fails_when_a_leg_failed() - { - adapter.Violating.Add("C1"); - var legA = State("leg-a"); var legB = State("leg-b"); var merged = State("merged"); - var (legExit, legOutput, _) = Replay(legA, "--all", "--family", "a"); - legExit.ShouldBe(1, "P violates"); - File.Exists(Path.Combine(BranchDir(legA), "logs", "P-0-host.log")).ShouldBeTrue("a failed run's full log is kept in the state"); - legOutput.ShouldContain(" log: "); - Replay(legB, "--all", "--family", "b").Exit.ShouldBe(0); - Directory.Exists(Path.Combine(BranchDir(legB), "logs")).ShouldBeFalse("a passing run keeps no log"); - - var (exit, output, _) = Ledger(merged, "merge", "--from", legA, legB); - exit.ShouldBe(1); - output.ShouldContain("failed: P"); - Files.Strings(Last(merged)["failed"]).ShouldBe(["P"]); - HasEvidence(merged, "Q").ShouldBeTrue("the merge still joined every leg"); - File.Exists(Path.Combine(BranchDir(merged), "logs", "P-0-host.log")).ShouldBeTrue("the merge carries the legs' logs"); - } - - [Fact] - public void Two_checkouts_of_one_repository_get_scratch_worktrees_of_their_own() - { - var other = fx.Linked("other"); - - var (main, linked) = (Home(fx.Root), Home(other)); - - linked.ShouldNotBe(main); - Path.GetFileName(linked).ShouldStartWith(Path.GetFileName(other) + "-"); - ReplayIn(other, "--where").Out.ShouldContain(Path.Combine(linked, "seeds")); - } - - /// Removing a linked worktree leaves its cache behind; the line `--where` prints removes it, worktrees first. - [Fact] - public void The_remove_line_of_where_removes_the_cache_and_its_worktrees() - { - var other = fx.Linked("other"); - var home = Home(other); - fx.Sh("git", "worktree", "add", "-q", "--detach", Path.Combine(home, "seeds"), "HEAD"); - var line = ReplayIn(other, "--where").Out.Split('\n').Single(l => l.StartsWith("remove:", StringComparison.Ordinal))["remove:".Length..].Trim(); - - static string Slashed(string path) => path.Replace('\\', '/'); - Slashed(line).ShouldContain($"'{Slashed(home)}'"); - fx.Sh("sh", "-c", line); - - Directory.Exists(home).ShouldBeFalse(); - Slashed(fx.Sh("git", "worktree", "list", "--porcelain")).ShouldNotContain(Slashed(home)); - } - - /// The main checkout's cache is keyed as it was before linked worktrees had their own, so its warm copy is kept. - [Fact] - public void The_main_checkout_keeps_the_cache_it_had() - { - var root = Path.GetDirectoryName(fx.Sh("git", "rev-parse", "--path-format=absolute", "--git-common-dir").Trim().TrimEnd('/', '\\'))!; - var key = Convert.ToHexStringLower(System.Security.Cryptography.SHA1.HashData(System.Text.Encoding.UTF8.GetBytes(root)))[..8]; - - Home(fx.Root).ShouldBe(Path.Combine(cache, $"{Path.GetFileName(root)}-{key}")); - } - - [Fact] - public void A_round_on_one_branch_does_not_skip_a_replay_of_another() - { - var other = fx.Linked("other"); - fx.Ledger("lock", "--holder", "round-explore-1", "--mode", "explore").Exit.ShouldBe(0); - - var (exit, output, error) = ReplayIn(other, "--all"); - - exit.ShouldBe(0, error + output); - output.ShouldNotContain("skipped"); - File.Exists(Path.Combine(fx.Memory, "branches", "other", "evidence", "P.json")).ShouldBeTrue(output); - } - - /// - /// The lock is per branch and the replay worktree per checkout: a replay of another branch in this checkout (the - /// branch switched while the post-commit replay runs) skips while one holds the worktree, and never shares it. - /// - [Fact] - public void A_replay_skips_while_another_branchs_replay_in_its_checkout_holds_the_worktree() - { - using (global::Prober.Replay.HoldWorktree(Home(fx.Root))) - { - var (exit, output, error) = Replay(null, "--all", "--branch", "other"); - - exit.ShouldBe(0, error); - output.ShouldContain("skipped, another branch's replay in this checkout"); - File.Exists(Path.Combine(fx.Memory, "branches", "other", "evidence", "P.json")).ShouldBeFalse(); - } - Replay(null, "--all", "--branch", "other").Out.ShouldNotContain("skipped"); - } - - /// P over C1's two rows and Q over C2's one; a run takes a moment, so runs at once can be counted. - private sealed class FakeAdapter : IProbeAdapter - { - private int running; - public int MostAtOnce; - public List CoverageAsked { get; } = []; - public HashSet Violating { get; } = new(StringComparer.Ordinal); - - public Ran Build(string worktree, IReadOnlyDictionary environment) => new(0, "Build: OK", ""); - - public Ran RunProbe(string worktree, string filter, string results, string coverageFile, IReadOnlyDictionary environment) - { - var now = Interlocked.Increment(ref running); - lock (this) MostAtOnce = Math.Max(MostAtOnce, now); - try - { - Thread.Sleep(300); - var claim = filter.EndsWith('P') ? "C1" : "C2"; - var violates = Violating.Contains(claim); - var outDir = environment["PROBE_OUT"]!; - Directory.CreateDirectory(outDir); - var lines = Enumerable.Range(0, claim == "C1" ? 2 : 1).Select(idx => new JsonObject - { - ["idx"] = idx, ["category"] = violates ? "violation" : "pass", ["applied"] = new JsonObject { ["x"] = "1", ["y"] = "1" }, - ["reasons"] = violates ? new JsonArray("violation:seeded") : new JsonArray(), - }.ToJsonString()); - File.WriteAllText(Path.Combine(outDir, claim + ".jsonl"), string.Join('\n', lines) + "\n"); - if (violates) - { - Directory.CreateDirectory(environment["PROBE_LOG_DIR"]!); - File.WriteAllText(Path.Combine(environment["PROBE_LOG_DIR"]!, "host.log"), "the host's full output"); - } - if (coverageFile.Length > 0) - { - lock (this) CoverageAsked.Add(coverageFile); - Directory.CreateDirectory(results); - File.WriteAllText(Path.Combine(results, coverageFile), - $"{worktree}" + - ""); - } - return new Ran(violates ? 1 : 0, violates ? "TESTS FAILED" : "TESTS OK", ""); - } - finally { Interlocked.Decrement(ref running); } - } - - public (JsonObject? Axes, string? Error) DeriveAxes(string checkout, string workDirectory) => (null, "none"); - - public IReadOnlyDictionary Environment(string cacheDirectory) => new Dictionary(); - } -} diff --git a/test/CodoMetis.TypeKit.Prober.Tests/RowsPerProbeTests.cs b/test/CodoMetis.TypeKit.Prober.Tests/RowsPerProbeTests.cs deleted file mode 100644 index c52e5b5d..00000000 --- a/test/CodoMetis.TypeKit.Prober.Tests/RowsPerProbeTests.cs +++ /dev/null @@ -1,299 +0,0 @@ -using System.Text.Json.Nodes; -using Prober; - -namespace CodoMetis.TypeKit.Prober.Tests; - -/// -/// Rows are per probe: each probe of a claim runs a rows file of its own, its identity reads that file only, and -/// rows --probe P writes it whole, over the claim's pairs the other probes of the claim have not credited. -/// -public sealed class RowsPerProbeTests : IDisposable -{ - private readonly Fixture fx = new(); - public void Dispose() => fx.Dispose(); - - private static Dictionary R(string x, string y) => new(StringComparer.Ordinal) { ["x"] = x, ["y"] = y }; - - /// C1 gets a second probe, Q, with a rows file of its own; P credits the pairs of rows 11 and 22. - public RowsPerProbeTests() - { - fx.Write("tools/probe/corpus/registry.json", """{"probes": {"P": {"claim": "C1", "filter": "*P"}, "Q": {"claim": "C1", "filter": "*Q"}}}"""); - QRows("1", "2"); - fx.Commit("a second probe of C1"); - fx.Footprint(); - fx.Ledger("record-run", "--probe", "P", "--result", "pass", "--evidence", fx.Evidence([R("1", "1"), R("2", "2")])).Exit.ShouldBe(0); - } - - private void QRows(string x, string y) => - fx.Write("tools/probe/corpus/rows/Q.json", $$$"""{"claim": "C1", "rows": [{"row": {"x": "{{{x}}}", "y": "{{{y}}}"}}]}"""); - - private string Staged(string relative) => Path.Combine(fx.BranchDir, "staging", relative); - - private List StagedRows(string probe) - { - var staged = JsonNode.Parse(File.ReadAllText(Staged($"rows/{probe}.json")))!; - staged["claim"]!.GetValue().ShouldBe("C1", "a probe's rows file keeps its claim inside"); - return [.. staged["rows"]!.AsArray().Select(r => $"{r!["row"]!["x"]}{r["row"]!["y"]}").Order(StringComparer.Ordinal)]; - } - - private string Identity(string probe) - { - var (exit, output, error) = fx.Ledger("identity", "--probe", probe); - exit.ShouldBe(0, error); - return output.Trim(); - } - - [Fact] - public void Rows_for_a_probe_leave_out_the_pairs_its_siblings_credited() - { - var (exit, output, error) = fx.Ledger("rows", "--probe", "Q"); - exit.ShouldBe(0, output + error); - StagedRows("Q").ShouldBe(["12", "21"], "P credited the pairs of 11 and 22, so Q's rows cover the other two"); - File.Exists(Staged("rows/P.json")).ShouldBeFalse("writing one probe's rows leaves its sibling's as they are"); - } - - [Fact] - public void Rows_for_a_probe_do_not_subtract_its_own_credit() - { - fx.Ledger("rows", "--probe", "P").Exit.ShouldBe(0); - StagedRows("P").ShouldBe(["11", "12", "21", "22"], "P's credit came from the rows the file replaces, so it would end with them"); - } - - [Fact] - public void The_file_is_written_whole_never_appended() - { - fx.Write(Path.GetRelativePath(fx.Root, Staged("rows/Q.json")), """{"claim": "C1", "rows": [{"row": {"x": "2", "y": "1"}}, {"row": {"x": "1", "y": "2"}}, {"row": {"x": "1", "y": "2"}}]}"""); - fx.Ledger("rows", "--probe", "Q").Exit.ShouldBe(0); - StagedRows("Q").ShouldBe(["12", "21"], "the staged rows are regenerated, not kept and added to"); - } - - [Fact] - public void A_probe_that_is_not_registered_is_refused() - { - var (exit, _, error) = fx.Ledger("rows", "--probe", "Nope"); - exit.ShouldBe(2); - error.ShouldContain("no probe Nope in the registry"); - } - - [Fact] - public void Publish_moves_the_staged_rows_by_probe_with_their_claim() - { - fx.Ledger("rows", "--probe", "Q").Exit.ShouldBe(0); - fx.Ledger("publish").Exit.ShouldBe(0); - JsonNode published; - using (var gz = new System.IO.Compression.GZipStream(File.OpenRead(Path.Combine(fx.Root, "tools/probe/corpus/rows/Q.json.gz")), System.IO.Compression.CompressionMode.Decompress)) - published = JsonNode.Parse(gz)!; - published["claim"]!.GetValue().ShouldBe("C1"); - published["rows"]!.AsArray().Select(r => $"{r!["row"]!["x"]}{r["row"]!["y"]}").Order(StringComparer.Ordinal).ShouldBe(["12", "21"]); - File.Exists(Path.Combine(fx.Root, "tools/probe/corpus/rows/C1.json.gz")).ShouldBeFalse("rows are published under the probe's name, not the claim's"); - } - - [Fact] - public void A_sibling_s_rows_changing_leaves_the_probe_s_identity_and_credit() - { - var before = Identity("P"); - QRows("2", "1"); - fx.Commit("Q's rows changed"); - Identity("P").ShouldBe(before, "P's identity reads P's rows, not Q's"); - fx.Covered().ShouldBe(2, "P's pass still counts"); - } - - [Fact] - public void The_overlay_unpacks_each_probe_s_rows_and_drops_a_file_the_corpus_no_longer_has() - { - var worktree = fx.Root + "-overlay"; - try - { - var ledger = new Ledger(new Context(fx.Root, new Dictionary(), TextWriter.Null, TextWriter.Null), "main"); - var config = new JsonObject { ["probeDir"] = "probes" }; - var rows = Path.Combine(global::Prober.Replay.Overlay(ledger, config, worktree), "rows"); - Directory.EnumerateFiles(rows).Select(Path.GetFileName).Order(StringComparer.Ordinal).ShouldBe(["P.json", "Q.json"]); - File.Delete(Path.Combine(fx.Root, "tools/probe/corpus/rows/Q.json")); - global::Prober.Replay.Overlay(ledger, config, worktree); - Directory.EnumerateFiles(rows).Select(Path.GetFileName).ShouldBe(["P.json"], "a rows file left from an earlier overlay would be read by a probe of that name"); - } - finally - { - try { Directory.Delete(worktree, recursive: true); } catch (IOException) { } - } - } - - [Fact] - public void The_probe_s_own_rows_changing_moves_its_identity() - { - var before = Identity("P"); - fx.Rows([("1", "1"), ("2", "2"), ("1", "2")]); - fx.Commit("P's rows changed"); - Identity("P").ShouldNotBe(before); - fx.Covered().ShouldBe(0); - } -} - -/// -/// A registry entry's watched paths stale its footprint: what the probe's result depends on that the footprint, -/// the paths its own process ran under src/, cannot see. -/// -public sealed class WatchedTests : IDisposable -{ - private readonly Fixture fx = new(); - public void Dispose() => fx.Dispose(); - - private const string Catalogue = "test/Variants/Catalogue.cs"; - - private static Dictionary R(string x, string y) => new(StringComparer.Ordinal) { ["x"] = x, ["y"] = y }; - - private void Registry(bool watched) => - fx.Write("tools/probe/corpus/registry.json", watched - ? $$$$"""{"probes": {"P": {"claim": "C1", "filter": "*P", "watched": ["{{{{Catalogue}}}}"]}}}""" - : """{"probes": {"P": {"claim": "C1", "filter": "*P"}}}"""); - - private void ChangeTheCatalogue() - { - fx.Write(Catalogue, "class Catalogue { int Site; }\n"); - fx.Commit("the catalogue gains a site"); - } - - /// A replay's record of P: the footprint from a coverage report of src/a.cs, through the command the replay runs, and a pass. - private void Replayed() - { - var report = Path.Combine(fx.Root, "coverage.cobertura.xml"); - File.WriteAllText(report, $"""{fx.Root}"""); - var (exit, output, error) = fx.Ledger("footprint", "--probe", "P", "--root", fx.Root, "--cobertura", report); - exit.ShouldBe(0, output + error); - File.Delete(report); - fx.Ledger("record-run", "--probe", "P", "--result", "pass", "--evidence", fx.Evidence([R("1", "1"), R("2", "2")])).Exit.ShouldBe(0); - } - - private bool Stale() - { - var (exit, output, error) = fx.Ledger("stale", "--json"); - exit.ShouldBe(0, error); - return JsonNode.Parse(output)!.AsArray().Single(s => s!["probe"]!.GetValue() == "P")!["stale"]!.GetValue(); - } - - private Ledger Ledger() => new(new Context(fx.Root, new Dictionary(), TextWriter.Null, TextWriter.Null), "main"); - - [Fact] - public void The_footprint_records_each_watched_path_with_its_id() - { - Registry(watched: true); - fx.Write(Catalogue, "class Catalogue {}\n"); - fx.Commit("a probe watching a catalogue"); - Replayed(); - var line = JsonNode.Parse(File.ReadLines(Path.Combine(fx.BranchDir, "footprints.jsonl")).Last())!; - line["watched"]![Catalogue]!.GetValue().ShouldBe(fx.Sh("git", "rev-parse", $"HEAD:{Catalogue}").Trim()); - Stale().ShouldBeFalse(); - fx.Covered().ShouldBe(2); - } - - [Fact] - public void A_change_to_a_watched_path_stales_the_footprint() - { - Registry(watched: true); - fx.Write(Catalogue, "class Catalogue {}\n"); - fx.Commit("a probe watching a catalogue"); - Replayed(); - ChangeTheCatalogue(); - Stale().ShouldBeTrue("the catalogue P watches changed"); - fx.Covered().ShouldBe(0); - } - - [Fact] - public void Without_watched_the_footprint_does_not_see_the_path() - { - Registry(watched: false); - fx.Write(Catalogue, "class Catalogue {}\n"); - fx.Commit("a probe watching nothing"); - Replayed(); - ChangeTheCatalogue(); - Stale().ShouldBeFalse("the control: nothing P ran or watches changed, which is the gap watched closes"); - fx.Covered().ShouldBe(2); - } - - [Fact] - public void A_watched_path_the_footprint_did_not_record_is_not_current() - { - Registry(watched: true); - fx.Write(Catalogue, "class Catalogue {}\n"); - fx.Commit("a probe watching a catalogue"); - fx.Footprint(); - var ledger = Ledger(); - ledger.FootprintCurrent("P", ledger.Footprints()).ShouldBeFalse("the footprint line records nothing of the catalogue P watches"); - } - - [Fact] - public void A_change_to_a_watched_path_changes_the_footprint_since_a_release() - { - Registry(watched: true); - fx.Write(Catalogue, "class Catalogue {}\n"); - fx.Commit("a probe watching a catalogue"); - Replayed(); - fx.Sh("git", "tag", "v1"); - var ledger = Ledger(); - ledger.FootprintChangedSince("P", "v1", ledger.Footprints()).ShouldBeFalse(); - ChangeTheCatalogue(); - ledger = Ledger(); - ledger.FootprintChangedSince("P", "v1", ledger.Footprints()).ShouldBeTrue(); - } - - /// A replay's record of a probe that runs nothing under src/ in its own process, as C22, which builds its consumer in a child process. - private (int Exit, string Output, string Error) RanNothingUnderSrc() - { - var report = Path.Combine(fx.Root, "coverage.cobertura.xml"); - File.WriteAllText(report, $"""{fx.Root}"""); - var result = fx.Ledger("footprint", "--probe", "P", "--root", fx.Root, "--cobertura", report); - File.Delete(report); - return result; - } - - [Fact] - public void A_probe_that_ran_nothing_under_src_is_recorded_by_what_it_watches() - { - Registry(watched: true); - fx.Write(Catalogue, "class Catalogue {}\n"); - fx.Commit("a probe watching a catalogue, building in a child process"); - var (exit, output, error) = RanNothingUnderSrc(); - exit.ShouldBe(0, output + error); - var line = JsonNode.Parse(File.ReadLines(Path.Combine(fx.BranchDir, "footprints.jsonl")).Last())!; - line["footprint"]!.AsObject().Count.ShouldBe(0); - line["watched"]![Catalogue]!.GetValue().ShouldBe(fx.Sh("git", "rev-parse", $"HEAD:{Catalogue}").Trim()); - fx.Ledger("record-run", "--probe", "P", "--result", "pass", "--evidence", fx.Evidence([R("1", "1"), R("2", "2")])).Exit.ShouldBe(0); - Stale().ShouldBeFalse("what P watches is unchanged"); - fx.Covered().ShouldBe(2); - ChangeTheCatalogue(); - Stale().ShouldBeTrue("the catalogue P watches changed"); - fx.Covered().ShouldBe(0); - } - - [Fact] - public void A_probe_that_ran_nothing_under_src_and_watches_nothing_is_refused() - { - Registry(watched: false); - fx.Write(Catalogue, "class Catalogue {}\n"); - fx.Commit("a probe watching nothing, building in a child process"); - var (exit, output, error) = RanNothingUnderSrc(); - exit.ShouldBe(1, output + error); - (output + error).ShouldContain("refusing to record an empty footprint"); - File.Exists(Path.Combine(fx.BranchDir, "footprints.jsonl")).ShouldBeFalse(); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void The_gate_counts_a_change_to_a_watched_path_as_touching_the_claim(bool watched) - { - Registry(watched); - fx.Write(Catalogue, "class Catalogue {}\n"); - fx.Commit("a probe and a catalogue"); - fx.Footprint(); - fx.Sh("git", "tag", "v1"); - ChangeTheCatalogue(); - var (_, output, _) = fx.Ledger("gate", "--since", "v1", "--json"); - var gate = JsonNode.Parse(output)!; - static List Names(JsonNode? list) => [.. list!.AsArray().Select(x => x!.GetValue())]; - Names(gate["touched"]).ShouldBe(watched ? ["C1"] : [], "a change to what a probe of the claim watches touches the claim, and nothing else does here"); - // The conservative rule and the footprint agree: neither selects the claim without the other. - Names(gate["selected_by_footprint_only"]).ShouldBeEmpty(); - Names(gate["missed_by_footprint_alone"]).ShouldBeEmpty(); - } -} diff --git a/test/CodoMetis.TypeKit.Prober.Tests/SeedsTests.cs b/test/CodoMetis.TypeKit.Prober.Tests/SeedsTests.cs deleted file mode 100644 index 2fce1dab..00000000 --- a/test/CodoMetis.TypeKit.Prober.Tests/SeedsTests.cs +++ /dev/null @@ -1,408 +0,0 @@ -using System.Text.Json.Nodes; -using Prober; - -namespace CodoMetis.TypeKit.Prober.Tests; - -/// -/// A probe proves it can fail (decision 100): `seeds` applies the defect its registry entry names in a scratch -/// worktree, runs the probe there, and counts the seed caught only for a violation the unseeded probe does -/// not have. The probe here is a fake adapter: row 1 of C1 violates while the product holds DEFECT. -/// -public sealed class SeedsTests : IDisposable -{ - private readonly Fixture fx = new(); - private readonly string cache; - private readonly FakeAdapter adapter = new(); - - public SeedsTests() - { - cache = fx.Root + "-cache"; - fx.Sh("git", "config", "core.autocrlf", "false"); - fx.Write(".claude/probe-axes.md", "```json probe-axes\n" + new JsonObject - { - ["axes"] = new JsonObject { ["x"] = new JsonArray("1", "2"), ["y"] = new JsonArray("1", "2") }, - ["surfaces"] = new JsonObject { ["s"] = new JsonObject { ["axes"] = new JsonArray("x", "y"), ["watched"] = new JsonArray("src") } }, - ["replay"] = new JsonObject { ["probeDir"] = "probes", ["include"] = new JsonArray("src") }, - }.ToJsonString() + "\n```\n"); - fx.Commit("replay configuration"); - } - - public void Dispose() - { - fx.Dispose(); - try { Directory.Delete(cache, recursive: true); } catch (IOException) { } catch (UnauthorizedAccessException) { } - } - - /// Writes the seed for P: a patch made by editing the file and taking the diff, and the registry entry naming it. - private void Seed(string? file = "src/a.cs", string? edit = "class A { /* DEFECT */ }\n", JsonObject? env = null) - { - var seed = new JsonObject { ["defect"] = "A holds the defect." }; - if (file is not null) - { - var original = File.ReadAllText(Path.Combine(fx.Root, file)); - fx.Write(file, edit!); - fx.Write("tools/probe/corpus/seeds/C1.patch", fx.Sh("git", "diff", "--", file)); - fx.Write(file, original); - seed["patch"] = "seeds/C1.patch"; - } - if (env is not null) seed["env"] = env; - fx.Write("tools/probe/corpus/registry.json", new JsonObject - { - ["probes"] = new JsonObject { ["P"] = new JsonObject { ["claim"] = "C1", ["filter"] = "*P", ["seed"] = seed } }, - }.ToJsonString()); - fx.Commit("seed"); - } - - private (int Exit, string Out, string Err) Seeds(params string[] args) - { - var output = new StringWriter(); var error = new StringWriter(); - var context = new Context(fx.Root, new Dictionary { ["PROBE_CACHE_DIR"] = cache }, output, error); - int exit; - try { exit = global::Prober.Seeds.Run(context, new Args(args, new HashSet(StringComparer.Ordinal) { "dry-run", "all" }), adapter); } - catch (Refusal refusal) { error.WriteLine(refusal.Message); exit = refusal.ExitCode; } - return (exit, output.ToString(), error.ToString()); - } - - private string Worktree => Directory.GetDirectories(cache).Single() is var home ? Path.Combine(home, "seeds") : ""; - - [Fact] - public void A_seed_the_probe_catches_passes() - { - Seed(); - var (exit, output, error) = Seeds(); - exit.ShouldBe(0, output + error); - output.ShouldContain("P (C1): caught, 1 of 2 rows violate; e.g. #1: violation:"); - output.ShouldContain("1 caught"); - } - - [Fact] - public void A_seed_the_probe_misses_fails_the_command() - { - Seed(edit: "class A { /* harmless */ }\n"); - var (exit, output, _) = Seeds(); - exit.ShouldBe(1); - output.ShouldContain("P (C1): missed, no row violates that did not already (2 rows: pass 2;"); - } - - [Fact] - public void A_violation_the_probe_has_without_the_seed_is_not_the_seeds() - { - adapter.AlwaysViolates = 1; - Seed(); - var (exit, output, _) = Seeds(); - exit.ShouldBe(1, "row 1 fails unseeded too, so the seed proved nothing"); - output.ShouldContain("1 violations unseeded"); - output.ShouldContain("P (C1): missed"); - } - - [Fact] - public void A_probe_without_a_seed_fails_the_command() - { - var (exit, output, _) = Seeds(); - exit.ShouldBe(1); - output.ShouldContain("P (C1): no seed, its registry entry names none"); - } - - [Fact] - public void An_environment_toggle_is_a_seed() - { - Seed(file: null, env: new JsonObject { ["SEED_DEFECT"] = "1" }); - var (exit, output, error) = Seeds(); - exit.ShouldBe(0, output + error); - output.ShouldContain("P (C1): caught"); - } - - [Fact] - public void A_seed_outside_the_product_files_is_refused() - { - Seed(file: "README.md", edit: "# a\n\nThe claim, DEFECT.\n"); - var (exit, output, _) = Seeds(); - exit.ShouldBe(1); - output.ShouldContain("P (C1): does not apply, it patches README.md, outside the product files (src)"); - } - - [Fact] - public void A_seed_that_no_longer_applies_fails_the_command() - { - Seed(); - fx.Write("src/a.cs", "class B {}\n"); - fx.Commit("the product moved on"); - var (exit, output, _) = Seeds(); - exit.ShouldBe(1); - output.ShouldContain("P (C1): does not apply"); - } - - [Fact] - public void A_seed_that_breaks_the_build_proves_nothing() - { - adapter.BreaksOn = "DEFECT"; - Seed(); - var (exit, output, _) = Seeds(); - exit.ShouldBe(1); - output.ShouldContain("P (C1): build failed"); - } - - [Fact] - public void The_seed_is_applied_only_in_the_scratch_worktree_and_undone() - { - Seed(); - Seeds().Exit.ShouldBe(0); - File.ReadAllText(Path.Combine(fx.Root, "src/a.cs")).ShouldBe("class A {}\n"); - File.ReadAllText(Path.Combine(Worktree, "src/a.cs")).ShouldBe("class A {}\n"); - adapter.Worktrees.ShouldAllBe(w => w == Worktree); - } - - [Fact] - public void A_seeded_run_is_never_evidence() - { - Seed(); - Seeds().Exit.ShouldBe(0); - File.Exists(Path.Combine(fx.BranchDir, "runs.jsonl")).ShouldBeFalse(); - Directory.Exists(Path.Combine(fx.BranchDir, "evidence")).ShouldBeFalse(); - File.Exists(Path.Combine(fx.BranchDir, "seeds-last.json")).ShouldBeTrue(); - } - - [Fact] - public void The_seed_is_not_part_of_the_probe_s_identity() - { - var before = fx.Ledger("identity", "--probe", "P").Out; - Seed(); - fx.Ledger("identity", "--probe", "P").Out.ShouldBe(before, "a seed proves the probe; adding or changing one must not stale its results"); - } - - /// What the replay records for P: the product files it ran, at HEAD. - private void Footprint(params string[] files) => - File.AppendAllText(Path.Combine(fx.BranchDir, "footprints.jsonl"), new JsonObject - { - ["probe"] = "P", - ["footprint"] = new JsonObject(files.Select(f => KeyValuePair.Create(f, (JsonNode?)fx.Sh("git", "rev-parse", "HEAD:" + f).Trim()))), - ["compileTime"] = new JsonObject(), ["toolchain"] = new JsonObject(), - }.ToJsonString() + "\n"); - - /// P seeded and caught once, with a footprint over src/a.cs and src/b.cs or the files given, so the next run has a record to skip by. - private void CaughtOnce(params string[] footprint) - { - fx.Write("src/b.cs", "class B {}\n"); - fx.Commit("a second product file"); - Seed(); - Footprint(footprint.Length > 0 ? footprint : ["src/a.cs", "src/b.cs"]); - var (exit, output, error) = Seeds(); - exit.ShouldBe(0, output + error); - output.ShouldContain(" runs P: no run caught every seed of it yet"); - output.ShouldContain("P (C1): caught"); - } - - private JsonObject Last() => (JsonObject)JsonNode.Parse(File.ReadAllText(Path.Combine(fx.BranchDir, "seeds-last.json")))!; - - [Fact] - public void An_unchanged_probe_is_skipped_and_says_why() - { - CaughtOnce(); - var builds = adapter.Worktrees.Count; - - var (exit, output, error) = Seeds(); - - exit.ShouldBe(0, output + error); - output.ShouldContain(" skips P: every seed caught at " + fx.Sh("git", "rev-parse", "--short=7", "HEAD").Trim() + ", and neither its identity nor its product footprint changed since"); - output.ShouldContain("0 seeds of 0 probes"); - output.ShouldContain("1 probes skipped"); - adapter.Worktrees.Count.ShouldBe(builds, "nothing built or ran"); - Last()["skipped"]!.AsArray().Single()!["probe"]!.GetValue().ShouldBe("P"); - Last()["probes"]!["P"]!["commit"]!.GetValue().ShouldBe(fx.Sh("git", "rev-parse", "HEAD").Trim(), "a skipped probe's record is carried forward"); - } - - [Fact] - public void A_changed_identity_reruns() - { - CaughtOnce(); - fx.Write("tools/probe/corpus/harness/P.cs", "// probe P, changed\n"); - fx.Commit("the harness changed"); - - var (_, output, _) = Seeds(); - - output.ShouldContain(" runs P: its identity or its seeds changed since they were caught at "); - output.ShouldContain("P (C1): caught"); - } - - [Fact] - public void A_changed_seed_reruns() - { - CaughtOnce(); - Seed(edit: "class A { /* DEFECT, another */ }\n"); - - Seeds().Out.ShouldContain(" runs P: its identity or its seeds changed since they were caught at "); - } - - [Fact] - public void A_changed_product_footprint_reruns() - { - CaughtOnce(); - fx.Write("src/b.cs", "class B { int changed; }\n"); - fx.Commit("the product changed where P runs"); - - var (_, output, _) = Seeds(); - - output.ShouldContain(" runs P: its product footprint changed since "); - output.ShouldContain("P (C1): caught"); - } - - /// The seed's own file may be outside what the probe ran; a change there may leave the seed behind, so it reruns. - [Fact] - public void A_change_to_the_file_the_seed_patches_reruns() - { - CaughtOnce("src/b.cs"); - fx.Write("src/a.cs", "class A {}\n// moved on\n"); - fx.Commit("the seeded file changed"); - - Seeds().Out.ShouldContain(" runs P: a file its seeds patch changed since "); - } - - [Fact] - public void A_change_outside_the_footprint_skips() - { - CaughtOnce(); - fx.Write("src/c.cs", "class C {}\n"); - fx.Commit("the product changed where P does not run"); - - Seeds().Out.ShouldContain(" skips P: "); - } - - [Fact] - public void A_probe_without_a_recorded_footprint_runs() - { - Seed(); - Seeds().Exit.ShouldBe(0); - - Seeds().Out.ShouldContain(" runs P: no product footprint recorded for it (a replay records one)"); - } - - [Fact] - public void All_ignores_the_record() - { - CaughtOnce(); - - var (exit, output, _) = Seeds("--all"); - - exit.ShouldBe(0); - output.ShouldNotContain("skips P"); - output.ShouldContain("P (C1): caught"); - Last()["all"]!.GetValue().ShouldBeTrue(); - } - - [Fact] - public void A_probe_named_runs_whatever_its_record_says() - { - CaughtOnce(); - - Seeds("--probe", "P").Out.ShouldContain("P (C1): caught"); - } - - [Fact] - public void A_missed_seed_is_never_skipped_next_time() - { - fx.Write("src/b.cs", "class B {}\n"); - fx.Commit("a second product file"); - Seed(edit: "class A { /* harmless */ }\n"); - Footprint("src/a.cs", "src/b.cs"); - Seeds().Exit.ShouldBe(1); - Last()["probes"]!.AsObject().ShouldBeEmpty(); - - var (exit, output, _) = Seeds(); - - exit.ShouldBe(1); - output.ShouldContain(" runs P: no run caught every seed of it yet"); - output.ShouldContain("P (C1): missed"); - } - - [Fact] - public void A_seed_missed_after_a_catch_drops_the_record() - { - CaughtOnce(); - Seed(edit: "class A { /* harmless */ }\n"); - Seeds().Exit.ShouldBe(1); - - Last()["probes"]!.AsObject().ContainsKey("P").ShouldBeFalse(); - Seeds().Out.ShouldContain(" runs P: no run caught every seed of it yet"); - } - - [Fact] - public void The_record_is_what_its_schema_says() - { - CaughtOnce(); - Seeds(); - var root = AppContext.BaseDirectory; - while (!File.Exists(Path.Combine(root, "CodoMetis.TypeKit.slnx"))) root = Path.GetDirectoryName(root) ?? throw new InvalidOperationException("no CodoMetis.TypeKit.slnx above the test output"); - var schema = (JsonObject)JsonNode.Parse(File.ReadAllText(Path.Combine(root, "tools", "Prober", "Schemas", "seeds-run.schema.json")))!["properties"]!; - - foreach (var field in new[] { "probes", "skipped", "all" }) - SchemaCheck.Problems(Last()[field], (JsonObject)schema[field]!, field).ShouldBeEmpty(field); - Last()["probes"]!["P"]!["identity"]!.GetValue().ShouldBe(global::Prober.Seeds.SeedsIdentity(new Ledger(new Context(fx.Root, new Dictionary(), TextWriter.Null, TextWriter.Null), null), "P")); - } - - private List Problems() => global::Prober.Seeds.Problems(new Ledger(new Context(fx.Root, new Dictionary(), TextWriter.Null, TextWriter.Null), null)); - - [Fact] - public void The_check_without_a_build_names_a_probe_without_a_seed() - { - Problems().ShouldBe(["P: no seed, its registry entry names none"]); - Seed(); - Problems().ShouldBeEmpty(); - } - - [Fact] - public void The_check_without_a_build_names_a_seed_the_product_left_behind() - { - Seed(); - fx.Write("src/a.cs", "class B {}\n"); - fx.Commit("the product moved on"); - Problems().ShouldHaveSingleItem().ShouldStartWith("P: does not apply, "); - } - - [Fact] - public void Every_probe_in_the_repository_has_a_seed_that_applies() - { - var root = AppContext.BaseDirectory; - while (!File.Exists(Path.Combine(root, "CodoMetis.TypeKit.slnx"))) root = Path.GetDirectoryName(root) ?? throw new InvalidOperationException("no CodoMetis.TypeKit.slnx above the test output"); - global::Prober.Seeds.Problems(new Ledger(new Context(root, new Dictionary(), TextWriter.Null, TextWriter.Null), null)).ShouldBeEmpty(); - } - - /// The probe P over C1's two rows, without a build: row 1 violates where src/a.cs holds DEFECT or SEED_DEFECT is set. - private sealed class FakeAdapter : IProbeAdapter - { - public int? AlwaysViolates { get; set; } - public string? BreaksOn { get; set; } - public List Worktrees { get; } = []; - - public Ran Build(string worktree, IReadOnlyDictionary environment) - { - Worktrees.Add(worktree); - return BreaksOn is { } marker && File.ReadAllText(Path.Combine(worktree, "src", "a.cs")).Contains(marker, StringComparison.Ordinal) - ? new Ran(1, "Build: FAILED", "") - : new Ran(0, "Build: OK", ""); - } - - public Ran RunProbe(string worktree, string filter, string results, string coverageFile, IReadOnlyDictionary environment) - { - Worktrees.Add(worktree); - var defect = File.ReadAllText(Path.Combine(worktree, "src", "a.cs")).Contains("DEFECT", StringComparison.Ordinal) - || environment.GetValueOrDefault("SEED_DEFECT") == "1"; - var outDir = environment["PROBE_OUT"]!; - Directory.CreateDirectory(outDir); - var lines = Enumerable.Range(0, 2).Select(idx => - { - var violates = idx == AlwaysViolates || (defect && idx == 1); - return new JsonObject - { - ["idx"] = idx, ["category"] = violates ? "violation" : "pass", - ["reasons"] = violates ? new JsonArray("violation:A holds the defect") : new JsonArray(), - }.ToJsonString(); - }); - File.WriteAllText(Path.Combine(outDir, "C1.jsonl"), string.Join('\n', lines) + "\n"); - return new Ran(lines.Any(l => l.Contains("violation", StringComparison.Ordinal)) ? 1 : 0, "", ""); - } - - public (JsonObject? Axes, string? Error) DeriveAxes(string checkout, string workDirectory) => (null, "none"); - - public IReadOnlyDictionary Environment(string cacheDirectory) => new Dictionary(); - } -} diff --git a/test/CodoMetis.TypeKit.Prober.Tests/SourceLintTests.cs b/test/CodoMetis.TypeKit.Prober.Tests/SourceLintTests.cs deleted file mode 100644 index 7e28922b..00000000 --- a/test/CodoMetis.TypeKit.Prober.Tests/SourceLintTests.cs +++ /dev/null @@ -1,231 +0,0 @@ -using System.Text.Json.Nodes; - -namespace CodoMetis.TypeKit.Prober.Tests; - -/// -/// `register` and `publish` read the staged sources before anything is built (decision 103): a note names a -/// key of the catalogue as NoteKeys.Name, the harness's two-argument skip names an open finding of -/// this ledger as a string literal, and a check's Inapplicable or Refused cites by a Cite -/// factory with a string literal something that exists. Each refusal names the file and the line. -/// -public sealed class SourceLintTests : IDisposable -{ - private readonly Fixture fx = new(); - - public SourceLintTests() - { - fx.Write("tools/probe/corpus/harness/Row.cs", """ - public sealed class NoteKeys - { - // public static readonly NoteKeys InAComment = new("in-a-comment", Pass); - public static readonly NoteKeys Compared = new("compared", Pass); - public static readonly NoteKeys BothThrew = new("both-threw", ExpectedRefusal); - } - """); - // C1's not-promised entry, C2's, and what a citation may name besides: a file of the docs, a knowledge entry, a record. - fx.Write("tools/probe/corpus/claims.json", new JsonArray( - new JsonObject { ["id"] = "C1", ["source"] = "README.md#a", ["surfaces"] = new JsonArray("s"), - ["not-promised"] = new JsonArray(new JsonObject { ["id"] = "c1-limit", ["source"] = "README.md#a", ["sentence"] = "The claim." }) }, - new JsonObject { ["id"] = "C2", ["source"] = "README.md#a", ["surfaces"] = new JsonArray("s"), - ["not-promised"] = new JsonArray(new JsonObject { ["id"] = "c2-limit", ["source"] = "README.md#a", ["sentence"] = "The claim." }) }).ToJsonString()); - fx.Write("design/knowledge/KB-007-a-fact.md", "# KB-007\n"); - fx.Write("design/adr/0094-a-record.md", "# 94\n"); - fx.Commit("the catalogue"); - } - - public void Dispose() => fx.Dispose(); - - private string Staged => Path.Combine(fx.BranchDir, "staging"); - - private void Stage(string body) => - fx.Write(Path.GetRelativePath(fx.Root, Path.Combine(Staged, "probes", "C1Probe.cs")), $$""" - public sealed class C1Probe - { - static string? Check(Row r, string x) - { - var rest = new[] { 1, 2 }.Skip(x.Length); - // r.Note("a comment is no call", x); - var text = "r.Note(\"a string is no call\")"; - {{body}} - return null; - } - } - """); - - /// Stages a source of the probe P (C1's), or a harness source no one probe owns, holding in a check. - private void StageCheck(string body, string file = "probes/P.cs") => - fx.Write(Path.GetRelativePath(fx.Root, Path.Combine(Staged, file)), $$""" - public sealed class P - { - static string? Check(Check c, string x) - { - var refused = new Entry().Refused(); - // c.Inapplicable(Cite.NotPromised("in-a-comment")); - var text = "c.Refused(Cite.Docs(\"in-a-string.md\"))"; - {{body}} - return null; - } - } - """); - - private (int Exit, string Out, string Err) Register() => fx.Ledger("register", "--probe", "P", "--claim", "C1", "--filter", "*C1Probe"); - - private void Finding(string id, string status) => - File.AppendAllText(Path.Combine(fx.BranchDir, "findings.jsonl"), - new JsonObject { ["id"] = id, ["claim"] = "C1", ["status"] = status }.ToJsonString() + "\n"); - - [Fact] - public void A_probe_whose_notes_and_skips_are_written_out_registers() - { - Finding("F7", "open"); - Stage(""" - r.Note(NoteKeys.Compared, $"{x}: {(x.Length > 1 ? "long" : "short")}"); - r.Note(NoteKeys.BothThrew); - r.Skip("F7", "the open finding"); - """); - var (exit, output, error) = Register(); - exit.ShouldBe(0, output + error); - } - - [Fact] - public void A_note_key_built_from_text_is_refused_by_file_and_line() - { - Stage(""" r.Note("msg:" + x, "detail");"""); - var (exit, _, error) = Register(); - exit.ShouldBe(1); - error.ShouldContain("staging/probes/C1Probe.cs:8: Note(\"msg:\" + x, ...) does not name a key of NoteKeys"); - } - - [Fact] - public void A_note_key_the_catalogue_does_not_declare_is_refused() - { - Stage(""" r.Note(NoteKeys.InAComment);"""); - var (exit, _, error) = Register(); - exit.ShouldBe(1); - error.ShouldContain("C1Probe.cs:8: Note(NoteKeys.InAComment, ...) names no key of NoteKeys"); - } - - [Fact] - public void A_finding_built_from_text_is_refused() - { - Stage(""" r.Skip("F" + x, "why");"""); - var (exit, _, error) = Register(); - exit.ShouldBe(1); - error.ShouldContain("C1Probe.cs:8: Skip(\"F\" + x, ...) does not name its finding as a string literal"); - } - - [Fact] - public void A_skip_for_a_finding_that_is_not_open_is_refused() - { - Finding("F7", "open"); - Finding("F7", "withdrawn"); - Stage(""" - r.Skip("F7", "withdrawn since"); - r.Skip("F8", "never filed"); - """); - var (exit, _, error) = Register(); - exit.ShouldBe(1); - error.ShouldContain("C1Probe.cs:8: Skip(\"F7\", ...) names finding F7, which is withdrawn"); - error.ShouldContain("C1Probe.cs:9: Skip(\"F8\", ...) names no finding this ledger has"); - } - - [Fact] - public void Publish_refuses_what_register_refuses_and_moves_nothing() - { - Stage(""" r.Note("msg:" + x, "detail");"""); - var (exit, _, error) = fx.Ledger("publish"); - exit.ShouldBe(1); - error.ShouldContain("publish: "); - error.ShouldContain("C1Probe.cs:8: Note(\"msg:\" + x, ...)"); - File.Exists(Path.Combine(fx.Root, "tools/probe/corpus/probes/C1Probe.cs")).ShouldBeFalse(); - } - - [Fact] - public void A_check_citing_what_exists_registers() - { - StageCheck(""" - c.Inapplicable(Cite.NotPromised("c1-limit"), x); - c.Refused(Cite.Docs("README.md#a")); - c.Refused(Cite.Docs("README.md")); - c.Inapplicable(Cite.Fact("KB-007")); - c.Refused(Cite.Record( "0094" ), "detail"); - """); - var (exit, output, error) = Register(); - exit.ShouldBe(0, output + error); - } - - [Fact] - public void A_citation_that_is_not_a_factory_with_a_string_literal_is_refused() - { - StageCheck(""" - var cite = Cite.NotPromised("c1-limit"); - c.Inapplicable(cite); - c.Refused(Cite.NotPromised("c1-" + x)); - c.Inapplicable(Cite.Text("c1-limit")); - """); - var (exit, _, error) = Register(); - exit.ShouldBe(1); - error.ShouldContain("staging/probes/P.cs:9: Inapplicable(cite, ...) does not cite by a Cite factory with a string literal"); - error.ShouldContain("P.cs:10: Refused(Cite.NotPromised(\"c1-\" + x), ...) does not cite by a Cite factory with a string literal"); - error.ShouldContain("P.cs:11: Inapplicable(Cite.Text(\"c1-limit\"), ...) does not cite by a Cite factory with a string literal"); - } - - [Fact] - public void A_not_promised_id_the_probes_claim_does_not_list_is_refused() - { - StageCheck(""" c.Inapplicable(Cite.NotPromised("c2-limit"));"""); - var (exit, _, error) = Register(); - exit.ShouldBe(1); - error.ShouldContain("P.cs:8: Inapplicable(Cite.NotPromised(\"c2-limit\"), ...) names no not-promised entry of its claim C1"); - } - - [Fact] - public void A_harness_source_cites_any_claims_not_promised_entry_and_none_that_no_claim_lists() - { - StageCheck(""" - c.Inapplicable(Cite.NotPromised("c2-limit")); - c.Inapplicable(Cite.NotPromised("no-claim-lists-it")); - """, "harness/Shared.cs"); - var (exit, _, error) = Register(); - exit.ShouldBe(1); - error.ShouldNotContain("Shared.cs:8:"); - error.ShouldContain("staging/harness/Shared.cs:9: Inapplicable(Cite.NotPromised(\"no-claim-lists-it\"), ...) names no not-promised entry of any claim"); - } - - [Fact] - public void A_docs_source_naming_no_file_is_refused() - { - StageCheck(""" c.Refused(Cite.Docs("docs/gone.md#a"));"""); - var (exit, _, error) = Register(); - exit.ShouldBe(1); - error.ShouldContain("P.cs:8: Refused(Cite.Docs(\"docs/gone.md#a\"), ...) names no file docs/gone.md in the repository"); - } - - [Fact] - public void A_fact_with_no_knowledge_entry_is_refused() - { - StageCheck(""" c.Refused(Cite.Fact("KB-070"));"""); - var (exit, _, error) = Register(); - exit.ShouldBe(1); - error.ShouldContain("P.cs:8: Refused(Cite.Fact(\"KB-070\"), ...) names no entry under design/knowledge/"); - } - - [Fact] - public void A_record_with_no_file_under_design_adr_is_refused() - { - StageCheck(""" c.Inapplicable(Cite.Record("0095"));"""); - var (exit, _, error) = Register(); - exit.ShouldBe(1); - error.ShouldContain("P.cs:8: Inapplicable(Cite.Record(\"0095\"), ...) names no record under design/adr/"); - } - - [Fact] - public void Publish_refuses_a_citation_that_names_nothing() - { - StageCheck(""" c.Inapplicable(Cite.Record("0095"));"""); - var (exit, _, error) = fx.Ledger("publish"); - exit.ShouldBe(1); - error.ShouldContain("publish: "); - error.ShouldContain("P.cs:8: Inapplicable(Cite.Record(\"0095\"), ...) names no record under design/adr/"); - } -} diff --git a/test/CodoMetis.TypeKit.Prober.Tests/ToolchainTests.cs b/test/CodoMetis.TypeKit.Prober.Tests/ToolchainTests.cs deleted file mode 100644 index 3c22bc0d..00000000 --- a/test/CodoMetis.TypeKit.Prober.Tests/ToolchainTests.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System.Text.Json.Nodes; - -namespace CodoMetis.TypeKit.Prober.Tests; - -/// -/// A result is bound to what the axes file's toolchain commands print. A command that cannot say it refuses, by -/// name: hashed into the identity, its error text made every result read as stale, so `status` showed 0% current -/// when the dotnet first on PATH had no SDK global.json accepts. -/// -public sealed class ToolchainTests : IDisposable -{ - private readonly Fixture fx = new(); - public void Dispose() => fx.Dispose(); - - private static Dictionary R(string x, string y) => new(StringComparer.Ordinal) { ["x"] = x, ["y"] = y }; - - private void Toolchain(params string[] command) - { - var path = Path.Combine(fx.Root, ".claude/probe-axes.md"); - var text = File.ReadAllText(path); - var block = text[(text.IndexOf('\n') + 1)..text.LastIndexOf("```", StringComparison.Ordinal)]; - var axes = JsonNode.Parse(block)!.AsObject(); - axes["replay"] = new JsonObject { ["toolchain"] = new JsonObject { ["sdk"] = new JsonArray([.. command.Select(c => (JsonNode)c)]) } }; - fx.Write(".claude/probe-axes.md", "```json probe-axes\n" + axes.ToJsonString() + "\n```\n"); - fx.Commit("toolchain"); - } - - /// A pass recorded under a toolchain that names itself, then read under one that cannot. - private void RecordedThenBroken(params string[] command) - { - Toolchain("git", "--version"); - RecordPass(); - Toolchain(command); - } - - private void RecordPass() - { - fx.Footprint(); - fx.Ledger("record-run", "--probe", "P", "--result", "pass", "--evidence", fx.Evidence([R("1", "1"), R("2", "2")])).Exit.ShouldBe(0); - } - - /// The ways a toolchain command fails to name the toolchain, each with what the refusal must say. - public static TheoryData Failing() => new() - { - { ["git", "no-such-subcommand"], "exited 1" }, - { ["cmtk-no-such-program"], "did not start" }, - { ["git", "diff", "--quiet"], "printed nothing" }, - { [], "is empty" }, - }; - - [Theory, MemberData(nameof(Failing))] - public void A_toolchain_command_that_cannot_name_the_toolchain_refuses(string[] command, string reason) - { - RecordedThenBroken(command); - var (exit, output, error) = fx.Ledger("status"); - exit.ShouldBe(1, output); - error.ShouldContain("the toolchain command sdk"); - error.ShouldContain(reason); - output.ShouldNotContain("pairs current", Case.Sensitive, "a refusal, never a status of 0% current"); - } - - /// - /// The case that was found: a dotnet with no SDK global.json accepts lists the installed SDKs on stdout, which - /// reads like a version, and says why on stderr. The refusal quotes the why. - /// - [Fact] - public void A_dotnet_without_the_pinned_sdk_refuses_with_its_error_not_its_sdk_list() - { - fx.Write("global.json", """{"sdk": {"version": "0.0.1"}}"""); - RecordedThenBroken("dotnet", "--version"); - var (exit, output, error) = fx.Ledger("status"); - exit.ShouldBe(1, output); - error.ShouldContain("the toolchain command sdk (`dotnet --version`) exited"); - error.ShouldNotContain("sdk]", Case.Insensitive, "the installed SDKs on stdout are not the reason"); - } - - /// Every command that reads an identity, not only status. - [Theory] - [InlineData("status")] - [InlineData("gate")] - [InlineData("stale")] - [InlineData("rows", "--claim", "C1")] - [InlineData("identity", "--probe", "P")] - public void Every_reader_of_an_identity_refuses(params string[] command) - { - RecordedThenBroken("git", "no-such-subcommand"); - var (exit, _, error) = fx.Ledger(command); - exit.ShouldBe(1, error); - error.ShouldContain("the toolchain command sdk"); - } - - /// - /// The case that was found next: Homebrew's dotnet first on PATH (10.0.401 against the 10.0.402 pin) made every - /// `dotnet` the prober starts fail, though the SDK installer's had the pinned SDK. Here the dotnet that runs the - /// built prober has no SDK the pin accepts, and a stub in the installer's place (TOOLS_DOTNET_FALLBACK_DIR) answers: - /// the prober falls back to it, records the pass under what it prints, and keeps the pass current. - /// - [Fact] - public void A_dotnet_without_the_pinned_sdk_falls_back_to_the_installers() - { - Assert.SkipWhen(OperatingSystem.IsWindows(), "the stub installer is a shell script"); - Toolchain("dotnet", "--version"); - fx.Write("global.json", """{"sdk": {"version": "0.0.1", "rollForward": "disable"}}"""); - fx.Commit("a pin no dotnet here has"); - fx.Footprint(); - var evidence = fx.Evidence([R("1", "1"), R("2", "2")]); - - var installer = Directory.CreateTempSubdirectory("prober-installer-").FullName; - try - { - var stub = Path.Combine(installer, "dotnet"); - File.WriteAllText(stub, "#!/bin/sh\necho 0.0.1\n"); - if (!OperatingSystem.IsWindows()) File.SetUnixFileMode(stub, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute); - using var sandbox = new ToolSandbox("Prober", "prober"); - // The prober runs as its own process, so the caller's state settings would reach it: under a PROBE_STATE_DIR it - // recorded the run there, not in this repository's state, and status found no claim (#58). None reaches it. - var environment = new Dictionary - { - ["TOOLS_DOTNET_FALLBACK_DIR"] = installer, ["PROBE_STATE_DIR"] = null, ["PROBE_CACHE_DIR"] = null, ["PROBE_HOLDER"] = null, - }; - (int, string, string) Prober(params string[] arguments) => ToolSandbox.RunProcess("dotnet", [sandbox.Dll, .. arguments], fx.Root, environment); - - var (recorded, _, why) = Prober("record-run", "--probe", "P", "--result", "pass", "--evidence", evidence); - recorded.ShouldBe(0, why); - var (exit, output, error) = Prober("status", "--json"); - exit.ShouldBe(0, error); - ((int)JsonNode.Parse(output)!["claims"]!["C1"]!["current"]!.GetValue()).ShouldBe(2, "the pass, recorded under the fallback, is current"); - } - finally - { - Directory.Delete(installer, recursive: true); - } - } - - [Fact] - public void A_toolchain_that_names_itself_keeps_the_result_current() - { - Toolchain("git", "--version"); - RecordPass(); - fx.Covered().ShouldBe(2); - } -} diff --git a/test/CodoMetis.TypeKit.Prober.Tests/TryProbeTests.cs b/test/CodoMetis.TypeKit.Prober.Tests/TryProbeTests.cs deleted file mode 100644 index 7b0076f4..00000000 --- a/test/CodoMetis.TypeKit.Prober.Tests/TryProbeTests.cs +++ /dev/null @@ -1,265 +0,0 @@ -using System.Security.Cryptography; -using System.Text; -using System.Text.Json.Nodes; -using Prober; - -namespace CodoMetis.TypeKit.Prober.Tests; - -/// -/// `try` is the writer's loop for one probe: it builds once in the replay's worktree, runs that probe alone, -/// prints how each row ended and exits with the probe's code, and records nothing. The probes here are a fake -/// adapter: P runs C1's three rows (row 1 violates, and the run then exits 7) and Q runs C2's. -/// -public sealed class TryProbeTests : IDisposable -{ - private readonly Fixture fx = new(); - private readonly string cache; - private readonly FakeAdapter adapter = new(); - - public TryProbeTests() - { - cache = fx.Root + "-cache"; - fx.Sh("git", "config", "core.autocrlf", "false"); - fx.Write(".claude/probe-axes.md", "```json probe-axes\n" + new JsonObject - { - ["axes"] = new JsonObject { ["x"] = new JsonArray("1", "2"), ["y"] = new JsonArray("1", "2") }, - ["surfaces"] = new JsonObject { ["s"] = new JsonObject { ["axes"] = new JsonArray("x", "y"), ["watched"] = new JsonArray("src") } }, - ["replay"] = new JsonObject { ["probeDir"] = "probes", ["include"] = new JsonArray("src") }, - }.ToJsonString() + "\n```\n"); - fx.Write("tools/probe/corpus/claims.json", """[{"id": "C1", "source": "README.md#a", "surfaces": ["s"]}, {"id": "C2", "source": "README.md#a", "surfaces": ["s"]}]"""); - fx.Write("tools/probe/corpus/rows/Q.json", """{"claim": "C2", "rows": [{"row": {"x": "1", "y": "1"}}]}"""); - fx.Write("tools/probe/corpus/harness/Q.cs", "// probe Q\n"); - Registry(new JsonObject { ["claim"] = "C1", ["filter"] = "*P" }); - fx.Rows([("1", "1"), ("2", "2"), ("1", "2")]); - fx.Commit("two probes, C1 of three rows"); - } - - public void Dispose() - { - fx.Dispose(); - try { Directory.Delete(cache, recursive: true); } catch (IOException) { } catch (UnauthorizedAccessException) { } - } - - private void Registry(JsonObject p) => - fx.Write("tools/probe/corpus/registry.json", new JsonObject - { - ["probes"] = new JsonObject { ["P"] = p, ["Q"] = new JsonObject { ["claim"] = "C2", ["filter"] = "*Q" } }, - }.ToJsonString()); - - private (int Exit, string Out, string Err) Try(params string[] args) => TryEnding(Environment.NewLine, args); - - /// As , with the line end the output writers use, so a test sees Windows' output on any OS. - private (int Exit, string Out, string Err) TryEnding(string newLine, params string[] args) - { - var output = new StringWriter { NewLine = newLine }; var error = new StringWriter { NewLine = newLine }; - var context = new Context(fx.Root, new Dictionary { ["PROBE_CACHE_DIR"] = cache }, output, error); - int exit; - try { exit = TryProbe.Run(context, new Args(args, new HashSet(StringComparer.Ordinal) { "no-build" }), adapter); } - catch (Refusal refusal) { error.WriteLine(refusal.Message); exit = refusal.ExitCode; } - return (exit, output.ToString(), error.ToString()); - } - - /// The output's lines, whatever line end the writer used. - private static string[] Lines(string output) => output.ReplaceLineEndings("\n").Split('\n'); - - [Theory] - [InlineData("\n")] - [InlineData("\r\n")] - public void It_runs_the_one_probe_and_prints_how_each_row_ended(string newLine) - { - var (exit, output, error) = TryEnding(newLine, "--probe", "P"); - exit.ShouldBe(7, output + error); - adapter.Builds.ShouldBe(1); - adapter.Runs.ShouldHaveSingleItem().Filter.ShouldBe("*P"); - adapter.Runs[0].Rows.ShouldBeNull("every row, as the registry runs it"); - var lines = Lines(output); - lines.ShouldContain(l => l.StartsWith(" P: Tests: 1 failed of 3 (exit 7, ", StringComparison.Ordinal)); - lines.ShouldContain(" #0 pass"); - lines.ShouldContain(" #1 violation: row 1 breaks the claim"); - lines.ShouldContain(" #2 pass"); - lines.ShouldContain("prober try: P: pass 2, violation 1; exit 7"); - } - - [Fact] - public void A_probe_that_passes_exits_0() - { - adapter.Violates = null; - var (exit, output, _) = Try("--probe", "P"); - exit.ShouldBe(0, output); - output.ShouldContain("prober try: P: pass 3; exit 0"); - } - - [Fact] - public void It_records_nothing_under_the_state_directory() - { - // A staged probe and a ledger with runs and evidence, as a round leaves them. - fx.Write(".claude/agent-memory-local/prober/branches/main/staging/probes/R.cs", "// staged\n"); - fx.Write(".claude/agent-memory-local/prober/branches/main/runs.jsonl", """{"probe": "P", "result": "pass"}""" + "\n"); - var before = Hash(fx.Memory); - Try("--probe", "P").Exit.ShouldBe(7); - Try("--probe", "P", "--rows", "0").Exit.ShouldBe(0); - Hash(fx.Memory).ShouldBe(before); - } - - [Fact] - public void Rows_runs_the_rows_given_one_stretch_at_a_time() - { - var (exit, output, _) = Try("--probe", "P", "--rows", "0,2"); - exit.ShouldBe(0, output); - adapter.Runs.Select(r => r.Rows).ShouldBe(["0-0", "2-2"]); - output.ShouldContain("#0 pass"); - output.ShouldContain("#2 pass"); - output.ShouldNotContain("#1 "); - Try("--probe", "P", "--rows", "1,2").Exit.ShouldBe(7); - adapter.Runs.Skip(2).Select(r => r.Rows).ShouldBe(["1-2"]); - } - - [Fact] - public void Rows_runs_each_row_in_the_variant_that_selects_it() - { - Registry(new JsonObject - { - ["claim"] = "C1", ["filter"] = "*P", - ["variants"] = new JsonArray(new JsonObject { ["PROBE_ONLY"] = "x=1" }, new JsonObject { ["PROBE_ONLY"] = "x=2" }), - }); - fx.Commit("variants"); - var (exit, output, _) = Try("--probe", "P", "--rows", "1,2"); - exit.ShouldBe(7, output); - adapter.Runs.Select(r => (r.Only, r.Rows)).ShouldBe([("x=1", "2-2"), ("x=2", "1-1")]); - output.ShouldContain(" P [PROBE_ONLY=x=1] rows 2-2: "); - } - - [Fact] - public void A_row_the_claim_does_not_have_is_refused_before_anything_runs() - { - var (exit, _, error) = Try("--probe", "P", "--rows", "3"); - exit.ShouldBe(2); - error.ShouldContain("--rows 3: the claim has 3 row(s), 0 to 2"); - adapter.Builds.ShouldBe(0); - adapter.Runs.ShouldBeEmpty(); - } - - [Fact] - public void A_probe_not_in_the_registry_is_refused() - { - var (exit, _, error) = Try("--probe", "Nope"); - exit.ShouldBe(2); - error.ShouldContain("no probe Nope in the registry"); - } - - [Fact] - public void No_build_runs_without_building() - { - Try("--probe", "P", "--no-build").Exit.ShouldBe(2, "no worktree yet, so nothing was ever built"); - Try("--probe", "P").Exit.ShouldBe(7); - Try("--probe", "P", "--no-build").Exit.ShouldBe(7); - adapter.Builds.ShouldBe(1); - adapter.Runs.Count.ShouldBe(2); - } - - [Fact] - public void A_failed_build_runs_nothing() - { - adapter.BuildFails = true; - var (exit, output, _) = Try("--probe", "P"); - exit.ShouldBe(1); - output.ShouldContain("Build: FAILED"); - adapter.Runs.ShouldBeEmpty(); - } - - [Fact] - public void The_staging_is_overlaid_where_the_probe_runs() - { - fx.Write(".claude/agent-memory-local/prober/branches/main/staging/probes/R.cs", "// staged\n"); - Try("--probe", "P").Exit.ShouldBe(7); - var worktree = Path.Combine(Directory.GetDirectories(cache).Single(), "wt"); - adapter.Worktrees.ShouldAllBe(w => w == worktree); - File.ReadAllText(Path.Combine(worktree, "probes", "R.cs")).ShouldBe("// staged\n"); - File.Exists(Path.Combine(worktree, "probes", "P.cs")).ShouldBeTrue(); - } - - /// Every file and directory under , by relative path and content. - private static string Hash(string dir) => - Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(string.Join("\n", - Directory.EnumerateFileSystemEntries(dir, "*", SearchOption.AllDirectories).Order(StringComparer.Ordinal) - .Select(p => Path.GetRelativePath(dir, p) + ":" + (File.Exists(p) ? Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(p))) : "dir")))))); - - [Fact] - public void It_holds_the_replay_worktree_while_it_runs() - { - var (exit, output, error) = Try("--probe", "P"); - exit.ShouldBe(7, output + error); - adapter.WorktreeFreeDuringRun.ShouldBe([false], "a replay of the same checkout must not take the worktree while a try runs in it"); - } - - [Fact] - public void It_refuses_while_a_replay_holds_the_worktree_and_runs_nothing() - { - var context = new Context(fx.Root, new Dictionary { ["PROBE_CACHE_DIR"] = cache }, TextWriter.Null, TextWriter.Null); - var home = global::Prober.Replay.Home(context, new Ledger(context, null)); - using (global::Prober.Replay.HoldWorktree(home).ShouldNotBeNull()) - { - var (exit, _, error) = Try("--probe", "P"); - exit.ShouldBe(1); - error.ShouldContain($"a replay in this checkout is using {Path.Combine(home, "wt")}; run `prober try` when it ends"); - adapter.Runs.ShouldBeEmpty(); - } - Try("--probe", "P").Exit.ShouldBe(7, "the worktree is free again once the replay ends"); - } - - /// - /// Runs the probe the filter names over its claim's rows in the worktree, as the harness does: PROBE_ONLY and - /// PROBE_ROWS select, and each row it ran is one line of PROBE_OUT's jsonl. Row 1 of C1 violates. - /// - private sealed class FakeAdapter : IProbeAdapter - { - public int? Violates { get; set; } = 1; - public bool BuildFails { get; set; } - public int Builds { get; private set; } - public List<(string Filter, string? Only, string? Rows)> Runs { get; } = []; - public List Worktrees { get; } = []; - /// For each run, whether a replay could have taken the worktree (replay.lock beside it) while the probe ran. - public List WorktreeFreeDuringRun { get; } = []; - - public Ran Build(string worktree, IReadOnlyDictionary environment) - { - Builds++; - Worktrees.Add(worktree); - return BuildFails ? new Ran(1, "Build: FAILED\nerror X", "") : new Ran(0, "Build: OK", ""); - } - - public Ran RunProbe(string worktree, string filter, string results, string coverageFile, IReadOnlyDictionary environment) - { - Worktrees.Add(worktree); - using (var replay = global::Prober.Replay.HoldWorktree(Path.GetDirectoryName(worktree)!)) WorktreeFreeDuringRun.Add(replay is not null); - var only = environment.GetValueOrDefault("PROBE_ONLY"); - var range = environment.GetValueOrDefault("PROBE_ROWS"); - Runs.Add((filter, only, range)); - coverageFile.ShouldBeEmpty("try asks for no coverage"); - // The probe's own rows: P's are C1's, Q's C2's. - var probe = filter.TrimStart('*'); - var rows = JsonNode.Parse(File.ReadAllText(Path.Combine(worktree, "probes", "rows", probe + ".json")))!["rows"]!.AsArray(); - var (lo, hi) = range is null ? (0, int.MaxValue) : (int.Parse(range.Split('-')[0]), int.Parse(range.Split('-')[1])); - var lines = new List(); var failed = 0; - for (var i = 0; i < rows.Count; i++) - { - if (i < lo || i > hi || !global::Prober.Replay.Selected(Files.StringMap(rows[i]!["row"]), only ?? "")) continue; - var violates = probe == "P" && i == Violates; - if (violates) failed++; - lines.Add(new JsonObject - { - ["idx"] = i, ["category"] = violates ? "violation" : "pass", - ["reasons"] = violates ? new JsonArray("violation:row 1 breaks the claim") : new JsonArray(), ["note"] = "compared", - }.ToJsonString()); - } - var outDir = environment["PROBE_OUT"]!; - Directory.CreateDirectory(outDir); - File.WriteAllText(Path.Combine(outDir, probe + ".jsonl"), string.Join('\n', lines) + "\n"); - return failed > 0 ? new Ran(7, $"Tests: {failed} failed of {lines.Count}\n", "") : new Ran(0, $"Tests: {lines.Count} passed\n", ""); - } - - public (JsonObject? Axes, string? Error) DeriveAxes(string checkout, string workDirectory) => (null, "none"); - - public IReadOnlyDictionary Environment(string cacheDirectory) => new Dictionary(); - } -} diff --git a/test/CodoMetis.TypeKit.Samples/Analyzers/QueryConditionSamples.cs b/test/CodoMetis.TypeKit.Samples/Analyzers/QueryConditionSamples.cs new file mode 100644 index 00000000..da47dda7 --- /dev/null +++ b/test/CodoMetis.TypeKit.Samples/Analyzers/QueryConditionSamples.cs @@ -0,0 +1,30 @@ +using CodoMetis.TypeKit; +using Microsoft.EntityFrameworkCore; + +namespace ReadmeSamples.Analyzers.QueryConditions; + +public sealed class Order +{ + public int Id { get; init; } + + public bool Open { get; set; } +} + +public sealed class OrdersDb(DbContextOptions options) : DbContext(options) +{ + public DbSet Orders => Set(); +} + +public static class QueryConditionSamples +{ + public static void DelegateCondition(OrdersDb db) + { +#pragma warning disable CMTK0009 // what the sample shows the rule reporting + // sample: CodoMetis.TypeKit.Analyzers/cmtk0009-query + Func isOpen = o => o.Open; + var first = db.Orders.FirstOrNone(isOpen); // CMTK0009: binds to the IEnumerable overload and reads every row + var found = db.Orders.FirstOrNone(o => o.Open); // the database runs the condition and returns one row + // end sample +#pragma warning restore CMTK0009 + } +} diff --git a/test/CodoMetis.TypeKit.Samples/AspNetCore/AspNetCoreSamples.cs b/test/CodoMetis.TypeKit.Samples/AspNetCore/AspNetCoreSamples.cs index e957852e..3b638173 100644 --- a/test/CodoMetis.TypeKit.Samples/AspNetCore/AspNetCoreSamples.cs +++ b/test/CodoMetis.TypeKit.Samples/AspNetCore/AspNetCoreSamples.cs @@ -1,8 +1,3 @@ -// sample: CodoMetis.TypeKit.AspNetCore/inline -using System.Reflection; -using CodoMetis.TypeKit.CompilerServices; // GeneratedValueObjectAttribute, on every value object - -// end sample // sample: CodoMetis.TypeKit.AspNetCore/every-document using Microsoft.AspNetCore.OpenApi; // OpenApiOptions @@ -12,6 +7,7 @@ // end sample using System.Net; +using System.Reflection; using System.Text; using System.Text.Json.Nodes; using System.Text.Json.Serialization; @@ -102,20 +98,6 @@ public static void Use(WebApplicationBuilder builder) } } -public static class Inlining -{ - public static void Use(OpenApiOptions options) - { - // sample: CodoMetis.TypeKit.AspNetCore/inline - options.AddTypeKit(); - options.CreateSchemaReferenceId = type => - (Nullable.GetUnderlyingType(type.Type) ?? type.Type).GetCustomAttribute() is not null - ? null - : OpenApiOptions.CreateDefaultSchemaReferenceId(type); - // end sample - } -} - public static class RequiredProperties { public static void Use(WebApplicationBuilder builder) diff --git a/test/CodoMetis.TypeKit.Samples/AspNetCore/FaultResponseSamples.cs b/test/CodoMetis.TypeKit.Samples/AspNetCore/FaultResponseSamples.cs index d094614b..15033077 100644 --- a/test/CodoMetis.TypeKit.Samples/AspNetCore/FaultResponseSamples.cs +++ b/test/CodoMetis.TypeKit.Samples/AspNetCore/FaultResponseSamples.cs @@ -39,6 +39,14 @@ public sealed record Account(int Id, string Email); public sealed record NewAccount(string Email); +/// What a store throws when a unique index rejects a row. +public sealed class UniqueViolationException(string message) : Exception(message); + +public interface IAccountStore +{ + Account Save(Account account); +} + public interface IAccounts { Result Open(NewAccount body); @@ -79,6 +87,25 @@ public static void Endpoints(WebApplication app, ISignIn accounts) } } +/// The README's sign-in with the name withheld: the domain's faults answered as they are, and alike. +public static class WithheldSignIns +{ + public static void Responses(WebApplicationBuilder builder) + { + // sample: CodoMetis.TypeKit.AspNetCore/withheld-fault-name + builder.Services.AddFaultResponses(_ => FaultResponse.Status(StatusCodes.Status401Unauthorized).WithoutFaultName()); + + // end sample + } + + public static void Endpoints(WebApplication app, ISignIn accounts) + { + // sample: CodoMetis.TypeKit.AspNetCore/withheld-fault-name + app.MapPost("/login", (Credentials credentials) => accounts.SignIn(credentials).ToHttpResult()); // no fault, no rule + // end sample + } +} + public static class Faults { public static void Responses(WebApplicationBuilder builder) @@ -142,6 +169,28 @@ public static Result> Reserved() return reserved; } + public static Result> Saved(IAccountStore store, Account account) + { + // sample: CodoMetis.TypeKit.AspNetCore/fault-cause-domain + Result> saved = Result.Catch(() => store.Save(account)) + .MapError(e => Fault.Of(AccountFault.EmailTaken, "Another account took the address a moment ago.", e)); + // end sample + + return saved; + } + + public static void Logged(WebApplicationBuilder builder) + { + // sample: CodoMetis.TypeKit.AspNetCore/fault-cause-log + builder.Services.AddProblemDetails(options => options.CustomizeProblemDetails = context => + { + if (context.HttpContext.Features.Get() is { Cause: { } cause } fault) + context.HttpContext.RequestServices.GetRequiredService().CreateLogger("Faults") + .LogWarning(cause, "Answered {Fault} for an exception", fault.Name); // your log gets the exception, the client doesn't + }); + // end sample + } + public static void Shaped(WebApplicationBuilder builder) { // sample: CodoMetis.TypeKit.AspNetCore/fault-feature @@ -222,6 +271,25 @@ private static async Task StartWithAsync(Action throw thrown; + } + + /// The README's cause: the store's exception is kept on the fault, and the fault is still the one the client sees. + [Fact] + public void A_caught_exception_is_kept_as_the_cause() + { + var thrown = new UniqueViolationException("duplicate key value violates unique constraint \"ix_accounts_email\""); + + var saved = Faults.Saved(new TakenStore(thrown), new Account(7, "ada@example.com")); + + saved.TryGetValue(out _, out var fault).ShouldBeFalse(); + fault.Reason.ShouldBe(AccountFault.EmailTaken); + fault.Cause.ShouldBeSameAs(thrown); + fault.ShouldBe(Fault.Of(AccountFault.EmailTaken, "Another account took the address a moment ago.")); + } + /// The README's one call for every fault set: AccountFault answered by its kind with no mapping of its own. [Fact] public async Task Every_fault_set_is_answered_by_its_kind_with_one_call() @@ -408,6 +476,28 @@ public async Task Two_causes_mapped_to_one_fault_answer_alike_where_one_status_a (await AnswerAsync(app, "/login-telling", "ann")).Body["fault"]!.GetValue().ShouldBe("WrongPassword"); } + /// The README's second way: the domain's two faults answered as they are, the name withheld, and alike. + [Fact] + public async Task Two_causes_whose_name_the_mapping_withholds_answer_alike_without_a_fault() + { + var builder = WebApplication.CreateBuilder(new WebApplicationOptions { EnvironmentName = Environments.Production }); + builder.WebHost.UseTestServer(); + builder.Logging.ClearProviders(); + WithheldSignIns.Responses(builder); + + await using var app = builder.Build(); + WithheldSignIns.Endpoints(app, new Refusing()); + await app.StartAsync(TestContext.Current.CancellationToken); + + var unknownUser = await AnswerAsync(app, "/login", "nobody"); + var wrongPassword = await AnswerAsync(app, "/login", "ann"); + unknownUser.Status.ShouldBe(HttpStatusCode.Unauthorized); + unknownUser.Body.ContainsKey("fault").ShouldBeFalse(unknownUser.Body.ToJsonString()); + wrongPassword.Status.ShouldBe(unknownUser.Status); + JsonNode.DeepEquals(wrongPassword.Body, unknownUser.Body) + .ShouldBeTrue($"{wrongPassword.Body.ToJsonString()} tells itself apart from {unknownUser.Body.ToJsonString()}"); + } + private static async Task<(HttpStatusCode Status, JsonObject Body)> AnswerAsync(WebApplication app, string path, string user) { using var content = new StringContent($$"""{"user":"{{user}}","password":"secret"}""", System.Text.Encoding.UTF8, "application/json"); diff --git a/test/CodoMetis.TypeKit.Samples/Base/ResultSamples.cs b/test/CodoMetis.TypeKit.Samples/Base/ResultSamples.cs index 219d70db..d1481893 100644 --- a/test/CodoMetis.TypeKit.Samples/Base/ResultSamples.cs +++ b/test/CodoMetis.TypeKit.Samples/Base/ResultSamples.cs @@ -83,6 +83,21 @@ from price in pricing.For(item, customer) Result, SkuFault> all = parsed.Sequence(); // end sample } + + public static async Task<(List, Result, SkuFault>, Result, OrderFault>)> Streams( + IOrders orders, + IAsyncEnumerable lines, + IAsyncEnumerable> rows, + IAsyncEnumerable ids, + CancellationToken cancellationToken) + { + // sample: CodoMetis.TypeKit/streams + IAsyncEnumerable known = lines.Choose(line => Sku.TryFrom(line)); // the lines that are SKUs, as they arrive + Result, SkuFault> imported = await rows.SequenceAsync(cancellationToken); // every SKU, or the first fault + Result, OrderFault> found = await ids.TraverseAsync(orders.FindAsync); // one lookup at a time, in order + // end sample + return (await known.ToListAsync(cancellationToken), imported, found); + } } public readonly partial record struct OrderId : IValue; diff --git a/test/CodoMetis.TypeKit.Samples/CodoMetis.TypeKit.Samples.csproj b/test/CodoMetis.TypeKit.Samples/CodoMetis.TypeKit.Samples.csproj index 8116a63d..a79cceb7 100644 --- a/test/CodoMetis.TypeKit.Samples/CodoMetis.TypeKit.Samples.csproj +++ b/test/CodoMetis.TypeKit.Samples/CodoMetis.TypeKit.Samples.csproj @@ -16,6 +16,11 @@ The samples' namespaces are outside CodoMetis.TypeKit on purpose: inside it, the enclosing namespace would supply Option, Result and OrderId.New() to a sample whose README forgot the using. --> + + + true + + @@ -26,6 +31,17 @@ + + + + + + @@ -35,27 +51,4 @@ - - - - - - - - - - - - - diff --git a/test/CodoMetis.TypeKit.Samples/Docs/AspNetCoreSamples.cs b/test/CodoMetis.TypeKit.Samples/Docs/AspNetCoreSamples.cs index 1e4fb9e4..c530b4f2 100644 --- a/test/CodoMetis.TypeKit.Samples/Docs/AspNetCoreSamples.cs +++ b/test/CodoMetis.TypeKit.Samples/Docs/AspNetCoreSamples.cs @@ -89,6 +89,13 @@ public static void Configure(WebApplicationBuilder builder) // end sample } + public static void Inline(WebApplicationBuilder builder) + { + // sample: aspnet-core/inline + builder.Services.AddOpenApi(options => options.AddTypeKit(typeKit => typeKit.InlineValueObjects = true)); + // end sample + } + public static void Map(WebApplication app, ICustomers customers) { #pragma warning disable ASP0020 // the value objects are declared in this project, as the page says @@ -136,7 +143,7 @@ public sealed partial class AspNetCoreTests { private const string Readme = "docs/aspnet-core.md"; - private static async Task<(WebApplication App, InMemoryCustomers Customers)> StartAsync(bool customize = false) + private static async Task<(WebApplication App, InMemoryCustomers Customers)> StartAsync(bool customize = false, bool inline = false) { var builder = WebApplication.CreateBuilder(new WebApplicationOptions { EnvironmentName = Environments.Production }); builder.WebHost.UseTestServer(); @@ -146,6 +153,7 @@ public sealed partial class AspNetCoreTests builder.Services.AddControllers().AddApplicationPart(typeof(CustomersController).Assembly); Host.Configure(builder); if (customize) Host.Customize(builder); + if (inline) Host.Inline(builder); // a second AddTypeKit on the same document: the setting's names come first var app = builder.Build(); app.MapOpenApi(); @@ -248,6 +256,24 @@ public async Task The_document_says_what_the_page_shows() page!["schema"]!["type"]!.ToJsonString().ShouldContain("integer", Case.Sensitive, "a query parameter is described as a parameter of the wrapped type, inline"); } + /// The component the page shows under "Describe value objects as plain types", read from its third JSON block. + [Fact] + public async Task With_InlineValueObjects_the_document_says_what_the_page_shows() + { + var (app, _) = await StartAsync(inline: true); + await using var _ = app; + + var json = await app.GetTestClient().GetStringAsync("/openapi/v1.json", TestContext.Current.CancellationToken); + var document = JsonNode.Parse(json)!; + var shown = JsonBlocks()[2].AsObject(); + + foreach (var (name, schema) in shown) document.Component(name).ShouldContainSchema(schema!, name); + document["components"]!["schemas"]!.AsObject().Select(component => component.Key).ShouldNotContain("CustomerId"); + document["components"]!["schemas"]!.AsObject().Select(component => component.Key).ShouldNotContain("Email"); + var page = document["paths"]!["/customers"]!["get"]!["parameters"]!.AsArray().Single(parameter => parameter!["name"]!.GetValue() == "page"); + page!["schema"]!["type"]!.ToJsonString().ShouldContain("integer", Case.Sensitive, "a query parameter is still a parameter of the underlying type"); + } + private static List JsonBlocks() => [.. JsonBlock().Matches(SampleOutputs.Readme(Readme)).Select(block => JsonNode.Parse(block.Groups["body"].Value.TrimStart().StartsWith('{') ? block.Groups["body"].Value : $"{{{block.Groups["body"].Value}}}")!)]; diff --git a/test/CodoMetis.TypeKit.Samples/Docs/ResultSamples.cs b/test/CodoMetis.TypeKit.Samples/Docs/ResultSamples.cs index ad225150..2c49a521 100644 --- a/test/CodoMetis.TypeKit.Samples/Docs/ResultSamples.cs +++ b/test/CodoMetis.TypeKit.Samples/Docs/ResultSamples.cs @@ -175,16 +175,18 @@ public static (Result, CheckoutFault> Lines, Result, CheckoutFault> Reserved, Result Charged, Result, CheckoutFault> Found)> CollectAsync( - Checkout checkout, List carts, List totals, List ids, CancellationToken cancellationToken) + public static async Task<(Result, CheckoutFault> Reserved, Result Charged, Result, CheckoutFault> Found, Result, CheckoutFault> Saved, Result, CheckoutFault> Moved)> CollectAsync( + Checkout checkout, List carts, List totals, List ids, Option savedCart, Patch movedTo, CancellationToken cancellationToken) { // sample: result/collect-async Result, CheckoutFault> reserved = await carts.TraverseAsync(checkout.ReserveAsync); // one at a time, in order; nothing starts after a fault Result charged = await totals.TraverseAsync(checkout.ChargeAsync); // a command per element, likewise Result, CheckoutFault> found = await ids.TraverseAsync(checkout.LoadCartAsync, cancellationToken); // the token checked before each, and handed to it + Result, CheckoutFault> saved = await savedCart.TraverseAsync(checkout.FindCartAsync); // None calls nothing; a value is looked up + Result, CheckoutFault> moved = await movedTo.TraverseAsync(checkout.FindCartAsync); // a clear or an absence passes through // end sample - return (reserved, charged, found); + return (reserved, charged, found, saved, moved); } public static async Task<(Result Url, Result Narrowed, Result AsFault, Result Written, Result Read)> Catching(string text, string path) @@ -331,13 +333,24 @@ public async Task Collecting_asynchronously_runs_one_operation_at_a_time_and_sto { var checkout = new Checkout(); - var (reserved, charged, found) = await Usage.CollectAsync(checkout, [checkout.Cart, checkout.Empty], [10m, 20m], [checkout.Cart.Id], TestContext.Current.CancellationToken); + var (reserved, charged, found, saved, moved) = await Usage.CollectAsync( + checkout, [checkout.Cart, checkout.Empty], [10m, 20m], [checkout.Cart.Id], Option.Some(checkout.Cart.Id), Patch.Null(), TestContext.Current.CancellationToken); reserved.TryGetValue(out _, out var fault).ShouldBeFalse(); fault.ShouldBe(CheckoutFault.CartEmpty); ((bool)charged).ShouldBeTrue(); found.TryGetValue(out var carts, out _).ShouldBeTrue(); carts.ShouldBe([checkout.Cart]); + saved.TryGetValue(out var savedCart, out _).ShouldBeTrue(); + savedCart.ShouldBe(Option.Some(checkout.Cart)); + moved.TryGetValue(out var movedTo, out _).ShouldBeTrue(); + movedTo.State.ShouldBe(PatchState.Null); + + var (_, _, _, none, absent) = await Usage.CollectAsync(checkout, [], [], [], Option.None(), Patch.Absent(), TestContext.Current.CancellationToken); + none.TryGetValue(out var noCart, out _).ShouldBeTrue(); + noCart.IsNone().ShouldBeTrue(); + absent.TryGetValue(out var untouched, out _).ShouldBeTrue(); + untouched.State.ShouldBe(PatchState.Absent); } [Fact] diff --git a/test/CodoMetis.TypeKit.Samples/Docs/WalkthroughTests.cs b/test/CodoMetis.TypeKit.Samples/Docs/WalkthroughTests.cs new file mode 100644 index 00000000..8cc3ac5f --- /dev/null +++ b/test/CodoMetis.TypeKit.Samples/Docs/WalkthroughTests.cs @@ -0,0 +1,110 @@ +using System.Collections.Concurrent; +using CodoMetis.TypeKit.AspNetCore; +using ConsumerSmokeTest; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Serilog; +using Walkthrough; + +namespace ReadmeSamples.Docs; + +/// +/// The walkthrough's orders service, run in memory from the project references: every answer, log line and SQL +/// statement docs/walkthrough.md states in a comment is what the service gives. The consumer smoke test +/// runs the same service from the packed packages (tools/ConsumerSmokeTest, its walkthrough +/// consumer, whose sources this project compiles too); this runs it on every pull request's test leg. +/// +/// +/// The expected text is read from the page by , the smoke test's own reader, compiled +/// here too: never written into this test, so a changed answer is corrected in the page's comment. +/// +[Collection(nameof(Serilog.ConsoleOutput))] +public sealed class WalkthroughTests +{ + private static readonly string Page = SampleOutputs.Readme(WalkthroughPage.Path); + + [Fact] + public async Task The_service_answers_what_the_page_states() + { + await using var connection = new SqliteConnection("Data Source=:memory:"); + connection.Open(); + var sql = new ConcurrentQueue(); + + // The setup Program.cs has, but for the host's address and the SQL, which is kept here rather than printed. + // The log is written to the console as there, with the console captured: the collection runs alone. + var builder = WebApplication.CreateBuilder(new WebApplicationOptions { EnvironmentName = Environments.Production }); + builder.WebHost.UseTestServer(); + builder.Logging.ClearProviders(); + builder.Services.AddOpenApi(options => options.AddTypeKit()); + builder.Services.AddFaultResponses(fault => fault switch + { + OrderFault.NotFound => StatusCodes.Status404NotFound, + OrderFault.AlreadyShipped or OrderFault.Cancelled => StatusCodes.Status409Conflict, + OrderFault.Cleared => StatusCodes.Status422UnprocessableEntity, + _ => throw new ArgumentOutOfRangeException(nameof(fault)), + }); + builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.RespectRequiredConstructorParameters = true); + builder.Services.AddDbContext(options => options.UseSqlite(connection).UseTypeKit() + .LogTo(sql.Enqueue, [RelationalEventId.CommandExecuted])); + builder.Services.AddSingleton(new LoggerConfiguration() + .Destructure.WithTypeKit() + .WriteTo.Console(outputTemplate: "[{Level:u3}] {Message:lj}{NewLine}") + .CreateLogger()); + + await using var app = builder.Build(); + app.UseStatusCodePages(); + app.MapOpenApi(); + app.MapOrders(); + using (var scope = app.Services.CreateScope()) scope.ServiceProvider.GetRequiredService().Database.EnsureCreated(); + await app.StartAsync(TestContext.Current.CancellationToken); + + var reported = new List<(string Name, string Value)>(); + var console = new StringWriter(); + var original = Console.Out; + Console.SetOut(console); + try + { + await Tour.RunAsync(app.GetTestClient(), (name, value) => reported.Add((name, value))); + } + finally + { + Console.SetOut(original); + } + await app.StopAsync(TestContext.Current.CancellationToken); + + var problems = new List(); + foreach (var (name, value) in reported) + { + var stated = WalkthroughPage.Stated(Page, WalkthroughPage.Answer(name)); + if (!WalkthroughPage.Is(stated, value)) problems.Add($"the page states '{stated.Single()}' for {name}, but it is '{value}'"); + } + + WalkthroughPage.Declarations(Page).Except(reported.Select(answer => answer.Name)) + .ShouldBeEmpty("The page states these answers, which the tour never reports."); + + var logged = console.ToString().ReplaceLineEndings("\n").Split('\n').Where(line => line.StartsWith("[INF] ", StringComparison.Ordinal)).ToArray(); + Within(problems, "log.Information(\"Placed", logged, "the log"); + + var statements = sql.SelectMany(message => message.Split('\n')).ToArray(); + Within(problems, "order.Quantity.Value >= minimum.Value", statements, "the SQL"); + Within(problems, "Database.EnsureCreated()", statements, "the SQL"); + + logged.ShouldNotContain(line => line.Contains("ann@example.com", StringComparison.Ordinal), "The e-mail address is masked in every log line."); + problems.ShouldBeEmpty(); + } + + private static void Within(List problems, string fragment, IReadOnlyList lines, string what) + { + var stated = WalkthroughPage.Stated(Page, fragment); + if (!WalkthroughPage.Within(stated, lines)) + problems.Add($"the page states '{string.Join(" / ", stated)}' for '{fragment}', which {what} does not hold: {string.Join(" | ", lines)}"); + } +} diff --git a/test/CodoMetis.TypeKit.Samples/Generators/AddCreateSamples.cs b/test/CodoMetis.TypeKit.Samples/Generators/AddCreateSamples.cs new file mode 100644 index 00000000..a40a6257 --- /dev/null +++ b/test/CodoMetis.TypeKit.Samples/Generators/AddCreateSamples.cs @@ -0,0 +1,30 @@ +using CodoMetis.TypeKit; +using CodoMetis.TypeKit.Testing; +using CodoMetis.TypeKit.ValueObjects; + +namespace ReadmeSamples.Generators.AddCreate; + +// sample: CodoMetis.TypeKit.Generators/add-create +public enum NameFault { Blank, TooLong } + +public readonly partial record struct Name : IValidatedValue +{ + public static Result Create(string value) + { + // Add your rules: if (...) return Result.Error(NameFault.Blank); + return new Name(value); + } +} +// end sample + +/// What the fix writes is generated as any hand-written Create is: every factory runs it. +public sealed class AddCreateTests +{ + [Fact] + public void The_written_Create_accepts_every_value_and_the_factories_run_it() + { + Name.Create("").ShouldBeSuccess().Value.ShouldBe(""); + Name.TryFrom("Ada").ShouldBeSome().Value.ShouldBe("Ada"); + Name.FromKnownGood("Ada").Value.ShouldBe("Ada"); + } +} diff --git a/test/CodoMetis.TypeKit.Samples/Root/IntroSamples.cs b/test/CodoMetis.TypeKit.Samples/Root/IntroSamples.cs index 7b5b15af..d5f1fdd5 100644 --- a/test/CodoMetis.TypeKit.Samples/Root/IntroSamples.cs +++ b/test/CodoMetis.TypeKit.Samples/Root/IntroSamples.cs @@ -8,7 +8,9 @@ public enum EmailFault { NoAt } // sample: README/intro public readonly partial record struct OrderId : IValue; +// end sample +// sample: README/intro-validated public readonly partial record struct Email : IValidatedValue { public static Result Create(string value) => @@ -16,6 +18,44 @@ public static Result Create(string value) => } // end sample +public readonly partial record struct CustomerId : IValue; + +public sealed record Customer(CustomerId Id, Email Email); + +public enum SignUpFault { InvalidEmail, EmailTaken } + +public sealed class IntroUsage(List customers) +{ + public string Greet(Email email) + { + // sample: README/intro-option + Option found = customers.FirstOrNone(c => c.Email == email); + + string greeting = found.Match( + customer => $"Welcome back, {customer.Email}", + () => "Please sign up first"); + // end sample + + return greeting; + } + + public string SignUp(string input) + { + // sample: README/intro-result + Result signedUp = Email.Create(input) + .MapError(_ => SignUpFault.InvalidEmail) + .Ensure(email => !customers.Exists(c => c.Email == email), SignUpFault.EmailTaken) + .Map(email => new Customer(CustomerId.New(), email)); + + string reply = signedUp.Match( + customer => $"Welcome, {customer.Email}", + fault => $"Sign-up failed: {fault}"); + // end sample + + return reply; + } +} + // sample: README/closed-set public sealed partial class OrderStatus : IClosedSet { diff --git a/test/CodoMetis.TypeKit.Samples/Root/QuickStartSamples.cs b/test/CodoMetis.TypeKit.Samples/Root/QuickStartSamples.cs index b3b7f686..1f4d48fe 100644 --- a/test/CodoMetis.TypeKit.Samples/Root/QuickStartSamples.cs +++ b/test/CodoMetis.TypeKit.Samples/Root/QuickStartSamples.cs @@ -1,8 +1,5 @@ -// sample: README/quick-start-declaration using CodoMetis.TypeKit; using CodoMetis.TypeKit.ValueObjects; - -// end sample using System.Text.Json; using System.Text.Json.Nodes; using CodoMetis.TypeKit.AspNetCore; @@ -14,7 +11,6 @@ namespace ReadmeSamples.Root.QuickStart; -// sample: README/quick-start-declaration public enum EmailFault { Blank, NoAt } public readonly partial record struct Email : IValidatedValue @@ -27,7 +23,6 @@ public static Result Create(string value) return new Email(value.Trim()); } } -// end sample public readonly partial record struct OrderId : IValue; @@ -49,17 +44,13 @@ public static class QuickStart { public static void Use(string input) { - // sample: README/quick-start-use + // sample: README/factories var id = OrderId.New(); // a version 7 Guid Result created = Email.Create(input); // the fault says which rule refused it Option maybe = Email.TryFrom(input); // is it valid? Email known = Email.FromKnownGood("ops@example.com"); // a literal you vouch for; throws otherwise - string reply = created.Match( - email => $"Welcome, {email}", - fault => $"Please check the address ({fault})"); - JsonSerializer.Serialize(known); // "ops@example.com" JsonSerializer.Deserialize("\"nobody\""); // JsonException naming Email and NoAt, never the text Email.Parse("ops@example.com", null); // IParsable, so it binds as a route or query parameter @@ -68,7 +59,7 @@ public static void Use(string input) public static void UseTheSatellites(IServiceCollection services, string connectionString, ShopDb db) { - // sample: README/quick-start-satellites + // sample: README/setup services.AddDbContext(options => options.UseNpgsql(connectionString).UseTypeKit()); db.Customers.Where(c => c.Email.Value.EndsWith("@example.com")); // WHERE c."Email" LIKE '%@example.com' diff --git a/test/CodoMetis.TypeKit.Samples/Testing/TestingSamples.cs b/test/CodoMetis.TypeKit.Samples/Testing/TestingSamples.cs index 974c5016..c4e84f1b 100644 --- a/test/CodoMetis.TypeKit.Samples/Testing/TestingSamples.cs +++ b/test/CodoMetis.TypeKit.Samples/Testing/TestingSamples.cs @@ -22,6 +22,18 @@ public Result> Rename(int id, int version) => Fault.Of(EditFault.Stale, $"Order {id} is at version {version + 1}, not {version}."); } +/// A service answering through tasks, for the asynchronous block. +public sealed class Signups +{ + public async Task> RegisterAsync(string address) + { + await Task.Yield(); + return Email.Create(address); + } + + public ValueTask> FindAsync(string address) => new(Option.None()); +} + public sealed partial class TestingSamples { private const string Readme = "src/CodoMetis.TypeKit.Testing/README.md"; @@ -74,6 +86,21 @@ public void A_composed_fault_hands_back_the_fault_it_holds() Should.Throw(() => refusal.ShouldHold(OrderFault.NotFound)); } + /// The README's asynchronous block, run: each line awaits and passes, or this test fails with its assertion. + [Fact] + public async Task The_assertions_on_a_task_pass_as_the_README_shows() + { + var signups = new Signups(); + + // sample: CodoMetis.TypeKit.Testing/async + Email email = await signups.RegisterAsync("ops@example.com").ShouldBeSuccess(); // awaited, then asserted + await signups.RegisterAsync("nobody").ShouldBeError(EmailFault.NoAt); + await signups.FindAsync("ops@example.com").ShouldBeNone(); // a ValueTask too + // end sample + + email.Value.ShouldBe("ops@example.com"); + } + /// The failure message the README quotes, read from the README, is the one the assertion throws. [Fact] public void A_failure_says_what_the_README_quotes() diff --git a/test/CodoMetis.TypeKit.Testing.Tests/AsyncAssertionTests.cs b/test/CodoMetis.TypeKit.Testing.Tests/AsyncAssertionTests.cs new file mode 100644 index 00000000..d3f3ddef --- /dev/null +++ b/test/CodoMetis.TypeKit.Testing.Tests/AsyncAssertionTests.cs @@ -0,0 +1,244 @@ +using System.Reflection; + +namespace CodoMetis.TypeKit.Testing.Tests; + +/// +/// Every assertion has a form on a Task and on a ValueTask of what it asserts on, which +/// awaits it and answers as the synchronous form does: the same content back, the same message on a +/// failure. A task that faults is the test's own failure, and its exception propagates as it is. +/// +public sealed class AsyncAssertionTests +{ + public enum Fault { NotFound, AlreadyShipped } + + public enum Other { Conflict } + + public enum Third { Gone } + + private const string Secret = "s3cr3t-value"; + + private static readonly Result Success = Secret; + private static readonly Result Error = Result.Error(Fault.NotFound); + private static readonly Result Done = Result.Success(); + private static readonly Result Refused = Result.Error(Fault.AlreadyShipped); + private static readonly Fault Pair = Fault.Of(Fault.NotFound); + private static readonly Fault Triple = Fault.Of(Third.Gone); + + private static async Task Later(T value) + { + await Task.Yield(); + return value; + } + + private static async ValueTask LaterValue(T value) + { + await Task.Yield(); + return value; + } + + /// + /// Each synchronous assertion, keyed by its signature, with inputs it passes and fails on: the + /// synchronous call, the Task form's and the ValueTask form's over the same input. + /// + private static readonly Dictionary Sync, Func> OnTask, Func> OnValueTask)[]> Cases = new() + { + ["ShouldBeSome(Option)"] = + [ + (() => Option.Some(Secret).ShouldBeSome(), async () => await Later(Option.Some(Secret)).ShouldBeSome(), async () => await LaterValue(Option.Some(Secret)).ShouldBeSome()), + (() => Option.None().ShouldBeSome(), async () => await Later(Option.None()).ShouldBeSome(), async () => await LaterValue(Option.None()).ShouldBeSome()), + ], + ["ShouldBeNone(Option)"] = + [ + (Void(() => Option.None().ShouldBeNone()), Void(() => Later(Option.None()).ShouldBeNone()), Void(() => LaterValue(Option.None()).ShouldBeNone())), + (Void(() => Option.Some(Secret).ShouldBeNone()), Void(() => Later(Option.Some(Secret)).ShouldBeNone()), Void(() => LaterValue(Option.Some(Secret)).ShouldBeNone())), + ], + ["ShouldBeSuccess(Result)"] = + [ + (() => Success.ShouldBeSuccess(), async () => await Later(Success).ShouldBeSuccess(), async () => await LaterValue(Success).ShouldBeSuccess()), + (() => Error.ShouldBeSuccess(), async () => await Later(Error).ShouldBeSuccess(), async () => await LaterValue(Error).ShouldBeSuccess()), + (() => default(Result).ShouldBeSuccess(), async () => await Later(default(Result)).ShouldBeSuccess(), async () => await LaterValue(default(Result)).ShouldBeSuccess()), + ], + ["ShouldBeError(Result)"] = + [ + (() => Error.ShouldBeError(), async () => await Later(Error).ShouldBeError(), async () => await LaterValue(Error).ShouldBeError()), + (() => Success.ShouldBeError(), async () => await Later(Success).ShouldBeError(), async () => await LaterValue(Success).ShouldBeError()), + ], + ["ShouldBeError(Result, TError)"] = + [ + (Void(() => Error.ShouldBeError(Fault.NotFound)), Void(() => Later(Error).ShouldBeError(Fault.NotFound)), Void(() => LaterValue(Error).ShouldBeError(Fault.NotFound))), + (Void(() => Error.ShouldBeError(Fault.AlreadyShipped)), Void(() => Later(Error).ShouldBeError(Fault.AlreadyShipped)), Void(() => LaterValue(Error).ShouldBeError(Fault.AlreadyShipped))), + ], + ["ShouldBeSuccess(Result)"] = + [ + (Void(() => Done.ShouldBeSuccess()), Void(() => Later(Done).ShouldBeSuccess()), Void(() => LaterValue(Done).ShouldBeSuccess())), + (Void(() => Refused.ShouldBeSuccess()), Void(() => Later(Refused).ShouldBeSuccess()), Void(() => LaterValue(Refused).ShouldBeSuccess())), + ], + ["ShouldBeError(Result)"] = + [ + (() => Refused.ShouldBeError(), async () => await Later(Refused).ShouldBeError(), async () => await LaterValue(Refused).ShouldBeError()), + (() => Done.ShouldBeError(), async () => await Later(Done).ShouldBeError(), async () => await LaterValue(Done).ShouldBeError()), + ], + ["ShouldBeError(Result, TError)"] = + [ + (Void(() => Refused.ShouldBeError(Fault.AlreadyShipped)), Void(() => Later(Refused).ShouldBeError(Fault.AlreadyShipped)), Void(() => LaterValue(Refused).ShouldBeError(Fault.AlreadyShipped))), + (Void(() => Refused.ShouldBeError(Fault.NotFound)), Void(() => Later(Refused).ShouldBeError(Fault.NotFound)), Void(() => LaterValue(Refused).ShouldBeError(Fault.NotFound))), + ], + ["ShouldBeValue(Patch)"] = + [ + (() => Patch.Value(Secret).ShouldBeValue(), async () => await Later(Patch.Value(Secret)).ShouldBeValue(), async () => await LaterValue(Patch.Value(Secret)).ShouldBeValue()), + (() => Patch.Null().ShouldBeValue(), async () => await Later(Patch.Null()).ShouldBeValue(), async () => await LaterValue(Patch.Null()).ShouldBeValue()), + ], + ["ShouldBeNull(Patch)"] = + [ + (Void(() => Patch.Null().ShouldBeNull()), Void(() => Later(Patch.Null()).ShouldBeNull()), Void(() => LaterValue(Patch.Null()).ShouldBeNull())), + (Void(() => Patch.Value(Secret).ShouldBeNull()), Void(() => Later(Patch.Value(Secret)).ShouldBeNull()), Void(() => LaterValue(Patch.Value(Secret)).ShouldBeNull())), + ], + ["ShouldBeAbsent(Patch)"] = + [ + (Void(() => Patch.Absent().ShouldBeAbsent()), Void(() => Later(Patch.Absent()).ShouldBeAbsent()), Void(() => LaterValue(Patch.Absent()).ShouldBeAbsent())), + (Void(() => Patch.Null().ShouldBeAbsent()), Void(() => Later(Patch.Null()).ShouldBeAbsent()), Void(() => LaterValue(Patch.Null()).ShouldBeAbsent())), + ], + ["ShouldHold(Fault, T1)"] = + [ + (() => Pair.ShouldHold(Fault.NotFound), async () => await Later(Pair).ShouldHold(Fault.NotFound), async () => await LaterValue(Pair).ShouldHold(Fault.NotFound)), + (() => Pair.ShouldHold(Fault.AlreadyShipped), async () => await Later(Pair).ShouldHold(Fault.AlreadyShipped), async () => await LaterValue(Pair).ShouldHold(Fault.AlreadyShipped)), + ], + ["ShouldHold(Fault, T2)"] = + [ + (() => Pair.ShouldHold(Other.Conflict), async () => await Later(Pair).ShouldHold(Other.Conflict), async () => await LaterValue(Pair).ShouldHold(Other.Conflict)), + ], + ["ShouldHold(Fault, T1)"] = + [ + (() => Triple.ShouldHold(Fault.NotFound), async () => await Later(Triple).ShouldHold(Fault.NotFound), async () => await LaterValue(Triple).ShouldHold(Fault.NotFound)), + ], + ["ShouldHold(Fault, T2)"] = + [ + (() => Triple.ShouldHold(Other.Conflict), async () => await Later(Triple).ShouldHold(Other.Conflict), async () => await LaterValue(Triple).ShouldHold(Other.Conflict)), + ], + ["ShouldHold(Fault, T3)"] = + [ + (() => Triple.ShouldHold(Third.Gone), async () => await Later(Triple).ShouldHold(Third.Gone), async () => await LaterValue(Triple).ShouldHold(Third.Gone)), + ], + }; + + public static TheoryData CaseNames => [.. Cases.Keys]; + + [Theory] + [MemberData(nameof(CaseNames))] + public async Task Each_form_answers_as_the_synchronous_assertion_does(string assertion) + { + foreach (var (sync, onTask, onValueTask) in Cases[assertion]) + { + var expected = Outcome(sync); + + (await OutcomeAsync(onTask)).ShouldBe(expected, $"{assertion} on a Task"); + (await OutcomeAsync(onValueTask)).ShouldBe(expected, $"{assertion} on a ValueTask"); + } + } + + /// A task that faults failed the code under test, not the assertion: its exception comes out as it was thrown. + [Fact] + public async Task A_faulted_task_s_exception_propagates_as_it_is() + { + var thrown = new InvalidOperationException("the store is down"); + + async Task> Faults() { await Task.Yield(); throw thrown; } + async ValueTask> FaultsValue() { await Task.Yield(); throw thrown; } + + (await Should.ThrowAsync(() => Faults().ShouldBeSuccess())).ShouldBeSameAs(thrown); + (await Should.ThrowAsync(() => Faults().ShouldBeError(Fault.NotFound))).ShouldBeSameAs(thrown); + (await Should.ThrowAsync(async () => await FaultsValue().ShouldBeSome())).ShouldBeSameAs(thrown); + + var cancelled = Task.FromCanceled>(new CancellationToken(canceled: true)); + await Should.ThrowAsync(() => cancelled.ShouldBeNone()); + } + + [Fact] + public async Task A_null_task_is_refused_by_name() + { + (await Should.ThrowAsync(() => ((Task>)null!).ShouldBeSome())).ParamName.ShouldBe("option"); + (await Should.ThrowAsync(() => ((Task>)null!).ShouldBeSuccess())).ParamName.ShouldBe("result"); + (await Should.ThrowAsync(() => ((Task>)null!).ShouldBeAbsent())).ParamName.ShouldBe("patch"); + (await Should.ThrowAsync(() => ((Task>)null!).ShouldHold(Other.Conflict))).ParamName.ShouldBe("fault"); + } + + /// + /// Every public assertion on a value has a case here, a form on a Task and one on a + /// ValueTask of the value, with its other parameters, and nothing else is public. + /// + [Fact] + public void Every_assertion_has_a_Task_and_a_ValueTask_form() + { + var methods = (from type in typeof(TypeKitAssertionException).Assembly.GetExportedTypes() + from method in type.GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + select method).ToList(); + + var synchronous = methods.Where(method => Pending(method.GetParameters()[0].ParameterType) is null).ToList(); + var keys = synchronous.Select(Key).ToList(); + + keys.Count.ShouldBe(16); + keys.Except(Cases.Keys).ShouldBeEmpty("These assertions have no case here."); + Cases.Keys.Except(keys).ShouldBeEmpty("These cases name no assertion."); + + foreach (var wrapper in new[] { typeof(Task<>), typeof(ValueTask<>) }) + { + var forms = methods.Where(method => Pending(method.GetParameters()[0].ParameterType) == wrapper).ToList(); + forms.Select(form => Key(form)).Order(StringComparer.Ordinal).ShouldBe(keys.Order(StringComparer.Ordinal), $"each assertion has one form on a {wrapper.Name}"); + + foreach (var form in forms) + { + var twin = synchronous.Single(method => Key(method) == Key(form)); + form.DeclaringType.ShouldBe(twin.DeclaringType, $"{Key(form)} sits beside its synchronous form"); + Describe(form.ReturnType).ShouldBe(twin.ReturnType == typeof(void) ? "Task" : $"Task<{Describe(twin.ReturnType)}>", Key(form)); + } + } + } + + private static Type? Pending(Type type) => + type.IsGenericType && (type.GetGenericTypeDefinition() == typeof(Task<>) || type.GetGenericTypeDefinition() == typeof(ValueTask<>)) + ? type.GetGenericTypeDefinition() + : null; + + private static string Key(MethodInfo method) => + $"{method.Name}({string.Join(", ", method.GetParameters().Select(p => Describe(Pending(p.ParameterType) is null || p.Position > 0 ? p.ParameterType : p.ParameterType.GetGenericArguments()[0])))})"; + + private static string Describe(Type type) => + type.IsGenericType ? $"{type.Name[..type.Name.IndexOf('`')]}<{string.Join(", ", type.GetGenericArguments().Select(Describe))}>" : type.Name; + + private static string Outcome(Func call) + { + try + { + return $"passes: {Show(call())}"; + } + catch (TypeKitAssertionException failure) + { + return $"fails: {failure.Message}"; + } + } + + private static async Task OutcomeAsync(Func> call) + { + try + { + return $"passes: {Show(await call())}"; + } + catch (TypeKitAssertionException failure) + { + return $"fails: {failure.Message}"; + } + } + + private static string Show(object? value) => value switch + { + null => "nothing", + Fault held => $"{held.Reason} {held.Detail}", + Fault held => $"{held.Reason} {held.Detail}", + Fault held => $"{held.Reason} {held.Detail}", + _ => value.ToString()!, + }; + + private static Func Void(Action call) => () => { call(); return null; }; + + private static Func> Void(Func call) => async () => { await call(); return null; }; +} diff --git a/test/CodoMetis.TypeKit.Tests/AsyncStreamTests.cs b/test/CodoMetis.TypeKit.Tests/AsyncStreamTests.cs new file mode 100644 index 00000000..148e4f8d --- /dev/null +++ b/test/CodoMetis.TypeKit.Tests/AsyncStreamTests.cs @@ -0,0 +1,445 @@ +namespace CodoMetis.TypeKit.Tests; + +/// +/// Choose, SequenceAsync and TraverseAsync over an : +/// in order, the caller's token reaching the stream's enumerator, nothing read after the first error, +/// and the enumerator disposed however the reading ends. +/// +/// +/// Every case runs over a stream whose elements arrive at once and over one whose elements each wait, +/// since an async iterator that never waits completes every step synchronously and hides a step that +/// resumes in the wrong place. +/// +public sealed class AsyncStreamTests +{ + public static TheoryData Waits => [false, true]; + + private static Option Even(int n) => n % 2 == 0 ? Option.Some(n) : Option.None(); + + private static Result Ok(int value) => Result.Success(value); + + private static Result Fail(string error) => Result.Error(error); + + [Theory] + [MemberData(nameof(Waits))] + public async Task Choose_keeps_the_values_of_the_options_that_hold_one_in_order(bool waits) + { + var options = new TrackedStream>(waits, Option.Some(1), Option.None(), Option.Some(3), default, Option.Some(5)); + + (await options.Choose().ToListAsync(TestContext.Current.CancellationToken)).ShouldBe([1, 3, 5]); + (await new TrackedStream>(waits).Choose().ToListAsync(TestContext.Current.CancellationToken)).ShouldBeEmpty(); + options.Disposed.ShouldBeTrue(); + } + + /// A default option is None, so a stream holding one answers what the same sequence does. + [Theory] + [MemberData(nameof(Waits))] + public async Task Choose_over_a_stream_answers_what_it_answers_over_the_same_sequence(bool waits) + { + Option[] options = [default, Option.Some(2), Option.None(), default, Option.Some(4)]; + int[] numbers = [1, 2, 3, 4, 5, 6]; + + (await new TrackedStream>(waits, options).Choose().ToListAsync(TestContext.Current.CancellationToken)).ShouldBe(options.Choose().ToList()); + (await new TrackedStream(waits, numbers).Choose(Even).ToListAsync(TestContext.Current.CancellationToken)).ShouldBe(numbers.Choose(Even).ToList()); + } + + [Theory] + [MemberData(nameof(Waits))] + public async Task Choose_with_a_selector_keeps_what_it_answers_Some_for_and_reads_null_elements(bool waits) + { + (await new TrackedStream(waits, 1, 2, 3, 4, 5, 6).Choose(Even).ToListAsync(TestContext.Current.CancellationToken)).ShouldBe([2, 4, 6]); + (await new TrackedStream(waits, "Annie", null, "Bo").Choose(nickname => nickname.ToOption()).ToListAsync(TestContext.Current.CancellationToken)).ShouldBe(["Annie", "Bo"]); + (await new TrackedStream(waits, 3, null, 5).Choose(floor => floor.ToOption()).ToListAsync(TestContext.Current.CancellationToken)).ShouldBe([3, 5]); + } + + /// An async LINQ operator reads its source when it is enumerated, not when it is called, and only as far as the caller reads. + [Theory] + [MemberData(nameof(Waits))] + public async Task Choose_is_lazy_and_disposes_the_stream_when_the_caller_stops(bool waits) + { + var numbers = new TrackedStream(waits, 1, 2, 3, 4, 5, 6); + + var chosen = numbers.Choose(Even); + numbers.Enumerations.ShouldBe(0, "nothing is read until the result is enumerated"); + + await foreach (var value in chosen) + { + value.ShouldBe(2); + break; + } + + numbers.Read.ShouldBe(2, "the stream is read only as far as the first value found"); + numbers.Disposed.ShouldBeTrue("a break disposes the stream's enumerator"); + } + + /// The arguments are checked when the call is made, as LINQ's are, not when the stream is first read. + [Fact] + public void Choose_checks_its_arguments_before_the_stream_is_read() + { + Should.Throw(() => ((IAsyncEnumerable)null!).Choose(Even)).ParamName.ShouldBe("source"); + Should.Throw(() => ((IAsyncEnumerable>)null!).Choose()).ParamName.ShouldBe("options"); + Should.Throw(() => new TrackedStream(false, 1).Choose((Func>)null!)).ParamName.ShouldBe("selector"); + } + + [Theory] + [MemberData(nameof(Waits))] + public async Task Choose_hands_the_token_of_WithCancellation_and_of_GetAsyncEnumerator_to_the_stream(bool waits) + { + using var cancellation = new CancellationTokenSource(); + var numbers = new TrackedStream(waits, 1, 2); + var options = new TrackedStream>(waits, Option.Some(1)); + + await foreach (var _ in numbers.Choose(Even).WithCancellation(cancellation.Token)) { } + numbers.Token.ShouldBe(cancellation.Token); + + await foreach (var _ in options.Choose().WithCancellation(cancellation.Token)) { } + options.Token.ShouldBe(cancellation.Token); + + var held = new TrackedStream(waits, 1, 2, 3, 4); + await using (var enumerator = held.Choose(Even).GetAsyncEnumerator(cancellation.Token)) + { + (await enumerator.MoveNextAsync()).ShouldBeTrue(); + held.Disposed.ShouldBeFalse(); + } + + held.Token.ShouldBe(cancellation.Token); + held.Disposed.ShouldBeTrue("disposing the chosen values disposes the stream's enumerator"); + } + + /// A stream that honours its token stops the chosen values when the caller cancels. + [Theory] + [MemberData(nameof(Waits))] + public async Task Choose_stops_when_the_caller_cancels_a_stream_that_honours_its_token(bool waits) + { + using var cancellation = new CancellationTokenSource(); + var numbers = new TrackedStream(waits, 2, 4, 6) { OnRead = n => { if (n == 4) cancellation.Cancel(); }, HonoursToken = true }; + var seen = new List(); + + await Should.ThrowAsync(async () => + { + await foreach (var value in numbers.Choose(Even).WithCancellation(cancellation.Token)) seen.Add(value); + }); + + seen.ShouldBe([2, 4]); + numbers.Disposed.ShouldBeTrue(); + } + + [Theory] + [MemberData(nameof(Waits))] + public async Task Choose_disposes_the_stream_when_the_selector_throws(bool waits) + { + var numbers = new TrackedStream(waits, 1, 2, 3); + + await Should.ThrowAsync(() => numbers.Choose(n => n == 2 ? throw new FormatException() : Option.Some(n)).ToListAsync(TestContext.Current.CancellationToken).AsTask()); + + numbers.Read.ShouldBe(2); + numbers.Disposed.ShouldBeTrue(); + } + + [Theory] + [MemberData(nameof(Waits))] + public async Task SequenceAsync_collects_every_value_in_order(bool waits) + { + var values = await new TrackedStream>(waits, Ok(1), Ok(2), Ok(3)).SequenceAsync(TestContext.Current.CancellationToken); + values.Match(list => list, _ => []).ShouldBe([1, 2, 3]); + + var none = await new TrackedStream>(waits).SequenceAsync(TestContext.Current.CancellationToken); + none.Match(list => list.Count, _ => -1).ShouldBe(0); + + ((bool)await new TrackedStream>(waits, Result.Success(), Result.Success()).SequenceAsync(TestContext.Current.CancellationToken)).ShouldBeTrue(); + ((bool)await new TrackedStream>(waits).SequenceAsync(TestContext.Current.CancellationToken)).ShouldBeTrue(); + } + + [Theory] + [MemberData(nameof(Waits))] + public async Task SequenceAsync_answers_the_first_error_reads_nothing_after_it_and_disposes_the_stream(bool waits) + { + var results = new TrackedStream>(waits, Ok(1), Fail("x"), Fail("y"), Ok(4)); + + (await results.SequenceAsync(TestContext.Current.CancellationToken)).Match(_ => "", error => error).ShouldBe("x"); + results.Read.ShouldBe(2); + results.Disposed.ShouldBeTrue(); + + var commands = new TrackedStream>(waits, Result.Success(), Result.Error("refused"), default); + + (await commands.SequenceAsync(TestContext.Current.CancellationToken)).TryGetError(out var refused).ShouldBeTrue(); + refused.ShouldBe("refused"); + commands.Read.ShouldBe(2, "the uninitialized result after the error is never read"); + commands.Disposed.ShouldBeTrue(); + } + + [Theory] + [MemberData(nameof(Waits))] + public async Task SequenceAsync_refuses_an_uninitialized_result_and_disposes_the_stream(bool waits) + { + var results = new TrackedStream>(waits, Ok(1), default, Ok(3)); + var commands = new TrackedStream>(waits, Result.Success(), default); + + (await Should.ThrowAsync(() => results.SequenceAsync())).Message.ShouldContain("never initialized"); + (await Should.ThrowAsync(() => commands.SequenceAsync())).Message.ShouldContain("never initialized"); + results.Disposed.ShouldBeTrue(); + commands.Disposed.ShouldBeTrue(); + } + + /// + /// The token reaches the stream's enumerator and is checked before each result is read, so a stream + /// that ignores its token still stops: the cancellation propagates rather than becoming an error. + /// + [Theory] + [MemberData(nameof(Waits))] + public async Task SequenceAsync_hands_the_token_to_the_stream_and_stops_once_cancelled(bool waits) + { + using var cancellation = new CancellationTokenSource(); + var results = new TrackedStream>(waits, Ok(1), Ok(2), Ok(3)) { OnRead = r => { if (r == Ok(2)) cancellation.Cancel(); } }; + + await Should.ThrowAsync(() => results.SequenceAsync(cancellation.Token)); + results.Token.ShouldBe(cancellation.Token); + results.Read.ShouldBe(2, "nothing is read after the token was cancelled"); + results.Disposed.ShouldBeTrue(); + + using var commandCancellation = new CancellationTokenSource(); + var commands = new TrackedStream>(waits, Result.Success(), Result.Success(), Result.Success()); + commands.OnRead = _ => { if (commands.Read == 2) commandCancellation.Cancel(); }; + + await Should.ThrowAsync(() => commands.SequenceAsync(commandCancellation.Token)); + commands.Token.ShouldBe(commandCancellation.Token); + commands.Read.ShouldBe(2); + commands.Disposed.ShouldBeTrue(); + } + + [Fact] + public async Task SequenceAsync_with_a_cancelled_token_reads_nothing() + { + var results = new TrackedStream>(false, Ok(1)); + var commands = new TrackedStream>(false, Result.Success()); + + await Should.ThrowAsync(() => results.SequenceAsync(new CancellationToken(canceled: true))); + await Should.ThrowAsync(() => commands.SequenceAsync(new CancellationToken(canceled: true))); + results.Read.ShouldBe(0); + commands.Read.ShouldBe(0); + } + + [Theory] + [MemberData(nameof(Waits))] + public async Task TraverseAsync_awaits_each_operation_in_order_and_collects_the_values(bool waits) + { + var order = new List(); + var texts = new TrackedStream(waits, "1", "2", "3") { OnRead = text => order.Add($"read {text}") }; + + async Task> Parse(string text) + { + order.Add($"start {text}"); + await Task.Yield(); + order.Add($"end {text}"); + return int.TryParse(text, out var n) ? Ok(n) : Fail(text); + } + + var parsed = await texts.TraverseAsync(Parse); + + parsed.Match(values => values, _ => []).ShouldBe([1, 2, 3]); + order.ShouldBe(["read 1", "start 1", "end 1", "read 2", "start 2", "end 2", "read 3", "start 3", "end 3"], "one element at a time, its operation finished before the next is read"); + texts.Disposed.ShouldBeTrue(); + } + + [Theory] + [MemberData(nameof(Waits))] + public async Task TraverseAsync_stops_reading_after_an_error_and_disposes_the_stream(bool waits) + { + var started = new List(); + var texts = new TrackedStream(waits, "1", "x", "y", "4"); + + Task> Parse(string text) + { + started.Add(text); + return Task.FromResult(int.TryParse(text, out var n) ? Ok(n) : Fail(text)); + } + + (await texts.TraverseAsync(Parse)).Match(_ => "", error => error).ShouldBe("x"); + started.ShouldBe(["1", "x"]); + texts.Read.ShouldBe(2); + texts.Disposed.ShouldBeTrue(); + + var sent = new List(); + var numbers = new TrackedStream(waits, 1, 2, 3, 4); + + Task> Send(int n) + { + if (n == 3) return Task.FromResult(Result.Error($"refused {n}")); + + sent.Add(n); + return Task.FromResult(Result.Success()); + } + + (await numbers.TraverseAsync(Send)).TryGetError(out var error).ShouldBeTrue(); + error.ShouldBe("refused 3"); + sent.ShouldBe([1, 2]); + numbers.Read.ShouldBe(3); + numbers.Disposed.ShouldBeTrue(); + + ((bool)await new TrackedStream(waits, 1, 2).TraverseAsync(Send)).ShouldBeTrue(); + ((bool)await new TrackedStream(waits).TraverseAsync(Send)).ShouldBeTrue(); + (await new TrackedStream(waits).TraverseAsync(Parse)).Match(list => list.Count, _ => -1).ShouldBe(0); + } + + [Theory] + [MemberData(nameof(Waits))] + public async Task TraverseAsync_disposes_the_stream_when_the_selector_throws(bool waits) + { + var numbers = new TrackedStream(waits, 1, 2, 3); + var commands = new TrackedStream(waits, 1, 2, 3); + + await Should.ThrowAsync(() => numbers.TraverseAsync(n => n == 2 ? throw new FormatException() : Task.FromResult(Ok(n)))); + await Should.ThrowAsync(() => commands.TraverseAsync((n, _) => n == 2 ? throw new FormatException() : Task.FromResult(Result.Success()), CancellationToken.None)); + + numbers.Read.ShouldBe(2); + numbers.Disposed.ShouldBeTrue(); + commands.Read.ShouldBe(2); + commands.Disposed.ShouldBeTrue(); + } + + /// + /// The token is handed to the stream and to each operation, and checked before each element is read + /// and before each operation: once cancelled, nothing more is read or started, and the cancellation + /// propagates rather than becoming an error. + /// + [Theory] + [MemberData(nameof(Waits))] + public async Task TraverseAsync_hands_the_token_on_and_stops_once_cancelled(bool waits) + { + using var cancellation = new CancellationTokenSource(); + var numbers = new TrackedStream(waits, 1, 2, 3); + var started = new List(); + + Task> Load(int n, CancellationToken token) + { + token.ShouldBe(cancellation.Token, "the operation receives the caller's token"); + started.Add(n); + if (n == 2) cancellation.Cancel(); + return Task.FromResult(Ok(n)); + } + + await Should.ThrowAsync(() => numbers.TraverseAsync(Load, cancellation.Token)); + numbers.Token.ShouldBe(cancellation.Token, "the stream's enumerator receives the caller's token"); + started.ShouldBe([1, 2]); + numbers.Read.ShouldBe(2, "the third element is not read after the token was cancelled"); + numbers.Disposed.ShouldBeTrue(); + + using var commandCancellation = new CancellationTokenSource(); + var commands = new TrackedStream(waits, 1, 2, 3) { OnRead = n => { if (n == 2) commandCancellation.Cancel(); } }; + started.Clear(); + + Task> Send(int n, CancellationToken token) + { + token.ShouldBe(commandCancellation.Token); + started.Add(n); + return Task.FromResult(Result.Success()); + } + + await Should.ThrowAsync(() => commands.TraverseAsync(Send, commandCancellation.Token)); + commands.Token.ShouldBe(commandCancellation.Token); + started.ShouldBe([1], "the command for an element read after the token was cancelled is not started"); + commands.Disposed.ShouldBeTrue(); + } + + [Fact] + public async Task TraverseAsync_with_a_cancelled_token_reads_and_starts_nothing() + { + var numbers = new TrackedStream(false, 1); + var started = 0; + + await Should.ThrowAsync(() => numbers.TraverseAsync((n, _) => { started++; return Task.FromResult(Ok(n)); }, new CancellationToken(canceled: true))); + await Should.ThrowAsync(() => numbers.TraverseAsync((_, _) => { started++; return Task.FromResult(Result.Success()); }, new CancellationToken(canceled: true))); + + started.ShouldBe(0); + numbers.Read.ShouldBe(0); + } + + [Theory] + [MemberData(nameof(Waits))] + public async Task TraverseAsync_with_a_token_that_is_not_cancelled_runs_every_operation(bool waits) + { + var values = await new TrackedStream(waits, 1, 2).TraverseAsync((n, _) => Task.FromResult(Ok(n * 10)), CancellationToken.None); + values.Match(list => list, _ => []).ShouldBe([10, 20]); + + var command = await new TrackedStream(waits, 1, 2).TraverseAsync((_, _) => Task.FromResult(Result.Success()), CancellationToken.None); + ((bool)command).ShouldBeTrue(); + + var refused = new TrackedStream(waits, 1, 2, 3); + (await refused.TraverseAsync((n, _) => Task.FromResult(n == 2 ? Fail("two") : Ok(n)), CancellationToken.None)).Match(_ => "", error => error).ShouldBe("two"); + refused.Read.ShouldBe(2); + refused.Disposed.ShouldBeTrue(); + } + + /// The calls in a consumer's compilation, nullable annotations on, beside System.Linq.AsyncEnumerable's operators: no warning and no ambiguity. + [Theory] + [InlineData("IAsyncEnumerable nicknames", "nicknames.Choose(nickname => nickname.ToOption())")] + [InlineData("IAsyncEnumerable floors", "floors.Where(floor => floor > 0).Choose(floor => floor.ToOption()).Select(floor => floor * 10)")] + [InlineData("IAsyncEnumerable numbers", "numbers.Select(n => n > 0 ? Option.Some(n) : Option.None()).Choose()")] + [InlineData("IAsyncEnumerable> results", "results.SequenceAsync()")] + [InlineData("IAsyncEnumerable> results", "results.Where(result => true).SequenceAsync(CancellationToken.None)")] + [InlineData("IAsyncEnumerable texts", "texts.TraverseAsync(text => Task.FromResult(Result.Success(text.Length)))")] + [InlineData("IAsyncEnumerable texts", "texts.TraverseAsync(async (text, token) => { await Task.Delay(1, token); return Result.Success(); }, CancellationToken.None)")] + [InlineData("int[] numbers", "numbers.Choose(n => Option.Some(n))")] + [InlineData("int[] numbers", "numbers.TraverseAsync(n => Task.FromResult(Result.Success(n)))")] + public void The_stream_members_compile_without_a_warning_beside_the_async_LINQ_operators(string parameter, string call) => + ConsumerCompilation.Of($$""" + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using CodoMetis.TypeKit; + + public static class Checks + { + public static object Run({{parameter}}) => {{call}}; + } + """).Problems().ShouldBeEmpty(); + + /// + /// A stream that records what its enumerator was given and how far it was read, and whether it was + /// disposed. It ignores its token unless told to honour it, as many producers do. + /// + private sealed class TrackedStream(bool waits, params T[] elements) : IAsyncEnumerable + { + public CancellationToken Token { get; private set; } + + public int Enumerations { get; private set; } + + /// How many elements were handed out. + public int Read { get; private set; } + + public bool Disposed { get; private set; } + + public Action? OnRead { get; set; } + + public bool HonoursToken { get; init; } + + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + { + Token = cancellationToken; + Enumerations++; + + return Enumerate(cancellationToken); + } + + private async IAsyncEnumerator Enumerate(CancellationToken cancellationToken) + { + try + { + foreach (var element in elements) + { + if (waits) await Task.Yield(); + if (HonoursToken) cancellationToken.ThrowIfCancellationRequested(); + + Read++; + OnRead?.Invoke(element); + yield return element; + } + } + finally + { + Disposed = true; + } + } + } +} diff --git a/test/CodoMetis.TypeKit.Tests/AsynchronousContextTests.cs b/test/CodoMetis.TypeKit.Tests/AsynchronousContextTests.cs index 9b2ca8bc..7a9f7ce8 100644 --- a/test/CodoMetis.TypeKit.Tests/AsynchronousContextTests.cs +++ b/test/CodoMetis.TypeKit.Tests/AsynchronousContextTests.cs @@ -20,7 +20,7 @@ namespace CodoMetis.TypeKit.Tests; /// (.Result, .Wait(), GetAwaiter().GetResult()) never does. A member that resumed on it /// would hang that caller, which is why ConfigureAwait(false) stays; such a post fails here at once. /// The cases are written by hand, and the completeness test holds them to every public member of the -/// assembly that returns a task. +/// assembly that returns a task or an , whose cases read it to the end. /// /// public sealed class AsynchronousContextTests @@ -72,11 +72,30 @@ public sealed class AsynchronousContextTests ["Result.TraverseAsync(IEnumerable, Func>>, CancellationToken)"] = p => new[] { 1, 2 }.TraverseAsync((x, _) => x == 1 ? p.First(Ok) : p.Later(p.Seen(Ok)), CancellationToken.None), + // A stream's first element is what the rest wait for, so the member's own await of the stream is + // the one that would resume on the caller's context; a selector runs as each element arrives. + ["Result.TraverseAsync(IAsyncEnumerable, Func>>)"] = + p => p.Stream(1, 2).TraverseAsync(x => x == 1 ? p.First(Value) : p.Later(p.Seen(Value))), + ["Result.TraverseAsync(IAsyncEnumerable, Func>>, CancellationToken)"] = + p => p.Stream(1, 2).TraverseAsync((x, _) => x == 1 ? p.First(Value) : p.Later(p.Seen(Value)), CancellationToken.None), + ["Result.TraverseAsync(IAsyncEnumerable, Func>>)"] = + p => p.Stream(1, 2).TraverseAsync(x => x == 1 ? p.First(Ok) : p.Later(p.Seen(Ok))), + ["Result.TraverseAsync(IAsyncEnumerable, Func>>, CancellationToken)"] = + p => p.Stream(1, 2).TraverseAsync((x, _) => x == 1 ? p.First(Ok) : p.Later(p.Seen(Ok)), CancellationToken.None), + ["Result.SequenceAsync(IAsyncEnumerable>, CancellationToken)"] = p => p.Stream(Value, Value).SequenceAsync(), + ["Result.SequenceAsync(IAsyncEnumerable>, CancellationToken)"] = p => p.Stream(Ok, Ok).SequenceAsync(), + ["Option.Choose(IAsyncEnumerable>)"] = p => Drain(p.Stream(Some, Some).Choose()), + ["SequenceOptionExtensions.Choose(IAsyncEnumerable, Func>)"] = p => Drain(p.Stream(1, 2).Choose(x => p.Seen(Some))), + // The delegate runs at once, on the caller's context; the task it returns is awaited without it. ["Result.CatchAsync(Func>)"] = p => Result.CatchAsync(() => p.Later(p.Seen(1))), ["Result.CatchAsync(Func)"] = p => Result.CatchAsync(() => (Task)p.Later(p.Seen(1))), ["Result.CatchAsync(Func>)"] = p => Result.CatchAsync(() => p.Later(p.Seen(1))), ["Result.CatchAsync(Func)"] = p => Result.CatchAsync(() => (Task)p.Later(p.Seen(1))), + ["Result.CatchAsync(Func>)"] = p => Result.CatchAsync(() => new ValueTask(p.Later(p.Seen(1)))), + ["Result.CatchAsync(Func)"] = p => Result.CatchAsync(() => new ValueTask(p.Later(p.Seen(1)))), + ["Result.CatchAsync(Func>)"] = p => Result.CatchAsync(() => new ValueTask(p.Later(p.Seen(1)))), + ["Result.CatchAsync(Func)"] = p => Result.CatchAsync(() => new ValueTask(p.Later(p.Seen(1)))), ["Option.MapAsync(Task>, Func)"] = p => p.First(Some).MapAsync(x => p.Seen(x)), ["Option.MapAsync(Task>, Func>)"] = p => p.First(Some).MapAsync(x => p.Later(p.Seen(x))), @@ -91,6 +110,12 @@ public sealed class AsynchronousContextTests ["Option.MapAsync(Func>)"] = p => Some.MapAsync(x => p.Later(p.Seen(x))), ["Option.BindAsync(Func>>)"] = p => Some.BindAsync(x => p.Later(p.Seen(Some))), ["Option.TapAsync(Func)"] = p => Some.TapAsync(x => p.Later(p.Seen(x))), + ["Result.TraverseAsync(Option, Func>>)"] = p => Some.TraverseAsync(x => p.Later(p.Seen(Value))), + ["Result.TraverseAsync(Option, Func>>, CancellationToken)"] = + p => Some.TraverseAsync((x, _) => p.Later(p.Seen(Value)), CancellationToken.None), + ["Result.TraverseAsync(Patch, Func>>)"] = p => Patch.Value(1).TraverseAsync(x => p.Later(p.Seen(Value))), + ["Result.TraverseAsync(Patch, Func>>, CancellationToken)"] = + p => Patch.Value(1).TraverseAsync((x, _) => p.Later(p.Seen(Value)), CancellationToken.None), ["Result.TapAsync(Func)"] = p => Ok.TapAsync(() => p.Later(p.Seen(0))), ["Result.TapErrorAsync(Func)"] = p => Failed.TapErrorAsync(e => p.Later(p.Seen(e))), @@ -107,7 +132,13 @@ public sealed class AsynchronousContextTests }; /// Members that take no callback: only that nothing resumes on the caller's context is checked. - private static readonly HashSet WithoutCallback = ["Option.ToResultAsync(Task>, TError)"]; + private static readonly HashSet WithoutCallback = + [ + "Option.ToResultAsync(Task>, TError)", + "Result.SequenceAsync(IAsyncEnumerable>, CancellationToken)", + "Result.SequenceAsync(IAsyncEnumerable>, CancellationToken)", + "Option.Choose(IAsyncEnumerable>)", + ]; public static TheoryData CaseNames => [.. Cases.Keys]; @@ -163,15 +194,21 @@ await caller.Run(async () => } [Fact] - public void Every_public_member_that_returns_a_task_has_a_case() + public void Every_public_member_that_returns_a_task_or_a_stream_has_a_case() { var members = AsynchronousMembers().ToList(); members.Count.ShouldBeGreaterThanOrEqualTo(50); - members.Except(Cases.Keys).ShouldBeEmpty("These public members return a task and have no case here."); + members.Except(Cases.Keys).ShouldBeEmpty("These public members return a task or a stream and have no case here."); Cases.Keys.Except(members).ShouldBeEmpty("These cases name no public member of the assembly."); } + /// Reads a stream to the end without capturing the caller's context, as a member's own awaits must not. + private static async Task Drain(IAsyncEnumerable stream) + { + await foreach (var _ in stream.ConfigureAwait(false)) { } + } + private static Task Start(Func chain, Probe probe, SynchronizationContext caller) { var previous = SynchronizationContext.Current; @@ -192,7 +229,7 @@ private static IEnumerable AsynchronousMembers() from type in typeof(Option<>).Assembly.GetExportedTypes() where !type.Name.Contains('<') && type.GetCustomAttribute() is null from method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly) - where typeof(Task).IsAssignableFrom(method.ReturnType) + where typeof(Task).IsAssignableFrom(method.ReturnType) || method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>) where method.GetCustomAttribute() is not { IsError: true } select (Type: type, Method: method, Signature: $"({string.Join(", ", method.GetParameters().Select(p => Describe(p.ParameterType)))})"); @@ -240,6 +277,13 @@ public async Task Later(T value) return value; } + /// A stream whose first element is 's task, the second following it at once. + public async IAsyncEnumerable Stream(T first, T second) + { + yield return await First(first).ConfigureAwait(false); + yield return second; + } + public T Seen(T value) { Called = true; diff --git a/test/CodoMetis.TypeKit.Tests/CatchAsyncValueTaskTests.cs b/test/CodoMetis.TypeKit.Tests/CatchAsyncValueTaskTests.cs new file mode 100644 index 00000000..2704c09e --- /dev/null +++ b/test/CodoMetis.TypeKit.Tests/CatchAsyncValueTaskTests.cs @@ -0,0 +1,190 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace CodoMetis.TypeKit.Tests; + +/// +/// CatchAsync over a call that returns a ValueTask: the same outcomes as the Task +/// forms, and the overloads' lower priority, which keeps every async lambda on the Task form +/// it bound to before they existed (decision 110). +/// +public sealed class CatchAsyncValueTaskTests +{ + private static readonly InvalidOperationException Boom = new("boom"); + + private static readonly TaskCanceledException Timeout = new("timed out", new TimeoutException()); + + private static async ValueTask ThrowsLaterAsync() + { + await Task.Yield(); + throw Boom; + } + + private static async ValueTask ThrowsLaterCommandAsync() + { + await Task.Yield(); + throw Boom; + } + + private static async ValueTask AnswersLaterAsync() + { + await Task.Yield(); + return 7; + } + + private static async ValueTask DoesLaterAsync() => await Task.Yield(); + + [Fact] + public async Task Every_form_answers_the_value_or_a_success() + { + (await Result.CatchAsync(AnswersLaterAsync)).Match(value => value, _ => -1).ShouldBe(7); + (await Result.CatchAsync(() => new ValueTask(7))).Match(value => value, _ => -1).ShouldBe(7); + ((bool)await Result.CatchAsync(DoesLaterAsync)).ShouldBeTrue(); + (await Result.CatchAsync(AnswersLaterAsync)).Match(value => value, _ => -1).ShouldBe(7); + ((bool)await Result.CatchAsync(DoesLaterAsync)).ShouldBeTrue(); + } + + [Fact] + public async Task Every_form_catches_an_exception_thrown_after_the_first_await() + { + (await Result.CatchAsync(ThrowsLaterAsync)).TryGetValue(out _, out var valued).ShouldBeFalse(); + valued.ShouldBeSameAs(Boom); + (await Result.CatchAsync(ThrowsLaterCommandAsync)).TryGetError(out var command).ShouldBeTrue(); + command.ShouldBeSameAs(Boom); + (await Result.CatchAsync(ThrowsLaterAsync)).TryGetValue(out _, out var narrowed).ShouldBeFalse(); + narrowed.ShouldBeSameAs(Boom); + (await Result.CatchAsync(ThrowsLaterCommandAsync)).TryGetError(out var narrowedCommand).ShouldBeTrue(); + narrowedCommand.ShouldBeSameAs(Boom); + } + + [Fact] + public async Task Every_form_catches_an_exception_thrown_before_any_task() + { + static ValueTask ThrowsForAValue() => throw Boom; + static ValueTask ThrowsForACommand() => throw Boom; + + (await Result.CatchAsync(ThrowsForAValue)).TryGetValue(out _, out var valued).ShouldBeFalse(); + valued.ShouldBeSameAs(Boom); + (await Result.CatchAsync(ThrowsForACommand)).TryGetError(out var command).ShouldBeTrue(); + command.ShouldBeSameAs(Boom); + (await Result.CatchAsync(ThrowsForAValue)).TryGetValue(out _, out var narrowed).ShouldBeFalse(); + narrowed.ShouldBeSameAs(Boom); + (await Result.CatchAsync(ThrowsForACommand)).TryGetError(out var narrowedCommand).ShouldBeTrue(); + narrowedCommand.ShouldBeSameAs(Boom); + } + + [Fact] + public async Task A_narrowed_form_lets_any_other_exception_propagate() + { + (await Should.ThrowAsync(() => Result.CatchAsync(ThrowsLaterAsync))).ShouldBeSameAs(Boom); + (await Should.ThrowAsync(() => Result.CatchAsync(ThrowsLaterCommandAsync))).ShouldBeSameAs(Boom); + } + + [Fact] + public async Task A_cancellation_passes_through_every_form() + { + using var cancellation = new CancellationTokenSource(); + await cancellation.CancelAsync(); + + async ValueTask Read() { await Task.Delay(10, cancellation.Token); return 1; } + async ValueTask Write() => await Task.Delay(10, cancellation.Token); + + await Should.ThrowAsync(() => Result.CatchAsync(Read)); + await Should.ThrowAsync(() => Result.CatchAsync(Write)); + await Should.ThrowAsync(() => Result.CatchAsync(Read)); + await Should.ThrowAsync(() => Result.CatchAsync(Write)); + } + + /// A cancellation over a is how HttpClient reports its timeout: the error, as the Task forms answer it. + [Fact] + public async Task A_timeout_is_the_error_in_every_form() + { + async ValueTask Read() { await Task.Yield(); throw Timeout; } + async ValueTask Write() { await Task.Yield(); throw Timeout; } + + (await Result.CatchAsync(Read)).TryGetValue(out _, out var valued).ShouldBeFalse(); + valued.ShouldBeSameAs(Timeout); + (await Result.CatchAsync(Write)).TryGetError(out var command).ShouldBeTrue(); + command.ShouldBeSameAs(Timeout); + (await Result.CatchAsync(Read)).TryGetValue(out _, out var narrowed).ShouldBeFalse(); + narrowed.ShouldBeSameAs(Timeout); + (await Result.CatchAsync(Write)).TryGetError(out var narrowedCommand).ShouldBeTrue(); + narrowedCommand.ShouldBeSameAs(Timeout); + } + + [Fact] + public async Task A_cancellation_that_Result_wraps_passes_through_every_form() + { + var cancelled = Task.FromCanceled(new CancellationToken(canceled: true)); + + async ValueTask Read() { await Task.Yield(); return cancelled.Result; } + async ValueTask Write() { await Task.Yield(); cancelled.Wait(); } + + await Should.ThrowAsync(() => Result.CatchAsync(Read)); + await Should.ThrowAsync(() => Result.CatchAsync(Write)); + await Should.ThrowAsync(() => Result.CatchAsync(Read)); + await Should.ThrowAsync(() => Result.CatchAsync(Write)); + } + + [Fact] + public async Task A_null_value_and_a_task_of_a_task_are_refused_rather_than_caught() + { + await Should.ThrowAsync(() => Result.CatchAsync(() => new ValueTask((string)null!))); + await Should.ThrowAsync(() => Result.CatchAsync(() => new ValueTask((string)null!))); + + (await Should.ThrowAsync(() => Result.CatchAsync(() => new ValueTask>(Task.FromResult(1))))).Message.ShouldContain("Await the inner task"); + await Should.ThrowAsync(() => Result.CatchAsync, InvalidOperationException>(() => new ValueTask>(new ValueTask(1)))); + } + + /// + /// An async lambda converts to a delegate over either task type, and without the + /// ValueTask forms' lower priority every one of them was ambiguous (CS0121, decision 30). Each + /// call here compiles and binds to the form it names; the Task rows are what bound before the + /// ValueTask forms existed. + /// + [Theory] + [InlineData("Result.CatchAsync(async () => { await Task.Yield(); return 1; })", "Task")] + [InlineData("Result.CatchAsync(async () => await FetchAsync())", "Task")] + [InlineData("Result.CatchAsync(async () => await FetchValueAsync())", "Task")] + [InlineData("Result.CatchAsync(async () => { await DoAsync(); })", "Task")] + [InlineData("Result.CatchAsync(async () => await DoValueAsync())", "Task")] + [InlineData("Result.CatchAsync(async () => await FetchAsync())", "Task")] + [InlineData("Result.CatchAsync(async () => await DoAsync())", "Task")] + [InlineData("Result.CatchAsync(() => throw new Exception())", "Task")] + [InlineData("Result.CatchAsync(() => throw new Exception())", "Task")] + [InlineData("Result.CatchAsync(() => FetchAsync())", "Task")] + [InlineData("Result.CatchAsync(FetchAsync)", "Task")] + [InlineData("Result.CatchAsync(DoAsync)", "Task")] + [InlineData("Result.CatchAsync(() => FetchValueAsync())", "ValueTask")] + [InlineData("Result.CatchAsync(FetchValueAsync)", "ValueTask")] + [InlineData("Result.CatchAsync(() => DoValueAsync())", "ValueTask")] + [InlineData("Result.CatchAsync(DoValueAsync)", "ValueTask")] + [InlineData("Result.CatchAsync(FetchValueAsync)", "ValueTask")] + [InlineData("Result.CatchAsync(() => DoValueAsync())", "ValueTask")] + public void A_call_binds_to_the_form_of_its_task_and_an_async_lambda_to_the_Task_form(string call, string task) + { + var compilation = ConsumerCompilation.Of($$""" + using System; + using System.Threading.Tasks; + using CodoMetis.TypeKit; + + static class Consumer + { + static Task FetchAsync() => Task.FromResult(1); + static Task DoAsync() => Task.CompletedTask; + static ValueTask FetchValueAsync() => new(1); + static ValueTask DoValueAsync() => ValueTask.CompletedTask; + static object Call() => {{call}}; + } + """); + + compilation.ErrorIds().ShouldBeEmpty(compilation.Problems()); + + var tree = compilation.SyntaxTrees.Single(); + var invocation = tree.GetRoot(TestContext.Current.CancellationToken).DescendantNodes().OfType() + .Single(node => node.Expression.ToString().StartsWith("Result.CatchAsync", StringComparison.Ordinal)); + var method = (IMethodSymbol)compilation.GetSemanticModel(tree).GetSymbolInfo(invocation, TestContext.Current.CancellationToken).Symbol!; + + ((INamedTypeSymbol)method.Parameters.Single().Type).DelegateInvokeMethod!.ReturnType.Name.ShouldBe(task, call); + } +} diff --git a/test/CodoMetis.TypeKit.Tests/ComposedFaultTests.cs b/test/CodoMetis.TypeKit.Tests/ComposedFaultTests.cs index 8348827b..e9dac228 100644 --- a/test/CodoMetis.TypeKit.Tests/ComposedFaultTests.cs +++ b/test/CodoMetis.TypeKit.Tests/ComposedFaultTests.cs @@ -67,6 +67,31 @@ public void Of_names_the_type_where_a_fault_becomes_a_result() thirdFault.ShouldBe(Fault.Of(RemovalFault.StillReferenced)); } + /// + /// A fault of several types holds the fault intact, its cause included, whether it converts or is made + /// by Of; and a cause takes no part in its equality, as in the fault's own (decision 52). + /// + [Fact] + public void The_cause_is_kept_and_takes_no_part_in_equality() + { + var timeout = new TimeoutException(); + + Fault converted = Fault.Of(ConcurrencyFault.Stale, null, timeout); + var first = Fault.Of(OrderFault.NotFound, "Order 7 is gone.", timeout); + var second = Fault.Of(ConcurrencyFault.Conflict, null, timeout); + var third = Fault.Of(RemovalFault.StillReferenced, null, timeout); + + converted.Match(f => f.Cause, f => f.Cause).ShouldBeSameAs(timeout); + first.Match(f => f.Cause, f => f.Cause).ShouldBeSameAs(timeout); + second.Match(f => f.Cause, f => f.Cause).ShouldBeSameAs(timeout); + third.Match(f => f.Cause, f => f.Cause, f => f.Cause).ShouldBeSameAs(timeout); + Fault.Of(OrderFault.NameTaken, null, timeout).Match(f => f.Cause, f => null, f => null).ShouldBeSameAs(timeout); + Fault.Of(ConcurrencyFault.Stale, null, timeout).Match(f => null, f => f.Cause, f => null).ShouldBeSameAs(timeout); + first.ShouldBe(Fault.Of(OrderFault.NotFound, "Order 7 is gone.")); + first.GetHashCode().ShouldBe(Fault.Of(OrderFault.NotFound, "Order 7 is gone.").GetHashCode()); + first.ToString().ShouldBe(Fault.Of(OrderFault.NotFound, "Order 7 is gone.").ToString()); + } + [Fact] public void Two_faults_are_equal_when_they_hold_the_same_type_s_equal_fault() { diff --git a/test/CodoMetis.TypeKit.Tests/FaultTests.cs b/test/CodoMetis.TypeKit.Tests/FaultTests.cs index e13485b2..cc192cd9 100644 --- a/test/CodoMetis.TypeKit.Tests/FaultTests.cs +++ b/test/CodoMetis.TypeKit.Tests/FaultTests.cs @@ -104,6 +104,77 @@ public void Two_faults_are_equal_by_reason_and_detail() Fault.Of(OrderFault.NotFound, "Order 7 is gone.").Reason.ShouldBe(OrderFault.NotFound); } + /// A cause is kept for the log beside the reason and the detail (decision 52). + [Fact] + public void A_fault_keeps_the_exception_that_caused_it() + { + var timeout = new TimeoutException("db-host-7 timed out"); + + var fault = Fault.Of(OrderFault.NotFound, "Order 7 could not be read.", timeout); + + fault.Cause.ShouldBeSameAs(timeout); + fault.Reason.ShouldBe(OrderFault.NotFound); + fault.Detail.ShouldBe("Order 7 could not be read."); + Fault.Of(OrderFault.NotFound, null, timeout).Detail.ShouldBeNull(); + new Fault(OrderFault.NotFound, null, timeout).Cause.ShouldBeSameAs(timeout); + Fault.Of(OrderFault.NotFound, "Order 7 is gone.").Cause.ShouldBeNull(); + } + + /// The pattern the docs show: a caught exception mapped to a fault keeps the exception as its cause. + [Fact] + public void MapError_from_a_caught_exception_keeps_it_as_the_cause() + { + var thrown = new InvalidOperationException("the file is locked"); + + var mapped = Result.Catch(() => Read(thrown)) + .MapError(e => Fault.Of(OrderFault.NotFound, "The order could not be read.", e)); + + mapped.TryGetValue(out _, out var fault).ShouldBeFalse(); + fault.Cause.ShouldBeSameAs(thrown); + + static int Read(Exception thrown) => throw thrown; + } + + /// + /// The cause is for the log: a fault with one equals the same fault without one and hashes alike, so a + /// test that compares faults, or a set of them, is unchanged by where the fault came from; and it + /// prints as 2.2 printed it, since assertion messages quote it. + /// + [Fact] + public void A_cause_takes_no_part_in_equality_or_printing() + { + var caused = Fault.Of(OrderFault.NotFound, "Order 7 is gone.", new TimeoutException("db-host-7")); + var plain = Fault.Of(OrderFault.NotFound, "Order 7 is gone."); + + caused.ShouldBe(plain); + (caused == plain).ShouldBeTrue(); + caused.GetHashCode().ShouldBe(plain.GetHashCode()); + caused.ToString().ShouldBe(plain.ToString()); + caused.ToString().ShouldNotContain("db-host-7"); + caused.ShouldNotBe(Fault.Of(OrderFault.NotFound, "Order 8 is gone.", new TimeoutException())); + caused.WithRule(Access.Read).ShouldNotBe(plain); + } + + [Fact] + public void A_rule_and_a_copy_keep_the_cause() + { + var timeout = new TimeoutException(); + var fault = Fault.Of(OrderFault.NotFound, null, timeout); + + fault.WithRule(Access.Read).Cause.ShouldBeSameAs(timeout); + fault.WithRule(EmailRule.NoAt).Cause.ShouldBeSameAs(timeout); + (fault with { }).Cause.ShouldBeSameAs(timeout); + } + + /// A null cause is a mistake where one is given: leave it out for none. + [Fact] + public void A_null_cause_is_refused() + { + Should.Throw(() => Fault.Of(OrderFault.NotFound, "Order 7 is gone.", null!)).ParamName.ShouldBe("cause"); + Should.Throw(() => new Fault(OrderFault.NotFound, null, null!)).ParamName.ShouldBe("cause"); + Should.Throw(() => Fault.Of(OrderFault.NotFound, " ", new TimeoutException())).ParamName.ShouldBe("detail"); + } + [Flags] private enum Access { Read = 1, Write = 2 } diff --git a/test/CodoMetis.TypeKit.Tests/NullDelegateTests.cs b/test/CodoMetis.TypeKit.Tests/NullDelegateTests.cs index 6da0bf37..9a1ef06a 100644 --- a/test/CodoMetis.TypeKit.Tests/NullDelegateTests.cs +++ b/test/CodoMetis.TypeKit.Tests/NullDelegateTests.cs @@ -115,6 +115,24 @@ public sealed class NullDelegateTests [() => new[] { 1 }.TraverseAsync(Null>>>()), () => Array.Empty().TraverseAsync(Null>>>())], ["Result.TraverseAsync(IEnumerable, Func>>, CancellationToken): selector"] = [() => new[] { 1 }.TraverseAsync(Null>>>(), CancellationToken.None), () => Array.Empty().TraverseAsync(Null>>>(), CancellationToken.None)], + ["SequenceOptionExtensions.Choose(IAsyncEnumerable, Func>): selector"] = + [() => new[] { 1 }.ToAsyncEnumerable().Choose(Null>>()), () => Array.Empty().ToAsyncEnumerable().Choose(Null>>())], + ["Result.TraverseAsync(IAsyncEnumerable, Func>>): selector"] = + [() => new[] { 1 }.ToAsyncEnumerable().TraverseAsync(Null>>>()), () => Array.Empty().ToAsyncEnumerable().TraverseAsync(Null>>>())], + ["Result.TraverseAsync(IAsyncEnumerable, Func>>, CancellationToken): selector"] = + [() => new[] { 1 }.ToAsyncEnumerable().TraverseAsync(Null>>>(), CancellationToken.None), () => Array.Empty().ToAsyncEnumerable().TraverseAsync(Null>>>(), CancellationToken.None)], + ["Result.TraverseAsync(IAsyncEnumerable, Func>>): selector"] = + [() => new[] { 1 }.ToAsyncEnumerable().TraverseAsync(Null>>>()), () => Array.Empty().ToAsyncEnumerable().TraverseAsync(Null>>>())], + ["Result.TraverseAsync(IAsyncEnumerable, Func>>, CancellationToken): selector"] = + [() => new[] { 1 }.ToAsyncEnumerable().TraverseAsync(Null>>>(), CancellationToken.None), () => Array.Empty().ToAsyncEnumerable().TraverseAsync(Null>>>(), CancellationToken.None)], + ["Result.TraverseAsync(Option, Func>>): selector"] = + [() => Some.TraverseAsync(Null>>>()), () => None.TraverseAsync(Null>>>())], + ["Result.TraverseAsync(Option, Func>>, CancellationToken): selector"] = + [() => Some.TraverseAsync(Null>>>(), CancellationToken.None), () => None.TraverseAsync(Null>>>(), CancellationToken.None)], + ["Result.TraverseAsync(Patch, Func>>): selector"] = + [() => Patch.Value(1).TraverseAsync(Null>>>()), () => Patch.Null().TraverseAsync(Null>>>()), () => Patch.Absent().TraverseAsync(Null>>>())], + ["Result.TraverseAsync(Patch, Func>>, CancellationToken): selector"] = + [() => Patch.Value(1).TraverseAsync(Null>>>(), CancellationToken.None), () => Patch.Null().TraverseAsync(Null>>>(), CancellationToken.None), () => Patch.Absent().TraverseAsync(Null>>>(), CancellationToken.None)], ["Patch.IfPresent(Patch, Action>): action"] = [() => { Patch.Value(1).IfPresent(Null>()); return null; }, () => { Patch.Null().IfPresent(Null>()); return null; }, () => { Patch.Absent().IfPresent(Null>()); return null; }], // Catch has no branches: the delegate is checked before anything runs. Overloads that differ @@ -127,6 +145,10 @@ public sealed class NullDelegateTests ["Result.CatchAsync(Func): action"] = [() => Result.CatchAsync(Null>())], ["Result.CatchAsync(Func>): action"] = [() => Result.CatchAsync(Null>>())], ["Result.CatchAsync(Func): action"] = [() => Result.CatchAsync(Null>())], + ["Result.CatchAsync(Func>): action"] = [() => Result.CatchAsync(Null>>())], + ["Result.CatchAsync(Func): action"] = [() => Result.CatchAsync(Null>())], + ["Result.CatchAsync(Func>): action"] = [() => Result.CatchAsync(Null>>())], + ["Result.CatchAsync(Func): action"] = [() => Result.CatchAsync(Null>())], ["Result.Match(Func, Func): onSuccess"] = [() => Ok.Match(Null>(), _ => 0), () => Failed.Match(Null>(), _ => 0)], ["Result.Match(Func, Func): onError"] = [() => Ok.Match(() => 0, Null>()), () => Failed.Match(() => 0, Null>())], diff --git a/test/CodoMetis.TypeKit.Tests/NullTaskTests.cs b/test/CodoMetis.TypeKit.Tests/NullTaskTests.cs index b203c377..5407e0da 100644 --- a/test/CodoMetis.TypeKit.Tests/NullTaskTests.cs +++ b/test/CodoMetis.TypeKit.Tests/NullTaskTests.cs @@ -22,6 +22,14 @@ public sealed class NullTaskTests ["TraverseAsync(selector, token)"] = ("TraverseAsync", "selector", () => Elements.TraverseAsync((_, _) => (Task>)null!, CancellationToken.None)), ["command TraverseAsync(selector)"] = ("TraverseAsync", "selector", () => Elements.TraverseAsync(_ => (Task>)null!)), ["command TraverseAsync(selector, token)"] = ("TraverseAsync", "selector", () => Elements.TraverseAsync((_, _) => (Task>)null!, CancellationToken.None)), + ["stream TraverseAsync(selector)"] = ("TraverseAsync", "selector", () => Elements.ToAsyncEnumerable().TraverseAsync(_ => (Task>)null!)), + ["stream TraverseAsync(selector, token)"] = ("TraverseAsync", "selector", () => Elements.ToAsyncEnumerable().TraverseAsync((_, _) => (Task>)null!, CancellationToken.None)), + ["stream command TraverseAsync(selector)"] = ("TraverseAsync", "selector", () => Elements.ToAsyncEnumerable().TraverseAsync(_ => (Task>)null!)), + ["stream command TraverseAsync(selector, token)"] = ("TraverseAsync", "selector", () => Elements.ToAsyncEnumerable().TraverseAsync((_, _) => (Task>)null!, CancellationToken.None)), + ["Option.TraverseAsync(selector)"] = ("TraverseAsync", "selector", () => Some.TraverseAsync(_ => (Task>)null!)), + ["Option.TraverseAsync(selector, token)"] = ("TraverseAsync", "selector", () => Some.TraverseAsync((_, _) => (Task>)null!, CancellationToken.None)), + ["Patch.TraverseAsync(selector)"] = ("TraverseAsync", "selector", () => Patch.Value(1).TraverseAsync(_ => (Task>)null!)), + ["Patch.TraverseAsync(selector, token)"] = ("TraverseAsync", "selector", () => Patch.Value(1).TraverseAsync((_, _) => (Task>)null!, CancellationToken.None)), ["Option.MapAsync"] = ("MapAsync", "selector", () => Some.MapAsync(_ => (Task)null!)), ["Option.BindAsync"] = ("BindAsync", "selector", () => Some.BindAsync(_ => (Task>)null!)), ["Option.TapAsync"] = ("TapAsync", "action", () => Some.TapAsync(_ => null!)), diff --git a/test/CodoMetis.TypeKit.Tests/OptionPatchTraverseAsyncTests.cs b/test/CodoMetis.TypeKit.Tests/OptionPatchTraverseAsyncTests.cs new file mode 100644 index 00000000..e886e53c --- /dev/null +++ b/test/CodoMetis.TypeKit.Tests/OptionPatchTraverseAsyncTests.cs @@ -0,0 +1,106 @@ +namespace CodoMetis.TypeKit.Tests; + +/// +/// TraverseAsync on an Option and a Patch: the synchronous Traverse with the +/// operation awaited. None, a clear and an absent field never call the selector, and the token is +/// checked before the one operation, as the sequence's TraverseAsync checks it before each. +/// +public sealed class OptionPatchTraverseAsyncTests +{ + private static async Task> ParseLater(string text) + { + await Task.Yield(); + return int.TryParse(text, out var n) ? Result.Success(n) : Result.Error($"not a number: {text}"); + } + + private static Task> Unexpected(string _) => throw new InvalidOperationException("the selector was called"); + + private static Task> Unexpected(string _, CancellationToken __) => throw new InvalidOperationException("the selector was called"); + + [Fact] + public async Task An_option_with_a_value_is_awaited_into_some_or_the_error() + { + var parsed = await Option.Some("42").TraverseAsync(ParseLater); + parsed.TryGetValue(out var option, out _).ShouldBeTrue(); + option.TryGetValue(out var value).ShouldBeTrue(); + value.ShouldBe(42); + + var refused = await Option.Some("x").TraverseAsync(ParseLater); + refused.TryGetValue(out _, out var error).ShouldBeFalse(); + error.ShouldBe("not a number: x"); + } + + [Fact] + public async Task None_is_a_success_without_a_value_and_the_selector_is_not_called() + { + var none = await Option.None().TraverseAsync(Unexpected); + none.TryGetValue(out var option, out _).ShouldBeTrue(); + option.IsNone().ShouldBeTrue(); + + var noneWithToken = await Option.None().TraverseAsync(Unexpected, new CancellationToken(canceled: true)); + noneWithToken.TryGetValue(out var withToken, out _).ShouldBeTrue("None checks no token: there is no operation to start"); + withToken.IsNone().ShouldBeTrue(); + } + + [Fact] + public async Task A_patch_with_a_value_is_awaited_into_a_value_or_the_error() + { + var parsed = await Patch.Value("42").TraverseAsync(ParseLater); + parsed.TryGetValue(out var patch, out _).ShouldBeTrue(); + patch.State.ShouldBe(PatchState.Value); + patch.TryGetValue(out var value).ShouldBeTrue(); + value.ShouldBe(42); + + var refused = await Patch.Value("x").TraverseAsync(ParseLater); + refused.TryGetValue(out _, out var error).ShouldBeFalse(); + error.ShouldBe("not a number: x"); + } + + [Theory] + [InlineData(PatchState.Null)] + [InlineData(PatchState.Absent)] + public async Task A_clear_or_an_absent_field_passes_through_and_the_selector_is_not_called(PatchState state) + { + var patch = state == PatchState.Null ? Patch.Null() : Patch.Absent(); + + var passed = await patch.TraverseAsync(Unexpected); + passed.TryGetValue(out var produced, out _).ShouldBeTrue(); + produced.State.ShouldBe(state); + + var passedWithToken = await patch.TraverseAsync(Unexpected, new CancellationToken(canceled: true)); + passedWithToken.TryGetValue(out var withToken, out _).ShouldBeTrue("a field without a value checks no token"); + withToken.State.ShouldBe(state); + } + + [Fact] + public async Task The_token_is_handed_to_the_operation() + { + using var cancellation = new CancellationTokenSource(); + var seen = new List(); + + Task> Load(string text, CancellationToken token) + { + seen.Add(token); + return ParseLater(text); + } + + (await Option.Some("1").TraverseAsync(Load, cancellation.Token)).TryGetValue(out var option, out _).ShouldBeTrue(); + option.TryGetValue(out var fromOption).ShouldBeTrue(); + fromOption.ShouldBe(1); + + (await Patch.Value("2").TraverseAsync(Load, cancellation.Token)).TryGetValue(out var patch, out _).ShouldBeTrue(); + patch.TryGetValue(out var fromPatch).ShouldBeTrue(); + fromPatch.ShouldBe(2); + + seen.ShouldBe([cancellation.Token, cancellation.Token]); + } + + [Fact] + public async Task A_cancelled_token_starts_nothing_and_propagates() + { + var cancelled = new CancellationToken(canceled: true); + + await Should.ThrowAsync(() => Option.Some("1").TraverseAsync(Unexpected, cancelled)); + await Should.ThrowAsync(() => Patch.Value("1").TraverseAsync(Unexpected, cancelled)); + } +} diff --git a/test/CodoMetis.TypeKit.Tests/ResultCatchTests.cs b/test/CodoMetis.TypeKit.Tests/ResultCatchTests.cs index 04f41f98..1b98ef8f 100644 --- a/test/CodoMetis.TypeKit.Tests/ResultCatchTests.cs +++ b/test/CodoMetis.TypeKit.Tests/ResultCatchTests.cs @@ -90,6 +90,32 @@ static class Consumer if (error.Length > 0) compilation.Problems().ShouldContain("Catch"); } + /// + /// A delegate that returns a Task, an async lambda among them, is a compile error whose message + /// names CatchAsync: its exception would be thrown after Catch had returned a success. + /// + [Theory] + [InlineData("Result.Catch(() => Task.FromResult(1))")] + [InlineData("Result.Catch(() => Task.CompletedTask)")] + [InlineData("Result.Catch(async () => await Task.Yield())")] + [InlineData("Result.Catch(() => Task.CompletedTask)")] + public void Catch_over_a_task_is_a_compile_error_naming_CatchAsync(string call) + { + var compilation = ConsumerCompilation.Of($$""" + using System; + using System.Threading.Tasks; + using CodoMetis.TypeKit; + + static class Consumer + { + static object Call() => {{call}}; + } + """); + + compilation.ErrorIds().ShouldBe(["CS0619"], compilation.Problems()); + compilation.Problems().ShouldContain("Use CatchAsync."); + } + /// /// A task's type spelled as the value, or inferred where no overload can name it (a /// ValueTask), would make the task's exception arrive after a success was returned. It is @@ -425,9 +451,9 @@ public void The_combinators_are_unchanged_so_a_throwing_selector_propagates() /// nothing it throws, and given to the command form Catch<TException> would be /// discarded, an async lambda as async void. The overloads over a task-returning /// delegate are more specific, so the compiler picks them, and they are errors that name - /// CatchAsync. A ValueTask has no such overload (it would make every async lambda - /// ambiguous): the command form's refusal of any value covers it, and the valued form refuses it at - /// run time. + /// CatchAsync. A ValueTask has no such overload, since it would break a build that works + /// today (decision 110): the command form's refusal of any value covers it, and the valued form + /// refuses it at run time. /// [Theory] [InlineData("Result.Catch(() => FetchAsync())")] diff --git a/test/CodoMetis.TypeKit.Tests/ResultContinuationTests.cs b/test/CodoMetis.TypeKit.Tests/ResultContinuationTests.cs index dc2668b4..bcd6a785 100644 --- a/test/CodoMetis.TypeKit.Tests/ResultContinuationTests.cs +++ b/test/CodoMetis.TypeKit.Tests/ResultContinuationTests.cs @@ -1,4 +1,7 @@ using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; namespace CodoMetis.TypeKit.Tests; @@ -228,6 +231,40 @@ public async Task A_task_returning_selector_is_awaited_rather_than_wrapped() order.Match(o => o.Id, _ => -1).ShouldBe(4); } + /// + /// What the Result page warns of: a callback that returns a ValueTask is no Func<T, Task>, so + /// TapAsync and TapErrorAsync on a pending result bind to the overload that takes an action and + /// waits for nothing, and MapAsync to the one that takes a value, which keeps the ValueTask as the + /// value. .AsTask() or an async lambda binds to the overload that waits. + /// + [Theory] + [InlineData("pending.TapAsync(x => new ValueTask())", "System.Action", "int")] + [InlineData("pending.TapErrorAsync(e => new ValueTask())", "System.Action", "int")] + [InlineData("pending.MapAsync(x => new ValueTask(x))", "System.Func>", "System.Threading.Tasks.ValueTask")] + [InlineData("pending.TapAsync(x => new ValueTask().AsTask())", "System.Func", "int")] + [InlineData("pending.TapErrorAsync(async e => await new ValueTask())", "System.Func", "int")] + [InlineData("pending.MapAsync(async x => await new ValueTask(x))", "System.Func>", "int")] + public void A_callback_that_returns_a_ValueTask_binds_to_the_overload_that_does_not_wait(string call, string parameter, string value) + { + var compilation = ConsumerCompilation.Of($$""" + using System.Threading.Tasks; + using CodoMetis.TypeKit; + + static class Consumer + { + static object Call(Task> pending) => {{call}}; + } + """); + compilation.ErrorIds().ShouldBeEmpty(compilation.Problems()); + + var tree = compilation.SyntaxTrees.Single(); + var outermost = tree.GetRoot(TestContext.Current.CancellationToken).DescendantNodes().OfType().First(); + var called = compilation.GetSemanticModel(tree).GetSymbolInfo(outermost, TestContext.Current.CancellationToken).Symbol.ShouldBeAssignableTo()!; + + called.Parameters.ShouldHaveSingleItem().Type.ToDisplayString().ShouldBe(parameter); + called.ReturnType.ToDisplayString().ShouldBe($"System.Threading.Tasks.Task>"); + } + /// /// Everything that can be done to a result can be done to one still being produced: every /// combinator of both shapes that returns a result has a continuation on diff --git a/test/CodoMetis.TypeKit.Tests/ResultWithValueTests.cs b/test/CodoMetis.TypeKit.Tests/ResultWithValueTests.cs index c9a210b3..2d8575c2 100644 --- a/test/CodoMetis.TypeKit.Tests/ResultWithValueTests.cs +++ b/test/CodoMetis.TypeKit.Tests/ResultWithValueTests.cs @@ -280,6 +280,52 @@ public void Select_enables_query_syntax() value.ShouldBe(42); } + /// + /// A result has no where: a value filtered out would need an error the query cannot name. + /// Ensure(predicate, error) is the filter, and the query pattern finds no Where. + /// + [Fact] + public void Query_syntax_has_no_where() + { + var compilation = ConsumerCompilation.Of(""" + using CodoMetis.TypeKit; + + static class Consumer + { + static object Call(Result result) => from x in result where x > 1 select x; + } + """); + + compilation.ErrorIds().ShouldBe(["CS1936"], compilation.Problems()); + compilation.Problems().ShouldContain("'Where'"); + } + + /// + /// A bare value converts to a success, and a bare error does not: in a Result<long, int> a + /// bare 5 could be either, so only Result.Error(...) makes an error. + /// + [Theory] + [InlineData("Result result = \"boom\";", "CS0029")] + [InlineData("Result result = Result.Error(\"boom\");", "")] + [InlineData("Result result = 5;", "")] + public void A_bare_error_does_not_convert(string statement, string error) + { + var compilation = ConsumerCompilation.Of($$""" + using CodoMetis.TypeKit; + + static class Consumer + { + static object Call() + { + {{statement}} + return result; + } + } + """); + + compilation.ErrorIds().ShouldBe(error.Length == 0 ? [] : [error], compilation.Problems()); + } + /// /// A second from needs the two-selector SelectMany: each step can use every earlier /// value, and the first error ends the query. diff --git a/test/CodoMetis.TypeKit.Tests/UninitializedResultTests.cs b/test/CodoMetis.TypeKit.Tests/UninitializedResultTests.cs index 8c7e7320..0f334910 100644 --- a/test/CodoMetis.TypeKit.Tests/UninitializedResultTests.cs +++ b/test/CodoMetis.TypeKit.Tests/UninitializedResultTests.cs @@ -84,6 +84,8 @@ public sealed class UninitializedResultTests ["Transpose(Option>)"] = () => Task.FromResult(Option.Some(default(Result)).Transpose()), ["Transpose(Result, TError>)"] = () => Task.FromResult(default(Result, string>).Transpose()), ["Traverse(Option, Func>)"] = () => Task.FromResult(Option.Some(1).Traverse(_ => default(Result))), + ["TraverseAsync(Option, Func>>)"] = () => Option.Some(1).TraverseAsync(_ => Task.FromResult(default(Result))), + ["TraverseAsync(Option, Func>>, CancellationToken)"] = () => Option.Some(1).TraverseAsync((_, _) => Task.FromResult(default(Result)), CancellationToken.None), ["SelectMany(Result, Func>, Func)"] = () => Task.FromResult(default(Result).SelectMany(_ => Value, (x, _) => x)), @@ -127,12 +129,20 @@ public sealed class UninitializedResultTests ["Traverse(IEnumerable, Func>)"] = () => Task.FromResult(new[] { 1 }.Traverse(_ => default(Result))), ["Traverse(IEnumerable, Func>)"] = () => Task.FromResult(new[] { 1 }.Traverse(_ => default(Result))), ["Traverse(Patch, Func>)"] = () => Task.FromResult(Patch.Value(1).Traverse(_ => default(Result))), + ["TraverseAsync(Patch, Func>>)"] = () => Patch.Value(1).TraverseAsync(_ => Task.FromResult(default(Result))), + ["TraverseAsync(Patch, Func>>, CancellationToken)"] = () => Patch.Value(1).TraverseAsync((_, _) => Task.FromResult(default(Result)), CancellationToken.None), ["TraverseAsync(IEnumerable, Func>>)"] = () => new[] { 1 }.TraverseAsync(_ => Task.FromResult(default(Result))), ["TraverseAsync(IEnumerable, Func>>, CancellationToken)"] = () => new[] { 1 }.TraverseAsync((_, _) => Task.FromResult(default(Result)), CancellationToken.None), ["TraverseAsync(IEnumerable, Func>>)"] = () => new[] { 1 }.TraverseAsync(_ => Task.FromResult(default(Result))), ["TraverseAsync(IEnumerable, Func>>, CancellationToken)"] = () => new[] { 1 }.TraverseAsync((_, _) => Task.FromResult(default(Result)), CancellationToken.None), ["Sequence(IEnumerable>)"] = () => Task.FromResult(new[] { Value, default }.Sequence()), ["Sequence(IEnumerable>)"] = () => Task.FromResult(new[] { Result.Success(), default }.Sequence()), + ["TraverseAsync(IAsyncEnumerable, Func>>)"] = () => new[] { 1 }.ToAsyncEnumerable().TraverseAsync(_ => Task.FromResult(default(Result))), + ["TraverseAsync(IAsyncEnumerable, Func>>, CancellationToken)"] = () => new[] { 1 }.ToAsyncEnumerable().TraverseAsync((_, _) => Task.FromResult(default(Result)), CancellationToken.None), + ["TraverseAsync(IAsyncEnumerable, Func>>)"] = () => new[] { 1 }.ToAsyncEnumerable().TraverseAsync(_ => Task.FromResult(default(Result))), + ["TraverseAsync(IAsyncEnumerable, Func>>, CancellationToken)"] = () => new[] { 1 }.ToAsyncEnumerable().TraverseAsync((_, _) => Task.FromResult(default(Result)), CancellationToken.None), + ["SequenceAsync(IAsyncEnumerable>, CancellationToken)"] = () => new[] { Value, default }.ToAsyncEnumerable().SequenceAsync(), + ["SequenceAsync(IAsyncEnumerable>, CancellationToken)"] = () => new[] { Result.Success(), default }.ToAsyncEnumerable().SequenceAsync(), }; public static TheoryData ExtensionMemberNames => [.. ExtensionMembers.Keys]; diff --git a/test/Prose.slnf b/test/Prose.slnf new file mode 100644 index 00000000..d62947f8 --- /dev/null +++ b/test/Prose.slnf @@ -0,0 +1,12 @@ +{ + "solution": { + "path": "../CodoMetis.TypeKit.slnx", + "projects": [ + "test/CodoMetis.TypeKit.Conventions.Tests/CodoMetis.TypeKit.Conventions.Tests.csproj", + "test/CodoMetis.TypeKit.DocsQuery.Tests/CodoMetis.TypeKit.DocsQuery.Tests.csproj", + "test/CodoMetis.TypeKit.Generators.Probes/CodoMetis.TypeKit.Generators.Probes.csproj", + "test/CodoMetis.TypeKit.Oracles.Tests/CodoMetis.TypeKit.Oracles.Tests.csproj", + "test/CodoMetis.TypeKit.Samples/CodoMetis.TypeKit.Samples.csproj" + ] + } +} diff --git a/test/oracles/Candidates.cs b/test/oracles/Candidates.cs new file mode 100644 index 00000000..52aa41ef --- /dev/null +++ b/test/oracles/Candidates.cs @@ -0,0 +1,112 @@ +using System.Text.Json; + +namespace CodoMetis.TypeKit.Oracles; + +/// +/// The known disagreements of an oracle, read from test/oracles/candidates.json, and the verdict a sweep +/// reaches once its failures are matched against them (design/adr/0112). +/// +/// +/// +/// An oracle sweep collects one line per disagreement and hands them here instead of asserting the list empty. +/// A line that matches no candidate fails. A line that matches a candidate whose issue is still empty fails too, +/// labelled [known candidate: family], so nothing is quarantined before it is filed. A line that matches a +/// candidate with an issue is written as SKIPPED [known candidate: family, issue] and does not fail. A +/// candidate that no line matched fails with candidate fixed: remove the entry, so the registry cannot +/// outlive the defect it recorded. +/// +/// +/// A candidate matches a failure line when the line contains the candidate's row text (ordinal): the stable +/// prefix of the sweep's own line, the operands and the operation, never the actual value, which changes with the fix. +/// This file is linked into every test project that holds an oracle rather than referenced. +/// +/// +public static class Candidates +{ + /// + /// One known disagreement: the oracle class, the row's stable text, its family and the issue that tracks it. + /// scopes the row to one test method of the class. + /// + public sealed record Candidate(string Oracle, string Row, string Family, string Issue, string? Method = null); + + private static readonly Lazy> All_ = new(Load); + + /// Every registered candidate. + public static IReadOnlyList All => All_.Value; + + /// Candidates registered for . + public static IReadOnlyList For(string oracle) => + [.. All.Where(candidate => string.Equals(candidate.Oracle, oracle, StringComparison.Ordinal))]; + + /// + /// Passes when every failure line is a filed candidate and every candidate of the oracle still fails; + /// otherwise fails with followed by the offending lines. + /// + /// The oracle class, as registered in test/oracles/oracles.json. + /// The sweep's disagreements, one line each. + /// The first line of the failure message, naming the sweep. + /// How many lines of the failure message to show. + /// The calling test method; a candidate scoped to another method is not this sweep's. + public static void Verdict(string oracle, IReadOnlyList failures, string header, int shown = 25, string? method = null) => + Judge([.. For(oracle).Where(candidate => candidate.Method is null || method is null || string.Equals(candidate.Method, method, StringComparison.Ordinal))], + failures, header, shown); + + /// over the candidates given directly, as SeedVerdictTests drives it. + internal static void Judge(IReadOnlyList known, IReadOnlyList failures, string header, int shown = 25) + { + var failing = new List(); + var unfiled = new List(); + var matched = new HashSet(); + + foreach (var line in failures) + { + var candidate = known.FirstOrDefault(c => line.Contains(c.Row, StringComparison.Ordinal)); + if (candidate is null) + { + failing.Add(OneLine(line)); + continue; + } + + matched.Add(candidate); + if (candidate.Issue.Length == 0) + unfiled.Add($"{SeedVerdict.KnownMarker} {candidate.Family}] {OneLine(line.TrimStart())}"); + else + Console.WriteLine($"SKIPPED [known candidate: {candidate.Family}, {candidate.Issue}] {OneLine(line.TrimStart())}"); + } + + foreach (var fixedCandidate in known.Where(candidate => !matched.Contains(candidate))) + failing.Add($"candidate fixed: remove the entry for '{fixedCandidate.Row}' ({fixedCandidate.Family}) from test/oracles/candidates.json"); + + // Known rows last, so a message cut short hides them first (SeedVerdict). + failing.AddRange(unfiled); + if (failing.Count == 0) return; + + Assert.Fail( + $"{OneLine(header)} ({failing.Count} failures):" + Environment.NewLine + + string.Join(Environment.NewLine, failing.Take(shown)) + + (failing.Count > shown ? $"{Environment.NewLine}{SeedVerdict.MorePrefix}{failing.Count - shown} more" : "")); + } + + /// A row as one line, line breaks shown as ⏎: the message is one row per line, and SeedVerdict reads it so. + private static string OneLine(string text) => + text.Contains('\n') || text.Contains('\r') ? text.Replace("\r\n", "⏎").Replace('\n', '⏎').Replace('\r', '⏎') : text; + + private static IReadOnlyList Load() + { + var path = Path.Combine(RepositoryRoot(), "test", "oracles", "candidates.json"); + using var stream = File.OpenRead(path); + return JsonSerializer.Deserialize>(stream, new JsonSerializerOptions { PropertyNameCaseInsensitive = true, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true }) + ?? throw new InvalidOperationException($"'{path}' holds no array."); + } + + private static string RepositoryRoot() + { + const string marker = "CodoMetis.TypeKit.slnx"; + for (var directory = new DirectoryInfo(AppContext.BaseDirectory); directory is not null; directory = directory.Parent) + { + if (File.Exists(Path.Combine(directory.FullName, marker))) return directory.FullName; + } + + throw new InvalidOperationException($"Could not find '{marker}' above '{AppContext.BaseDirectory}'."); + } +} diff --git a/test/oracles/README.md b/test/oracles/README.md new file mode 100644 index 00000000..deceee86 --- /dev/null +++ b/test/oracles/README.md @@ -0,0 +1,54 @@ +# Oracles + +How bug-finding works here as a system rather than one sweep per bug (design/adr/0112): a catalogue of what the +documents promise, an enumeration of what the API offers, oracle tests that check the library against an +independent answer, seeds that prove each oracle can fail, and a registry of the disagreements found but not yet +fixed. It is CodoMetis.ValueRanges' kit, kept to what pays for itself: no covering arrays, no ledger, no replay by +footprint, no agent. The oracles are ordinary tests in the projects that already have their harness, and run with +the suite. + +| File | What it holds | Held by | +|---|---|---| +| `claims.json` | One entry per sentence a consumer-facing document promises: its `source` heading, the `quote` verbatim, the `oracle` kind, the `surfaces` (public members, `TypeName.Member`, `*` a wildcard) and `heldBy`, the test classes that assert it. An empty `heldBy` is the worklist. | `OracleCatalogueTests` | +| `surface-waivers.json` | The public members no claim names, each with why it needs no oracle. A reason that starts `unread:` names a document not yet read for claims, and the issue that reads it. | `OracleSurfaceCoverageTests` | +| `oracles.json` | The test classes that are oracles: checks against an independent answer over many cases, not worked examples. | `OracleSeedTests` | +| `seeds/*.patch` | A defect in `src/` with a `Seed:` line, the `Oracles:` it fails and what `Fails:`. Every oracle has one. | `OracleSeedTests`, and `oracles seeds` | +| `candidates.json` | Disagreements an oracle found that are not fixed yet, each tied to an issue (`Candidates.Verdict`). | `OracleCandidateTests` | +| `axes.md` | What a consumer varies, which tests vary it, and the pairs none varies yet. | read by hand | + +`test/CodoMetis.TypeKit.Oracles.Tests` holds all of it in CI without building anything but itself: a quote its +document no longer holds, a surface or a test class that does not exist, a public member neither claimed nor +waived, an oracle without a seed, and a seed that no longer applies all fail there. + +Whether a seed is still **caught** takes a build, so it is a local tool, run before a release and after changing an +oracle or the code a seed patches: + +```bash +dotnet tools/Oracles/bin/Release/net10.0/oracles.dll seeds [name filter] # each seed: a copy, the patch, one project built, one class run +dotnet tools/Oracles/bin/Release/net10.0/oracles.dll status # the worklist: unheld claims, unread waivers, unseeded oracles, candidates +``` + +`seeds` copies the working tree (without `bin/`, `obj/` and `.git/`, so uncommitted work is included), builds each +oracle the selected seeds name and runs that one class: the baseline, with no seed. Then, for each seed, it copies +the tree again, applies the patch, and builds and runs the same classes with the same filter. A seed is caught when +its oracle passes without it and fails with it, in a test of its own: a failed test with a row that is not a known +candidate (`SeedVerdict`, design/adr/0117). It prints one `BASELINE` line per oracle, then one line per seed and +oracle: + +| Verdict | Meaning | Exit code | +|---|---|---| +| `CAUGHT` | The baseline passes, and with the seed a test fails on its own assertion or on what the code it calls throws. | 0 when every pair is caught | +| `MISSED` | The baseline passes, and with the seed every test passes, or fails only on known candidates. | 1 | +| `ERROR` | A run never reached the tests' own code, or the baseline fails. | 4 | + +The TRX decides what reached the tests. A fixture that fails to construct or initialize fails every test with +xunit's own message (`Collection fixture type 'SqlServer' threw in InitializeAsync`). A fixture that throws in +`Dispose` shows up only as an `error` in the TRX's counters. A crashed host writes no TRX and exits with something +other than 0 or 2. A run whose every test was skipped ran nothing. Each of these is `ERROR`, with or without the +seed: a dead fixture fails every test either way, so its failures prove nothing about the seed. On an arm64 Mac, +run it with `TYPEKIT_MSSQL_IMAGE=mcr.microsoft.com/azure-sql-edge:latest`, as the suite runs (CONTRIBUTING's Docker +bullet), or the SQL Server oracles are `ERROR`. + +To add an oracle: write the test class, register it in `oracles.json`, write a seed by planting the defect it +guards against, `git diff` it into `seeds/.patch` under its header lines, undo the edit, and run +`oracles seeds `. diff --git a/test/oracles/SeedVerdict.cs b/test/oracles/SeedVerdict.cs new file mode 100644 index 00000000..536bce3d --- /dev/null +++ b/test/oracles/SeedVerdict.cs @@ -0,0 +1,131 @@ +using System.Text.RegularExpressions; +using System.Xml.Linq; + +namespace CodoMetis.TypeKit.Oracles; + +/// +/// Whether an oracle caught a seed: the rule oracles seeds applies, compiled into tools/Oracles and +/// linked into CodoMetis.TypeKit.Oracles.Tests (SeedVerdictTests), so the two cannot disagree +/// (design/adr/0112, design/adr/0117). +/// +/// +/// +/// A seed is caught when its oracle passes without it and fails with it, in a test of its own. Each oracle class is +/// run twice with the same filter in the same environment: once on a copy of the tree with no seed (the baseline), +/// once with the seed planted, and each run is from the exit code of dotnet test and its +/// TRX. A run that did not reach the tests' own code is an , and an error on either side makes +/// the pair ERROR, never CAUGHT: a dead fixture fails every test, with or without a seed. +/// +/// +/// What the TRX holds, measured with xunit.v3 4.0.2 on Microsoft Testing Platform (2026-10-29): a class, collection +/// or assembly fixture that fails to construct or to initialize fails every test that uses it, each with xunit's own +/// message, Collection fixture type 'T' threw in InitializeAsync, the fixture's exception below it +/// (). A fixture that throws in Dispose fails no test and is counted only as an +/// error in the TRX's counters. A test host that crashes writes no TRX and exits with a code that is not +/// Microsoft Testing Platform's 0 (passed) or 2 (a test failed). A failure in the test's own constructor or +/// InitializeAsync is written like one in its body, the exception's message and the stack trace below the +/// test's code, so it counts as the test's: the oracles build what they assert on there. +/// +/// +/// A sweep reports through Candidates.Verdict: a header line, then one line per failing row, where a +/// row that matches an unfiled candidate starts with . A failed test catches the +/// seed when at least one of its rows is not a known candidate (). Counting a test as known +/// whenever its message held the marker anywhere read a seed that broke a row beside a known one as missed, as +/// CodoMetis.ValueRanges found (its #55). A baseline whose only failures are known candidates passes. +/// +/// +/// Candidates.Verdict lists the other rows before the known ones, so a message cut short +/// (… and N more) hides known rows first: when every row shown is known, the hidden ones are too. +/// A message with no known row at all is any other assertion, and catches. +/// +/// +public static class SeedVerdict +{ + /// The prefix Candidates.Verdict gives a row that matches an unfiled candidate. + public const string KnownMarker = "[known candidate:"; + + /// The line Candidates.Verdict ends a cut-short message with. + public const string MorePrefix = "… and "; + + /// The exit codes of a run that reached its tests: Microsoft Testing Platform's success and "a test failed". + private static readonly int[] RanExitCodes = [0, 2]; + + /// The TRX counters of a result outside any test, such as a fixture's Dispose, or of a run cut short. + private static readonly string[] ErrorCounters = ["error", "aborted", "timeout", "disconnected"]; + + private static readonly XNamespace Trx = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010"; + + /// + /// xunit's own messages for a test it could not set up, every one in xunit.v3.core 4.0.2 (its fixture kind is + /// Assembly, Collection or Class). + /// + private static readonly Regex SetupMessage = new( + @"\A(\w+ fixture type '[^']*' (threw in |had one or more unresolved constructor arguments|may only define a single public constructor)" + + @"|The following constructor parameters did not have matching fixture data" + + @"|Cannot create type '[^']*' due to missing constructor arguments)", + RegexOptions.CultureInvariant); + + /// One run of one oracle class, read from the exit code of dotnet test and its TRX. + /// The tests that ran, passed or failed; a skipped test is neither. + /// The tests that failed. + /// The failed tests with a row that is not a known candidate. + /// The first test in , or empty. + /// Why the run never reached the tests' own code, or when it did. + public sealed record Run(int Executed, int Failed, int Caught, string Example, string? Error); + + /// Whether a failed test's message holds at least one row that is not a known candidate. + public static bool Catches(string message) + { + var lines = message.Split('\n').Select(line => line.Trim()).Where(line => line.Length > 0).ToList(); + if (!lines.Any(IsKnown)) return true; + + // A Verdict message: the first line is its header, the last may say how many rows were cut. + return lines.Skip(1).Any(line => !IsKnown(line) && !line.StartsWith(MorePrefix, StringComparison.Ordinal)); + } + + /// Whether a failed test's message is xunit's own for a fixture or a constructor it could not set up. + public static bool IsSetupFailure(string message) => SetupMessage.IsMatch(message); + + /// Reads one run of : the exit code of dotnet test and its TRX, if it wrote one. + public static Run Read(int exitCode, XDocument? trx, string oracle) + { + if (trx is null) return Failure($"no test results written: the test host exited {exitCode}"); + if (!RanExitCodes.Contains(exitCode)) return Failure($"the test host exited {exitCode}, not 0 or 2"); + + var counters = trx.Descendants(Trx + "Counters").FirstOrDefault(); + var outside = ErrorCounters.Select(name => (Name: name, Count: (int?)counters?.Attribute(name) ?? 0)).Where(counter => counter.Count > 0).ToList(); + if (outside.Count > 0) return Failure($"the run counts {string.Join(", ", outside.Select(counter => $"{counter.Count} {counter.Name}"))} outside any test (a fixture's Dispose, a host cut short)"); + + var results = trx.Descendants(Trx + "UnitTestResult") + .Select(result => (Outcome: (string?)result.Attribute("outcome"), Test: (string?)result.Attribute("testName") ?? "?", Message: result.Descendants(Trx + "Message").FirstOrDefault()?.Value ?? "")) + .Where(result => result.Outcome is "Passed" or "Failed") + .ToList(); + if (results.Count == 0) return Failure($"no test of {oracle} ran"); + + var failed = results.Where(result => result.Outcome == "Failed").ToList(); + var setup = failed.Where(result => IsSetupFailure(result.Message)).ToList(); + if (setup.Count > 0) return Failure($"{setup.Count} of {results.Count} tests failed in setup, not in the test: {setup[0].Message.Split('\n')[0].Trim()}"); + + var caught = failed.Where(result => Catches(result.Message)).ToList(); + return new Run(results.Count, failed.Count, caught.Count, caught.Count > 0 ? caught[0].Test : "", Error: null); + + static Run Failure(string error) => new(0, 0, 0, "", error); + } + + /// + /// The verdict on one seed and one oracle: CAUGHT, MISSED or ERROR, padded to eight + /// characters, then what was seen. + /// + public static string Judge(Run baseline, Run seeded) + { + if (baseline.Error is not null) return $"ERROR without the seed, {baseline.Error}"; + if (baseline.Caught > 0) return $"ERROR {baseline.Caught} of {baseline.Executed} tests fail without the seed, e.g. {baseline.Example}"; + if (seeded.Error is not null) return $"ERROR with the seed, {seeded.Error}"; + if (seeded.Caught > 0) return $"CAUGHT {seeded.Caught} of {seeded.Executed} tests fail, e.g. {seeded.Example}"; + return seeded.Failed == 0 + ? $"MISSED all {seeded.Executed} tests pass with the defect planted" + : $"MISSED {seeded.Failed} of {seeded.Executed} tests fail, every row a known candidate"; + } + + private static bool IsKnown(string line) => line.StartsWith(KnownMarker, StringComparison.Ordinal); +} diff --git a/test/oracles/axes.md b/test/oracles/axes.md new file mode 100644 index 00000000..05e1cbff --- /dev/null +++ b/test/oracles/axes.md @@ -0,0 +1,45 @@ +# What a consumer varies + +The claims say what is promised, and the surface says what is offered. Neither says which settings a consumer +combines: the JSON options of the host, the OpenAPI version, MVC or a minimal API, the EF provider, the shape of +their own type. This is the hand-written list (design/adr/0112): one section per surface family, each axis with +its values, the tests that already vary it, and the pairs no test varies yet, which are the next probe class. + +A **probe class** (`*ProbeTests`) is an ordinary test over the full product of a few axes, in the project that +already has the harness. Its oracle is the document's promise, it ends in `Candidates.Verdict`, and it is +registered in `oracles.json` with a seed like every other oracle. It stays under a minute: nothing replays it by +footprint, it runs with the suite. + +## The oracles this repository promises + +- **JSON parity (differential).** A shape with a value object and the same shape with the wrapped type serialize + to the same bytes, as a value, a property and a dictionary key. `JsonParityTests`. +- **Schema parity (differential).** The OpenAPI schema of a value-object property equals the wrapped type's, with + value objects as components and inlined (`InlineValueObjects`). `SchemaTests`, `NullablePositionTests`. +- **Column parity (differential).** The EF model gives a value-object property the wrapped type's column, and a + key the wrapped type's generation. `MappingTests`, `KeyGenerationVariantTests`, `ColumnFacetVariantTests`. +- **One set of rules (differential).** `TryFrom`, `Parse`, the JSON converter, the type converter and the + binders refuse what `Create` refuses. `EntryPointValidationTests`. +- **No refusal quotes the input (exception).** A sentinel in refused input never reaches a message. + `UnreadableInputTests`. +- **Off the wire (exception).** `Option` and `Result` refuse System.Text.Json in both directions. `NotWireTypeTests`. +- **Every member, by reflection (completeness).** A null delegate on every branch, an uninitialized result, the + context an asynchronous callback runs on. `NullDelegateTests`, `UninitializedResultTests`, `AsynchronousContextTests`. + +## The axes + +| Family | Axis | Values | Varied by | Pairs no test varies yet | +|---|---|---|---|---| +| JSON | the host's options | defaults, Web, a naming policy, a dictionary-key policy, number handling, a host converter | `JsonParityTests` | reference preservation beside a dictionary key | +| JSON | the wrapped type | every built-in the generators wrap, nullable or not | `JsonParityTests`, `JsonTests` | | +| OpenAPI | the version | 3.1, 3.0, 2.0 | `SchemaTests`, `FaultResponseTests` | a withheld fault name under 2.0 | +| OpenAPI | where value objects are described | components, inlined by `InlineValueObjects`, everything inlined by the host | `SchemaTests`, `NullablePositionTests`, `DescribedParameterTests` | `InlineValueObjects` beside a described type's parameter (`DescribedParameterTests` inlines by the host's own delegate) | +| HTTP | the host | minimal API, MVC with `[ApiController]`, MVC without | `MvcBindingTests`, `FaultStatusScopeTests` | | +| EF | the provider | SQLite, Npgsql, SQL Server | `KeyGenerationVariantTests`, `QueryTranslationVariantTests` | | +| EF | where a key sits | `KeyShapes` (`test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/Variants/`) | `KeyGenerationVariantTests` | | +| EF | the query | `QueryShapes` | `QueryTranslationVariantTests` | | +| Analyzers | where a type argument is written, and what it is | `TypeArgumentSites`, `TypeArgumentKinds` (`test/CodoMetis.TypeKit.Analyzers.Tests/Variants/`) | `TypeArgumentConstraintVariantTests` | | +| Generation | the declaration's shape | record struct, record class, struct, class; sealed or not; over each wrapped kind | `BuildOutcomeTests`, `GeneratedSurfaceTests` | | + +An empty last column means nobody has looked yet, not that nothing is missing. Fill it when a feature lands or a +finding names a combination no test varied. diff --git a/test/oracles/candidates.json b/test/oracles/candidates.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/test/oracles/candidates.json @@ -0,0 +1 @@ +[] diff --git a/test/oracles/claims.json b/test/oracles/claims.json new file mode 100644 index 00000000..47a89c5b --- /dev/null +++ b/test/oracles/claims.json @@ -0,0 +1,5058 @@ +[ + { + "id": "A01-choose-reads-a-stream", + "source": "src/CodoMetis.TypeKit/README.md#find-one-in-a-collection", + "quote": "Both read an async stream too, as its elements arrive.", + "claim": "Both read an async stream too, as its elements arrive.", + "oracle": "claim", + "surfaces": [ + "Option.Choose", + "SequenceOptionExtensions.Choose" + ], + "heldBy": [ + "AsyncStreamTests" + ] + }, + { + "id": "A01-dispose-however-they-stop", + "source": "src/CodoMetis.TypeKit/README.md#chain-steps", + "quote": "All three read the stream in order, and dispose its enumerator however they stop: at an error, at a `break`, or when your selector throws.", + "claim": "All three read the stream in order, and dispose its enumerator however they stop: at an error, at a `break`, or when your selector throws.", + "oracle": "claim", + "surfaces": [ + "Option.Choose", + "SequenceOptionExtensions.Choose", + "Result.SequenceAsync", + "Result.TraverseAsync" + ], + "heldBy": [ + "AsyncStreamTests" + ] + }, + { + "id": "A01-stop-at-the-first-error", + "source": "src/CodoMetis.TypeKit/README.md#chain-steps", + "quote": "`SequenceAsync` and `TraverseAsync` stop at the first error and read nothing after it.", + "claim": "`SequenceAsync` and `TraverseAsync` stop at the first error and read nothing after it.", + "oracle": "claim", + "surfaces": [ + "Result.SequenceAsync", + "Result.TraverseAsync" + ], + "heldBy": [ + "AsyncStreamTests" + ] + }, + { + "id": "A01-token-reaches-the-stream", + "source": "src/CodoMetis.TypeKit/README.md#chain-steps", + "quote": "The token you give `SequenceAsync` or `TraverseAsync` reaches the stream.", + "claim": "The token you give `SequenceAsync` or `TraverseAsync` reaches the stream.", + "oracle": "claim", + "surfaces": [ + "Result.SequenceAsync", + "Result.TraverseAsync" + ], + "heldBy": [ + "AsyncStreamTests" + ] + }, + { + "id": "A01-with-cancellation-for-choose", + "source": "src/CodoMetis.TypeKit/README.md#chain-steps", + "quote": "For `Choose`, pass yours to `WithCancellation` on what it returns.", + "claim": "For `Choose`, pass yours to `WithCancellation` on what it returns.", + "oracle": "claim", + "surfaces": [ + "Option.Choose", + "SequenceOptionExtensions.Choose" + ], + "heldBy": [ + "AsyncStreamTests" + ] + }, + { + "id": "C01-covers-every-decimal", + "source": "docs/json.md#decimals-in-one-spelling", + "quote": "Register either one once, and it covers every decimal: nullable ones, dictionary keys, and every value object over a decimal, since its JSON is the underlying type's.", + "claim": "Register either one once, and it covers every decimal: nullable ones, dictionary keys, and every value object over a decimal, since its JSON is the underlying type's.", + "oracle": "claim", + "surfaces": [ + "DecimalNumberJsonConverter.*", + "DecimalStringJsonConverter.*" + ], + "heldBy": [ + "DecimalJsonConvertersTests" + ] + }, + { + "id": "C01-number-is-a-number", + "source": "docs/json.md#decimals-in-one-spelling", + "quote": "**`DecimalNumberJsonConverter`** writes it as a number.", + "claim": "**`DecimalNumberJsonConverter`** writes it as a number.", + "oracle": "claim", + "surfaces": [ + "DecimalNumberJsonConverter.*", + "DecimalStringJsonConverter.*" + ], + "heldBy": [ + "DecimalJsonConvertersTests" + ] + }, + { + "id": "C01-reads-a-number-or-a-string", + "source": "docs/json.md#decimals-in-one-spelling", + "quote": "Both read a number, or a string holding one, and keep the scale they read.", + "claim": "Both read a number, or a string holding one, and keep the scale they read.", + "oracle": "claim", + "surfaces": [ + "DecimalNumberJsonConverter.*", + "DecimalStringJsonConverter.*" + ], + "heldBy": [ + "DecimalJsonConvertersTests" + ] + }, + { + "id": "C01-smallest-scale", + "source": "docs/json.md#decimals-in-one-spelling", + "quote": "Two converters write the smallest scale instead:", + "claim": "Two converters write the smallest scale instead:", + "oracle": "claim", + "surfaces": [ + "DecimalNumberJsonConverter.*", + "DecimalStringJsonConverter.*" + ], + "heldBy": [ + "DecimalJsonConvertersTests" + ] + }, + { + "id": "C01-string-is-a-string", + "source": "docs/json.md#decimals-in-one-spelling", + "quote": "**`DecimalStringJsonConverter`** writes it as a string.", + "claim": "**`DecimalStringJsonConverter`** writes it as a string.", + "oracle": "claim", + "surfaces": [ + "DecimalNumberJsonConverter.*", + "DecimalStringJsonConverter.*" + ], + "heldBy": [ + "DecimalJsonConvertersTests" + ] + }, + { + "id": "C02-hidden-is-an-error-in-both", + "source": "docs/text.md#required-and-optional", + "quote": "`Hidden` is an error in both cases.", + "claim": "`Hidden` is an error in both cases.", + "oracle": "claim", + "surfaces": [ + "StringTextExtensions.*", + "TextFault.*" + ], + "heldBy": [ + "StringTextExtensionsTests" + ] + }, + { + "id": "C02-null-is-blank", + "source": "docs/text.md#required-and-optional", + "quote": "A null string is `Blank`.", + "claim": "A null string is `Blank`.", + "oracle": "claim", + "surfaces": [ + "StringTextExtensions.*", + "TextFault.*" + ], + "heldBy": [ + "StringTextExtensionsTests" + ] + }, + { + "id": "C02-optional-blank-is-no-value", + "source": "docs/text.md#required-and-optional", + "quote": "For an optional one, a blank means \"no value\", and `AsOptionalText()` and `AsOptionalIdentifier()` return a `Result, TextFault>`.", + "claim": "For an optional one, a blank means \"no value\", and `AsOptionalText()` and `AsOptionalIdentifier()` return a `Result, TextFault>`.", + "oracle": "claim", + "surfaces": [ + "StringTextExtensions.*", + "TextFault.*" + ], + "heldBy": [ + "StringTextExtensionsTests" + ] + }, + { + "id": "C02-required-blank-is-an-error", + "source": "docs/text.md#required-and-optional", + "quote": "For a required field, `Blank` is an error.", + "claim": "For a required field, `Blank` is an error.", + "oracle": "claim", + "surfaces": [ + "StringTextExtensions.*", + "TextFault.*" + ], + "heldBy": [ + "StringTextExtensionsTests" + ] + }, + { + "id": "C03-detail-and-rule-intact", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#faults-of-several-types", + "quote": "`Fault` (from CodoMetis.TypeKit) holds one error of either set, with its detail and rule intact.", + "claim": "`Fault` (from CodoMetis.TypeKit) holds one error of either set, with its detail and rule intact.", + "oracle": "claim", + "surfaces": [ + "TypeKitHttpResultExtensions.ToHttpResult", + "Fault.*" + ], + "heldBy": [ + "ComposedFaultResponseTests" + ] + }, + { + "id": "C03-each-error-own-set", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#faults-of-several-types", + "quote": "Each error is answered as its own set maps it.", + "claim": "Each error is answered as its own set maps it.", + "oracle": "claim", + "surfaces": [ + "TypeKitHttpResultExtensions.ToHttpResult", + "Fault.*" + ], + "heldBy": [ + "ComposedFaultResponseTests" + ] + }, + { + "id": "C03-statuses-ascending", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#faults-of-several-types", + "quote": "So its operation lists the statuses of both sets, in ascending order, with the problem types of both where they share a status.", + "claim": "So its operation lists the statuses of both sets, in ascending order, with the problem types of both where they share a status.", + "oracle": "claim", + "surfaces": [ + "Fault.*", + "TypeKitHttpResultExtensions.ToHttpResult" + ], + "heldBy": [ + "ComposedFaultResponseTests" + ] + }, + { + "id": "C03-unmapped-set-stops-start", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#faults-of-several-types", + "quote": "The host doesn't start while either set is unmapped.", + "claim": "The host doesn't start while either set is unmapped.", + "oracle": "claim", + "surfaces": [ + "TypeKitHttpResultExtensions.ToHttpResult", + "Fault.*" + ], + "heldBy": [ + "ComposedFaultResponseTests" + ] + }, + { + "id": "C04-doc-omits-left-out", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#leaving-out-what-a-route-cannot-answer", + "quote": "The operation then lists no status and no problem type for a member left out.", + "claim": "The operation then lists no status and no problem type for a member left out.", + "oracle": "exception", + "surfaces": [ + "WithoutFaultsAttribute.*", + "TypeKitWithoutFaultsEndpointConventionBuilderExtensions.*" + ], + "heldBy": [ + "FaultExclusionTests" + ] + }, + { + "id": "C04-left-out-everywhere-stops-start", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#leaving-out-what-a-route-cannot-answer", + "quote": "**These stop the host from starting**, as for a status: a value or a name no member has, a status that every endpoint it's set on leaves out (on the endpoint itself, or on a group whose endpoints all leave the member out), and a group or a controller none of whose endpoints answers with the type.", + "claim": "**These stop the host from starting**, as for a status: a value or a name no member has, a status that every endpoint it's set on leaves out (on the endpoint itself, or on a group whose endpoints all leave the member out), and a group or a controller none of whose endpoints answers with the type.", + "oracle": "exception", + "surfaces": [ + "WithoutFaultsAttribute.*", + "TypeKitWithoutFaultsEndpointConventionBuilderExtensions.*" + ], + "heldBy": [ + "FaultExclusionTests" + ] + }, + { + "id": "C04-narrows-wider-status", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#leaving-out-what-a-route-cannot-answer", + "quote": "If a group gives `NotFound` a status and one endpoint inside leaves `NotFound` out, that endpoint drops it, and the group's other endpoints keep the status.", + "claim": "If a group gives `NotFound` a status and one endpoint inside leaves `NotFound` out, that endpoint drops it, and the group's other endpoints keep the status.", + "oracle": "exception", + "surfaces": [ + "WithoutFaultsAttribute.*", + "TypeKitWithoutFaultsEndpointConventionBuilderExtensions.*" + ], + "heldBy": [ + "FaultExclusionTests" + ] + }, + { + "id": "C04-reporting-left-out-throws", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#leaving-out-what-a-route-cannot-answer", + "quote": "Reporting a member left out anyway throws an `InvalidOperationException` naming the endpoint and the member, rather than answering with a status the document doesn't list.", + "claim": "Reporting a member left out anyway throws an `InvalidOperationException` naming the endpoint and the member, rather than answering with a status the document doesn't list.", + "oracle": "exception", + "surfaces": [ + "WithoutFaultsAttribute.*", + "TypeKitWithoutFaultsEndpointConventionBuilderExtensions.*" + ], + "heldBy": [ + "FaultExclusionTests" + ] + }, + { + "id": "C05-nested-chain", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#the-rule-that-refused", + "quote": "A rule of a nested type is named after its nesting chain, whatever `QualifiedFaultNames` says.", + "claim": "A rule of a nested type is named after its nesting chain, whatever `QualifiedFaultNames` says.", + "oracle": "claim", + "surfaces": [ + "FaultRuleExtensions.*", + "FaultClosedSetRuleExtensions.*" + ], + "heldBy": [ + "FaultResponsePartsTests" + ] + }, + { + "id": "C05-no-namespace", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#the-rule-that-refused", + "quote": "**The name carries no namespace**, which would publish your application's structure to every client.", + "claim": "**The name carries no namespace**, which would publish your application's structure to every client.", + "oracle": "claim", + "surfaces": [ + "FaultRuleExtensions.*", + "FaultClosedSetRuleExtensions.*" + ], + "heldBy": [ + "FaultResponsePartsTests" + ] + }, + { + "id": "C05-rule-qualified", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#the-rule-that-refused", + "quote": "The problem carries it as `rule`, qualified with its type's name, so two types' `Blank` can be told apart:", + "claim": "The problem carries it as `rule`, qualified with its type's name, so two types' `Blank` can be told apart:", + "oracle": "claim", + "surfaces": [ + "FaultRuleExtensions.*", + "FaultClosedSetRuleExtensions.*" + ], + "heldBy": [ + "FaultResponsePartsTests" + ] + }, + { + "id": "C05-stays-the-errors", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#the-rule-that-refused", + "quote": "The status, the mapping and the document stay the error's.", + "claim": "The status, the mapping and the document stay the error's.", + "oracle": "claim", + "surfaces": [ + "FaultRuleExtensions.*", + "FaultClosedSetRuleExtensions.*" + ], + "heldBy": [ + "FaultResponsePartsTests" + ] + }, + { + "id": "C05-unnamed-value-throws", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#the-rule-that-refused", + "quote": "A value no member names, such as a combination of flags or a cast number, throws where it's given.", + "claim": "A value no member names, such as a combination of flags or a cast number, throws where it's given.", + "oracle": "claim", + "surfaces": [ + "FaultRuleExtensions.*", + "FaultClosedSetRuleExtensions.*" + ], + "heldBy": [ + "FaultResponsePartsTests" + ] + }, + { + "id": "C06-description-as-detail", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#a-closed-set-as-the-fault-type", + "quote": "The description becomes the problem's `detail`, so a client can show it.", + "claim": "The description becomes the problem's `detail`, so a client can show it.", + "oracle": "claim", + "surfaces": [ + "FaultKind.*", + "IFault.*", + "FaultResponse.For", + "TypeKitClosedSetHttpResultExtensions.ToHttpResult" + ], + "heldBy": [ + "FaultSetDefaultTests" + ] + }, + { + "id": "C06-kind-gives-status", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#a-closed-set-as-the-fault-type", + "quote": "The kind gives the status:", + "claim": "The kind gives the status:", + "oracle": "claim", + "surfaces": [ + "FaultKind.*", + "IFault.*", + "FaultResponse.For", + "TypeKitClosedSetHttpResultExtensions.ToHttpResult" + ], + "heldBy": [ + "FaultResponsePartsTests", + "FaultSetDefaultTests" + ] + }, + { + "id": "C06-member-must-have-kind", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#a-closed-set-as-the-fault-type", + "quote": "**A member must have a kind.** A member whose kind is `default`, which is no kind, fails this call.", + "claim": "**A member must have a kind.** A member whose kind is `default`, which is no kind, fails this call.", + "oracle": "claim", + "surfaces": [ + "FaultKind.*", + "IFault.*", + "FaultResponse.For", + "TypeKitClosedSetHttpResultExtensions.ToHttpResult" + ], + "heldBy": [ + "FaultResponsePartsTests", + "FaultSetDefaultTests" + ] + }, + { + "id": "C06-no-description-no-detail", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#a-closed-set-as-the-fault-type", + "quote": "A member without one has no `detail`.", + "claim": "A member without one has no `detail`.", + "oracle": "claim", + "surfaces": [ + "FaultKind.*", + "IFault.*", + "FaultResponse.For", + "TypeKitClosedSetHttpResultExtensions.ToHttpResult" + ], + "heldBy": [ + "FaultResponsePartsTests" + ] + }, + { + "id": "C07-by-kind", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#every-fault-set-at-once", + "quote": "every fault set an endpoint answers with, by its kind", + "claim": "every fault set an endpoint answers with, by its kind", + "oracle": "claim", + "surfaces": [ + "TypeKitFaultResponsesServiceCollectionExtensions.*", + "TypeKitClosedSetFaultResponsesServiceCollectionExtensions.*" + ], + "heldBy": [ + "FaultSetDefaultTests" + ] + }, + { + "id": "C07-enum-needs-own-mapping", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#every-fault-set-at-once", + "quote": "Neither has a kind, and the host doesn't start while an endpoint answers with one that's unmapped.", + "claim": "Neither has a kind, and the host doesn't start while an endpoint answers with one that's unmapped.", + "oracle": "claim", + "surfaces": [ + "TypeKitFaultResponsesServiceCollectionExtensions.*", + "TypeKitClosedSetFaultResponsesServiceCollectionExtensions.*" + ], + "heldBy": [ + "FaultSetDefaultTests" + ] + }, + { + "id": "C07-member-without-kind-stops-start", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#every-fault-set-at-once", + "quote": "A member with no kind stops the start, and so does one the mapping throws for or gives a status that isn't an error.", + "claim": "A member with no kind stops the start, and so does one the mapping throws for or gives a status that isn't an error.", + "oracle": "claim", + "surfaces": [ + "TypeKitClosedSetFaultResponsesServiceCollectionExtensions.*", + "TypeKitFaultResponsesServiceCollectionExtensions.*" + ], + "heldBy": [ + "FaultSetDefaultTests" + ] + }, + { + "id": "C07-own-mapping", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#every-fault-set-at-once", + "quote": "The mapping can be your own. It reads each member's kind and description through `IFault`:", + "claim": "The mapping can be your own. It reads each member's kind and description through `IFault`:", + "oracle": "claim", + "surfaces": [ + "TypeKitFaultResponsesServiceCollectionExtensions.*", + "TypeKitClosedSetFaultResponsesServiceCollectionExtensions.*" + ], + "heldBy": [ + "FaultSetDefaultTests" + ] + }, + { + "id": "C07-own-set-keeps-mapping", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#every-fault-set-at-once", + "quote": "A set mapped on its own keeps its mapping.", + "claim": "A set mapped on its own keeps its mapping.", + "oracle": "claim", + "surfaces": [ + "TypeKitFaultResponsesServiceCollectionExtensions.*", + "TypeKitClosedSetFaultResponsesServiceCollectionExtensions.*" + ], + "heldBy": [ + "FaultSetDefaultTests" + ] + }, + { + "id": "C08-bare-names-warn", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#faults-of-several-types", + "quote": "With bare names, the default, that's `AccountFault.NotFound` beside an `EditFault.NotFound`.", + "claim": "With bare names, the default, that's `AccountFault.NotFound` beside an `EditFault.NotFound`.", + "oracle": "claim", + "surfaces": [ + "FaultResponseOptions.*" + ], + "heldBy": [ + "ComposedFaultResponseTests" + ] + }, + { + "id": "C08-qualified-chain-warns", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#faults-of-several-types", + "quote": "With qualified names, it's two sets with one nesting chain in two namespaces, since the name carries no namespace.", + "claim": "With qualified names, it's two sets with one nesting chain in two namespaces, since the name carries no namespace.", + "oracle": "claim", + "surfaces": [ + "FaultResponseOptions.*" + ], + "heldBy": [ + "ComposedFaultResponseTests" + ] + }, + { + "id": "C08-qualified-names-fix", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#faults-of-several-types", + "quote": "Fix it with `QualifiedFaultNames`, a rename, or by leaving one out of the route", + "claim": "Fix it with `QualifiedFaultNames`, a rename, or by leaving one out of the route", + "oracle": "claim", + "surfaces": [ + "FaultResponseOptions.*" + ], + "heldBy": [ + "ComposedFaultResponseTests" + ] + }, + { + "id": "C08-warning-names-both", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#faults-of-several-types", + "quote": "A client reads the two errors as one, so the host logs a warning at startup naming both.", + "claim": "A client reads the two errors as one, so the host logs a warning at startup naming both.", + "oracle": "claim", + "surfaces": [ + "FaultResponseOptions.*" + ], + "heldBy": [ + "ComposedFaultResponseTests" + ] + }, + { + "id": "C09-equal-by-the-wrapped-value", + "source": "CHANGELOG.md", + "quote": "Over a base record, a value object's equality is its wrapped value's", + "claim": "Over a base record, a value object's equality is its wrapped value's", + "oracle": "metamorphic", + "surfaces": [ + "IValueObject.*" + ], + "heldBy": [] + }, + { + "id": "C09-in-a-collection-of-the-base", + "source": "CHANGELOG.md", + "quote": "in a collection of the base too, whatever equality the base declares.", + "claim": "in a collection of the base too, whatever equality the base declares.", + "oracle": "metamorphic", + "surfaces": [ + "IValueObject.*" + ], + "heldBy": [] + }, + { + "id": "C10-ignore-cycles-unaffected", + "source": "src/CodoMetis.TypeKit.Generators/README.md#json", + "quote": "a struct of such values, and `ReferenceHandler.IgnoreCycles`.", + "claim": "a struct of such values, and `ReferenceHandler.IgnoreCycles`.", + "oracle": "claim", + "surfaces": [ + "GeneratedJson.*" + ], + "heldBy": [ + "ReferenceHandlingTests" + ] + }, + { + "id": "C10-own-converters-unaffected", + "source": "src/CodoMetis.TypeKit.Generators/README.md#json", + "quote": "anything a converter of your own writes, on the options or on one property", + "claim": "anything a converter of your own writes, on the options or on one property", + "oracle": "claim", + "surfaces": [ + "GeneratedJson.*" + ], + "heldBy": [ + "ReferenceHandlingTests" + ] + }, + { + "id": "C10-refused-where-references-could-be-written", + "source": "src/CodoMetis.TypeKit.Generators/README.md#json", + "quote": "**Preserved references** (`ReferenceHandler.Preserve`) throw a `NotSupportedException` where the serializer could write `$id` or `$ref` inside the underlying value.", + "claim": "**Preserved references** (`ReferenceHandler.Preserve`) throw a `NotSupportedException` where the serializer could write `$id` or `$ref` inside the underlying value.", + "oracle": "claim", + "surfaces": [ + "GeneratedJson.*" + ], + "heldBy": [ + "ReferenceHandlingTests" + ] + }, + { + "id": "C10-value-converters-unaffected", + "source": "src/CodoMetis.TypeKit.Generators/README.md#json", + "quote": "These are unaffected: a value the serializer writes with a value converter (a number, a string, a `Guid`, a `Uri`)", + "claim": "These are unaffected: a value the serializer writes with a value converter (a number, a string, a `Guid`, a `Uri`)", + "oracle": "claim", + "surfaces": [ + "GeneratedJson.*" + ], + "heldBy": [ + "ReferenceHandlingTests" + ] + }, + { + "id": "C11-abstract-string-constructor", + "source": "CHANGELOG.md", + "quote": "or an abstract class's string constructor, no longer breaks the build; the value object has no parsing.", + "claim": "or an abstract class's string constructor, no longer breaks the build; the value object has no parsing.", + "oracle": "claim", + "surfaces": [ + "GeneratedParsing.*" + ], + "heldBy": [] + }, + { + "id": "C11-implicit-result", + "source": "CHANGELOG.md", + "quote": "A wrapped type's `Parse` whose result can't be assigned to it", + "claim": "A wrapped type's `Parse` whose result can't be assigned to it", + "oracle": "claim", + "surfaces": [ + "GeneratedParsing.*" + ], + "heldBy": [] + }, + { + "id": "C11-parsing-where-it-can-be-parsed", + "source": "CHANGELOG.md", + "quote": "`IParsable`/`ISpanParsable`/`IUtf8SpanParsable`, formatting in the invariant culture", + "claim": "`IParsable`/`ISpanParsable`/`IUtf8SpanParsable`, formatting in the invariant culture", + "oracle": "claim", + "surfaces": [ + "GeneratedParsing.*" + ], + "heldBy": [] + }, + { + "id": "C11-ref-text", + "source": "CHANGELOG.md", + "quote": "or that takes its text by `ref`", + "claim": "or that takes its text by `ref`", + "oracle": "claim", + "surfaces": [ + "GeneratedParsing.*" + ], + "heldBy": [] + }, + { + "id": "C12-every-position", + "source": "docs/json.md#describing-what-a-converter-writes", + "quote": "With CodoMetis.TypeKit.AspNetCore, `AddTypeKit()` writes the same descriptions into the OpenAPI document, in every position, a list's elements and a dictionary's values included.", + "claim": "With CodoMetis.TypeKit.AspNetCore, `AddTypeKit()` writes the same descriptions into the OpenAPI document, in every position, a list's elements and a dictionary's values included.", + "oracle": "claim", + "surfaces": [ + "IDescribedJsonConverter.*", + "JsonSchemaDescriptions.*", + "JsonSchemaDescription.*" + ], + "heldBy": [ + "DescribedSchemaTests" + ] + }, + { + "id": "C12-options-describe", + "source": "docs/json.md#describing-what-a-converter-writes", + "quote": "**A converter someone else wrote**, or the serializer's own enum converter, is described in the options by `JsonSchemaDescriptions`.", + "claim": "**A converter someone else wrote**, or the serializer's own enum converter, is described in the options by `JsonSchemaDescriptions`.", + "oracle": "claim", + "surfaces": [ + "IDescribedJsonConverter.*", + "JsonSchemaDescriptions.*", + "JsonSchemaDescription.*" + ], + "heldBy": [ + "DescribedSchemaTests" + ] + }, + { + "id": "C12-own-converter-describes-itself", + "source": "docs/json.md#describing-what-a-converter-writes", + "quote": "**Your own converter** implements `IDescribedJsonConverter` and describes itself, as both decimal converters do.", + "claim": "**Your own converter** implements `IDescribedJsonConverter` and describes itself, as both decimal converters do.", + "oracle": "claim", + "surfaces": [ + "IDescribedJsonConverter.*", + "JsonSchemaDescriptions.*", + "JsonSchemaDescription.*" + ], + "heldBy": [ + "DescribedSchemaTests" + ] + }, + { + "id": "C12-parameter-keeps-aspnet", + "source": "docs/json.md#describing-what-a-converter-writes", + "quote": "So it keeps the description ASP.NET gives the type without your converters", + "claim": "So it keeps the description ASP.NET gives the type without your converters", + "oracle": "claim", + "surfaces": [ + "IDescribedJsonConverter.*", + "JsonSchemaDescriptions.*", + "JsonSchemaDescription.*" + ], + "heldBy": [ + "DescribedSchemaTests" + ] + }, + { + "id": "C13-absent-is-default", + "source": "docs/merge-patches.md#required-constructor-parameters", + "quote": "a property the request leaves out keeps its default, which is `Absent`.", + "claim": "a property the request leaves out keeps its default, which is `Absent`.", + "oracle": "claim", + "surfaces": [ + "PatchJsonConverterFactory.*", + "PatchState.*" + ], + "heldBy": [ + "PatchSchemaTests" + ] + }, + { + "id": "C13-doc-optional-where-absent", + "source": "docs/merge-patches.md#required-constructor-parameters", + "quote": "`AddTypeKit()` leaves a patch out of `required` where the serializer lets it be absent, and keeps it there where the options reject a body without it.", + "claim": "`AddTypeKit()` leaves a patch out of `required` where the serializer lets it be absent, and keeps it there where the options reject a body without it.", + "oracle": "claim", + "surfaces": [ + "PatchJsonConverterFactory.*", + "PatchState.*" + ], + "heldBy": [ + "PatchSchemaTests" + ] + }, + { + "id": "C13-doc-required-where-rejected", + "source": "docs/merge-patches.md#required-constructor-parameters", + "quote": "So the document never calls a field optional when the endpoint rejects a request without it.", + "claim": "So the document never calls a field optional when the endpoint rejects a request without it.", + "oracle": "claim", + "surfaces": [ + "PatchJsonConverterFactory.*", + "PatchState.*" + ], + "heldBy": [ + "PatchSchemaTests" + ] + }, + { + "id": "C14-application-json-declared", + "source": "CHANGELOG.md", + "quote": "OpenAPI: a problem response an endpoint declares as `application/json`, such as `Produces(404)` or `[ProducesResponseType(404)]`, describes `fault` and `rule` and names its problem types, and keeps its media type", + "claim": "OpenAPI: a problem response an endpoint declares as `application/json`, such as `Produces(404)` or `[ProducesResponseType(404)]`, describes `fault` and `rule` and names its problem types, and keeps its media type", + "oracle": "claim", + "surfaces": [ + "FaultProblem.*", + "TypeKitOpenApiOptionsExtensions.AddTypeKit" + ], + "heldBy": [ + "FaultResponseTests" + ] + }, + { + "id": "C14-ascending-with-declared", + "source": "CHANGELOG.md", + "quote": "OpenAPI: an operation with fault responses lists every status ascending, the ones its endpoint declares itself among them", + "claim": "OpenAPI: an operation with fault responses lists every status ascending, the ones its endpoint declares itself among them", + "oracle": "claim", + "surfaces": [ + "FaultProblem.*", + "TypeKitOpenApiOptionsExtensions.AddTypeKit" + ], + "heldBy": [ + "FaultResponseTests" + ] + }, + { + "id": "C14-declared-describes-fault", + "source": "CHANGELOG.md", + "quote": "OpenAPI: a problem response an endpoint declares itself (`ProducesProblem`, `[ProducesResponseType]`) describes `fault`, and names its problem types.", + "claim": "OpenAPI: a problem response an endpoint declares itself (`ProducesProblem`, `[ProducesResponseType]`) describes `fault`, and names its problem types.", + "oracle": "claim", + "surfaces": [ + "FaultProblem.*", + "TypeKitOpenApiOptionsExtensions.AddTypeKit" + ], + "heldBy": [ + "FaultResponseTests" + ] + }, + { + "id": "C15-builder-reaches-doc", + "source": "CHANGELOG.md", + "quote": "OpenAPI: a `WithFaultStatus` on the builder `MapControllers()` returns reaches the document, as it reached the response.", + "claim": "OpenAPI: a `WithFaultStatus` on the builder `MapControllers()` returns reaches the document, as it reached the response.", + "oracle": "claim", + "surfaces": [ + "FaultStatusAttribute.*", + "TypeKitFaultStatusEndpointConventionBuilderExtensions.*" + ], + "heldBy": [] + }, + { + "id": "C15-status-per-scope", + "source": "CHANGELOG.md", + "quote": "`WithFaultStatus`/`[FaultStatus]` per endpoint, group or controller", + "claim": "`WithFaultStatus`/`[FaultStatus]` per endpoint, group or controller", + "oracle": "claim", + "surfaces": [ + "FaultStatusAttribute.*", + "TypeKitFaultStatusEndpointConventionBuilderExtensions.*" + ], + "heldBy": [] + }, + { + "id": "C15-without-faults-leaves-doc", + "source": "CHANGELOG.md", + "quote": "**`WithoutFaults`** and `[WithoutFaults]`: a route leaves out of its document the members it never answers with, and reporting one throws", + "claim": "**`WithoutFaults`** and `[WithoutFaults]`: a route leaves out of its document the members it never answers with, and reporting one throws", + "oracle": "claim", + "surfaces": [ + "FaultStatusAttribute.*", + "TypeKitFaultStatusEndpointConventionBuilderExtensions.*" + ], + "heldBy": [] + }, + { + "id": "C16-differing-refused", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says", + "quote": "Otherwise building the document fails rather than describe JSON the controllers don't write.", + "claim": "Otherwise building the document fails rather than describe JSON the controllers don't write.", + "oracle": "claim", + "surfaces": [ + "TypeKitOpenApiOptionsExtensions.AddTypeKit" + ], + "heldBy": [] + }, + { + "id": "C16-error-names-type-and-descriptions", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says", + "quote": "The error names the type and what each set of options says of it, also for a type held only inside a patch.", + "claim": "The error names the type and what each set of options says of it, also for a type held only inside a patch.", + "oracle": "claim", + "surfaces": [ + "TypeKitOpenApiOptionsExtensions.AddTypeKit" + ], + "heldBy": [] + }, + { + "id": "C16-same-converters-build", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says", + "quote": "A host with controllers registers the same converters and descriptions on both, in the same order.", + "claim": "A host with controllers registers the same converters and descriptions on both, in the same order.", + "oracle": "claim", + "surfaces": [ + "TypeKitOpenApiOptionsExtensions.AddTypeKit" + ], + "heldBy": [] + }, + { + "id": "C17-a-cast-naming-no-member", + "source": "src/CodoMetis.TypeKit.Analyzers/README.md#cmtk0014", + "quote": "A `FaultKind` or a `ConstructorAccessibility` that names no member: a cast", + "claim": "A `FaultKind` or a `ConstructorAccessibility` that names no member: a cast", + "oracle": "exception", + "surfaces": [ + "FaultKind.*", + "ConstructorAccessibility.*", + "RequireParameterlessConstructorAttribute.*" + ], + "heldBy": [ + "UndefinedEnumValueAnalyzerTests" + ] + }, + { + "id": "C17-a-member-is-silent", + "source": "src/CodoMetis.TypeKit.Analyzers/README.md#cmtk0014", + "quote": "public static readonly ImportFault Duplicate = new(FaultKind.Conflict);", + "claim": "public static readonly ImportFault Duplicate = new(FaultKind.Conflict);", + "oracle": "exception", + "surfaces": [ + "FaultKind.*", + "ConstructorAccessibility.*", + "RequireParameterlessConstructorAttribute.*" + ], + "heldBy": [ + "UndefinedEnumValueAnalyzerTests" + ] + }, + { + "id": "C17-constructor-accessibility-too", + "source": "src/CodoMetis.TypeKit.Analyzers/README.md#cmtk0014", + "quote": "[RequireParameterlessConstructor((ConstructorAccessibility)7)] // CMTK0014: no member, read as public", + "claim": "[RequireParameterlessConstructor((ConstructorAccessibility)7)] // CMTK0014: no member, read as public", + "oracle": "exception", + "surfaces": [ + "FaultKind.*", + "ConstructorAccessibility.*", + "RequireParameterlessConstructorAttribute.*" + ], + "heldBy": [ + "UndefinedEnumValueAnalyzerTests" + ] + }, + { + "id": "C17-default-and-zero", + "source": "src/CodoMetis.TypeKit.Analyzers/README.md#cmtk0014", + "quote": "`default(FaultKind)`, a zero", + "claim": "`default(FaultKind)`, a zero", + "oracle": "exception", + "surfaces": [ + "FaultKind.*", + "ConstructorAccessibility.*", + "RequireParameterlessConstructorAttribute.*" + ], + "heldBy": [ + "UndefinedEnumValueAnalyzerTests" + ] + }, + { + "id": "C17-reported-where-written", + "source": "src/CodoMetis.TypeKit.Analyzers/README.md#cmtk0014", + "quote": "Both are reported where the value is written:", + "claim": "Both are reported where the value is written:", + "oracle": "exception", + "surfaces": [ + "FaultKind.*", + "ConstructorAccessibility.*", + "RequireParameterlessConstructorAttribute.*" + ], + "heldBy": [ + "UndefinedEnumValueAnalyzerTests" + ] + }, + { + "id": "C18-a-warning-and-generated", + "source": "src/CodoMetis.TypeKit.Generators/README.md#build-errors", + "quote": "A warning, and the value object is generated.", + "claim": "A warning, and the value object is generated.", + "oracle": "exception", + "surfaces": [ + "IValueObject.*" + ], + "heldBy": [] + }, + { + "id": "C18-generated-equality-never-calls-the-base", + "source": "src/CodoMetis.TypeKit.Generators/README.md#build-errors", + "quote": "Over a base record, the generated `Equals(TSelf)` and `GetHashCode()` compare the underlying value alone, and never call the base's.", + "claim": "Over a base record, the generated `Equals(TSelf)` and `GetHashCode()` compare the underlying value alone, and never call the base's.", + "oracle": "exception", + "surfaces": [ + "IValueObject.*" + ], + "heldBy": [] + }, + { + "id": "C18-names-each-base-member", + "source": "src/CodoMetis.TypeKit.Generators/README.md#build-errors", + "quote": "So a hand-written `Equals(TBase)`, `GetHashCode()` or `IEquatable.Equals` on a base record isn't used, and a warning names it (CMTK1023).", + "claim": "So a hand-written `Equals(TBase)`, `GetHashCode()` or `IEquatable.Equals` on a base record isn't used, and a warning names it (CMTK1023).", + "oracle": "exception", + "surfaces": [ + "IValueObject.*" + ], + "heldBy": [] + }, + { + "id": "C19-a-pair-once-at-the-later-argument", + "source": "docs/type-constraints.md#the-kind-of-a-type-argument", + "quote": "A breach is reported once, at the later argument.", + "claim": "A breach is reported once, at the later argument.", + "oracle": "exception", + "surfaces": [ + "InterfaceAttribute.*", + "UnsealedAttribute.*", + "ValueObjectAttribute.*", + "FlagsEnumAttribute.*", + "ReadOnlyStructAttribute.*", + "SealedAttribute.*", + "RecordAttribute.*", + "UnrelatedAttribute.*" + ], + "heldBy": [ + "TypeArgumentConstraintKindTests" + ] + }, + { + "id": "C19-flags-enum-and-read-only-struct", + "source": "docs/type-constraints.md#the-kind-of-a-type-argument", + "quote": "**`[FlagsEnum]` and `[ReadOnlyStruct]`** narrow what `where T : Enum` and `where T : struct` admit.", + "claim": "**`[FlagsEnum]` and `[ReadOnlyStruct]`** narrow what `where T : Enum` and `where T : struct` admit.", + "oracle": "exception", + "surfaces": [ + "InterfaceAttribute.*", + "UnsealedAttribute.*", + "ValueObjectAttribute.*", + "FlagsEnumAttribute.*", + "ReadOnlyStructAttribute.*", + "SealedAttribute.*", + "RecordAttribute.*", + "UnrelatedAttribute.*" + ], + "heldBy": [ + "TypeArgumentConstraintKindTests" + ] + }, + { + "id": "C19-interface-and-unsealed", + "source": "docs/type-constraints.md#the-kind-of-a-type-argument", + "quote": "**`[Interface]` and `[Unsealed]`** are what a proxy needs: a contract to implement, or a class to derive from.", + "claim": "**`[Interface]` and `[Unsealed]`** are what a proxy needs: a contract to implement, or a class to derive from.", + "oracle": "exception", + "surfaces": [ + "InterfaceAttribute.*", + "UnsealedAttribute.*", + "ValueObjectAttribute.*", + "FlagsEnumAttribute.*", + "ReadOnlyStructAttribute.*", + "SealedAttribute.*", + "RecordAttribute.*", + "UnrelatedAttribute.*" + ], + "heldBy": [ + "TypeArgumentConstraintKindTests" + ] + }, + { + "id": "C19-methods-the-compiler-calls", + "source": "docs/type-constraints.md#the-kind-of-a-type-argument", + "quote": "**Methods the compiler calls for you:** a `foreach`'s `GetEnumerator`, an `await`'s `GetAwaiter`, a `Deconstruct` (in `var (a, b) = x`, a `foreach`, or a pattern like `x is (1, 2)`), and a collection expression's `Add` or builder method.", + "claim": "**Methods the compiler calls for you:** a `foreach`'s `GetEnumerator`, an `await`'s `GetAwaiter`, a `Deconstruct` (in `var (a, b) = x`, a `foreach`, or a pattern like `x is (1, 2)`), and a collection expression's `Add` or builder method.", + "oracle": "exception", + "surfaces": [ + "InterfaceAttribute.*", + "UnsealedAttribute.*", + "ValueObjectAttribute.*", + "FlagsEnumAttribute.*", + "ReadOnlyStructAttribute.*", + "SealedAttribute.*", + "RecordAttribute.*", + "UnrelatedAttribute.*" + ], + "heldBy": [ + "TypeArgumentConstraintKindTests" + ] + }, + { + "id": "C19-value-object-and-closed-set", + "source": "docs/type-constraints.md#the-kind-of-a-type-argument", + "quote": "**`[ValueObject]` and `[ClosedSet]`** accept their marker interface with any type argument.", + "claim": "**`[ValueObject]` and `[ClosedSet]`** accept their marker interface with any type argument.", + "oracle": "exception", + "surfaces": [ + "InterfaceAttribute.*", + "UnsealedAttribute.*", + "ValueObjectAttribute.*", + "FlagsEnumAttribute.*", + "ReadOnlyStructAttribute.*", + "SealedAttribute.*", + "RecordAttribute.*", + "UnrelatedAttribute.*" + ], + "heldBy": [ + "TypeArgumentConstraintKindTests" + ] + }, + { + "id": "C20-a-failure-says-the-outcome", + "source": "src/CodoMetis.TypeKit.Testing/README.md", + "quote": "**A failure says what the outcome was**", + "claim": "**A failure says what the outcome was**", + "oracle": "claim", + "surfaces": [ + "FaultAssertions.*" + ], + "heldBy": [ + "ComposedFaultAssertionTests" + ] + }, + { + "id": "C20-fails-unless-it-holds-that-reason", + "source": "src/CodoMetis.TypeKit.Testing/README.md", + "quote": "`ShouldHold(reason)` fails unless the fault holds that reason, whichever of its types the reason belongs to.", + "claim": "`ShouldHold(reason)` fails unless the fault holds that reason, whichever of its types the reason belongs to.", + "oracle": "claim", + "surfaces": [ + "FaultAssertions.*" + ], + "heldBy": [ + "ComposedFaultAssertionTests" + ] + }, + { + "id": "C20-hands-back-detail-and-rule", + "source": "src/CodoMetis.TypeKit.Testing/README.md", + "quote": "It hands back the `Fault` with its detail and rule intact.", + "claim": "It hands back the `Fault` with its detail and rule intact.", + "oracle": "claim", + "surfaces": [ + "FaultAssertions.*" + ], + "heldBy": [ + "ComposedFaultAssertionTests" + ] + }, + { + "id": "C20-uninitialized-fails", + "source": "src/CodoMetis.TypeKit.Testing/README.md", + "quote": "**An uninitialized result fails every assertion.**", + "claim": "**An uninitialized result fails every assertion.**", + "oracle": "claim", + "surfaces": [ + "FaultAssertions.*" + ], + "heldBy": [ + "ComposedFaultAssertionTests" + ] + }, + { + "id": "C20-whichever-of-its-types", + "source": "src/CodoMetis.TypeKit.Testing/README.md", + "quote": "whichever of its types the reason belongs to", + "claim": "whichever of its types the reason belongs to", + "oracle": "claim", + "surfaces": [ + "FaultAssertions.*" + ], + "heldBy": [ + "ComposedFaultAssertionTests" + ] + }, + { + "id": "C21-nullable-admits-null-as-the-wrapped-does", + "source": "README.md#what-you-get", + "quote": "Its OpenAPI schema is the one ASP.NET Core publishes for the underlying type.", + "claim": "Its OpenAPI schema is the one ASP.NET Core publishes for the underlying type.", + "oracle": "differential", + "surfaces": [ + "GeneratedJsonPlan.*", + "TypeKitOpenApiOptionsExtensions.AddTypeKit" + ], + "heldBy": [] + }, + { + "id": "C21-the-underlying-types-schema", + "source": "README.md#what-you-get", + "quote": "The JSON, the database column and the OpenAPI schema are the same as the underlying type's.", + "claim": "The JSON, the database column and the OpenAPI schema are the same as the underlying type's.", + "oracle": "differential", + "surfaces": [ + "TypeKitOpenApiOptionsExtensions.AddTypeKit", + "GeneratedJsonPlan.*" + ], + "heldBy": [] + }, + { + "id": "C22-abstract", + "source": "src/CodoMetis.TypeKit.Generators/README.md#build-errors", + "quote": "An abstract type with a value-object marker. It would get no member, and a value held as it, of a type derived from it, would have no `Value` and be written as `{}`.", + "claim": "An abstract type with a value-object marker. It would get no member, and a value held as it, of a type derived from it, would have no `Value` and be written as `{}`.", + "oracle": "exception", + "surfaces": [ + "IPlainValueObject.*", + "IValidatedValue.*", + "IClosedSet.*" + ], + "heldBy": [ + "BuildOutcomeTests" + ] + }, + { + "id": "C22-closed-set-named-as-generated-member", + "source": "src/CodoMetis.TypeKit.Generators/README.md#build-errors", + "quote": "a set itself named as one of them (a member can't have the name of its type)", + "claim": "a set itself named as one of them (a member can't have the name of its type)", + "oracle": "exception", + "surfaces": [ + "IPlainValueObject.*", + "IValidatedValue.*", + "IClosedSet.*" + ], + "heldBy": [ + "BuildOutcomeTests" + ] + }, + { + "id": "C22-equality-operator", + "source": "src/CodoMetis.TypeKit.Generators/README.md#build-errors", + "quote": "So a hand-written `Equals(TSelf)`, `GetHashCode()`, `IEquatable.Equals`, or `operator ==` or `!=` between two of the type, fails the build (CMTK1011).", + "claim": "So a hand-written `Equals(TSelf)`, `GetHashCode()`, `IEquatable.Equals`, or `operator ==` or `!=` between two of the type, fails the build (CMTK1011).", + "oracle": "exception", + "surfaces": [ + "IPlainValueObject.*", + "IValidatedValue.*", + "IClosedSet.*" + ], + "heldBy": [ + "BuildOutcomeTests" + ] + }, + { + "id": "C22-every-error-links-the-table", + "source": "src/CodoMetis.TypeKit.Generators/README.md#build-errors", + "quote": "Every error ends with a link to this table.", + "claim": "Every error ends with a link to this table.", + "oracle": "exception", + "surfaces": [ + "IPlainValueObject.*", + "IValidatedValue.*", + "IClosedSet.*" + ], + "heldBy": [ + "BuildOutcomeTests" + ] + }, + { + "id": "C22-generic-wrapped", + "source": "src/CodoMetis.TypeKit.Generators/README.md#build-errors", + "quote": "A generic underlying type, a tuple included, fails the build (CMTK1005).", + "claim": "A generic underlying type, a tuple included, fails the build (CMTK1005).", + "oracle": "exception", + "surfaces": [ + "IPlainValueObject.*", + "IValidatedValue.*", + "IClosedSet.*" + ], + "heldBy": [ + "BuildOutcomeTests" + ] + }, + { + "id": "C22-named-as-generated-member", + "source": "src/CodoMetis.TypeKit.Generators/README.md#build-errors", + "quote": "named `Value` or as another member the generators add (`From`, `TryFrom`, `FromKnownGood`, `Revalidate`, `Parse`, `TryParse`, `TryFormat`, `MinValue`, `MaxValue`, `CompareTo`)", + "claim": "named `Value` or as another member the generators add (`From`, `TryFrom`, `FromKnownGood`, `Revalidate`, `Parse`, `TryParse`, `TryFormat`, `MinValue`, `MaxValue`, `CompareTo`)", + "oracle": "exception", + "surfaces": [ + "IPlainValueObject.*", + "IValidatedValue.*", + "IClosedSet.*" + ], + "heldBy": [ + "BuildOutcomeTests" + ] + }, + { + "id": "C22-no-type-half-generated", + "source": "src/CodoMetis.TypeKit.Generators/README.md#build-errors", + "quote": "A declaration that can't be generated fails the build, so no type is left half-generated.", + "claim": "A declaration that can't be generated fails the build, so no type is left half-generated.", + "oracle": "exception", + "surfaces": [ + "IPlainValueObject.*", + "IValidatedValue.*", + "IClosedSet.*" + ], + "heldBy": [ + "BuildOutcomeTests" + ] + }, + { + "id": "C22-over-object", + "source": "src/CodoMetis.TypeKit.Generators/README.md#build-errors", + "quote": "or wrapping `object`, `System.Enum`, an array, a pointer, a nullable type, a generic type (a tuple included) or a value object (itself included).", + "claim": "or wrapping `object`, `System.Enum`, an array, a pointer, a nullable type, a generic type (a tuple included) or a value object (itself included).", + "oracle": "exception", + "surfaces": [ + "IClosedSet.*", + "IPlainValueObject.*", + "IValidatedValue.*" + ], + "heldBy": [ + "BuildOutcomeTests" + ] + }, + { + "id": "C22-over-value-object", + "source": "src/CodoMetis.TypeKit.Generators/README.md#build-errors", + "quote": "or a value object (itself included). Wrap what the other value object wraps instead.", + "claim": "or a value object (itself included). Wrap what the other value object wraps instead.", + "oracle": "exception", + "surfaces": [ + "IPlainValueObject.*", + "IValidatedValue.*", + "IClosedSet.*" + ], + "heldBy": [ + "BuildOutcomeTests" + ] + }, + { + "id": "C22-unsealed-record-class", + "source": "src/CodoMetis.TypeKit.Generators/README.md#build-errors", + "quote": "A record class not declared `sealed`.", + "claim": "A record class not declared `sealed`.", + "oracle": "exception", + "surfaces": [ + "IPlainValueObject.*", + "IValidatedValue.*", + "IClosedSet.*" + ], + "heldBy": [ + "BuildOutcomeTests" + ] + }, + { + "id": "C23-withheld-host-sees", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#faults-a-client-must-not-tell-apart", + "quote": "**Your host still sees the fault.** `IFaultFeature` carries its name, and `NameWithheld` says the mapping keeps it from the client", + "claim": "**Your host still sees the fault.** `IFaultFeature` carries its name, and `NameWithheld` says the mapping keeps it from the client", + "oracle": "claim", + "surfaces": [ + "IFaultFeature.*" + ], + "heldBy": [ + "WithheldFaultNameTests" + ] + }, + { + "id": "C23-withheld-says-nothing", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#faults-a-client-must-not-tell-apart", + "quote": "**The problem says nothing of the member**: no `fault`, no `rule`, and no fault set member's description as its `detail`.", + "claim": "**The problem says nothing of the member**: no `fault`, no `rule`, and no fault set member's description as its `detail`.", + "oracle": "claim", + "surfaces": [ + "FaultResponse.*" + ], + "heldBy": [ + "WithheldFaultNameTests" + ] + }, + { + "id": "C24-content-comes-back", + "source": "src/CodoMetis.TypeKit.Testing/README.md", + "quote": "**The content comes back.** `ShouldBeSome()`, `ShouldBeSuccess()`, `ShouldBeError()` and `ShouldBeValue()` return the value or the error.", + "claim": "**The content comes back.** `ShouldBeSome()`, `ShouldBeSuccess()`, `ShouldBeError()` and `ShouldBeValue()` return the value or the error.", + "oracle": "claim", + "surfaces": [ + "OptionAssertions.*", + "ResultAssertions.*", + "PatchAssertions.*" + ], + "heldBy": [ + "AssertionTests", + "PatchAssertionTests" + ] + }, + { + "id": "C25-add-create-accepts-every-value", + "source": "src/CodoMetis.TypeKit.Generators/README.md#declare-a-value-object", + "quote": "It writes a `Create` that accepts every value, ready for your rules:", + "claim": "It writes a `Create` that accepts every value, ready for your rules:", + "oracle": "claim", + "surfaces": [ + "IValidatedValue.Create" + ], + "heldBy": [ + "MissingCreateCodeFixTests", + "AddCreateTests" + ] + }, + { + "id": "C25-add-create-offered", + "source": "src/CodoMetis.TypeKit.Generators/README.md#declare-a-value-object", + "quote": "The build then fails because `Create` is missing (CS0535), and your editor offers a fix for that error: **Add Create**.", + "claim": "The build then fails because `Create` is missing (CS0535), and your editor offers a fix for that error: **Add Create**.", + "oracle": "claim", + "surfaces": [ + "IValidatedValue.Create" + ], + "heldBy": [ + "MissingCreateCodeFixTests" + ] + }, + { + "id": "C25-create-taking-t", + "source": "src/CodoMetis.TypeKit.Analyzers/README.md#add-create", + "quote": "**The type already has a `Create` that takes a `T`**, static or not.", + "claim": "**The type already has a `Create` that takes a `T`**, static or not.", + "oracle": "claim", + "surfaces": [ + "IValidatedValue.Create" + ], + "heldBy": [ + "MissingCreateCodeFixTests" + ] + }, + { + "id": "C25-first-member", + "source": "src/CodoMetis.TypeKit.Analyzers/README.md#add-create", + "quote": "`Create` goes in as the type's first member, indented as the other members are.", + "claim": "`Create` goes in as the type's first member, indented as the other members are.", + "oracle": "claim", + "surfaces": [ + "IValidatedValue.Create" + ], + "heldBy": [ + "MissingCreateCodeFixTests" + ] + }, + { + "id": "C25-one-line-body", + "source": "src/CodoMetis.TypeKit.Analyzers/README.md#add-create", + "quote": "**The type's body is on one line and already holds members.**", + "claim": "**The type's body is on one line and already holds members.**", + "oracle": "claim", + "surfaces": [ + "IValidatedValue.Create" + ], + "heldBy": [ + "MissingCreateCodeFixTests" + ] + }, + { + "id": "C25-semicolon-gets-a-body", + "source": "src/CodoMetis.TypeKit.Analyzers/README.md#add-create", + "quote": "A declaration that ends in `;` gets a body.", + "claim": "A declaration that ends in `;` gets a body.", + "oracle": "claim", + "surfaces": [ + "IValidatedValue.Create" + ], + "heldBy": [ + "MissingCreateCodeFixTests", + "MissingCreateVariantTests" + ] + }, + { + "id": "C25-tself-another-type", + "source": "src/CodoMetis.TypeKit.Analyzers/README.md#add-create", + "quote": "**`TSelf` is another type.** The value to return isn't this type's to build.", + "claim": "**`TSelf` is another type.** The value to return isn't this type's to build.", + "oracle": "claim", + "surfaces": [ + "IValidatedValue.Create" + ], + "heldBy": [ + "MissingCreateCodeFixTests" + ] + }, + { + "id": "C25-types-spelled-as-the-file-names-them", + "source": "src/CodoMetis.TypeKit.Analyzers/README.md#add-create", + "quote": "The types are spelled as your file names them, so a type from a namespace the file doesn't import is qualified.", + "claim": "The types are spelled as your file names them, so a type from a namespace the file doesn't import is qualified.", + "oracle": "claim", + "surfaces": [ + "IValidatedValue.Create" + ], + "heldBy": [ + "MissingCreateCodeFixTests", + "MissingCreateVariantTests" + ] + }, + { + "id": "C26-sized-array-warns", + "source": "README.md#value-objects", + "quote": "An array created with a length, whose slots all start out as `default`, gets a warning (CMTK0005).", + "claim": "An array created with a length, whose slots all start out as `default`, gets a warning (CMTK0005).", + "oracle": "claim", + "surfaces": [ + "IValueObject.*", + "IValidatedValue.*" + ], + "heldBy": [ + "DefaultFilledCollectionAnalyzerTests" + ] + }, + { + "id": "C26-struct-default-fails-the-build", + "source": "README.md#value-objects", + "quote": "So the analyzers make `new OrderId()` and `default(OrderId)` build errors (CMTK0001).", + "claim": "So the analyzers make `new OrderId()` and `default(OrderId)` build errors (CMTK0001).", + "oracle": "claim", + "surfaces": [ + "IValueObject.*", + "IValidatedValue.*" + ], + "heldBy": [ + "ForbiddenDefaultInitializationAnalyzerTests" + ] + }, + { + "id": "C27-inline-every-position", + "source": "docs/aspnet-core.md#describe-value-objects-as-plain-types", + "quote": "Each one gets exactly the schema the underlying type gets in that place.", + "claim": "Each one gets exactly the schema the underlying type gets in that place.", + "oracle": "differential", + "surfaces": [ + "TypeKitOpenApiOptions.*", + "TypeKitOpenApiOptionsExtensions.AddTypeKit" + ], + "heldBy": [ + "SchemaTests", + "NullablePositionTests" + ] + }, + { + "id": "C27-inline-no-component", + "source": "docs/aspnet-core.md#describe-value-objects-as-plain-types", + "quote": "Turn on `InlineValueObjects`, and every value object is described in place by the schema of the type inside it:", + "claim": "Turn on `InlineValueObjects`, and every value object is described in place by the schema of the type inside it:", + "oracle": "claim", + "surfaces": [ + "TypeKitOpenApiOptions.*", + "TypeKitOpenApiOptionsExtensions.AddTypeKit" + ], + "heldBy": [ + "InlineValueObjectsSettingTests", + "AspNetCoreTests" + ] + }, + { + "id": "C27-inline-nullable-in-2-0", + "source": "docs/aspnet-core.md#describe-value-objects-as-plain-types", + "quote": "That holds under OpenAPI 2.0 too, where a nullable property of a component loses its type.", + "claim": "That holds under OpenAPI 2.0 too, where a nullable property of a component loses its type.", + "oracle": "differential", + "surfaces": [ + "TypeKitOpenApiOptions.*", + "TypeKitOpenApiOptionsExtensions.AddTypeKit" + ], + "heldBy": [ + "SchemaTests", + "NullablePositionTests" + ] + }, + { + "id": "C27-inline-off-unchanged", + "source": "docs/aspnet-core.md#describe-value-objects-as-plain-types", + "quote": "`AddTypeKit()` writes the same document as before, with a component per value object.", + "claim": "`AddTypeKit()` writes the same document as before, with a component per value object.", + "oracle": "claim", + "surfaces": [ + "TypeKitOpenApiOptions.*", + "TypeKitOpenApiOptionsExtensions.AddTypeKit" + ], + "heldBy": [ + "InlineValueObjectsSettingTests" + ] + }, + { + "id": "C27-inline-same-name", + "source": "docs/aspnet-core.md#describe-value-objects-as-plain-types", + "quote": "Neither has a component, so neither needs a name of its own.", + "claim": "Neither has a component, so neither needs a name of its own.", + "oracle": "claim", + "surfaces": [ + "TypeKitOpenApiOptions.*", + "TypeKitOpenApiOptionsExtensions.AddTypeKit" + ], + "heldBy": [ + "InlineValueObjectsSettingTests" + ] + }, + { + "id": "E01-column-type", + "source": "docs/ef-core.md#the-model", + "quote": "Every property maps as a column of the underlying type, nullable where the property is.", + "claim": "Every property maps as a column of the underlying type, nullable where the property is.", + "oracle": "differential", + "surfaces": [ + "TypeKitDbContextOptionsBuilderExtensions.UseTypeKit", + "ValueObjectConverter.*" + ], + "heldBy": [ + "MappingTests" + ] + }, + { + "id": "E01-facet-kept", + "source": "docs/ef-core.md#the-model", + "quote": "A facet you configure on it is kept: a precision, a length, a column type.", + "claim": "A facet you configure on it is kept: a precision, a length, a column type.", + "oracle": "differential", + "surfaces": [ + "TypeKitDbContextOptionsBuilderExtensions.UseTypeKit", + "ValueObjectConverter.*" + ], + "heldBy": [ + "MappingTests" + ] + }, + { + "id": "E01-nullable", + "source": "docs/ef-core.md#the-model", + "quote": "nullable where the property is", + "claim": "nullable where the property is", + "oracle": "differential", + "surfaces": [ + "TypeKitDbContextOptionsBuilderExtensions.UseTypeKit", + "ValueObjectConverter.*" + ], + "heldBy": [ + "MappingTests" + ] + }, + { + "id": "E02-column-not-cast", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#queries", + "quote": "A table's column is re-typed, not cast, so an index on it still serves the query.", + "claim": "A table's column is re-typed, not cast, so an index on it still serves the query.", + "oracle": "differential", + "surfaces": [ + "TranslatedAsWrappedValueAttribute.*", + "IValueObject.Value" + ], + "heldBy": [ + "SqlTranslationTests" + ] + }, + { + "id": "E02-get-value", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#queries", + "quote": "db.Orders.Where(o => o.Total.GetValue() > 100m);", + "claim": "db.Orders.Where(o => o.Total.GetValue() > 100m);", + "oracle": "differential", + "surfaces": [ + "TranslatedAsWrappedValueAttribute.*", + "IValueObject.Value" + ], + "heldBy": [ + "SqlTranslationTests" + ] + }, + { + "id": "E02-value", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#queries", + "quote": "`.Value`, `GetValue()` and `ValueOrNull()` translate to the column itself.", + "claim": "`.Value`, `GetValue()` and `ValueOrNull()` translate to the column itself.", + "oracle": "differential", + "surfaces": [ + "TranslatedAsWrappedValueAttribute.*", + "IValueObject.Value" + ], + "heldBy": [ + "SqlTranslationTests" + ] + }, + { + "id": "E02-value-or-null", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#queries", + "quote": "Use `ValueOrNull()` for an optional value object, where `.Value` would first unwrap the `Nullable`.", + "claim": "Use `ValueOrNull()` for an optional value object, where `.Value` would first unwrap the `Nullable`.", + "oracle": "differential", + "surfaces": [ + "TranslatedAsWrappedValueAttribute.*", + "IValueObject.Value" + ], + "heldBy": [ + "SqlTranslationTests" + ] + }, + { + "id": "E03-keys", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#what-maps", + "quote": "Keys, foreign keys, nullable properties, primitive collections and query parameters all map as plain columns of the underlying type.", + "claim": "Keys, foreign keys, nullable properties, primitive collections and query parameters all map as plain columns of the underlying type.", + "oracle": "metamorphic", + "surfaces": [ + "ValueObjectConverter.*" + ], + "heldBy": [ + "PostgreSqlRoundTripTests" + ] + }, + { + "id": "E03-stored-as-underlying", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#what-maps", + "quote": "Every value object is stored in a plain column of its underlying type", + "claim": "Every value object is stored in a plain column of its underlying type", + "oracle": "metamorphic", + "surfaces": [ + "ValueObjectConverter.*" + ], + "heldBy": [ + "PostgreSqlRoundTripTests" + ] + }, + { + "id": "E04-byte-not-generated", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#keys", + "quote": "That's EF's own list: `int`, `long`, `short`, `sbyte`, `uint`, `ulong` and `ushort`, and not `byte`.", + "claim": "That's EF's own list: `int`, `long`, `short`, `sbyte`, `uint`, `ulong` and `ushort`, and not `byte`.", + "oracle": "claim", + "surfaces": [ + "WrappedTypeValueGeneratorFactory.*" + ], + "heldBy": [ + "KeyGenerationTests" + ] + }, + { + "id": "E04-char-key-yours", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#keys", + "quote": "A key over `char` is yours to assign.", + "claim": "A key over `char` is yours to assign.", + "oracle": "claim", + "surfaces": [ + "WrappedTypeValueGeneratorFactory.*" + ], + "heldBy": [ + "KeyGenerationTests" + ] + }, + { + "id": "E04-generated-on-add", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#keys", + "quote": "A single-column key over an integer is generated on add, like a key of that integer type.", + "claim": "A single-column key over an integer is generated on add, like a key of that integer type.", + "oracle": "claim", + "surfaces": [ + "WrappedTypeValueGeneratorFactory.*" + ], + "heldBy": [ + "KeyGenerationTests" + ] + }, + { + "id": "E04-guid-key-yours", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#keys", + "quote": "A key over a `Guid` is yours to assign", + "claim": "A key over a `Guid` is yours to assign", + "oracle": "claim", + "surfaces": [ + "WrappedTypeValueGeneratorFactory.*" + ], + "heldBy": [ + "KeyGenerationTests" + ] + }, + { + "id": "E04-hilo-and-own-generator-first", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#keys", + "quote": "HiLo and a value generator of your own still come first.", + "claim": "HiLo and a value generator of your own still come first.", + "oracle": "claim", + "surfaces": [ + "WrappedTypeValueGeneratorFactory.*" + ], + "heldBy": [ + "KeyGenerationTests" + ] + }, + { + "id": "E04-own-configuration-wins", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#keys", + "quote": "A configuration you write yourself always wins over these defaults.", + "claim": "A configuration you write yourself always wins over these defaults.", + "oracle": "claim", + "surfaces": [ + "WrappedTypeValueGeneratorFactory.*" + ], + "heldBy": [ + "KeyGenerationTests" + ] + }, + { + "id": "E04-temporary-as-plain", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#keys", + "quote": "At `Add`, a key that is generated gets the same temporary value a plain key of the underlying type gets, also where the column is of another type, such as the `decimal` that holds a `ulong`.", + "claim": "At `Add`, a key that is generated gets the same temporary value a plain key of the underlying type gets, also where the column is of another type, such as the `decimal` that holds a `ulong`.", + "oracle": "claim", + "surfaces": [ + "WrappedTypeValueGeneratorFactory.*" + ], + "heldBy": [ + "KeyGenerationTests", + "KeyGenerationVariantTests" + ] + }, + { + "id": "E04-ulong-key-native-aot", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#keys", + "quote": "Otherwise the key gets a temporary `ulong`, as a plain one does.", + "claim": "A default key over `ulong` throws at `Add` where the column is a `decimal` in a compiled model made with `--nativeaot` and where `ValueObjectConverter` is configured by hand; otherwise it gets a temporary `ulong`, as a plain one does.", + "oracle": "claim", + "surfaces": [ + "WrappedTypeValueGeneratorFactory.*" + ], + "heldBy": [ + "KeyGenerationTests" + ] + }, + { + "id": "E05-comparison", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#queries", + "quote": "a comparison (`<`, `>`, `<=`, `>=`, `CompareTo`);", + "claim": "a comparison (`<`, `>`, `<=`, `>=`, `CompareTo`);", + "oracle": "exception", + "surfaces": [ + "HandWrittenOrderingAttribute.*" + ], + "heldBy": [ + "HandWrittenOrderingTests" + ] + }, + { + "id": "E05-equality-allowed", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#queries", + "quote": "Equality isn't ordering, and stays allowed.", + "claim": "Equality isn't ordering, and stays allowed.", + "oracle": "exception", + "surfaces": [ + "HandWrittenOrderingAttribute.*" + ], + "heldBy": [ + "HandWrittenOrderingTests" + ] + }, + { + "id": "E05-ordering", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#queries", + "quote": "an ordering (`OrderBy`, `ThenBy`, their descending forms, `Order`);", + "claim": "an ordering (`OrderBy`, `ThenBy`, their descending forms, `Order`);", + "oracle": "exception", + "surfaces": [ + "HandWrittenOrderingAttribute.*" + ], + "heldBy": [ + "HandWrittenOrderingTests" + ] + }, + { + "id": "E05-query-filter", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#queries", + "quote": "A query filter (`HasQueryFilter`, named or not) that does the same fails when the model is built, naming the entity type, since it applies to every query of that type.", + "claim": "A query filter (`HasQueryFilter`, named or not) that does the same fails when the model is built, naming the entity type, since it applies to every query of that type.", + "oracle": "exception", + "surfaces": [ + "HandWrittenOrderingAttribute.*" + ], + "heldBy": [ + "HandWrittenOrderingTests" + ] + }, + { + "id": "E05-value-order-allowed", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#queries", + "quote": "Compare or order by `.Value` where the underlying value's order is what you mean", + "claim": "Compare or order by `.Value` where the underlying value's order is what you mean", + "oracle": "exception", + "surfaces": [ + "HandWrittenOrderingAttribute.*" + ], + "heldBy": [ + "HandWrittenOrderingTests" + ] + }, + { + "id": "E06-value-not-translated", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#queries", + "quote": "So a filter, a sort, a grouping or an aggregate that reads `.Value` there fails with EF's \"could not be translated\", rather than comparing against what the column really holds.", + "claim": "So a filter, a sort, a grouping or an aggregate that reads `.Value` there fails with EF's \"could not be translated\", rather than comparing against what the column really holds.", + "oracle": "exception", + "surfaces": [ + "ValueObjectConverter.*" + ], + "heldBy": [ + "ForeignConverterTests" + ] + }, + { + "id": "E06-value-object-comparison", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#queries", + "quote": "Comparing the value object itself (`o.Code == code`) goes through that converter and still works.", + "claim": "Comparing the value object itself (`o.Code == code`) goes through that converter and still works.", + "oracle": "exception", + "surfaces": [ + "ValueObjectConverter.*" + ], + "heldBy": [ + "ForeignConverterTests" + ] + }, + { + "id": "E07-in-collections-and-json", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#closed-sets", + "quote": "That holds in a table, in a primitive collection of names (`text[]` on PostgreSQL, a JSON array elsewhere) and in a JSON column alike, with nothing registered per set.", + "claim": "That holds in a table, in a primitive collection of names (`text[]` on PostgreSQL, a JSON array elsewhere) and in a JSON column alike, with nothing registered per set.", + "oracle": "claim", + "surfaces": [ + "ClosedSetConverter.*" + ], + "heldBy": [ + "ClosedSetMappingTests" + ] + }, + { + "id": "E07-non-member-throws", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#closed-sets", + "quote": "An instance that isn't a member throws when it's written.", + "claim": "An instance that isn't a member throws when it's written.", + "oracle": "claim", + "surfaces": [ + "ClosedSetConverter.*" + ], + "heldBy": [ + "ClosedSetMappingTests" + ] + }, + { + "id": "E07-stored-as-name", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#closed-sets", + "quote": "A closed set (`IClosedSet`) is stored as a text column of the member's name, and read back as the member itself.", + "claim": "A closed set (`IClosedSet`) is stored as a text column of the member's name, and read back as the member itself.", + "oracle": "claim", + "surfaces": [ + "ClosedSetConverter.*" + ], + "heldBy": [ + "ClosedSetMappingTests" + ] + }, + { + "id": "E07-unknown-name-throws", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#closed-sets", + "quote": "A name the set no longer has throws when it's read, rather than reading as null.", + "claim": "A name the set no longer has throws when it's read, rather than reading as null.", + "oracle": "claim", + "surfaces": [ + "ClosedSetConverter.*" + ], + "heldBy": [ + "ClosedSetMappingTests" + ] + }, + { + "id": "E08-error-says-so", + "source": "docs/ef-core.md#option-and-result", + "quote": "building the model fails with an error that says so", + "claim": "building the model fails with an error that says so", + "oracle": "exception", + "surfaces": [ + "TypeKitDbContextOptionsBuilderExtensions.UseTypeKit" + ], + "heldBy": [] + }, + { + "id": "E08-model-fails", + "source": "docs/ef-core.md#option-and-result", + "quote": "EF can't map an `Option` or a `Result` property", + "claim": "EF can't map an `Option` or a `Result` property", + "oracle": "exception", + "surfaces": [ + "TypeKitDbContextOptionsBuilderExtensions.UseTypeKit" + ], + "heldBy": [] + }, + { + "id": "E09-element-at-skips", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#an-option-for-nothing-found", + "quote": "skips to its row: `OFFSET @p`.", + "claim": "skips to its row: `OFFSET @p`.", + "oracle": "claim", + "surfaces": [ + "TypeKitQueryableExtensions.*", + "Option.FirstOrNone", + "Option.SingleOrNone" + ], + "heldBy": [ + "QueryOptionTests" + ] + }, + { + "id": "E09-first-reads-one-row", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#an-option-for-nothing-found", + "quote": "read one row: `WHERE ... LIMIT @p`.", + "claim": "read one row: `WHERE ... LIMIT @p`.", + "oracle": "claim", + "surfaces": [ + "TypeKitQueryableExtensions.*", + "Option.FirstOrNone", + "Option.SingleOrNone" + ], + "heldBy": [ + "QueryOptionTests" + ] + }, + { + "id": "E09-single-asks-for-two", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#an-option-for-nothing-found", + "quote": "ask for two rows, and throw for more than one.", + "claim": "ask for two rows, and throw for more than one.", + "oracle": "claim", + "surfaces": [ + "TypeKitQueryableExtensions.*", + "Option.FirstOrNone", + "Option.SingleOrNone" + ], + "heldBy": [ + "QueryOptionTests" + ] + }, + { + "id": "E10-reads-without-create", + "source": "docs/ef-core.md#reading-back-without-validation", + "quote": "EF reads a column back without validation, through the value object's `Materialize` rather than `Create`.", + "claim": "EF reads a column back without validation, through the value object's `Materialize` rather than `Create`.", + "oracle": "claim", + "surfaces": [ + "IValueObjectMaterializer.*", + "ValueObjectConverter.Materialize" + ], + "heldBy": [] + }, + { + "id": "E10-revalidate-finds", + "source": "docs/ef-core.md#reading-back-without-validation", + "quote": "`Revalidate()` finds the rows that today's rules reject, as above.", + "claim": "`Revalidate()` finds the rows that today's rules reject, as above.", + "oracle": "claim", + "surfaces": [ + "IValueObjectMaterializer.*", + "ValueObjectConverter.Materialize" + ], + "heldBy": [] + }, + { + "id": "E11-order-does-not-matter", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#setup", + "quote": "it doesn't matter whether you call it before or after `UseNpgsql` or `UseSqlite`", + "claim": "it doesn't matter whether you call it before or after `UseNpgsql` or `UseSqlite`", + "oracle": "claim", + "surfaces": [ + "TypeKitDbContextOptionsBuilderExtensions.UseTypeKit" + ], + "heldBy": [] + }, + { + "id": "E12-underlying-type-does-not-reach", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#model-wide-conventions-follow-the-propertys-type", + "quote": "So `configurationBuilder.Properties().HavePrecision(18, 2)` reaches `decimal` properties, and not an `Amount` that wraps one.", + "claim": "So `configurationBuilder.Properties().HavePrecision(18, 2)` reaches `decimal` properties, and not an `Amount` that wraps one.", + "oracle": "claim", + "surfaces": [ + "TypeKitDbContextOptionsBuilderExtensions.UseTypeKit" + ], + "heldBy": [] + }, + { + "id": "E12-value-object-type-reaches", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#model-wide-conventions-follow-the-propertys-type", + "quote": "Configure the value object instead: `configurationBuilder.Properties().HavePrecision(18, 2)`.", + "claim": "Configure the value object instead: `configurationBuilder.Properties().HavePrecision(18, 2)`.", + "oracle": "claim", + "surfaces": [ + "TypeKitDbContextOptionsBuilderExtensions.UseTypeKit" + ], + "heldBy": [] + }, + { + "id": "E13-contains", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#queries", + "quote": "db.Orders.Where(o => o.Tags.Contains(tag));", + "claim": "db.Orders.Where(o => o.Tags.Contains(tag));", + "oracle": "claim", + "surfaces": [ + "ValueObjectConverter.*" + ], + "heldBy": [] + }, + { + "id": "E13-element-value", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#queries", + "quote": "db.Orders.Where(o => o.Tags.Any(t => t.Value == \"x\"));", + "claim": "db.Orders.Where(o => o.Tags.Any(t => t.Value == \"x\"));", + "oracle": "claim", + "surfaces": [ + "ValueObjectConverter.*" + ], + "heldBy": [] + }, + { + "id": "E14-tpc-assign-on-sqlite", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#keys", + "quote": "SQLite has no such sequence, so there you assign it.", + "claim": "SQLite has no such sequence, so there you assign it.", + "oracle": "claim", + "surfaces": [ + "WrappedTypeValueGeneratorFactory.*" + ], + "heldBy": [ + "KeyGenerationTests" + ] + }, + { + "id": "E14-tph-tpt-root", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#keys", + "quote": "The root of an inheritance hierarchy follows the same rules under TPH and TPT.", + "claim": "The root of an inheritance hierarchy follows the same rules under TPH and TPT.", + "oracle": "claim", + "surfaces": [ + "WrappedTypeValueGeneratorFactory.*" + ], + "heldBy": [ + "KeyGenerationTests" + ] + }, + { + "id": "E15-as-for-primitives", + "source": "docs/ef-core.md#keys", + "quote": "A composite key of value objects is `HasKey(x => new { x.Order, x.Line })`, as for primitives.", + "claim": "A composite key of value objects is `HasKey(x => new { x.Order, x.Line })`, as for primitives.", + "oracle": "claim", + "surfaces": [ + "WrappedTypeValueGeneratorFactory.*" + ], + "heldBy": [] + }, + { + "id": "E16-column-type", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#keys", + "quote": "A facet you configure on the property, such as a maximum length or a column type, is kept.", + "claim": "A facet you configure on the property, such as a maximum length or a column type, is kept.", + "oracle": "claim", + "surfaces": [ + "WrappedTypeValueGeneratorFactory.*" + ], + "heldBy": [ + "KeyGenerationTests" + ] + }, + { + "id": "E17-as-the-underlying-type", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#on-sql-server", + "quote": "A value object behaves on SQL Server as the type inside it does there.", + "claim": "A value object behaves on SQL Server as the type inside it does there.", + "oracle": "differential", + "surfaces": [ + "TranslatedAsWrappedValueAttribute.*", + "IValueObject.Value" + ], + "heldBy": [ + "SqlServerQueryVariantTests", + "SqlServerKeyVariantTests", + "SqlServerRoundTripTests" + ] + }, + { + "id": "E17-datetime-kind", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#on-sql-server", + "quote": "`datetime2` keeps every tick, and a UTC value reads back with `Kind` `Unspecified`.", + "claim": "`datetime2` keeps every tick, and a UTC value reads back with `Kind` `Unspecified`.", + "oracle": "metamorphic", + "surfaces": [ + "ValueObjectConverter.*" + ], + "heldBy": [ + "SqlServerRoundTripTests" + ] + }, + { + "id": "E17-decimal-scale", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#on-sql-server", + "quote": "`10m` comes back as `10.00m`, which is equal but prints `10.00`.", + "claim": "`10m` comes back as `10.00m`, which is equal but prints `10.00`.", + "oracle": "metamorphic", + "surfaces": [ + "ValueObjectConverter.*" + ], + "heldBy": [ + "SqlServerRoundTripTests", + "SqlServerQueryVariantTests", + "SqlServerKeyVariantTests" + ] + }, + { + "id": "E17-greatest-level-160", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#on-sql-server", + "quote": "`EF.Functions.Greatest` and `Least` need compatibility level 160.", + "claim": "`EF.Functions.Greatest` and `Least` need compatibility level 160.", + "oracle": "differential", + "surfaces": [ + "TranslatedAsWrappedValueAttribute.*", + "IValueObject.Value" + ], + "heldBy": [ + "SqlServerQueryVariantTests" + ] + }, + { + "id": "E17-guid-order", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#on-sql-server", + "quote": "SQL Server orders a `uniqueidentifier` by its last six bytes first.", + "claim": "SQL Server orders a `uniqueidentifier` by its last six bytes first.", + "oracle": "metamorphic", + "surfaces": [ + "ValueObjectConverter.*" + ], + "heldBy": [ + "SqlServerQueryVariantTests" + ] + }, + { + "id": "E17-text-ignores-case", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#on-sql-server", + "quote": "Under SQL Server's default collation, `o.Note == note`, `notes.Contains(o.Note)` and `StartsWith` also match text that differs in case or in trailing spaces.", + "claim": "Under SQL Server's default collation, `o.Note == note`, `notes.Contains(o.Note)` and `StartsWith` also match text that differs in case or in trailing spaces.", + "oracle": "metamorphic", + "surfaces": [ + "ValueObjectConverter.*" + ], + "heldBy": [ + "SqlServerRoundTripTests", + "SqlServerQueryVariantTests" + ] + }, + { + "id": "E17-timespan-under-a-day", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#on-sql-server", + "quote": "A longer one fails `SaveChanges` in a column.", + "claim": "A longer one fails `SaveChanges` in a column.", + "oracle": "metamorphic", + "surfaces": [ + "ValueObjectConverter.*" + ], + "heldBy": [ + "SqlServerQueryVariantTests" + ] + }, + { + "id": "G01-a-version-7-guid", + "source": "src/CodoMetis.TypeKit.Generators/README.md#declare-a-value-object", + "quote": "var id = OrderId.New(); // a version 7 Guid", + "claim": "var id = OrderId.New(); // a version 7 Guid", + "oracle": "claim", + "surfaces": [ + "GuidValueExtensions.New" + ], + "heldBy": [ + "GuidValueExtensionsTests" + ] + }, + { + "id": "H01-any-closed-set", + "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#a-closed-set-as-the-fault-type", + "quote": "**Any closed set can be an error type**, with a mapping of its own: `AddFaultResponses(fault => ...)`. `ToHttpResult()` takes a result over either.", + "claim": "**Any closed set can be an error type**, with a mapping of its own: `AddFaultResponses(fault => ...)`. `ToHttpResult()` takes a result over either.", + "oracle": "claim", + "surfaces": [ + "TypeKitClosedSetHttpResultExtensions.ToHttpResult" + ], + "heldBy": [ + "FaultSetDefaultTests", + "FaultResponsePartsTests" + ] + }, + { + "id": "I01-mark-it", + "source": "src/CodoMetis.TypeKit.Analyzers/README.md#cmtk0001", + "quote": "Mark it with `[RequireCustomInitialization]`, and CMTK0001, CMTK0005, CMTK0006 and CMTK0009 then treat it as they treat a value object.", + "claim": "Mark it with `[RequireCustomInitialization]`, and CMTK0001, CMTK0005, CMTK0006 and CMTK0009 then treat it as they treat a value object.", + "oracle": "claim", + "surfaces": [ + "RequireCustomInitializationAttribute..ctor", + "RequireCustomInitializationAttribute.ErrorMessage" + ], + "heldBy": [ + "ForbiddenDefaultInitializationAnalyzerTests", + "DefaultFilledCollectionAnalyzerTests", + "UnassignedNoDefaultMemberAnalyzerTests", + "DefaultProducingCallAnalyzerTests" + ] + }, + { + "id": "I01-not-one-itself", + "source": "src/CodoMetis.TypeKit.Analyzers/README.md#cmtk0001", + "quote": "Its `default` holds default value objects that no rule sees, because the rules don't look inside another struct.", + "claim": "Its `default` holds default value objects that no rule sees, because the rules don't look inside another struct.", + "oracle": "claim", + "surfaces": [ + "RequireCustomInitializationAttribute..ctor", + "RequireCustomInitializationAttribute.ErrorMessage" + ], + "heldBy": [ + "UnassignedNoDefaultMemberAnalyzerTests" + ] + }, + { + "id": "I01-the-message-you-give", + "source": "src/CodoMetis.TypeKit.Analyzers/README.md#cmtk0001", + "quote": "The rule reports the message you give, as `Option` and `Result` name their own factories.", + "claim": "The rule reports the message you give, as `Option` and `Result` name their own factories.", + "oracle": "claim", + "surfaces": [ + "RequireCustomInitializationAttribute..ctor", + "RequireCustomInitializationAttribute.ErrorMessage" + ], + "heldBy": [ + "ForbiddenDefaultInitializationAnalyzerTests" + ] + }, + { + "id": "L01-read-as-the-underlying-type-reads", + "source": "README.md#what-typekit-guarantees", + "quote": "**Swapping a primitive for a value object keeps the JSON, the column and the schema.**", + "claim": "**Swapping a primitive for a value object keeps the JSON, the column and the schema.**", + "oracle": "differential", + "surfaces": [ + "GeneratedJson.*", + "GeneratedJsonPlan.*", + "StoredJsonConverterFactory.*" + ], + "heldBy": [ + "JsonParityTests" + ] + }, + { + "id": "L01-the-underlying-types-json", + "source": "README.md#what-typekit-guarantees", + "quote": "Its JSON is the underlying type's under your options", + "claim": "Its JSON is the underlying type's under your options", + "oracle": "differential", + "surfaces": [ + "GeneratedJson.*", + "GeneratedJsonPlan.*", + "StoredJsonConverterFactory.*" + ], + "heldBy": [ + "JsonParityTests" + ] + }, + { + "id": "L02-json-never-repeats", + "source": "README.md#what-typekit-guarantees", + "quote": "A JSON or parsing rejection names the value object and the rule, or the underlying type it could not read.", + "claim": "A JSON or parsing rejection names the value object and the rule, or the underlying type it could not read.", + "oracle": "exception", + "surfaces": [ + "GeneratedParsing.*", + "GeneratedFactories.*" + ], + "heldBy": [ + "UnreadableInputTests" + ] + }, + { + "id": "L02-no-inner-that-quotes", + "source": "README.md#what-typekit-guarantees", + "quote": "It carries no inner exception that quotes the text.", + "claim": "It carries no inner exception that quotes the text.", + "oracle": "exception", + "surfaces": [ + "GeneratedParsing.*", + "GeneratedFactories.*" + ], + "heldBy": [ + "UnreadableInputTests" + ] + }, + { + "id": "L02-parsing-never-repeats", + "source": "README.md#what-typekit-guarantees", + "quote": "**Rejections never repeat the input.**", + "claim": "**Rejections never repeat the input.**", + "oracle": "exception", + "surfaces": [ + "GeneratedParsing.*", + "GeneratedFactories.*" + ], + "heldBy": [ + "UnreadableInputTests" + ] + }, + { + "id": "L03-refusals-name-the-fault", + "source": "README.md#what-typekit-guarantees", + "quote": "the `JsonException`, the `FormatException` and the `FromKnownGood` exception all name your `EmailFault.NoAt`", + "claim": "the `JsonException`, the `FormatException` and the `FromKnownGood` exception all name your `EmailFault.NoAt`", + "oracle": "claim", + "surfaces": [ + "IValidatedValue.Create", + "GeneratedFactories.*" + ], + "heldBy": [ + "EntryPointValidationTests" + ] + }, + { + "id": "L03-tryfrom-runs-create", + "source": "README.md#what-typekit-guarantees", + "quote": "The generated `TryFrom`, `FromKnownGood`, JSON converter, parsing and type converter all run it", + "claim": "The generated `TryFrom`, `FromKnownGood`, JSON converter, parsing and type converter all run it", + "oracle": "claim", + "surfaces": [ + "GeneratedFactories.*", + "IValidatedValue.Create" + ], + "heldBy": [ + "EntryPointValidationTests" + ] + }, + { + "id": "L04-invariant-whatever-the-culture", + "source": "src/CodoMetis.TypeKit.Generators/README.md#parsing-and-formatting", + "quote": "A null `IFormatProvider` means the invariant culture in every generated `Parse`, `TryParse`, `ToString` and `TryFormat`, so the text doesn't depend on the culture the process runs in.", + "claim": "A null `IFormatProvider` means the invariant culture in every generated `Parse`, `TryParse`, `ToString` and `TryFormat`, so the text doesn't depend on the culture the process runs in.", + "oracle": "metamorphic", + "surfaces": [ + "GeneratedFormatting.*", + "GeneratedParsing.*" + ], + "heldBy": [] + }, + { + "id": "L04-json-refusal-is-a-json-exception", + "source": "src/CodoMetis.TypeKit.Generators/README.md#parsing-and-formatting", + "quote": "Input it can't read throws a `JsonException` naming the value object and the underlying type", + "claim": "Input it can't read throws a `JsonException` naming the value object and the underlying type", + "oracle": "metamorphic", + "surfaces": [ + "GeneratedFormatting.*", + "GeneratedParsing.*" + ], + "heldBy": [] + }, + { + "id": "L04-text-reads-back", + "source": "src/CodoMetis.TypeKit.Generators/README.md#parsing-and-formatting", + "quote": "That text reads back as the same value wherever the underlying type's own does", + "claim": "That text reads back as the same value wherever the underlying type's own does", + "oracle": "metamorphic", + "surfaces": [ + "GeneratedFormatting.*", + "GeneratedParsing.*" + ], + "heldBy": [] + }, + { + "id": "L04-the-type-converter-writes-the-same", + "source": "src/CodoMetis.TypeKit.Generators/README.md#parsing-and-formatting", + "quote": "`ToString()` is the underlying type's own default text under the invariant culture, and the type converter and `$\"{x}\"` write the same.", + "claim": "`ToString()` is the underlying type's own default text under the invariant culture, and the type converter and `$\"{x}\"` write the same.", + "oracle": "metamorphic", + "surfaces": [ + "GeneratedFormatting.*", + "GeneratedParsing.*" + ], + "heldBy": [] + }, + { + "id": "L05-equality-is-the-underlying-values", + "source": "src/CodoMetis.TypeKit.Generators/README.md#equality", + "quote": "Equality is the underlying value's, like the ordering, the JSON and the column.", + "claim": "Equality is the underlying value's, like the ordering, the JSON and the column.", + "oracle": "metamorphic", + "surfaces": [ + "IValueObject.op_Equality", + "IValueObject.op_Inequality" + ], + "heldBy": [] + }, + { + "id": "L05-ordering-follows-the-underlying-type", + "source": "src/CodoMetis.TypeKit.Generators/README.md#equality", + "quote": "`CompareTo` and the operators follow the underlying type's ordering, ordinal for a string.", + "claim": "`CompareTo` and the operators follow the underlying type's ordering, ordinal for a string.", + "oracle": "metamorphic", + "surfaces": [ + "IValueObject.op_Equality", + "IValueObject.op_Inequality" + ], + "heldBy": [] + }, + { + "id": "L05-through-every-base", + "source": "src/CodoMetis.TypeKit.Generators/README.md#equality", + "quote": "The value object implements each base's `IEquatable` again (`IReimplementedEquatable`), so a collection of the base compares it the way the value object does.", + "claim": "The value object implements each base's `IEquatable` again (`IReimplementedEquatable`), so a collection of the base compares it the way the value object does.", + "oracle": "metamorphic", + "surfaces": [ + "IValueObject.op_Equality", + "IValueObject.op_Inequality" + ], + "heldBy": [] + }, + { + "id": "L06-never-written", + "source": "README.md#what-typekit-guarantees", + "quote": "System.Text.Json throws `NotSupportedException` for either, in both directions, instead of writing `{}` that reads back as `None`.", + "claim": "System.Text.Json throws `NotSupportedException` for either, in both directions, instead of writing `{}` that reads back as `None`.", + "oracle": "exception", + "surfaces": [ + "NotWireTypeJsonConverterFactory.*" + ], + "heldBy": [ + "NotWireTypeTests" + ] + }, + { + "id": "O01-default-is-none", + "source": "docs/option.md#option", + "quote": "An option that says it has a value always has one: `T` is `notnull`, `Option.Some(null)` throws, and a `default(Option)` is `None`.", + "claim": "An option that says it has a value always has one: `T` is `notnull`, `Option.Some(null)` throws, and a `default(Option)` is `None`.", + "oracle": "exception", + "surfaces": [ + "Option.Some", + "Option..ctor", + "Option.IsSome" + ], + "heldBy": [ + "OptionTests" + ] + }, + { + "id": "O01-tostring-never-prints", + "source": "docs/option.md#option", + "quote": "`ToString()` never prints the content.", + "claim": "`ToString()` never prints the content.", + "oracle": "claim", + "surfaces": [ + "Option.ToString" + ], + "heldBy": [ + "TextRepresentationTests" + ] + }, + { + "id": "O02-element-at-or-none", + "source": "docs/option.md#create-one", + "quote": "**`ElementAtOrNone(index)`**, for the element at a position.", + "claim": "**`ElementAtOrNone(index)`**, for the element at a position.", + "oracle": "claim", + "surfaces": [ + "Option.ElementAtOrNone" + ], + "heldBy": [ + "OptionLookupTests" + ] + }, + { + "id": "O02-first-and-last-or-none", + "source": "docs/option.md#create-one", + "quote": "**`FirstOrNone()` and `LastOrNone()`** on a sequence, with or without a predicate.", + "claim": "**`FirstOrNone()` and `LastOrNone()`** on a sequence, with or without a predicate.", + "oracle": "claim", + "surfaces": [ + "Option.FirstOrNone", + "Option.LastOrNone" + ], + "heldBy": [ + "OptionExtensionsTests" + ] + }, + { + "id": "O02-get-value-or-none", + "source": "docs/option.md#create-one", + "quote": "**`GetValueOrNone(key)`** on a dictionary.", + "claim": "**`GetValueOrNone(key)`** on a dictionary.", + "oracle": "claim", + "surfaces": [ + "Option.GetValueOrNone" + ], + "heldBy": [ + "OptionExtensionsTests" + ] + }, + { + "id": "O02-none-takes-its-type", + "source": "docs/option.md#create-one", + "quote": "`None()` takes its type from where it goes: a `return`, a conditional beside a `Some`, or a parameter.", + "claim": "`None()` takes its type from where it goes: a `return`, a conditional beside a `Some`, or a parameter.", + "oracle": "claim", + "surfaces": [ + "Option.None", + "Option.op_Implicit" + ], + "heldBy": [ + "OptionTests" + ] + }, + { + "id": "O02-single-or-none-throws", + "source": "docs/option.md#create-one", + "quote": "**`SingleOrNone()`**, which throws when there is more than one element.", + "claim": "**`SingleOrNone()`**, which throws when there is more than one element.", + "oracle": "exception", + "surfaces": [ + "Option.SingleOrNone" + ], + "heldBy": [ + "OptionLookupTests" + ] + }, + { + "id": "O02-to-option-null-is-none", + "source": "docs/option.md#create-one", + "quote": "**`ToOption()`** on a nullable, such as an `int?` or a `string?`. A null becomes `None`.", + "claim": "**`ToOption()`** on a nullable, such as an `int?` or a `string?`. A null becomes `None`.", + "oracle": "claim", + "surfaces": [ + "Option.ToOption" + ], + "heldBy": [ + "OptionExtensionsTests", + "OrNullTests" + ] + }, + { + "id": "O03-a-func-reads-every-row", + "source": "docs/option.md#in-a-database-query", + "quote": "A `Func` held in a variable, or a method group, binds to the sequence overload instead. That reads every row and checks the condition in memory", + "claim": "A `Func` held in a variable, or a method group, binds to the sequence overload instead. That reads every row and checks the condition in memory", + "oracle": "claim", + "surfaces": [ + "Option.FirstOrNone" + ], + "heldBy": [ + "QueryOptionTests" + ] + }, + { + "id": "O03-last-on-a-query-does-not-compile", + "source": "docs/option.md#in-a-database-query", + "quote": "`LastOrNone` on a query fails to compile, because a query has no last row until you order it.", + "claim": "`LastOrNone` on a query fails to compile, because a query has no last row until you order it.", + "oracle": "claim", + "surfaces": [ + "Option.LastOrNone" + ], + "heldBy": [ + "QueryableOptionTests" + ] + }, + { + "id": "O03-provider-runs-the-predicate", + "source": "docs/option.md#in-a-database-query", + "quote": "The database provider runs the predicate and fetches only the rows they need.", + "claim": "The database provider runs the predicate and fetches only the rows they need.", + "oracle": "claim", + "surfaces": [ + "Option.FirstOrNone", + "Option.SingleOrNone", + "Option.ElementAtOrNone" + ], + "heldBy": [ + "QueryableOptionTests", + "QueryOptionTests" + ] + }, + { + "id": "O03-work-on-a-query", + "source": "docs/option.md#in-a-database-query", + "quote": "`FirstOrNone`, `SingleOrNone` and `ElementAtOrNone` also work on an `IQueryable`.", + "claim": "`FirstOrNone`, `SingleOrNone` and `ElementAtOrNone` also work on an `IQueryable`.", + "oracle": "claim", + "surfaces": [ + "Option.FirstOrNone", + "Option.SingleOrNone", + "Option.ElementAtOrNone" + ], + "heldBy": [ + "QueryableOptionTests", + "QueryOptionTests" + ] + }, + { + "id": "O04-as-enumerable", + "source": "docs/option.md#get-the-value-out", + "quote": "**`AsEnumerable()`** gives you zero or one element, for a `foreach` or LINQ.", + "claim": "**`AsEnumerable()`** gives you zero or one element, for a `foreach` or LINQ.", + "oracle": "claim", + "surfaces": [ + "Option.AsEnumerable" + ], + "heldBy": [ + "OptionTests" + ] + }, + { + "id": "O04-is-some-and-is-none", + "source": "docs/option.md#get-the-value-out", + "quote": "**`IsSome()` and `IsNone()`** answer the question on its own.", + "claim": "**`IsSome()` and `IsNone()`** answer the question on its own.", + "oracle": "claim", + "surfaces": [ + "Option.IsSome", + "Option.IsNone" + ], + "heldBy": [ + "OptionTests" + ] + }, + { + "id": "O04-match-takes-both-branches", + "source": "docs/option.md#get-the-value-out", + "quote": "`Match` takes one branch for a value and one for nothing, and returns what they return.", + "claim": "`Match` takes one branch for a value and one for nothing, and returns what they return.", + "oracle": "claim", + "surfaces": [ + "Option.Match" + ], + "heldBy": [ + "OptionTests" + ] + }, + { + "id": "O04-or-and-or-default", + "source": "docs/option.md#get-the-value-out", + "quote": "**`Or(fallback)` and `OrDefault()`** give you the value, or a fallback when there is none.", + "claim": "**`Or(fallback)` and `OrDefault()`** give you the value, or a fallback when there is none.", + "oracle": "claim", + "surfaces": [ + "Option.Or", + "Option.OrDefault" + ], + "heldBy": [ + "OptionTests", + "OptionExtensionsTests" + ] + }, + { + "id": "O04-or-null", + "source": "docs/option.md#get-the-value-out", + "quote": "**`OrNull()`** gives you a nullable: an `int?` for an `Option`, a `string?` for an `Option`.", + "claim": "**`OrNull()`** gives you a nullable: an `int?` for an `Option`, a `string?` for an `Option`.", + "oracle": "claim", + "surfaces": [ + "Option.OrNull", + "ValueTypeOptionExtensions.OrNull" + ], + "heldBy": [ + "OrNullTests", + "OptionExtensionsTests" + ] + }, + { + "id": "O04-try-get-value", + "source": "docs/option.md#get-the-value-out", + "quote": "**`TryGetValue(out value)`** fits an `if`, and the value is non-null inside the branch.", + "claim": "**`TryGetValue(out value)`** fits an `if`, and the value is non-null inside the branch.", + "oracle": "claim", + "surfaces": [ + "Option.TryGetValue" + ], + "heldBy": [ + "OptionTests" + ] + }, + { + "id": "O05-bind-flattens", + "source": "docs/option.md#combine-options", + "quote": "**`Bind`** takes a selector that returns an option itself, so you never end up with an `Option>`.", + "claim": "**`Bind`** takes a selector that returns an option itself, so you never end up with an `Option>`.", + "oracle": "claim", + "surfaces": [ + "Option.Bind" + ], + "heldBy": [ + "OptionTests", + "OptionExtensionsTests" + ] + }, + { + "id": "O05-choose-is-lazy", + "source": "docs/option.md#combine-options", + "quote": "It runs lazily, like any LINQ operator.", + "claim": "It runs lazily, like any LINQ operator.", + "oracle": "claim", + "surfaces": [ + "Option.Choose", + "SequenceOptionExtensions.Choose" + ], + "heldBy": [ + "ChooseTests" + ] + }, + { + "id": "O05-choose-keeps-in-order", + "source": "docs/option.md#combine-options", + "quote": "It keeps, in order, the values the selector returns `Some` for, and drops the rest: a filter and a map in one.", + "claim": "It keeps, in order, the values the selector returns `Some` for, and drops the rest: a filter and a map in one.", + "oracle": "claim", + "surfaces": [ + "SequenceOptionExtensions.Choose" + ], + "heldBy": [ + "ChooseTests" + ] + }, + { + "id": "O05-choose-without-a-selector", + "source": "docs/option.md#combine-options", + "quote": "On a sequence of options, `Choose()` without a selector keeps the values they hold.", + "claim": "On a sequence of options, `Choose()` without a selector keeps the values they hold.", + "oracle": "claim", + "surfaces": [ + "Option.Choose" + ], + "heldBy": [ + "ChooseTests" + ] + }, + { + "id": "O05-each-from-sees-the-earlier", + "source": "docs/option.md#combine-options", + "quote": "Each `from` after the first can use the ones before it.", + "claim": "Each `from` after the first can use the ones before it.", + "oracle": "claim", + "surfaces": [ + "Option.SelectMany" + ], + "heldBy": [ + "OptionExtensionsTests" + ] + }, + { + "id": "O05-filter", + "source": "docs/option.md#combine-options", + "quote": "**`Filter`** turns a value that fails the predicate into `None`.", + "claim": "**`Filter`** turns a value that fails the predicate into `None`.", + "oracle": "claim", + "surfaces": [ + "Option.Filter" + ], + "heldBy": [ + "OptionTests" + ] + }, + { + "id": "O05-map", + "source": "docs/option.md#combine-options", + "quote": "**`Map`** changes the value, and a missing value stays missing.", + "claim": "**`Map`** changes the value, and a missing value stays missing.", + "oracle": "claim", + "surfaces": [ + "Option.Map" + ], + "heldBy": [ + "OptionTests" + ] + }, + { + "id": "O05-query-syntax", + "source": "docs/option.md#combine-options", + "quote": "`Select`, `SelectMany` and `Where` make query syntax work, as in the last example.", + "claim": "`Select`, `SelectMany` and `Where` make query syntax work, as in the last example.", + "oracle": "claim", + "surfaces": [ + "Option.Select", + "Option.SelectMany", + "Option.Where" + ], + "heldBy": [ + "OptionExtensionsTests" + ] + }, + { + "id": "O05-tap", + "source": "docs/option.md#combine-options", + "quote": "**`Tap`** runs an action on the value and hands the option on unchanged.", + "claim": "**`Tap`** runs an action on the value and hands the option on unchanged.", + "oracle": "claim", + "surfaces": [ + "Option.Tap" + ], + "heldBy": [ + "OptionTests" + ] + }, + { + "id": "O05-zip", + "source": "docs/option.md#combine-options", + "quote": "**`Zip`** combines two to six options into one value, when every one of them has a value.", + "claim": "**`Zip`** combines two to six options into one value, when every one of them has a value.", + "oracle": "claim", + "surfaces": [ + "Option.Zip" + ], + "heldBy": [ + "OptionExtensionsTests" + ] + }, + { + "id": "O06-a-finished-lookup-runs-at-once", + "source": "docs/option.md#continue-a-pending-lookup", + "quote": "If the lookup had already finished, the callback runs at once, on your synchronization context.", + "claim": "If the lookup had already finished, the callback runs at once, on your synchronization context.", + "oracle": "claim", + "surfaces": [ + "Option.MapAsync", + "Option.BindAsync", + "Option.FilterAsync", + "Option.TapAsync", + "Option.ToResultAsync" + ], + "heldBy": [ + "AsynchronousContextTests" + ] + }, + { + "id": "O06-a-pending-lookup-runs-without-the-context", + "source": "docs/option.md#continue-a-pending-lookup", + "quote": "If it had not, the callback runs without that context, usually on a thread-pool thread", + "claim": "If it had not, the callback runs without that context, usually on a thread-pool thread", + "oracle": "claim", + "surfaces": [ + "Option.MapAsync", + "Option.BindAsync", + "Option.FilterAsync", + "Option.TapAsync", + "Option.ToResultAsync" + ], + "heldBy": [ + "AsynchronousContextTests" + ] + }, + { + "id": "O06-continue-a-pending-option", + "source": "docs/option.md#continue-a-pending-lookup", + "quote": "`MapAsync`, `BindAsync`, `FilterAsync`, `TapAsync` and `ToResultAsync` continue it the way [Result's async chains](result.md#async-chains) continue a pending result.", + "claim": "`MapAsync`, `BindAsync`, `FilterAsync`, `TapAsync` and `ToResultAsync` continue it the way [Result's async chains](result.md#async-chains) continue a pending result.", + "oracle": "claim", + "surfaces": [ + "Option.MapAsync", + "Option.BindAsync", + "Option.FilterAsync", + "Option.TapAsync", + "Option.ToResultAsync" + ], + "heldBy": [ + "OptionAsyncTests" + ] + }, + { + "id": "O07-rejected-by-system-text-json", + "source": "docs/option.md#at-the-boundary", + "quote": "System.Text.Json rejects it in both directions", + "claim": "System.Text.Json rejects it in both directions", + "oracle": "exception", + "surfaces": [ + "NotWireTypeJsonConverterFactory.*" + ], + "heldBy": [ + "NotWireTypeTests" + ] + }, + { + "id": "O07-to-option-in-or-null-out", + "source": "docs/option.md#at-the-boundary", + "quote": "Use `ToOption()` on the way in and `OrNull()` on the way out.", + "claim": "Use `ToOption()` on the way in and `OrNull()` on the way out.", + "oracle": "claim", + "surfaces": [ + "Option.ToOption", + "Option.OrNull" + ], + "heldBy": [ + "OrNullTests" + ] + }, + { + "id": "O07-to-result", + "source": "docs/option.md#at-the-boundary", + "quote": "`ToResult(error)` turns the option into a [Result](result.md)", + "claim": "`ToResult(error)` turns the option into a [Result](result.md)", + "oracle": "claim", + "surfaces": [ + "Option.ToResult" + ], + "heldBy": [ + "OptionExtensionsTests" + ] + }, + { + "id": "O08-choose-reads-null-elements", + "source": "docs/option.md#the-surface", + "quote": "the elements `Choose(selector)` reads may be null, and only what the selector finds counts as a value.", + "claim": "the elements `Choose(selector)` reads may be null, and only what the selector finds counts as a value.", + "oracle": "claim", + "surfaces": [ + "SequenceOptionExtensions.Choose" + ], + "heldBy": [ + "ChooseTests" + ] + }, + { + "id": "O08-default-is-rejected-by-the-analyzer", + "source": "docs/option.md#the-surface", + "quote": "A `default(Option)` is `None`, but the analyzer still rejects writing it (CMTK0001).", + "claim": "A `default(Option)` is `None`, but the analyzer still rejects writing it (CMTK0001).", + "oracle": "claim", + "surfaces": [ + "Option..ctor" + ], + "heldBy": [ + "OptionTests", + "ForbiddenDefaultInitializationAnalyzerTests" + ] + }, + { + "id": "O08-equality", + "source": "docs/option.md#the-surface", + "quote": "Two options are equal when both are `None`, or when both hold equal values, as with any record struct.", + "claim": "Two options are equal when both are `None`, or when both hold equal values, as with any record struct.", + "oracle": "claim", + "surfaces": [ + "Option.Equals", + "Option.GetHashCode", + "Option.op_Equality", + "Option.op_Inequality" + ], + "heldBy": [ + "OptionTests" + ] + }, + { + "id": "O08-filter-async", + "source": "docs/option.md#the-surface", + "quote": "| `FilterAsync(predicate)` | `Task>` | `Filter`, once the option is known |", + "claim": "| `FilterAsync(predicate)` | `Task>` | `Filter`, once the option is known |", + "oracle": "claim", + "surfaces": [ + "Option.FilterAsync" + ], + "heldBy": [ + "OptionAsyncTests" + ] + }, + { + "id": "O08-implicit-from-none", + "source": "docs/option.md#the-surface", + "quote": "| implicit from `None` | `Option` | `None` |", + "claim": "| implicit from `None` | `Option` | `None` |", + "oracle": "claim", + "surfaces": [ + "Option.op_Implicit" + ], + "heldBy": [ + "OptionTests" + ] + }, + { + "id": "O08-index-on-a-query-does-not-compile", + "source": "docs/option.md#the-surface", + "quote": "an `Index` on a query is a compile error", + "claim": "an `Index` on a query is a compile error", + "oracle": "claim", + "surfaces": [ + "Option.ElementAtOrNone" + ], + "heldBy": [ + "QueryableOptionTests" + ] + }, + { + "id": "O08-map-refuses-null", + "source": "docs/option.md#the-surface", + "quote": "| `Map(selector)` | `Option` | `Option`; throws if the selector returns null |", + "claim": "| `Map(selector)` | `Option` | `Option`; throws if the selector returns null |", + "oracle": "exception", + "surfaces": [ + "Option.Map" + ], + "heldBy": [ + "OptionTests" + ] + }, + { + "id": "O08-none-marker-converts", + "source": "docs/option.md#the-surface", + "quote": "a `None` marker that converts to any `Option`", + "claim": "a `None` marker that converts to any `Option`", + "oracle": "claim", + "surfaces": [ + "Option.None", + "Option.op_Implicit" + ], + "heldBy": [ + "OptionTests" + ] + }, + { + "id": "O08-or-default-is-reported-for-a-value-object", + "source": "docs/option.md#the-surface", + "quote": "`T`, `default` for `None`; CMTK0009 for a value object, whose `default` is no value", + "claim": "`T`, `default` for `None`; CMTK0009 for a value object, whose `default` is no value", + "oracle": "claim", + "surfaces": [ + "Option.OrDefault" + ], + "heldBy": [ + "OptionExtensionsTests", + "DefaultProducingCallAnalyzerTests" + ] + }, + { + "id": "O08-single-or-none-asks-for-two-rows", + "source": "docs/option.md#the-surface", + "quote": "throws for more than one. A query is asked for two rows", + "claim": "throws for more than one. A query is asked for two rows", + "oracle": "claim", + "surfaces": [ + "Option.SingleOrNone" + ], + "heldBy": [ + "OptionLookupTests", + "QueryOptionTests" + ] + }, + { + "id": "O08-some-refuses-null", + "source": "docs/option.md#the-surface", + "quote": "`Option`; throws `ArgumentNullException` for null", + "claim": "`Option`; throws `ArgumentNullException` for null", + "oracle": "exception", + "surfaces": [ + "Option.Some" + ], + "heldBy": [ + "OptionTests" + ] + }, + { + "id": "O08-to-result-async", + "source": "docs/option.md#the-surface", + "quote": "`Task>`: a pending lookup into a `Result` pipeline", + "claim": "`Task>`: a pending lookup into a `Result` pipeline", + "oracle": "claim", + "surfaces": [ + "Option.ToResultAsync" + ], + "heldBy": [ + "OptionAsyncTests" + ] + }, + { + "id": "P01-a-value", + "source": "docs/merge-patches.md#tell-absent-null-and-a-value-apart", + "quote": "| a value | `Value` | `onValue` | true | with the value | with the value | `Some` |", + "claim": "| a value | `Value` | `onValue` | true | with the value | with the value | `Some` |", + "oracle": "claim", + "surfaces": [ + "Patch.State", + "Patch.Match", + "Patch.TryGetValue", + "Patch.Tap", + "Patch.IfPresent", + "Patch.ToOption" + ], + "heldBy": [ + "PatchTests", + "MergePatchesTests" + ] + }, + { + "id": "P01-default-is-absent", + "source": "docs/merge-patches.md#tell-absent-null-and-a-value-apart", + "quote": "**`default(Patch)` is `Absent`.** The serializer never touches a property the document leaves out, so it keeps the struct's default.", + "claim": "**`default(Patch)` is `Absent`.** The serializer never touches a property the document leaves out, so it keeps the struct's default.", + "oracle": "claim", + "surfaces": [ + "Patch.Absent", + "Patch.State" + ], + "heldBy": [ + "PatchTests", + "MergePatchesTests" + ] + }, + { + "id": "P01-left-alone-by-the-analyzers", + "source": "docs/merge-patches.md#tell-absent-null-and-a-value-apart", + "quote": "That's why the analyzers, which reject a `default` of every other struct in this package, leave a patch alone.", + "claim": "That's why the analyzers, which reject a `default` of every other struct in this package, leave a patch alone.", + "oracle": "claim", + "surfaces": [ + "Patch.Absent" + ], + "heldBy": [ + "PatchIsDefaultableTests" + ] + }, + { + "id": "P01-not-mentioned", + "source": "docs/merge-patches.md#tell-absent-null-and-a-value-apart", + "quote": "| not mentioned | `Absent` | `onAbsent` | false | no | no | `None` |", + "claim": "| not mentioned | `Absent` | `onAbsent` | false | no | no | `None` |", + "oracle": "claim", + "surfaces": [ + "Patch.State", + "Patch.Match", + "Patch.TryGetValue", + "Patch.Tap", + "Patch.IfPresent", + "Patch.ToOption" + ], + "heldBy": [ + "PatchTests", + "MergePatchesTests" + ] + }, + { + "id": "P01-null", + "source": "docs/merge-patches.md#tell-absent-null-and-a-value-apart", + "quote": "| `null` | `Null` | `onNull` | false | no | with `null` | `None` |", + "claim": "| `null` | `Null` | `onNull` | false | no | with `null` | `None` |", + "oracle": "claim", + "surfaces": [ + "Patch.State", + "Patch.Match", + "Patch.TryGetValue", + "Patch.Tap", + "Patch.IfPresent", + "Patch.ToOption" + ], + "heldBy": [ + "PatchTests", + "MergePatchesTests" + ] + }, + { + "id": "P01-reading-accepts-a-null", + "source": "docs/merge-patches.md#tell-absent-null-and-a-value-apart", + "quote": "**Reading always accepts a `null`, whatever `T` is.**", + "claim": "**Reading always accepts a `null`, whatever `T` is.**", + "oracle": "claim", + "surfaces": [ + "Patch.Null", + "PatchJsonConverterFactory.*" + ], + "heldBy": [ + "MergePatchesTests", + "PatchTests" + ] + }, + { + "id": "P01-to-string-prints-the-state", + "source": "docs/merge-patches.md#tell-absent-null-and-a-value-apart", + "quote": "**`ToString()` prints the state**, never the value.", + "claim": "**`ToString()` prints the state**, never the value.", + "oracle": "claim", + "surfaces": [ + "Patch.ToString", + "Patch.Equals", + "Patch.GetHashCode", + "Patch.op_Equality", + "Patch.op_Inequality" + ], + "heldBy": [ + "PatchTests" + ] + }, + { + "id": "P02-a-struct-s-clear-is-null", + "source": "docs/merge-patches.md#apply-a-patch", + "quote": "A field of a value type receives the clear as `null`, never as `0`.", + "claim": "A field of a value type receives the clear as `null`, never as `0`.", + "oracle": "claim", + "surfaces": [ + "Patch.IfPresent" + ], + "heldBy": [ + "PatchTests", + "MergePatchesTests" + ] + }, + { + "id": "P02-if-present", + "source": "docs/merge-patches.md#apply-a-patch", + "quote": "**`IfPresent`** runs for a value and for a clear.", + "claim": "**`IfPresent`** runs for a value and for a clear.", + "oracle": "claim", + "surfaces": [ + "Patch.IfPresent" + ], + "heldBy": [ + "PatchTests" + ] + }, + { + "id": "P02-match", + "source": "docs/merge-patches.md#apply-a-patch", + "quote": "**`Match`** takes all three, for a rule that has to say what each one means.", + "claim": "**`Match`** takes all three, for a rule that has to say what each one means.", + "oracle": "claim", + "surfaces": [ + "Patch.Match" + ], + "heldBy": [ + "PatchTests" + ] + }, + { + "id": "P02-tap", + "source": "docs/merge-patches.md#apply-a-patch", + "quote": "**`Tap`** runs only for a value, as `Option`'s `Tap` does.", + "claim": "**`Tap`** runs only for a value, as `Option`'s `Tap` does.", + "oracle": "claim", + "surfaces": [ + "Patch.Tap" + ], + "heldBy": [ + "PatchTests" + ] + }, + { + "id": "P02-tap-and-if-present-return-the-patch", + "source": "docs/merge-patches.md#apply-a-patch", + "quote": "`Tap` and `IfPresent` both return the patch unchanged.", + "claim": "`Tap` and `IfPresent` both return the patch unchanged.", + "oracle": "claim", + "surfaces": [ + "Patch.Tap", + "Patch.IfPresent" + ], + "heldBy": [ + "PatchTests" + ] + }, + { + "id": "P02-to-option", + "source": "docs/merge-patches.md#apply-a-patch", + "quote": "**`ToOption()`** is for code that treats a clear and an absence the same.", + "claim": "**`ToOption()`** is for code that treats a clear and an absence the same.", + "oracle": "claim", + "surfaces": [ + "Patch.ToOption" + ], + "heldBy": [ + "PatchTests" + ] + }, + { + "id": "P03-a-null-is-no-error", + "source": "docs/merge-patches.md#a-value-object-inside-a-patch", + "quote": "**A `null` isn't an error.** It's the `Null` state", + "claim": "**A `null` isn't an error.** It's the `Null` state", + "oracle": "claim", + "surfaces": [ + "Patch.Null", + "PatchJsonConverterFactory.*" + ], + "heldBy": [ + "MergePatchesTests" + ] + }, + { + "id": "P03-a-refused-patch-is-a-400", + "source": "docs/merge-patches.md#a-value-object-inside-a-patch", + "quote": "In an endpoint, the client gets the same 400 as for any body a value object rejects, with problem details where the host adds `UseStatusCodePages()`", + "claim": "In an endpoint, the client gets the same 400 as for any body a value object rejects, with problem details where the host adds `UseStatusCodePages()`", + "oracle": "claim", + "surfaces": [ + "PatchJsonConverterFactory.*" + ], + "heldBy": [ + "HostConfigurationTests" + ] + }, + { + "id": "P03-not-null-warning", + "source": "docs/merge-patches.md#a-value-object-inside-a-patch", + "quote": "`Patch` or `Patch` gets a warning at the declaration (CS8714).", + "claim": "`Patch` or `Patch` gets a warning at the declaration (CS8714).", + "oracle": "claim", + "surfaces": [ + "Patch.Value" + ], + "heldBy": [ + "PatchNullabilityTests" + ] + }, + { + "id": "P03-not-quoted", + "source": "docs/merge-patches.md#a-value-object-inside-a-patch", + "quote": "A value `Create` rejects is rejected here as anywhere else, and the error doesn't quote it.", + "claim": "A value `Create` rejects is rejected here as anywhere else, and the error doesn't quote it.", + "oracle": "exception", + "surfaces": [ + "PatchJsonConverterFactory.*" + ], + "heldBy": [ + "MergePatchesTests", + "SourceGeneratedJsonTests" + ] + }, + { + "id": "P03-the-field-s-path", + "source": "docs/merge-patches.md#a-value-object-inside-a-patch", + "quote": "The exception's `Path` is the field's, `$.email`, as it is for any property.", + "claim": "The exception's `Path` is the field's, `$.email`, as it is for any property.", + "oracle": "claim", + "surfaces": [ + "PatchJsonConverterFactory.*" + ], + "heldBy": [ + "MergePatchesTests", + "SourceGeneratedJsonTests" + ] + }, + { + "id": "P03-the-value-object-s-converter", + "source": "docs/merge-patches.md#a-value-object-inside-a-patch", + "quote": "A patch reads its value with the value object's own converter, so the value object keeps its rule.", + "claim": "A patch reads its value with the value object's own converter, so the value object keeps its rule.", + "oracle": "claim", + "surfaces": [ + "PatchJsonConverterFactory.*" + ], + "heldBy": [ + "MergePatchesTests", + "SourceGeneratedJsonTests" + ] + }, + { + "id": "P04-a-clear-or-an-absence-passes", + "source": "docs/merge-patches.md#validate-a-patch", + "quote": "**A clear or an absent field** passes through, since there's nothing to validate.", + "claim": "**A clear or an absent field** passes through, since there's nothing to validate.", + "oracle": "claim", + "surfaces": [ + "Result.Traverse" + ], + "heldBy": [ + "PatchCombinationTests", + "MergePatchesTests" + ] + }, + { + "id": "P04-a-value-is-validated", + "source": "docs/merge-patches.md#validate-a-patch", + "quote": "**A value** is validated, and a failure gives `Create`'s own error.", + "claim": "**A value** is validated, and a failure gives `Create`'s own error.", + "oracle": "claim", + "surfaces": [ + "Result.Traverse" + ], + "heldBy": [ + "PatchCombinationTests", + "MergePatchesTests" + ] + }, + { + "id": "P04-map", + "source": "docs/merge-patches.md#validate-a-patch", + "quote": "`Map` changes the value the same way, and keeps a clear and an absence as they are.", + "claim": "`Map` changes the value the same way, and keeps a clear and an absence as they are.", + "oracle": "claim", + "surfaces": [ + "Patch.Map" + ], + "heldBy": [ + "PatchCombinationTests", + "MergePatchesTests" + ] + }, + { + "id": "P05-an-absent-patch-is-not-written", + "source": "docs/merge-patches.md#write-a-patch", + "quote": "So writing an absent patch throws a `NotSupportedException`, unless the property says to skip a default, as above.", + "claim": "So writing an absent patch throws a `NotSupportedException`, unless the property says to skip a default, as above.", + "oracle": "exception", + "surfaces": [ + "PatchJsonConverterFactory.*", + "Patch.Absent" + ], + "heldBy": [ + "MergePatchesTests" + ] + }, + { + "id": "P05-value-of-null-is-a-clear", + "source": "docs/merge-patches.md#write-a-patch", + "quote": "`Patch.Value(null)` is the `Null` state, so a client that holds a nullable value can pass it as it is.", + "claim": "`Patch.Value(null)` is the `Null` state, so a client that holds a nullable value can pass it as it is.", + "oracle": "claim", + "surfaces": [ + "Patch.Value", + "Patch.Null" + ], + "heldBy": [ + "PatchTests" + ] + }, + { + "id": "P06-absent-when-left-out", + "source": "docs/merge-patches.md#use-it-in-an-endpoint-and-in-the-document", + "quote": "Each property of `ContactPatch` is read by its patch, and one the request leaves out is `Absent`.", + "claim": "Each property of `ContactPatch` is read by its patch, and one the request leaves out is `Absent`.", + "oracle": "claim", + "surfaces": [ + "PatchJsonConverterFactory.*", + "Patch.State" + ], + "heldBy": [ + "PatchSchemaTests", + "MergePatchesTests" + ] + }, + { + "id": "P06-admitting-null", + "source": "docs/merge-patches.md#use-it-in-an-endpoint-and-in-the-document", + "quote": "So a validator accepts null, and judges every other value as it judges the argument's.", + "claim": "So a validator accepts null, and judges every other value as it judges the argument's.", + "oracle": "claim", + "surfaces": [ + "TypeKitOpenApiOptionsExtensions.AddTypeKit" + ], + "heldBy": [ + "PatchSchemaTests" + ] + }, + { + "id": "P06-inlined", + "source": "docs/merge-patches.md#use-it-in-an-endpoint-and-in-the-document", + "quote": "**Inlined**, rather than as a component of its own.", + "claim": "**Inlined**, rather than as a component of its own.", + "oracle": "claim", + "surfaces": [ + "TypeKitOpenApiOptionsExtensions.AddTypeKit" + ], + "heldBy": [ + "PatchSchemaTests" + ] + }, + { + "id": "P06-through-the-underlying-type", + "source": "docs/merge-patches.md#use-it-in-an-endpoint-and-in-the-document", + "quote": "A value object inside a patch is described through its underlying type, so `email` is a `string`.", + "claim": "A value object inside a patch is described through its underlying type, so `email` is a `string`.", + "oracle": "claim", + "surfaces": [ + "TypeKitOpenApiOptionsExtensions.AddTypeKit" + ], + "heldBy": [ + "PatchSchemaTests", + "MergePatchesTests" + ] + }, + { + "id": "P07-default-breaks-the-document", + "source": "docs/merge-patches.md#required-constructor-parameters", + "quote": "But System.Text.Json's schema exporter, which ASP.NET Core builds the document with, throws on a parameter of a struct type whose default is `default`. So the endpoint's document fails.", + "claim": "But System.Text.Json's schema exporter, which ASP.NET Core builds the document with, throws on a parameter of a struct type whose default is `default`. So the endpoint's document fails.", + "oracle": "exception", + "surfaces": [ + "TypeKitOpenApiOptionsExtensions.AddTypeKit" + ], + "heldBy": [ + "PatchSchemaTests" + ] + }, + { + "id": "P07-required-too", + "source": "docs/merge-patches.md#required-constructor-parameters", + "quote": "It makes every constructor parameter without a default required, a patch's too.", + "claim": "It makes every constructor parameter without a default required, a patch's too.", + "oracle": "claim", + "surfaces": [ + "PatchJsonConverterFactory.*" + ], + "heldBy": [ + "MergePatchesTests", + "PatchSchemaTests" + ] + }, + { + "id": "P08-a-built-in-type", + "source": "docs/merge-patches.md#source-generated-contexts-and-native-aot", + "quote": "**A primitive, a `string`, a `Guid`, a date or a `Uri`** is read with the serializer's built-in converter.", + "claim": "**A primitive, a `string`, a `Guid`, a date or a `Uri`** is read with the serializer's built-in converter.", + "oracle": "claim", + "surfaces": [ + "PatchJsonConverterFactory.*" + ], + "heldBy": [ + "PatchTests" + ] + }, + { + "id": "P08-a-value-object", + "source": "docs/merge-patches.md#source-generated-contexts-and-native-aot", + "quote": "**A value object** is read with its own converter.", + "claim": "**A value object** is read with its own converter.", + "oracle": "claim", + "surfaces": [ + "PatchJsonConverterFactory.*" + ], + "heldBy": [ + "SourceGeneratedJsonTests" + ] + }, + { + "id": "P08-any-other-type-is-listed", + "source": "docs/merge-patches.md#source-generated-contexts-and-native-aot", + "quote": "**Any other type, a class or an enum,** must be listed in the context. Otherwise the serializer throws with its own message naming the type.", + "claim": "**Any other type, a class or an enum,** must be listed in the context. Otherwise the serializer throws with its own message naming the type.", + "oracle": "exception", + "surfaces": [ + "PatchJsonConverterFactory.*" + ], + "heldBy": [ + "PatchTests", + "MergePatchesTests" + ] + }, + { + "id": "Q01-delegate-condition-reported", + "source": "src/CodoMetis.TypeKit.Analyzers/README.md#a-condition-that-reads-every-row", + "quote": "The rule reports that call, and its message names the overload it fell to.", + "claim": "The rule reports that call, and its message names the overload it fell to.", + "oracle": "claim", + "surfaces": [ + "Option.FirstOrNone", + "Option.SingleOrNone", + "Option.LastOrNone", + "Result.FirstOrError", + "Result.LastOrError" + ], + "heldBy": [ + "QueryDelegateConditionTests" + ] + }, + { + "id": "Q02-last-forms-too", + "source": "src/CodoMetis.TypeKit.Analyzers/README.md#a-condition-that-reads-every-row", + "quote": "`LastOrNone` and `LastOrError` are reported as well.", + "claim": "`LastOrNone` and `LastOrError` are reported as well.", + "oracle": "claim", + "surfaces": [ + "Option.FirstOrNone", + "Option.SingleOrNone", + "Option.LastOrNone", + "Result.FirstOrError", + "Result.LastOrError" + ], + "heldBy": [ + "QueryDelegateConditionTests" + ] + }, + { + "id": "Q03-as-enumerable-silent", + "source": "src/CodoMetis.TypeKit.Analyzers/README.md#a-condition-that-reads-every-row", + "quote": "To read every row on purpose, call `AsEnumerable()` first, and the rule stays silent.", + "claim": "To read every row on purpose, call `AsEnumerable()` first, and the rule stays silent.", + "oracle": "claim", + "surfaces": [ + "Option.FirstOrNone", + "Option.SingleOrNone", + "Option.LastOrNone", + "Result.FirstOrError", + "Result.LastOrError" + ], + "heldBy": [ + "QueryDelegateConditionTests" + ] + }, + { + "id": "Q04-sequence-held-query-silent", + "source": "src/CodoMetis.TypeKit.Analyzers/README.md#a-condition-that-reads-every-row", + "quote": "A query held as an `IEnumerable` is a sequence to the rule.", + "claim": "A query held as an `IEnumerable` is a sequence to the rule.", + "oracle": "claim", + "surfaces": [ + "Option.FirstOrNone", + "Option.SingleOrNone", + "Option.LastOrNone", + "Result.FirstOrError", + "Result.LastOrError" + ], + "heldBy": [ + "QueryDelegateConditionTests" + ] + }, + { + "id": "Q05-ef-readme-warns", + "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#an-option-for-nothing-found", + "quote": "Then every row is read and the condition runs in memory, and the analyzer warns at the call", + "claim": "Then every row is read and the condition runs in memory, and the analyzer warns at the call", + "oracle": "claim", + "surfaces": [ + "Option.FirstOrNone", + "Option.SingleOrNone", + "Option.LastOrNone", + "Result.FirstOrError", + "Result.LastOrError" + ], + "heldBy": [ + "QueryDelegateConditionTests" + ] + }, + { + "id": "R01-a-bare-error-converts-to-a-command", + "source": "docs/result.md#pick-a-shape", + "quote": "A `Result` has no value to confuse it with, so there a bare error converts on its own, and so does the `Result.Success()` marker.", + "claim": "A `Result` has no value to confuse it with, so there a bare error converts on its own, and so does the `Result.Success()` marker.", + "oracle": "claim", + "surfaces": [ + "Result.op_Implicit", + "Result.Success" + ], + "heldBy": [ + "ResultTests" + ] + }, + { + "id": "R01-a-bare-value-is-a-success", + "source": "docs/result.md#pick-a-shape", + "quote": "Return a bare value for a success. Wrap an error in `Result.Error(...)`, because in a `Result` a bare `5` could be either.", + "claim": "Return a bare value for a success. Wrap an error in `Result.Error(...)`, because in a `Result` a bare `5` could be either.", + "oracle": "claim", + "surfaces": [ + "Result.op_Implicit", + "Result.Error" + ], + "heldBy": [ + "ResultWithValueTests" + ] + }, + { + "id": "R01-to-http-result-accepts-four", + "source": "docs/result.md#pick-a-shape", + "quote": "`ToHttpResult()` accepts the first four.", + "claim": "`ToHttpResult()` accepts the first four.", + "oracle": "claim", + "surfaces": [ + "TypeKitHttpResultExtensions.ToHttpResult", + "TypeKitClosedSetHttpResultExtensions.ToHttpResult" + ], + "heldBy": [ + "FaultResponsePartsTests", + "ComposedFaultResponseTests" + ] + }, + { + "id": "R02-a-default-result-throws", + "source": "docs/result.md#get-the-outcome-out", + "quote": "Every branching member throws on it, rather than inventing a `default(TError)`.", + "claim": "Every branching member throws on it, rather than inventing a `default(TError)`.", + "oracle": "exception", + "surfaces": [ + "Result..ctor", + "Result.Match" + ], + "heldBy": [ + "UninitializedResultTests" + ] + }, + { + "id": "R02-match", + "source": "docs/result.md#get-the-outcome-out", + "quote": "`Match` takes one branch for a success and one for an error, and returns what they return.", + "claim": "`Match` takes one branch for a success and one for an error, and returns what they return.", + "oracle": "claim", + "surfaces": [ + "Result.Match" + ], + "heldBy": [ + "ResultTests", + "ResultWithValueTests" + ] + }, + { + "id": "R02-only-a-command-converts-to-bool", + "source": "docs/result.md#get-the-outcome-out", + "quote": "Only `Result` converts to `bool`, so `if (checkout.Charge(total))` reads as it should. A result with a value doesn't convert.", + "claim": "Only `Result` converts to `bool`, so `if (checkout.Charge(total))` reads as it should. A result with a value doesn't convert.", + "oracle": "claim", + "surfaces": [ + "Result.op_Implicit" + ], + "heldBy": [ + "ResultWithValueTests", + "ResultTests" + ] + }, + { + "id": "R02-state", + "source": "docs/result.md#get-the-outcome-out", + "quote": "`State` tells the three states apart without throwing: `Success`, `Error`, and `Uninitialized`.", + "claim": "`State` tells the three states apart without throwing: `Success`, `Error`, and `Uninitialized`.", + "oracle": "claim", + "surfaces": [ + "Result.State" + ], + "heldBy": [ + "UninitializedResultTests" + ] + }, + { + "id": "R02-the-analyzer-reports-defaults", + "source": "docs/result.md#get-the-outcome-out", + "quote": "It reports an array of results created with a length (CMTK0005), and a `default` written in your code (CMTK0001).", + "claim": "It reports an array of results created with a length (CMTK0005), and a `default` written in your code (CMTK0001).", + "oracle": "claim", + "surfaces": [ + "Result..ctor" + ], + "heldBy": [ + "DefaultFilledCollectionAnalyzerTests", + "ForbiddenDefaultInitializationAnalyzerTests" + ] + }, + { + "id": "R02-try-get-value", + "source": "docs/result.md#get-the-outcome-out", + "quote": "`value` is non-null when it returns true, and `error` is non-null when it returns false.", + "claim": "`value` is non-null when it returns true, and `error` is non-null when it returns false.", + "oracle": "claim", + "surfaces": [ + "Result.TryGetValue" + ], + "heldBy": [ + "ResultWithValueTests" + ] + }, + { + "id": "R03-on-a-success", + "source": "docs/result.md#chain-steps", + "quote": "**On a success:** `Map`, `Bind`, `Tap` and `Ensure`. An error passes through them.", + "claim": "**On a success:** `Map`, `Bind`, `Tap` and `Ensure`. An error passes through them.", + "oracle": "claim", + "surfaces": [ + "Result.Map", + "Result.Bind", + "Result.Tap", + "Result.Ensure" + ], + "heldBy": [ + "ResultTests", + "ResultWithValueTests" + ] + }, + { + "id": "R03-on-an-error", + "source": "docs/result.md#chain-steps", + "quote": "**On an error:** `MapError`, `TapError` and `Recover`. A success passes through them.", + "claim": "**On an error:** `MapError`, `TapError` and `Recover`. A success passes through them.", + "oracle": "claim", + "surfaces": [ + "Result.MapError", + "Result.TapError", + "Result.Recover" + ], + "heldBy": [ + "ResultTests", + "ResultWithValueTests" + ] + }, + { + "id": "R03-recover-is-a-second-chance", + "source": "docs/result.md#chain-steps", + "quote": "Its selector returns a success or another error, which gives the operation a second chance", + "claim": "Its selector returns a success or another error, which gives the operation a second chance", + "oracle": "claim", + "surfaces": [ + "Result.Recover" + ], + "heldBy": [ + "ResultTests", + "ResultWithValueTests" + ] + }, + { + "id": "R03-the-first-error-stops-the-chain", + "source": "docs/result.md#chain-steps", + "quote": "The first error stops the chain, and every step after it is skipped.", + "claim": "The first error stops the chain, and every step after it is skipped.", + "oracle": "claim", + "surfaces": [ + "Result.Bind", + "Result.Map", + "Result.Ensure" + ], + "heldBy": [ + "ResultWithValueTests", + "ResultContinuationTests" + ] + }, + { + "id": "R04-a-callback-s-exception-comes-out", + "source": "docs/result.md#async-chains", + "quote": "So whatever a callback throws comes out of the chain's `await`.", + "claim": "So whatever a callback throws comes out of the chain's `await`.", + "oracle": "exception", + "surfaces": [ + "Result.BindAsync", + "Result.TapErrorAsync" + ], + "heldBy": [ + "ResultContinuationTests" + ] + }, + { + "id": "R04-a-value-task-does-not-wait", + "source": "docs/result.md#async-chains", + "quote": "Otherwise `TapAsync` and `TapErrorAsync` bind to their synchronous overload and don't wait, and `MapAsync` keeps the `ValueTask` itself as the value.", + "claim": "Otherwise `TapAsync` and `TapErrorAsync` bind to their synchronous overload and don't wait, and `MapAsync` keeps the `ValueTask` itself as the value.", + "oracle": "silent-default", + "surfaces": [ + "Result.TapAsync", + "Result.TapErrorAsync", + "Result.MapAsync" + ], + "heldBy": [ + "ResultContinuationTests" + ] + }, + { + "id": "R04-awaited-once", + "source": "docs/result.md#async-chains", + "quote": "The asynchronous members continue a `Task>` without an `await` at each step.", + "claim": "The asynchronous members continue a `Task>` without an `await` at each step.", + "oracle": "claim", + "surfaces": [ + "Result.MapAsync", + "Result.BindAsync", + "Result.EnsureAsync", + "Result.TapErrorAsync" + ], + "heldBy": [ + "ResultContinuationTests" + ] + }, + { + "id": "R05-a-finished-task-runs-at-once", + "source": "docs/result.md#where-a-callback-runs", + "quote": "**The task had already finished.** The callback runs at once, on your thread and context.", + "claim": "**The task had already finished.** The callback runs at once, on your thread and context.", + "oracle": "claim", + "surfaces": [ + "Result.MapAsync" + ], + "heldBy": [ + "AsynchronousContextTests" + ] + }, + { + "id": "R05-a-running-task-runs-elsewhere", + "source": "docs/result.md#where-a-callback-runs", + "quote": "**The task was still running.** The callback runs wherever the task finishes, usually on a thread-pool thread, without your context.", + "claim": "**The task was still running.** The callback runs wherever the task finishes, usually on a thread-pool thread, without your context.", + "oracle": "claim", + "surfaces": [ + "Result.MapAsync" + ], + "heldBy": [ + "AsynchronousContextTests" + ] + }, + { + "id": "R05-configure-await-false", + "source": "docs/result.md#where-a-callback-runs", + "quote": "Every `await` inside the package uses `ConfigureAwait(false)`. Nothing resumes on your `SynchronizationContext`.", + "claim": "Every `await` inside the package uses `ConfigureAwait(false)`. Nothing resumes on your `SynchronizationContext`.", + "oracle": "claim", + "surfaces": [ + "Result.MapAsync", + "Result.BindAsync", + "Result.TraverseAsync" + ], + "heldBy": [ + "AsynchronousContextTests" + ] + }, + { + "id": "R05-every-continuation", + "source": "docs/result.md#where-a-callback-runs", + "quote": "That holds for every continuation of a pending result, for `TraverseAsync` after its first operation, and for every step after one that waited.", + "claim": "That holds for every continuation of a pending result, for `TraverseAsync` after its first operation, and for every step after one that waited.", + "oracle": "claim", + "surfaces": [ + "Result.TraverseAsync", + "Result.MapAsync" + ], + "heldBy": [ + "AsynchronousContextTests" + ] + }, + { + "id": "R05-no-deadlock", + "source": "docs/result.md#where-a-callback-runs", + "quote": "If you block on a chain with `.Result` or `.Wait()`, the package's own awaits don't deadlock you.", + "claim": "If you block on a chain with `.Result` or `.Wait()`, the package's own awaits don't deadlock you.", + "oracle": "claim", + "surfaces": [ + "Result.MapAsync", + "Result.BindAsync" + ], + "heldBy": [ + "AsynchronousContextTests" + ] + }, + { + "id": "R06-a-cancellation-propagates", + "source": "docs/result.md#collect-many-results", + "quote": "A cancellation propagates as an exception and doesn't become an error.", + "claim": "A cancellation propagates as an exception and doesn't become an error.", + "oracle": "exception", + "surfaces": [ + "Result.TraverseAsync" + ], + "heldBy": [ + "ResultTraverseAsyncTests", + "OptionPatchTraverseAsyncTests" + ] + }, + { + "id": "R06-none-calls-nothing", + "source": "docs/result.md#collect-many-results", + "quote": "`None`, a clear and an absent field call nothing and pass through.", + "claim": "`None`, a clear and an absent field call nothing and pass through.", + "oracle": "claim", + "surfaces": [ + "Result.TraverseAsync" + ], + "heldBy": [ + "OptionPatchTraverseAsyncTests" + ] + }, + { + "id": "R06-one-at-a-time", + "source": "docs/result.md#collect-many-results", + "quote": "It awaits each operation before it starts the next, so nothing starts after a fault.", + "claim": "It awaits each operation before it starts the next, so nothing starts after a fault.", + "oracle": "claim", + "surfaces": [ + "Result.TraverseAsync" + ], + "heldBy": [ + "ResultTraverseAsyncTests" + ] + }, + { + "id": "R06-option-or-patch-once", + "source": "docs/result.md#collect-many-results", + "quote": "On an `Option` or a `Patch`, `TraverseAsync` runs the operation once, when there is a value.", + "claim": "On an `Option` or a `Patch`, `TraverseAsync` runs the operation once, when there is a value.", + "oracle": "claim", + "surfaces": [ + "Result.TraverseAsync" + ], + "heldBy": [ + "OptionPatchTraverseAsyncTests" + ] + }, + { + "id": "R06-stop-at-the-first-error", + "source": "docs/result.md#collect-many-results", + "quote": "`Traverse`, `Sequence` and `Zip` (two to six results) stop at the first error, in the order of the sequence or the arguments.", + "claim": "`Traverse`, `Sequence` and `Zip` (two to six results) stop at the first error, in the order of the sequence or the arguments.", + "oracle": "claim", + "surfaces": [ + "Result.Traverse", + "Result.Sequence", + "Result.Zip" + ], + "heldBy": [ + "ResultCombinationTests" + ] + }, + { + "id": "R06-the-token", + "source": "docs/result.md#collect-many-results", + "quote": "Give it a cancellation token, and it checks the token before each operation and hands it on.", + "claim": "Give it a cancellation token, and it checks the token before each operation and hands it on.", + "oracle": "claim", + "surfaces": [ + "Result.TraverseAsync" + ], + "heldBy": [ + "ResultTraverseAsyncTests", + "OptionPatchTraverseAsyncTests" + ] + }, + { + "id": "R06-traverse-calls-nothing-after-an-error", + "source": "docs/result.md#collect-many-results", + "quote": "`Traverse` doesn't call its selector for anything after that error.", + "claim": "`Traverse` doesn't call its selector for anything after that error.", + "oracle": "claim", + "surfaces": [ + "Result.Traverse" + ], + "heldBy": [ + "ResultCombinationTests" + ] + }, + { + "id": "R07-a-cancellation-is-not-an-outcome", + "source": "docs/result.md#an-exception-as-an-outcome", + "quote": "An `OperationCanceledException` passes through every overload. So does an `AggregateException` that holds only cancellations, as `Wait()` and `.Result` throw for a cancelled task.", + "claim": "An `OperationCanceledException` passes through every overload. So does an `AggregateException` that holds only cancellations, as `Wait()` and `.Result` throw for a cancelled task.", + "oracle": "exception", + "surfaces": [ + "Result.Catch", + "Result.CatchAsync" + ], + "heldBy": [ + "ResultCatchTests", + "CatchAsyncValueTaskTests" + ] + }, + { + "id": "R07-a-timeout-is-an-error", + "source": "docs/result.md#an-exception-as-an-outcome", + "quote": "A cancellation over a `TimeoutException` is the error, wrapped or not. That is how `HttpClient` reports that its `Timeout` elapsed and the call failed. `.Result` on such a call gives the error too.", + "claim": "A cancellation over a `TimeoutException` is the error, wrapped or not. That is how `HttpClient` reports that its `Timeout` elapsed and the call failed. `.Result` on such a call gives the error too.", + "oracle": "claim", + "surfaces": [ + "Result.Catch", + "Result.CatchAsync" + ], + "heldBy": [ + "ResultCatchTests", + "CatchAsyncValueTaskTests" + ] + }, + { + "id": "R07-a-value-task-alike", + "source": "docs/result.md#an-exception-as-an-outcome", + "quote": "A `ValueTask` works the same way", + "claim": "A `ValueTask` works the same way", + "oracle": "claim", + "surfaces": [ + "Result.CatchAsync" + ], + "heldBy": [ + "CatchAsyncValueTaskTests" + ] + }, + { + "id": "R07-an-aggregate-of-anything-else", + "source": "docs/result.md#an-exception-as-an-outcome", + "quote": "An `AggregateException` that holds anything else is the error, as it was thrown.", + "claim": "An `AggregateException` that holds anything else is the error, as it was thrown.", + "oracle": "claim", + "surfaces": [ + "Result.Catch" + ], + "heldBy": [ + "ResultCatchTests" + ] + }, + { + "id": "R07-catch", + "source": "docs/result.md#an-exception-as-an-outcome", + "quote": "`Result.Catch` runs the call and gives you its value, or the exception as the error", + "claim": "`Result.Catch` runs the call and gives you its value, or the exception as the error", + "oracle": "claim", + "surfaces": [ + "Result.Catch" + ], + "heldBy": [ + "ResultCatchTests" + ] + }, + { + "id": "R07-catch-async-awaits", + "source": "docs/result.md#an-exception-as-an-outcome", + "quote": "It awaits the task, so an exception thrown after the task's first `await` is the error too.", + "claim": "It awaits the task, so an exception thrown after the task's first `await` is the error too.", + "oracle": "claim", + "surfaces": [ + "Result.CatchAsync" + ], + "heldBy": [ + "ResultCatchTests", + "CatchAsyncValueTaskTests" + ] + }, + { + "id": "R07-catch-over-a-task-does-not-compile", + "source": "docs/result.md#an-exception-as-an-outcome", + "quote": "Passing a delegate that returns a `Task` to `Catch` fails to compile, and the error names `CatchAsync`.", + "claim": "Passing a delegate that returns a `Task` to `Catch` fails to compile, and the error names `CatchAsync`.", + "oracle": "claim", + "surfaces": [ + "Result.Catch" + ], + "heldBy": [ + "ResultCatchTests" + ] + }, + { + "id": "R07-only-the-first-of-several", + "source": "docs/result.md#an-exception-as-an-outcome", + "quote": "A task that faulted with several exceptions throws only the first of them.", + "claim": "A task that faulted with several exceptions throws only the first of them.", + "oracle": "claim", + "surfaces": [ + "Result.CatchAsync" + ], + "heldBy": [ + "ResultCatchTests", + "ResultTests" + ] + }, + { + "id": "R07-the-other-members-do-not-catch", + "source": "docs/result.md#an-exception-as-an-outcome", + "quote": "`Map`, `Bind` and the rest are unchanged: a selector that throws propagates.", + "claim": "`Map`, `Bind` and the rest are unchanged: a selector that throws propagates.", + "oracle": "exception", + "surfaces": [ + "Result.Map", + "Result.Bind" + ], + "heldBy": [ + "ResultCatchTests" + ] + }, + { + "id": "R07-to-http-result-refuses-an-exception", + "source": "docs/result.md#an-exception-as-an-outcome", + "quote": "`ToHttpResult()` doesn't accept a `Result`", + "claim": "`ToHttpResult()` doesn't accept a `Result`", + "oracle": "claim", + "surfaces": [ + "TypeKitHttpResultExtensions.ToHttpResult", + "TypeKitClosedSetHttpResultExtensions.ToHttpResult" + ], + "heldBy": [ + "FaultResponsePartsTests" + ] + }, + { + "id": "R08-no-where", + "source": "docs/result.md#use-query-syntax", + "quote": "There is no `where`: a result has no error to give a value that is filtered out.", + "claim": "There is no `where`: a result has no error to give a value that is filtered out.", + "oracle": "claim", + "surfaces": [ + "Result.Select", + "Result.SelectMany" + ], + "heldBy": [ + "ResultWithValueTests" + ] + }, + { + "id": "R08-select-and-select-many", + "source": "docs/result.md#use-query-syntax", + "quote": "`Select` and `SelectMany` make query syntax work.", + "claim": "`Select` and `SelectMany` make query syntax work.", + "oracle": "claim", + "surfaces": [ + "Result.Select", + "Result.SelectMany" + ], + "heldBy": [ + "ResultWithValueTests" + ] + }, + { + "id": "R09-a-bare-error-is-not-a-result", + "source": "docs/result.md#the-surface", + "quote": "a bare value is a success; a bare error is not, since it could be either", + "claim": "a bare value is a success; a bare error is not, since it could be either", + "oracle": "claim", + "surfaces": [ + "Result.op_Implicit" + ], + "heldBy": [ + "ResultWithValueTests" + ] + }, + { + "id": "R09-as-enumerable", + "source": "docs/result.md#the-surface", + "quote": "| `AsEnumerable()` | `Result` | zero or one element |", + "claim": "| `AsEnumerable()` | `Result` | zero or one element |", + "oracle": "claim", + "surfaces": [ + "Result.AsEnumerable" + ], + "heldBy": [ + "ResultWithValueTests" + ] + }, + { + "id": "R09-bind", + "source": "docs/result.md#the-surface", + "quote": "the result the selector returns on a success; a valued one can bind to a command", + "claim": "the result the selector returns on a success; a valued one can bind to a command", + "oracle": "claim", + "surfaces": [ + "Result.Bind" + ], + "heldBy": [ + "ResultTests", + "ResultWithValueTests" + ] + }, + { + "id": "R09-catch-async", + "source": "docs/result.md#the-surface", + "quote": "the same, awaiting the `Task` or `ValueTask`", + "claim": "the same, awaiting the `Task` or `ValueTask`", + "oracle": "claim", + "surfaces": [ + "Result.CatchAsync" + ], + "heldBy": [ + "ResultCatchTests", + "CatchAsyncValueTaskTests" + ] + }, + { + "id": "R09-catch-narrowed", + "source": "docs/result.md#the-surface", + "quote": "`Result` or `Result`; narrowed to one exception type", + "claim": "`Result` or `Result`; narrowed to one exception type", + "oracle": "claim", + "surfaces": [ + "Result.Catch" + ], + "heldBy": [ + "ResultCatchTests" + ] + }, + { + "id": "R09-continuations", + "source": "docs/result.md#the-surface", + "quote": "the same, taking or continuing a task; the chain is awaited once", + "claim": "the same, taking or continuing a task; the chain is awaited once", + "oracle": "claim", + "surfaces": [ + "Result.MapAsync", + "Result.BindAsync", + "Result.RecoverAsync", + "Result.TapAsync", + "Result.TapErrorAsync" + ], + "heldBy": [ + "ResultContinuationTests", + "ResultTests", + "ResultWithValueTests" + ] + }, + { + "id": "R09-debugger-shows-both", + "source": "docs/result.md#the-surface", + "quote": "`ToString()` prints the state and never the value or the error, so a result is safe to log. The debugger shows both.", + "claim": "`ToString()` prints the state and never the value or the error, so a result is safe to log. The debugger shows both.", + "oracle": "claim", + "surfaces": [ + "Result.ToString" + ], + "heldBy": [ + "TextRepresentationTests" + ] + }, + { + "id": "R09-ensure", + "source": "docs/result.md#the-surface", + "quote": "a success that fails the predicate becomes that error", + "claim": "a success that fails the predicate becomes that error", + "oracle": "claim", + "surfaces": [ + "Result.Ensure" + ], + "heldBy": [ + "ResultWithValueTests" + ] + }, + { + "id": "R09-equality", + "source": "docs/result.md#the-surface", + "quote": "Equality works as for any record struct.", + "claim": "Equality works as for any record struct.", + "oracle": "claim", + "surfaces": [ + "Result.Equals", + "Result.GetHashCode", + "Result.op_Equality", + "Result.op_Inequality" + ], + "heldBy": [ + "ResultTests", + "ResultWithValueTests" + ] + }, + { + "id": "R09-error-marker-converts-to-either", + "source": "docs/result.md#the-surface", + "quote": "an `Error` marker that converts to either shape", + "claim": "an `Error` marker that converts to either shape", + "oracle": "claim", + "surfaces": [ + "Result.Error", + "Result.op_Implicit" + ], + "heldBy": [ + "ResultTests", + "ResultWithValueTests" + ] + }, + { + "id": "R09-first-or-error", + "source": "docs/result.md#the-surface", + "quote": "the first or last match, or the error; on a query the provider runs the predicate, and `LastOrError` is a compile error", + "claim": "the first or last match, or the error; on a query the provider runs the predicate, and `LastOrError` is a compile error", + "oracle": "claim", + "surfaces": [ + "Result.FirstOrError", + "Result.LastOrError" + ], + "heldBy": [ + "ResultWithValueTests", + "QueryableOptionTests" + ] + }, + { + "id": "R09-map", + "source": "docs/result.md#the-surface", + "quote": "a new value on a success; `Result` from a `Result`", + "claim": "a new value on a success; `Result` from a `Result`", + "oracle": "claim", + "surfaces": [ + "Result.Map" + ], + "heldBy": [ + "ResultTests", + "ResultWithValueTests" + ] + }, + { + "id": "R09-map-error", + "source": "docs/result.md#the-surface", + "quote": "| `MapError(selector)` | both | a new error type |", + "claim": "| `MapError(selector)` | both | a new error type |", + "oracle": "claim", + "surfaces": [ + "Result.MapError" + ], + "heldBy": [ + "ResultTests", + "ResultWithValueTests" + ] + }, + { + "id": "R09-map-error-and-ensure-async", + "source": "docs/result.md#the-surface", + "quote": "`MapError` and `Ensure`, once the result is known", + "claim": "`MapError` and `Ensure`, once the result is known", + "oracle": "claim", + "surfaces": [ + "Result.MapErrorAsync", + "Result.EnsureAsync" + ], + "heldBy": [ + "ResultContinuationTests" + ] + }, + { + "id": "R09-match", + "source": "docs/result.md#the-surface", + "quote": "what the branch returns; `onError` may take the error or nothing", + "claim": "what the branch returns; `onError` may take the error or nothing", + "oracle": "claim", + "surfaces": [ + "Result.Match" + ], + "heldBy": [ + "ResultTests", + "ResultWithValueTests" + ] + }, + { + "id": "R09-recover", + "source": "docs/result.md#the-surface", + "quote": "the selector's result on an error, a success or another error", + "claim": "the selector's result on an error, a success or another error", + "oracle": "claim", + "surfaces": [ + "Result.Recover", + "Result.RecoverAsync" + ], + "heldBy": [ + "ResultTests", + "ResultWithValueTests" + ] + }, + { + "id": "R09-sequence", + "source": "docs/result.md#the-surface", + "quote": "| `Sequence()` | `IEnumerable>`, `IEnumerable>` | the list of values, or the first error |", + "claim": "| `Sequence()` | `IEnumerable>`, `IEnumerable>` | the list of values, or the first error |", + "oracle": "claim", + "surfaces": [ + "Result.Sequence" + ], + "heldBy": [ + "ResultCombinationTests" + ] + }, + { + "id": "R09-success-marker", + "source": "docs/result.md#the-surface", + "quote": "a `Success` marker that converts to any `Result`", + "claim": "a `Success` marker that converts to any `Result`", + "oracle": "claim", + "surfaces": [ + "Result.Success", + "Result.op_Implicit" + ], + "heldBy": [ + "ResultTests" + ] + }, + { + "id": "R09-tap", + "source": "docs/result.md#the-surface", + "quote": "runs the action on that branch, hands the result on", + "claim": "runs the action on that branch, hands the result on", + "oracle": "claim", + "surfaces": [ + "Result.Tap", + "Result.TapError" + ], + "heldBy": [ + "ResultTests", + "ResultWithValueTests" + ] + }, + { + "id": "R09-to-option", + "source": "docs/result.md#the-surface", + "quote": "`Some(value)`, or `None` for an error", + "claim": "`Some(value)`, or `None` for an error", + "oracle": "claim", + "surfaces": [ + "Result.ToOption" + ], + "heldBy": [ + "ResultWithValueTests" + ] + }, + { + "id": "R09-transpose", + "source": "docs/result.md#the-surface", + "quote": "the one turned inside out: `None` is a success without a value, an error stays the error", + "claim": "the one turned inside out: `None` is a success without a value, an error stays the error", + "oracle": "claim", + "surfaces": [ + "Result.Transpose" + ], + "heldBy": [ + "OptionLookupTests" + ] + }, + { + "id": "R09-traverse-option", + "source": "docs/result.md#the-surface", + "quote": "`Result, TError>`: an optional input validated when present", + "claim": "`Result, TError>`: an optional input validated when present", + "oracle": "claim", + "surfaces": [ + "Result.Traverse" + ], + "heldBy": [ + "OptionLookupTests" + ] + }, + { + "id": "R09-traverse-patch", + "source": "docs/result.md#the-surface", + "quote": "`Result, TError>`: a patch field validated when it carries a value; a clear or an absence passes through", + "claim": "`Result, TError>`: a patch field validated when it carries a value; a clear or an absence passes through", + "oracle": "claim", + "surfaces": [ + "Result.Traverse" + ], + "heldBy": [ + "PatchCombinationTests" + ] + }, + { + "id": "R09-try-get-error", + "source": "docs/result.md#the-surface", + "quote": "| `TryGetError(out error)` | `Result` | `bool` |", + "claim": "| `TryGetError(out error)` | `Result` | `bool` |", + "oracle": "claim", + "surfaces": [ + "Result.TryGetError" + ], + "heldBy": [ + "ResultTests" + ] + }, + { + "id": "R09-zip", + "source": "docs/result.md#the-surface", + "quote": "one value from all, or the first error", + "claim": "one value from all, or the first error", + "oracle": "claim", + "surfaces": [ + "Result.Zip" + ], + "heldBy": [ + "ResultCombinationTests" + ] + }, + { + "id": "S01-a-closed-set-is-its-name", + "source": "src/CodoMetis.TypeKit.Serilog/README.md#codometistypekitserilog", + "quote": "**A closed set is logged as its name**, or as what its own `ToString()` prints, never by its properties.", + "claim": "**A closed set is logged as its name**, or as what its own `ToString()` prints, never by its properties.", + "oracle": "claim", + "surfaces": [ + "TypeKitSerilogExtensions.WithTypeKit" + ], + "heldBy": [ + "DestructuringTests" + ] + }, + { + "id": "S01-a-list-of-them", + "source": "src/CodoMetis.TypeKit.Serilog/README.md#codometistypekitserilog", + "quote": "A list or a dictionary of them becomes a list or a dictionary of strings.", + "claim": "A list or a dictionary of them becomes a list or a dictionary of strings.", + "oracle": "claim", + "surfaces": [ + "TypeKitSerilogExtensions.WithTypeKit" + ], + "heldBy": [ + "DestructuringTests" + ] + }, + { + "id": "S01-an-instance-beside-the-members", + "source": "src/CodoMetis.TypeKit.Serilog/README.md#codometistypekitserilog", + "quote": "An instance that the set's own code made beside its members has no name, and Serilog logs that it could not capture it.", + "claim": "An instance that the set's own code made beside its members has no name, and Serilog logs that it could not capture it.", + "oracle": "claim", + "surfaces": [ + "TypeKitSerilogExtensions.WithTypeKit" + ], + "heldBy": [ + "DestructuringTests" + ] + }, + { + "id": "S01-as-what-they-print", + "source": "src/CodoMetis.TypeKit.Serilog/README.md#codometistypekitserilog", + "quote": "value objects and closed sets as what they print, also where it destructures them. A value object that masks itself stays masked.", + "claim": "value objects and closed sets as what they print, also where it destructures them. A value object that masks itself stays masked.", + "oracle": "claim", + "surfaces": [ + "TypeKitSerilogExtensions.WithTypeKit" + ], + "heldBy": [ + "DestructuringTests", + "SerilogSamples", + "LoggingTests" + ] + }, + { + "id": "S01-every-other-type-as-before", + "source": "src/CodoMetis.TypeKit.Serilog/README.md#codometistypekitserilog", + "quote": "Serilog handles every other type as before.", + "claim": "Serilog handles every other type as before.", + "oracle": "claim", + "surfaces": [ + "TypeKitSerilogExtensions.WithTypeKit" + ], + "heldBy": [ + "DestructuringTests" + ] + }, + { + "id": "S01-every-value-object-is-a-string", + "source": "src/CodoMetis.TypeKit.Serilog/README.md#codometistypekitserilog", + "quote": "**Every value object becomes a string**, masked or not.", + "claim": "**Every value object becomes a string**, masked or not.", + "oracle": "claim", + "surfaces": [ + "TypeKitSerilogExtensions.WithTypeKit" + ], + "heldBy": [ + "DestructuringTests" + ] + }, + { + "id": "S01-not-the-generators", + "source": "src/CodoMetis.TypeKit.Serilog/README.md#codometistypekitserilog", + "quote": "So the host needs this package and the base package, not the generators.", + "claim": "So the host needs this package and the base package, not the generators.", + "oracle": "claim", + "surfaces": [ + "TypeKitSerilogExtensions.WithTypeKit" + ], + "heldBy": [ + "PackageDependencyTests" + ] + }, + { + "id": "S01-unmasked-without-it", + "source": "src/CodoMetis.TypeKit.Serilog/README.md#codometistypekitserilog", + "quote": "Without this package, a value object that masks itself with a hand-written `ToString()` is logged unmasked there, as `{ Value: \"k-123\" }`.", + "claim": "Without this package, a value object that masks itself with a hand-written `ToString()` is logged unmasked there, as `{ Value: \"k-123\" }`.", + "oracle": "claim", + "surfaces": [ + "TypeKitSerilogExtensions.WithTypeKit" + ], + "heldBy": [ + "DestructuringTests" + ] + }, + { + "id": "T01-an-error-learns-only-new-api-forms", + "source": "src/CodoMetis.TypeKit.Analyzers/README.md#configuration", + "quote": "A minor version may teach an Error a form that only its own new API can write.", + "claim": "Within a major version an Error reports nothing more of the code a consumer could already write; a minor version's Error reports only forms that call that version's new API, such as 2.3.0's CMTK0003 on an unawaited Testing assertion over a Task or ValueTask, recognised by the Testing assembly so that a consumer's own extension of the same name stays silent.", + "oracle": "claim", + "surfaces": [ + "OptionAssertions.*", + "ResultAssertions.*", + "PatchAssertions.*" + ], + "heldBy": [ + "IgnoredOutcomeAnalyzerTests" + ] + }, + { + "id": "T02-a-few-2-2-forms-are-reported", + "source": "src/CodoMetis.TypeKit.Analyzers/README.md#configuration", + "quote": "2.3.0 makes an exception for a few forms that code from before 2.3.0 can contain.", + "claim": "2.3.0's CMTK0003 reports, on tasks of results, a Task.WhenAny of tasks the statement made in any use, a dropped Task.WhenEach, Wait() on a branch and an array written in Task.WaitAll or Task.WaitAny, all forms 2.2.0 code can contain; a WhenAny of held tasks stays silent.", + "oracle": "claim", + "surfaces": [ + "Result.*" + ], + "heldBy": [ + "DroppedTaskVariantTests", + "IgnoredOutcomeAnalyzerTests" + ] + }, + { + "id": "T02-a-wrapped-assertion-is-reported", + "source": "src/CodoMetis.TypeKit.Analyzers/README.md#cmtk0003", + "quote": "It also reports an assertion passed through `ConfigureAwait` or `WaitAsync`, and one in a branch of a `?:`, `??` or `switch` expression, as it does for a dropped result.", + "claim": "It also reports an assertion passed through `ConfigureAwait` or `WaitAsync`, and one in a branch of a `?:`, `??` or `switch` expression, as it does for a dropped result.", + "oracle": "claim", + "surfaces": [ + "OptionAssertions.*", + "ResultAssertions.*", + "PatchAssertions.*", + "FaultAssertions.*" + ], + "heldBy": [ + "DroppedTaskVariantTests" + ] + }, + { + "id": "T02-combined-assertions-are-reported", + "source": "src/CodoMetis.TypeKit.Analyzers/README.md#cmtk0003", + "quote": "You get the report when you hand assertions to `Task.WhenAll`, `Task.Run` or `Task.WhenEach` and drop what it returns.", + "claim": "You get the report when you hand assertions to `Task.WhenAll`, `Task.Run` or `Task.WhenEach` and drop what it returns.", + "oracle": "claim", + "surfaces": [ + "OptionAssertions.*", + "ResultAssertions.*", + "PatchAssertions.*", + "FaultAssertions.*" + ], + "heldBy": [ + "DroppedTaskVariantTests" + ] + }, + { + "id": "T02-wait-all-observes-assertions", + "source": "src/CodoMetis.TypeKit.Analyzers/README.md#cmtk0003", + "quote": "`Task.WaitAll` throws the failures, so it is silent.", + "claim": "`Task.WaitAll` throws the failures, so it is silent.", + "oracle": "claim", + "surfaces": [ + "OptionAssertions.*", + "ResultAssertions.*", + "PatchAssertions.*", + "FaultAssertions.*" + ], + "heldBy": [ + "DroppedTaskVariantTests" + ] + }, + { + "id": "T02-when-any-of-results-is-reported", + "source": "src/CodoMetis.TypeKit.Analyzers/README.md#cmtk0003", + "quote": "A `WhenAny` of tasks the statement made is reported whether you drop it, await it, wait for it or return it as a plain `Task`.", + "claim": "A `WhenAny` of tasks the statement made is reported whether you drop it, await it, wait for it or return it as a plain `Task`.", + "oracle": "claim", + "surfaces": [ + "Result.*" + ], + "heldBy": [ + "IgnoredOutcomeAnalyzerTests", + "DroppedTaskVariantTests" + ] + }, + { + "id": "T03-a-catch-that-cannot-keep-the-failure-is-reported", + "source": "src/CodoMetis.TypeKit.Analyzers/README.md#cmtk0003", + "quote": "A `catch` that throws again, one for another exception, such as `catch (OperationCanceledException)`, and one with a `when (false)` filter are reported.", + "claim": "A `catch` that throws again, one for another exception, such as `catch (OperationCanceledException)`, and one with a `when (false)` filter are reported.", + "oracle": "claim", + "surfaces": [ + "OptionAssertions.*", + "ResultAssertions.*", + "PatchAssertions.*", + "FaultAssertions.*" + ], + "heldBy": [ + "DroppedTaskVariantTests", + "IgnoredOutcomeAnalyzerTests" + ] + }, + { + "id": "T03-a-nested-when-any-is-reported", + "source": "src/CodoMetis.TypeKit.Analyzers/README.md#cmtk0003", + "quote": "A `Task.WhenAny` inside a `Task.WhenAll` or a `Task.Run` is reported even when you await the outer task, because that task succeeds with the failed assertion as its value", + "claim": "A `Task.WhenAny` inside a `Task.WhenAll` or a `Task.Run` is reported even when you await the outer task, because that task succeeds with the failed assertion as its value", + "oracle": "claim", + "surfaces": [ + "OptionAssertions.*", + "ResultAssertions.*", + "PatchAssertions.*", + "FaultAssertions.*" + ], + "heldBy": [ + "DroppedTaskVariantTests", + "IgnoredOutcomeAnalyzerTests" + ] + }, + { + "id": "T03-an-unknown-filter-counts-as-catching", + "source": "src/CodoMetis.TypeKit.Analyzers/README.md#cmtk0003", + "quote": "A filter the rule can't work out, such as `when (Expected(e))`, counts as catching the failure.", + "claim": "A filter the rule can't work out, such as `when (Expected(e))`, counts as catching the failure.", + "oracle": "claim", + "surfaces": [ + "OptionAssertions.*", + "ResultAssertions.*", + "PatchAssertions.*", + "FaultAssertions.*" + ], + "heldBy": [ + "DroppedTaskVariantTests", + "IgnoredOutcomeAnalyzerTests" + ] + }, + { + "id": "W01-answers-are-real", + "source": "docs/walkthrough.md#walkthrough-one-orders-service", + "quote": "CI runs it on every change, and builds it from the packed packages too, so every answer below is one it really gives", + "claim": "CI runs it on every change, and builds it from the packed packages too, so every answer below is one it really gives", + "oracle": "claim", + "surfaces": [ + "TypeKitOpenApiOptionsExtensions.AddTypeKit", + "TypeKitHttpResultExtensions.ToHttpResult", + "TypeKitDbContextOptionsBuilderExtensions.UseTypeKit", + "TypeKitSerilogExtensions.WithTypeKit" + ], + "heldBy": [ + "WalkthroughTests" + ] + }, + { + "id": "W02-a-column-of-the-underlying-type", + "source": "docs/walkthrough.md#store-them", + "quote": "The id is a `TEXT` key, as a `Guid` is on SQLite, and the quantity an `INTEGER`. The status is its member's name.", + "claim": "The id is a `TEXT` key, as a `Guid` is on SQLite, and the quantity an `INTEGER`. The status is its member's name.", + "oracle": "claim", + "surfaces": [ + "TypeKitDbContextOptionsBuilderExtensions.UseTypeKit", + "ClosedSetConverter.*", + "ValueObjectConverter.*" + ], + "heldBy": [ + "WalkthroughTests" + ] + }, + { + "id": "W03-json-reads-through-create", + "source": "docs/walkthrough.md#place-an-order", + "quote": "So the handler receives `ann@example.com`, trimmed, and never sees the spaces.", + "claim": "So the handler receives `ann@example.com`, trimmed, and never sees the spaces.", + "oracle": "claim", + "surfaces": [ + "GeneratedJson.*", + "StringTextExtensions.*", + "IValidatedValue.Create" + ], + "heldBy": [ + "WalkthroughTests" + ] + }, + { + "id": "W04-json-writes-the-string", + "source": "docs/walkthrough.md#place-an-order", + "quote": "the JSON is the underlying type's: `\"email\":\"ann@example.com\"`, a plain string, and the closed set is its name", + "claim": "the JSON is the underlying type's: `\"email\":\"ann@example.com\"`, a plain string, and the closed set is its name", + "oracle": "claim", + "surfaces": [ + "GeneratedJson.*", + "IClosedSet.*" + ], + "heldBy": [ + "WalkthroughTests" + ] + }, + { + "id": "W05-log-masks", + "source": "docs/walkthrough.md#place-an-order", + "quote": "`Destructure.WithTypeKit()` logs each value object as what its `ToString()` prints.", + "claim": "`Destructure.WithTypeKit()` logs each value object as what its `ToString()` prints.", + "oracle": "claim", + "surfaces": [ + "TypeKitSerilogExtensions.WithTypeKit" + ], + "heldBy": [ + "WalkthroughTests" + ] + }, + { + "id": "W06-rejections-never-quote", + "source": "docs/walkthrough.md#reject-what-doesnt-fit", + "quote": "Each of these is a 400 with problem details, and none of them quotes what the client sent", + "claim": "Each of these is a 400 with problem details, and none of them quotes what the client sent", + "oracle": "claim", + "surfaces": [ + "GeneratedJson.*", + "GeneratedParsing.*" + ], + "heldBy": [ + "WalkthroughTests" + ] + }, + { + "id": "W07-none-is-a-404", + "source": "docs/walkthrough.md#find-an-order", + "quote": "`ToResult` turns a missing order into the error `NotFound`, and `ToHttpResult()` answers it with the status the setup maps it to, as problem details.", + "claim": "`ToResult` turns a missing order into the error `NotFound`, and `ToHttpResult()` answers it with the status the setup maps it to, as problem details.", + "oracle": "claim", + "surfaces": [ + "TypeKitQueryableExtensions.*", + "TypeKitHttpResultExtensions.ToHttpResult" + ], + "heldBy": [ + "WalkthroughTests" + ] + }, + { + "id": "W08-left-out-errors-leave-the-document", + "source": "docs/walkthrough.md#find-an-order", + "quote": "The OpenAPI document then shows only the 404 for it, where it would otherwise list every status `OrderFault` maps to.", + "claim": "The OpenAPI document then shows only the 404 for it, where it would otherwise list every status `OrderFault` maps to.", + "oracle": "claim", + "surfaces": [ + "TypeKitWithoutFaultsEndpointConventionBuilderExtensions.*", + "TypeKitOpenApiOptionsExtensions.AddTypeKit" + ], + "heldBy": [ + "WalkthroughTests" + ] + }, + { + "id": "W09-value-is-the-column", + "source": "docs/walkthrough.md#query-on-value", + "quote": "In the query, `order.Quantity.Value` is the `Quantity` column itself, so the comparison runs in the database.", + "claim": "In the query, `order.Quantity.Value` is the `Quantity` column itself, so the comparison runs in the database.", + "oracle": "claim", + "surfaces": [ + "IValueObject.Value", + "TypeKitDbContextOptionsBuilderExtensions.UseTypeKit" + ], + "heldBy": [ + "WalkthroughTests" + ] + }, + { + "id": "W10-patch-reads-through-create", + "source": "docs/walkthrough.md#change-an-order", + "quote": "Each field reads its value through its own `Create`, so `{\"quantity\":0}` is a 400 like any other body.", + "claim": "Each field reads its value through its own `Create`, so `{\"quantity\":0}` is a 400 like any other body.", + "oracle": "claim", + "surfaces": [ + "PatchJsonConverterFactory.*", + "GeneratedJson.*" + ], + "heldBy": [ + "WalkthroughTests" + ] + }, + { + "id": "W11-document-describes-the-underlying-type", + "source": "docs/walkthrough.md#read-the-openapi-document", + "quote": "`AddTypeKit()` describes every value object as the schema of the type inside it", + "claim": "`AddTypeKit()` describes every value object as the schema of the type inside it", + "oracle": "claim", + "surfaces": [ + "TypeKitOpenApiOptionsExtensions.AddTypeKit" + ], + "heldBy": [ + "WalkthroughTests" + ] + }, + { + "id": "W12-closed-set-is-a-string-enum", + "source": "docs/walkthrough.md#read-the-openapi-document", + "quote": "**The closed set** is a string enum of its names.", + "claim": "**The closed set** is a string enum of its names.", + "oracle": "claim", + "surfaces": [ + "TypeKitOpenApiOptionsExtensions.AddTypeKit", + "IClosedSet.*" + ], + "heldBy": [ + "WalkthroughTests" + ] + } +] diff --git a/test/oracles/oracles.json b/test/oracles/oracles.json new file mode 100644 index 00000000..26396de8 --- /dev/null +++ b/test/oracles/oracles.json @@ -0,0 +1,50 @@ +[ + { + "class": "AsynchronousContextTests", + "project": "CodoMetis.TypeKit.Tests", + "kind": "completeness", + "against": "every public member of the base package that returns a task, by reflection: a callback runs where the task it waited for completed, and nothing resumes on the caller's context" + }, + { + "class": "JsonParityTests", + "project": "CodoMetis.TypeKit.Generators.Tests", + "kind": "differential", + "against": "the wrapped type's own JSON under the same options, as a value, a property and a dictionary key" + }, + { + "class": "KeyGenerationVariantTests", + "project": "CodoMetis.TypeKit.EntityFrameworkCore.Tests", + "kind": "differential", + "against": "the same key shape keyed by the wrapped type, on SQLite, Npgsql and SQL Server" + }, + { + "class": "SqlServerKeyVariantTests", + "project": "CodoMetis.TypeKit.EntityFrameworkCore.Tests", + "kind": "differential", + "against": "the same key shape keyed by the wrapped type, inserted into a real SQL Server and read back" + }, + { + "class": "SqlServerQueryVariantTests", + "project": "CodoMetis.TypeKit.EntityFrameworkCore.Tests", + "kind": "differential", + "against": "the same query over the wrapped type on a real SQL Server, the objects in memory and SQLite, each difference from memory that is SQL Server's own a pinned fact" + }, + { + "class": "NotWireTypeTests", + "project": "CodoMetis.TypeKit.Tests", + "kind": "completeness", + "against": "every exported struct of the base package, by reflection: System.Text.Json refuses it in both directions" + }, + { + "class": "NullDelegateTests", + "project": "CodoMetis.TypeKit.Tests", + "kind": "completeness", + "against": "every delegate parameter of the base package, by reflection, on every branch of its receiver" + }, + { + "class": "SchemaTests", + "project": "CodoMetis.TypeKit.AspNetCore.Tests", + "kind": "differential", + "against": "the wrapped type's schema in the same document, property by property, with each value object a component and inlined, in OpenAPI 3.1, 3.0 and 2.0" + } +] diff --git a/tools/probe/corpus/seeds/E02-value-translates-to-column-column-not-cast.patch b/test/oracles/seeds/column-cast-not-re-typed.patch similarity index 80% rename from tools/probe/corpus/seeds/E02-value-translates-to-column-column-not-cast.patch rename to test/oracles/seeds/column-cast-not-re-typed.patch index 6b2d61d9..1b0c3dc7 100644 --- a/tools/probe/corpus/seeds/E02-value-translates-to-column-column-not-cast.patch +++ b/test/oracles/seeds/column-cast-not-re-typed.patch @@ -1,3 +1,7 @@ +Seed: A table column read through .Value is cast to the wrapped type, not re-typed +Oracles: SqlServerQueryVariantTests +Fails: the SQL of every query that reads a table column through an accessor, which is no longer the wrapped type's + diff --git a/src/CodoMetis.TypeKit.EntityFrameworkCore/ValueObjectTranslators.cs b/src/CodoMetis.TypeKit.EntityFrameworkCore/ValueObjectTranslators.cs index 4cbf797..074e67d 100644 --- a/src/CodoMetis.TypeKit.EntityFrameworkCore/ValueObjectTranslators.cs diff --git a/test/oracles/seeds/inline-value-objects-ignored.patch b/test/oracles/seeds/inline-value-objects-ignored.patch new file mode 100644 index 00000000..e787cae8 --- /dev/null +++ b/test/oracles/seeds/inline-value-objects-ignored.patch @@ -0,0 +1,16 @@ +Seed: InlineValueObjects is ignored, so every value object keeps its component +Oracles: SchemaTests +Fails: every inline row but the dictionary keyed by a value object, in 3.1, 3.0 and 2.0 + +diff --git a/src/CodoMetis.TypeKit.AspNetCore/ValueObjectSchemaReferenceIds.cs b/src/CodoMetis.TypeKit.AspNetCore/ValueObjectSchemaReferenceIds.cs +--- a/src/CodoMetis.TypeKit.AspNetCore/ValueObjectSchemaReferenceIds.cs ++++ b/src/CodoMetis.TypeKit.AspNetCore/ValueObjectSchemaReferenceIds.cs +@@ -50,7 +50,7 @@ + options.CreateSchemaReferenceId = type => + { + var claimant = Nullable.GetUnderlyingType(type.Type) ?? type.Type; +- if (inlineValueObjects && ValueObjectTypes.IsValueObject(claimant)) return null; ++ if (inlineValueObjects && false) return null; + + if (inner(type) is not { } id) return null; + diff --git a/tools/probe/corpus/seeds/L01-json-bytes-as-a-dictionary-key-too.patch b/test/oracles/seeds/json-key-written-by-to-string.patch similarity index 77% rename from tools/probe/corpus/seeds/L01-json-bytes-as-a-dictionary-key-too.patch rename to test/oracles/seeds/json-key-written-by-to-string.patch index 832c2257..0e656c20 100644 --- a/tools/probe/corpus/seeds/L01-json-bytes-as-a-dictionary-key-too.patch +++ b/test/oracles/seeds/json-key-written-by-to-string.patch @@ -1,3 +1,7 @@ +Seed: A value object as a dictionary key is written by the wrapped value's ToString(), not by its converter's WriteAsPropertyName +Oracles: JsonParityTests +Fails: every dictionary-key row whose wrapped type's key is not its ToString(): dates, decimals, a host converter's keys + diff --git a/src/CodoMetis.TypeKit/CompilerServices/GeneratedJsonPlan.cs b/src/CodoMetis.TypeKit/CompilerServices/GeneratedJsonPlan.cs index 8597206..1990a4a 100644 --- a/src/CodoMetis.TypeKit/CompilerServices/GeneratedJsonPlan.cs diff --git a/test/oracles/seeds/patch-traverse-takes-a-null-selector.patch b/test/oracles/seeds/patch-traverse-takes-a-null-selector.patch new file mode 100644 index 00000000..cbe223a7 --- /dev/null +++ b/test/oracles/seeds/patch-traverse-takes-a-null-selector.patch @@ -0,0 +1,17 @@ +Seed: Patch.Traverse checks its selector only where it calls it, so a null passes on a clear and an absent field +Oracles: NullDelegateTests +Fails: Result.Traverse(Patch, ...): selector, on the Null and Absent branches + +diff --git a/src/CodoMetis.TypeKit/ResultExtensions.cs b/src/CodoMetis.TypeKit/ResultExtensions.cs +index a4394be..1568b0a 100644 +--- a/src/CodoMetis.TypeKit/ResultExtensions.cs ++++ b/src/CodoMetis.TypeKit/ResultExtensions.cs +@@ -1568,8 +1568,6 @@ public static class Result + where TResult : notnull + where TError : notnull + { +- ArgumentNullException.ThrowIfNull(selector); +- + if (!patch.TryGetValue(out var value)) + return Result, TError>.Success(patch.State == PatchState.Null ? Patch.Null() : Patch.Absent()); + diff --git a/test/oracles/seeds/result-reads-as-json.patch b/test/oracles/seeds/result-reads-as-json.patch new file mode 100644 index 00000000..2f2b00ad --- /dev/null +++ b/test/oracles/seeds/result-reads-as-json.patch @@ -0,0 +1,16 @@ +Seed: Result loses the converter that refuses System.Text.Json, so a success is written as {} and read back as uninitialized +Oracles: NotWireTypeTests +Fails: the completeness row for Result, and its write and read + +diff --git a/src/CodoMetis.TypeKit/Result.cs b/src/CodoMetis.TypeKit/Result.cs +index b92fd68..1f7da85 100644 +--- a/src/CodoMetis.TypeKit/Result.cs ++++ b/src/CodoMetis.TypeKit/Result.cs +@@ -29,7 +29,6 @@ namespace CodoMetis.TypeKit; + /// + /// The type of the error. + [RequireCustomInitialization("Use Result.Success() or Result.Error(error).")] +-[JsonConverter(typeof(NotWireTypeJsonConverterFactory))] + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public readonly record struct Result where TError : notnull + { diff --git a/test/oracles/seeds/traverse-async-resumes-on-the-caller-s-context.patch b/test/oracles/seeds/traverse-async-resumes-on-the-caller-s-context.patch new file mode 100644 index 00000000..cbb6029a --- /dev/null +++ b/test/oracles/seeds/traverse-async-resumes-on-the-caller-s-context.patch @@ -0,0 +1,17 @@ +Seed: The sequence's TraverseAsync awaits each operation without ConfigureAwait(false), so it resumes on the caller's SynchronizationContext +Oracles: AsynchronousContextTests +Fails: Result.TraverseAsync(IEnumerable, Func>>), with the first operation pending + +diff --git a/src/CodoMetis.TypeKit/ResultExtensions.cs b/src/CodoMetis.TypeKit/ResultExtensions.cs +index a4394be..b2eae14 100644 +--- a/src/CodoMetis.TypeKit/ResultExtensions.cs ++++ b/src/CodoMetis.TypeKit/ResultExtensions.cs +@@ -1286,7 +1286,7 @@ public static class Result + + foreach (var element in source) + { +- var result = await ThrowHelper.Awaitable(selector(element), nameof(TraverseAsync), nameof(selector)).ConfigureAwait(false); ++ var result = await ThrowHelper.Awaitable(selector(element), nameof(TraverseAsync), nameof(selector)); + if (!result.TryGetValue(out var value, out var error)) return Result, TError>.Error(error); + + values.Add(value); diff --git a/tools/probe/corpus/seeds/E04-integer-key-generated-char-key-yours.patch b/test/oracles/seeds/ushort-key-not-generated.patch similarity index 70% rename from tools/probe/corpus/seeds/E04-integer-key-generated-char-key-yours.patch rename to test/oracles/seeds/ushort-key-not-generated.patch index 457da14a..54b876f4 100644 --- a/tools/probe/corpus/seeds/E04-integer-key-generated-char-key-yours.patch +++ b/test/oracles/seeds/ushort-key-not-generated.patch @@ -1,5 +1,9 @@ +Seed: A value-object key over ushort is not declared generated, where a ushort key is +Oracles: KeyGenerationVariantTests, SqlServerKeyVariantTests +Fails: every key shape over ushort, on the three providers, and on a real SQL Server + diff --git a/src/CodoMetis.TypeKit.EntityFrameworkCore/ValueObjectKeyValueGenerationConvention.cs b/src/CodoMetis.TypeKit.EntityFrameworkCore/ValueObjectKeyValueGenerationConvention.cs -index 934f884..1f5343b 100644 +index c64d73e..be3d015 100644 --- a/src/CodoMetis.TypeKit.EntityFrameworkCore/ValueObjectKeyValueGenerationConvention.cs +++ b/src/CodoMetis.TypeKit.EntityFrameworkCore/ValueObjectKeyValueGenerationConvention.cs @@ -77,7 +77,7 @@ internal sealed class ValueObjectKeyValueGenerationConvention : IModelFinalizing @@ -7,7 +11,7 @@ index 934f884..1f5343b 100644 private static bool IsGeneratedByTheDatabase(Type wrapped) => wrapped == typeof(int) || wrapped == typeof(long) || wrapped == typeof(short) || wrapped == typeof(sbyte) - || wrapped == typeof(uint) || wrapped == typeof(ulong) || wrapped == typeof(ushort); -+ || wrapped == typeof(uint) || wrapped == typeof(ulong) || wrapped == typeof(ushort) || wrapped == typeof(char); ++ || wrapped == typeof(uint) || wrapped == typeof(ulong); } /// diff --git a/test/oracles/surface-waivers.json b/test/oracles/surface-waivers.json new file mode 100644 index 00000000..5d9dfe39 --- /dev/null +++ b/test/oracles/surface-waivers.json @@ -0,0 +1,19 @@ +{ + "Error.*": "the error marker's record equality and printing, compiler-generated; the conversions it exists for are claimed through Result", + "None.*": "the None marker's record equality and printing, compiler-generated", + "Success.*": "the success marker's record equality and printing, compiler-generated", + "ResultState.*": "enum members; what each state means is claimed through the members that read it", + "AbstractAttribute..ctor": "a constraint attribute's constructor; what it constrains is CMTK0010's, held by the analyzer tests", + "ConcreteAttribute..ctor": "a constraint attribute's constructor; what it constrains is CMTK0010's, held by the analyzer tests", + "DistinctAttribute..ctor": "a constraint attribute's constructor; what it constrains is CMTK0010's, held by the analyzer tests", + "ClosedSetAttribute..ctor": "a constraint attribute's constructor; what it constrains is CMTK0010's, held by the analyzer tests", + "GeneratedClosedSetAttribute.*": "plumbing the generators write on a closed set, read by the satellites through Accept", + "GeneratedValueObjectAttribute.*": "plumbing the generators write on a value object, read by the satellites through Accept", + "GeneratedClosedSets.*": "refusal helpers that generated closed-set code calls; what they refuse is claimed through Parse and the JSON reads", + "IClosedSetVisitor.Visit": "the visitor GeneratedClosedSetAttribute.Accept calls, plumbing", + "IValueObjectVisitor.Visit": "the visitor GeneratedValueObjectAttribute.Accept calls, plumbing", + "IStoredJsonConverterSource.CreateStoredJsonConverter": "plumbing StoredJsonConverterFactory calls on a generated converter", + "DescribedJsonSchema.ExporterOptions": "the exporter options the schema descriptions are read with, plumbing", + "TypeKitServiceCollectionExtensions.AddEntityFrameworkTypeKit": "the internal-service-provider form of UseTypeKit, whose claims hold it", + "TypeKitAssertionException..ctor": "the exception the assertions throw; its messages are claimed through the assertions" +} diff --git a/tools/AgentTools.slnf b/tools/AgentTools.slnf index be0719e0..88c49e3c 100644 --- a/tools/AgentTools.slnf +++ b/tools/AgentTools.slnf @@ -3,9 +3,11 @@ "path": "../CodoMetis.TypeKit.slnx", "projects": [ "tools/BuildVerdict/BuildVerdict.csproj", + "tools/CiLanes/CiLanes.csproj", "tools/ConsumerSmokeTest/ConsumerSmokeTest.csproj", "tools/DocsQuery/DocsQuery.csproj", - "tools/Prober/Prober.csproj" + "tools/Oracles/Oracles.csproj", + "tools/PrHygiene/PrHygiene.csproj" ] } } diff --git a/tools/BuildVerdict/BuildVerdict.csproj b/tools/BuildVerdict/BuildVerdict.csproj index 6d4ac2f5..56d0c520 100644 --- a/tools/BuildVerdict/BuildVerdict.csproj +++ b/tools/BuildVerdict/BuildVerdict.csproj @@ -3,7 +3,7 @@ Exe diff --git a/tools/CiLanes/CiLanes.csproj b/tools/CiLanes/CiLanes.csproj new file mode 100644 index 00000000..0732338c --- /dev/null +++ b/tools/CiLanes/CiLanes.csproj @@ -0,0 +1,14 @@ + + + + + Exe + ci-lanes + CiLanes + + + diff --git a/tools/CiLanes/Program.cs b/tools/CiLanes/Program.cs new file mode 100644 index 00000000..754a220f --- /dev/null +++ b/tools/CiLanes/Program.cs @@ -0,0 +1,363 @@ +// Which lanes of .github/workflows/dotnet.yml a run needs, and whether the run did what was selected +// (design/adr/0113-ci-runs-the-lanes-a-change-can-affect.md). Four lanes: the Linux test leg, in every run; +// `windows` and `macos`, those test legs; `pack`, the Pack and Consumer jobs. Each changed path selects lanes by +// the first class of Table whose pattern matches it, and a path no class names selects every lane a selection +// can: all but macOS, which only the runs that are not selected get. +// +// Usage: +// ci-lanes select --event --ref [--base ] [--before ] [--output ] [--summary ] +// A pull request into release/** is selected by `git diff origin/...HEAD`, a push to release/** by +// `git diff HEAD`; every other run gets every lane, macOS included. Appends `legs=`, +// `pack=true|false` and `suite=prose|full` to --output (the step's GITHUB_OUTPUT), or prints them, and each path's +// class to --summary. The suite is prose when every changed path is: the Linux leg then builds and runs only the +// test projects of test/Prose.slnf, the ones that read prose. +// ci-lanes classify ... +// One line per path: the path, its class and its lanes, tab-separated. +// ci-lanes gate [--summary ] +// Reads the workflow's toJSON(needs) and passes only when the changes job succeeded with answers this tool +// writes, every job it selected succeeded and every other was skipped. +// +// Exit 0 is a pass, 1 a failed gate or a git command that failed, 2 a usage error, 3 a tool built from other +// sources than the checkout holds, which names the command that rebuilds it. +using System.Diagnostics; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; + +Console.Out.NewLine = "\n"; +Console.Error.NewLine = "\n"; + +var root = SourceFingerprint.FindRoot(); +var stale = SourceFingerprint.Difference(root); +if (stale is not null) +{ + Console.WriteLine($"TOOL STALE: {SourceFingerprint.Project} was built from other sources than the checkout holds ({stale})"); + Console.WriteLine($" rebuild it: {SourceFingerprint.RebuildCommand}"); + return SourceFingerprint.StaleExitCode; +} + +return args switch +{ + ["select", .. var rest] when Options.Parse(rest, ["event", "ref", "base", "before", "output", "summary"]) is { } options + && options.ContainsKey("event") && options.ContainsKey("ref") => Selection.Run(root!, options), + ["classify", _, ..] => Classify(args[1..]), + ["gate", var needs, .. var rest] when Options.Parse(rest, ["summary"]) is { } options => Gate.Run(needs, options), + _ => Usage(), +}; + +static int Classify(string[] paths) +{ + foreach (var path in paths) + { + var @class = Table.Of(path); + Console.WriteLine($"{path}\t{@class.Name}\t{Table.Describe(@class.Lanes)}"); + } + return 0; +} + +static int Usage() +{ + Console.Error.WriteLine("Usage: dotnet tools/CiLanes/bin/Release/net10.0/ci-lanes.dll"); + Console.Error.WriteLine(" select --event --ref [--base ] [--before ] [--output ] [--summary ]"); + Console.Error.WriteLine(" classify ..."); + Console.Error.WriteLine(" gate [--summary ]"); + return 2; +} + +[Flags] +enum Lane +{ + Linux = 1, + Windows = 2, + MacOs = 4, + Pack = 8, + + // The most a changed path can select: macOS runs only on the release's pull request, main, the nightly and the release. + Selectable = Linux | Windows | Pack, + All = Selectable | MacOs, +} + +static class Options +{ + // `--name value` pairs, each name one of those allowed and given once; null for anything else. + public static Dictionary? Parse(string[] args, string[] allowed) + { + var options = new Dictionary(StringComparer.Ordinal); + for (var i = 0; i < args.Length; i += 2) + { + if (i + 1 >= args.Length || !args[i].StartsWith("--", StringComparison.Ordinal)) return null; + var name = args[i][2..]; + if (!allowed.Contains(name) || !options.TryAdd(name, args[i + 1])) return null; + } + return options; + } +} + +static partial class Table +{ + public sealed record Class(string Name, Lane Lanes, string[] Patterns, string Why); + + // Read by the test projects of test/Prose.slnf alone, which ProseReaderTests holds; built into nothing. The docs + // site's config (site/) is read by the docs workflow and the conventions tests (decision 116). + public static readonly Class Prose = new("prose", Lane.Linux, ["docs/**", "design/**", "site/**", ".github/ISSUE_TEMPLATE/**", "**/*.md"], + "read by the prose suite, built into nothing"); + + // The first class whose pattern matches a path decides its lanes. `**` matches any number of directories, + // `*` and `?` stay within one. Decision 113 holds this table; CiLaneTests holds it to the files MSBuild + // evaluates for the packages and the smoke test. + public static readonly Class[] Classes = + [ + new("changelog", Lane.Linux | Lane.Pack, ["CHANGELOG.md"], + "the packages' release notes are its section, read by a property function no evaluation lists"), + new("source text", Lane.Selectable, ["src/CodoMetis.TypeKit.Analyzers/**", "src/CodoMetis.TypeKit.Generators/**"], + "a package that writes or edits source text, whose line endings a Windows checkout changes"), + new("package", Lane.Linux | Lane.Pack, ["src/**"], "a shipping project"), + new("smoke test", Lane.Selectable, ["tools/ConsumerSmokeTest/**", "tools/Shared/**", "tools/Directory.Build.*", "tools/CiLanes/**"], + "the smoke test, what it compiles in, or this selection"), + Prose, + new("code no package reads", Lane.Linux | Lane.Windows, ["test/**", "spikes/**", "tools/**"], + "built and tested, never packed, and handling paths, line endings and child processes itself"), + ]; + + public static readonly Class Unlisted = new("unlisted", Lane.Selectable, [], "no class names it"); + + static readonly Dictionary Globs = []; + + public static Class Of(string path) => Classes.FirstOrDefault(c => c.Patterns.Any(p => Glob(p).IsMatch(path))) ?? Unlisted; + + public static string Describe(Lane lanes) => + string.Join(", ", new[] { (Lane.Linux, "linux"), (Lane.Windows, "windows"), (Lane.MacOs, "macos"), (Lane.Pack, "pack") }.Where(l => lanes.HasFlag(l.Item1)).Select(l => l.Item2)); + + // The test legs and the lane that selects each, in the order the matrix lists them. + public static readonly (Lane Lane, string Runner)[] Runners = [(Lane.Linux, "ubuntu-latest"), (Lane.Windows, "windows-latest"), (Lane.MacOs, "macos-latest")]; + + // The test matrix: one leg per selected test lane. Linux is in every selection. + public static string[] Legs(Lane lanes) => [.. Runners.Where(r => lanes.HasFlag(r.Lane)).Select(r => r.Runner)]; + + static Regex Glob(string pattern) + { + if (Globs.TryGetValue(pattern, out var regex)) return regex; + + var builder = new StringBuilder("^"); + for (var i = 0; i < pattern.Length; i++) + { + if (pattern.AsSpan(i).StartsWith("**/")) { builder.Append("(?:.*/)?"); i += 2; } + else if (pattern.AsSpan(i).StartsWith("**")) { builder.Append(".*"); i += 1; } + else if (pattern[i] == '*') builder.Append("[^/]*"); + else if (pattern[i] == '?') builder.Append("[^/]"); + else builder.Append(Regex.Escape(pattern[i].ToString())); + } + return Globs[pattern] = new Regex(builder.Append('$').ToString(), RegexOptions.CultureInvariant); + } +} + +static class Selection +{ + public static int Run(string root, Dictionary options) + { + var @event = options["event"]; + var @ref = options["ref"]; + var target = options.GetValueOrDefault("base", ""); + var before = options.GetValueOrDefault("before", ""); + + string? range = null; + string why; + if (@event == "pull_request" && target.StartsWith("release/", StringComparison.Ordinal)) + { + range = $"origin/{target}...HEAD"; + why = $"a pull request into {target}, selected by its diff against origin/{target}"; + } + else if (@event == "pull_request" && target == "main") + why = "a pull request into main is the release's, which runs every lane"; + else if (@event == "push" && @ref.StartsWith("refs/heads/release/", StringComparison.Ordinal)) + { + if (before.Length == 0 || before.All(c => c == '0')) + why = "the push creates the branch, so there is nothing to diff against"; + else if (Git(root, out _, "cat-file", "-e", $"{before}^{{commit}}") != 0) + why = $"the clone does not have {before}, the commit the push replaced"; + else + { + range = $"{before} HEAD"; + why = $"a push to {@ref["refs/heads/".Length..]}, selected by its diff against {before}"; + } + } + else + why = $"a {@event} of {@ref} runs every lane"; + + var lanes = Lane.All; + var classified = new List<(string Path, Table.Class Class)>(); + if (range is not null) + { + var code = Git(root, out var output, ["diff", "--name-only", "--no-renames", "-z", .. range.Split(' '), "--"]); + if (code != 0) + { + Console.WriteLine($"::error::git diff {range} failed with exit {code}, so nothing can be selected: {output.Trim()}"); + return 1; + } + + var paths = output.Split('\0', StringSplitOptions.RemoveEmptyEntries); + if (paths.Length == 0) + why += "; it names no path, so every lane runs"; + else + { + classified = [.. paths.Select(path => (path, Table.Of(path)))]; + lanes = classified.Aggregate(Lane.Linux, (all, row) => all | row.Class.Lanes); + } + } + + var legs = JsonSerializer.Serialize(Table.Legs(lanes)); + var pack = lanes.HasFlag(Lane.Pack) ? "true" : "false"; + var suite = classified.Count > 0 && classified.All(row => row.Class == Table.Prose) ? "prose" : "full"; + + Console.WriteLine($"{char.ToUpperInvariant(why[0])}{why[1..]}."); + foreach (var (path, @class) in classified) Console.WriteLine($" {path}: {@class.Name} ({Table.Describe(@class.Lanes)})"); + Console.WriteLine($"Lanes: {Table.Describe(lanes)}. Test legs: {string.Join(", ", Table.Legs(lanes))}. Pack and Consumer: {(pack == "true" ? "run" : "skipped")}. Suite: {suite}."); + + var answers = $"legs={legs}\npack={pack}\nsuite={suite}\n"; + if (options.TryGetValue("output", out var file)) File.AppendAllText(file, answers); + else Console.Write(answers); + + if (options.TryGetValue("summary", out var summary)) + { + var text = new StringBuilder($"### Lanes: {Table.Describe(lanes)}\n\n{char.ToUpperInvariant(why[0])}{why[1..]}.\n\n"); + if (classified.Count > 0) + { + text.Append("| Path | Class | Lanes |\n|---|---|---|\n"); + foreach (var (path, @class) in classified.Take(200)) text.Append($"| `{path}` | {@class.Name} | {Table.Describe(@class.Lanes)} |\n"); + if (classified.Count > 200) text.Append($"\nand {classified.Count - 200} more paths.\n"); + } + File.AppendAllText(summary, text.ToString()); + } + return 0; + } + + // git in the repository root, its stdout (or stderr, on a failure) as text. + static int Git(string root, out string output, params string[] arguments) + { + var psi = new ProcessStartInfo("git") { WorkingDirectory = root, RedirectStandardOutput = true, RedirectStandardError = true }; + foreach (var argument in arguments) psi.ArgumentList.Add(argument); + using var process = Process.Start(psi)!; + var stdout = process.StandardOutput.ReadToEndAsync(); + var stderr = process.StandardError.ReadToEndAsync(); + process.WaitForExit(); + output = process.ExitCode == 0 ? stdout.Result : stderr.Result; + return process.ExitCode; + } +} + +static class Gate +{ + // Every job of dotnet.yml the gate needs, and the lane that selects it: null for the changes job, which + // selects, and Linux for the test matrix, which runs in every selection. + static readonly Dictionary Jobs = new(StringComparer.Ordinal) + { + ["changes"] = null, + ["test"] = Lane.Linux, + ["pack"] = Lane.Pack, + ["consumer"] = Lane.Pack, + }; + + public static int Run(string file, Dictionary options) + { + Dictionary Outputs)> needs; + try + { + using var document = JsonDocument.Parse(File.ReadAllText(file)); + needs = document.RootElement.EnumerateObject().ToDictionary( + job => job.Name, + job => (Text(job.Value, "result"), + job.Value.TryGetProperty("outputs", out var outputs) && outputs.ValueKind == JsonValueKind.Object + ? outputs.EnumerateObject().ToDictionary(o => o.Name, o => o.Value.ValueKind == JsonValueKind.String ? o.Value.GetString()! : o.Value.GetRawText()) + : []), + StringComparer.Ordinal); + } + catch (Exception e) when (e is IOException or UnauthorizedAccessException or JsonException or InvalidOperationException) + { + Console.Error.WriteLine($"{file} cannot be read as the workflow's needs: {e.Message}"); + return 2; + } + + var problems = new List(); + foreach (var job in needs.Keys.Where(job => !Jobs.ContainsKey(job))) + problems.Add($"The gate does not know the job '{job}': add it to Gate.Jobs in tools/CiLanes with the lane that selects it."); + foreach (var job in Jobs.Keys.Where(job => !needs.ContainsKey(job))) + problems.Add($"The gate does not need the job '{job}', so it cannot judge it: add it to the CI job's needs."); + + Lane? lanes = null; + var changesResult = needs.TryGetValue("changes", out var changes) ? changes.Result : "missing"; + if (changesResult != "missing") + { + if (changes.Result != "success") + problems.Add($"The changes job ended '{changes.Result}', so nothing it selected can be trusted."); + else + { + var legs = Legs(changes.Outputs.GetValueOrDefault("legs")); + var pack = changes.Outputs.GetValueOrDefault("pack"); + var suite = changes.Outputs.GetValueOrDefault("suite"); + if (legs is null) + problems.Add($"The changes job's legs output is {Quote(changes.Outputs.GetValueOrDefault("legs"))}, not one of the three matrices ci-lanes writes."); + if (pack is not ("true" or "false")) + problems.Add($"The changes job's pack output is {Quote(pack)}, not 'true' or 'false'."); + if (suite is not ("prose" or "full")) + problems.Add($"The changes job's suite output is {Quote(suite)}, not 'prose' or 'full'."); + else if (suite == "prose" && (legs != Lane.Linux || pack != "false")) + problems.Add("The changes job chose the prose suite for a run with more than the Linux leg: only a prose-only change runs it."); + if (legs is not null && pack is ("true" or "false")) + lanes = legs.Value | (pack == "true" ? Lane.Pack : 0); + } + } + + var rows = new List(); + foreach (var (job, lane) in Jobs.Where(j => j.Value is not null)) + { + if (!needs.TryGetValue(job, out var need)) continue; + var selected = lanes is { } l ? l.HasFlag(lane!.Value) : (bool?)null; + rows.Add($"| {job} | {(selected is null ? "unknown" : selected.Value ? "yes" : "no")} | {need.Result} |"); + if (selected is null) continue; + + var expected = selected.Value ? "success" : "skipped"; + if (need.Result != expected) + problems.Add(selected.Value + ? $"The job '{job}' was selected and ended '{need.Result}'." + : $"The job '{job}' was not selected and ended '{need.Result}', not skipped: its condition no longer reads the selection."); + } + + if (options.TryGetValue("summary", out var summary)) + { + var text = new StringBuilder($"### Gate: {(problems.Count == 0 ? "passed" : "failed")}\n\n"); + text.Append($"Changes: {changesResult}, lanes: {(lanes is { } s ? Table.Describe(s) : "unknown")}.\n\n"); + text.Append("| Job | Selected | Result |\n|---|---|---|\n").AppendJoin('\n', rows).Append('\n'); + foreach (var problem in problems) text.Append($"\n- {problem}"); + File.AppendAllText(summary, text.Append('\n').ToString()); + } + + if (problems.Count == 0) + { + Console.WriteLine($"Every selected job passed and every other was skipped (lanes: {Table.Describe(lanes!.Value)})."); + return 0; + } + foreach (var problem in problems) Console.WriteLine($"::error::{problem}"); + return 1; + } + + // The test lanes of every matrix Selection writes: Linux alone, with Windows, and every leg on a run that is not selected. + static readonly Lane[] Matrices = [Lane.Linux, Lane.Linux | Lane.Windows, Lane.Linux | Lane.Windows | Lane.MacOs]; + + // The lanes a legs output selects, when it is exactly one of the matrices Selection writes; null otherwise. + static Lane? Legs(string? legs) + { + string[]? parsed; + try { parsed = legs is null ? null : JsonSerializer.Deserialize(legs); } + catch (JsonException) { return null; } + + if (parsed is null) return null; + foreach (var lanes in Matrices) + if (parsed.SequenceEqual(Table.Legs(lanes))) return lanes; + return null; + } + + static string Text(JsonElement element, string name) => + element.ValueKind == JsonValueKind.Object && element.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.String ? value.GetString()! : ""; + + static string Quote(string? value) => value is null ? "missing" : $"'{value}'"; +} diff --git a/tools/ConsumerSmokeTest/Consumers/walkthrough/OrderEndpoints.cs b/tools/ConsumerSmokeTest/Consumers/walkthrough/OrderEndpoints.cs new file mode 100644 index 00000000..9b3eb015 --- /dev/null +++ b/tools/ConsumerSmokeTest/Consumers/walkthrough/OrderEndpoints.cs @@ -0,0 +1,60 @@ +// The orders service's endpoints. docs/walkthrough.md shows each region; test/CodoMetis.TypeKit.Samples +// compiles this file too, for WalkthroughTests. +using CodoMetis.TypeKit; +using CodoMetis.TypeKit.AspNetCore; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.EntityFrameworkCore; + +namespace Walkthrough; + +// The value objects are declared beside the endpoints, so ASP.NET's route analyzer reads OrderId before the +// generators make it parsable (docs/aspnet-core.md). In a domain project of their own they would need no pragma. +#pragma warning disable ASP0020 +public static class OrderEndpoints +{ + public static void MapOrders(this IEndpointRouteBuilder app) + { + // sample: walkthrough/place + app.MapPost("/orders", async (PlaceOrder request, OrdersDb db, Serilog.ILogger log) => + { + var order = new Order { Id = OrderId.New(), Email = request.Email, Quantity = request.Quantity, Status = OrderStatus.Placed }; + db.Orders.Add(order); + await db.SaveChangesAsync(); + + var view = OrderView.Of(order); + log.Information("Placed {@Order}", view); // Placed {"Id": "…", "Email": "a***@example.com", "Quantity": "2", "Status": "Placed", "$type": "OrderView"} + return TypedResults.Created($"/orders/{order.Id}", view); + }); + // end sample + + // sample: walkthrough/find + app.MapGet("/orders/{id}", async (OrderId id, OrdersDb db) => + (await db.Orders.FirstOrNoneAsync(order => order.Id == id)) // an Option: the order, or none + .ToResult(OrderFault.NotFound) + .Map(OrderView.Of) + .ToHttpResult()) + .WithoutFaults(OrderFault.AlreadyShipped, OrderFault.Cancelled, OrderFault.Cleared); // finding one reports only NotFound + // end sample + + // sample: walkthrough/list + app.MapGet("/orders", async (Quantity minimum, OrdersDb db) => + await db.Orders.Where(order => order.Quantity.Value >= minimum.Value) // WHERE "o"."Quantity" >= @minimum_Value + .Select(order => OrderView.Of(order)) + .ToListAsync()); + // end sample + + // sample: walkthrough/change + app.MapPatch("/orders/{id}", async (OrderId id, OrderPatch patch, OrdersDb db) => + { + var changed = (await db.Orders.FirstOrNoneAsync(order => order.Id == id)) + .ToResult(OrderFault.NotFound) + .Bind(order => order.Apply(patch)); + await db.SaveChangesAsync(); + return changed.Map(OrderView.Of).ToHttpResult(); + }); + // end sample + } +} +#pragma warning restore ASP0020 diff --git a/tools/ConsumerSmokeTest/Consumers/walkthrough/Orders.cs b/tools/ConsumerSmokeTest/Consumers/walkthrough/Orders.cs new file mode 100644 index 00000000..2a489b72 --- /dev/null +++ b/tools/ConsumerSmokeTest/Consumers/walkthrough/Orders.cs @@ -0,0 +1,88 @@ +// The orders service's domain: its value objects, what it stores, and what it exchanges. docs/walkthrough.md +// shows each region; test/CodoMetis.TypeKit.Samples compiles this file too, for WalkthroughTests. +using CodoMetis.TypeKit; +using CodoMetis.TypeKit.ClosedSets; +using CodoMetis.TypeKit.ValueObjects; +using Microsoft.EntityFrameworkCore; + +namespace Walkthrough; + +// sample: walkthrough/values +public readonly partial record struct OrderId : IValue; + +public enum EmailFault { Blank, Hidden, NoAt } + +public readonly partial record struct Email : IValidatedValue +{ + public static Result Create(string value) => + value.AsText() // trimmed, composed, no hidden characters + .MapError(fault => fault == TextFault.Blank ? EmailFault.Blank : EmailFault.Hidden) + .Ensure(text => text.IndexOf('@') > 0, EmailFault.NoAt) + .Map(text => new Email(text)); + + public override string ToString() => $"{Value[0]}***{Value[Value.IndexOf('@')..]}"; // what a log shows +} + +public enum QuantityFault { NotPositive } + +public readonly partial record struct Quantity : IValidatedValue +{ + public static Result Create(int value) => + value > 0 ? new Quantity(value) : Result.Error(QuantityFault.NotPositive); +} + +public sealed partial class OrderStatus : IClosedSet +{ + public static readonly OrderStatus Placed = new(); + public static readonly OrderStatus Shipped = new(); + public static readonly OrderStatus Cancelled = new(); +} +// end sample + +// sample: walkthrough/storage +public sealed class Order +{ + public required OrderId Id { get; init; } + public required Email Email { get; set; } + public required Quantity Quantity { get; set; } + public required OrderStatus Status { get; set; } +} + +public sealed class OrdersDb(DbContextOptions options) : DbContext(options) +{ + public DbSet Orders => Set(); +} +// end sample + +// sample: walkthrough/messages +public sealed record PlaceOrder(Email Email, Quantity Quantity); + +public sealed record OrderView(OrderId Id, Email Email, Quantity Quantity, OrderStatus Status) +{ + public static OrderView Of(Order order) => new(order.Id, order.Email, order.Quantity, order.Status); +} + +public enum OrderFault { NotFound, AlreadyShipped, Cancelled, Cleared } +// end sample + +// sample: walkthrough/patch +public sealed record OrderPatch +{ + public Patch Quantity { get; init; } + public Patch Status { get; init; } +} + +public static class OrderPatching +{ + public static Result Apply(this Order order, OrderPatch patch) + { + if (order.Status == OrderStatus.Shipped) return Result.Error(OrderFault.AlreadyShipped); + if (order.Status == OrderStatus.Cancelled) return Result.Error(OrderFault.Cancelled); + if (patch.Quantity.State == PatchState.Null || patch.Status.State == PatchState.Null) return Result.Error(OrderFault.Cleared); // neither field can be cleared + + patch.Quantity.Tap(quantity => order.Quantity = quantity); + patch.Status.Tap(status => order.Status = status); + return order; + } +} +// end sample diff --git a/tools/ConsumerSmokeTest/Consumers/walkthrough/Program.cs b/tools/ConsumerSmokeTest/Consumers/walkthrough/Program.cs new file mode 100644 index 00000000..85b0c4f5 --- /dev/null +++ b/tools/ConsumerSmokeTest/Consumers/walkthrough/Program.cs @@ -0,0 +1,63 @@ +// The orders service docs/walkthrough.md follows: its host, started on a free port, sent the page's requests by +// Tour.cs. Each answer is printed as name=value, the SQL EF Core runs and the log lines as they are written, and +// the smoke test compares them with what the page states. +// sample: walkthrough/usings +using CodoMetis.TypeKit.AspNetCore; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Serilog; +using Walkthrough; +// end sample + +// sample: walkthrough/setup +var builder = WebApplication.CreateBuilder(args); +// end sample +builder.WebHost.UseUrls("http://127.0.0.1:0"); +builder.Logging.ClearProviders(); +// sample: walkthrough/setup + +builder.Services.AddOpenApi(options => options.AddTypeKit()); +builder.Services.AddFaultResponses(fault => fault switch +{ + OrderFault.NotFound => StatusCodes.Status404NotFound, + OrderFault.AlreadyShipped or OrderFault.Cancelled => StatusCodes.Status409Conflict, + OrderFault.Cleared => StatusCodes.Status422UnprocessableEntity, + _ => throw new ArgumentOutOfRangeException(nameof(fault)), +}); +builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.RespectRequiredConstructorParameters = true); + +var connection = new SqliteConnection("Data Source=:memory:"); // the database lives as long as this connection is open +connection.Open(); +builder.Services.AddDbContext(options => options.UseSqlite(connection).UseTypeKit() + .LogTo(Console.WriteLine, [RelationalEventId.CommandExecuted])); + +builder.Services.AddSingleton(new LoggerConfiguration() + .Destructure.WithTypeKit() + .WriteTo.Console(outputTemplate: "[{Level:u3}] {Message:lj}{NewLine}") + .CreateLogger()); +// end sample + +// sample: walkthrough/app +var app = builder.Build(); +app.UseStatusCodePages(); +app.MapOpenApi(); +app.MapOrders(); + +using (var scope = app.Services.CreateScope()) +{ + scope.ServiceProvider.GetRequiredService().Database.EnsureCreated(); + // CREATE TABLE "Orders" ( + // "Id" TEXT NOT NULL CONSTRAINT "PK_Orders" PRIMARY KEY, + // "Email" TEXT NOT NULL, + // "Quantity" INTEGER NOT NULL, + // "Status" TEXT NOT NULL + // ); +} +// end sample + +await app.StartAsync(); +using var http = new HttpClient { BaseAddress = new Uri(app.Urls.First()) }; +await Tour.RunAsync(http, (name, value) => Console.WriteLine($"{name}={value}")); +await app.StopAsync(); +connection.Dispose(); diff --git a/tools/ConsumerSmokeTest/Consumers/walkthrough/Tour.cs b/tools/ConsumerSmokeTest/Consumers/walkthrough/Tour.cs new file mode 100644 index 00000000..bf89dba3 --- /dev/null +++ b/tools/ConsumerSmokeTest/Consumers/walkthrough/Tour.cs @@ -0,0 +1,90 @@ +// The requests docs/walkthrough.md follows, sent to a running orders service. Each answer the page states in a +// comment is reported under the name of the variable that holds it, and the smoke test and WalkthroughTests +// compare the two (test/CodoMetis.TypeKit.Samples compiles this file too). +using System.Text; +using CodoMetis.TypeKit; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Walkthrough; + +/// An HTTP answer as the page writes it: the status, a space, the body. +public sealed record Answer(int Status, string Body) +{ + public override string ToString() => $"{Status} {Body}"; +} + +public static class Tour +{ + public static async Task RunAsync(HttpClient http, Action report) + { + async Task Send(string method, string path, string? json = null) + { + using var request = new HttpRequestMessage(new HttpMethod(method), path); + if (json is not null) request.Content = new StringContent(json, Encoding.UTF8, "application/json"); + using var response = await http.SendAsync(request); + return new Answer((int)response.StatusCode, await response.Content.ReadAsStringAsync()); + } + + // sample: walkthrough/place-request + Answer placed = await Send("POST", "/orders", """{"email":" ann@example.com ","quantity":2}"""); // 201 {"id":"…","email":"ann@example.com","quantity":2,"status":"Placed"} + OrderId id = JsonSerializer.Deserialize(placed.Body, JsonSerializerOptions.Web)!.Id; + // end sample + report(nameof(placed), placed.ToString()); + + // sample: walkthrough/refused + Answer noAt = await Send("POST", "/orders", """{"email":"ann","quantity":2}"""); // 400 {"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"Bad Request","status":400,"traceId":"…"} + Answer noEmail = await Send("POST", "/orders", """{"quantity":2}"""); // 400 {"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"Bad Request","status":400,"traceId":"…"} + Answer notAnId = await Send("GET", "/orders/42"); // 400 {"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"Bad Request","status":400,"traceId":"…"} + Answer noneAtAll = await Send("GET", "/orders?minimum=0"); // 400 {"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"Bad Request","status":400,"traceId":"…"} + // end sample + report(nameof(noAt), noAt.ToString()); + report(nameof(noEmail), noEmail.ToString()); + report(nameof(notAnId), notAnId.ToString()); + report(nameof(noneAtAll), noneAtAll.ToString()); + + // sample: walkthrough/find-request + Answer found = await Send("GET", $"/orders/{id}"); // 200 {"id":"…","email":"ann@example.com","quantity":2,"status":"Placed"} + Answer missing = await Send("GET", $"/orders/{OrderId.New()}"); // 404 {"type":"https://tools.ietf.org/html/rfc9110#section-15.5.5","title":"Not Found","status":404,"fault":"NotFound","traceId":"…"} + // end sample + report(nameof(found), found.ToString()); + report(nameof(missing), missing.ToString()); + + // sample: walkthrough/list-request + Answer atLeastTwo = await Send("GET", "/orders?minimum=2"); // 200 [{"id":"…","email":"ann@example.com","quantity":2,"status":"Placed"}] + Answer atLeastFive = await Send("GET", "/orders?minimum=5"); // 200 [] + // end sample + report(nameof(atLeastTwo), atLeastTwo.ToString()); + report(nameof(atLeastFive), atLeastFive.ToString()); + + // sample: walkthrough/change-request + Answer three = await Send("PATCH", $"/orders/{id}", """{"quantity":3}"""); // 200 {"id":"…","email":"ann@example.com","quantity":3,"status":"Placed"} + Answer zero = await Send("PATCH", $"/orders/{id}", """{"quantity":0}"""); // 400 {"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"Bad Request","status":400,"traceId":"…"} + Answer cleared = await Send("PATCH", $"/orders/{id}", """{"quantity":null}"""); // 422 {"type":"https://tools.ietf.org/html/rfc4918#section-11.2","title":"Unprocessable Entity","status":422,"fault":"Cleared","traceId":"…"} + Answer shipped = await Send("PATCH", $"/orders/{id}", """{"status":"Shipped"}"""); // 200 {"id":"…","email":"ann@example.com","quantity":3,"status":"Shipped"} + Answer tooLate = await Send("PATCH", $"/orders/{id}", """{"quantity":4}"""); // 409 {"type":"https://tools.ietf.org/html/rfc9110#section-15.5.10","title":"Conflict","status":409,"fault":"AlreadyShipped","traceId":"…"} + // end sample + report(nameof(three), three.ToString()); + report(nameof(zero), zero.ToString()); + report(nameof(cleared), cleared.ToString()); + report(nameof(shipped), shipped.ToString()); + report(nameof(tooLate), tooLate.ToString()); + + // sample: walkthrough/document + JsonNode document = JsonNode.Parse(await http.GetStringAsync("/openapi/v1.json"))!; + JsonNode schemas = document["components"]!["schemas"]!; + JsonNode find = document["paths"]!["/orders/{id}"]!["get"]!; + + string idParameter = find["parameters"]![0]!["schema"]!.ToJsonString(); // {"type":"string","format":"uuid"} + string findAnswers = string.Join(",", find["responses"]!.AsObject().Select(answer => answer.Key)); // 200,404 + string emailSchema = schemas["Email"]!.ToJsonString(); // {"type":"string"} + string statusSchema = schemas["OrderStatus"]!.ToJsonString(); // {"enum":["Placed","Shipped","Cancelled"],"type":"string"} + string patchType = schemas["OrderPatch"]!["properties"]!["quantity"]!["type"]!.ToJsonString(); // ["null","integer","string"] + // end sample + report(nameof(idParameter), idParameter); + report(nameof(findAnswers), findAnswers); + report(nameof(emailSchema), emailSchema); + report(nameof(statusSchema), statusSchema); + report(nameof(patchType), patchType); + } +} diff --git a/tools/ConsumerSmokeTest/Program.cs b/tools/ConsumerSmokeTest/Program.cs index df978a2b..6365952a 100644 --- a/tools/ConsumerSmokeTest/Program.cs +++ b/tools/ConsumerSmokeTest/Program.cs @@ -12,7 +12,7 @@ // consumer builds), compiles real code against them, runs it, and asserts on what it prints, on the // diagnostics the build reports, and on the resolved package graph. Never on the exit code alone. // -// Four consumers, their sources under Consumers/: +// Five consumers, their sources under Consumers/: // core references only CodoMetis.TypeKit: Option and Result work, no Metalama arrives, and // CMTK0001 and CMTK0002 fire (design/analyzers.md). // layered a domain library references CodoMetis.TypeKit.Generators, and an app reaches everything @@ -25,13 +25,17 @@ // aot the same ground published with Native AOT (design/native-aot.md): no trim or AOT warning // from a package or from the code woven into the consumer, and the native binary does // what the JIT build does. +// walkthrough the orders service docs/walkthrough.md follows, on all four packages a web service uses: every +// answer, log line and SQL statement the page states in a comment is what it gives, read from the page +// (WalkthroughPage.cs). // // Native AOT needs the platform linker: clang (and zlib on Linux), which the workflows install, or // MSVC's C++ build tools on Windows. // -// Usage: dotnet tools/ConsumerSmokeTest/bin/Release/net10.0/consumer-smoke-test.dll [feed-directory] +// Usage: dotnet tools/ConsumerSmokeTest/bin/Release/net10.0/consumer-smoke-test.dll [feed-directory] [--only ] // With no argument the packages are packed fresh. Pass a directory of existing .nupkg files (the -// release workflow passes its pack output) to test exactly the artifacts being shipped. +// release workflow passes its pack output) to test exactly the artifacts being shipped. --only runs one +// consumer, for a local check: CI runs them all. using System.Diagnostics; using System.Text; using System.Text.RegularExpressions; @@ -43,19 +47,22 @@ Console.Error.WriteLine($"consumer-smoke-test: this dll was built from other sources than the checkout holds ({stale}). Run `{SourceFingerprint.RebuildCommand}`."); return SourceFingerprint.StaleExitCode; } -if (args.Length > 1) +var at = Array.IndexOf(args, "--only"); +var only = at >= 0 && at + 1 < args.Length ? args[at + 1] : null; +string[] positional = at >= 0 ? [.. args[..at], .. args[Math.Min(at + 2, args.Length)..]] : args; +if (positional.Length > 1 || (at >= 0 && only is not ("core" or "layered" or "host" or "aot" or "walkthrough"))) { - Console.Error.WriteLine("Usage: consumer-smoke-test.dll [feed-directory]"); + Console.Error.WriteLine("Usage: consumer-smoke-test.dll [feed-directory] [--only core|layered|host|aot|walkthrough]"); return 2; } -return new ConsumerSmokeTest.Smoke(root!, args.FirstOrDefault()).Run(); +return new ConsumerSmokeTest.Smoke(root!, positional.FirstOrDefault(), only).Run(); namespace ConsumerSmokeTest { /// A command that failed where it had to succeed, or the reverse: the run stops, as the script's set -e did. internal sealed class Stop : Exception; - internal sealed partial class Smoke(string repoRoot, string? givenFeed) + internal sealed partial class Smoke(string repoRoot, string? givenFeed, string? only) { private readonly DirectoryInfo _created = Directory.CreateTempSubdirectory("consumer-smoke-test-"); private string Work => _created.FullName; @@ -184,10 +191,12 @@ private void Main() """); File.Copy(Path.Combine(repoRoot, "global.json"), Path.Combine(Work, "global.json")); - Core(); - Layered(); - Host(); - Aot(); + // host builds on layered's Domain project, so it brings layered along. + if (only is null or "core") Core(); + if (only is null or "layered" or "host") Layered(); + if (only is null or "host") Host(); + if (only is null or "aot") Aot(); + if (only is null or "walkthrough") Walkthrough(); } // ── core: the base package alone ─────────────────────────────────────────────────────────────── @@ -491,6 +500,87 @@ private void AotPublishAndRun(string name) File.AppendAllText(Path.Combine(Work, "aot.out"), Run(native) + "\n"); } + // ── walkthrough: the orders service of docs/walkthrough.md ───────────────────────────────────── + // + // One web project on the four packages a service uses, as a reader would set it up: what the page shows is + // what it compiles. It prints each answer as name=value, the SQL EF Core runs and its log lines, and every + // expectation is read from the page's comments (WalkthroughPage), never written here: a changed answer fails + // until the page says it. ReadmeSampleTests holds the page's blocks to these sources, so an edit to a stated + // answer is an edit here too, which selects the pack lane; WalkthroughTests runs the same service in memory + // on every test leg. + + private void Walkthrough() + { + Console.WriteLine("==> walkthrough: the orders service docs/walkthrough.md follows"); + NewProject("web", "walkthrough", "CodoMetis.TypeKit.Generators", "CodoMetis.TypeKit.EntityFrameworkCore", "CodoMetis.TypeKit.AspNetCore", "CodoMetis.TypeKit.Serilog"); + Run("dotnet", "add", "package", "Microsoft.EntityFrameworkCore.Sqlite", "--version", CentralVersion("Microsoft.EntityFrameworkCore.Relational")); + Run("dotnet", "add", "package", "Microsoft.AspNetCore.OpenApi", "--version", CentralVersion("Microsoft.AspNetCore.OpenApi")); + Run("dotnet", "add", "package", "Serilog.Sinks.Console", "--version", CentralVersion("Serilog.Sinks.Console")); + foreach (var source in (string[])["Orders.cs", "OrderEndpoints.cs", "Tour.cs", "Program.cs"]) Write(source, $"walkthrough/{source}"); + + Run("dotnet", "build"); + ListPackages("walkthrough.packages"); + Save("walkthrough.out", Run("dotnet", "run", "--no-build", "--no-launch-profile")); + Print("walkthrough.out"); + + Console.WriteLine("==> walkthrough: asserting against docs/walkthrough.md"); + AssertContains("> CodoMetis.TypeKit.Generators ", "walkthrough.packages", "the generators resolved from the feed"); + AssertContains("> CodoMetis.TypeKit.Serilog ", "walkthrough.packages", "the Serilog package resolved from the feed"); + AssertVersions("walkthrough.packages"); + + var page = File.ReadAllText(Path.Combine(repoRoot, WalkthroughPage.Path)); + var lines = SplitLines(Read("walkthrough.out")).ToList(); + var answers = lines.Select(line => AnswerLine().Match(line)).Where(match => match.Success) + .ToDictionary(match => match.Groups["name"].Value, match => match.Groups["value"].Value, StringComparer.Ordinal); + + foreach (var name in WalkthroughPage.Declarations(page).Union(answers.Keys, StringComparer.Ordinal)) + { + var value = answers.GetValueOrDefault(name); + AssertStated(page, WalkthroughPage.Answer(name), stated => value is not null && WalkthroughPage.Is(stated, value), + value is null ? $"{name} is stated, and the service never answered it" : $"{name} is what the page states"); + } + var logged = lines.Where(line => line.StartsWith("[INF] ", StringComparison.Ordinal)).ToList(); + AssertStated(page, "log.Information(\"Placed", stated => WalkthroughPage.Within(stated, logged), "the log line is what the page states: the address masked"); + AssertStated(page, "order.Quantity.Value >= minimum.Value", stated => WalkthroughPage.Within(stated, lines), ".Value in the query is the column in the SQL EF Core logs"); + AssertStated(page, "Database.EnsureCreated()", stated => WalkthroughPage.Within(stated, lines), "each value object is a column of its underlying type"); + if (logged.Count == 0 || logged.Any(line => line.Contains("ann@example.com", StringComparison.Ordinal))) + { + Console.WriteLine(" FAIL: the e-mail address is masked in every log line, and there is one"); + foreach (var line in logged) Console.WriteLine(" " + line); + _failed = true; + } + else + { + Console.WriteLine(" ok: the e-mail address is masked in every log line"); + } + } + + // What the page states for the fragment holds; a page that no longer states it fails too. + private void AssertStated(string page, string fragment, Func, bool> holds, string message) + { + IReadOnlyList stated; + try + { + stated = WalkthroughPage.Stated(page, fragment); + } + catch (InvalidOperationException e) + { + Console.WriteLine($" FAIL: {message} ({e.Message})"); + _failed = true; + return; + } + if (holds(stated)) + { + Console.WriteLine($" ok: {message}"); + return; + } + Console.WriteLine($" FAIL: {message} (the page states '{string.Join(" / ", stated)}' for '{fragment.Trim()}')"); + _failed = true; + } + + [GeneratedRegex(@"^(?[a-z]\w*)=(?.*)$")] + private static partial Regex AnswerLine(); + // ── the steps ────────────────────────────────────────────────────────────────────────────────── // Creates a project from a template under the work directory and installs packages into it. No diff --git a/tools/ConsumerSmokeTest/WalkthroughPage.cs b/tools/ConsumerSmokeTest/WalkthroughPage.cs new file mode 100644 index 00000000..190cf75c --- /dev/null +++ b/tools/ConsumerSmokeTest/WalkthroughPage.cs @@ -0,0 +1,94 @@ +using System.Text.RegularExpressions; + +namespace ConsumerSmokeTest; + +/// +/// What docs/walkthrough.md states in its C# comments: the answers, the log line and the SQL of the orders service +/// its blocks show. The smoke test's walkthrough consumer is that service, and test/CodoMetis.TypeKit.Samples compiles +/// this file for WalkthroughTests, which runs the service in memory; both compare what it gives with this. +/// +/// +/// A statement is the comment after the one line of C# whose code holds a fragment, or the comment lines right below +/// that line. An ellipsis (U+2026) in it stands for any text, such as an order's id or a trace id. The expected text +/// is read from the page, never written into a check, so a changed answer is corrected in the page's comment: and +/// ReadmeSampleTests holds the page's blocks to the consumer's sources, so the line read here is the line it runs. +/// +internal static partial class WalkthroughPage +{ + public const string Path = "docs/walkthrough.md"; + + private const char Ellipsis = (char)0x2026; + + /// The statement for , one entry per line; throws when no single line holds it, or it states nothing. + public static IReadOnlyList Stated(string page, string fragment) + { + var lines = CSharpLines(page).ToArray(); + var matches = Enumerable.Range(0, lines.Length).Where(i => CodeOf(lines[i]).Contains(fragment, StringComparison.Ordinal)).ToArray(); + if (matches.Length != 1) throw new InvalidOperationException($"{Path} should have one line of C# holding '{fragment}', but has {matches.Length}."); + + if (CommentOf(lines[matches[0]]) is { } comment) return [comment]; + + var below = lines.Skip(matches[0] + 1).TakeWhile(line => line.TrimStart().StartsWith("//", StringComparison.Ordinal)) + .Select(line => line.TrimStart()[2..].Trim()).ToArray(); + return below.Length > 0 ? below : throw new InvalidOperationException($"{Path} states nothing for '{fragment}'."); + } + + /// The answers the page states: each Answer or string declared on a line with a comment, by the variable's name. + public static IReadOnlyList Declarations(string page) => + [ + .. CSharpLines(page).Where(line => CommentOf(line) is not null) + .Select(line => Declaration().Match(CodeOf(line))) + .Where(match => match.Success) + .Select(match => match.Groups["name"].Value) + ]; + + /// The fragment that finds the statement of a declared answer: its name and the assignment. + public static string Answer(string name) => $" {name} = "; + + /// Whether is the one-line statement, whole. + public static bool Is(IReadOnlyList stated, string value) => stated.Count == 1 && Pattern(stated[0], whole: true).IsMatch(value); + + /// Whether the stated lines are consecutive lines of , each ending in its statement. + public static bool Within(IReadOnlyList stated, IReadOnlyList lines) + { + var patterns = stated.Select(line => Pattern(line, whole: false)).ToArray(); + return Enumerable.Range(0, Math.Max(0, lines.Count - patterns.Length + 1)) + .Any(start => patterns.Select((pattern, i) => pattern.IsMatch(lines[start + i].Trim())).All(matched => matched)); + } + + private static Regex Pattern(string statement, bool whole) => + new((whole ? "^" : "") + string.Join(".*?", statement.Split(Ellipsis).Select(Regex.Escape)) + "$", RegexOptions.CultureInvariant); + + private static IEnumerable CSharpLines(string page) + { + var inCSharp = false; + foreach (var line in page.ReplaceLineEndings("\n").Split('\n')) + { + if (line.TrimStart().StartsWith("```", StringComparison.Ordinal)) inCSharp = !inCSharp && line.TrimStart()[3..].Trim() is "csharp" or "cs"; + else if (inCSharp) yield return line; + } + } + + private static string CodeOf(string line) => (CommentStart(line) is var start and >= 0 ? line[..start] : line).Trim(); + + // The comment after code on the same line; a line that is only a comment has none. + private static string? CommentOf(string line) => + CommentStart(line) is var start and >= 0 && line[..start].Trim().Length > 0 ? line[(start + 2)..].Trim() : null; + + // Where a line's // comment starts, outside any string literal. + private static int CommentStart(string line) + { + var inString = false; + for (var i = 0; i < line.Length - 1; i++) + { + if (inString && line[i] == '\\') i++; + else if (line[i] == '"') inString = !inString; + else if (!inString && line[i] == '/' && line[i + 1] == '/') return i; + } + + return -1; + } + + [GeneratedRegex(@"^(?:Answer|string) (?\w+) = ")] + private static partial Regex Declaration(); +} diff --git a/tools/Directory.Build.props b/tools/Directory.Build.props index e87f7909..443b70ff 100644 --- a/tools/Directory.Build.props +++ b/tools/Directory.Build.props @@ -9,7 +9,7 @@ false false - + **/*.cs diff --git a/tools/DocsQuery/Citations.cs b/tools/DocsQuery/Citations.cs deleted file mode 100644 index ef97fba4..00000000 --- a/tools/DocsQuery/Citations.cs +++ /dev/null @@ -1,260 +0,0 @@ -using System.Globalization; -using System.Text; -using System.Text.Json; -using System.Text.RegularExpressions; - -namespace DocsQuery; - -/// -/// Where the probe corpus leans on a verified fact to prune a row or excuse one (design/adr/0107): a refused -/// entry of the axes file whose reason names KB-NNN, a not-promised entry of claims.json whose -/// source is a fact's file or which says fact:KB-NNN, an obligation's declaration of where it does not -/// apply whose cite is fact:KB-NNN, and a Cite.Fact("KB-NNN") or Baseline.Fact("KB-NNN") -/// literal in the harness and probe sources, read as text with comments blanked out. Each is read from the checkout -/// at every check, never from the index. -/// -internal static partial class FactCitations -{ - public const string AxesFile = ".claude/probe-axes.md", ClaimsFile = "tools/probe/corpus/claims.json"; - - public static readonly string[] SourceDirectories = ["tools/probe/corpus/harness", "tools/probe/corpus/probes"]; - - /// The four places, in the order check counts them. - public static readonly string[] Kinds = [Refused, NotPromised, Obligation, Harness]; - - public const string Refused = "refused", NotPromised = "not-promised", Obligation = "obligation", Harness = "harness"; - - /// - /// One citation: the kind of place, where it is written (path:line), the fact's id, the place in words, and - /// whether it reads the fact's fixture as a baseline (Baseline.Fact), which takes a fixture as well as a test. - /// - internal sealed record Citation(string Kind, string At, string Fact, string Place, bool Baseline = false); - - /// What a scan read: every citation, and how many places of each kind it looked at, cited or not. - internal sealed record Scan(IReadOnlyList Citations, IReadOnlyDictionary Read) - { - public int Of(string kind) => Citations.Count(citation => citation.Kind == kind); - } - - public static Scan Find(string root) - { - var citations = new List(); - var read = Kinds.ToDictionary(kind => kind, _ => 0, StringComparer.Ordinal); - - ReadAxes(root, citations, read); - ReadClaims(root, citations, read); - ReadSources(root, citations, read); - - return new Scan(citations, read); - } - - /// The axes file's refused entries: axis to value to the reason the framework refuses it. - private static void ReadAxes(string root, List citations, Dictionary read) - { - var path = Path.Combine(root, AxesFile); - if (!File.Exists(path)) return; - - const string marker = "```json probe-axes"; - var text = File.ReadAllText(path).ReplaceLineEndings("\n"); - var start = text.IndexOf(marker, StringComparison.Ordinal); - if (start < 0) return; - start += marker.Length; - var end = text.IndexOf("```", start, StringComparison.Ordinal); - if (end < 0) return; - - using var block = JsonDocument.Parse(text[start..end]); - if (!block.RootElement.TryGetProperty("refused", out var refused) || refused.ValueKind != JsonValueKind.Object) return; - - var cursor = new Cursor(AxesFile, text, text.IndexOf("\"refused\"", start, StringComparison.Ordinal)); - foreach (var axis in refused.EnumerateObject().Where(axis => axis.Value.ValueKind == JsonValueKind.Object)) - { - foreach (var value in axis.Value.EnumerateObject()) - { - read[Refused]++; - var place = $"the refused entry {axis.Name}={value.Name}"; - cursor.Seek($"\"{value.Name}\""); - foreach (var fact in FactIds(value.Value.ValueKind == JsonValueKind.String ? value.Value.GetString()! : value.Value.GetRawText())) - citations.Add(new Citation(Refused, cursor.At(fact), fact, place)); - } - } - } - - /// Each claim's not-promised entries and its obligations' inapplicable declarations. - private static void ReadClaims(string root, List citations, Dictionary read) - { - var path = Path.Combine(root, ClaimsFile); - if (!File.Exists(path)) return; - - var text = File.ReadAllText(path).ReplaceLineEndings("\n"); - using var claims = JsonDocument.Parse(text); - if (claims.RootElement.ValueKind != JsonValueKind.Array) return; - - // Walked in the file's order, so each citation's line is the next occurrence of its id from the last one. - var cursor = new Cursor(ClaimsFile, text, 0); - foreach (var claim in claims.RootElement.EnumerateArray()) - { - var id = Str(claim, "id"); - cursor.Seek($"\"{id}\""); - - foreach (var property in claim.EnumerateObject()) - { - if (property.NameEquals("obligations") && property.Value.ValueKind == JsonValueKind.Array) - { - foreach (var obligation in property.Value.EnumerateArray()) - { - cursor.Seek($"\"{Str(obligation, "id")}\""); - var number = 0; - foreach (var declaration in Array(obligation, "inapplicable")) - { - read[Obligation]++; - number++; - if (FactOf(Str(declaration, "cite")) is not { } fact) continue; - citations.Add(new Citation(Obligation, cursor.At("fact:" + fact), fact, - $"claim {id}, obligation {Str(obligation, "id")}, inapplicable declaration {number.ToString(CultureInfo.InvariantCulture)}")); - } - } - } - else if (property.NameEquals("not-promised") && property.Value.ValueKind == JsonValueKind.Array) - { - foreach (var entry in property.Value.EnumerateArray()) - { - read[NotPromised]++; - cursor.Seek($"\"{Str(entry, "id")}\""); - var place = $"claim {id}, not-promised {Str(entry, "id")}"; - var facts = new List(); - if (KnowledgeSource().Match(Str(entry, "source")) is { Success: true } source) facts.Add(source.Groups["id"].Value); - foreach (var field in entry.EnumerateObject().Where(field => field.Value.ValueKind == JsonValueKind.String)) - facts.AddRange(CiteFact().Matches(field.Value.GetString()!).Select(match => match.Groups["id"].Value)); - - foreach (var fact in facts.Distinct(StringComparer.Ordinal)) - citations.Add(new Citation(NotPromised, cursor.At(fact), fact, place)); - } - } - } - } - } - - /// The harness's and the probes' Cite.Fact and Baseline.Fact literals, comments blanked out. - private static void ReadSources(string root, List citations, Dictionary read) - { - foreach (var directory in SourceDirectories.Select(directory => Path.Combine(root, directory)).Where(Directory.Exists)) - { - foreach (var file in Directory.EnumerateFiles(directory, "*.cs").Order(StringComparer.Ordinal)) - { - read[Harness]++; - var path = Corpus.Relative(root, file); - var source = File.ReadAllText(file).ReplaceLineEndings("\n"); - foreach (Match match in FactLiteral().Matches(WithoutComments(source))) - { - var line = 1 + source.AsSpan(0, match.Index).Count('\n'); - citations.Add(new Citation(Harness, $"{path}:{line.ToString(CultureInfo.InvariantCulture)}", match.Groups["id"].Value, - $"{match.Groups["factory"].Value}.Fact(\"{match.Groups["id"].Value}\")", match.Groups["factory"].Value == "Baseline")); - } - } - } - } - - /// - /// The source with each comment's content replaced by spaces, newlines kept, and string literals as they are, so a - /// citation written in a comment does not count and a line number still holds. - /// - internal static string WithoutComments(string source) - { - var result = new StringBuilder(source); - for (var i = 0; i < source.Length; i++) - { - if (source[i] == '/' && i + 1 < source.Length && source[i + 1] == '/') - { - for (; i < source.Length && source[i] != '\n'; i++) result[i] = ' '; - } - else if (source[i] == '/' && i + 1 < source.Length && source[i + 1] == '*') - { - var close = source.IndexOf("*/", i + 2, StringComparison.Ordinal); - var stop = close < 0 ? source.Length : close + 2; - for (; i < stop; i++) if (source[i] != '\n') result[i] = ' '; - i--; - } - else if (source[i] == '"') - { - i = AfterString(source, i); - } - else if (source[i] == '\'') - { - // A character literal: '"' must not open a string. - var j = i + 1; - if (j < source.Length && source[j] == '\\') j++; - if (j + 1 < source.Length && source[j + 1] == '\'') i = j + 1; - } - } - - return result.ToString(); - } - - /// The index of the closing quote of the string literal opened at : raw, verbatim or regular. - private static int AfterString(string source, int i) - { - var quotes = 0; - while (i + quotes < source.Length && source[i + quotes] == '"') quotes++; - - if (quotes >= 3) - { - var close = source.IndexOf(new string('"', quotes), i + quotes, StringComparison.Ordinal); - return close < 0 ? source.Length : close + quotes - 1; - } - - if (quotes == 2) return i + 1; // "" - - var verbatim = i > 0 && source[i - 1] == '@' || i > 1 && source[i - 1] == '$' && source[i - 2] == '@'; - for (var j = i + 1; j < source.Length; j++) - { - if (verbatim && source[j] == '"' && j + 1 < source.Length && source[j + 1] == '"') { j++; continue; } - if (!verbatim && source[j] == '\\') { j++; continue; } - if (source[j] == '"' || !verbatim && source[j] == '\n') return j; - } - - return source.Length; - } - - /// The ids a refused entry's reason names, as fact:KB-NNN or plain KB-NNN. - private static IEnumerable FactIds(string text) => AnyFact().Matches(text).Select(match => match.Value).Distinct(StringComparer.Ordinal); - - private static string? FactOf(string cite) => cite.StartsWith("fact:", StringComparison.Ordinal) ? cite["fact:".Length..] : null; - - private static string Str(JsonElement element, string name) => - element.ValueKind == JsonValueKind.Object && element.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.String ? value.GetString()! : ""; - - private static IEnumerable Array(JsonElement element, string name) => - element.ValueKind == JsonValueKind.Object && element.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.Array ? value.EnumerateArray() : []; - - /// Line numbers for places read in a file's order: each needle is looked for from the last place found. - private sealed class Cursor(string path, string text, int offset) - { - private int at = Math.Max(0, offset); - - public void Seek(string needle) - { - var found = text.IndexOf(needle, at, StringComparison.Ordinal); - if (found >= 0) at = found; - } - - public string At(string needle) - { - var found = text.IndexOf(needle, at, StringComparison.Ordinal); - var line = 1 + text.AsSpan(0, found >= 0 ? found : at).Count('\n'); - if (found >= 0) at = found + needle.Length; - return $"{path}:{line.ToString(CultureInfo.InvariantCulture)}"; - } - } - - [GeneratedRegex(@"\bKB-\d{3}\b")] - private static partial Regex AnyFact(); - - [GeneratedRegex(@"\bfact:\s?(?KB-\d{3})\b")] - private static partial Regex CiteFact(); - - [GeneratedRegex(@"^design/knowledge/(?KB-\d{3})-[^/#]*\.md(#.*)?$")] - private static partial Regex KnowledgeSource(); - - [GeneratedRegex(@"\b(?Cite|Baseline)\s*\.\s*Fact\s*\(\s*""(?KB-\d{3})""\s*\)")] - private static partial Regex FactLiteral(); -} diff --git a/tools/DocsQuery/Corpus.cs b/tools/DocsQuery/Corpus.cs index 01bd3b1e..d294e68d 100644 --- a/tools/DocsQuery/Corpus.cs +++ b/tools/DocsQuery/Corpus.cs @@ -95,17 +95,24 @@ void Add(string relativeDirectory, string pattern, SearchOption option) .Order(StringComparer.Ordinal)]; } - /// The samples project's C# files, whose // sample: regions compile the pages' blocks. - public static IReadOnlyList Samples(string root) - { - var directory = Path.Combine(root, SamplesProject); - if (!Directory.Exists(directory)) return []; - - return [.. Directory.EnumerateFiles(directory, "*.cs", SearchOption.AllDirectories) - .Select(path => Relative(root, path)) - .Where(path => !InBuildOutput(path)) - .Order(StringComparer.Ordinal)]; - } + /// + /// Where a page keeps its regions outside the samples project: the walkthrough's sample application, the smoke + /// test's consumer that the packed packages build. ReadmeSamples.RegionSources in the conventions tests is the + /// same list. + /// + public static readonly string[] PageSources = ["tools/ConsumerSmokeTest/Consumers/walkthrough"]; + + /// The C# files whose // sample: regions compile the pages' blocks: the samples project's and each page's own sources. + public static IReadOnlyList Samples(string root) => + [ + .. new[] { SamplesProject }.Concat(PageSources) + .Select(relative => Path.Combine(root, relative)) + .Where(Directory.Exists) + .SelectMany(directory => Directory.EnumerateFiles(directory, "*.cs", SearchOption.AllDirectories)) + .Select(path => Relative(root, path)) + .Where(path => !InBuildOutput(path)) + .Order(StringComparer.Ordinal) + ]; /// /// The fingerprint of an index: the schema version, the tool's own version (the hash of its diff --git a/tools/DocsQuery/Facts.cs b/tools/DocsQuery/Facts.cs index 9f5d81ec..d4f051eb 100644 --- a/tools/DocsQuery/Facts.cs +++ b/tools/DocsQuery/Facts.cs @@ -83,9 +83,25 @@ public static (State State, string Detail) Compare(string pin, Installed install }; } + /// + /// An external {name} {version} pin's name and version, where the version is one (10.6.0, + /// 5.0.0-beta-78), or null: another kind of pin, or an external one dated or described rather than + /// versioned (external NuGet registration data). A dated pin, external GitHub-Actions 2026-10-29, + /// reads as a version: that NuGet has no package of the name is what keeps it from being compared. + /// + public static (string Name, string Version)? External(string pin) + { + var parts = pin.Split(' ', StringSplitOptions.RemoveEmptyEntries); + return parts is ["external", var name, var version, ..] && VersionText().IsMatch(version) ? (name, version) : null; + } + + /// Whether a version is a pre-release: a label after a dash. + public static bool IsPreRelease(string version) => version.Contains('-', StringComparison.Ordinal); + /// /// Orders two versions: numeric parts numerically, then a release after any of its pre-releases, and - /// pre-release labels ordinally. Enough for the versions Directory.Packages.props holds. + /// pre-release labels as SemVer 2 and NuGet order them. Enough for the versions Directory.Packages.props + /// holds and the ones NuGet lists. /// public static int CompareVersions(string left, string right) { @@ -107,9 +123,30 @@ public static int CompareVersions(string left, string right) (null, null) => 0, (null, _) => 1, (_, null) => -1, - _ => Math.Sign(string.CompareOrdinal(leftLabel, rightLabel)), + _ => Labels(leftLabel, rightLabel), }; + // Dot-separated identifiers: numeric ones numerically and before an alphanumeric one, alphanumeric ones + // ignoring case, and a label before a longer one it starts (beta.2 before beta.10, beta before beta.1). + static int Labels(string left, string right) + { + var (a, b) = (left.Split('.'), right.Split('.')); + + for (var i = 0; i < Math.Min(a.Length, b.Length); i++) + { + var order = (long.TryParse(a[i], NumberStyles.None, CultureInfo.InvariantCulture, out var x), long.TryParse(b[i], NumberStyles.None, CultureInfo.InvariantCulture, out var y)) switch + { + (true, true) => x.CompareTo(y), + (true, false) => -1, + (false, true) => 1, + _ => string.Compare(a[i], b[i], StringComparison.OrdinalIgnoreCase), + }; + if (order != 0) return Math.Sign(order); + } + + return a.Length.CompareTo(b.Length); + } + static (string[] Core, string? Label) Split(string version) { var dash = version.IndexOf('-', StringComparison.Ordinal); @@ -119,15 +156,73 @@ public static int CompareVersions(string left, string right) [GeneratedRegex(@"^(?\d+)\.(?\d+)\.(?\d)xx$")] private static partial Regex Band(); + + [GeneratedRegex(@"^\d+(\.\d+)*(-[0-9A-Za-z.-]+)?$")] + private static partial Regex VersionText(); +} + +/// +/// The facts due for a re-read before a release (CONTRIBUTING's "Releasing"). An external pin names what this +/// checkout never installs, a competitor's release among them, so check never compares it and the fact is as +/// fresh as its date. A fact with one is due when it was verified more than a number of days ago and, when a feed is +/// given, when NuGet has a later release of a package it pins: a later stable release for a stable pin, a later one +/// of any kind for a pre-release. Run by hand, never in CI: a build must not fail because time passed or someone +/// else released. +/// +internal static class FactReview +{ + /// Six months: a fact read longer ago than this is due, whatever its pins. + public const int DefaultDays = 183; + + internal sealed record Due(string At, string Kind, string Id, string Target, string Detail); + + /// The versions NuGet lists for a package id, or null where it has no package of that id. + internal delegate IReadOnlyList? Feed(string packageId); + + public static IReadOnlyList List(DocIndex index, DateOnly today, int days, Feed? feed, out int facts, out int compared) + { + var due = new List(); + var order = Comparer.Create(Pins.CompareVersions); + (facts, compared) = (0, 0); + + foreach (var doc in index.Docs.Where(doc => doc.Kind == Kind.Fact)) + { + var pins = doc.Meta.Where(field => field.Key == "pins" && field.Value.StartsWith("external ", StringComparison.Ordinal)).ToList(); + if (pins.Count == 0) continue; + facts++; + + string At(int line) => $"{doc.Path}:{line.ToString(CultureInfo.InvariantCulture)}"; + + if (doc.Meta.FirstOrDefault(field => field.Key == "verified") is { } verified + && DateOnly.TryParseExact(verified.Value, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) + && today.DayNumber - date.DayNumber is var age && age > days) + due.Add(new Due(At(verified.Line), "fact-old", doc.Id!, verified.Value, $"verified {age.ToString(CultureInfo.InvariantCulture)} days ago, more than {days.ToString(CultureInfo.InvariantCulture)}: re-read it at its latest release")); + + if (feed is null) continue; + + foreach (var pin in pins) + { + if (Pins.External(pin.Value) is not { } external || feed(external.Name) is not { Count: > 0 } versions) continue; + var (name, version) = external; + compared++; + + var track = Pins.IsPreRelease(version) ? versions : [.. versions.Where(listed => !Pins.IsPreRelease(listed))]; + var latest = track.Count == 0 ? null : track.Max(order); + + if (latest is not null && Pins.CompareVersions(version, latest) < 0) + due.Add(new Due(At(pin.Line), "fact-released", doc.Id!, pin.Value, $"NuGet's latest{(Pins.IsPreRelease(version) ? "" : " stable")} {name} is {latest}: re-read it there")); + } + } + + return due; + } } /// /// The rules a verified fact is held to (design/adr/0093): its frontmatter is complete and plain, each pin /// matches what is installed, and its test: names the characterization test that observes it, or says /// none and why. A test of the facts project that no fact names is a problem too. A fixture:, the -/// measured answer of the fact's test as data, sits beside that test and exists (design/adr/0107). A fact the probe -/// corpus cites to prune or excuse a row () is executable: its test exists, and where the -/// harness reads it as a baseline, its fixture too. +/// measured answer of the fact's test as data, sits beside that test and exists (design/adr/0107). /// internal static partial class FactRules { @@ -139,9 +234,7 @@ internal sealed record Problem(string At, string Kind, string Target, string Det public const string FixtureSuffix = ".fixture.json"; - public static IReadOnlyList Check(DocIndex index, string root, out int facts) => Check(index, root, out facts, out _); - - public static IReadOnlyList Check(DocIndex index, string root, out int facts, out FactCitations.Scan cited) + public static IReadOnlyList Check(DocIndex index, string root, out int facts) { var problems = new List(); var installed = Installed.Read(root); @@ -149,7 +242,6 @@ public static IReadOnlyList Check(DocIndex index, string root, out int var fixtures = new HashSet(StringComparer.Ordinal); // Why each fact cannot settle a pruning or an excuse, or null where its test exists; and its fixture, where one exists. var notExecutable = new Dictionary(StringComparer.Ordinal); - var fixtureOf = new Dictionary(StringComparer.Ordinal); facts = 0; for (var d = 0; d < index.Docs.Count; d++) @@ -221,7 +313,6 @@ public static IReadOnlyList Check(DocIndex index, string root, out int } } - fixtureOf[doc.Id!] = null; if (doc.Meta.FirstOrDefault(field => field.Key == "fixture") is { } fixture) { var expected = $"{FactsProject}/{className}{FixtureSuffix}"; @@ -235,23 +326,9 @@ public static IReadOnlyList Check(DocIndex index, string root, out int problems.Add(new Problem(At(fixture.Line), "fact-fixture", fixture.Value, $"write it beside the test, as {expected}")); else if (!File.Exists(Path.Combine(root, fixture.Value))) problems.Add(new Problem(At(fixture.Line), "fact-fixture-missing", fixture.Value, "the file does not exist")); - else - fixtureOf[doc.Id!] = fixture.Value; } } - cited = FactCitations.Find(root); - foreach (var citation in cited.Citations) - { - var why = !notExecutable.TryGetValue(citation.Fact, out var reason) ? "there is no such fact under design/knowledge/" - : reason is not null ? reason - : citation.Baseline && fixtureOf[citation.Fact] is null ? "the harness reads its fixture as a baseline, and it names no fixture: that exists" - : null; - if (why is not null) - problems.Add(new Problem(citation.At, "fact-not-executable-cited", citation.Fact, - $"{citation.Place} cites {citation.Fact}, and {why}: a fact without its test explains only, never prunes or excuses a row; write its test in {FactsProject}/, or cite a docs sentence")); - } - var project = Path.Combine(root, FactsProject); if (Directory.Exists(project)) { diff --git a/tools/DocsQuery/NuGetFeed.cs b/tools/DocsQuery/NuGetFeed.cs new file mode 100644 index 00000000..ebc72510 --- /dev/null +++ b/tools/DocsQuery/NuGetFeed.cs @@ -0,0 +1,26 @@ +using System.Net; +using System.Text.Json; + +namespace DocsQuery; + +/// +/// The versions nuget.org lists for a package, from its flat container (v3-flatcontainer/{id}/index.json), for +/// due --nuget. The one command that reaches the network, and only when asked; nothing in CI asks. +/// +internal static class NuGetFeed +{ + private static readonly HttpClient Http = new() { Timeout = TimeSpan.FromSeconds(30) }; + + /// Every version of , or null where nuget.org has no package of that id. + public static IReadOnlyList? Versions(string packageId) + { + var url = $"https://api.nuget.org/v3-flatcontainer/{Uri.EscapeDataString(packageId.ToLowerInvariant())}/index.json"; + + using var response = Http.GetAsync(url).GetAwaiter().GetResult(); + if (response.StatusCode == HttpStatusCode.NotFound) return null; + response.EnsureSuccessStatusCode(); + + using var document = JsonDocument.Parse(response.Content.ReadAsStream()); + return [.. document.RootElement.GetProperty("versions").EnumerateArray().Select(version => version.GetString() ?? "")]; + } +} diff --git a/tools/DocsQuery/Program.cs b/tools/DocsQuery/Program.cs index 71652172..47881519 100644 --- a/tools/DocsQuery/Program.cs +++ b/tools/DocsQuery/Program.cs @@ -18,6 +18,9 @@ internal static class Program find ranked (BM25); --top N, --kind K. A miss proves nothing claims [page] every C# block, stated output and shown block of the README and docs pages; --kind csharp|stated|block + due [days] the facts to re-read before a release: an external pin verified more than + days ago (default 183); --nuget also lists a pin NuGet has a later release + of, read from api.nuget.org. By hand, never in CI check broken links, missing anchors, ids defined nowhere, and the verified facts' rules: a pin behind, ahead or naming nothing installed, a fact without its test, a test without its fact @@ -36,13 +39,14 @@ private static int Main(string[] args) var positional = new List(); string? root = null, kind = null; int top = 10, depth = 1; - var json = false; + bool json = false, nuget = false; for (var i = 0; i < args.Length; i++) { switch (args[i]) { case "--json": json = true; break; + case "--nuget": nuget = true; break; case "--root" when i + 1 < args.Length: root = args[++i]; break; case "--kind" when i + 1 < args.Length: kind = args[++i]; break; case "--top" when i + 1 < args.Length && int.TryParse(args[i + 1], CultureInfo.InvariantCulture, out var n) && n > 0: top = n; i++; break; @@ -68,7 +72,25 @@ private static int Main(string[] args) var queries = new Queries(index, root); var query = string.Join(' ', positional.Skip(1)); - Answer? answer = positional[0] switch + Answer? answer; + try + { + answer = Run(queries, positional, query, top, kind, depth, nuget); + } + catch (Exception exception) when (exception is HttpRequestException or TaskCanceledException) + { + Console.Error.WriteLine($"docs-query: nuget.org could not be read ({exception.Message})."); + return 1; + } + + if (answer is null) return Fail($"'{string.Join(' ', positional)}' is no command; run `help`."); + + Console.Write(json ? answer.Json() : answer.Text()); + return answer.ExitCode; + } + + private static Answer? Run(Queries queries, List positional, string query, int top, string? kind, int depth, bool nuget) => + positional[0] switch { "entry" when query.Length > 0 => queries.Entry(query), "outline" when query.Length > 0 => queries.Outline(query), @@ -77,14 +99,17 @@ private static int Main(string[] args) "list" => queries.List([.. positional.Skip(1)]), "find" when query.Length > 0 => queries.Find(query, top, kind), "claims" => queries.Claims(query.Length > 0 ? query : null, kind), + "due" => Due(queries, positional, nuget), "check" => queries.Check(), _ => null, }; - if (answer is null) return Fail($"'{string.Join(' ', positional)}' is no command; run `help`."); + private static Answer? Due(Queries queries, List positional, bool nuget) + { + var days = FactReview.DefaultDays; + if (positional.Count > 2 || positional.Count == 2 && !(int.TryParse(positional[1], NumberStyles.None, CultureInfo.InvariantCulture, out days) && days >= 0)) return null; - Console.Write(json ? answer.Json() : answer.Text()); - return answer.ExitCode; + return queries.Due(days, DateOnly.FromDateTime(DateTime.UtcNow), nuget ? NuGetFeed.Versions : null); } private static int Fail(string message) diff --git a/tools/DocsQuery/Queries.cs b/tools/DocsQuery/Queries.cs index ec174ae0..7d22f716 100644 --- a/tools/DocsQuery/Queries.cs +++ b/tools/DocsQuery/Queries.cs @@ -131,6 +131,22 @@ public Answer List(IReadOnlyList filters) }; } + /// + /// The facts due for a re-read (): an external fact verified more than + /// days before , and, given a feed, one pinning a package NuGet has + /// a later release of. Exit code 0 when one is due, 1 when none is. + /// + public Answer Due(int days, DateOnly today, FactReview.Feed? feed) + { + var due = FactReview.List(index, today, days, feed, out var facts, out var compared); + var rows = due.Select(item => Row.Of(("at", item.At), ("kind", item.Kind), ("id", item.Id), ("target", item.Target), ("detail", item.Detail))).ToList(); + + var summary = $"{Count(due.Select(item => item.Id).Distinct().Count(), "fact")} due of {Count(facts, "fact")} pinned external, by a verified: date more than {days.ToString(CultureInfo.InvariantCulture)} days before {today.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}" + + (feed is null ? "; --nuget also compares each pin with NuGet's latest release" : $" or a later release on NuGet, against which {Count(compared, "pin")} compared"); + + return new Answer(Answer.Exact, "due", feed is null ? days.ToString(CultureInfo.InvariantCulture) : $"{days.ToString(CultureInfo.InvariantCulture)} --nuget", summary, rows, rows.Count == 0 ? 1 : 0); + } + /// /// BM25 over the sections of the ranked documents, the heading weighted above the headings over /// it, the prose above the code. A section that defines an id the query names (a record's title, a @@ -206,8 +222,7 @@ public Answer Claims(string? page, string? kind) /// /// Links whose file or anchor does not exist, ids that name no record, diagnostic or fact, and the facts' /// rules (): a pin behind or ahead of what is installed, or naming nothing installed, - /// a fact without its test, a test without its fact, a fixture missing, and a fact the probe corpus cites to prune - /// or excuse a row that has no test. + /// a fact without its test, a test without its fact, and a fixture missing. /// public Answer Check() { @@ -239,12 +254,9 @@ public Answer Check() } } - rows.AddRange(FactRules.Check(index, root, out var facts, out var cited).Select(problem => Row.Of(("at", problem.At), ("kind", problem.Kind), ("target", problem.Target), ("detail", problem.Detail)))); - - // Each kind of citation counted, so a scan that found none of a kind it should have is seen in the summary. - var citations = string.Join(", ", FactCitations.Kinds.Select(kind => $"{cited.Of(kind).ToString(CultureInfo.InvariantCulture)} {kind}")); + rows.AddRange(FactRules.Check(index, root, out var facts).Select(problem => Row.Of(("at", problem.At), ("kind", problem.Kind), ("target", problem.Target), ("detail", problem.Detail)))); - return new Answer(Answer.Exact, "check", "", $"{Count(rows.Count, "problem")} in {links} links, {ids} ids, {Count(cited.Citations.Count, "fact citation")} ({citations}) and {Count(facts, "fact")}", rows, rows.Count == 0 ? 0 : 1); + return new Answer(Answer.Exact, "check", "", $"{Count(rows.Count, "problem")} in {links} links, {ids} ids and {Count(facts, "fact")}", rows, rows.Count == 0 ? 0 : 1); Row Problem(Edge edge, string kind, string detail) => Row.Of(("at", At(edge.Doc, edge.Line)), ("kind", kind), ("target", edge.Written), ("detail", detail)); diff --git a/tools/DocsQuery/README.md b/tools/DocsQuery/README.md index 6e8e666e..09f4ec8a 100644 --- a/tools/DocsQuery/README.md +++ b/tools/DocsQuery/README.md @@ -16,6 +16,7 @@ dotnet tools/DocsQuery/bin/Release/net10.0/docs-query.dll entry CMTK0010 | `list [filter...]` | Documents: `kind=adr status=open`, `date>=2026-10-01`, `title~json` | exact | | `find ` | BM25 over the sections of the ranked documents; `--top N`, `--kind K`. A section that defines an id the query names comes first. A miss proves nothing | ranked | | `claims [page]` | Every C# block, every line whose comment states an output, and every other fenced block of the README and the docs pages, with the sample region that compiles it; `--kind csharp\|stated\|block`, `--json` | exact | +| `due [days]` | The facts to re-read before a release: each fact with an `external` pin verified more than `days` ago (default 183, six months). `--nuget` also lists each pin of a package nuget.org has a later release of. Run by hand, never in CI | exact | | `check` | Broken links, missing anchors, ids defined nowhere, and the verified facts' rules (below) | exact | `--json` prints one object per line, the summary first; a pointer is `path` and `line`. Exit codes: 0 @@ -54,11 +55,21 @@ rules: | `fact-orphan-test` | A `KBNNN.cs` in the facts project that no fact names | | `fact-fixture-missing`, `fact-fixture` | A `fixture:` whose file does not exist; one that is not `KBNNN.fixture.json` beside the test, or belongs to a fact without a test | | `fact-orphan-fixture` | A `KBNNN.fixture.json` in the facts project that no fact names | -| `fact-not-executable-cited` | The probe corpus cites a fact that has no test: a `refused` entry of `.claude/probe-axes.md` whose reason names `KB-NNN`, a `not-promised` entry of `tools/probe/corpus/claims.json` whose `source` is the fact's file or which says `fact:KB-NNN`, an obligation's `inapplicable` declaration citing `fact:KB-NNN`, or a `Cite.Fact("KB-NNN")` or `Baseline.Fact("KB-NNN")` literal in the harness and probe sources (comments blanked out). No test means `none, …`, no file, a file that does not exist or declares no class of the id; a `Baseline.Fact` takes a fixture as well. Reported at the citing place | -The pins and the corpus's citations are read from the checkout at each `check`, never from the index. The summary -counts the citations of each kind (`4 fact citations (0 refused, 2 not-promised, 0 obligation, 2 harness)`), and -`RepositoryTests` holds floors on them, so a scan that finds nothing cannot pass for a clean one. +The pins are read from the checkout at each `check`, never from the index. + +An `external {name} {version}` pin is never compared by `check`: it names what the checkout never installs, a +competitor's release among them. `due` lists what to re-read (CONTRIBUTING, "Releasing"): + +| Problem | When | +|---|---| +| `fact-old` | The fact has an `external` pin and its `verified:` date is more than `days` before today | +| `fact-released` | With `--nuget`: nuget.org lists a later release of a pinned package, a stable one for a stable pin and any for a pre-release (SemVer order, `beta.10` after `beta.2`). Read from `v3-flatcontainer/{id}/index.json`, which lists unlisted versions too | + +A pin whose version is no version (`external NuGet registration data, 2026-10-29`) is never looked up, and one whose +name nuget.org has no package of (`external PostgreSQL 17.11`) finds nothing to compare: both are due by their date alone. +`--nuget` is the only command that reaches the network. Exit codes: 0 when a fact is due, 1 when none is or nuget.org +could not be read. ## Freshness diff --git a/tools/Oracles/Oracles.csproj b/tools/Oracles/Oracles.csproj new file mode 100644 index 00000000..3ee2c980 --- /dev/null +++ b/tools/Oracles/Oracles.csproj @@ -0,0 +1,22 @@ + + + + + Exe + oracles + Oracles + **/*.cs;../../test/oracles/SeedVerdict.cs + + + + + + + diff --git a/tools/Oracles/Program.cs b/tools/Oracles/Program.cs new file mode 100644 index 00000000..95ceccf1 --- /dev/null +++ b/tools/Oracles/Program.cs @@ -0,0 +1,245 @@ +// The oracle kit's local tool (design/adr/0112, test/oracles/README.md). Never part of CI. +// +// dotnet tools/Oracles/bin/Release/net10.0/oracles.dll seeds [name filter] show that every seed is caught +// dotnet tools/Oracles/bin/Release/net10.0/oracles.dll status the catalogue's worklist +// +// `seeds`: first a baseline, a copy of the working tree with no seed (bin/, obj/, .git/ and the agents' worktrees left +// out, so uncommitted work is included) under the temp directory, each oracle the selected seeds name built in Release +// and run alone; then, for each test/oracles/seeds/.patch whose file name holds the filter, another copy, the +// patch applied, each oracle project its `Oracles:` line names built once and each named class run alone, with the +// same filter. An oracle catches a seed when it passes without it and fails with it in a test of its own: a test +// that fails with a row that is not a known candidate, never one xunit could not set up, a test host that crashed or +// a run that wrote no results, which are ERROR (SeedVerdict, design/adr/0117). One line per baseline and one per +// (seed, oracle), CAUGHT, MISSED or ERROR. +// +// Exit 0 is a pass, 1 a seed not caught (or a status that could not read the catalogue), 2 a usage error, 3 a tool +// built from other sources than the checkout holds, which names the command that rebuilds it, 4 an oracle that could +// not be judged: its baseline does not pass, or a run with the seed did not reach the tests' own code. +using System.Diagnostics; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Xml.Linq; +using CodoMetis.TypeKit.Oracles; + +Console.Out.NewLine = "\n"; + +var root = SourceFingerprint.FindRoot(); +var stale = SourceFingerprint.Difference(root); +if (stale is not null) +{ + Console.WriteLine($"TOOL STALE: {SourceFingerprint.Project} was built from other sources than the checkout holds ({stale})"); + Console.WriteLine($" rebuild it: {SourceFingerprint.RebuildCommand}"); + return SourceFingerprint.StaleExitCode; +} + +var oraclesDirectory = Path.Combine(root!, "test", "oracles"); +var json = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true }; + +return args switch +{ + ["seeds"] => Seeds(""), + ["seeds", var filter] => Seeds(filter), + ["status"] or [] => Status(), + _ => Usage(), +}; + +int Usage() +{ + Console.Error.WriteLine("usage: dotnet tools/Oracles/bin/Release/net10.0/oracles.dll seeds [filter] | status"); + return 2; +} + +int Seeds(string filter) +{ + var oracles = Read>("oracles.json").ToDictionary(oracle => oracle.Class, StringComparer.Ordinal); + var seeds = Directory.GetFiles(Path.Combine(oraclesDirectory, "seeds"), "*.patch") + .Where(seed => Path.GetFileName(seed).Contains(filter, StringComparison.OrdinalIgnoreCase)) + .Order(StringComparer.Ordinal) + .Select(seed => (Path: seed, Name: Path.GetFileName(seed), Named: Header(File.ReadAllText(seed), "Oracles").Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))) + .ToList(); + if (seeds.Count == 0) + { + Console.WriteLine($"no seed matches '{filter}' in test/oracles/seeds"); + return 1; + } + + // The baseline: every registered oracle a seed names, run once on a copy with no seed, built and filtered as the + // seeded runs are. A seed is judged against it (SeedVerdict.Judge), so an oracle that cannot pass here, a dead + // fixture or a failing test, makes its pairs ERROR instead of crediting every seed with its failures. + var started = Stopwatch.StartNew(); + var baselines = RunOracles(seed: null, [.. seeds.SelectMany(seed => seed.Named).Distinct(StringComparer.Ordinal).Where(oracles.ContainsKey).Select(oracle => oracles[oracle])]); + foreach (var (oracle, run) in baselines.OrderBy(pair => pair.Key, StringComparer.Ordinal)) + { + Console.WriteLine(run.Error is null && run.Caught == 0 + ? $"BASELINE {oracle}: passes without a seed, {run.Executed} tests ({started.Elapsed.TotalSeconds:F0} s)" + : $"BASELINE {oracle}: DOES NOT PASS without a seed, {run.Error ?? $"{run.Caught} of {run.Executed} tests fail, e.g. {run.Example}"}"); + } + + var notRun = new SeedVerdict.Run(0, 0, 0, "", "not run, since it does not pass without the seed"); + var missed = false; + var errors = false; + foreach (var (seed, name, named) in seeds) + { + started.Restart(); + try + { + var registered = named.Select(oracle => oracles.TryGetValue(oracle, out var found) ? found : throw new SeedError($"'{oracle}' is not in oracles.json")).ToList(); + var seeded = RunOracles(seed, [.. registered.Where(oracle => baselines[oracle.Class] is { Error: null, Caught: 0 })]); + foreach (var oracle in registered) + { + var verdict = SeedVerdict.Judge(baselines[oracle.Class], seeded.GetValueOrDefault(oracle.Class) ?? notRun); + missed |= verdict.StartsWith("MISSED", StringComparison.Ordinal); + errors |= verdict.StartsWith("ERROR", StringComparison.Ordinal); + Console.WriteLine($"{verdict[..8]}{name} -> {oracle.Class}: {verdict[8..]} ({started.Elapsed.TotalSeconds:F0} s)"); + } + } + catch (SeedError error) + { + errors = true; + Console.WriteLine($"ERROR {name}: {error.Message}"); + } + } + + if (missed) Console.WriteLine("A SEED IS NOT CAUGHT: the oracle it names cannot fail for that defect"); + if (errors) Console.WriteLine("AN ORACLE COULD NOT BE JUDGED: its baseline or its run did not reach the tests' own code; fix that and run again"); + if (!missed && !errors) Console.WriteLine("every seed is caught"); + return errors ? 4 : missed ? 1 : 0; +} + +// A copy of the working tree with the seed applied, or none, each oracle's project built once in Release and each +// class run alone. A project that does not build is an error run for each of its oracles. +Dictionary RunOracles(string? seed, IReadOnlyList selected) +{ + var runs = new Dictionary(StringComparer.Ordinal); + if (selected.Count == 0) return runs; + + var scratch = Path.Combine(Path.GetTempPath(), $"typekit-seed-{Guid.NewGuid():N}"); + try + { + CopyTree(root!, scratch); + if (seed is not null) Require(RunProcess(scratch, "git", null, ["apply", "--whitespace=nowarn", seed]), "apply the patch"); + + foreach (var group in selected.GroupBy(oracle => oracle.Project)) + { + var project = Path.Combine("test", group.Key); + if (Failure(RunDotnet(scratch, "build", project, "-c", "Release", "-nologo", "-v", "q"), $"build {group.Key}") is { } failure) + { + foreach (var oracle in group) runs[oracle.Class] = new SeedVerdict.Run(0, 0, 0, "", failure); + continue; + } + + foreach (var oracle in group) + { + var results = Path.Combine(scratch, "SeedResults", oracle.Class); + var (exit, _) = RunDotnet(scratch, "test", "--project", project, "-c", "Release", "--no-build", + "--filter-class", $"*.{oracle.Class}", "--report-xunit-trx", "--report-xunit-trx-filename", $"{oracle.Class}.trx", + "--results-directory", results); + var trx = Path.Combine(results, $"{oracle.Class}.trx"); + runs[oracle.Class] = SeedVerdict.Read(exit, File.Exists(trx) ? XDocument.Load(trx) : null, oracle.Class); + } + } + + return runs; + } + finally + { + try { Directory.Delete(scratch, recursive: true); } catch (IOException) { } catch (UnauthorizedAccessException) { } + } +} + +int Status() +{ + var claims = Read>("claims.json"); + var oracles = Read>("oracles.json"); + var waivers = Read>("surface-waivers.json"); + var candidates = Read>("candidates.json"); + var seeds = Directory.GetFiles(Path.Combine(oraclesDirectory, "seeds"), "*.patch") + .Select(seed => Header(File.ReadAllText(seed), "Oracles").Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)) + .ToList(); + + var unheld = claims.Where(claim => claim.HeldBy is not { Count: > 0 }).ToList(); + Console.WriteLine($"claims: {claims.Count}, held by a test: {claims.Count - unheld.Count}, unheld (the worklist): {unheld.Count}"); + foreach (var claim in unheld) Console.WriteLine($" UNHELD {claim.Id,-40} {claim.Source}"); + + var unread = waivers.Where(waiver => waiver.Value.StartsWith("unread:", StringComparison.Ordinal)).ToList(); + Console.WriteLine($"surface waivers: {waivers.Count}, unread (members whose document has no claims yet): {unread.Count}"); + foreach (var waiver in unread) Console.WriteLine($" UNREAD {waiver.Key}"); + + var seeded = seeds.SelectMany(names => names).ToHashSet(StringComparer.Ordinal); + Console.WriteLine($"oracles: {oracles.Count}, seeds: {seeds.Count}, oracles without a seed: {oracles.Count(oracle => !seeded.Contains(oracle.Class))}"); + foreach (var oracle in oracles.Where(oracle => !seeded.Contains(oracle.Class))) Console.WriteLine($" UNSEEDED {oracle.Class}"); + + Console.WriteLine($"candidates: {candidates.Count} ({candidates.Count(candidate => candidate.Issue.Length == 0)} without an issue)"); + foreach (var candidate in candidates) Console.WriteLine($" {candidate.Oracle}: '{candidate.Row}' [{candidate.Family}] {(candidate.Issue.Length == 0 ? "NOT FILED" : candidate.Issue)}"); + + return 0; +} + +T Read(string file) +{ + using var stream = File.OpenRead(Path.Combine(oraclesDirectory, file)); + return JsonSerializer.Deserialize(stream, json) ?? throw new InvalidOperationException($"test/oracles/{file} is empty."); +} + +static string Header(string text, string key) => + Regex.Match(text, $@"^{key}:\s*(?.+?)\s*$", RegexOptions.Multiline) is { Success: true } match ? match.Groups["value"].Value : ""; + +static void CopyTree(string from, string to) +{ + string[] skipped = ["bin", "obj", ".git", "SeedResults", "TestResults", "dist", ".codebase-memory"]; + foreach (var directory in Directory.EnumerateDirectories(from, "*", SearchOption.TopDirectoryOnly)) + { + var name = Path.GetFileName(directory); + if (skipped.Contains(name, StringComparer.Ordinal)) continue; + if (name == ".claude") { CopyClaude(directory, Path.Combine(to, name)); continue; } + CopyTree(directory, Path.Combine(to, name)); + } + + Directory.CreateDirectory(to); + foreach (var file in Directory.EnumerateFiles(from, "*", SearchOption.TopDirectoryOnly)) + File.Copy(file, Path.Combine(to, Path.GetFileName(file))); +} + +// The agents' worktrees under .claude/worktrees are whole checkouts; nothing a seed builds reads them. +static void CopyClaude(string from, string to) +{ + Directory.CreateDirectory(to); + foreach (var file in Directory.EnumerateFiles(from)) File.Copy(file, Path.Combine(to, Path.GetFileName(file))); + foreach (var directory in Directory.EnumerateDirectories(from).Where(directory => Path.GetFileName(directory) != "worktrees")) + CopyTree(directory, Path.Combine(to, Path.GetFileName(directory))); +} + +static (int Exit, string Output) RunDotnet(string directory, params string[] arguments) +{ + var (dotnet, home) = Dotnet.For(directory); + return RunProcess(directory, dotnet, home, arguments); +} + +static (int Exit, string Output) RunProcess(string directory, string file, string? dotnetHome, string[] arguments) +{ + var start = new ProcessStartInfo(file) { WorkingDirectory = directory, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false }; + foreach (var argument in arguments) start.ArgumentList.Add(argument); + Dotnet.Apply(dotnetHome, start.Environment); + + using var process = Process.Start(start) ?? throw new SeedError($"{file} could not be started"); + var error = process.StandardError.ReadToEndAsync(); + var output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + return (process.ExitCode, output + error.Result); +} + +static void Require((int Exit, string Output) result, string step) +{ + if (Failure(result, step) is { } failure) throw new SeedError(failure); +} + +static string? Failure((int Exit, string Output) result, string step) => + result.Exit == 0 ? null : $"{step} failed (exit {result.Exit}): {string.Join(" | ", result.Output.Split('\n').Where(line => line.Contains("error", StringComparison.OrdinalIgnoreCase)).Take(3).Select(line => line.Trim()))}"; + +internal sealed record Claim(string Id, string Source, IReadOnlyList? HeldBy); + +internal sealed record Oracle(string Class, string Project, string Kind, string Against); + +internal sealed record Candidate(string Oracle, string Row, string Family, string Issue); + +internal sealed class SeedError(string message) : Exception(message); diff --git a/tools/PrHygiene/PrHygiene.csproj b/tools/PrHygiene/PrHygiene.csproj new file mode 100644 index 00000000..bf50b846 --- /dev/null +++ b/tools/PrHygiene/PrHygiene.csproj @@ -0,0 +1,15 @@ + + + + + Exe + pr-hygiene + PrHygiene + + + diff --git a/tools/PrHygiene/Program.cs b/tools/PrHygiene/Program.cs new file mode 100644 index 00000000..fa0f74a3 --- /dev/null +++ b/tools/PrHygiene/Program.cs @@ -0,0 +1,157 @@ +// Checks a pull_request event payload against CONTRIBUTING's "Issues and labels" +// (design/adr/0109-work-is-tracked-as-labelled-issues-and-ci-checks-each-pull-request.md): +// - exactly one type label: bug, enhancement, documentation, tests or chore; +// - a bug carries exactly one severity/* label, and `silent` is only ever a bug; +// - the description closes its issue with one of GitHub's closing keywords ("Closes #12"); +// - the head branch is named / (decision 94), and its type agrees with the type label; +// - a pull request into release/, or of release/ into main, has the milestone ; +// - a `breaking` pull request goes into a major (x.0.0): ApiCompat holds every other version to the +// surface of the one before. +// It reads the payload alone: no token, no network, nothing of the pull request's code run. Dependabot's +// pull requests carry their own labels and close no issue, so they are not checked. +// +// Usage: +// dotnet tools/PrHygiene/bin/Release/net10.0/pr-hygiene.dll +// +// Each problem is printed as a GitHub annotation (::error::), so the run's page lists them. Exit 0 is a +// pass, 1 a pull request with a problem, 2 a usage error or a payload without a pull request, 3 a tool built +// from other sources than the checkout holds, which names the command that rebuilds it. +using System.Text.Json; +using System.Text.RegularExpressions; + +Console.Out.NewLine = "\n"; +Console.Error.NewLine = "\n"; + +var stale = SourceFingerprint.Difference(SourceFingerprint.FindRoot()); +if (stale is not null) +{ + Console.WriteLine($"TOOL STALE: {SourceFingerprint.Project} was built from other sources than the checkout holds ({stale})"); + Console.WriteLine($" rebuild it: {SourceFingerprint.RebuildCommand}"); + return SourceFingerprint.StaleExitCode; +} + +if (args.Length != 1) +{ + Console.Error.WriteLine("Usage: dotnet tools/PrHygiene/bin/Release/net10.0/pr-hygiene.dll "); + return 2; +} + +PullRequest pr; +try +{ + using var document = JsonDocument.Parse(File.ReadAllText(args[0])); + if (!document.RootElement.TryGetProperty("pull_request", out var element) || element.ValueKind != JsonValueKind.Object) + { + Console.Error.WriteLine($"{args[0]} is no pull_request event: it has no pull_request object."); + return 2; + } + pr = PullRequest.Read(element); +} +catch (Exception e) when (e is IOException or UnauthorizedAccessException or JsonException) +{ + Console.Error.WriteLine($"{args[0]} cannot be read as an event payload: {e.Message}"); + return 2; +} + +if (pr.Author == "dependabot[bot]") +{ + Console.WriteLine("Dependabot pull request, not checked."); + return 0; +} + +var problems = Hygiene.Problems(pr); +if (problems.Count == 0) +{ + Console.WriteLine("Labels, milestone, branch and issue link are in order."); + return 0; +} +foreach (var problem in problems) Console.WriteLine($"::error::{problem}"); +Console.WriteLine("See CONTRIBUTING.md, \"Issues and labels\"."); +return 1; + +sealed record PullRequest(string Author, IReadOnlyList Labels, string Body, string Head, string Base, string? Milestone) +{ + public static PullRequest Read(JsonElement pr) => new( + Author: Text(pr, "user", "login") ?? "", + Labels: pr.TryGetProperty("labels", out var labels) && labels.ValueKind == JsonValueKind.Array + ? [.. labels.EnumerateArray().Select(label => Text(label, "name")).OfType()] + : [], + Body: Text(pr, "body") ?? "", + Head: Text(pr, "head", "ref") ?? "", + Base: Text(pr, "base", "ref") ?? "", + Milestone: Text(pr, "milestone", "title")); + + // The string at the path, or null where a step is missing or null (GitHub writes "body": null for an empty description). + static string? Text(JsonElement element, params string[] path) + { + foreach (var name in path) + { + if (element.ValueKind != JsonValueKind.Object || !element.TryGetProperty(name, out element)) return null; + } + return element.ValueKind == JsonValueKind.String ? element.GetString() : null; + } +} + +static partial class Hygiene +{ + static readonly string[] Types = ["bug", "enhancement", "documentation", "tests", "chore"]; + + // A branch's , as decision 94 names it after the main commit's conventional type, and the type label that goes with it. + static readonly Dictionary TypeOfBranch = new(StringComparer.Ordinal) + { + ["feat"] = "enhancement", + ["fix"] = "bug", + ["docs"] = "documentation", + ["test"] = "tests", + ["build"] = "chore", + ["refactor"] = "chore", + ["spike"] = "chore", + ["release"] = "chore", + }; + + // GitHub's closing keywords, before #n or owner/repo#n. Only these link a pull request to the issue it closes. + [GeneratedRegex(@"\b(close[sd]?|fix(e[sd])?|resolve[sd]?):? +([A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+)?#[0-9]+\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + private static partial Regex Closing(); + + [GeneratedRegex(@"^release/(?(?[0-9]+)\.(?[0-9]+)\.(?[0-9]+))$", RegexOptions.CultureInvariant)] + private static partial Regex ReleaseBranch(); + + public static List Problems(PullRequest pr) + { + var problems = new List(); + var labels = pr.Labels.ToHashSet(StringComparer.Ordinal); + + var types = Types.Where(labels.Contains).ToList(); + if (types.Count != 1) + problems.Add($"Needs exactly one type label ({string.Join(", ", Types)}); it has {types.Count}{(types.Count > 1 ? $": {string.Join(", ", types)}" : "")}."); + + var severities = pr.Labels.Count(label => label.StartsWith("severity/", StringComparison.Ordinal)); + if (labels.Contains("bug") && severities != 1) + problems.Add($"A bug needs exactly one severity/* label (critical, high, medium, low); it has {severities}."); + if (labels.Contains("silent") && !labels.Contains("bug")) + problems.Add("Only a bug is silent: label it bug, or take silent off."); + + if (!Closing().IsMatch(pr.Body)) + problems.Add("The description needs a closing reference to its issue, such as 'Closes #12'."); + + var slash = pr.Head.IndexOf('/', StringComparison.Ordinal); + var branchType = slash > 0 ? pr.Head[..slash] : null; + if (branchType is null || slash == pr.Head.Length - 1 || !TypeOfBranch.TryGetValue(branchType, out var expected)) + problems.Add($"The branch '{pr.Head}' is not named /, where the type is one of {string.Join(", ", TypeOfBranch.Keys)} (decision 94)."); + else if (types.Count == 1 && types[0] != expected) + problems.Add($"The branch '{pr.Head}' is a {branchType} branch, which is labelled {expected}, not {types[0]}."); + + var release = ReleaseBranch().Match(pr.Base); + if (!release.Success && pr.Base == "main") release = ReleaseBranch().Match(pr.Head); + if (release.Success) + { + var version = release.Groups["version"].Value; + if (pr.Milestone != version) + problems.Add($"A pull request into {version} has the milestone {version}; it has {(pr.Milestone is null ? "none" : $"'{pr.Milestone}'")}."); + if (labels.Contains("breaking") && (release.Groups["minor"].Value != "0" || release.Groups["patch"].Value != "0")) + problems.Add($"A breaking change needs a major, and {version} is not one: it waits for the next x.0.0."); + } + + return problems; + } +} diff --git a/tools/Prober/Axes.cs b/tools/Prober/Axes.cs deleted file mode 100644 index e37f6a46..00000000 --- a/tools/Prober/Axes.cs +++ /dev/null @@ -1,386 +0,0 @@ -using System.Text.Json.Nodes; -using System.Text.RegularExpressions; - -namespace Prober; - -/// Two settings of one row, "axis=value" each, held in ordinal order so a pair has one form. -internal readonly record struct Pair -{ - public string A { get; } - public string B { get; } - - public Pair(string x, string y) - { - if (string.CompareOrdinal(x, y) <= 0) { A = x; B = y; } else { A = y; B = x; } - } - - public override string ToString() => $"{A} {B}"; -} - -/// Pairs a claim excludes, and the one sentence that says why no row can hold them or the documents do not promise them. -internal sealed record Exclusion(string Reason, IReadOnlyList Pairs); - -/// One thing a claim promises, checkable on its own: a sentence quoted from the claim's source, under an id a seed names (decision 104). -internal sealed record Obligation(string Id, string Sentence, IReadOnlyDictionary> Axes) -{ - /// Where the obligation does not apply, each declaration with its citation; the harness's Row.Check reads the same ones. - public IReadOnlyList Inapplicable { get; init; } = []; -} - -/// -/// Where an obligation does not apply: every axis of the declaration at one of the values it lists, and the citation -/// that says why (not-promised:id, docs:file#anchor, fact:KB-NNN, record:NNNN). A row -/// matches when it holds each axis at a listed value; a pair matches when every row holding it does, so when each -/// axis of the declaration is one of the pair's two at a listed value. -/// -internal sealed record Inapplicability(IReadOnlyDictionary> Axes, string Cite) -{ - public bool Matches(Pair pair) => Axes.All(axis => Holds(pair.A, axis) || Holds(pair.B, axis)); - - private static bool Holds(string setting, KeyValuePair> axis) - { - var at = setting.IndexOf('=', StringComparison.Ordinal); - return at > 0 && string.Equals(setting[..at], axis.Key, StringComparison.Ordinal) && axis.Value.Contains(setting[(at + 1)..]); - } - - public JsonObject ToJson() => new() - { - ["axes"] = new JsonObject(Axes.Select(kv => KeyValuePair.Create(kv.Key, (JsonNode?)new JsonArray([.. kv.Value.Select(v => (JsonNode)v)])))), - ["cite"] = Cite, - }; -} - -/// One thing a claim does not promise: the sentence of the document or record that makes the difference intended, and what it concerns. -internal sealed record NotPromised(string Id, string Source, string Sentence, IReadOnlyDictionary> Axes); - -/// One claim of the universe: its axes and values, its watched paths, and the pairs it excludes. -internal sealed class Claim -{ - public required string Id { get; init; } - public required string Source { get; init; } - public required IReadOnlyList Surfaces { get; init; } - public required IReadOnlyList Watched { get; init; } - public required SortedDictionary> Axes { get; init; } - public required HashSet Excluded { get; init; } - - /// The claim's exclusions with their reasons, as claims.json holds them; universe.json keeps only . - public IReadOnlyList Exclusions { get; init; } = []; - - /// Each axis the claim drops, with the document sentence that limits it; universe.json does not keep these. - public IReadOnlyDictionary Narrowed { get; init; } = new SortedDictionary(StringComparer.Ordinal); - - /// What the claim promises, each checkable on its own, as claims.json lists them (empty: one implicit obligation, the claim); universe.json does not keep these. - public IReadOnlyList Obligations { get; init; } = []; - - /// What the claim does not promise, each with the sentence that makes it intended; universe.json does not keep these. - public IReadOnlyList NotPromised { get; init; } = []; - - /// The id of a claim's one obligation where claims.json lists none: the claim itself. - public const string ImplicitObligation = "claim"; - - /// The obligation ids of a claim as claims.json holds it: the listed ones, or the implicit one. - public static List ObligationIds(JsonObject claim) - { - var ids = (claim["obligations"] as JsonArray ?? []).OfType().Select(o => Files.Str(o["id"])).Where(id => id.Length > 0).ToList(); - return ids.Count > 0 ? ids : [ImplicitObligation]; - } - - /// The ids of the claim's obligations as built: the listed ones, or the implicit one. - public IEnumerable ObligationIdsBuilt => Obligations.Count > 0 ? Obligations.Select(o => o.Id) : [ImplicitObligation]; - - /// - /// The pairs wants: the claim's pairs less those a declaration of that obligation - /// matches on either side. An obligation that declares nothing, the implicit one among them, wants them all. - /// - public HashSet PairsWantedBy(string obligation) - { - var declared = Obligations.FirstOrDefault(o => o.Id == obligation)?.Inapplicable ?? []; - var result = Pairs(); - if (declared.Count > 0) result.RemoveWhere(p => declared.Any(d => d.Matches(p))); - return result; - } - - /// The pairs at least one obligation wants: the union of , each pair once. - public HashSet PairsWanted() - { - if (!Obligations.Any(o => o.Inapplicable.Count > 0)) return Pairs(); - var result = new HashSet(); - foreach (var id in ObligationIdsBuilt) result.UnionWith(PairsWantedBy(id)); - return result; - } - - /// - /// The pairs no obligation wants, each with the citations of the declarations that match it: declared - /// inapplicable, so no row is generated for them and the gate leaves them out as it leaves out an exclusion. - /// - public Dictionary> DeclaredInapplicable() - { - var result = new Dictionary>(); - var wanted = PairsWanted(); - foreach (var pair in Pairs()) - if (!wanted.Contains(pair)) - result[pair] = new SortedSet(Obligations.SelectMany(o => o.Inapplicable).Where(d => d.Matches(pair)).Select(d => d.Cite), StringComparer.Ordinal); - return result; - } - - public HashSet Pairs() - { - var result = new HashSet(); - var names = Axes.Keys.ToList(); - for (var i = 0; i < names.Count; i++) - for (var j = i + 1; j < names.Count; j++) - foreach (var va in Axes[names[i]]) - foreach (var vb in Axes[names[j]]) - { - var pair = new Pair($"{names[i]}={va}", $"{names[j]}={vb}"); - if (!Excluded.Contains(pair)) result.Add(pair); - } - return result; - } - - /// The pairs one row sets, restricted to the claim's axes and values (an applied value the claim does not list earns nothing). - public HashSet PairsOfRow(IReadOnlyDictionary row) - { - var items = row.Where(kv => Axes.TryGetValue(kv.Key, out var values) && values.Contains(kv.Value)) - .OrderBy(kv => kv.Key, StringComparer.Ordinal).ToList(); - var result = new HashSet(); - for (var i = 0; i < items.Count; i++) - for (var j = i + 1; j < items.Count; j++) - result.Add(new Pair($"{items[i].Key}={items[i].Value}", $"{items[j].Key}={items[j].Value}")); - return result; - } - - public JsonObject ToJson() - { - var o = new JsonObject - { - ["id"] = Id, ["source"] = Source, ["surfaces"] = new JsonArray([.. Surfaces.Select(s => (JsonNode)s)]), - ["watched"] = new JsonArray([.. Watched.Select(s => (JsonNode)s)]), - ["axes"] = new JsonObject(Axes.Select(kv => KeyValuePair.Create(kv.Key, (JsonNode?)new JsonArray([.. kv.Value.Select(v => (JsonNode)v)])))), - }; - if (Excluded.Count > 0) o["exclude"] = new JsonArray([.. Excluded.Select(p => (JsonNode)new JsonArray(p.A, p.B))]); - // Every obligation's id with its declarations, written only where one declares something, so the covering - // generator and the gate, which read universe.json, know which pairs each wants. - if (Obligations.Any(ob => ob.Inapplicable.Count > 0)) - o["obligations"] = new JsonArray([.. Obligations.Select(ob => - { - var item = new JsonObject { ["id"] = ob.Id }; - if (ob.Inapplicable.Count > 0) item["inapplicable"] = new JsonArray([.. ob.Inapplicable.Select(d => (JsonNode)d.ToJson())]); - return (JsonNode)item; - })]); - return o; - } - - public static Claim FromJson(JsonObject o) => new() - { - Id = Files.Str(o["id"]), Source = Files.Str(o["source"]), Surfaces = [.. Files.Strings(o["surfaces"])], - Watched = [.. Files.Strings(o["watched"])], - Axes = new((o["axes"] as JsonObject ?? []).ToDictionary(kv => kv.Key, kv => Files.Strings(kv.Value).ToList(), StringComparer.Ordinal), StringComparer.Ordinal), - Excluded = [.. (o["exclude"] as JsonArray ?? []).OfType().Where(p => p.Count == 2).Select(p => new Pair(Files.Str(p[0]), Files.Str(p[1])))], - Obligations = [.. (o["obligations"] as JsonArray ?? []).OfType().Select(ob => new Obligation(Files.Str(ob["id"]), "", new SortedDictionary>(StringComparer.Ordinal)) - { - Inapplicable = [.. (ob["inapplicable"] as JsonArray ?? []).OfType().Select(Prober.Axes.Declaration)], - })], - }; -} - -internal sealed class Universe -{ - public required string Base { get; init; } - public required string Commit { get; init; } - public required JsonObject GeneratedVersions { get; init; } - public required List Claims { get; init; } - public Dictionary ById => field ??= Claims.ToDictionary(c => c.Id, StringComparer.Ordinal); - - public JsonObject ToJson() => new() - { - ["schemaVersion"] = Files.SchemaVersion, ["base"] = Base, ["commit"] = Commit, - ["generated_versions"] = GeneratedVersions.DeepClone(), ["claims"] = new JsonArray([.. Claims.Select(c => (JsonNode)c.ToJson())]), - }; - - public static Universe? Load(string path) => Files.ReadJson(path) is JsonObject o ? new Universe - { - Base = Files.Str(o["base"]), Commit = Files.Str(o["commit"]), GeneratedVersions = o["generated_versions"] as JsonObject is { } g ? (JsonObject)g.DeepClone() : [], - Claims = [.. (o["claims"] as JsonArray ?? []).OfType().Select(Claim.FromJson)], - } : null; -} - -internal static class Axes -{ - /// The fenced ```json probe-axes block of the axes file: hand, ignored, refused, axes, surfaces, replay. - public static JsonObject Block(string path) - { - if (!File.Exists(path)) throw new Refusal($"no axes file at {path}"); - var text = File.ReadAllText(path); - const string marker = "```json probe-axes"; - var at = text.IndexOf(marker, StringComparison.Ordinal); - if (at < 0) throw new Refusal($"{path} has no {marker} block"); - var body = text[(at + marker.Length)..]; - body = body[..body.IndexOf("```", StringComparison.Ordinal)]; - return JsonNode.Parse(body) as JsonObject ?? throw new Refusal($"the {marker} block of {path} is not a JSON object"); - } - - /// fnmatch's "*" (any run of characters), the only wildcard the axes file uses. - public static bool Matches(string pattern, string name) => - Regex.IsMatch(name, "^" + Regex.Escape(pattern).Replace("\\*", ".*", StringComparison.Ordinal) + "$", RegexOptions.CultureInvariant); - - private static string Collapse(string s) => Regex.Replace(s, @"\s+", " "); - - /// An axis-to-values object of claims.json (an obligation's or a not-promised entry's `axes`), or none. - private static SortedDictionary> AxesOf(JsonNode? node) - { - var result = new SortedDictionary>(StringComparer.Ordinal); - foreach (var (name, values) in node as JsonObject ?? []) result[name] = [.. Files.Strings(values)]; - return result; - } - - /// A declaration of where an obligation does not apply, as claims.json and universe.json hold it. - internal static Inapplicability Declaration(JsonObject d) => new(AxesOf(d["axes"]), Files.Str(d["cite"])); - - private static readonly Regex CiteId = new("^[a-z0-9][a-z0-9-]*$"); - private static readonly Regex CiteSource = new(@"^(?!/)(?!.*(^|/)\.\.?(/|#|$))[A-Za-z0-9_./-]+\.[A-Za-z0-9]+(#[A-Za-z0-9][A-Za-z0-9._-]*)?$"); - private static readonly Regex CiteFact = new(@"^KB-\d{3}$"); - private static readonly Regex CiteRecord = new(@"^\d{4}$"); - - /// - /// What is wrong with a citation's form, or null: the forms the harness's Cite (harness/Row.cs) accepts, so - /// every declaration the universe reads, the harness can cite (CiteFormTests holds the two alike). - /// - internal static string? CiteProblem(string cite) - { - var at = cite.IndexOf(':', StringComparison.Ordinal); - var (kind, value) = at < 0 ? ("", cite) : (cite[..at], cite[(at + 1)..]); - return kind switch - { - "not-promised" => CiteId.IsMatch(value) ? null : "is not a not-promised id (not-promised:closed-set-has-no-text)", - "docs" => CiteSource.IsMatch(value) ? null : "is not a docs source (docs:docs/json.md#decimals-in-one-spelling)", - "fact" => CiteFact.IsMatch(value) ? null : "is not a knowledge entry's id (fact:KB-031)", - "record" => CiteRecord.IsMatch(value) ? null : "is not a decision record's number (record:0094)", - _ => "is not a citation (not-promised:id, docs:file#anchor, fact:KB-NNN or record:NNNN)", - }; - } - - /// Builds the universe: each claim takes every axis of its surfaces, minus quoted narrowings, plus its own axes. - public static (Universe Universe, List Problems) Build(Ledger ledger, string baseRef, List? claimsOverride = null, JsonObject? generatedOverride = null) - { - var block = ledger.AxesBlock(); - var generated = generatedOverride ?? Files.ReadJson(Path.Combine(ledger.Memory, "axes-generated.json")) as JsonObject - ?? throw new Refusal("no axes-generated.json; run `prober replay --all`, which derives the axes"); - var claims = claimsOverride ?? ledger.Claims(); - if (claims.Count == 0) throw new Refusal("no claims (tools/probe/corpus/claims.json, or the branch's staging)"); - - var ignored = block["ignored"] as JsonObject ?? []; - var hand = block["hand"] as JsonObject ?? []; - var axes = new SortedDictionary>(StringComparer.Ordinal); - foreach (var (name, values) in generated["axes"] as JsonObject ?? []) - if (!ignored.ContainsKey(name)) axes[name] = [.. Files.Strings(values)]; - var problems = new List(); - foreach (var name in Files.Strings(generated["needsValues"])) - { - if (ignored.ContainsKey(name)) continue; - if (hand[name] is JsonArray h) axes[name] = ["default", .. Files.Strings(h)]; - else problems.Add($"needs values: {name} (no hand values and not ignored)"); - } - foreach (var (name, values) in hand) - if (!axes.ContainsKey(name) && !ignored.ContainsKey(name)) axes[name] = ["default", .. Files.Strings(values)]; - foreach (var (name, values) in block["axes"] as JsonObject ?? []) axes[name] = [.. Files.Strings(values)]; - foreach (var (name, refused) in block["refused"] as JsonObject ?? []) - if (axes.TryGetValue(name, out var list) && refused is JsonObject r) axes[name] = [.. list.Where(v => !r.ContainsKey(v))]; - - var surfaces = block["surfaces"] as JsonObject ?? throw new Refusal("the axes block has no surfaces"); - var reached = new HashSet(StringComparer.Ordinal); - foreach (var (_, surface) in surfaces) - foreach (var pattern in Files.Strings(surface?["axes"])) - reached.UnionWith(axes.Keys.Where(k => Matches(pattern, k))); - var allGenerated = (generated["axes"] as JsonObject ?? []).Select(kv => kv.Key).Concat(Files.Strings(generated["needsValues"])); - foreach (var name in allGenerated.Distinct(StringComparer.Ordinal).Order(StringComparer.Ordinal)) - if (!ignored.ContainsKey(name) && axes.ContainsKey(name) && !reached.Contains(name)) - problems.Add($"unassigned: {name} (no surface reaches it and it is not ignored)"); - - var built = new List(); - foreach (var c in claims) - { - var id = Files.Str(c["id"]); var source = Files.Str(c["source"]); var sourceFile = source.Split('#')[0]; - var mine = new SortedDictionary>(StringComparer.Ordinal); - var watched = new List(); - foreach (var surfaceName in Files.Strings(c["surfaces"])) - { - if (surfaces[surfaceName] is not JsonObject surface) throw new Refusal($"claim {id} names unknown surface {surfaceName}"); - foreach (var pattern in Files.Strings(surface["axes"])) - foreach (var name in axes.Keys.Where(k => Matches(pattern, k))) mine[name] = axes[name]; - watched.AddRange(Files.Strings(surface["watched"])); - } - var sourcePath = Path.Combine(ledger.Top, sourceFile); - var sourceText = File.Exists(sourcePath) ? Collapse(File.ReadAllText(sourcePath)) : ""; - var narrowed = new SortedDictionary(StringComparer.Ordinal); - foreach (var (name, quote) in c["narrow"] as JsonObject ?? []) - { - if (!sourceText.Contains(Collapse(Files.Str(quote)), StringComparison.Ordinal)) - throw new Refusal($"claim {id} narrows {name} by a quote not found in {sourceFile}"); - mine.Remove(name); - narrowed[name] = Files.Str(quote); - } - foreach (var (name, values) in c["axes"] as JsonObject ?? []) mine[name] = [.. Files.Strings(values)]; - // What the claim promises and what it does not, each a sentence quoted from its document, checked as a - // narrowing is: a sentence the document no longer holds is a refusal, never a stale promise (decision 104). - var obligations = new List(); - var notPromisedIds = (c["not-promised"] as JsonArray ?? []).OfType().Select(n => Files.Str(n["id"])).ToHashSet(StringComparer.Ordinal); - foreach (var o in (c["obligations"] as JsonArray ?? []).OfType()) - { - var oid = Files.Str(o["id"]); - if (obligations.Any(x => x.Id == oid)) throw new Refusal($"claim {id}: obligation {oid} is listed twice"); - if (!sourceText.Contains(Collapse(Files.Str(o["sentence"])), StringComparison.Ordinal)) - throw new Refusal($"claim {id}: obligation {oid} quotes a sentence not found in {sourceFile}"); - // Where the obligation does not apply, said by the claim's own axes and values and cited: the covering - // generator, the gate and the harness read the same declarations, so they never disagree. - var declared = new List(); - foreach (var (d, i) in (o["inapplicable"] as JsonArray ?? []).OfType().Select((d, i) => (d, i))) - { - var at = $"claim {id}: obligation {oid}: inapplicable[{i}]"; - var declaration = Declaration(d); - if (declaration.Axes.Count == 0) throw new Refusal($"{at} names no axis"); - foreach (var (name, values) in declaration.Axes) - { - if (!mine.TryGetValue(name, out var listed)) throw new Refusal($"{at} names axis {name}, which the claim does not have"); - if (values.Count == 0) throw new Refusal($"{at} names no value of {name}"); - foreach (var value in values.Where(v => !listed.Contains(v))) - throw new Refusal($"{at} names {name}={value}, a value the claim does not list for {name}"); - } - if (CiteProblem(declaration.Cite) is { } problem) throw new Refusal($"{at}: cite {declaration.Cite} {problem}"); - if (declaration.Cite.StartsWith("not-promised:", StringComparison.Ordinal) && !notPromisedIds.Contains(declaration.Cite["not-promised:".Length..])) - throw new Refusal($"{at} cites {declaration.Cite}, which the claim's not-promised list does not hold"); - declared.Add(declaration); - } - obligations.Add(new Obligation(oid, Files.Str(o["sentence"]), AxesOf(o["axes"])) { Inapplicable = declared }); - } - var notPromised = new List(); - foreach (var n in (c["not-promised"] as JsonArray ?? []).OfType()) - { - var nid = Files.Str(n["id"]); var nsource = Files.Str(n["source"]); var nfile = nsource.Split('#')[0]; - if (notPromised.Any(x => x.Id == nid)) throw new Refusal($"claim {id}: not-promised {nid} is listed twice"); - var npath = Path.Combine(ledger.Top, nfile); - var ntext = File.Exists(npath) ? Collapse(File.ReadAllText(npath)) : ""; - if (!ntext.Contains(Collapse(Files.Str(n["sentence"])), StringComparison.Ordinal)) - throw new Refusal($"claim {id}: not-promised {nid} quotes a sentence not found in {nfile}"); - notPromised.Add(new NotPromised(nid, nsource, Files.Str(n["sentence"]), AxesOf(n["axes"]))); - } - // The claims file's schema holds the form (Ledger.ReadClaims): each entry a reason and its pairs. - List exclusions = [.. (c["exclude"] as JsonArray ?? []).OfType().Select(e => new Exclusion(Files.Str(e["reason"]), - [.. (e["pairs"] as JsonArray ?? []).OfType().Select(p => new Pair(Files.Str(p[0]), Files.Str(p[1])))]))]; - built.Add(new Claim - { - Id = id, Source = source, Surfaces = [.. Files.Strings(c["surfaces"])], - Watched = [.. watched.Append(sourceFile).Distinct(StringComparer.Ordinal).Order(StringComparer.Ordinal)], Axes = mine, - Excluded = [.. exclusions.SelectMany(e => e.Pairs)], Exclusions = exclusions, Narrowed = narrowed, - Obligations = obligations, NotPromised = notPromised, - }); - } - var universe = new Universe - { - Base = Git.Out(ledger.Top, "merge-base", "HEAD", baseRef), Commit = ledger.Head, - GeneratedVersions = generated["versions"] as JsonObject is { } v ? (JsonObject)v.DeepClone() : [], Claims = built, - }; - Files.WriteJson(ledger.P("universe.json"), universe.ToJson()); - return (universe, problems); - } -} diff --git a/tools/Prober/Commands.cs b/tools/Prober/Commands.cs deleted file mode 100644 index 91cc7df3..00000000 --- a/tools/Prober/Commands.cs +++ /dev/null @@ -1,747 +0,0 @@ -using System.Globalization; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Xml.Linq; - -namespace Prober; - -/// The command line: a command, then --name value options and --flag switches. -internal sealed class Args -{ - private readonly Dictionary> options = new(StringComparer.Ordinal); - public List Positional { get; } = []; - - public Args(IEnumerable argv, IReadOnlySet flags) - { - var list = argv.ToList(); - for (var i = 0; i < list.Count; i++) - { - var a = list[i]; - if (!a.StartsWith("--", StringComparison.Ordinal)) { Positional.Add(a); continue; } - var name = a[2..]; - if (flags.Contains(name)) { Add(name, "true"); continue; } - // A multi-value option takes values until the next option. - var any = false; - while (i + 1 < list.Count && !list[i + 1].StartsWith("--", StringComparison.Ordinal)) { Add(name, list[++i]); any = true; if (!Multi.Contains(name)) break; } - if (!any) Add(name, ""); - } - } - - private static readonly HashSet Multi = new(StringComparer.Ordinal) { "cobertura", "include", "compile-time", "from", "seed-env" }; - private void Add(string name, string value) { if (!options.TryGetValue(name, out var l)) options[name] = l = []; l.Add(value); } - public string? Get(string name) => options.TryGetValue(name, out var l) ? l[^1] : null; - public string Need(string name) => Get(name) is { Length: > 0 } v ? v : throw new Refusal($"--{name} is required", 2); - public bool Flag(string name) => options.ContainsKey(name); - public List All(string name) => options.TryGetValue(name, out var l) ? l : []; -} - -internal static class Commands -{ - private static readonly JsonSerializerOptions Pretty = new() { WriteIndented = true }; - private static string Today => DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); - - public static int Rows(Ledger l, Args a) - { - if (a.Get("probe") is not { Length: > 0 } probe) - { - l.Ctx.Out.WriteLine(CoveringRows(l, a.Need("claim"), a.Flag("full"), a.Get("max"), a.Get("seed")).ToJsonString(Pretty)); - return 0; - } - if (!l.Registry().TryGetValue(probe, out var entry)) throw new Refusal($"no probe {probe} in the registry, tracked or staged; `prober new` stages one", 2); - var claimId = Files.Str(entry["claim"]); - if (a.Get("claim") is { Length: > 0 } named && named != claimId) throw new Refusal($"{probe} is a probe of {claimId}, not of {named}", 2); - var (rows, uncovered) = StageRows(l, probe, claimId, a.Flag("full"), a.Get("max"), a.Get("seed")); - l.Ctx.Out.WriteLine($"rows/{probe}.json: {rows} rows for claim {claimId} over its pairs{(a.Flag("full") ? "" : " its other probes have not covered")}" + - $"{(uncovered > 0 ? $", {uncovered} left uncovered" : "")}; staged in {l.StagingDir}, the file written whole. " + - "`prober publish` moves it into the corpus"); - return 0; - } - - /// - /// Writes a probe's rows file in the staging, whole, never appended: a covering array over its claim's pairs (all of - /// them with ) less those the claim's other probes have credited. The probe's own credit is - /// not subtracted, since it came from the rows this file replaces and ends with them. The stored rows are - /// definitions; the evidence is in the state. - /// - internal static (int Rows, int Uncovered) StageRows(Ledger l, string probe, string claimId, bool full, string? max = null, string? seed = null) - { - var covering = CoveringRows(l, claimId, full, max, seed, except: probe); - var rows = new JsonArray([.. (covering["rows"] as JsonArray ?? []).Select(r => (JsonNode)new JsonObject { ["row"] = r?["row"]?.DeepClone() })]); - Files.WriteJson(Path.Combine(l.StagingDir, "rows", probe + ".json"), new JsonObject { ["claim"] = claimId, ["rows"] = rows }); - return (rows.Count, (int)Files.Num(covering["uncovered_after"])); - } - - /// - /// What `rows` prints: a pairwise covering array over the claim's pairs not yet covered (all of them with - /// ), each row with the pairs it covers. Inside a capped round the claim counts toward - /// the round's cap, and past the deadline or the cap this refuses. - /// - public static JsonObject CoveringRows(Ledger l, string claimId, bool full, string? max = null, string? seed = null, string? except = null) - { - var universe = Universe.Load(l.P("universe.json")) ?? throw new Refusal("no universe.json for this branch yet; run `universe` first"); - if (!universe.ById.TryGetValue(claimId, out var claim)) throw new Refusal($"no claim {claimId} in the universe"); - if (!full && ActiveRound(l) is { } round) - { - if (RoundLock.Now() > Files.Num(round["deadline"])) - throw new Refusal("the round's time is spent; start no new work. Record what ran, hand the corpus off, and report."); - var started = round["claims"] as JsonArray ?? []; - if (!Files.Strings(started).Contains(claimId)) - { - if (round["max_claims"] is JsonValue m && m.TryGetValue(out var cap) && started.Count >= cap) - throw new Refusal($"the round's cap of {cap} claim(s) is reached ({string.Join(", ", Files.Strings(started))}); finish those and report."); - started.Add(claimId); - round["claims"] = started.DeepClone(); - Files.WriteJson(l.P("round.json"), round); - } - } - var covered = full ? [] : l.Covered(universe, claimId, except); - // The pairs some obligation wants: a pair declared inapplicable for every obligation is asked for by no row. - var todo = claim.PairsWanted().Where(p => !covered.Contains((claimId, p))).ToHashSet(); - var rows = Covering.Rows(claim, todo, int.Parse(max ?? "2000", CultureInfo.InvariantCulture), int.Parse(seed ?? "1", CultureInfo.InvariantCulture)); - return new JsonObject - { - ["claim"] = claimId, - ["rows"] = new JsonArray([.. rows.Select(r => (JsonNode)new JsonObject - { - ["row"] = new JsonObject(r.Row.Select(kv => KeyValuePair.Create(kv.Key, (JsonNode?)kv.Value))), - ["pairs"] = new JsonArray([.. r.Pairs.Select(p => (JsonNode)new JsonArray(p.A, p.B))]), - })]), - ["uncovered_after"] = todo.Count - rows.Sum(r => r.Pairs.Count), - }; - } - - public static int UniverseCommand(Ledger l, Args a) - { - var claims = a.Get("claims") is { } cp ? Ledger.ReadClaims(cp, cp) : null; - var generated = a.Get("generated") is { } gp ? Files.ReadJson(gp) as JsonObject : null; - var (universe, problems) = Axes.Build(l, a.Get("base") ?? "origin/main", claims, generated); - var total = 0; - foreach (var c in universe.Claims) - { - var n = c.Pairs().Count; total += n; - l.Ctx.Out.WriteLine($"{c.Id}: {c.Axes.Count} axes, {n} pairs"); - // What the claim leaves out and why: a narrowing's quote is checked against its document, an exclusion's reason by a reviewer only. - foreach (var (axis, quote) in c.Narrowed) l.Ctx.Out.WriteLine($" narrow {axis}: \"{quote}\""); - foreach (var e in c.Exclusions) - l.Ctx.Out.WriteLine($" exclude {e.Pairs.Count} pair(s), first {e.Pairs[0]}: {e.Reason}"); - if (Inapplicable(c) is { } line) l.Ctx.Out.WriteLine($" {line}"); - } - l.Ctx.Out.WriteLine($"universe: {universe.Claims.Count} claims, {total} pairs"); - foreach (var p in problems) l.Ctx.Out.WriteLine($" {p}"); - return 0; - } - - /// - /// The pairs of a claim no obligation wants, as one line: their count, the first, and the citations of the - /// declarations that leave them out; null where there are none. - /// - private static string? Inapplicable(Claim c) - { - var dead = c.DeclaredInapplicable(); - if (dead.Count == 0) return null; - var first = dead.Keys.OrderBy(p => p.A, StringComparer.Ordinal).ThenBy(p => p.B, StringComparer.Ordinal).First(); - var cites = new SortedSet(dead.Values.SelectMany(x => x), StringComparer.Ordinal); - return $"inapplicable {dead.Count} pair(s), wanted by no obligation, first {first}: {string.Join(", ", cites)}"; - } - - public static JsonObject ComputeStatus(Ledger l) - { - var universe = Universe.Load(l.P("universe.json")); - if (universe is null) return new JsonObject { ["branch"] = l.Branch, ["done"] = false, ["reason"] = "no universe.json: run `universe` (no round has run here)" }; - var credits = l.Credits(universe); - var done = credits.Select(x => (x.Claim, x.Pair)).ToHashSet(); - // How the rows and the checks of each claim's probes ended at their last replay: only a passing check earns - // pairs, so the rest is what the claim's coverage loses, and a skip for a finding is an open finding the corpus - // records. A row without checks counts as its one check. - var outcomes = new Dictionary>(StringComparer.Ordinal); - var checkOutcomes = new Dictionary>(StringComparer.Ordinal); - var passed = new Dictionary>(StringComparer.Ordinal); - var skips = new SortedDictionary(StringComparer.Ordinal); - foreach (var (probe, entry) in l.Registry()) - { - var evidence = Files.ReadJson(l.EvidencePath(probe)) as JsonObject; - var claim = Files.Str(entry["claim"]); - var sum = outcomes.TryGetValue(claim, out var o) ? o : outcomes[claim] = new(StringComparer.Ordinal); - foreach (var (category, n) in Ledger.Outcomes(evidence)) sum[category] = sum.GetValueOrDefault(category) + n; - var checkSum = checkOutcomes.TryGetValue(claim, out var co) ? co : checkOutcomes[claim] = new(StringComparer.Ordinal); - if (evidence is not null) foreach (var (category, n) in Ledger.CheckOutcomes(evidence)) checkSum[category] = checkSum.GetValueOrDefault(category) + n; - (passed.TryGetValue(claim, out var p) ? p : passed[claim] = new(StringComparer.Ordinal)).UnionWith(Ledger.PassedObligations(evidence)); - foreach (var (finding, n) in Ledger.FindingSkips(evidence)) skips[finding] = skips.GetValueOrDefault(finding) + n; - } - // Each claim's obligations (decision 104), the probes registered for the claim, and the probes whose seeds - // break each obligation: the claim's promise is checked only where a probe can be shown to fail on it. - var listed = l.Claims().ToDictionary(c => Files.Str(c["id"]), Claim.ObligationIds, StringComparer.Ordinal); - var registry = l.Registry(); - var claims = new JsonObject(); var total = 0; var current = 0; var inapplicableTotal = 0; var wantedTotal = 0; var wantedCurrent = 0; - foreach (var c in universe.Claims) - { - var pairs = c.Pairs(); var hit = pairs.Count(p => done.Contains((c.Id, p))); - // The pairs no obligation wants are declared inapplicable: left out of what is still to cover, as the gate leaves them out. - var wanted = c.PairsWanted(); var inapplicable = pairs.Count - wanted.Count; - inapplicableTotal += inapplicable; wantedTotal += wanted.Count; wantedCurrent += wanted.Count(p => done.Contains((c.Id, p))); - var probes = registry.Where(kv => Files.Str(kv.Value["claim"]) == c.Id).ToList(); - var ids = listed.GetValueOrDefault(c.Id) ?? [Claim.ImplicitObligation]; - var obligations = new JsonArray(); - foreach (var id in ids) - { - var seededBy = probes.Where(kv => Seeds.SeedsOf(kv.Value).Any(s => (s.Obligation ?? (ids is [Claim.ImplicitObligation] ? Claim.ImplicitObligation : null)) == id)) - .Select(kv => (JsonNode)kv.Key).ToList(); - // current: the claim's pairs credited by current checks naming this obligation; passed: whether any check - // named it and passed at the last replay, current or not (status's "K uncredited" counts those that did not). - obligations.Add(new JsonObject - { - ["id"] = id, ["seeded_by"] = new JsonArray([.. seededBy]), - ["current"] = pairs.Count(p => credits.Contains((c.Id, id, p))), - ["passed"] = passed.GetValueOrDefault(c.Id)?.Contains(id) ?? false, - }); - } - claims[c.Id] = new JsonObject - { - ["pairs"] = pairs.Count, ["current"] = hit, ["inapplicable_pairs"] = inapplicable, - ["outcomes"] = new JsonObject((outcomes.GetValueOrDefault(c.Id) ?? []).Select(kv => KeyValuePair.Create(kv.Key, (JsonNode?)kv.Value))), - ["check_outcomes"] = new JsonObject((checkOutcomes.GetValueOrDefault(c.Id) ?? []).Select(kv => KeyValuePair.Create(kv.Key, (JsonNode?)kv.Value))), - ["probes"] = new JsonArray([.. probes.Select(kv => (JsonNode)kv.Key)]), ["obligations"] = obligations, - }; - total += pairs.Count; current += hit; - } - var open = l.OpenFindings(); - return new JsonObject - { - ["branch"] = l.Branch, ["head"] = l.Head, ["universe_commit"] = universe.Commit, ["pairs"] = total, ["current"] = current, - ["inapplicable_pairs"] = inapplicableTotal, - ["share"] = total > 0 ? Math.Round((double)current / total, 4) : 1.0, - ["open_findings"] = new JsonArray([.. open.Select(f => (JsonNode)f)]), - ["skipped_for_findings"] = new JsonObject(skips.Select(kv => KeyValuePair.Create(kv.Key, (JsonNode?)kv.Value))), ["claims"] = claims, - ["done"] = total > 0 && wantedCurrent == wantedTotal && open.Count == 0, - }; - } - - public static int Status(Ledger l, Args a) - { - var s = ComputeStatus(l); - if (a.Flag("json")) { l.Ctx.Out.WriteLine(s.ToJsonString(Pretty)); return 0; } - if (s["reason"] is { } reason) { l.Ctx.Out.WriteLine($"{l.Branch}: not done ({Files.Str(reason)})"); return 0; } - var open = Files.Strings(s["open_findings"]).ToList(); - l.Ctx.Out.WriteLine($"{l.Branch} at {Files.Str(s["head"])[..7]}: {Files.Num(s["current"])} of {Files.Num(s["pairs"])} pairs current " + - $"({(Files.Num(s["share"]) * 100).ToString("F1", CultureInfo.InvariantCulture)}%), {open.Count} open finding(s)" + - (open.Count > 0 ? ": " + string.Join(", ", open) : "") + $" -> {(s["done"]!.GetValue() ? "DONE" : "not done")}"); - foreach (var (finding, rows) in (s["skipped_for_findings"] as JsonObject)!) - l.Ctx.Out.WriteLine($" {rows} row(s) skip for finding {finding}"); - foreach (var (id, c) in (s["claims"] as JsonObject)!.OrderBy(kv => kv.Key, StringComparer.Ordinal)) - { - var rows = (c!["outcomes"] as JsonObject)!.Select(kv => $"{kv.Key} {Files.Num(kv.Value)}").ToList(); - var checks = (c["check_outcomes"] as JsonObject ?? []).Select(kv => $"{kv.Key} {Files.Num(kv.Value)}").ToList(); - var obligations = (c["obligations"] as JsonArray ?? []).OfType().ToList(); - var unseeded = obligations.Where(o => (o["seeded_by"] as JsonArray ?? []).Count == 0).Select(o => Files.Str(o["id"])).ToList(); - // K uncredited: the obligations no passing check named at the last replay. - var uncredited = obligations.Count(o => o["passed"] is not JsonValue v || !v.GetValue()); - var promise = obligations.Count == 0 ? "" : $" obligations {obligations.Count}" + (unseeded.Count > 0 ? $" ({unseeded.Count} without a seed: {string.Join(", ", unseeded)})" : ", each seeded") - + $", {uncredited} uncredited"; - var inapplicable = Files.Num(c["inapplicable_pairs"]) is var n and > 0 ? $", {n} inapplicable" : ""; - l.Ctx.Out.WriteLine($" {id}: {Files.Num(c["current"])}/{Files.Num(c["pairs"])}{inapplicable}" + (rows.Count > 0 ? $" (rows: {string.Join(", ", rows)}; checks: {string.Join(", ", checks)})" : "") + promise); - } - return 0; - } - - /// - /// What the corpus leaves out, each with its recorded reason, from the tracked corpus and the generated axes - /// alone: the settings no surface reaches and nothing ignores, the settings without values, the declarations - /// the generators refuse, the values the frameworks refuse on their own, what the claims say they do not - /// promise (their not-promised entries and narrowings), and the surfaces the axes file declares out of reach. - /// A report for the release checklist and a feature-research round; it decides nothing (decision 104). - /// - public static int Gaps(Ledger l, Args a) - { - var (universe, problems) = Axes.Build(l, a.Get("base") ?? "origin/main"); - var block = l.AxesBlock(); - var generated = Files.ReadJson(Path.Combine(l.Memory, "axes-generated.json")) as JsonObject ?? []; - var versions = string.Join(", ", (generated["versions"] as JsonObject ?? []).Select(kv => $"{kv.Key} {Files.Str(kv.Value)}")); - l.Ctx.Out.WriteLine($"prober gaps: {l.Branch} at {l.Head[..7]}: what the corpus leaves out, each with its recorded reason; a report, no decision"); - Section("settings no surface reaches and nothing ignores" + (versions.Length > 0 ? $" (enumerated at {versions})" : ""), - problems.Where(p => p.StartsWith("unassigned: ", StringComparison.Ordinal)).Select(p => p["unassigned: ".Length..])); - Section("settings without values", problems.Where(p => p.StartsWith("needs values: ", StringComparison.Ordinal)).Select(p => p["needs values: ".Length..])); - Section("declarations the generators refuse (shape.refused-kind)", Files.Strings(block["axes"]?["shape.refused-kind"])); - Section("values the frameworks refuse on their own", - (block["refused"] as JsonObject ?? []).SelectMany(axis => (axis.Value as JsonObject ?? []).Select(v => $"{axis.Key}={v.Key}: {Files.Str(v.Value)}"))); - Section("not promised, by what it concerns", - universe.Claims.SelectMany(c => c.NotPromised.Select(n => (Key: Concerns(n.Axes), Line: $"{Concerns(n.Axes)}: {c.Id}/{n.Id} \"{n.Sentence}\" ({n.Source})")) - .Concat(c.Narrowed.Select(kv => (Key: kv.Key, Line: $"{kv.Key}: {c.Id} narrows it, \"{kv.Value}\" ({c.Source})")))) - .OrderBy(x => x.Key, StringComparer.Ordinal).ThenBy(x => x.Line, StringComparer.Ordinal).Select(x => x.Line)); - Section("pairs no obligation wants (declared inapplicable)", - universe.Claims.Select(c => Inapplicable(c) is { } line ? $"{c.Id}: {line}" : null).OfType()); - Section("pairs no passing check read both axes of (the last replay's evidence)", Unread(l, universe)); - Section("out of reach", (block["out-of-reach"] as JsonObject ?? []).Select(kv => $"{kv.Key}: {Files.Str(kv.Value)}")); - return 0; - - void Section(string title, IEnumerable items) - { - var list = items.ToList(); - l.Ctx.Out.WriteLine($"{title}: {(list.Count == 0 ? "none" : list.Count.ToString(CultureInfo.InvariantCulture))}"); - foreach (var item in list) l.Ctx.Out.WriteLine($" {item}"); - } - - static string Concerns(IReadOnlyDictionary> axes) => - axes.Count == 0 ? "the whole claim" : string.Join(" ", axes.Select(kv => $"{kv.Key}={string.Join("|", kv.Value)}")); - } - - /// - /// The wanted pairs of each claim with evidence that no passing check could earn: no obligation that wants the pair - /// has a passing check, in its probes' last replay, that read both of the pair's axes. More rows cannot cover such a - /// pair; a check that reads both axes, an exclusion or a declaration can (#54's four C11 pairs, wanted by obligations - /// whose checks never read shape.wrapped). A claim whose probes have no evidence is left out. - /// - private static IEnumerable Unread(Ledger l, Universe universe) - { - var registry = l.Registry(); - foreach (var claim in universe.Claims.OrderBy(c => c.Id, StringComparer.Ordinal)) - { - // The axes each obligation's passing checks read together, as "a\nb" with a before b. - var read = new Dictionary>(StringComparer.Ordinal); - var any = false; - foreach (var (probe, entry) in registry) - { - if (Files.Str(entry["claim"]) != claim.Id || Files.ReadJson(l.EvidencePath(probe)) is not JsonObject evidence) continue; - any = true; - foreach (var (_, row) in evidence["rows"] as JsonObject ?? []) - if (row is JsonObject r) - foreach (var check in Ledger.ChecksOf(r).Where(c => c.Category == "pass")) - { - if (!read.TryGetValue(check.Obligation, out var axes)) read[check.Obligation] = axes = new(StringComparer.Ordinal); - var names = check.Applied.Keys.Order(StringComparer.Ordinal).ToList(); - for (var i = 0; i < names.Count; i++) - for (var j = i + 1; j < names.Count; j++) axes.Add(names[i] + "\n" + names[j]); - } - } - if (!any) continue; - var wantedBy = claim.ObligationIdsBuilt.Concat(read.Keys).Distinct(StringComparer.Ordinal).ToDictionary(o => o, claim.PairsWantedBy, StringComparer.Ordinal); - foreach (var pair in claim.PairsWanted().OrderBy(p => p.A, StringComparer.Ordinal).ThenBy(p => p.B, StringComparer.Ordinal)) - { - var (a, b) = (Axis(pair.A), Axis(pair.B)); - var both = string.CompareOrdinal(a, b) <= 0 ? a + "\n" + b : b + "\n" + a; - if (!wantedBy.Any(o => o.Value.Contains(pair) && read.TryGetValue(o.Key, out var axes) && axes.Contains(both))) - yield return $"{claim.Id}: {pair.A} {pair.B}"; - } - } - - static string Axis(string setting) => setting[..setting.IndexOf('=', StringComparison.Ordinal)]; - } - - public static int Gate(Ledger l, Args a) => ReleaseGate.Run(l, a.Get("since"), a.Flag("json")); - - public static int Footprint(Ledger l, Args a) - { - var worktree = Path.GetFullPath(a.Need("root")); - var include = a.All("include") is { Count: > 0 } inc ? inc : ["src"]; - var hit = new SortedSet(StringComparer.Ordinal); - foreach (var report in a.All("cobertura")) - { - var doc = XDocument.Load(report); - var sources = doc.Descendants("source").Select(s => Path.GetFullPath(s.Value)).ToList(); - foreach (var cls in doc.Descendants("class")) - { - if (!cls.Descendants("line").Any(line => int.TryParse((string?)line.Attribute("hits"), out var h) && h > 0)) continue; - var name = (string?)cls.Attribute("filename") ?? ""; - var candidates = Path.IsPathRooted(name) ? [name] : sources.Count > 0 ? sources.Select(s => Path.Combine(s, name)).ToList() : [name]; - foreach (var candidate in candidates) - { - var full = Path.GetFullPath(candidate); - if (!full.StartsWith(worktree + Path.DirectorySeparatorChar, StringComparison.Ordinal)) continue; - var rel = Path.GetRelativePath(worktree, full).Replace('\\', '/'); - if (include.Any(p => rel == p || rel.StartsWith(p.TrimEnd('/') + "/", StringComparison.Ordinal))) hit.Add(rel); - break; - } - } - } - // A probe whose own process runs nothing under the include paths (one that builds a consumer in a child process, as C22 - // does) is recorded by what its registry entry watches; with nothing watched either, the footprint would never go stale. - var watchedPaths = l.Watched(a.Need("probe")); - if (hit.Count == 0 && watchedPaths.Count == 0) - throw new Refusal("the coverage report shows no executed file under " + string.Join(", ", include) + - " and the registry entry watches nothing; refusing to record an empty footprint (it would never go stale): name what the probe depends on in its entry's watched"); - var rev = Git.Out(worktree, "rev-parse", "HEAD"); - var footprint = new JsonObject(hit.Select(p => KeyValuePair.Create(p, (JsonNode?)Git.ObjectId(worktree, p)))); - var compileTime = new JsonObject(a.All("compile-time").Select(p => KeyValuePair.Create(p, (JsonNode?)Git.ObjectId(worktree, p)))); - // What the probe's registry entry watches beyond what ran: recorded with the ids it ran at, so a change stales it. - var watched = new JsonObject(watchedPaths.Select(p => KeyValuePair.Create(p, (JsonNode?)Git.ObjectId(worktree, p)))); - Files.AppendLine(l.P("footprints.jsonl"), new JsonObject - { - ["schemaVersion"] = Files.SchemaVersion, ["probe"] = a.Need("probe"), ["commit"] = rev, ["footprint"] = footprint, - ["compileTime"] = compileTime, ["watched"] = watched, ["toolchain"] = l.Toolchain().DeepClone(), ["date"] = a.Get("date") ?? Today, - }); - l.Ctx.Out.WriteLine($"{a.Need("probe")}: {footprint.Count} src files, {compileTime.Count} compile-time paths" + - $"{(watched.Count > 0 ? $", {watched.Count} watched" : "")} at {rev[..Math.Min(7, rev.Length)]}"); - return 0; - } - - public static int Stale(Ledger l, Args a) - { - var states = l.ProbeStates(a.Flag("include-retired")); - if (a.Flag("json")) - { - l.Ctx.Out.WriteLine(new JsonArray([.. states.Select(s => (JsonNode)new JsonObject - { - ["probe"] = s.Probe, ["claim"] = s.Claim, ["filter"] = s.Filter, ["variants"] = s.Variants.DeepClone(), ["stale"] = s.Stale, - ["retired"] = s.Retired, ["passes"] = s.Passes, ["replay"] = s.Replay, - })]).ToJsonString(Pretty)); - return 0; - } - var todo = states.Where(s => s.Replay).ToList(); - l.Ctx.Out.WriteLine($"{todo.Count} of {states.Count} probes to replay{(a.Flag("include-retired") ? " (retired included)" : "")}"); - foreach (var s in todo) l.Ctx.Out.WriteLine($" {s.Probe} {s.Filter}"); - return 0; - } - - public static int RecordRun(Ledger l, Args a) - { - var probe = a.Need("probe"); var result = a.Need("result"); - if (result is not ("pass" or "fail")) throw new Refusal("--result is pass or fail", 2); - var rev = l.Head; var identity = l.Identity(probe); - // A regression is a probe that passed with this very identity and fails now; after any change to its - // corpus, axes, environment or this tool, a failure is the probe's to triage, not the product's. - var passes = Files.ReadLines(l.P("runs.jsonl")).Where(x => Files.Str(x["probe"]) == probe && Files.Str(x["result"]) == "pass").ToList(); - var passedBefore = passes.Any(x => Files.Str(x["identity"]) == identity); - Files.AppendLine(l.P("runs.jsonl"), new JsonObject - { - ["schemaVersion"] = Files.SchemaVersion, ["probe"] = probe, ["result"] = result, ["commit"] = rev, ["date"] = a.Get("date") ?? Today, - ["seconds"] = a.Get("seconds") is { } s ? double.Parse(s, CultureInfo.InvariantCulture) : null, ["identity"] = identity, - }); - if (a.Get("evidence") is { } evidencePath) - { - var evidence = Files.ReadJson(evidencePath) as JsonObject ?? []; - evidence["schemaVersion"] = Files.EvidenceSchemaVersion; evidence["probe"] = probe; evidence["identity"] = identity; - evidence["commit"] = rev; evidence["result"] = result; evidence["date"] = a.Get("date") ?? Today; - Files.WriteJson(l.EvidencePath(probe), evidence); - } - if (result == "fail" && passes.Count > 0 && !passedBefore) - l.Ctx.Out.WriteLine($"{probe}: fails after a change to its corpus, axes, environment or this tool; triage the probe, not the product"); - if (result == "fail" && passedBefore) - Files.AppendLine(l.P("findings.jsonl"), new JsonObject - { - ["schemaVersion"] = Files.SchemaVersion, ["id"] = $"R-{probe}-{rev[..7]}", ["claim"] = Files.Str(l.Registry().GetValueOrDefault(probe)?["claim"]), - ["status"] = "open", ["commit"] = rev, ["probe"] = probe, ["reason"] = "replay: a probe that passed with this identity fails at this commit", - }); - return 0; - } - - public static int IdentityCommand(Ledger l, Args a) { l.Ctx.Out.WriteLine(l.Identity(a.Need("probe"))); return 0; } - - public static int Register(Ledger l, Args a) - { - // A note or a skip built from text fails at run time after a build and a replay; the staged sources are read first. - SourceLint.Refuse(l, "register"); - var path = Path.Combine(l.StagingDir, "registry.json"); - var staged = Files.ReadJson(path) as JsonObject ?? new JsonObject { ["probes"] = new JsonObject() }; - var probes = staged["probes"] as JsonObject ?? []; - var entry = l.Registry().GetValueOrDefault(a.Need("probe")) ?? []; - entry["claim"] = a.Need("claim"); entry["filter"] = a.Need("filter"); - if (a.Get("variants") is { Length: > 0 } v) entry["variants"] = JsonNode.Parse(v); - // The defect `seeds` applies to prove the probe can fail (decision 100): a patch under the corpus, an - // environment toggle, or both, and what it breaks. - var seed = new JsonObject(); - if (a.Get("seed") is { Length: > 0 } patch) seed["patch"] = patch; - if (a.All("seed-env") is { Count: > 0 } toggles) seed["env"] = SeedEnv(toggles); - if (a.Get("obligation") is { } obligation) - { - // A seed per obligation (decision 104): into `seeds`, in place of any seed that names the same obligation. - if (obligation.Length == 0) throw new Refusal("--obligation needs the id of one of the claim's obligations", 2); - if (seed.Count == 0) throw new Refusal("--obligation names the obligation of a seed: give the seed with --seed PATCH or --seed-env NAME=VALUE", 2); - var claim = l.Claims().FirstOrDefault(c => Files.Str(c["id"]) == a.Need("claim")) - ?? throw new Refusal($"no claim {a.Need("claim")} in claims.json (tracked or staged), so obligation {obligation} cannot be checked"); - var ids = Claim.ObligationIds(claim); - if (!ids.Contains(obligation)) - throw new Refusal($"claim {a.Need("claim")} lists no obligation {obligation} (it lists {string.Join(", ", ids)})", 2); - var named = new JsonObject { ["obligation"] = obligation }; - foreach (var (key, value) in seed) named[key] = value?.DeepClone(); - named["defect"] = a.Need("defect"); - var kept = (entry["seeds"] as JsonArray ?? []).Where(s => Files.Str(s?["obligation"]) != obligation).Select(s => s?.DeepClone()).ToList(); - var replaced = (entry["seeds"] as JsonArray ?? []).Count - kept.Count; - entry["seeds"] = new JsonArray([.. kept, named]); - if (entry["seed"] is JsonObject one && Files.Str(one["obligation"]) == obligation) { entry.Remove("seed"); replaced++; } - l.Ctx.Out.WriteLine($"seed for obligation {obligation} of {a.Need("probe")}" + (replaced > 0 ? $", in place of the {replaced} it had" : "")); - } - else if (seed.Count > 0) { seed["defect"] = a.Need("defect"); entry["seed"] = seed; } - probes[a.Need("probe")] = entry; - staged["probes"] = probes.DeepClone(); - Files.WriteJson(path, staged); - l.Ctx.Out.WriteLine($"registered {a.Need("probe")} (staged; `publish` moves it into the corpus)"); - return 0; - } - - /// A seed's environment toggle from --seed-env NAME=VALUE ...; a toggle without a name or an = is refused. - private static JsonObject SeedEnv(List toggles) - { - var env = new JsonObject(); - foreach (var toggle in toggles) - { - var eq = toggle.IndexOf('=', StringComparison.Ordinal); - if (eq <= 0) throw new Refusal($"--seed-env {toggle}: a toggle is NAME=VALUE", 2); - env[toggle[..eq]] = toggle[(eq + 1)..]; - } - return env; - } - - public static int Publish(Ledger l, Args a) - { - if (!Directory.Exists(l.StagingDir)) { l.Ctx.Out.WriteLine("nothing staged"); return 0; } - SourceLint.Refuse(l, "publish"); - var moved = new List(); - foreach (var src in Directory.EnumerateFiles(l.StagingDir, "*", SearchOption.AllDirectories).Order(StringComparer.Ordinal)) - { - var rel = Path.GetRelativePath(l.StagingDir, src).Replace('\\', '/'); - var dst = Path.Combine(l.CorpusDir, rel); - if (rel == "registry.json") - { - var tracked = Files.ReadJson(dst) as JsonObject ?? new JsonObject { ["probes"] = new JsonObject() }; - var probes = tracked["probes"] as JsonObject ?? []; - foreach (var (name, e) in Files.ReadJson(src)?["probes"] as JsonObject ?? []) probes[name] = e?.DeepClone(); - tracked["probes"] = new JsonObject(probes.OrderBy(kv => kv.Key, StringComparer.Ordinal).Select(kv => KeyValuePair.Create(kv.Key, kv.Value?.DeepClone()))); - Files.WriteJson(dst, tracked); - } - else if (rel == "claims.json") - { - var merged = new SortedDictionary(StringComparer.Ordinal); - foreach (var c in Ledger.ReadClaims(dst, "the corpus's claims.json").Concat(Ledger.ReadClaims(src, "the staged claims.json"))) - merged[Files.Str(c["id"])] = c.DeepClone(); - Files.WriteJson(dst, new JsonArray([.. merged.Values])); - } - else if (rel.StartsWith("rows/", StringComparison.Ordinal) && rel.EndsWith(".json", StringComparison.Ordinal)) - { - // A probe's rows, by probe: the file keeps the claim it names, else its probe's registry entry's. - var probe = Path.GetFileNameWithoutExtension(rel); - var claim = Files.Str(Files.ReadJson(src)?["claim"]) is { Length: > 0 } named ? named - : l.Registry().GetValueOrDefault(probe)?["claim"] is { } registered ? Files.Str(registered) - : throw new Refusal($"the staged {rel} names no claim and {probe} is no registered probe; a rows file is a probe's, its claim inside"); - Files.WriteRowsGzipped(dst + ".gz", claim, Files.ReadRows(src).OfType().Select(r => r["row"] as JsonObject ?? r)); - rel += ".gz"; - } - else if (rel.EndsWith(".jsonl", StringComparison.Ordinal)) - { - Directory.CreateDirectory(Path.GetDirectoryName(dst)!); - Files.WriteAtomically(dst, (File.Exists(dst) ? SharedFile.ReadAllText(dst) : "") + SharedFile.ReadAllText(src)); - } - else - { - Directory.CreateDirectory(Path.GetDirectoryName(dst)!); - File.Copy(src, dst, overwrite: true); - } - moved.Add(rel); - } - Directory.Delete(l.StagingDir, recursive: true); - l.Ctx.Out.WriteLine($"published {moved.Count} file(s) into {Path.GetRelativePath(l.Top, l.CorpusDir).Replace('\\', '/')}:"); - foreach (var rel in moved) l.Ctx.Out.WriteLine($" {rel}"); - return 0; - } - - public static int Mark(Ledger l, Args a) - { - if (a.Positional.FirstOrDefault() != "retired") throw new Refusal("mark takes `retired`", 2); - var probe = a.Need("probe"); - if (!l.Registry().TryGetValue(probe, out var entry)) throw new Refusal($"no probe {probe} in the registry"); - var path = Path.Combine(l.StagingDir, "registry.json"); - var staged = Files.ReadJson(path) as JsonObject ?? new JsonObject { ["probes"] = new JsonObject() }; - entry["retired"] = true; - (staged["probes"] as JsonObject)![probe] = entry; - Files.WriteJson(path, staged); - l.Ctx.Out.WriteLine($"{probe} retired (staged)"); - return 0; - } - - public static int Dismiss(Ledger l, Args a) - { - Files.AppendLine(Path.Combine(l.StagingDir, "dismissed.jsonl"), - new JsonObject { ["schemaVersion"] = Files.SchemaVersion, ["id"] = a.Need("id"), ["reason"] = a.Need("reason"), ["date"] = a.Get("date") ?? Today }); - l.Ctx.Out.WriteLine($"{a.Need("id")} dismissed (staged; `publish` puts it in the corpus)"); - return 0; - } - - /// - /// Records an open finding fixed at a commit: the fixed line of findings.jsonl (finding-line.schema.json), with the - /// open line's claim and probe and the commit's full id. It refuses an id this ledger does not hold open and a - /// commit the repository does not have; a commit outside HEAD's history is recorded and counts once it is in it. - /// - public static int Fix(Ledger l, Args a) - { - var id = a.Positional is [var only] ? only : throw new Refusal("fix takes one finding id: fix F --commit ", 2); - var given = a.Need("commit"); - var resolved = Git.Run(l.Top, "rev-parse", "-q", "--verify", given + "^{commit}"); - if (resolved.ExitCode != 0) throw new Refusal($"--commit {given} is no commit of this repository"); - var commit = resolved.Stdout.Trim(); - if (!l.Findings().TryGetValue(id, out var open)) throw new Refusal($"{id} is no finding of this ledger"); - if (Files.Str(open["status"]) is var status and not "open") throw new Refusal($"{id} is {status}, not open"); - var line = new JsonObject - { - ["schemaVersion"] = Files.SchemaVersion, ["id"] = id, ["claim"] = open["claim"]?.DeepClone(), ["status"] = "fixed", ["commit"] = commit, - }; - if (Files.Str(open["probe"]) is { Length: > 0 } probe) line["probe"] = probe; - if (a.Get("reason") is { Length: > 0 } reason) line["reason"] = reason; - Files.AppendLine(l.P("findings.jsonl"), line); - var inHistory = Git.Run(l.Top, "merge-base", "--is-ancestor", commit, "HEAD").ExitCode == 0; - l.Ctx.Out.WriteLine($"{id} fixed at {commit[..7]}" + (inHistory ? "" : "; it counts once that commit is in HEAD's history")); - return 0; - } - - public static int Lock(Ledger l, Args a) - { - var holder = a.Need("holder"); var mode = a.Need("mode"); - if (mode is not ("replay" or "explore" or "full")) throw new Refusal("--mode is replay, explore or full", 2); - var pid = a.Get("pid") is { Length: > 0 } p ? long.Parse(p, CultureInfo.InvariantCulture) : (long?)null; - var (taken, previous) = RoundLock.Take(l, holder, mode, pid); - if (!taken) - { - var since = DateTimeOffset.FromUnixTimeMilliseconds((long)(Files.Num(previous!["since"]) * 1000)).ToLocalTime().ToString("HH:mm", CultureInfo.InvariantCulture); - l.Ctx.Error.WriteLine($"prober: locked by {Files.Str(previous["holder"])} ({Files.Str(previous["mode"])}) since {since}"); - return 1; - } - var over = previous is not null && Files.Str(previous["holder"]) != holder ? $" (took over from {Files.Str(previous["holder"])}, whose heartbeat stopped)" : ""; - l.Ctx.Out.WriteLine($"locked for {mode} by {holder}{over}"); - return 0; - } - - public static int Unlock(Ledger l, Args a) - { - var owed = RoundLock.Release(l, a.Need("holder")); - l.Ctx.Out.WriteLine(owed ? "unlocked; a replay was skipped while locked: run `prober replay` now" : "unlocked"); - return 0; - } - - public static int HeartbeatCommand(Ledger l, Args a) { RoundLock.Beat(l, a.Need("holder")); return 0; } - - public static int OweReplay(Ledger l, Args a) { RoundLock.OweReplay(l); return 0; } - - public static JsonObject? ActiveRound(Ledger l) - { - var round = Files.ReadJson(l.P("round.json")) as JsonObject; - var held = RoundLock.Holder(l); - return round is not null && held is not null && Files.Str(held["holder"]) == Files.Str(round["holder"]) ? round : null; - } - - public static int RoundStart(Ledger l, Args a) - { - var holder = a.Need("holder"); - if (RoundLock.Holder(l) is not { } held || Files.Str(held["holder"]) != holder) - throw new Refusal("take the lock first; round-start records the caps of the lock's holder"); - var minutes = double.Parse(a.Need("minutes"), CultureInfo.InvariantCulture); - var claims = a.Get("claims") is { Length: > 0 } c ? int.Parse(c, CultureInfo.InvariantCulture) : (int?)null; - Files.WriteJson(l.P("round.json"), new JsonObject - { - ["schemaVersion"] = Files.SchemaVersion, ["holder"] = holder, ["started"] = RoundLock.Now(), - ["deadline"] = RoundLock.Now() + minutes * 60, ["max_claims"] = claims, ["claims"] = new JsonArray(), - }); - l.Ctx.Out.WriteLine($"round capped at {minutes.ToString(CultureInfo.InvariantCulture)} min" + (claims is { } n ? $" and {n} claim(s)" : "")); - return 0; - } - - public static int RoundEnd(Ledger l, Args a) - { - if (Files.ReadJson(l.P("round.json")) is JsonObject round && Files.Str(round["holder"]) == a.Need("holder")) - { - File.Delete(l.P("round.json")); - l.Ctx.Out.WriteLine($"round ended: claims {(Files.Strings(round["claims"]).Any() ? string.Join(", ", Files.Strings(round["claims"])) : "none")}"); - } - return 0; - } - - public static int Repair(Ledger l, Args a) - { - var fixedCount = 0; - if (Directory.Exists(l.Dir)) - foreach (var path in Directory.EnumerateFiles(l.Dir, "*.jsonl", SearchOption.AllDirectories)) - { - var data = File.ReadAllText(path); - if (data.Length > 0 && !data.EndsWith('\n')) - { - Files.WriteAtomically(path, data[..(data.LastIndexOf('\n') + 1)]); - fixedCount++; - l.Ctx.Out.WriteLine($" removed a partial last line from {Path.GetRelativePath(l.Dir, path)}"); - } - } - l.Ctx.Out.WriteLine($"repair: {fixedCount} file(s) fixed"); - return 0; - } - - public static int Abandon(Ledger l, Args a) - { - Repair(l, a); - if (Files.ReadJson(l.P("round.json")) is JsonObject round) - { - File.Delete(l.P("round.json")); - l.Ctx.Out.WriteLine($"round caps cleared ({Files.Str(round["holder"])})"); - } - var held = RoundLock.Abandon(l); - l.Ctx.Out.WriteLine(held is null ? "no lock was held" : $"lock of {Files.Str(held["holder"])} ({Files.Str(held["mode"])}) released"); - return 0; - } - - /// - /// Joins the states of a replay split by family (replay --family F, one state directory per leg) into - /// this branch's state: each leg's evidence files are copied in, its run, footprint, finding and ledger lines - /// are added where this state lacks them (a line a leg carried over from a restored state is not added - /// twice), and one replay-last.json sums the legs. It refuses while a probe the registry lists was neither - /// replayed nor skipped by any leg, which is how a matrix that leaves a family out fails instead of passing - /// on what it ran, and it fails (exit 1) when any leg failed, so the job that merges fails as the legs did. - /// - public static int Merge(Ledger l, Args a) - { - var sources = a.All("from").Where(x => x.Length > 0).ToList(); - if (sources.Count == 0) throw new Refusal("--from DIR... is required: the state directories of the legs to join into this one", 2); - var branchDir = Path.GetFileName(l.Dir); - Directory.CreateDirectory(Path.Combine(l.Dir, "evidence")); - var legs = new List(); - var seen = new SortedSet(StringComparer.Ordinal); - foreach (var from in sources) - { - var root = Path.GetFullPath(from); - var dir = Path.Combine(root, "branches", branchDir); - var last = Files.ReadJson(Path.Combine(dir, "replay-last.json")) as JsonObject - ?? throw new Refusal($"{from} holds no replay of {l.Branch}: no branches/{branchDir}/replay-last.json"); - if (legs.Count > 0 && Files.Str(last["commit"]) != Files.Str(legs[0]["commit"])) - throw new Refusal($"{from} replayed {Files.Str(last["commit"])[..7]}, another leg {Files.Str(legs[0]["commit"])[..7]}; the legs of one replay share a commit"); - legs.Add(last); - foreach (var name in new[] { "runs.jsonl", "footprints.jsonl", "findings.jsonl", "ledger.jsonl" }) UnionLines(Path.Combine(dir, name), l.P(name)); - foreach (var sub in new[] { "evidence", "logs" }) - { - var source = Path.Combine(dir, sub); - if (!Directory.Exists(source)) continue; - Directory.CreateDirectory(l.P(sub)); - foreach (var file in Directory.EnumerateFiles(source)) File.Copy(file, l.P(Path.Combine(sub, Path.GetFileName(file))), overwrite: true); - } - foreach (var name in new[] { "universe.json" }) - if (File.Exists(Path.Combine(dir, name)) && !File.Exists(l.P(name))) File.Copy(Path.Combine(dir, name), l.P(name)); - var axes = Path.Combine(root, "axes-generated.json"); - if (File.Exists(axes) && !File.Exists(Path.Combine(l.Memory, "axes-generated.json"))) File.Copy(axes, Path.Combine(l.Memory, "axes-generated.json")); - foreach (var probe in Files.Strings(last["selected"]).Concat(Files.Strings(last["skipped"]))) seen.Add(probe); - } - - var all = legs.Any(x => x["all"] is JsonValue v && v.TryGetValue(out var b) && b); - var selected = new SortedSet(legs.SelectMany(x => Files.Strings(x["selected"])), StringComparer.Ordinal); - var skipped = new SortedSet(legs.SelectMany(x => Files.Strings(x["skipped"])).Where(p => !selected.Contains(p)), StringComparer.Ordinal); - var failed = new SortedSet(legs.SelectMany(x => Files.Strings(x["failed"])), StringComparer.Ordinal); - static JsonArray Names(IEnumerable names) => new([.. names.Select(x => (JsonNode)x)]); - Files.WriteJson(l.P("replay-last.json"), new JsonObject - { - ["branch"] = l.Branch, ["commit"] = Files.Str(legs[0]["commit"]), ["replayed"] = selected.Count, ["failed"] = Names(failed), - ["seconds"] = Math.Round(legs.Max(x => Files.Num(x["seconds"])), 1), ["build_seconds"] = Math.Round(legs.Max(x => Files.Num(x["build_seconds"])), 1), - ["all"] = all, ["legs"] = Names(legs.Select(x => Files.Str(x["family"], "all"))), ["footprint"] = legs.All(x => x["footprint"] is not JsonValue f || !f.TryGetValue(out var ft) || ft), - ["selected"] = Names(selected), ["skipped"] = Names(skipped), - }); - l.Ctx.Out.WriteLine($"prober merge: {legs.Count} leg(s) into {l.Dir}: {selected.Count} replayed, {failed.Count} failed, {skipped.Count} current"); - - var missing = l.Registry().Where(kv => all || kv.Value["retired"] is not JsonValue r || !r.TryGetValue(out var retired) || !retired) - .Select(kv => kv.Key).Where(p => !seen.Contains(p)).ToList(); - if (missing.Count > 0) - throw new Refusal($"no leg replayed or skipped {string.Join(", ", missing)}: every probe's family is a leg of the matrix, or the merged state is short"); - if (failed.Count > 0) { l.Ctx.Out.WriteLine($" failed: {string.Join(", ", failed)}"); return 1; } - return 0; - } - - /// Adds to every line of it does not already hold, in order. - private static void UnionLines(string source, string target) - { - if (!File.Exists(source)) return; - var have = File.Exists(target) ? SharedFile.ReadAllLines(target).Where(x => x.Trim().Length > 0).ToList() : []; - var known = new HashSet(have, StringComparer.Ordinal); - var added = SharedFile.ReadAllLines(source).Where(x => x.Trim().Length > 0 && known.Add(x)).ToList(); - if (added.Count == 0 && File.Exists(target)) return; - Files.WriteAtomically(target, string.Join('\n', have.Concat(added)) + (have.Count + added.Count > 0 ? "\n" : "")); - } -} diff --git a/tools/Prober/Context.cs b/tools/Prober/Context.cs deleted file mode 100644 index 605654a5..00000000 --- a/tools/Prober/Context.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Diagnostics; -using System.Text; - -namespace Prober; - -/// -/// Everything a command reads from its surroundings: the directory it runs in, the environment and where -/// it writes. Commands never touch the process's own current directory or environment, so tests run -/// them in-process, side by side, each in a checkout of its own. -/// -internal sealed class Context(string workingDirectory, IReadOnlyDictionary environment, TextWriter output, TextWriter error) -{ - public string WorkingDirectory { get; } = workingDirectory; - public TextWriter Out { get; } = output; - public TextWriter Error { get; } = error; - - public string? Env(string name) => environment.TryGetValue(name, out var value) && !string.IsNullOrEmpty(value) ? value : null; - - public static Context FromProcess() => - new(Directory.GetCurrentDirectory(), - System.Environment.GetEnvironmentVariables().Cast() - .ToDictionary(e => (string)e.Key, e => (string?)e.Value, StringComparer.Ordinal), - Console.Out, Console.Error); - - public IReadOnlyDictionary Environment => environment; -} - -/// A child process's result. -internal sealed record Ran(int ExitCode, string Stdout, string Stderr); - -internal static class Proc -{ - /// - /// Runs a program to completion, capturing its output. Never through a shell. `dotnet` is the one that - /// satisfies the directory's global.json, the installer's when the one on PATH cannot (Dotnet). - /// - public static Ran Run(string fileName, IEnumerable arguments, string workingDirectory, - IReadOnlyDictionary? environment = null, TimeSpan? timeout = null) - { - string? dotnetDirectory = null; - if (fileName == "dotnet") (fileName, dotnetDirectory) = Dotnet.For(workingDirectory); - var start = new ProcessStartInfo(fileName) - { - WorkingDirectory = workingDirectory, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, - StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8, - }; - foreach (var argument in arguments) start.ArgumentList.Add(argument); - if (environment is not null) - foreach (var (name, value) in environment) - if (value is null) start.Environment.Remove(name); else start.Environment[name] = value; - Dotnet.Apply(dotnetDirectory, start.Environment); - - using var process = Process.Start(start) ?? throw new InvalidOperationException($"could not start {fileName}"); - var stdout = process.StandardOutput.ReadToEndAsync(); - var stderr = process.StandardError.ReadToEndAsync(); - if (timeout is { } limit && !process.WaitForExit(limit)) - { - // The whole tree: a test host or a headless agent starts children of its own. Portable, no signals. - try { process.Kill(entireProcessTree: true); } catch (InvalidOperationException) { } - process.WaitForExit(); - return new Ran(-1, stdout.Result, stderr.Result + "\n(stopped at the time limit)"); - } - process.WaitForExit(); - return new Ran(process.ExitCode, stdout.Result, stderr.Result); - } -} - -internal static class Git -{ - public static Ran Run(string workingDirectory, params string[] arguments) => Proc.Run("git", arguments, workingDirectory); - - public static string Out(string workingDirectory, params string[] arguments) => Run(workingDirectory, arguments).Stdout.Trim(); - - /// The object id of a file (blob) or a directory (tree) at a revision, or null when it does not exist there. - public static string? ObjectId(string workingDirectory, string path, string revision = "HEAD") - { - var ran = Run(workingDirectory, "rev-parse", "-q", "--verify", $"{revision}:{path}"); - return ran.ExitCode == 0 ? ran.Stdout.Trim() : null; - } -} diff --git a/tools/Prober/Covering.cs b/tools/Prober/Covering.cs deleted file mode 100644 index d4cfd13c..00000000 --- a/tools/Prober/Covering.cs +++ /dev/null @@ -1,93 +0,0 @@ -namespace Prober; - -/// -/// A greedy pairwise covering array: each row is the best of a few randomized greedy candidates, and the -/// rows together cover every pair asked for that at least one of the claim's obligations wants. A pair no -/// obligation wants (declared inapplicable) is never asked for, and neither it nor an excluded pair is ever put -/// in a row, not even to fill an axis: every check of such a row would be inapplicable, and an excluded pair is -/// one the claim says no row can run. An axis none of whose values can join a row without one is left out of -/// that row: shape.containing beside shape.position=root, and shape.refused-kind wherever the claim excludes each of -/// its pairs. Deterministic for a seed. -/// -internal static class Covering -{ - public static List<(SortedDictionary Row, List Pairs)> Rows(Claim claim, IReadOnlySet wanted, int max, int seed) - { - var dead = claim.DeclaredInapplicable(); - bool Blocked(Pair pair) => claim.Excluded.Contains(pair) || dead.ContainsKey(pair); - // The pairs asked for, each once however many obligations want it, so a row's gain counts it once. - var todo = new HashSet(wanted); - todo.RemoveWhere(Blocked); - var names = claim.Axes.Keys.ToList(); - var random = new Random(seed); - var rows = new List<(SortedDictionary, List)>(); - while (todo.Count > 0 && rows.Count < max) - { - SortedDictionary? best = null; var bestGain = -1; - for (var attempt = 0; attempt < 30; attempt++) - { - var order = names.OrderBy(_ => random.Next()).ToList(); - var row = new SortedDictionary(StringComparer.Ordinal); - foreach (var name in order) - { - var options = claim.Axes[name].OrderBy(_ => random.Next()).ToList(); - string? pick = null; var pickGain = -1; - foreach (var v in options) - { - var gain = 0; - foreach (var (n, w) in row) - { - var pair = new Pair($"{name}={v}", $"{n}={w}"); - if (Blocked(pair)) { gain = -1; break; } - if (todo.Contains(pair)) gain++; - } - if (gain > pickGain) { pick = v; pickGain = gain; } - } - // Every value of this axis puts a blocked pair in the row: the axis is left out of it. - if (pick is not null) row[name] = pick; - } - var g = Gained(row, todo).Count; - if (g > bestGain) { best = row; bestGain = g; } - } - if (bestGain <= 0) - { - // Seed a row from one uncovered pair so progress never stalls. - best = Seeded(claim, todo.OrderBy(p => p.A, StringComparer.Ordinal).ThenBy(p => p.B, StringComparer.Ordinal).First(), dead); - } - var newly = Gained(best!, todo); - todo.ExceptWith(newly); - rows.Add((best!, newly)); - } - return rows; - } - - static string Axis(string setting) => setting[..setting.IndexOf('=', StringComparison.Ordinal)]; - - /// - /// A row seeded from one pair: the pair's two settings, and each other axis at its first value that puts neither an excluded pair - /// nor a pair no obligation wants () in the row, or left out where none does. - /// - internal static SortedDictionary Seeded(Claim claim, Pair pair, IReadOnlyDictionary> dead) - { - bool Blocked(Pair p) => claim.Excluded.Contains(p) || dead.ContainsKey(p); - var row = new SortedDictionary(StringComparer.Ordinal); - foreach (var item in new[] { pair.A, pair.B }) row[Axis(item)] = item[(item.IndexOf('=', StringComparison.Ordinal) + 1)..]; - foreach (var name in claim.Axes.Keys.Where(n => !row.ContainsKey(n)).ToList()) - if (claim.Axes[name].FirstOrDefault(v => !row.Any(kv => Blocked(new Pair($"{name}={v}", $"{kv.Key}={kv.Value}")))) is { } value) - row[name] = value; - return row; - } - - private static List Gained(SortedDictionary row, HashSet todo) - { - var result = new List(); - var items = row.ToList(); - for (var i = 0; i < items.Count; i++) - for (var j = i + 1; j < items.Count; j++) - { - var pair = new Pair($"{items[i].Key}={items[i].Value}", $"{items[j].Key}={items[j].Value}"); - if (todo.Contains(pair)) result.Add(pair); - } - return result; - } -} diff --git a/tools/Prober/Files.cs b/tools/Prober/Files.cs deleted file mode 100644 index 51f83769..00000000 --- a/tools/Prober/Files.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System.IO.Compression; -using System.Text; -using System.Text.Encodings.Web; -using System.Text.Json; -using System.Text.Json.Nodes; - -namespace Prober; - -/// -/// Reading and writing the ledger's files. Every write goes to a temp file renamed over the target, so a -/// reader sees the file before or after a change, never half of it, on every OS; reads and the rename go -/// through SharedFile, which Windows needs while another process reads the target. A partial or unreadable -/// line in a JSON-lines file is skipped, never fatal (a writer killed mid-write in an older version). -/// -internal static class Files -{ - /// The schema version this tool writes. A file or line without one is version 1. - public const int SchemaVersion = 1; - - /// - /// The version of evidence/<probe>.json this tool writes: 2 records each row's checks, one entry per check - /// with its own obligation, baseline, outcome and settings. A version-1 file reads as rows without checks, each one - /// check of the obligation claim. - /// - public const int EvidenceSchemaVersion = 2; - - /// - /// Indented, and with only what JSON itself requires escaped: the tracked corpus is read in a diff, where - /// the default encoder's six-character escapes for +, a backtick and < made every publish a - /// rewrite (decision 102). No reader here or in the harness parses these files as - /// anything but JSON, and an identity hashes , not the file's text. - /// - private static readonly JsonSerializerOptions Indented = new() { WriteIndented = true, NewLine = "\n", Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; - - public static JsonNode? ReadJson(string path) - { - if (!File.Exists(path)) return null; - try { return JsonNode.Parse(SharedFile.ReadAllText(path)); } - catch (JsonException) { return null; } // a file another process is creating this instant: not there yet - catch (IOException) { return null; } - catch (UnauthorizedAccessException) { return null; } // Windows: a file being replaced this instant - } - - public static void WriteJson(string path, JsonNode value) => WriteAtomically(path, Format(value)); - - /// The text writes: a tracked file in this form is unchanged by a publish that changes nothing in it. - public static string Format(JsonNode value) => value.ToJsonString(Indented) + "\n"; - - public static List ReadLines(string path) - { - var result = new List(); - if (!File.Exists(path)) return result; - foreach (var line in SharedFile.ReadAllLines(path)) - { - if (line.Trim().Length == 0) continue; - try { if (JsonNode.Parse(line) is JsonObject o) result.Add(o); } - catch (JsonException) { } - } - return result; - } - - /// Appends one line by writing the whole file anew and renaming it over the old one. - public static void AppendLine(string path, JsonObject value) - { - var old = File.Exists(path) ? SharedFile.ReadAllText(path) : ""; - if (old.Length > 0 && !old.EndsWith('\n')) old = old[..(old.LastIndexOf('\n') + 1)]; - WriteAtomically(path, old + value.ToJsonString() + "\n"); - } - - public static void WriteAtomically(string path, string text) - { - Directory.CreateDirectory(Path.GetDirectoryName(path)!); - var tmp = $"{path}.{Environment.ProcessId}.{Guid.NewGuid():N}.tmp"; - File.WriteAllText(tmp, text, new UTF8Encoding(false)); - SharedFile.Replace(tmp, path); - } - - /// A rows file, plain (staging) or gzipped (the corpus). - public static JsonArray ReadRows(string path) - { - if (File.Exists(path)) return ReadJson(path)?["rows"] as JsonArray ?? []; - if (File.Exists(path + ".gz")) - { - using var gz = new GZipStream(SharedFile.OpenRead(path + ".gz"), CompressionMode.Decompress); - using var reader = new StreamReader(gz, Encoding.UTF8); - return JsonNode.Parse(reader.ReadToEnd())?["rows"] as JsonArray ?? []; - } - return []; - } - - public static byte[] Gunzip(string path) - { - using var gz = new GZipStream(SharedFile.OpenRead(path), CompressionMode.Decompress); - using var memory = new MemoryStream(); - gz.CopyTo(memory); - return memory.ToArray(); - } - - /// Writes rows gzipped and reproducibly: one row per line, keys sorted, no timestamp in the header. - public static void WriteRowsGzipped(string path, string claim, IEnumerable rows) - { - var body = new StringBuilder(); - body.Append("{\"claim\": ").Append(JsonSerializer.Serialize(claim)).Append(", \"rows\": [\n"); - body.Append(string.Join(",\n", rows.Select(r => "{\"row\":" + Canonical(r) + "}"))); - body.Append("\n]}\n"); - var tmp = $"{path}.{Environment.ProcessId}.tmp"; - Directory.CreateDirectory(Path.GetDirectoryName(path)!); - using (var file = File.Create(tmp)) - using (var gz = new GZipStream(file, CompressionLevel.SmallestSize)) - gz.Write(Encoding.UTF8.GetBytes(body.ToString())); - SharedFile.Replace(tmp, path); - } - - /// A JSON object with its keys in ordinal order, recursively: the form hashed for an identity. - public static string Canonical(JsonNode? node) => node switch - { - JsonObject o => "{" + string.Join(",", o.OrderBy(kv => kv.Key, StringComparer.Ordinal) - .Select(kv => JsonSerializer.Serialize(kv.Key) + ":" + Canonical(kv.Value))) + "}", - JsonArray a => "[" + string.Join(",", a.Select(Canonical)) + "]", - null => "null", - _ => node.ToJsonString(), - }; - - public static string Str(JsonNode? node, string fallback = "") => node is JsonValue v && v.TryGetValue(out var s) ? s : fallback; - - /// A number, whether parsed from text or built in memory from any numeric type. - public static double Num(JsonNode? node, double fallback = 0) - { - if (node is not JsonValue v) return fallback; - if (v.TryGetValue(out var d)) return d; - if (v.TryGetValue(out var l)) return l; - if (v.TryGetValue(out var i)) return i; - if (v.TryGetValue(out var m)) return (double)m; - return double.TryParse(v.ToJsonString(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var p) ? p : fallback; - } - - public static IEnumerable Strings(JsonNode? node) => node is JsonArray a ? a.Select(x => Str(x)) : []; - - public static Dictionary StringMap(JsonNode? node) => - node is JsonObject o ? o.Where(kv => kv.Value is JsonValue).ToDictionary(kv => kv.Key, kv => Str(kv.Value, kv.Value!.ToJsonString()), StringComparer.Ordinal) - : new(StringComparer.Ordinal); -} diff --git a/tools/Prober/Ledger.cs b/tools/Prober/Ledger.cs deleted file mode 100644 index 50673244..00000000 --- a/tools/Prober/Ledger.cs +++ /dev/null @@ -1,431 +0,0 @@ -using System.Security.Cryptography; -using System.Text; -using System.Text.Json.Nodes; - -namespace Prober; - -/// -/// One branch's ledger. Tracked, in the repository: the corpus under tools/probe/corpus (claims, registry, -/// harness and probe sources, rows, waivers, dismissed findings) and the axes file. Local, under the state -/// directory: the universe, evidence, footprints, runs, findings, the round's caps and the lock. A pair is -/// covered only by evidence a replay recorded under an identity that still holds. -/// -internal sealed class Ledger -{ - public Context Ctx { get; } - public string Root { get; } // the main checkout, also from a linked worktree - public string Top { get; } // this checkout - public bool Linked { get; } // a linked worktree (git worktree add), not the main checkout - public string Branch { get; } - public string Memory { get; } // PROBE_STATE_DIR, else the main checkout's prober memory - public string Dir { get; } // the branch's directory under Memory - public string CorpusDir => Path.Combine(Top, "tools", "probe", "corpus"); - public string AxesFile => Path.Combine(Top, ".claude", "probe-axes.md"); - public string StagingDir => Path.Combine(Dir, "staging"); - - public Ledger(Context ctx, string? branch) - { - Ctx = ctx; - var dirs = Git.Run(ctx.WorkingDirectory, "rev-parse", "--path-format=absolute", "--git-common-dir", "--git-dir"); - if (dirs.ExitCode != 0) throw new Refusal("not inside a git repository"); - var lines = dirs.Stdout.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Select(d => d.TrimEnd('/', '\\')).ToArray(); - if (lines is not [var common, var own]) throw new Refusal("git rev-parse named no git directory"); - Root = Path.GetDirectoryName(common)!; - Linked = own != common; - Top = Git.Out(ctx.WorkingDirectory, "rev-parse", "--show-toplevel"); - Branch = branch ?? Git.Out(ctx.WorkingDirectory, "rev-parse", "--abbrev-ref", "HEAD"); - Memory = ctx.Env("PROBE_STATE_DIR") ?? Path.Combine(Root, ".claude", "agent-memory-local", "prober"); - Dir = Path.Combine(Memory, "branches", Branch.Replace('/', '-')); - } - - public string Head => Git.Out(Top, "rev-parse", "HEAD"); - public string P(string name) => Path.Combine(Dir, name); - - // ---- the corpus, tracked, overlaid by the branch's unpublished staging ------------------------------ - - public string CorpusPath(string relative) - { - var staged = Path.Combine(StagingDir, relative); - return File.Exists(staged) ? staged : Path.Combine(CorpusDir, relative); - } - - public SortedDictionary Registry() - { - var merged = new SortedDictionary(StringComparer.Ordinal); - foreach (var path in new[] { Path.Combine(CorpusDir, "registry.json"), Path.Combine(StagingDir, "registry.json") }) - if (Files.ReadJson(path)?["probes"] is JsonObject probes) - foreach (var (name, entry) in probes) - if (entry is JsonObject e) merged[name] = (JsonObject)e.DeepClone(); - return merged; - } - - public List Claims() - { - var merged = new SortedDictionary(StringComparer.Ordinal); - foreach (var path in new[] { Path.Combine(CorpusDir, "claims.json"), Path.Combine(StagingDir, "claims.json") }) - foreach (var c in ReadClaims(path, Path.GetRelativePath(Top, path).Replace('\\', '/'))) - merged[Files.Str(c["id"])] = (JsonObject)c.DeepClone(); - return [.. merged.Values]; - } - - /// - /// A claims file, refused unless it matches Schemas/claims.schema.json, each problem named by its claim and - /// field (decision 102). No file is no claims. - /// - public static List ReadClaims(string path, string shown) - { - if (!File.Exists(path)) return []; - if (Files.ReadJson(path) is not JsonArray claims) throw new Refusal($"{shown} is not a JSON list of claims"); - var schema = SchemaCheck.Load("claims.schema.json")["items"] as JsonObject ?? throw new InvalidOperationException("claims.schema.json has no items"); - var problems = new List(); - for (var i = 0; i < claims.Count; i++) - { - var id = claims[i] is JsonObject o && Files.Str(o["id"]) is { Length: > 0 } s ? s : $"#{i}"; - problems.AddRange(SchemaCheck.Problems(claims[i], schema).Select(p => $"claim {id}: {p}")); - } - if (problems.Count > 0) - throw new Refusal($"{shown} does not match tools/Prober/Schemas/claims.schema.json:\n " + string.Join("\n ", problems)); - return [.. claims.OfType()]; - } - - /// - /// A probe's own rows (rows/P.json, staged, else the corpus's rows/P.json.gz). Each probe has a file of its own, its claim - /// inside, so the rows of one probe of a claim never move another's identity. - /// - public JsonArray RowsOf(string probe) - { - var relative = Path.Combine("rows", probe + ".json"); - var staged = Path.Combine(StagingDir, relative); - return File.Exists(staged) ? Files.ReadRows(staged) : Files.ReadRows(Path.Combine(CorpusDir, relative)); - } - - /// - /// The paths a probe's registry entry watches: what its result depends on that the footprint cannot see, such as a - /// variant catalogue under test/ compiled into the probes or a build input a child process reads. They stale its - /// footprint like the paths it ran. - /// - public IReadOnlyList Watched(string probe) => [.. Files.Strings(Registry().GetValueOrDefault(probe)?["watched"])]; - - public JsonObject AxesBlock() => Axes.Block(AxesFile); - - // ---- identity -------------------------------------------------------------------------------------- - - /// - /// What a probe's result depends on besides the product footprint: its registry entry (not its seed), every source - /// compiled with it (the harness and all probes), its own rows (not a sibling's), the axes file, the build environment - /// (SDK, central package versions, build props, global.json), this tool's version and the schema version, and - /// where its claim declares where an obligation does not apply, those declarations, which the harness reads. - /// A result counts only while this is unchanged. - /// - public string Identity(string probe) - { - using var sha = IncrementalHash.CreateHash(HashAlgorithmName.SHA256); - void Add(string s) => sha.AppendData(Encoding.UTF8.GetBytes(s + "\0")); - var entry = Registry().GetValueOrDefault(probe) ?? []; - // The seeds prove the probe; a result of the probe depends on none of them (decisions 100 and 104). - var registered = (JsonObject)entry.DeepClone(); - registered.Remove("seed"); - registered.Remove("seeds"); - Add(Files.Canonical(registered)); - var sources = new SortedDictionary(StringComparer.Ordinal); - foreach (var baseDir in new[] { CorpusDir, StagingDir }) - foreach (var sub in new[] { "harness", "probes" }) - { - var folder = Path.Combine(baseDir, sub); - if (Directory.Exists(folder)) - foreach (var file in Directory.EnumerateFiles(folder, "*.cs")) sources[Path.GetFileName(file)] = file; - } - foreach (var (name, file) in sources) { Add(name); sha.AppendData(File.ReadAllBytes(file)); } - Add(Files.Canonical(RowsOf(probe))); - // The declarations Row.Check applies before a check's body runs: a change to one changes what the probe's rows - // check. Added only where the claim declares something, so a claim without declarations leaves the identity as it was. - if (Declarations(Files.Str(entry["claim"])) is { } declared) Add("inapplicable:" + Files.Canonical(declared)); - try { Add(Files.Canonical(AxesBlock())); } catch (Refusal) { Add("no axes file"); } - foreach (var path in new[] { "Directory.Packages.props", "Directory.Build.props", "global.json" }) - Add(path + (Git.ObjectId(Top, path) ?? "-")); - Add(Files.Canonical(Toolchain())); - Add("tool:" + SourceFingerprint.ToolVersion[..16]); - Add("schema:" + Files.SchemaVersion); - return Convert.ToHexStringLower(sha.GetHashAndReset())[..16]; - } - - private Dictionary? declarations; - - /// A claim's declarations of where its obligations do not apply, as claims.json holds them, by obligation; null where it declares none. - private JsonArray? Declarations(string claim) - { - declarations ??= Claims().ToDictionary(c => Files.Str(c["id"]), c => new JsonArray([.. (c["obligations"] as JsonArray ?? []).OfType() - .Where(o => o["inapplicable"] is JsonArray { Count: > 0 }) - .Select(o => (JsonNode)new JsonObject { ["obligation"] = Files.Str(o["id"]), ["inapplicable"] = o["inapplicable"]!.DeepClone() })]), StringComparer.Ordinal); - return declarations.TryGetValue(claim, out var declared) && declared.Count > 0 ? declared : null; - } - - private JsonObject? toolchain; - - /// - /// What no file in the repository records: the SDK a global.json roll-forward picked, say. A command that cannot - /// say it (it does not start, exits non-zero or prints nothing) is a refusal: its error text in every identity - /// would read as a toolchain change, and every result as stale, without a word why. - /// - public JsonObject Toolchain() - { - if (toolchain is not null) return toolchain; - JsonObject? commands; - try { commands = AxesBlock()["replay"]?["toolchain"] as JsonObject; } catch (Refusal) { commands = null; } - var result = new JsonObject(); - foreach (var (name, argv) in (commands ?? []).OrderBy(kv => kv.Key, StringComparer.Ordinal)) - { - var parts = Files.Strings(argv).ToArray(); - var shown = $"the toolchain command {name} (`{string.Join(' ', parts)}`)"; - if (parts.Length == 0) throw new Refusal($"{shown} is empty"); - Ran ran; - try { ran = Proc.Run(parts[0], parts[1..], Top); } - catch (System.ComponentModel.Win32Exception e) { throw new Refusal($"{shown} did not start: {e.Message}"); } - static string FirstLine(string s) => s.Trim().Split('\n')[0].Trim(); - // A failing `dotnet --version` lists the installed SDKs on stdout ("10.0.401 [path]", a version to look at) - // and says why on stderr. - if (ran.ExitCode != 0) - { - var why = FirstLine(ran.Stderr) is { Length: > 0 } err ? err : FirstLine(ran.Stdout); - throw new Refusal($"{shown} exited {ran.ExitCode}: {(why.Length > 0 ? why : "no output")}. A result is bound to what it prints, " + - "so none could be current; put the toolchain global.json asks for first on PATH, or install it where " + - $"the SDK installer puts it ({Dotnet.InstallerDirectory}), which `dotnet` falls back to"); - } - var first = FirstLine(ran.Stdout.Trim().Length > 0 ? ran.Stdout : ran.Stderr); - if (first.Length == 0) throw new Refusal($"{shown} printed nothing, so a result could be bound to no toolchain"); - result[name] = first; - } - return toolchain = result; - } - - // ---- footprints, evidence, coverage ------------------------------------------------------------------ - - public Dictionary Footprints() => - Files.ReadLines(P("footprints.jsonl")).GroupBy(l => Files.Str(l["probe"]), StringComparer.Ordinal) - .ToDictionary(g => g.Key, g => g.Last(), StringComparer.Ordinal); - - private readonly Dictionary currentCache = new(StringComparer.Ordinal); - - /// - /// A probe's footprint is current when every path it ran, every compile-time path, every path its registry entry - /// watches and the toolchain are unchanged at HEAD. A watched path the footprint did not record (one added to the - /// entry since) is not current. A footprint that ran nothing under src/ is current only by watched paths it recorded. - /// - public bool FootprintCurrent(string probe, Dictionary footprints) - { - if (currentCache.TryGetValue(probe, out var cached)) return cached; - var ok = false; - if (footprints.TryGetValue(probe, out var r) && r["footprint"] is JsonObject fp && (fp.Count > 0 || r["watched"] is JsonObject { Count: > 0 })) - { - var watched = r["watched"] as JsonObject ?? []; - ok = Watched(probe).All(watched.ContainsKey) - && fp.Concat(r["compileTime"] as JsonObject ?? []).Concat(watched).All(kv => Git.ObjectId(Top, kv.Key) == Files.Str(kv.Value, null!)); - if (ok) - { - var now = Toolchain(); - ok = (r["toolchain"] as JsonObject ?? []).All(kv => Files.Str(now[kv.Key]) == Files.Str(kv.Value)); - } - } - return currentCache[probe] = ok; - } - - public bool FootprintChangedSince(string probe, string since, Dictionary footprints) - { - if (!footprints.TryGetValue(probe, out var r)) return true; - var paths = (r["footprint"] as JsonObject ?? []).Select(kv => kv.Key).Concat((r["compileTime"] as JsonObject ?? []).Select(kv => kv.Key)) - .Concat((r["watched"] as JsonObject ?? []).Select(kv => kv.Key)).Concat(Watched(probe)) - .Distinct(StringComparer.Ordinal).ToArray(); - if (paths.Length == 0) return true; - return Git.Run(Top, ["diff", "--quiet", since, "HEAD", "--", .. paths]).ExitCode != 0; - } - - public string EvidencePath(string probe) => P(Path.Combine("evidence", probe + ".json")); - - /// A check that earned credit: the obligation it named and the settings it ran under. - public sealed record Credit(string Obligation, Dictionary Applied); - - /// - /// The checks of one probe's evidence that earn credit, each with its obligation and settings: only while the - /// probe's identity and product footprint are current, and only checks that ended as a pass in rows that were - /// expected and executed. A check's settings are the row's (read outside any check, so every check ran under them) - /// with its own laid over them; a row without checks is one check of the obligation claim, credited when - /// the row passed. - /// - public List CreditedChecks(string probe, JsonObject? evidence, Dictionary? footprints) - { - var result = new List(); - if (evidence is null || Files.Str(evidence["identity"]) != Identity(probe)) return result; - if (footprints is not null && !FootprintCurrent(probe, footprints)) return result; - var expected = new HashSet(); var executed = new HashSet(); - foreach (var v in (evidence["variants"] as JsonArray ?? []).OfType()) - { - foreach (var i in v["expected"] as JsonArray ?? []) expected.Add((int)Files.Num(i)); - foreach (var i in v["executed"] as JsonArray ?? []) executed.Add((int)Files.Num(i)); - } - foreach (var (key, row) in evidence["rows"] as JsonObject ?? []) - { - var i = int.Parse(key, System.Globalization.CultureInfo.InvariantCulture); - if (row is not JsonObject r || !expected.Contains(i) || !executed.Contains(i)) continue; - foreach (var check in ChecksOf(r)) - if (check.Category == "pass" && check.Applied.Count > 0) result.Add(new Credit(check.Obligation, check.Applied)); - } - return result; - } - - /// One check of an evidence row as the ledger reads it. - public sealed record CheckRead(string Obligation, string Category, Dictionary Applied, string? Baseline); - - /// - /// The checks of one evidence row: each with its obligation, category and settings (the row's, the check's laid - /// over them). A row without checks, every row of a version-1 file among them, is one check of the implicit - /// obligation claim with the row's category and settings and no baseline. - /// - public static List ChecksOf(JsonObject row) - { - var applied = row["applied"] is JsonObject a ? Files.StringMap(a) : new Dictionary(StringComparer.Ordinal); - if (row["checks"] is not JsonArray { Count: > 0 } checks) - return [new CheckRead(Claim.ImplicitObligation, Files.Str(row["category"], "violation"), applied, null)]; - var result = new List(); - foreach (var c in checks.OfType()) - { - var own = new Dictionary(applied, StringComparer.Ordinal); - if (c["applied"] is JsonObject ca) foreach (var (axis, value) in Files.StringMap(ca)) own[axis] = value; - result.Add(new CheckRead(Files.Str(c["obligation"]), Files.Str(c["category"], "violation"), own, c["baseline"] is JsonValue b ? Files.Str(b) : null)); - } - return result; - } - - /// - /// (claim, obligation, pair) for every pair a current check earned, leaving out the probe - /// names. Each check's settings alone make its pairs: a pair is never put together from two checks' settings. - /// - public HashSet<(string Claim, string Obligation, Pair Pair)> Credits(Universe universe, string? claimId = null, string? except = null) - { - var footprints = Footprints(); - var done = new HashSet<(string, string, Pair)>(); - foreach (var (name, entry) in Registry()) - { - var cid = Files.Str(entry["claim"]); - if (name == except || (claimId is not null && cid != claimId) || !universe.ById.TryGetValue(cid, out var claim)) continue; - var evidence = Files.ReadJson(EvidencePath(name)) as JsonObject; - foreach (var credit in CreditedChecks(name, evidence, footprints)) - foreach (var pair in claim.PairsOfRow(credit.Applied)) done.Add((cid, credit.Obligation, pair)); - } - return done; - } - - /// - /// (claim, pair) for every pair current evidence earned, leaving out the probe names. A round's - /// own ledger lines earn nothing. - /// - public HashSet<(string Claim, Pair Pair)> Covered(Universe universe, string? claimId = null, string? except = null) => - [.. Credits(universe, claimId, except).Select(x => (x.Claim, x.Pair))]; - - /// The last event per finding; a fixed line counts once its commit is in HEAD's history, a dismissal in the corpus everywhere. - public Dictionary Findings() - { - var last = new Dictionary(StringComparer.Ordinal); - foreach (var f in Files.ReadLines(P("findings.jsonl"))) - { - if (Files.Str(f["status"]) == "fixed" && Files.Str(f["commit"]) is { Length: > 0 } c - && Git.Run(Top, "merge-base", "--is-ancestor", c, "HEAD").ExitCode != 0) continue; - last[Files.Str(f["id"])] = f; - } - foreach (var f in Files.ReadLines(CorpusPath("dismissed.jsonl"))) - { - var d = (JsonObject)f.DeepClone(); d["status"] = "dismissed"; - last[Files.Str(f["id"])] = d; - } - return last; - } - - /// The findings one probe's evidence steps around, with the number of rows that skip for each. - public static SortedDictionary FindingSkips(JsonObject? evidence) - { - var skips = new SortedDictionary(StringComparer.Ordinal); - foreach (var (_, row) in evidence?["rows"] as JsonObject ?? []) - foreach (var f in Files.Strings(row?["findings"])) skips[f] = skips.GetValueOrDefault(f) + 1; - return skips; - } - - /// - /// The skips of one probe's evidence that no longer stand: a finding this ledger has fixed (in HEAD's history) - /// or the corpus has dismissed. Such a skip would hide the defect coming back, so the replay fails the probe. - /// A finding this ledger has never seen (a fresh state, as in CI) is left to the gate, which refuses any skip. - /// - public List StaleSkips(JsonObject? evidence) - { - var findings = Findings(); - var stale = new List(); - foreach (var (id, rows) in FindingSkips(evidence)) - { - if (!findings.TryGetValue(id, out var f)) continue; - var status = Files.Str(f["status"]); - if (status == "fixed" && Files.Str(f["commit"]) is { Length: >= 7 } commit) status += " at " + commit[..7]; - if (status != "open") stale.Add($"{rows} row(s) skip for {id}, which is {status}: remove the skip"); - } - return stale; - } - - /// How the rows of one probe's evidence ended, by category. - public static SortedDictionary Outcomes(JsonObject? evidence) - { - var outcomes = new SortedDictionary(StringComparer.Ordinal); - foreach (var (_, row) in evidence?["rows"] as JsonObject ?? []) - { - var category = Files.Str(row?["category"], "violation"); - outcomes[category] = outcomes.GetValueOrDefault(category) + 1; - } - return outcomes; - } - - /// How the checks of one probe's evidence ended, by category; a row without checks counts as its one check claim. - public static SortedDictionary CheckOutcomes(JsonObject? evidence) - { - var outcomes = new SortedDictionary(StringComparer.Ordinal); - foreach (var (_, row) in evidence?["rows"] as JsonObject ?? []) - if (row is JsonObject r) - foreach (var check in ChecksOf(r)) outcomes[check.Category] = outcomes.GetValueOrDefault(check.Category) + 1; - return outcomes; - } - - /// The obligations a check of one probe's evidence passed for at its last replay, current or not. - public static HashSet PassedObligations(JsonObject? evidence) - { - var passed = new HashSet(StringComparer.Ordinal); - foreach (var (_, row) in evidence?["rows"] as JsonObject ?? []) - if (row is JsonObject r) - foreach (var check in ChecksOf(r).Where(c => c.Category == "pass")) passed.Add(check.Obligation); - return passed; - } - - public List OpenFindings() => [.. Findings().Where(kv => Files.Str(kv.Value["status"]) == "open").Select(kv => kv.Key).Order(StringComparer.Ordinal)]; - - public sealed record ProbeState(string Probe, string Claim, string Filter, JsonArray Variants, bool Stale, bool Retired, int Passes, bool Replay); - - /// A registry entry's process variants: its own, or one that sets nothing. - public static JsonArray VariantsOf(JsonObject entry) => entry["variants"] is JsonArray va && va.Count > 0 ? (JsonArray)va.DeepClone() : [new JsonObject()]; - - public List ProbeStates(bool includeRetired) - { - var footprints = Footprints(); - var runs = Files.ReadLines(P("runs.jsonl")); - var states = new List(); - foreach (var (probe, r) in Registry()) - { - var passes = runs.Count(x => Files.Str(x["probe"]) == probe && Files.Str(x["result"]) == "pass"); - var retired = r["retired"] is JsonValue rv && rv.TryGetValue(out var b) && b; - var evidence = Files.ReadJson(EvidencePath(probe)); - var stale = !FootprintCurrent(probe, footprints) || evidence is null || Files.Str(evidence["identity"]) != Identity(probe); - var variants = VariantsOf(r); - states.Add(new ProbeState(probe, Files.Str(r["claim"]), Files.Str(r["filter"]), variants, stale, retired, passes, stale && (includeRetired || !retired))); - } - return states; - } -} - -/// A refusal the user acts on: printed as "prober: ...", exit 1. Never a stack trace. -internal sealed class Refusal(string message, int exitCode = 1) : Exception(message) -{ - public int ExitCode { get; } = exitCode; -} diff --git a/tools/Prober/NewProbe.cs b/tools/Prober/NewProbe.cs deleted file mode 100644 index 97047dcd..00000000 --- a/tools/Prober/NewProbe.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System.Text; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.RegularExpressions; - -namespace Prober; - -/// -/// new: the mechanical half of writing a probe, done the same way every time, into the branch's staging. -/// It writes the probe's skeleton in the harness's form (probes/P.cs), headed by the claim's source and -/// every obligation's id and sentence, with one check per obligation that throws until it is written; stages the -/// probe's own rows (a covering array over its claim's pairs the claim's other probes have not covered, as -/// rows --probe P writes); -/// and registers the probe with an empty seeds, so seeds and Seeds.Problems name each -/// obligation's seed as owed. The skeleton builds, so the other probes keep building beside it, and every row of -/// it ends as a harness error, which fails the row, earns nothing and is never a seed's catch (decision 103). -/// It refuses, writing nothing, a claim that is not in the claims, a name a probe has already, and a claim -/// whose first surface is in no family of the replay configuration (or not in the one --family names). -/// -internal static partial class NewProbe -{ - [GeneratedRegex("^[A-Za-z_][A-Za-z0-9_]*Probe$")] - private static partial Regex ProbeName(); - - [GeneratedRegex("^([A-Za-z]+[0-9]+)(?:-(.*))?$")] - private static partial Regex ClaimPrefix(); - - [GeneratedRegex("[^A-Za-z0-9]+")] - private static partial Regex NotAlphanumeric(); - - public static int Run(Ledger l, Args a) - { - var claimId = a.Need("claim"); - var claim = l.Claims().FirstOrDefault(c => Files.Str(c["id"]) == claimId) - ?? throw new Refusal($"no claim {claimId} in claims.json (tracked or staged)"); - var probe = a.Get("probe") is { Length: > 0 } named ? named : DefaultName(claimId); - if (!ProbeName().IsMatch(probe)) throw new Refusal($"{probe} is not a probe's name: a C# class name ending in Probe", 2); - if (Taken(l, probe) is { } where) throw new Refusal($"a probe {probe} exists already ({where}); name another with --probe"); - var family = Family(l, claim, a.Get("family")); - var obligations = Obligations(claim); - - var source = Path.Combine(l.StagingDir, "probes", probe + ".cs"); - Files.WriteAtomically(source, Skeleton(claim, probe, family, obligations)); - // The probe's own rows, its claim's other probes' untouched: a covering array over the pairs they have not covered. - var (rows, _) = Commands.StageRows(l, probe, claimId, full: false); - var registry = Path.Combine(l.StagingDir, "registry.json"); - var staged = Files.ReadJson(registry) as JsonObject ?? new JsonObject { ["probes"] = new JsonObject() }; - var probes = staged["probes"] as JsonObject ?? []; - probes[probe] = new JsonObject { ["claim"] = claimId, ["filter"] = "*" + probe, ["seeds"] = new JsonArray() }; - staged["probes"] = probes.DeepClone(); - Files.WriteJson(registry, staged); - - var o = l.Ctx.Out; - o.WriteLine($"prober new: {probe} for claim {claimId}{(family is null ? "" : $" (family {family})")}, staged in {l.StagingDir}"); - o.WriteLine($" probes/{probe}.cs: the skeleton; it builds, and every row ends as a harness error until each obligation's check is written"); - o.WriteLine($" rows/{probe}.json: {rows} rows over the claim's pairs its other probes have not covered" + - (rows == 0 ? "; with no row the probe has no test to run" : "")); - o.WriteLine($" registry.json: {probe}, claim {claimId}, filter *{probe}, no seed yet"); - o.WriteLine("still owed, per obligation:"); - foreach (var (id, sentence, method) in obligations) - o.WriteLine($" {id}: the check {method} ({(sentence is null ? "the claim as a whole" : $"\"{sentence}\"")}), and a seed that breaks it " + - (id == Claim.ImplicitObligation && sentence is null - ? $"(`prober register --probe {probe} --claim {claimId} --filter \"*{probe}\" --seed seeds/{claimId}.patch --defect \"...\"`)" - : $"(`prober register --probe {probe} --claim {claimId} --filter \"*{probe}\" --obligation {id} --seed seeds/{claimId}-{id}.patch --defect \"...\"`)")); - o.WriteLine($"then: `prober try --probe {probe}` until each row ends as it should, `prober seeds --probe {probe}`, `prober register` (it lints the staged sources), `prober publish`"); - return 0; - } - - /// The claim id's letter-digit prefix, then its words run together, then Probe: C20-testing-shouldhold is C20TestingShouldholdProbe. - internal static string DefaultName(string claimId) - { - var m = ClaimPrefix().Match(claimId); - var (prefix, rest) = m.Success ? (m.Groups[1].Value, m.Groups[2].Value) : ("", claimId); - return Pascal(prefix) + Pascal(rest) + "Probe"; - } - - private static string Pascal(string text) => - string.Concat(NotAlphanumeric().Split(text).Where(w => w.Length > 0).Select(w => char.ToUpperInvariant(w[0]) + w[1..])); - - /// Where a probe of that name is already: the registry, a source file of that name, or a class of that name in any corpus source. - private static string? Taken(Ledger l, string probe) - { - if (l.Registry().ContainsKey(probe)) return "in the registry"; - var declares = new Regex($@"\b(class|record|struct)\s+{probe}\b"); - foreach (var dir in new[] { l.CorpusDir, l.StagingDir }) - foreach (var sub in new[] { "harness", "probes" }) - { - var folder = Path.Combine(dir, sub); - if (!Directory.Exists(folder)) continue; - foreach (var file in Directory.EnumerateFiles(folder, "*.cs")) - if (Path.GetFileNameWithoutExtension(file) == probe || declares.IsMatch(File.ReadAllText(file))) return file; - } - return null; - } - - /// - /// The family the replay runs the probe in, its claim's first surface's (decision 101), checked against - /// --family. Null where the replay configuration names no families and none was asked for. - /// - private static string? Family(Ledger l, JsonObject claim, string? asked) - { - var families = (l.AxesBlock()["replay"] as JsonObject)?["families"] as JsonObject; - if (families is null) - return asked is null ? null : throw new Refusal($"--family {asked}: the replay configuration names no families", 2); - var surface = Files.Strings(claim["surfaces"]).FirstOrDefault() ?? ""; - var family = families.FirstOrDefault(kv => Files.Strings(kv.Value).Contains(surface)).Key - ?? throw new Refusal($"the first surface of claim {Files.Str(claim["id"])} ({surface}) is in no family of the replay configuration, so a replay by family would never run its probe"); - if (asked is not null && asked != family) - throw new Refusal($"claim {Files.Str(claim["id"])} is of family {family} (its first surface, {surface}), not {asked}", 2); - return family; - } - - /// Each obligation of the claim with its sentence and the check method the skeleton gives it; a claim without obligations has one, the claim itself, with no sentence. - internal static List<(string Id, string? Sentence, string Method)> Obligations(JsonObject claim) - { - var listed = (claim["obligations"] as JsonArray ?? []).OfType() - .Select(o => (Id: Files.Str(o["id"]), Sentence: (string?)OneLine(Files.Str(o["sentence"])))).ToList(); - if (listed.Count == 0) listed = [(Claim.ImplicitObligation, null)]; - var methods = new HashSet(StringComparer.Ordinal); - var result = new List<(string, string?, string)>(); - foreach (var (id, sentence) in listed) - { - var method = "Check" + Pascal(id); - for (var n = 2; !methods.Add(method); n++) method = "Check" + Pascal(id) + n; - result.Add((id, sentence, method)); - } - return result; - } - - /// The probe's source: the form of the corpus's probes (Rows.Data, [Theory, MemberData(nameof(Data))], Rec.Run), one throwing check per obligation. - internal static string Skeleton(JsonObject claim, string probe, string? family, List<(string Id, string? Sentence, string Method)> obligations) - { - var id = Files.Str(claim["id"]); - var literal = JsonSerializer.Serialize(probe); - var b = new StringBuilder(); - void Line(string text = "") => b.Append(text).Append('\n'); - Line($"// Claim {id} ({Files.Str(claim["source"])}), surfaces {string.Join(", ", Files.Strings(claim["surfaces"]))}{(family is null ? "" : $", family {family}")}."); - Line("// Obligations (claims.json), each with a check below and a seed of its own in the registry entry (decision 104):"); - foreach (var (oid, sentence, _) in obligations) - Line($"// {oid}: {(sentence is null ? "the claim as a whole (claims.json lists no obligations for it)" : $"\"{sentence}\"")}"); - Line("// Oracle: TODO: what each row is compared against, and the result it expects."); - Line("// Written by `prober new`: it builds, and every row ends as a harness error until each obligation's check is written."); - Line("using Probes.Shapes;"); - Line("namespace Probes;"); - Line($"public sealed class {probe}"); - Line("{"); - Line($" public static TheoryData Data() => Rows.Data({literal});"); - Line(" [Theory, MemberData(nameof(Data))]"); - Line(" public Task Row(string probe, int idx) => Rec.Run(probe, idx, Check);"); - Line(); - Line(" static async Task Check(Row r) =>"); - for (var i = 0; i < obligations.Count; i++) - Line($" {(i == 0 ? "" : "?? ")}await {obligations[i].Method}(r){(i == obligations.Count - 1 ? ";" : "")}"); - foreach (var (oid, sentence, method) in obligations) - { - Line(); - Line($" // TODO {oid}: {(sentence is null ? "the claim as a whole" : $"\"{sentence}\"")}"); - Line(" // Return the failure text, or null where the row holds it; say how it was compared with r.Note(NoteKeys.Compared, detail)."); - Line($" static Task {method}(Row r) => throw new NotImplementedException({JsonSerializer.Serialize($"{id}/{oid}: its check is not written yet")});"); - } - Line("}"); - return b.ToString(); - } - - private static string OneLine(string text) => string.Join(" ", text.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)); -} diff --git a/tools/Prober/ProbeAdapter.cs b/tools/Prober/ProbeAdapter.cs deleted file mode 100644 index fc993e87..00000000 --- a/tools/Prober/ProbeAdapter.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System.Text.Json.Nodes; -using System.Text.RegularExpressions; - -namespace Prober; - -/// -/// What the core needs from a repository's toolchain, and nothing more: build the probes, run one probe in -/// one process with an environment (writing its rows' outcomes and a coverage report), and derive the -/// generated axes. The core holds nothing of .NET's; a repository on another stack plugs in its own. -/// -internal interface IProbeAdapter -{ - /// Builds what the probes need in the worktree. The first line of the output is shown. - Ran Build(string worktree, IReadOnlyDictionary environment); - - /// Runs one probe (by its filter) with the environment given; the coverage report goes to under . - Ran RunProbe(string worktree, string filter, string results, string coverageFile, IReadOnlyDictionary environment); - - /// The generated axes (settings of the option types the features read), or null with a reason. - (JsonObject? Axes, string? Error) DeriveAxes(string checkout, string workDirectory); - - /// The environment every build and run of the probes needs (for .NET: the coverage extension). - IReadOnlyDictionary Environment(string cacheDirectory); -} - -/// -/// The .NET adapter, configured by the "replay" object of the axes file: argv templates for the build and -/// the test run (through the repository's verdict tool), the commands that prepare the worktree first (building -/// that tool), the coverage extension referenced from a targets file outside the worktree, and the reflection -/// enumerator run with the repository's package versions. -/// -internal sealed class DotNetAdapter(JsonObject config) : IProbeAdapter -{ - private string Project => Files.Str(config["project"]); - - private static string[] Expand(JsonNode? template, Dictionary values) => - [.. Files.Strings(template).Select(arg => Regex.Replace(arg, @"\{(\w+)\}", m => values.TryGetValue(m.Groups[1].Value, out var v) ? v : m.Value))]; - - /// - /// The program a template starts: a bare name (no path separator, dotnet) is found on PATH; a path is - /// relative to the replay worktree and must exist there, so a template naming a file the repository no longer - /// has fails by name, never as a puzzling exit code. - /// - internal static string Executable(string worktree, string program) - { - if (program.IndexOfAny(['/', '\\']) < 0) return program; - var path = Path.Combine(worktree, program); - return File.Exists(path) ? path : throw new Refusal($"the replay template names {program}, which the worktree does not have"); - } - - private static Ran Start(string worktree, string[] argv, IReadOnlyDictionary environment) => - Proc.Run(Executable(worktree, argv[0]), argv[1..], worktree, environment); - - /// Prepares the worktree (the "prepare" commands: building the verdict tool, which a fresh or moved worktree lacks or holds stale), then builds the probes. - public Ran Build(string worktree, IReadOnlyDictionary environment) - { - foreach (var command in (config["prepare"] as JsonArray ?? []).OfType()) - { - var prepared = Start(worktree, Expand(command, new() { ["project"] = Project }), environment); - if (prepared.ExitCode != 0) - return prepared with { Stdout = $"PREPARE FAILED: {string.Join(' ', Files.Strings(command))}\n" + prepared.Stdout + prepared.Stderr }; - } - return Start(worktree, Expand(config["build"], new() { ["project"] = Project }), environment); - } - - /// - /// The "test" template, then the "coverage" arguments when a coverage file is asked for. A replay without a - /// footprint asks for none, so a template that carries the coverage arguments itself is refused rather than - /// run with an empty file name. - /// - public Ran RunProbe(string worktree, string filter, string results, string coverageFile, IReadOnlyDictionary environment) - { - var values = new Dictionary { ["project"] = Project, ["filter"] = filter, ["results"] = results, ["cobertura"] = coverageFile }; - if (coverageFile.Length == 0 && Files.Strings(config["test"]).Any(arg => arg.Contains("{cobertura}", StringComparison.Ordinal))) - throw new Refusal("the replay's test template carries the coverage arguments itself; move them to a \"coverage\" array of the replay configuration, so a run without a footprint can leave them out"); - string[] argv = coverageFile.Length > 0 ? [.. Expand(config["test"], values), .. Expand(config["coverage"], values)] : Expand(config["test"], values); - // The template runs the verdict tool, which keeps its full log where BUILD_VERDICT_LOG_DIR says: the - // directory the replay offers as PROBE_LOG_DIR, whose files it keeps for a failed run. - if (environment.TryGetValue("PROBE_LOG_DIR", out var logDir) && logDir is { Length: > 0 }) - environment = new Dictionary(environment, StringComparer.Ordinal) { ["BUILD_VERDICT_LOG_DIR"] = logDir }; - return Start(worktree, argv, environment); - } - - public IReadOnlyDictionary Environment(string cacheDirectory) - { - var result = new Dictionary(StringComparer.Ordinal); - if (Files.Str(config["coverageTargets"]) is { Length: > 0 } targets) - { - var path = Path.Combine(cacheDirectory, "coverage.targets"); - if (!File.Exists(path) || SharedFile.ReadAllText(path) != targets) Files.WriteAtomically(path, targets); - result["CustomAfterMicrosoftCommonTargets"] = path; - } - return result; - } - - public (JsonObject? Axes, string? Error) DeriveAxes(string checkout, string workDirectory) - { - if (config["enumerate"] is not JsonObject enumerate) return (null, "no enumerator configured"); - // The enumerator pins its packages in #:package directives; each version comes from the repository's - // central package versions, so the axes follow a dependency update without anyone editing the script. - var script = File.ReadAllText(Path.Combine(checkout, Files.Str(enumerate["script"]))); - var props = Path.Combine(checkout, "Directory.Packages.props"); - var versions = File.Exists(props) - ? Regex.Matches(File.ReadAllText(props), " m.Groups[1].Value, m => m.Groups[2].Value, StringComparer.Ordinal) - : new Dictionary(StringComparer.Ordinal); - script = Regex.Replace(script, @"^#:package ([^@\s]+)@(\S+)", m => $"#:package {m.Groups[1].Value}@{(versions.TryGetValue(m.Groups[1].Value, out var v) ? v : m.Groups[2].Value)}", RegexOptions.Multiline); - Directory.CreateDirectory(workDirectory); - var file = Path.Combine(workDirectory, Path.GetFileName(Files.Str(enumerate["script"]))); - Files.WriteAtomically(file, script); - var ran = Proc.Run("dotnet", ["run", file], workDirectory); - if (ran.ExitCode != 0) return (null, "the enumerator failed: " + ran.Stderr.Trim().Split('\n')[^1]); - var json = ran.Stdout[ran.Stdout.IndexOf('{')..]; - return (JsonNode.Parse(json) as JsonObject, null); - } -} diff --git a/tools/Prober/ProbeRun.cs b/tools/Prober/ProbeRun.cs deleted file mode 100644 index 484fc507..00000000 --- a/tools/Prober/ProbeRun.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System.Text.Json.Nodes; - -namespace Prober; - -/// -/// What replay, seeds and try share to run a probe: the worktree moved to HEAD, the -/// environment every build and run gets, one run of a probe in one process, and how each row of it ended, read -/// from the jsonl the harness writes under PROBE_OUT. -/// -internal static class ProbeRun -{ - /// - /// A row's outcome under one run: its category, the first reason given, whether any reason of the row or of a check - /// of it was a harness error, and its checks. A row that ran no check is one check of the obligation claim. - /// - internal sealed record RowOutcome(string Category, string Reason, bool HarnessError, IReadOnlyList Checks); - - /// - /// One check of a row under one run: its obligation, category and first reason, and whether a harness error broke it - /// or its row outside any check. Only a clean violation, one with no harness error, says the check fired (decision 103). - /// - internal sealed record CheckOutcome(string Obligation, string Category, string Reason, bool HarnessError) - { - public bool CleanViolation => Category == "violation" && !HarnessError; - } - - /// - /// Adds the worktree at ("cold"), or moves it there without force ("warm"): files the - /// commits did not change keep their mtimes, so the build stays incremental. - /// - public static string Ready(Ledger ledger, string worktree, string head) - { - if (!Directory.Exists(worktree)) - { - var added = Git.Run(ledger.Root, "worktree", "add", "--detach", worktree, head); - if (added.ExitCode != 0) throw new Refusal("could not add the worktree: " + added.Stderr.Trim(), 2); - return "cold"; - } - if (Git.Run(worktree, "checkout", "--detach", head).ExitCode != 0) Git.Run(worktree, "reset", "--hard", head); - return "warm"; - } - - /// The process's environment with what the adapter adds for every build and run of the probes. - public static Dictionary Environment(Context ctx, IProbeAdapter adapter, string home) - { - var environment = new Dictionary(ctx.Environment, StringComparer.Ordinal); - foreach (var (k, v) in adapter.Environment(home)) environment[k] = v; - return environment; - } - - /// The environment of one variant: the base, then the variant's own settings, then the extra ones (a seed's toggle). - public static Dictionary Variant(IReadOnlyDictionary environment, JsonObject variant, JsonObject? extra = null) - { - var env = new Dictionary(environment, StringComparer.Ordinal); - foreach (var (k, v) in variant) env[k] = Files.Str(v); - foreach (var (k, v) in extra ?? []) env[k] = Files.Str(v); - return env; - } - - /// - /// Runs the probe the filter selects once, with its rows written to {results}/{name}-rows, and reads how - /// each row ended. empty asks for no coverage. - /// - public static (Ran Ran, Dictionary Rows) Once(IProbeAdapter adapter, string worktree, string filter, string results, string name, - string coverageFile, Dictionary env) - { - var outDir = Path.Combine(results, name + "-rows"); - env["PROBE_OUT"] = outDir; - var ran = adapter.RunProbe(worktree, filter, results, coverageFile, env); - return (ran, Outcomes(outDir)); - } - - /// How each row ended, from every jsonl under ; a line without a category is a violation. - public static Dictionary Outcomes(string outDir) - { - var rows = new Dictionary(); - if (!Directory.Exists(outDir)) return rows; - foreach (var file in Directory.EnumerateFiles(outDir, "*.jsonl")) - foreach (var line in Files.ReadLines(file)) - { - var category = Files.Str(line["category"], "violation"); - var reasons = Files.Strings(line["reasons"]).ToList(); - var reason = reasons.FirstOrDefault(r => r.StartsWith(category + ":", StringComparison.Ordinal)) ?? Files.Str(line["note"]); - static bool Broken(IEnumerable reasons) => reasons.Any(r => r.StartsWith("harness-error:", StringComparison.Ordinal)); - var checks = new List(); - if (line["checks"] is JsonArray { Count: > 0 } ran) - { - // The row's own harness error (outside any check) breaks every check of it; a check's breaks that check. - var rowBroken = Broken(reasons); - foreach (var c in ran.OfType()) - { - var cc = Files.Str(c["category"], "violation"); - var cr = Files.Strings(c["reasons"]).ToList(); - checks.Add(new CheckOutcome(Files.Str(c["obligation"]), cc, cr.FirstOrDefault(r => r.StartsWith(cc + ":", StringComparison.Ordinal)) ?? Files.Str(c["notes"]), - rowBroken || cc == "harness-error" || Broken(cr))); - } - } - else checks.Add(new CheckOutcome(Claim.ImplicitObligation, category, reason, category == "harness-error" || Broken(reasons))); - var harnessError = Broken(reasons) || checks.Any(c => c.HarnessError); - rows[(int)Files.Num(line["idx"])] = new RowOutcome(category, reason, harnessError, checks); - } - return rows; - } - - /// The first line of a run's output (the verdict's), or its exit code where it printed nothing. - public static string FirstLine(Ran ran) => ran.Stdout.Split('\n')[0].Trim() is { Length: > 0 } line ? line : $"exit {ran.ExitCode}"; -} diff --git a/tools/Prober/Prober.csproj b/tools/Prober/Prober.csproj deleted file mode 100644 index ba12505a..00000000 --- a/tools/Prober/Prober.csproj +++ /dev/null @@ -1,37 +0,0 @@ - - - - - Exe - prober - Prober - - false - $(ToolSourcePatterns);Schemas/*.json - - - - - - - - - - - - diff --git a/tools/Prober/Program.cs b/tools/Prober/Program.cs deleted file mode 100644 index 7b62b3ae..00000000 --- a/tools/Prober/Program.cs +++ /dev/null @@ -1,135 +0,0 @@ -namespace Prober; - -internal static class Program -{ - private const string Usage = - """ - prober: the probing workflow's core. No command starts a model; only `round` does, locally. - - Ledger (any checkout of the repository; --branch B, default the checked-out branch): - universe [--base REF] each claim's axes, from the claims, the axes file and the generated axes - rows --claim C [--full] a pairwise covering array over the claim's pairs not yet covered; - inside a capped round it refuses past the deadline or the claim cap - rows --probe P [--full] writes P's rows file whole into the staging: a covering array over its - claim's pairs less those the claim's other probes have credited - status [--json] coverage from current evidence, open findings, done or not; per claim - its obligations and which have a seed - gaps [--base REF] what the corpus leaves out, each with its reason: unassigned settings, - refused shapes and values, what claims do not promise, out of reach, and - the wanted pairs no passing check of the last replay read both axes of - gate [--since REF] [--json] the release gate (needs only the tracked corpus and a replay) - stale [--json] [--include-retired] the probes a replay would run - identity --probe P what a result of P is bound to - record-run --probe P --result pass|fail [--evidence F] [--seconds S] - footprint --probe P --cobertura F... --root WT [--include src] [--compile-time PATH...] - register --probe P --claim C --filter F [--variants JSON] [--seed PATCH] [--seed-env NAME=VALUE...] [--defect TEXT] - [--obligation O]: the seed (a patch, a toggle or both) into `seed`, or with - --obligation into `seeds`, in place of a seed naming O; refuses an O that - claim C does not list - publish | dismiss --id F --reason TEXT | mark retired --probe P - fix F --commit SHA [--reason TEXT] records open finding F fixed at the commit (findings.jsonl); refuses - an id this ledger does not hold open and a commit the repository lacks - lock --holder H --mode replay|explore|full [--pid N] | unlock --holder H | heartbeat --holder H - round-start --holder H --minutes M [--claims N] | round-end --holder H | repair | abandon - merge --from DIR... joins the states of a replay split by family (one DIR per leg) into - this one; refuses while a registered probe was in no leg - - Writing a probe (into the branch's staging): - new --claim C [--probe P] [--family F] stages P's skeleton (the claim's source and obligations quoted, one - check per obligation that throws until written), the claim's rows and P's - registry entry with no seed yet; P defaults to the claim's prefix and words - (C20-testing-shouldhold: C20TestingShouldholdProbe); refuses a claim not in - the claims, a name taken, a family that is not the claim's - try --probe P [--rows i,j,...] [--no-build] builds once in the replay's worktree and runs P alone, every - variant or the rows given, printing how each row ended; records nothing, - holds the worktree from a replay, exits with the probe's exit code - - Running probes: - replay [--all] [--family F] [--parallel N] [--no-footprint] [--holder H] [--dry-run] [--where] - replays the corpus with no model: the stale probes, or all; one family - of the replay configuration; N probe processes at a time (default: - the cores); without coverage or footprint, for a state nobody keeps - round explore|full [--minutes M] [--claims N] [--usd X] a capped exploration round (local only) - seeds [--all] [--probe P] [--dry-run] applies each probe's seeded defect in a scratch worktree, replays the - probe there and requires a violation it causes (local only, a build per - seed): the probes whose identity or footprint changed since every seed of - theirs was caught, or all of them, as a release runs it - - State goes under PROBE_STATE_DIR when set (CI), else the main checkout's prober memory. - Exit codes: 0 done, 1 refused or failed, 2 usage, 3 this dll is stale; `try` exits with the probe's own. - """; - - private static readonly HashSet Flags = new(StringComparer.Ordinal) - { "json", "full", "all", "include-retired", "dry-run", "where", "no-footprint", "no-build" }; - - private static int Main(string[] args) - { - Console.OutputEncoding = new System.Text.UTF8Encoding(false); - if (args.Length == 0 || args[0] is "help" or "-h" or "--help") { Console.Write(Usage); return args.Length == 0 ? 2 : 0; } - var home = FindRoot(AppContext.BaseDirectory); - if (home is not null && SourceFingerprint.Difference(home) is { } stale) - { - Console.Error.WriteLine($"prober: this dll was built from other sources than the checkout holds ({stale}). Run `{SourceFingerprint.RebuildCommand}`."); - return SourceFingerprint.StaleExitCode; - } - return Run(Context.FromProcess(), args); - } - - /// Runs one command against a context: the process's, or a test's. - public static int Run(Context ctx, string[] args) - { - try - { - var command = args[0]; - var a = new Args(args[1..], Flags); - if (command == "replay") return Replay.Run(ctx, a); - if (command == "round") return Round.Run(ctx, a); - if (command == "seeds") return Seeds.Run(ctx, a); - if (command == "try") return TryProbe.Run(ctx, a); - var ledger = new Ledger(ctx, a.Get("branch")); - // Commands run under a round keep its lock alive. - if (command is not ("lock" or "unlock" or "abandon" or "heartbeat") && ctx.Env("PROBE_HOLDER") is { } holder) - RoundLock.Beat(ledger, holder); - return command switch - { - "universe" => Commands.UniverseCommand(ledger, a), - "rows" => Commands.Rows(ledger, a), - "status" => Commands.Status(ledger, a), - "gate" => Commands.Gate(ledger, a), - "stale" => Commands.Stale(ledger, a), - "identity" => Commands.IdentityCommand(ledger, a), - "record-run" => Commands.RecordRun(ledger, a), - "footprint" => Commands.Footprint(ledger, a), - "new" => NewProbe.Run(ledger, a), - "register" => Commands.Register(ledger, a), - "publish" => Commands.Publish(ledger, a), - "dismiss" => Commands.Dismiss(ledger, a), - "fix" => Commands.Fix(ledger, a), - "mark" => Commands.Mark(ledger, a), - "lock" => Commands.Lock(ledger, a), - "unlock" => Commands.Unlock(ledger, a), - "heartbeat" => Commands.HeartbeatCommand(ledger, a), - "owe-replay" => Commands.OweReplay(ledger, a), - "round-start" => Commands.RoundStart(ledger, a), - "round-end" => Commands.RoundEnd(ledger, a), - "repair" => Commands.Repair(ledger, a), - "abandon" => Commands.Abandon(ledger, a), - "merge" => Commands.Merge(ledger, a), - "gaps" => Commands.Gaps(ledger, a), - _ => throw new Refusal($"unknown command {command}; see `prober help`", 2), - }; - } - catch (Refusal refusal) - { - ctx.Error.WriteLine("prober: " + refusal.Message); - return refusal.ExitCode; - } - } - - private static string? FindRoot(string start) - { - for (var dir = new DirectoryInfo(start); dir is not null; dir = dir.Parent) - if (File.Exists(Path.Combine(dir.FullName, "CodoMetis.TypeKit.slnx"))) return dir.FullName; - return null; - } -} diff --git a/tools/Prober/README.md b/tools/Prober/README.md deleted file mode 100644 index 3abf10f2..00000000 --- a/tools/Prober/README.md +++ /dev/null @@ -1,181 +0,0 @@ -# prober - -The probing workflow's core: the ledger of what the documents claim and what the probes have shown, the -replay of the probe corpus, the release gate, and the capped exploration round. Repository tooling, not -a package. It references the base class library only and is built with the solution. - -```text -dotnet tools/Prober/bin/Release/net10.0/prober.dll help -``` - -## What runs where - -| Command | Model | Where | -|---|---|---| -| `replay [--all] [--family F] [--parallel N] [--no-footprint]` | none | anywhere, CI included: the probes whose identity or footprint changed, or all of them; one family of the replay configuration; N probe processes at a time (the cores by default); without coverage or a footprint, for a state nobody keeps (decision 101) | -| `merge --from DIR...` | none | anywhere, CI included: joins the states of a replay split by family, and refuses while a registered probe was in no leg | -| `gate [--since REF]` | none | anywhere, CI included: needs only the tracked corpus and a replay | -| `status`, `stale`, `universe`, `rows [--claim C \| --probe P]`, … | none | anywhere | -| `gaps [--base REF]` | none | anywhere: what the corpus leaves out, each with its recorded reason, and, from the last replay's evidence, each wanted pair no obligation wanting it has a passing check that read both its axes of (more rows cannot cover it); a report that decides nothing (decision 104) | -| `fix F --commit SHA [--reason TEXT]` | none | anywhere: records an open finding fixed at a commit, the fixed line of the state's `findings.jsonl` as `finding-line.schema.json` wants it; it counts once the commit is in HEAD's history | -| `new --claim C [--probe P] [--family F]` | none | anywhere: stages a probe's skeleton, its own rows and its registry entry with no seed yet, and prints what the writer still owes | -| `try --probe P [--rows i,j,...] [--no-build]` | none | locally: builds once in the replay's worktree and runs that one probe, every row or the rows given; records nothing, and holds the worktree from a replay while it runs | -| `round explore\|full` | the `prober` agent | locally only, under caps it cannot overrun | -| `seeds [--all] [--probe P]` | none | locally only: one build per seed, in a scratch worktree (decision 100); the probes whose seeds identity or product footprint changed since every seed was caught, or all of them, as a release runs it (decision 107) | - -No command in the gate's path needs a login, an API key or a Claude executable. A gate obligation is met -by a probe in the corpus, written by hand or by a round, or by a reviewed waiver in -`tools/probe/corpus/waivers.json`. - -## Files - -The corpus is tracked under `tools/probe/corpus/`: claims, the probe registry, harness and probe sources, -rows (gzipped), waivers and dismissed findings. State goes under `PROBE_STATE_DIR` when set (CI sets it), -else the main checkout's `.claude/agent-memory-local/prober/`. Every file's shape is in `Schemas/`, versioned; -a file or line without `schemaVersion` is version 1. `claims.json` is checked against its schema wherever it is -read, and a field the schema does not have is refused with the claim and the field named; each `exclude` entry -is a reason and its pairs, which `universe` prints beside the claim's narrowings. The JSON files the prober -writes keep `+`, backticks and `<` literal, so a `publish` that changes nothing in a tracked file leaves it as -it is (decision 102). The harness's generated declarations, `harness/*.g.cs`, are written by the file-based -apps of `tools/probe/generators/` from the rows and the axes file, and `GeneratedHarnessTests` in the -conventions tests holds each to its app's output (that directory's README). - -Rows are per probe: `rows/.json.gz`, one file for each registered probe, its claim inside (`claim`), so -two probes of one claim (C07's `BaseProbe` and `C07BlanketMappingProbe`) each run rows of their own, and a -probe's identity reads its own file only: a sibling's rows changing leaves it as it was. The probe's -`Rows.Data("P")` names the file, and the replay, `try` and the identity read it. `rows --probe P` writes P's file -whole into the staging, never appended: a covering array over its claim's pairs less those the claim's other -probes have credited (P's own credit is not subtracted, since it came from the rows the file replaces), and -`publish` moves it into the corpus by probe; `rows --claim C` only prints such an array. The stored rows are -definitions, the evidence is in the state. `ProbeRowsTests` in the conventions tests refuses two rows of one -file with the same axis map, a file whose `claim` is not in `claims.json` or is not its probe's, a file that -names no registered probe, and a registered probe without a file. - -A registry entry may list `watched`: paths, files or directories, that the probe's result depends on and its -footprint cannot see, since the footprint is what ran in the probe's own process under `src/` beside the -`compileTimeFootprint` paths. A variant catalogue under `test/` compiled into the probes is one (C19 builds -over the analyzer tests' `TypeArgumentSites` and `TypeArgumentKinds`), a build input a child process reads is -another (C18's consumer build compiles against the base package and evaluates `src/Directory.Build.*`). The -replay records each with its object id in the footprint line, a change to one stales the footprint like a path -the probe ran, and the gate counts it as touching the probe's claim. A probe whose own process runs nothing under `src/` -(C22 builds its consumer in a child process) is recorded by what its entry watches alone; where nothing ran and nothing -is watched, `footprint` refuses to record an empty footprint, which would never go stale. - -The round lock, `round.lock` and its mutex, sits in the branch's directory (`branches//`), beside -everything a round or a replay writes, so rounds and replays of two branches run at once and two of one branch -never do. The scratch worktrees, `wt` (the replay's), `seeds`, `explore` (the `prober` agent's) and `enumerate`, -are per checkout: under `PROBE_CACHE_DIR` when set, else `~/.cache/probe-replay/-`, -and `replay --where` prints them. So a linked worktree (`git worktree add`, as each session under -`.claude/worktrees/` is) builds a warm copy of its own, about a gigabyte of disk, and two sessions never reset -each other's copy; the main checkout keeps the directory it had before. Since the lock is per branch and the -worktree per checkout, a replay also holds `replay.lock` beside them (an OS lock, gone with its process), and a -replay of another branch in the same checkout, after a branch switch, is skipped while one runs. Removing a -linked worktree leaves its cache behind: before you remove it, run there the `remove:` line that `replay --where` -prints, which removes the cache's worktrees (`git worktree remove`) and then its directory, every path written out. - -A claim lists what it promises in `obligations`, each an id and a sentence quoted verbatim from the claim's -source, and what it does not promise in `not-promised`, each an id and the sentence of the document or record -that makes the difference intended, quoted from that source. `universe` checks every quote as it checks a -narrowing's and refuses a sentence its document does not hold, or an id listed twice. A claim without -`obligations` has one, the claim itself, under the id `claim`. `status` shows per claim its obligations and the -ones no seed names, and `gaps` prints every not-promised entry and narrowing beside the settings no surface -reaches, the refused shapes and values, and the axes file's `out-of-reach` surfaces (decision 104). - -An obligation may declare where it does not apply: `inapplicable`, each entry the claim's own axis values -(`axes`) and a citation (`cite`: `not-promised:id` of the claim's list, `docs:file#anchor`, `fact:KB-NNN` or -`record:NNNN`). A row holding every axis of a declaration at one of its values runs no check of that -obligation: `Row.Check` records the check inapplicable with the cite before its body runs, for that check only. -The replay stages the claims beside the rows (`claims.json` in the probe directory) for the harness's -`Rows.Claim`, and a probe's identity takes its claim's declarations. A pair a declaration matches is not wanted -by that obligation (`Claim.PairsWantedBy`); a pair no obligation wants is declared inapplicable: `universe` -prints each claim's count with the citations, `gaps` lists them under a heading of their own, `status --json` -gives `inapplicable_pairs` per claim, the covering generator never puts one in a row, and the gate leaves them -out as it leaves out an exclusion. A claim's pairs stay its pairs. `universe` refuses a declaration whose axis -or value the claim does not list, whose cite is malformed, or whose not-promised id the claim lacks. - -A pair of a claim's axes counts as covered only by evidence a replay recorded: a check that ended as a pass, in a -row that was expected and executed, for the settings that actually applied, while the result's identity holds -(the probe, every compiled corpus source, the rows, the axes file, the build environment, this tool's -version and the schema version) and the product files the probe ran are unchanged. A row's checks -(`Row.Check` in the harness) each name an obligation and a baseline and record the axes they read; a check is -credited with those and the row's own (read outside any check), and a pair is never put together from two -checks' axes. A row without checks is one check of the obligation `claim` with the row's category. Evidence is -version 2 (`Schemas/evidence.schema.json`): the replay copies each row's `checks` beside its `applied` and -`reasons`. `status --json` gives each obligation `current`, the pairs current checks naming it earned, and -`passed`; the text line counts rows and checks by outcome and ends with the obligations no passing check named -at the last replay (`K uncredited`). - -## Seeds - -Each probe's registry entry names a seeded product defect: a patch under `seeds/` against the product -files (the replay's `include`), an environment toggle, or both, and the sentence `defect`. Where its claim -lists obligations, the entry's `seeds` holds one such seed per obligation the probe checks, each naming it in -`obligation`, and every listed obligation of a claim with probes has a seed in one of them (decision 104). -`seeds` applies each seed on its own in the `seeds` worktree beside the replay's, builds, runs the probe -and counts the seed caught only for a check that ends as a violation, with no harness error in it or in its -row outside its checks (decision 103), in a row whose unseeded run had no violation of the same obligation; a -seed naming an obligation is caught only by a check naming it, so a probe whose rows run no checks (each the -one check `claim`) catches only seeds that name none. The unseeded rows come from the ledger where the probe's -evidence is current, else from an unseeded run first. Nothing it runs is evidence; it -writes `seeds-last.json` (`Schemas/seeds-run.schema.json`), one outcome per seed, reported as -`P/` where the seed names one. `register --obligation O --seed PATCH --defect TEXT` (or -`--seed-env NAME=VALUE...` for a toggle) writes a seed into `seeds`, in place of any seed of the entry that -names O, `seed` included, and refuses an O that the claim's entry in `claims.json` does not list; without -`--obligation`, `--seed` and `--seed-env` set the one `seed`. No seed is written into the registry by hand. -Without `--all`, `seeds` runs only the probes that changed since the run that caught every one of their seeds -(design/adr/0107), as `replay` runs only the stale ones: a probe runs when its seeds identity (the probe's identity, -its seeds and the content of their patches) differs from the one recorded, when the product footprint its last -replay recorded or a file its seeds patch changed since the recorded commit, when no footprint is recorded, and when -no run caught all its seeds. It prints each probe it runs or skips with the reason. `seeds-last.json` keeps, per probe, -the identity and the commit of the run that caught them all, carried forward for the probes a run skips; a missed seed -drops the probe's record, so it is never skipped next time. `--all` runs every probe whatever the record says, and a -release runs it before the tag; `--probe P` runs P. -A seed is not part of a probe's identity, neither `seed` nor `seeds`. Without a build, -`Seeds.Problems`, which the prober tests run over the repository, refuses a probe without a seed, a seed that -does not apply, a seed naming an obligation its claim does not list, and a listed obligation no seed names. - -## Writing a probe - -`new --claim C` starts one in the branch's staging, whose path it prints. It writes `probes/P.cs` in the form -the corpus's probes have (`Rows.Data`, `[Theory, MemberData(nameof(Data))]`, `Rec.Run`), headed by the claim's -source and each obligation's id and sentence, with one check per obligation that throws until it is written: -the probe builds beside the others, and every row of it ends as a harness error, which fails the row, earns -nothing and is never a seed's catch. It stages the probe's own rows, a covering array over its claim's pairs the -claim's other probes have not covered, as `rows --probe P` writes them, and the registry entry with an empty -`seeds`, so `seeds` and `Seeds.Problems` name the seed each obligation owes. P defaults to the claim id's -prefix and words (`C20-testing-shouldhold` is `C20TestingShouldholdProbe`); it refuses a claim that is not in -the claims, a name a probe has, and a claim whose first surface is in no family, or not in `--family`'s. - -Then fill each check and run `try --probe P` until each row ends as it should. It moves the replay's worktree -to HEAD, overlays the corpus and the staging as the replay does, builds once through the adapter (not with -`--no-build`), and runs P alone in each of its registry variants, or only the rows `--rows` names, through the -harness's `PROBE_ROWS`, within the rows each variant's `PROBE_ONLY` selects. It prints each run's verdict line -and how each row ended, and exits with the probe's code (1 when the build fails). It records no evidence, -footprint or run and takes no round lock, but holds the replay's worktree (`replay.lock`) while it runs, so a try -refuses while a replay of the checkout runs, and a replay skips while a try does. Then write a seed per -obligation and register it with the command `new` printed for that obligation (`register --obligation O`, -under "Seeds"), show each caught with `seeds --probe P`, and `publish`. `register` lints the staged sources -first (below). A writer needs no build or sync script of their own. - -## Register and publish - -`register` and `publish` read the staged harness and probe sources first and refuse, by file and line, a -`Note` whose first argument is not a `NoteKeys` member the corpus declares, a harness `Skip` whose finding -is not a string literal naming a finding this ledger holds open (decision 103), and a check's `Inapplicable` or -`Refused` whose first argument is not a `Cite` factory with a string literal, or names what does not exist: a -not-promised entry the probe's claim does not list (any claim's, for a harness source no one probe owns), a docs -file, a knowledge entry under `design/knowledge/`, a record under `design/adr/`. `ProbeCiteTests` in the -conventions tests holds the same over the tracked corpus. The scan blanks out comments -and string literals and reads text, since this tool references the base class library only. - -## The adapter - -Building the probes, running one probe in one process, and deriving the generated axes go through -`IProbeAdapter` (`ProbeAdapter.cs`), configured by the `replay` object of `.claude/probe-axes.md`. The .NET -adapter is the only one; the core holds nothing of .NET's, so another stack can plug in its own. - -## Freshness - -The dll hashes its own sources, the schemas among them, at build time and checks them at startup, as every -tool does (`tools/Directory.Build.targets`): a changed, added or removed source makes it refuse with exit 3 -and name the command that rebuilds it, `dotnet build tools/Prober -c Release`. Content hashes, never times. diff --git a/tools/Prober/ReleaseGate.cs b/tools/Prober/ReleaseGate.cs deleted file mode 100644 index 42caea16..00000000 --- a/tools/Prober/ReleaseGate.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Nodes; - -namespace Prober; - -/// -/// The release gate. It needs nothing but the tracked corpus and a replay: no model, no login, no local -/// memory of a developer's machine. It refuses while the staging is unpublished, a finding is open or a row -/// skips for one, a probe has not passed at its current identity and footprint, an expected row did not run, or a claim touched -/// since the release has pairs no probe covers and no reviewed waiver. Touched is chosen conservatively: the -/// claim's source document, its surfaces' source areas, the paths its probes watch or the build inputs changed; footprints only add. -/// -internal static class ReleaseGate -{ - private static readonly string[] BuildInputs = ["Directory.Packages.props", "Directory.Build.props", "global.json"]; - - public static int Run(Ledger l, string? sinceArg, bool json) - { - var since = sinceArg ?? Git.Out(l.Top, "describe", "--tags", "--abbrev=0"); - var (universe, _) = Axes.Build(l, since); - var footprints = l.Footprints(); - var reasons = new List(); - if (Directory.Exists(l.StagingDir) && Directory.EnumerateFiles(l.StagingDir, "*", SearchOption.AllDirectories).Any()) - reasons.Add("the staging corpus is not published (run `publish` and commit tools/probe/corpus)"); - var open = l.OpenFindings(); - if (open.Count > 0) reasons.Add($"open findings: {string.Join(", ", open)}"); - - var runs = Files.ReadLines(l.P("runs.jsonl")).GroupBy(r => Files.Str(r["probe"]), StringComparer.Ordinal).ToDictionary(g => g.Key, g => g.Last(), StringComparer.Ordinal); - foreach (var s in l.ProbeStates(includeRetired: true)) - { - if (s.Stale) { reasons.Add($"{s.Probe}: not replayed at its current identity and footprint (run `prober replay --all`)"); continue; } - if (Files.Str(runs.GetValueOrDefault(s.Probe)?["result"]) != "pass") { reasons.Add($"{s.Probe}: its last replay failed"); continue; } - var evidence = Files.ReadJson(l.EvidencePath(s.Probe)) as JsonObject; - var expected = new HashSet(); var executed = new HashSet(); - foreach (var v in (evidence?["variants"] as JsonArray ?? []).OfType()) - { - foreach (var i in v["expected"] as JsonArray ?? []) expected.Add((int)Files.Num(i)); - foreach (var i in v["executed"] as JsonArray ?? []) executed.Add((int)Files.Num(i)); - } - expected.ExceptWith(executed); - if (expected.Count > 0) reasons.Add($"{s.Probe}: {expected.Count} expected row(s) did not run"); - // A skip is an open finding the corpus itself records, so it binds where the ledger has none (CI). - foreach (var (finding, rows) in Ledger.FindingSkips(evidence)) - reasons.Add($"{s.Probe}: {rows} row(s) skip for finding {finding}; fix it and remove the skip, or dismiss it"); - } - - var done = l.Covered(universe); - var byClaim = l.Registry().GroupBy(kv => Files.Str(kv.Value["claim"]), StringComparer.Ordinal).ToDictionary(g => g.Key, g => g.Select(kv => kv.Key).ToList(), StringComparer.Ordinal); - var changed = Git.Out(l.Top, "diff", "--name-only", since, "HEAD").Split('\n', StringSplitOptions.RemoveEmptyEntries); - bool Under(IEnumerable paths) => paths.Any(w => changed.Any(f => f == w || f.StartsWith(w.TrimEnd('/') + "/", StringComparison.Ordinal))); - - // A reviewed waiver meets an obligation without a probe; one without a reason or a reviewer meets nothing. - var waivers = (Files.ReadJson(l.CorpusPath("waivers.json")) as JsonArray ?? []).OfType() - .Where(w => Files.Str(w["claim"]).Length > 0 && Files.Str(w["reason"]).Length > 0 && Files.Str(w["reviewed_by"]).Length > 0) - .Select(w => Files.Str(w["claim"])).ToHashSet(StringComparer.Ordinal); - - var required = new List(); var waived = new List(); var missedByFootprint = new List(); var footprintOnly = new List(); - var missingTotal = 0; var inapplicableTotal = 0; - foreach (var claim in universe.Claims) - { - var probes = byClaim.GetValueOrDefault(claim.Id) ?? []; - // What the claim's surfaces watch, and what its probes' registry entries watch beyond their footprints. - var conservative = Under([claim.Source.Split('#')[0]]) || Under(claim.Watched) || Under(probes.SelectMany(l.Watched)) || Under(BuildInputs); - var footprint = probes.Count > 0 && probes.Any(p => l.FootprintChangedSince(p, since, footprints)); - if (probes.Count > 0 && conservative && !footprint) missedByFootprint.Add(claim.Id); - if (footprint && !conservative) footprintOnly.Add(claim.Id); - if (!(conservative || footprint)) continue; - if (waivers.Contains(claim.Id)) { waived.Add(claim.Id); continue; } - // The pairs some obligation wants; one no obligation wants is declared inapplicable and left out, as an exclusion is. - var pairs = claim.PairsWanted(); - inapplicableTotal += claim.Pairs().Count - pairs.Count; - var missing = pairs.Count(p => !done.Contains((claim.Id, p))); - required.Add(claim.Id); missingTotal += missing; - if (missing > 0) reasons.Add($"{claim.Id}: {missing} of {pairs.Count} pairs uncovered (touched since {since})"); - } - - if (json) - { - l.Ctx.Out.WriteLine(new JsonObject - { - ["since"] = since, ["touched"] = new JsonArray([.. required.Select(x => (JsonNode)x)]), ["uncovered_pairs"] = missingTotal, ["inapplicable_pairs"] = inapplicableTotal, - ["reasons"] = new JsonArray([.. reasons.Select(x => (JsonNode)x)]), - ["selected_by_footprint_only"] = new JsonArray([.. footprintOnly.Select(x => (JsonNode)x)]), - ["waived"] = new JsonArray([.. waived.Select(x => (JsonNode)x)]), - ["missed_by_footprint_alone"] = new JsonArray([.. missedByFootprint.Select(x => (JsonNode)x)]), ["passes"] = reasons.Count == 0, - }.ToJsonString(new JsonSerializerOptions { WriteIndented = true })); - return reasons.Count == 0 ? 0 : 1; - } - if (reasons.Count > 0) - { - l.Ctx.Error.WriteLine($"probe gate: refused for {l.Branch} since {since} ({required.Count} claim(s) touched, {missingTotal} pair(s) uncovered):"); - foreach (var r in reasons) l.Ctx.Error.WriteLine($" {r}"); - return 1; - } - l.Ctx.Out.WriteLine($"probe gate: {l.Branch} passes since {since}: {required.Count} claim(s) touched and fully covered" + - (waived.Count > 0 ? $", {waived.Count} waived ({string.Join(", ", waived)})" : "") + - ", every probe replayed and passing, no open finding."); - return 0; - } -} diff --git a/tools/Prober/Replay.cs b/tools/Prober/Replay.cs deleted file mode 100644 index eba97a0f..00000000 --- a/tools/Prober/Replay.cs +++ /dev/null @@ -1,440 +0,0 @@ -using System.Diagnostics; -using System.Globalization; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json.Nodes; - -namespace Prober; - -/// -/// Replays the written probes with no model in the loop. Per commit it runs only the probes whose identity or -/// footprint changed; --all runs every probe, retired ones included (the release run). Each probe runs -/// once per process variant, and the replay records each probe in registry order whatever order the runs -/// finished in: per-row evidence (rows expected, rows executed, each row's outcome and applied settings), the -/// result, and the fresh footprint. --no-footprint runs without coverage and records no footprint, for a -/// state nobody keeps (a pull request's); only then do the variants of every selected probe run -/// --parallel N at a time (the machine's cores by default), since the coverage collector rewrites the -/// assemblies on disk and two hosts from one output directory corrupt each other (KB-025). -/// --family F replays the probes whose claim's first surface the -/// replay configuration's families puts under F, so a matrix of jobs can split the corpus and -/// merge join their states; it refuses while any probe's surface is in no family, so a matrix cannot -/// quietly leave a probe out. It works in one warm worktree kept between runs and moved to HEAD without force, -/// so only what changed is rebuilt. A dependency input that changed stales every probe, and the axes are -/// derived again. Only one round runs at a time. -/// -internal static class Replay -{ - /// How this replay was asked to run, recorded in replay-last.json for merge to read. - internal sealed record Options(bool All, string? Family, int Parallel, bool Footprint); - - public static int Run(Context ctx, Args a, IProbeAdapter? adapter = null) - { - var ledger = new Ledger(ctx, a.Get("branch")); - var config = ledger.AxesBlock()["replay"] as JsonObject ?? throw new Refusal("the axes file's block has no \"replay\" configuration"); - adapter ??= new DotNetAdapter(config); - var home = Home(ctx, ledger); - if (a.Flag("where")) - { - ctx.Out.WriteLine($"replay: {Path.Combine(home, "wt")}\nexplore: {Path.Combine(home, "explore")}\nseeds: {Path.Combine(home, "seeds")}"); - ctx.Out.WriteLine($"remove: {RemoveCommand(home)}"); - return 0; - } - - // With a footprint the coverage collector instruments the assemblies on disk, so two hosts from one - // output directory corrupt each other (KB-025): one at a time, whatever --parallel says. - var footprint = !a.Flag("no-footprint"); - var asked = Parallelism(a.Get("parallel")); - var options = new Options(a.Flag("all"), a.Get("family"), footprint ? 1 : asked, footprint); - var states = ledger.ProbeStates(options.All); - var scope = options.Family is null ? states : OfFamily(config, ledger, states, options.Family); - var todo = options.All ? scope : [.. scope.Where(s => s.Replay)]; - var skipped = scope.Where(s => !todo.Contains(s)).Select(s => s.Probe).ToList(); - var head = ledger.Head; - ctx.Out.WriteLine($"prober replay: {ledger.Branch} at {head[..7]}: {todo.Count} of {scope.Count} probes to replay" + - $"{(options.Family is null ? "" : $" (family {options.Family})")}, " + - (footprint ? "one at a time (the coverage collector rewrites the assemblies on disk)" : $"{options.Parallel} at a time, no footprint")); - if (a.Flag("dry-run")) { foreach (var s in todo) ctx.Out.WriteLine($" {s.Probe}"); return 0; } - - Directory.CreateDirectory(home); - var holder = a.Get("holder") ?? $"replay-{System.Environment.ProcessId}"; - // Under a round's lock (--holder) the replay takes nothing: re-locking would tie the round's liveness to - // this shorter-lived process. On its own it locks with its own process as the owner. - if (a.Get("holder") is null && !RoundLock.Take(ledger, holder, "replay", System.Environment.ProcessId).Taken) - { - RoundLock.OweReplay(ledger); - ctx.Out.WriteLine($"prober replay: skipped, the ledger is locked by another round; its unlock will say a replay is owed"); - return 0; - } - try - { - using var beat = new RoundLock.Heartbeat(ledger, holder); - using var held = HoldWorktree(home); - if (held is null) - { - ctx.Out.WriteLine($"prober replay: skipped, another branch's replay in this checkout is using {home}; run `prober replay` when it ends"); - return 0; - } - var axes = Rederive(ledger, adapter, config, Path.Combine(home, "enumerate"), force: options.All); - if (axes is not null) - { - if (axes["error"] is { } error) ctx.Out.WriteLine($" axes: {Files.Str(error)}"); - else - { - ctx.Out.WriteLine($" axes: re-derived ({(axes["added"] as JsonArray)!.Count} added, {(axes["removed"] as JsonArray)!.Count} removed)"); - foreach (var x in Files.Strings(axes["added"])) ctx.Out.WriteLine($" + {x}"); - foreach (var x in Files.Strings(axes["removed"])) ctx.Out.WriteLine($" - {x}"); - } - } - if (todo.Count == 0) - { - Summary(ledger, head, warmth: null, buildSeconds: 0, todo, skipped, failed: [], seconds: 0, axes, options); - return 0; - } - return Probes(ctx, ledger, adapter, config, home, Path.GetFullPath(a.Get("worktree") ?? Path.Combine(home, "wt")), head, todo, skipped, axes, options); - } - finally - { - if (a.Get("holder") is null) RoundLock.Release(ledger, holder); - } - } - - /// How many probe processes run at once: --parallel N, else one per core. - internal static int Parallelism(string? given) - { - if (given is null) return Math.Max(1, System.Environment.ProcessorCount); - if (!int.TryParse(given, NumberStyles.Integer, CultureInfo.InvariantCulture, out var n) || n < 1) throw new Refusal("--parallel takes a whole number of 1 or more", 2); - return n; - } - - /// - /// The probes of one family: those whose claim's first surface the replay configuration's families - /// (name to surfaces) puts under it. Every probe of the registry must have a family, or a matrix of jobs by - /// family would never run the one that has none; so this refuses for the whole registry, not only the family - /// asked for. - /// - internal static List OfFamily(JsonObject config, Ledger ledger, List states, string family) - { - var families = config["families"] as JsonObject ?? throw new Refusal("the replay configuration names no families; add \"families\" (a name to the surfaces it holds) to the axes file's replay block", 2); - if (families[family] is not JsonArray) throw new Refusal($"no family {family}; the replay configuration has {string.Join(", ", families.Select(kv => kv.Key))}", 2); - var bySurface = new Dictionary(StringComparer.Ordinal); - foreach (var (name, surfaces) in families) - foreach (var surface in Files.Strings(surfaces)) - { - if (bySurface.TryGetValue(surface, out var other) && other != name) throw new Refusal($"the surface {surface} is in two families, {other} and {name}", 2); - bySurface[surface] = name; - } - var firstSurface = ledger.Claims().ToDictionary(c => Files.Str(c["id"]), c => Files.Strings(c["surfaces"]).FirstOrDefault() ?? "", StringComparer.Ordinal); - var result = new List(); - foreach (var s in states) - { - var surface = firstSurface.GetValueOrDefault(s.Claim) ?? ""; - if (!bySurface.TryGetValue(surface, out var f)) - throw new Refusal($"{s.Probe}: the first surface of its claim {s.Claim} ({(surface.Length > 0 ? surface : "none")}) is in no family of the replay configuration, so a replay by family would never run it"); - if (f == family) result.Add(s); - } - return result; - } - - /// Derives the generated axes again when a dependency input or the toolchain changed, or always when forced. - private static JsonObject? Rederive(Ledger ledger, IProbeAdapter adapter, JsonObject config, string work, bool force) - { - if (config["enumerate"] is not JsonObject enumerate) return null; - var stampInput = Files.Canonical(new JsonArray( - new JsonArray([.. Files.Strings(enumerate["inputs"]).Select(p => (JsonNode?)(Git.ObjectId(ledger.Top, p) ?? ""))]), - ledger.Toolchain().DeepClone())); - var stamp = Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(stampInput))); - var path = Path.Combine(ledger.Memory, "axes-generated.json"); - var old = Files.ReadJson(path) as JsonObject ?? []; - if (!force && Files.Str(old["inputsStamp"]) == stamp) return null; - var (generated, error) = adapter.DeriveAxes(ledger.Top, work); - if (generated is null) return new JsonObject { ["error"] = error }; - generated["inputsStamp"] = stamp; - static SortedSet Settings(JsonObject o) => - new((o["axes"] as JsonObject ?? []).SelectMany(kv => Files.Strings(kv.Value).Select(v => $"{kv.Key}={v}")).Concat(Files.Strings(o["needsValues"])), StringComparer.Ordinal); - var before = Settings(old); var after = Settings(generated); - Files.WriteJson(path, generated); - return new JsonObject - { - ["added"] = new JsonArray([.. after.Except(before, StringComparer.Ordinal).Select(x => (JsonNode)x)]), - ["removed"] = new JsonArray([.. before.Except(after, StringComparer.Ordinal).Select(x => (JsonNode)x)]), - ["versions"] = generated["versions"]?.DeepClone(), - }; - } - - /// One process variant of a probe, run: what it was asked, what it answered, and where its rows, coverage and logs went. - private sealed record VariantRun(JsonObject Variant, int Index, Ran Ran, double Seconds, string OutDir, string Coverage, string LogDir); - - /// - /// Starts every variant of a probe, each on a thread of its own that waits its turn at the gate, so the - /// variants of all selected probes share the --parallel slots in the order they were started. - /// - private static List> Start(IProbeAdapter adapter, string worktree, string results, IReadOnlyDictionary environment, - Ledger.ProbeState s, SemaphoreSlim gate, bool coverage) - { - var pending = new List>(); - var index = 0; - foreach (var variant in s.Variants.OfType()) - { - var i = index++; - var coverageFile = coverage ? $"{s.Probe}-{i}.cobertura.xml" : ""; - var outDir = Path.Combine(results, $"{s.Probe}-{i}-rows"); - // PROBE_LOG_DIR: where the run may leave its full logs; a failed run's are kept in the state. - var logDir = Path.Combine(results, $"{s.Probe}-{i}-log"); - var env = new Dictionary(environment, StringComparer.Ordinal) { ["PROBE_OUT"] = outDir, ["PROBE_LOG_DIR"] = logDir }; - foreach (var (k, v) in variant) env[k] = Files.Str(v); - pending.Add(Task.Factory.StartNew(() => - { - gate.Wait(); - try - { - var clock = Stopwatch.StartNew(); - var ran = adapter.RunProbe(worktree, s.Filter, results, coverageFile, env); - return new VariantRun(variant, i, ran, clock.Elapsed.TotalSeconds, outDir, coverageFile, logDir); - } - finally { gate.Release(); } - }, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default)); - } - return pending; - } - - private static int Probes(Context ctx, Ledger ledger, IProbeAdapter adapter, JsonObject config, string home, string worktree, string head, - List todo, List skipped, JsonObject? axes, Options options) - { - var clock = Stopwatch.StartNew(); - var warmth = ProbeRun.Ready(ledger, worktree, head); - var probeDir = Overlay(ledger, config, worktree); - var environment = ProbeRun.Environment(ctx, adapter, home); - var results = Directory.CreateTempSubdirectory("prober-replay-").FullName; - - var buildClock = Stopwatch.StartNew(); - var build = adapter.Build(worktree, environment); - var buildSeconds = buildClock.Elapsed.TotalSeconds; - ctx.Out.WriteLine($" build ({warmth}): {buildSeconds.ToString("F1", CultureInfo.InvariantCulture)} s, {FirstLine(build)}"); - if (build.ExitCode != 0) { ctx.Out.WriteLine(build.Stdout); return 2; } - - // Every variant of every probe is started now and runs when a slot frees; the probes are then read, - // reported and recorded one by one in registry order, each once all its variants have ended. - using var gate = new SemaphoreSlim(options.Parallel); - var started = todo.Select(s => Start(adapter, worktree, results, environment, s, gate, options.Footprint)).ToList(); - - var failed = new List(); - foreach (var (s, pending) in todo.Zip(started)) - { - var variants = pending.Select(t => t.GetAwaiter().GetResult()).OrderBy(v => v.Index).ToList(); - var probeRows = Files.ReadRows(Path.Combine(probeDir, "rows", s.Probe + ".json")); - var evidence = new JsonObject { ["variants"] = new JsonArray(), ["rows"] = new JsonObject() }; - var result = "pass"; var seconds = 0.0; var reports = new List(); - foreach (var v in variants) - { - seconds += v.Seconds; - var label = v.Variant.Count > 0 ? " [" + string.Join(", ", v.Variant.Select(kv => $"{kv.Key}={Files.Str(kv.Value)}")) + "]" : ""; - ctx.Out.WriteLine($" {s.Probe}{label}: {FirstLine(v.Ran)} ({v.Seconds.ToString("F1", CultureInfo.InvariantCulture)} s)"); - if (v.Ran.ExitCode != 0) { result = "fail"; ctx.Out.WriteLine(string.Join('\n', v.Ran.Stdout.Split('\n').Skip(1).Take(11))); } - // A failed run's full logs, kept under the state's logs/ for triage: in CI the state is the artifact. - if (v.Ran.ExitCode != 0 && Directory.Exists(v.LogDir)) - foreach (var file in Directory.EnumerateFiles(v.LogDir)) - { - var kept = ledger.P(Path.Combine("logs", $"{s.Probe}-{v.Index}-{Path.GetFileName(file)}")); - Directory.CreateDirectory(Path.GetDirectoryName(kept)!); - File.Copy(file, kept, overwrite: true); - ctx.Out.WriteLine($" log: {kept}"); - } - if (v.Coverage.Length > 0) reports.AddRange(Directory.EnumerateFiles(results, v.Coverage, SearchOption.AllDirectories)); - - // Execution evidence: the rows this variant should run, the rows it did, how each ended and the - // settings that applied. A row missing here, or recorded without its settings, earns nothing. - var only = Files.Str(v.Variant["PROBE_ONLY"]); - var expected = new JsonArray(); var executed = new SortedSet(); - for (var i = 0; i < probeRows.Count; i++) - if (Selected(Files.StringMap(probeRows[i]?["row"] ?? probeRows[i]), only)) expected.Add(i); - if (Directory.Exists(v.OutDir)) - foreach (var file in Directory.EnumerateFiles(v.OutDir, "*.jsonl")) - foreach (var line in Files.ReadLines(file)) - { - var idx = (int)Files.Num(line["idx"]); - executed.Add(idx); - (evidence["rows"] as JsonObject)![idx.ToString(CultureInfo.InvariantCulture)] = EvidenceRow(line); - } - (evidence["variants"] as JsonArray)!.Add(new JsonObject - { - ["env"] = v.Variant.DeepClone(), ["expected"] = expected, ["executed"] = new JsonArray([.. executed.Select(x => (JsonNode)x)]), - }); - var missing = expected.Select(x => (int)Files.Num(x)).Where(x => !executed.Contains(x)).ToList(); - if (missing.Count > 0) ctx.Out.WriteLine($" {missing.Count} expected row(s) did not run: [{string.Join(", ", missing.Take(10))}]"); - } - foreach (var stale in ledger.StaleSkips(evidence)) { result = "fail"; ctx.Out.WriteLine($" {stale}"); } - if (result == "fail") failed.Add(s.Probe); - var evidencePath = Path.Combine(results, $"{s.Probe}-evidence.json"); - Files.WriteJson(evidencePath, evidence); - var categories = (evidence["rows"] as JsonObject)!.GroupBy(kv => Files.Str(kv.Value!["category"]), StringComparer.Ordinal) - .OrderBy(g => g.Key, StringComparer.Ordinal).Select(g => $"{g.Key} {g.Count()}"); - // Checks beside rows, once a row of the probe ran any: until then every row is its one check and the line says nothing more. - var checks = (evidence["rows"] as JsonObject)!.Any(kv => kv.Value?["checks"] is JsonArray { Count: > 0 }) - ? "; checks: " + string.Join(", ", Ledger.CheckOutcomes(evidence).Select(kv => $"{kv.Key} {kv.Value}")) : ""; - ctx.Out.WriteLine(" outcomes: " + string.Join(", ", categories) + checks); - var record = new StringWriter(); - Program.Run(new Context(ledger.Top, ctx.Environment, record, ctx.Error), - ["record-run", "--branch", ledger.Branch, "--probe", s.Probe, "--result", result, - "--seconds", seconds.ToString("F1", CultureInfo.InvariantCulture), "--evidence", evidencePath]); - if (record.ToString().Trim() is { Length: > 0 } said) ctx.Out.WriteLine(" " + said); - // The footprint is what ran, pass or fail: a probe failing for an open finding is skipped like any - // other until its footprint changes (a fix touches it). Without one the probe stays stale, which is - // right for a state nobody keeps. - if (!options.Footprint) ctx.Out.WriteLine(" footprint: not recorded (--no-footprint)"); - else if (reports.Count > 0) - { - var fp = new StringWriter(); var fpError = new StringWriter(); - Program.Run(new Context(ledger.Top, ctx.Environment, fp, fpError), - ["footprint", "--branch", ledger.Branch, "--probe", s.Probe, "--root", worktree, "--cobertura", .. reports, - "--include", .. (Files.Strings(config["include"]).ToArray() is { Length: > 0 } inc ? inc : ["src"]), - "--compile-time", .. Files.Strings(config["compileTimeFootprint"])]); - ctx.Out.WriteLine($" footprint: {(fp.ToString() + fpError.ToString()).Trim()}"); - } - else ctx.Error.WriteLine(" footprint: no coverage report found; the probe stays stale"); - } - - Summary(ledger, head, warmth, buildSeconds, todo, skipped, failed, clock.Elapsed.TotalSeconds, axes, options); - ctx.Out.WriteLine($"prober replay: {todo.Count - failed.Count} passed, {failed.Count} failed, {clock.Elapsed.TotalSeconds.ToString("F1", CultureInfo.InvariantCulture)} s in all"); - try { Directory.Delete(results, recursive: true); } catch (IOException) { } - return failed.Count > 0 ? 1 : 0; - } - - /// - /// replay-last.json: what this replay did, and how it was asked to run. selected and skipped - /// together are the probes in its scope (its family, or all), so merge can tell a probe no leg saw. - /// - private static void Summary(Ledger ledger, string head, string? warmth, double buildSeconds, List todo, List skipped, - List failed, double seconds, JsonObject? axes, Options options) - { - static JsonArray Names(IEnumerable names) => new([.. names.Select(x => (JsonNode)x)]); - Files.WriteJson(ledger.P("replay-last.json"), new JsonObject - { - ["branch"] = ledger.Branch, ["commit"] = head, ["build"] = warmth, ["build_seconds"] = Math.Round(buildSeconds, 1), - ["replayed"] = todo.Count, ["failed"] = Names(failed), ["seconds"] = Math.Round(seconds, 1), ["axes"] = axes?.DeepClone(), - ["all"] = options.All, ["family"] = options.Family, ["parallel"] = options.Parallel, ["footprint"] = options.Footprint, - ["selected"] = Names(todo.Select(s => s.Probe)), ["skipped"] = Names(skipped), - }); - } - - /// - /// The cache directory of this checkout's scratch worktrees (wt, seeds, explore, enumerate) under - /// PROBE_CACHE_DIR, else ~/.cache/probe-replay: the checkout's directory name and a hash of its path. A linked worktree - /// has one of its own, so sessions in two checkouts never reset each other's copy; the main checkout is keyed on - /// , as it was before, so its warm copy stays where it is. - /// - internal static string Home(Context ctx, Ledger ledger) - { - var checkout = ledger.Linked ? ledger.Top : ledger.Root; - var key = Convert.ToHexStringLower(SHA1.HashData(Encoding.UTF8.GetBytes(checkout)))[..8]; - return Path.Combine(ctx.Env("PROBE_CACHE_DIR") ?? Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile), ".cache", "probe-replay"), - $"{Path.GetFileName(checkout)}-{key}"); - } - - /// - /// The command that removes this checkout's cache, its worktrees first (for git worktree list) and then the - /// directory, every path literal: removing a linked worktree leaves its cache behind. - /// - internal static string RemoveCommand(string home) - { - // Forward slashes on Windows, which git and the sh of Git for Windows both read. - static string Quoted(string path) => "'" + (Path.DirectorySeparatorChar == '\\' ? path.Replace('\\', '/') : path).Replace("'", "'\\''") + "'"; - var worktrees = new[] { "wt", "seeds", "explore" }.Select(w => Path.Combine(home, w)).Where(w => File.Exists(Path.Combine(w, ".git"))); - return string.Join("; ", worktrees.Select(w => $"git worktree remove --force {Quoted(w)}").Append($"rm -rf {Quoted(home)}")); - } - - /// - /// Holds this checkout's replay worktree for one replay: the lock is per branch, the worktree per checkout, so two - /// branches' replays in one checkout (a branch switched while the post-commit replay runs) would otherwise share it. - /// An OS lock on replay.lock beside it, released by the OS when its process ends; null when another holds it. - /// - internal static FileStream? HoldWorktree(string home) - { - Directory.CreateDirectory(home); - try { return new FileStream(Path.Combine(home, "replay.lock"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); } - catch (IOException) { return null; } - } - - /// - /// Copies the corpus into the worktree's probe directory: tracked, overlaid by the branch's unpublished staging; rows - /// unpacked, one file per probe, and a rows file the corpus no longer has removed; and the claims as claims.json, - /// whose declarations of where an obligation does not apply the harness reads. - /// - internal static string Overlay(Ledger ledger, JsonObject config, string worktree) - { - var probeDir = Path.Combine(worktree, Files.Str(config["probeDir"])); - Directory.CreateDirectory(Path.Combine(probeDir, "rows")); - var wanted = new SortedDictionary(StringComparer.Ordinal); - var rows = new SortedDictionary(StringComparer.Ordinal); - foreach (var source in new[] { ledger.CorpusDir, ledger.StagingDir }) - { - foreach (var sub in new[] { "harness", "probes" }) - if (Directory.Exists(Path.Combine(source, sub))) - foreach (var f in Directory.EnumerateFiles(Path.Combine(source, sub), "*.cs")) wanted[Path.GetFileName(f)] = f; - if (Directory.Exists(Path.Combine(source, "rows"))) - foreach (var f in Directory.EnumerateFiles(Path.Combine(source, "rows")).Where(f => f.EndsWith(".json", StringComparison.Ordinal) || f.EndsWith(".json.gz", StringComparison.Ordinal))) - rows[Path.GetFileName(f).Replace(".gz", "", StringComparison.Ordinal)] = f; - } - foreach (var existing in Directory.EnumerateFiles(probeDir)) if (!wanted.ContainsKey(Path.GetFileName(existing)) && Path.GetFileName(existing) != "claims.json") File.Delete(existing); - foreach (var existing in Directory.EnumerateFiles(Path.Combine(probeDir, "rows"))) if (!rows.ContainsKey(Path.GetFileName(existing))) File.Delete(existing); - foreach (var (name, src) in wanted) - { - var dst = Path.Combine(probeDir, name); - if (!File.Exists(dst) || !File.ReadAllBytes(dst).AsSpan().SequenceEqual(File.ReadAllBytes(src))) File.Copy(src, dst, overwrite: true); - } - foreach (var (name, src) in rows) - { - var data = src.EndsWith(".gz", StringComparison.Ordinal) ? Files.Gunzip(src) : File.ReadAllBytes(src); - var dst = Path.Combine(probeDir, "rows", name); - if (!File.Exists(dst) || !File.ReadAllBytes(dst).AsSpan().SequenceEqual(data)) File.WriteAllBytes(dst, data); - } - // The claims beside the rows, the corpus's with the staging's over them: the harness's Rows.Claim reads each - // obligation's declarations from here, so Row.Check skips a check where its claim declares it inapplicable. - var claims = Path.Combine(probeDir, "claims.json"); - var text = new JsonArray([.. ledger.Claims().Select(c => (JsonNode)c)]).ToJsonString(); - if (!File.Exists(claims) || File.ReadAllText(claims) != text) File.WriteAllText(claims, text); - return probeDir; - } - - /// - /// One row of the harness's jsonl as evidence keeps it: its category, the settings that applied, six reasons, the - /// findings it skipped for, and each check it ran with the same fields (obligation, baseline, category, its own - /// settings, six reasons, notes). A row that ran no check keeps no checks. - /// - internal static JsonObject EvidenceRow(JsonObject line) - { - static JsonArray Six(JsonNode? reasons) => new([.. Files.Strings(reasons).Take(6).Select(x => (JsonNode)x)]); - var row = new JsonObject - { - ["category"] = Files.Str(line["category"], "violation"), ["applied"] = line["applied"]?.DeepClone() ?? new JsonObject(), - ["reasons"] = Six(line["reasons"]), - ["findings"] = new JsonArray([.. Files.Strings(line["findings"]).Select(x => (JsonNode)x)]), - }; - if (line["checks"] is JsonArray checks && checks.Count > 0) - row["checks"] = new JsonArray([.. checks.OfType().Select(c => (JsonNode)new JsonObject - { - ["obligation"] = Files.Str(c["obligation"]), ["baseline"] = c["baseline"] is JsonValue b ? Files.Str(b) : null, - ["category"] = Files.Str(c["category"], "violation"), ["applied"] = c["applied"]?.DeepClone() ?? new JsonObject(), - ["reasons"] = Six(c["reasons"]), ["notes"] = Files.Str(c["notes"]), - })]); - return row; - } - - /// The harness's PROBE_ONLY filter, replicated: the rows a variant is expected to run. - internal static bool Selected(IReadOnlyDictionary row, string only) - { - if (only.Length == 0) return true; - foreach (var condition in only.Split(',')) - { - var neq = condition.IndexOf("!=", StringComparison.Ordinal); - if (neq >= 0) { if (row.TryGetValue(condition[..neq], out var v) && v == condition[(neq + 2)..]) return false; } - else - { - var eq = condition.IndexOf('='); - if (!row.TryGetValue(condition[..eq], out var v) || v != condition[(eq + 1)..]) return false; - } - } - return true; - } - - private static string FirstLine(Ran ran) => ProbeRun.FirstLine(ran); -} diff --git a/tools/Prober/Round.cs b/tools/Prober/Round.cs deleted file mode 100644 index 9bbf4057..00000000 --- a/tools/Prober/Round.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System.Diagnostics; -using System.Globalization; -using System.Text.Json; -using System.Text.Json.Nodes; - -namespace Prober; - -/// -/// One exploration round (explore or full) under caps the model cannot overrun, run locally only: never in -/// CI, which needs no model. The wall clock is enforced here (the process tree is stopped at the deadline -/// plus a grace), the claim cap and the deadline by `rows`, which refuses new work, and the spend by -/// `--max-budget-usd`. The round always ends cleanly: partial lines repaired, caps cleared, lock released. -/// -internal static class Round -{ - public static int Run(Context ctx, Args a) - { - var mode = a.Positional.FirstOrDefault() ?? throw new Refusal("round takes explore or full", 2); - if (mode is not ("explore" or "full")) throw new Refusal("round takes explore or full", 2); - var minutes = a.Get("minutes") is { Length: > 0 } m ? double.Parse(m, CultureInfo.InvariantCulture) : mode == "explore" ? 10 : 30; - int? claims = a.Get("claims") is { Length: > 0 } c ? int.Parse(c, CultureInfo.InvariantCulture) : mode == "explore" ? 2 : null; - var grace = double.Parse(ctx.Env("PROBE_GRACE") ?? "60", CultureInfo.InvariantCulture); - var ledger = new Ledger(ctx, a.Get("branch")); - var holder = $"round-{mode}-{System.Environment.ProcessId}"; - - if (!RoundLock.Take(ledger, holder, mode, System.Environment.ProcessId).Taken) - { - ctx.Out.WriteLine("prober round: the ledger is locked by another round"); - return 1; - } - var clock = Stopwatch.StartNew(); - string outcome = "completed"; int? exit = null; JsonObject usage = []; var report = ""; - string roundEnded = "", repaired = "", unlocked; - try - { - Program.Run(new Context(ledger.Top, ctx.Environment, TextWriter.Null, ctx.Error), - ["round-start", "--branch", ledger.Branch, "--holder", holder, "--minutes", minutes.ToString(CultureInfo.InvariantCulture), - .. (claims is { } n ? new[] { "--claims", n.ToString(CultureInfo.InvariantCulture) } : [])]); - var caps = $"{minutes.ToString(CultureInfo.InvariantCulture)} minutes" + (claims is { } k ? $" and {k} claim(s)" : ""); - var prompt = - $"Mode: {mode}. Repository {ledger.Top}, branch {ledger.Branch}, base {a.Get("base") ?? "main"}.\n" + - $"This round runs under caps enforced outside you ({caps}). The round runner holds the lock as `{holder}`: do not lock, " + - $"unlock or start a round yourself, and pass `--holder {holder}` to `prober replay`. `prober rows` refuses new work once " + - "the time is spent or the claim cap is reached; when it refuses, start nothing new: record what ran, hand the corpus off " + - "to staging, and report. The process is stopped at the deadline whatever you are doing, so record each claim's results " + - "as soon as its rows have run."; - var argv = new List { "-p", prompt, "--agent", "prober", "--permission-mode", "auto", "--output-format", "json" }; - if (a.Get("usd") is { Length: > 0 } usd) argv.AddRange(["--max-budget-usd", usd]); - var env = new Dictionary(ctx.Environment, StringComparer.Ordinal) { ["PROBE_HOLDER"] = holder }; - - using (new RoundLock.Heartbeat(ledger, holder)) - { - var ran = Proc.Run(FindClaude(ctx), argv, ledger.Top, env, TimeSpan.FromSeconds(minutes * 60 + grace)); - exit = ran.ExitCode; - if (ran.ExitCode == -1) outcome = "stopped at the cap"; - else - { - try - { - if (JsonNode.Parse(ran.Stdout) is JsonObject result) - { - foreach (var key in new[] { "total_cost_usd", "num_turns", "duration_ms", "usage", "subtype" }) usage[key] = result[key]?.DeepClone(); - report = Files.Str(result["result"]); - } - } - catch (JsonException) { report = ran.Stdout.Length > 4000 ? ran.Stdout[^4000..] : ran.Stdout; } - if (ran.ExitCode != 0) outcome = "failed"; - if ((ran.Stdout + ran.Stderr).Contains("Not logged in", StringComparison.Ordinal)) - { - outcome = "not logged in"; - report = "The headless claude CLI is not authenticated. Log it in once (`claude`, then /login, or ANTHROPIC_API_KEY), or start the round from a Claude session with the /probe skill, which caps it with `prober round-start` and stops it at the deadline."; - } - } - } - } - catch (System.ComponentModel.Win32Exception) - { - outcome = "no claude executable"; report = "No claude executable was found; set CLAUDE_BIN. Exploration needs a model and runs locally only."; - } - finally - { - var w = new StringWriter(); - Program.Run(new Context(ledger.Top, ctx.Environment, w, ctx.Error), ["repair", "--branch", ledger.Branch]); repaired = w.ToString().Trim(); - w = new StringWriter(); - Program.Run(new Context(ledger.Top, ctx.Environment, w, ctx.Error), ["round-end", "--branch", ledger.Branch, "--holder", holder]); roundEnded = w.ToString().Trim(); - w = new StringWriter(); - Program.Run(new Context(ledger.Top, ctx.Environment, w, ctx.Error), ["unlock", "--branch", ledger.Branch, "--holder", holder]); unlocked = w.ToString().Trim(); - } - if (unlocked.Contains("replay was skipped", StringComparison.Ordinal)) - Replay.Run(new Context(ledger.Top, ctx.Environment, TextWriter.Null, ctx.Error), new Args(["--branch", ledger.Branch], new HashSet(StringComparer.Ordinal))); - var status = new StringWriter(); - Program.Run(new Context(ledger.Top, ctx.Environment, status, ctx.Error), ["status", "--branch", ledger.Branch]); - var summary = new JsonObject - { - ["mode"] = mode, ["branch"] = ledger.Branch, - ["caps"] = new JsonObject { ["minutes"] = minutes, ["claims"] = claims, ["usd"] = a.Get("usd") }, - ["outcome"] = outcome, ["exit"] = exit, ["seconds"] = Math.Round(clock.Elapsed.TotalSeconds, 1), ["usage"] = usage, - ["round"] = roundEnded, ["repair"] = repaired, ["status"] = status.ToString().Split('\n')[0].Trim(), - }; - var full = (JsonObject)summary.DeepClone(); full["report"] = report; - Files.WriteJson(ledger.P("round-last.json"), full); - ctx.Out.WriteLine(summary.ToJsonString(new JsonSerializerOptions { WriteIndented = true })); - if (report.Length > 0) ctx.Out.WriteLine("\n" + report); - return outcome == "completed" ? 0 : 1; - } - - /// $CLAUDE_BIN, else claude on PATH, else the desktop app's bundled copy. - private static string FindClaude(Context ctx) - { - if (ctx.Env("CLAUDE_BIN") is { } bin) return bin; - foreach (var dir in (ctx.Env("PATH") ?? "").Split(Path.PathSeparator)) - foreach (var name in new[] { "claude", "claude.exe" }) - if (dir.Length > 0 && File.Exists(Path.Combine(dir, name))) return Path.Combine(dir, name); - var bundled = Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Claude", "claude-code"); - if (Directory.Exists(bundled)) - { - var found = Directory.EnumerateFiles(bundled, "claude", SearchOption.AllDirectories) - .Where(p => p.Contains("Contents/MacOS", StringComparison.Ordinal)).Order(StringComparer.Ordinal).LastOrDefault(); - if (found is not null) return found; - } - throw new System.ComponentModel.Win32Exception("no claude executable"); - } -} diff --git a/tools/Prober/RoundLock.cs b/tools/Prober/RoundLock.cs deleted file mode 100644 index e3e16831..00000000 --- a/tools/Prober/RoundLock.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System.Text.Json.Nodes; - -namespace Prober; - -/// -/// One round at a time per branch. The lock file sits in the branch's directory (branches/<branch>/round.lock), -/// beside everything a round or a replay writes, so two branches' rounds may run at once and two of one branch never do. -/// It is created exclusively, with its holder's owner and a heartbeat inside; a holder is alive while its heartbeat is -/// younger than . Liveness is the heartbeat alone (no process is probed, no signal sent), -/// which is the same on every OS. Every read-judge-write of the lock runs under an exclusive file create (the mutex), so -/// two takers can never both see it free, or both take over a holder whose heartbeat stopped. -/// -internal static class RoundLock -{ - public const int HeartbeatMinutes = 20; - public static readonly TimeSpan BeatEvery = TimeSpan.FromSeconds(30); - private const int MutexStaleSeconds = 60; - - public static string LockPath(Ledger l) => Path.Combine(l.Dir, "round.lock"); - - public static double Now() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() / 1000.0; - - public static bool Alive(JsonObject held) => - Now() - Files.Num(held["heartbeat"], Files.Num(held["since"])) < HeartbeatMinutes * 60; - - public static JsonObject? Holder(Ledger l) => Files.ReadJson(LockPath(l)) is JsonObject held && Alive(held) ? held : null; - - /// - /// The mutex: a file created with , which fails when it exists, atomically - /// on every OS. A crashed writer's mutex goes stale after a minute and is removed. - /// - public static T WithMutex(Ledger l, Func body) - { - Directory.CreateDirectory(l.Dir); - var path = LockPath(l) + ".mutex"; - var deadline = DateTime.UtcNow.AddSeconds(10); - while (true) - { - try - { - using (new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { } - break; - } - catch (Exception busy) when (busy is IOException or UnauthorizedAccessException) - { - // It exists (or vanished an instant ago, when its holder released it): either way, look again. - // Windows says UnauthorizedAccess, not IOException, for a name whose file is being deleted. - try - { - if (File.Exists(path) && (DateTime.UtcNow - File.GetLastWriteTimeUtc(path)).TotalSeconds > MutexStaleSeconds) File.Delete(path); - } - catch (Exception gone) when (gone is IOException or UnauthorizedAccessException) { } - if (DateTime.UtcNow > deadline) throw new Refusal("the lock's mutex is held for over 10 s; another command is stuck"); - Thread.Sleep(Random.Shared.Next(5, 25)); - } - } - try { return body(); } - finally { try { File.Delete(path); } catch (Exception e) when (e is IOException or UnauthorizedAccessException) { } } - } - - public static void WithMutex(Ledger l, Action body) => WithMutex(l, () => { body(); return 0; }); - - public static (bool Taken, JsonObject? Previous) Take(Ledger l, string holder, string mode, long? pid) - { - var now = Now(); - var record = new JsonObject - { - ["schemaVersion"] = Files.SchemaVersion, ["holder"] = holder, ["mode"] = mode, - ["owner"] = new JsonObject { ["pid"] = pid ?? Environment.ProcessId, ["host"] = Environment.MachineName }, - ["since"] = now, ["heartbeat"] = now, ["pending_replay"] = false, - }; - return WithMutex(l, () => - { - var held = Files.ReadJson(LockPath(l)) as JsonObject; - if (held is not null && Files.Str(held["holder"]) != holder && Alive(held)) return (false, held); - if (held is not null) - { - record["pending_replay"] = held["pending_replay"] is JsonValue p && p.TryGetValue(out var b) && b; - Files.WriteJson(LockPath(l), record); // a holder taking it again, or one whose heartbeat stopped - } - else - { - // No lock: an exclusive create, so even a writer that bypasses the mutex cannot overwrite one. - using var stream = new FileStream(LockPath(l), FileMode.CreateNew, FileAccess.Write, FileShare.None); - using var writer = new StreamWriter(stream); - writer.Write(record.ToJsonString()); - } - return (true, held); - }); - } - - public static void Beat(Ledger l, string holder) => WithMutex(l, () => - { - if (Files.ReadJson(LockPath(l)) is JsonObject held && Files.Str(held["holder"]) == holder) - { - held["heartbeat"] = Now(); - Files.WriteJson(LockPath(l), held); - } - }); - - public static void OweReplay(Ledger l) => WithMutex(l, () => - { - if (Files.ReadJson(LockPath(l)) is JsonObject held) - { - held["pending_replay"] = true; - Files.WriteJson(LockPath(l), held); - } - }); - - /// Releases the lock; returns whether a replay was owed. Refuses another holder's lock. - public static bool Release(Ledger l, string holder) => WithMutex(l, () => - { - var held = Files.ReadJson(LockPath(l)) as JsonObject; - if (held is not null && Files.Str(held["holder"]) != holder) - throw new Refusal($"the lock is {Files.Str(held["holder"])}'s, not {holder}'s"); - if (held is not null) File.Delete(LockPath(l)); - return held?["pending_replay"] is JsonValue p && p.TryGetValue(out var b) && b; - }); - - public static JsonObject? Abandon(Ledger l) => WithMutex(l, () => - { - var held = Files.ReadJson(LockPath(l)) as JsonObject; - if (held is not null) File.Delete(LockPath(l)); - return held; - }); - - /// Keeps a lock alive while a long step runs: a timer beats every 30 s. - public sealed class Heartbeat(Ledger ledger, string holder) : IDisposable - { - private readonly Timer timer = new(_ => { try { Beat(ledger, holder); } catch (Exception) { } }, null, BeatEvery, BeatEvery); - public void Dispose() => timer.Dispose(); - } -} diff --git a/tools/Prober/SchemaCheck.cs b/tools/Prober/SchemaCheck.cs deleted file mode 100644 index 56725a7d..00000000 --- a/tools/Prober/SchemaCheck.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System.Text.Json.Nodes; -using System.Text.RegularExpressions; - -namespace Prober; - -/// -/// The part of JSON Schema the tracked files' schemas use, checked where a file is read, so a field nobody -/// reads is refused instead of kept (decision 102: three invented fields for an exclusion's reason sat in -/// claims.json unread). A keyword outside this part throws: a schema that says more than is checked would be -/// a check that silently does less than it reads. -/// -internal static class SchemaCheck -{ - private static readonly HashSet Annotations = ["$schema", "$id", "title", "description"]; - - /// The schema of Schemas/{name}, embedded in the dll, so it is the one the dll's fingerprint covers. - public static JsonObject Load(string name) - { - using var stream = typeof(SchemaCheck).Assembly.GetManifestResourceStream(name) - ?? throw new InvalidOperationException($"no embedded schema {name}"); - return JsonNode.Parse(stream) as JsonObject ?? throw new InvalidOperationException($"the schema {name} is not a JSON object"); - } - - /// What in the schema refuses, each at its path (exclude[0].reason). - public static List Problems(JsonNode? node, JsonObject schema, string path = "") - { - var problems = new List(); - Check(node, schema, path, problems); - return problems; - } - - private static void Check(JsonNode? node, JsonObject schema, string path, List problems) - { - var at = path.Length == 0 ? "the value" : path; - foreach (var (keyword, _) in schema) - if (!Annotations.Contains(keyword) && keyword is not ("type" or "const" or "required" or "properties" or "additionalProperties" - or "items" or "minItems" or "maxItems" or "minLength" or "pattern")) - throw new InvalidOperationException($"a schema uses {keyword}, which SchemaCheck does not check"); - - if (schema["type"] is JsonValue type && !IsType(node, Files.Str(type))) - { - problems.Add($"{at} is not {Article(Files.Str(type))}"); - return; - } - if (schema["const"] is { } constant && !JsonNode.DeepEquals(node, constant)) problems.Add($"{at} is not {constant.ToJsonString()}"); - - if (node is JsonObject o) - { - foreach (var name in Files.Strings(schema["required"])) - if (!o.ContainsKey(name)) problems.Add($"{Join(path, name)} is required"); - var properties = schema["properties"] as JsonObject ?? []; - foreach (var (name, value) in o) - { - if (properties[name] is JsonObject property) Check(value, property, Join(path, name), problems); - else if (schema["additionalProperties"] is JsonObject rest) Check(value, rest, Join(path, name), problems); - else if (schema["additionalProperties"] is JsonValue allowed && allowed.TryGetValue(out var b) && !b) - problems.Add($"{Join(path, name)} is not a field the schema has"); - } - } - if (node is JsonArray a) - { - if (schema["minItems"] is { } min && a.Count < Files.Num(min)) problems.Add($"{at} has {a.Count} item(s), fewer than {Files.Num(min)}"); - if (schema["maxItems"] is { } max && a.Count > Files.Num(max)) problems.Add($"{at} has {a.Count} item(s), more than {Files.Num(max)}"); - if (schema["items"] is JsonObject items) - for (var i = 0; i < a.Count; i++) Check(a[i], items, $"{path}[{i}]", problems); - } - if (node is JsonValue v && v.TryGetValue(out var s)) - { - if (schema["minLength"] is { } length && s.Length < Files.Num(length)) problems.Add($"{at} is shorter than {Files.Num(length)}"); - if (schema["pattern"] is JsonValue pattern && !Regex.IsMatch(s, Files.Str(pattern), RegexOptions.CultureInvariant)) - problems.Add($"{at} does not match {Files.Str(pattern)}"); - } - } - - private static string Join(string path, string name) => path.Length == 0 ? name : $"{path}.{name}"; - - private static string Article(string type) => type is "object" or "array" or "integer" ? $"an {type}" : $"a {type}"; - - private static bool IsType(JsonNode? node, string type) => type switch - { - "object" => node is JsonObject, - "array" => node is JsonArray, - "string" => node is JsonValue v && v.TryGetValue(out _), - "integer" => node is JsonValue v && v.TryGetValue(out _), - "boolean" => node is JsonValue v && v.TryGetValue(out _), - _ => throw new InvalidOperationException($"a schema names type {type}, which SchemaCheck does not check"), - }; -} diff --git a/tools/Prober/Schemas/claims.schema.json b/tools/Prober/Schemas/claims.schema.json deleted file mode 100644 index 3c944f19..00000000 --- a/tools/Prober/Schemas/claims.schema.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://codometis.typekit/probe/schemas/claims.schema.json", - "title": "Claims", - "description": "Tracked (corpus/claims.json): the claims the documents make, each naming the surfaces whose axes it takes. The prober refuses a file that does not match this schema when it reads one (decision 102).", - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "source", - "surfaces" - ], - "properties": { - "schemaVersion": { - "type": "integer", - "const": 1, - "description": "The schema version. A file or line without it is version 1 (the Python baseline)." - }, - "id": { - "type": "string", - "minLength": 1 - }, - "source": { - "type": "string", - "description": "file#heading of the document that states it" - }, - "surfaces": { - "type": "array", - "items": { - "type": "string" - }, - "minItems": 1 - }, - "axes": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - }, - "description": "claim-local axes" - }, - "narrow": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "description": "axis dropped, with the source sentence that limits it (checked verbatim)" - }, - "obligations": { - "type": "array", - "minItems": 1, - "description": "what the claim promises, each checkable on its own: a sentence quoted verbatim from the claim's source document (checked as a narrowing is), under an id a probe's seed names (decision 104); a claim without the field has one obligation, the claim itself, under the id `claim`", - "items": { - "type": "object", - "required": [ - "id", - "sentence" - ], - "properties": { - "id": { - "type": "string", - "pattern": "^[a-z0-9][a-z0-9-]*$" - }, - "sentence": { - "type": "string", - "pattern": "\\S" - }, - "axes": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - }, - "description": "the axis values the obligation concerns, where it is not the whole claim" - }, - "inapplicable": { - "type": "array", - "minItems": 1, - "description": "where the obligation does not apply: a row holding every axis of a declaration at one of its values runs no check of the obligation (Row.Check records it inapplicable with the cite), a pair matched by a declaration is not wanted by the obligation, and a pair no obligation wants is declared inapplicable, never put in a generated row and left out of the gate's count as an exclusion is. `Axes.Build` refuses an axis or value the claim does not list, a malformed cite, and a not-promised id the claim lacks", - "items": { - "type": "object", - "required": [ - "axes", - "cite" - ], - "properties": { - "axes": { - "type": "object", - "additionalProperties": { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - }, - "description": "axis to the values where the obligation does not apply; every axis must hold at one of its values" - }, - "cite": { - "type": "string", - "pattern": "^(not-promised|docs|fact|record):", - "description": "why: not-promised:, docs:, fact:KB-NNN or record:NNNN, in the forms the harness's Cite accepts" - } - }, - "additionalProperties": false - } - } - }, - "additionalProperties": false - } - }, - "not-promised": { - "type": "array", - "minItems": 1, - "description": "what the claim does not promise, one entry per intended difference: the sentence of the document or record that makes it intended, quoted verbatim from `source` (checked as a narrowing is), and the axis values it concerns; a note key of outcome expected-refusal, a narrowing or an applicability condition cites an entry by its id (decision 104)", - "items": { - "type": "object", - "required": [ - "id", - "source", - "sentence" - ], - "properties": { - "id": { - "type": "string", - "pattern": "^[a-z0-9][a-z0-9-]*$" - }, - "source": { - "type": "string", - "description": "file#heading of the document or record that states it" - }, - "sentence": { - "type": "string", - "pattern": "\\S" - }, - "axes": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - }, - "description": "the axis values or shapes the difference concerns" - } - }, - "additionalProperties": false - } - }, - "exclude": { - "type": "array", - "description": "pairs no row can hold, each group with the reason why; `prober universe` prints the reasons, since nothing checks them mechanically", - "items": { - "type": "object", - "required": [ - "reason", - "pairs" - ], - "properties": { - "reason": { - "type": "string", - "pattern": "\\S", - "description": "one sentence saying why the pairs cannot exist or are not promised, citing a docs sentence, a record or a KB fact where one applies" - }, - "pairs": { - "type": "array", - "minItems": 1, - "items": { - "type": "array", - "items": { - "type": "string", - "pattern": "^[^=]+=.*$" - }, - "minItems": 2, - "maxItems": 2 - } - } - }, - "additionalProperties": false - } - } - }, - "additionalProperties": false - } -} diff --git a/tools/Prober/Schemas/dismissed-line.schema.json b/tools/Prober/Schemas/dismissed-line.schema.json deleted file mode 100644 index 1556681b..00000000 --- a/tools/Prober/Schemas/dismissed-line.schema.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://codometis.typekit/probe/schemas/dismissed-line.schema.json", - "title": "Dismissed finding (one line)", - "description": "Tracked (corpus/dismissed.jsonl): a finding the maintainers dismissed.", - "type": "object", - "required": [ - "id", - "reason" - ], - "properties": { - "schemaVersion": { - "type": "integer", - "const": 1, - "description": "The schema version. A file or line without it is version 1 (the Python baseline)." - }, - "id": { - "type": "string" - }, - "reason": { - "type": "string" - }, - "date": { - "type": "string" - } - } -} diff --git a/tools/Prober/Schemas/evidence.schema.json b/tools/Prober/Schemas/evidence.schema.json deleted file mode 100644 index 7d86b9db..00000000 --- a/tools/Prober/Schemas/evidence.schema.json +++ /dev/null @@ -1,170 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://codometis.typekit/probe/schemas/evidence.schema.json", - "title": "Evidence", - "description": "State (branches//evidence/.json): what one replay of a probe did. Only checks that ended as a pass, in rows expected and executed, earn credit, for the settings that applied (the row's, the check's laid over them), while the identity holds. A row without checks is one check of the obligation `claim` with the row's category.", - "type": "object", - "required": [ - "identity", - "variants", - "rows" - ], - "properties": { - "schemaVersion": { - "type": "integer", - "enum": [ - 1, - 2 - ], - "description": "The schema version: 2 records each row's checks. A file without it is version 1, whose rows have no checks." - }, - "probe": { - "type": "string" - }, - "identity": { - "type": "string" - }, - "commit": { - "type": "string" - }, - "result": { - "enum": [ - "pass", - "fail" - ] - }, - "date": { - "type": "string" - }, - "variants": { - "type": "array", - "items": { - "type": "object", - "required": [ - "expected", - "executed" - ], - "properties": { - "env": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "description": "Axis name to value, both compared ordinally." - }, - "expected": { - "type": "array", - "items": { - "type": "integer" - } - }, - "executed": { - "type": "array", - "items": { - "type": "integer" - } - } - } - } - }, - "rows": { - "type": "object", - "additionalProperties": { - "type": "object", - "required": [ - "category", - "applied" - ], - "properties": { - "category": { - "enum": [ - "pass", - "violation", - "expected-refusal", - "inapplicable", - "harness-error", - "inconclusive", - "known-finding" - ], - "description": "The highest of the row's own outcome and its checks'." - }, - "applied": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "description": "Axis name to value, both compared ordinally: the axes the row read outside any check, which every check of it ran under." - }, - "reasons": { - "type": "array", - "items": { - "type": "string" - } - }, - "findings": { - "type": "array", - "items": { - "type": "string" - }, - "description": "The open findings the row stepped around (Row.Skip in the harness); the row ends as known-finding." - }, - "checks": { - "type": "array", - "description": "Version 2: one entry per check the row ran (Row.Check in the harness), in the order they ended. Absent for a row that ran none.", - "items": { - "type": "object", - "required": [ - "obligation", - "category", - "applied" - ], - "properties": { - "obligation": { - "type": "string", - "description": "An id of the claim's obligations, or `claim` for a claim without them." - }, - "baseline": { - "type": [ - "string", - "null" - ], - "description": "What the check compared against: twin, framework, round-trip, model, refusal, cost, fact:KB-NNN or docs:file#anchor. A pass always has one." - }, - "category": { - "enum": [ - "pass", - "violation", - "expected-refusal", - "inapplicable", - "harness-error", - "inconclusive", - "known-finding" - ] - }, - "applied": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "description": "The axes this check read, at the value that applied, and none another check read." - }, - "reasons": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Six at most, as the row's." - }, - "notes": { - "type": "string", - "description": "How the check was compared, and the citations of what did not apply or was refused, separated by ;." - } - }, - "additionalProperties": false - } - } - } - } - } - } -} diff --git a/tools/Prober/Schemas/finding-line.schema.json b/tools/Prober/Schemas/finding-line.schema.json deleted file mode 100644 index 317c24f6..00000000 --- a/tools/Prober/Schemas/finding-line.schema.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://codometis.typekit/probe/schemas/finding-line.schema.json", - "title": "Finding event (one line)", - "description": "State (findings.jsonl): open, fixed or withdrawn. The last line per id wins; a fixed line counts once its commit is in HEAD's history.", - "type": "object", - "required": [ - "id", - "status" - ], - "properties": { - "schemaVersion": { - "type": "integer", - "const": 1, - "description": "The schema version. A file or line without it is version 1 (the Python baseline)." - }, - "id": { - "type": "string" - }, - "claim": { - "type": [ - "string", - "null" - ] - }, - "status": { - "enum": [ - "open", - "fixed", - "withdrawn", - "dismissed" - ] - }, - "commit": { - "type": "string" - }, - "probe": { - "type": "string" - }, - "reason": { - "type": "string" - } - } -} diff --git a/tools/Prober/Schemas/footprint-line.schema.json b/tools/Prober/Schemas/footprint-line.schema.json deleted file mode 100644 index f5e41f2e..00000000 --- a/tools/Prober/Schemas/footprint-line.schema.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://codometis.typekit/probe/schemas/footprint-line.schema.json", - "title": "Footprint (one line)", - "description": "State (footprints.jsonl): the source files a run of a probe executed, the compile-time paths and the toolchain, each with its id. The last line per probe wins.", - "type": "object", - "required": [ - "probe", - "footprint" - ], - "properties": { - "schemaVersion": { - "type": "integer", - "const": 1, - "description": "The schema version. A file or line without it is version 1 (the Python baseline)." - }, - "probe": { - "type": "string" - }, - "commit": { - "type": "string" - }, - "footprint": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "description": "Axis name to value, both compared ordinally." - }, - "compileTime": { - "type": "object", - "additionalProperties": { - "type": [ - "string", - "null" - ] - } - }, - "watched": { - "type": "object", - "additionalProperties": { - "type": [ - "string", - "null" - ] - }, - "description": "The paths the probe's registry entry watches, each with its object id at the commit the run built (null where it does not exist). A footprint line without one of the entry's watched paths is not current." - }, - "toolchain": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "description": "Axis name to value, both compared ordinally." - }, - "date": { - "type": "string" - } - } -} diff --git a/tools/Prober/Schemas/ledger-line.schema.json b/tools/Prober/Schemas/ledger-line.schema.json deleted file mode 100644 index 188d2a8f..00000000 --- a/tools/Prober/Schemas/ledger-line.schema.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://codometis.typekit/probe/schemas/ledger-line.schema.json", - "title": "Ledger line (legacy, one line)", - "description": "State (ledger.jsonl): what a round recorded per pair. Informational: it earns no credit; only evidence does.", - "type": "object", - "required": [ - "claim", - "pair", - "commit", - "result" - ], - "properties": { - "schemaVersion": { - "type": "integer", - "const": 1, - "description": "The schema version. A file or line without it is version 1 (the Python baseline)." - }, - "claim": { - "type": "string" - }, - "pair": { - "type": "array", - "items": { - "type": "string", - "pattern": "^[^=]+=.*$" - }, - "minItems": 2, - "maxItems": 2 - }, - "commit": { - "type": "string" - }, - "result": { - "type": "string" - }, - "probe": { - "type": "string" - }, - "date": { - "type": "string" - } - } -} diff --git a/tools/Prober/Schemas/lock.schema.json b/tools/Prober/Schemas/lock.schema.json deleted file mode 100644 index 5e9e290d..00000000 --- a/tools/Prober/Schemas/lock.schema.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://codometis.typekit/probe/schemas/lock.schema.json", - "title": "Round lock", - "description": "State (branches//round.lock): one round at a time per branch. Created exclusively; alive while its heartbeat is recent.", - "type": "object", - "required": [ - "holder", - "mode", - "since", - "heartbeat" - ], - "properties": { - "schemaVersion": { - "type": "integer", - "const": 1, - "description": "The schema version. A file or line without it is version 1 (the Python baseline)." - }, - "holder": { - "type": "string" - }, - "mode": { - "enum": [ - "replay", - "explore", - "full" - ] - }, - "owner": { - "type": "object", - "properties": { - "pid": { - "type": [ - "integer", - "null" - ] - }, - "host": { - "type": "string" - } - } - }, - "since": { - "type": "number" - }, - "heartbeat": { - "type": "number" - }, - "pending_replay": { - "type": "boolean" - } - } -} diff --git a/tools/Prober/Schemas/registry.schema.json b/tools/Prober/Schemas/registry.schema.json deleted file mode 100644 index 3be5eb0a..00000000 --- a/tools/Prober/Schemas/registry.schema.json +++ /dev/null @@ -1,143 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://codometis.typekit/probe/schemas/registry.schema.json", - "title": "Probe registry", - "description": "Tracked (corpus/registry.json): every probe, the claim it covers, how to select it, and the process variants it runs in.", - "type": "object", - "required": [ - "probes" - ], - "properties": { - "schemaVersion": { - "type": "integer", - "const": 1, - "description": "The schema version. A file or line without it is version 1 (the Python baseline)." - }, - "probes": { - "type": "object", - "additionalProperties": { - "type": "object", - "required": [ - "claim", - "filter" - ], - "properties": { - "claim": { - "type": "string" - }, - "filter": { - "type": "string" - }, - "variants": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "description": "Axis name to value, both compared ordinally." - }, - "description": "one process per environment map" - }, - "retired": { - "type": "boolean" - }, - "watched": { - "type": "array", - "items": { - "type": "string", - "minLength": 1 - }, - "description": "Paths, files or directories relative to the repository root, that the probe's result depends on and its footprint cannot see: a variant catalogue under test/ compiled into the probes, a build input a child process reads. The replay records each with its object id in the probe's footprint line; a change to one stales the footprint (Ledger.FootprintCurrent, FootprintChangedSince), and the release gate counts a change to one as touching the probe's claim. Part of the probe's identity, as the whole entry but its seeds is." - }, - "seeds": { - "type": "array", - "description": "One seeded defect per obligation of the probe's claim that this probe checks (decision 104): each as `seed` below, naming the obligation it breaks. A probe may carry `seed` and `seeds` both. Empty in the entry `prober new` stages, until the writer registers each with `prober register --obligation O`, which replaces a seed naming O; `seeds` and Seeds.Problems refuse a probe with none. Not part of the probe's identity.", - "items": { - "type": "object", - "required": [ - "obligation", - "defect" - ], - "anyOf": [ - { - "required": [ - "patch" - ] - }, - { - "required": [ - "env" - ] - } - ], - "properties": { - "obligation": { - "type": "string", - "description": "The id of the claim's obligation this seed breaks (claims.json `obligations[].id`, or `claim` for a claim without the field)." - }, - "patch": { - "type": "string" - }, - "env": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "defect": { - "type": "string", - "minLength": 1 - } - }, - "additionalProperties": false - } - }, - "seed": { - "type": "object", - "required": [ - "defect" - ], - "anyOf": [ - { - "required": [ - "patch" - ] - }, - { - "required": [ - "env" - ] - } - ], - "properties": { - "patch": { - "type": "string", - "description": "A unified diff against the product files (the replay's \"include\"), relative to the corpus directory: seeds/.patch." - }, - "env": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "description": "Environment variables set for the seeded build and every variant's run." - }, - "defect": { - "type": "string", - "minLength": 1, - "description": "What the seed breaks, in one sentence." - }, - "obligation": { - "type": "string", - "description": "The id of the claim's obligation this seed breaks (decision 104); absent, the seed proves the probe and counts for the claim's one implicit obligation only where the claim lists none." - } - }, - "additionalProperties": false, - "description": "The product defect `prober seeds` applies in a scratch worktree to prove the probe can fail (decision 100). Not part of the probe's identity." - } - }, - "additionalProperties": false - } - } - } -} diff --git a/tools/Prober/Schemas/round.schema.json b/tools/Prober/Schemas/round.schema.json deleted file mode 100644 index dda984fe..00000000 --- a/tools/Prober/Schemas/round.schema.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://codometis.typekit/probe/schemas/round.schema.json", - "title": "Round caps", - "description": "State (branches//round.json): the caps `rows` enforces for the round that holds the lock.", - "type": "object", - "required": [ - "holder", - "deadline", - "claims" - ], - "properties": { - "schemaVersion": { - "type": "integer", - "const": 1, - "description": "The schema version. A file or line without it is version 1 (the Python baseline)." - }, - "holder": { - "type": "string" - }, - "started": { - "type": "number" - }, - "deadline": { - "type": "number" - }, - "max_claims": { - "type": [ - "integer", - "null" - ] - }, - "claims": { - "type": "array", - "items": { - "type": "string" - } - } - } -} diff --git a/tools/Prober/Schemas/rows.schema.json b/tools/Prober/Schemas/rows.schema.json deleted file mode 100644 index 290f9a7b..00000000 --- a/tools/Prober/Schemas/rows.schema.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://codometis.typekit/probe/schemas/rows.schema.json", - "title": "Rows", - "description": "Tracked (corpus/rows/.json.gz, gzipped; rows/.json in the staging): the rows one probe runs, a pairwise covering array over its claim's axes. One file per registered probe, written whole by `prober rows --probe P`, never appended; no two rows of a file have the same axis map (ProbeRowsTests).", - "type": "object", - "required": [ - "claim", - "rows" - ], - "properties": { - "schemaVersion": { - "type": "integer", - "const": 1, - "description": "The schema version. A file or line without it is version 1 (the Python baseline)." - }, - "claim": { - "type": "string", - "description": "The id of the probe's claim in claims.json; the file is named for the probe, so the claim is kept inside." - }, - "rows": { - "type": "array", - "items": { - "type": "object", - "required": [ - "row" - ], - "properties": { - "row": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "description": "Axis name to value, both compared ordinally." - }, - "pairs": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "string", - "pattern": "^[^=]+=.*$" - }, - "minItems": 2, - "maxItems": 2 - } - } - } - } - } - } -} diff --git a/tools/Prober/Schemas/run-line.schema.json b/tools/Prober/Schemas/run-line.schema.json deleted file mode 100644 index 5aca7f82..00000000 --- a/tools/Prober/Schemas/run-line.schema.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://codometis.typekit/probe/schemas/run-line.schema.json", - "title": "Run (one line)", - "description": "State (runs.jsonl): one replay result of one probe, with the identity it ran under.", - "type": "object", - "required": [ - "probe", - "result", - "commit" - ], - "properties": { - "schemaVersion": { - "type": "integer", - "const": 1, - "description": "The schema version. A file or line without it is version 1 (the Python baseline)." - }, - "probe": { - "type": "string" - }, - "result": { - "enum": [ - "pass", - "fail" - ] - }, - "commit": { - "type": "string" - }, - "date": { - "type": "string" - }, - "seconds": { - "type": [ - "number", - "null" - ] - }, - "identity": { - "type": "string" - } - } -} diff --git a/tools/Prober/Schemas/seeds-run.schema.json b/tools/Prober/Schemas/seeds-run.schema.json deleted file mode 100644 index 824b9919..00000000 --- a/tools/Prober/Schemas/seeds-run.schema.json +++ /dev/null @@ -1,131 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://codometis.typekit/probe/schemas/seeds-run.schema.json", - "title": "Seeds run", - "description": "State (seeds-last.json): the last `prober seeds` run on the branch, one outcome per seed it ran (a probe without one has a single outcome, `no seed`), the probes it skipped and why, and per probe the run that last caught every one of its seeds, which decides what the next run without --all skips (design/adr/0107). Never evidence: a seeded run proves the probe, not the product.", - "type": "object", - "required": [ - "branch", - "commit", - "seeds" - ], - "properties": { - "schemaVersion": { - "type": "integer", - "const": 1, - "description": "The schema version. A file or line without it is version 1 (the Python baseline)." - }, - "branch": { - "type": "string" - }, - "commit": { - "type": "string" - }, - "seconds": { - "type": "number" - }, - "all": { - "type": "boolean", - "description": "Whether the run was `--all`, every probe whatever its record says, as a release runs it. Absent in a file written before footprint selection." - }, - "probes": { - "type": "object", - "description": "Per probe, the run that last caught every seed it owes; a probe with a seed missed, or that never ran, has no entry, so it runs next time. Carried forward for the probes a run skipped.", - "additionalProperties": { - "type": "object", - "required": [ - "identity", - "commit" - ], - "properties": { - "identity": { - "type": "string", - "description": "The seeds identity it was caught under: the probe's identity, its seeds and the content of their patches." - }, - "commit": { - "type": "string", - "description": "HEAD of the run that caught them; the product footprint and the patched files are compared with it." - }, - "seeds": { - "type": "integer", - "description": "How many seeds were caught." - } - }, - "additionalProperties": false - } - }, - "skipped": { - "type": "array", - "description": "The probes this run skipped, unchanged since every seed of theirs was caught, each with the reason printed.", - "items": { - "type": "object", - "required": [ - "probe", - "reason" - ], - "properties": { - "probe": { - "type": "string" - }, - "reason": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "seeds": { - "type": "array", - "items": { - "type": "object", - "required": [ - "probe", - "claim", - "result" - ], - "properties": { - "probe": { - "type": "string" - }, - "claim": { - "type": "string" - }, - "obligation": { - "type": [ - "string", - "null" - ], - "description": "The claim's obligation the seed breaks (decision 104), or null for a seed that names none." - }, - "seed": { - "type": "string", - "description": "The patch and the environment toggle applied, or (none)." - }, - "result": { - "enum": [ - "caught", - "missed", - "no seed", - "does not apply", - "build failed" - ] - }, - "violations": { - "type": "integer", - "description": "Rows the seed made violations that the unseeded probe does not have." - }, - "rows": { - "type": "integer", - "description": "Rows that ran under the seed." - }, - "detail": { - "type": "string" - } - }, - "additionalProperties": false - }, - "description": "One outcome per seed this run ran; the probes it skipped have none." - } - }, - "additionalProperties": false -} diff --git a/tools/Prober/Schemas/universe.schema.json b/tools/Prober/Schemas/universe.schema.json deleted file mode 100644 index 776635f3..00000000 --- a/tools/Prober/Schemas/universe.schema.json +++ /dev/null @@ -1,120 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://codometis.typekit/probe/schemas/universe.schema.json", - "title": "Universe", - "description": "State (branches//universe.json): each claim's axes and values, derived from the claims, the axes file and the generated axes.", - "type": "object", - "required": [ - "claims" - ], - "properties": { - "schemaVersion": { - "type": "integer", - "const": 1, - "description": "The schema version. A file or line without it is version 1 (the Python baseline)." - }, - "base": { - "type": "string" - }, - "commit": { - "type": "string" - }, - "generated_versions": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "description": "Axis name to value, both compared ordinally." - }, - "claims": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "axes" - ], - "properties": { - "id": { - "type": "string" - }, - "source": { - "type": "string" - }, - "surfaces": { - "type": "array", - "items": { - "type": "string" - } - }, - "watched": { - "type": "array", - "items": { - "type": "string" - } - }, - "axes": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "exclude": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "string", - "pattern": "^[^=]+=.*$" - }, - "minItems": 2, - "maxItems": 2 - } - }, - "obligations": { - "type": "array", - "description": "written only where an obligation of the claim declares where it does not apply: every obligation's id with its declarations, as claims.json holds them, so the covering generator, status and the gate know the pairs each wants", - "items": { - "type": "object", - "required": [ - "id" - ], - "properties": { - "id": { - "type": "string" - }, - "inapplicable": { - "type": "array", - "items": { - "type": "object", - "required": [ - "axes", - "cite" - ], - "properties": { - "axes": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "cite": { - "type": "string" - } - } - } - } - } - } - } - } - } - } - } -} diff --git a/tools/Prober/Schemas/waivers.schema.json b/tools/Prober/Schemas/waivers.schema.json deleted file mode 100644 index 1b0d99c1..00000000 --- a/tools/Prober/Schemas/waivers.schema.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://codometis.typekit/probe/schemas/waivers.schema.json", - "title": "Waivers", - "description": "Tracked (corpus/waivers.json): a claim's gate obligation met without a probe. Counts only with a reason and a reviewer.", - "type": "array", - "items": { - "type": "object", - "required": [ - "claim", - "reason", - "reviewed_by" - ], - "properties": { - "schemaVersion": { - "type": "integer", - "const": 1, - "description": "The schema version. A file or line without it is version 1 (the Python baseline)." - }, - "claim": { - "type": "string" - }, - "reason": { - "type": "string", - "minLength": 1 - }, - "reviewed_by": { - "type": "string", - "minLength": 1 - }, - "date": { - "type": "string" - } - } - } -} diff --git a/tools/Prober/Seeds.cs b/tools/Prober/Seeds.cs deleted file mode 100644 index 92e5fe86..00000000 --- a/tools/Prober/Seeds.cs +++ /dev/null @@ -1,396 +0,0 @@ -using System.Diagnostics; -using System.Globalization; -using System.Text.Json.Nodes; - -namespace Prober; - -/// -/// Proves that each probe can fail (decision 100). A probe's registry entry names a seeded product defect: -/// a patch against the product files (the replay's include, src here), an environment toggle, -/// or both, and a sentence saying what it breaks. This applies each seed in a scratch worktree of its own, -/// builds, replays that probe there, and counts the seed caught only where a check ends as a violation that the -/// unseeded probe does not have in the same row for the same obligation and that no harness error raised, and, for a -/// seed naming an obligation, only a check naming that obligation (a row without checks is one check, claim). -/// Nothing it runs is recorded as evidence: a seeded run proves the probe, -/// not the product. Never in the checkout and never in the replay's worktree; one build per seed, so it runs -/// locally, not in CI. A missed seed, a probe without one, a seed that no longer applies and a seeded build -/// that fails all fail the command. Without --all it runs only the probes whose seeds identity or product -/// footprint changed since the run that last caught every seed of the probe (design/adr/0107), and says why it skips -/// each of the others; the release runs --all. -/// -internal static class Seeds -{ - /// How one seed ended; is the claim's obligation it breaks, or null for a seed that proves the probe alone. - internal sealed record Outcome(string Probe, string Claim, string? Obligation, string Seed, string Result, int Violations, int Rows, string Detail); - - /// - /// Every seed of a registry entry, each with the obligation it names: the one `seed` (its `obligation`, if - /// any) and each of `seeds` (decision 104). An entry without either has none. - /// - internal static List<(string? Obligation, JsonObject Seed)> SeedsOf(JsonObject entry) - { - var result = new List<(string?, JsonObject)>(); - if (entry["seed"] is JsonObject one) result.Add((Files.Str(one["obligation"]) is { Length: > 0 } o ? o : null, one)); - foreach (var seed in (entry["seeds"] as JsonArray ?? []).OfType()) - result.Add((Files.Str(seed["obligation"]) is { Length: > 0 } o ? o : null, seed)); - return result; - } - - private static string Named(string probe, string? obligation) => obligation is null ? probe : $"{probe}/{obligation}"; - - public static int Run(Context ctx, Args a, IProbeAdapter? adapter = null) - { - var ledger = new Ledger(ctx, a.Get("branch")); - var config = ledger.AxesBlock()["replay"] as JsonObject ?? throw new Refusal("the axes file's block has no \"replay\" configuration"); - adapter ??= new DotNetAdapter(config); - var registry = ledger.Registry(); - var states = ledger.ProbeStates(includeRetired: false).Where(s => !s.Retired).ToList(); - if (a.Get("probe") is { } only) - { - states = [.. states.Where(s => s.Probe == only)]; - if (states.Count == 0) throw new Refusal($"no registered probe {only} that is not retired", 2); - } - var head = ledger.Head; - var home = Replay.Home(ctx, ledger); - var worktree = Path.Combine(home, "seeds"); - var include = Files.Strings(config["include"]).ToArray() is { Length: > 0 } inc ? inc : ["src"]; - // The probes whose every seed the last runs caught, each at the commit and the identity it was caught at; a probe - // named by --probe, and every probe under --all, runs whatever its record says. - var last = Files.ReadJson(ledger.P(LastFile)) as JsonObject; - var record = (last?["probes"] as JsonObject ?? []).Where(kv => kv.Value is JsonObject).ToDictionary(kv => kv.Key, kv => (JsonObject)kv.Value!.DeepClone(), StringComparer.Ordinal); - var identities = states.ToDictionary(s => s.Probe, s => SeedsIdentity(ledger, s.Probe), StringComparer.Ordinal); - var skipped = new List<(string Probe, string Why)>(); - if (!a.Flag("all") && a.Get("probe") is null) - { - var footprints = ledger.Footprints(); - var runs = new List(); - foreach (var s in states) - { - var why = Stale(ledger, s.Probe, registry[s.Probe], record.GetValueOrDefault(s.Probe), identities[s.Probe], footprints); - if (why.Run) runs.Add(s); - else skipped.Add((s.Probe, why.Reason)); - ctx.Out.WriteLine($" {(why.Run ? "runs" : "skips")} {s.Probe}: {why.Reason}"); - } - states = runs; - } - var seedsOf = states.ToDictionary(s => s.Probe, s => SeedsOf(registry[s.Probe]), StringComparer.Ordinal); - // A probe without a seed owes one per obligation its claim lists (decision 104), each an outcome of its own; - // under a claim without obligations, one. - var listed = ledger.Claims().ToDictionary(c => Files.Str(c["id"]), Claim.ObligationIds, StringComparer.Ordinal); - List<(string? Obligation, JsonNode? Seed)> Owed(Ledger.ProbeState s) => - seedsOf[s.Probe].Count > 0 ? [.. seedsOf[s.Probe].Select(x => (x.Obligation, (JsonNode?)x.Seed))] - : listed.GetValueOrDefault(s.Claim) is { } ids && ids is not [Claim.ImplicitObligation] ? [.. ids.Select(id => ((string?)id, (JsonNode?)null))] - : [(null, null)]; - ctx.Out.WriteLine($"prober seeds: {ledger.Branch} at {head[..7]}: {seedsOf.Values.Sum(x => x.Count)} seeds of {states.Count} probes, in {worktree}" + - (skipped.Count > 0 ? $"; {skipped.Count} probes skipped, unchanged since every seed of theirs was caught (--all runs them)" : "")); - if (a.Flag("dry-run")) - { - foreach (var s in states) - { - foreach (var (obligation, seed) in Owed(s)) ctx.Out.WriteLine($" {Named(s.Probe, obligation)}: {Describe(seed)}"); - } - return 0; - } - - var clock = Stopwatch.StartNew(); - var outcomes = new List(); - // Nothing to run: no worktree and no build, only the record carried forward. - if (states.Count > 0) - { - Directory.CreateDirectory(home); - if (!Directory.Exists(worktree)) - { - var added = Git.Run(ledger.Root, "worktree", "add", "--detach", worktree, head); - if (added.ExitCode != 0) throw new Refusal("could not add the scratch worktree: " + added.Stderr.Trim()); - } - var environment = ProbeRun.Environment(ctx, adapter, home); - var results = Directory.CreateTempSubdirectory("prober-seeds-").FullName; - try - { - // The baseline: the checks each probe already fails without a seed, by row and obligation. From the ledger - // where its evidence is current, else from an unseeded run here, so a seed is credited only with violations - // of its own. - var baseline = new Dictionary>(StringComparer.Ordinal); - var unseeded = new List(); - foreach (var s in states) - { - if (!s.Stale && Files.ReadJson(ledger.EvidencePath(s.Probe)) is JsonObject evidence) - baseline[s.Probe] = [.. (evidence["rows"] as JsonObject ?? []).Where(kv => kv.Value is JsonObject) - .SelectMany(kv => Ledger.ChecksOf((JsonObject)kv.Value!).Where(c => c.Category == "violation") - .Select(c => (int.Parse(kv.Key, CultureInfo.InvariantCulture), c.Obligation)))]; - else unseeded.Add(s); - } - ctx.Out.WriteLine($" baseline: {states.Count - unseeded.Count} from the ledger, {unseeded.Count} to run unseeded"); - if (unseeded.Count > 0) - { - Reset(worktree, head); - Replay.Overlay(ledger, config, worktree); - var build = Timed(() => adapter.Build(worktree, environment), out var seconds); - ctx.Out.WriteLine($" unseeded build: {Seconds(seconds)}, {FirstLine(build)}"); - if (build.ExitCode != 0) { ctx.Out.WriteLine(build.Stdout); throw new Refusal("the unseeded build failed; no seed can be judged"); } - foreach (var s in unseeded) - { - var rows = RunProbe(adapter, worktree, s, environment, null, results, "base"); - baseline[s.Probe] = Violating(rows); - ctx.Out.WriteLine($" {s.Probe}: {rows.Count} rows, {baseline[s.Probe].Select(x => x.Row).Distinct().Count()} violations unseeded"); - } - } - - foreach (var s in states) - { - // A seed missing is an outcome "no seed"; a probe with several runs each under its obligation. - foreach (var (obligation, seed) in Owed(s)) - { - var outcome = One(ledger, config, adapter, worktree, head, include, environment, results, s, obligation, seed, baseline[s.Probe]); - outcomes.Add(outcome); - ctx.Out.WriteLine($" {Named(s.Probe, obligation)} ({s.Claim}): {outcome.Result}{(outcome.Detail.Length > 0 ? ", " + outcome.Detail : "")}"); - } - } - } - finally - { - // The worktree is left at HEAD, every seed undone, for the next run to build incrementally. - Reset(worktree, head); - try { Directory.Delete(results, recursive: true); } catch (IOException) { } - } - } - - // A probe is recorded only where every seed it owes was caught, so a missed one runs again next time, whatever changed. - foreach (var s in states) - { - var own = outcomes.Where(o => o.Probe == s.Probe).ToList(); - if (own.Count > 0 && own.All(o => o.Result == "caught")) - record[s.Probe] = new JsonObject { ["identity"] = identities[s.Probe], ["commit"] = head, ["seeds"] = own.Count }; - else record.Remove(s.Probe); - } - foreach (var gone in record.Keys.Where(probe => !registry.ContainsKey(probe)).ToList()) record.Remove(gone); - - Files.WriteJson(ledger.P(LastFile), new JsonObject - { - ["schemaVersion"] = Files.SchemaVersion, ["branch"] = ledger.Branch, ["commit"] = head, - ["seconds"] = Math.Round(clock.Elapsed.TotalSeconds, 1), - ["all"] = a.Flag("all"), - ["probes"] = new JsonObject(record.OrderBy(kv => kv.Key, StringComparer.Ordinal).Select(kv => KeyValuePair.Create(kv.Key, (JsonNode?)kv.Value))), - ["skipped"] = new JsonArray([.. skipped.Select(x => (JsonNode)new JsonObject { ["probe"] = x.Probe, ["reason"] = x.Why })]), - ["seeds"] = new JsonArray([.. outcomes.Select(o => (JsonNode)new JsonObject - { - ["probe"] = o.Probe, ["claim"] = o.Claim, ["obligation"] = o.Obligation, ["seed"] = o.Seed, ["result"] = o.Result, - ["violations"] = o.Violations, ["rows"] = o.Rows, ["detail"] = o.Detail, - })]), - }); - var caught = outcomes.Count(o => o.Result == "caught"); - var counts = outcomes.GroupBy(o => o.Result, StringComparer.Ordinal).OrderBy(g => g.Key, StringComparer.Ordinal).Select(g => $"{g.Count()} {g.Key}"); - ctx.Out.WriteLine($"prober seeds: {string.Join(", ", counts)}, {Seconds(clock.Elapsed.TotalSeconds)} in all"); - return caught == outcomes.Count ? 0 : 1; - } - - public const string LastFile = "seeds-last.json"; - - /// - /// What a probe's seeded runs depend on besides the product: the probe's identity (: - /// its registry entry, the harness and probe sources, its rows, the axes file, the build environment, the tool), - /// its seeds, which that identity leaves out, and the content of each patch they apply. - /// - internal static string SeedsIdentity(Ledger ledger, string probe) - { - using var sha = System.Security.Cryptography.IncrementalHash.CreateHash(System.Security.Cryptography.HashAlgorithmName.SHA256); - void Add(string text) => sha.AppendData(System.Text.Encoding.UTF8.GetBytes(text + "\0")); - Add(ledger.Identity(probe)); - foreach (var (obligation, seed) in SeedsOf(ledger.Registry().GetValueOrDefault(probe) ?? [])) - { - Add(obligation ?? "-"); - Add(Files.Canonical(seed)); - var patch = Files.Str(seed["patch"]); - if (patch.Length > 0 && File.Exists(ledger.CorpusPath(patch))) sha.AppendData(File.ReadAllBytes(ledger.CorpusPath(patch))); - } - return Convert.ToHexStringLower(sha.GetHashAndReset())[..16]; - } - - /// - /// Whether a probe's seeds run without --all, and why: they run unless every seed was caught at a recorded - /// commit, under the seeds identity the probe has now, and neither the product footprint the last replay recorded for - /// it nor a file its seeds patch changed since that commit. A probe without a recorded footprint runs. - /// - internal static (bool Run, string Reason) Stale(Ledger ledger, string probe, JsonObject entry, JsonObject? caught, string identity, Dictionary footprints) - { - if (caught is null) return (true, "no run caught every seed of it yet"); - var commit = Files.Str(caught["commit"]); - var at = commit.Length >= 7 ? commit[..7] : commit; - if (Files.Str(caught["identity"]) != identity) return (true, $"its identity or its seeds changed since they were caught at {at}"); - if (!footprints.ContainsKey(probe)) return (true, "no product footprint recorded for it (a replay records one)"); - if (ledger.FootprintChangedSince(probe, commit, footprints)) return (true, $"its product footprint changed since {at}"); - var patched = SeedsOf(entry).Select(x => Files.Str(x.Seed["patch"])).Where(p => p.Length > 0 && File.Exists(ledger.CorpusPath(p))) - .SelectMany(p => Patched(File.ReadAllText(ledger.CorpusPath(p)))).Distinct(StringComparer.Ordinal).ToArray(); - if (patched.Length > 0 && Git.Run(ledger.Top, ["diff", "--quiet", commit, "HEAD", "--", .. patched]).ExitCode != 0) - return (true, $"a file its seeds patch changed since {at}"); - return (false, $"every seed caught at {at}, and neither its identity nor its product footprint changed since"); - } - - /// The files a patch changes, as its +++ b/ lines name them. - private static IEnumerable Patched(string patch) => - patch.Split('\n').Where(line => line.StartsWith("+++ b/", StringComparison.Ordinal)).Select(line => line["+++ b/".Length..].TrimEnd('\r')); - - /// Applies one probe's seed, builds, runs the probe, and undoes the seed. - private static Outcome One(Ledger ledger, JsonObject config, IProbeAdapter adapter, string worktree, string head, string[] include, - Dictionary environment, string results, Ledger.ProbeState s, string? obligation, JsonNode? seed, - HashSet<(int Row, string Obligation)> baseline) - { - Outcome End(string result, string detail, int violations = 0, int rows = 0) => new(s.Probe, s.Claim, obligation, Describe(seed), result, violations, rows, detail); - - Reset(worktree, head); - Replay.Overlay(ledger, config, worktree); - var (refused, detail, patch, env) = Validate(ledger, include, seed, worktree); - if (refused is not null) return End(refused, detail); - if (patch is not null) - { - var ran = Git.Run(worktree, "apply", "--whitespace=nowarn", patch); - if (ran.ExitCode != 0) return End("does not apply", FirstLine(ran.Stderr)); - } - try - { - var seeded = new Dictionary(environment, StringComparer.Ordinal); - foreach (var (k, v) in env ?? []) seeded[k] = Files.Str(v); - var build = Timed(() => adapter.Build(worktree, seeded), out var buildSeconds); - if (build.ExitCode != 0) return End("build failed", $"the seeded build: {FirstLine(build)}"); - var rows = RunProbe(adapter, worktree, s, environment, env, results, "seed"); - // A check that broke the harness says nothing about the claim, even where it also returned a failure; a - // seed naming an obligation is caught only by a check naming it (decision 104), a seed naming none by any. - var fresh = rows.SelectMany(kv => kv.Value.Checks.Select(c => (Row: kv.Key, Check: c))) - .Where(x => x.Check.CleanViolation && !baseline.Contains((x.Row, x.Check.Obligation))).OrderBy(x => x.Row).ToList(); - var own = fresh.Where(x => obligation is null || x.Check.Obligation == obligation).ToList(); - var caughtRows = own.Select(x => x.Row).Distinct().Count(); - var tally = string.Join(", ", rows.Values.GroupBy(r => r.Category, StringComparer.Ordinal).OrderBy(g => g.Key, StringComparer.Ordinal).Select(g => $"{g.Key} {g.Count()}")); - var broken = rows.Values.Count(r => r.HarnessError); - var harness = broken > 0 ? $"; {broken} ended in a harness error, which is no catch" : ""; - // Violations by checks of other obligations are no catch of this one; say whose they were, a row without checks being `claim`'s. - var others = fresh.Count > own.Count - ? $"; {fresh.Select(x => x.Row).Distinct().Count()} violate by a check naming {string.Join(", ", fresh.Select(x => x.Check.Obligation).Distinct().Order(StringComparer.Ordinal))}, not {obligation}" - : ""; - if (own.Count == 0) return End("missed", $"no row violates that did not already ({rows.Count} rows: {(tally.Length > 0 ? tally : "none ran")}{harness}{others}; build {Seconds(buildSeconds)})", 0, rows.Count); - var example = own[0]; - var named = example.Check.Obligation == Claim.ImplicitObligation && rows[example.Row].Checks.Count == 1 ? "" : " " + example.Check.Obligation; - return End("caught", $"{caughtRows} of {rows.Count} rows violate; e.g. #{example.Row}{named}: {Cut(example.Check.Reason)}", caughtRows, rows.Count); - } - finally - { - if (patch is not null) Git.Run(worktree, "apply", "-R", "--whitespace=nowarn", patch); - } - } - - /// - /// Why a seed cannot be applied in (a result and a detail), or a null result with - /// the patch's full path and the environment toggle to apply. A seed names its defect and a patch, an - /// environment toggle or both; a patch exists in the corpus, touches only the product files and applies. - /// - private static (string? Refused, string Detail, string? Patch, JsonObject? Env) Validate(Ledger ledger, string[] include, JsonNode? seed, string worktree) - { - if (seed is not JsonObject entry) return ("no seed", "its registry entry names none", null, null); - var patch = Files.Str(entry["patch"]); - var env = entry["env"] as JsonObject; - if (Files.Str(entry["defect"]).Length == 0 || (patch.Length == 0 && env is not { Count: > 0 })) - return ("no seed", "a seed names its defect and a patch or an environment toggle", null, null); - if (patch.Length == 0) return (null, "", null, env); - - var file = Path.GetFullPath(ledger.CorpusPath(patch)); - if (!File.Exists(file)) return ("does not apply", $"{patch} is not in the corpus", null, null); - var touched = Git.Run(worktree, "apply", "--numstat", file); - if (touched.ExitCode != 0) return ("does not apply", FirstLine(touched.Stderr), null, null); - var outside = touched.Stdout.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(line => line.Split('\t')[^1].Trim()) - .Where(path => !include.Any(p => path.StartsWith(p.TrimEnd('/') + "/", StringComparison.Ordinal))).ToList(); - if (outside.Count > 0) return ("does not apply", $"it patches {string.Join(", ", outside)}, outside the product files ({string.Join(", ", include)})", null, null); - var check = Git.Run(worktree, "apply", "--check", file); - return check.ExitCode != 0 ? ("does not apply", FirstLine(check.Stderr), null, null) : (null, "", file, env); - } - - /// - /// What `seeds` would refuse before building anything, against this checkout: a probe without a seed, or a - /// seed that does not apply. No build, so the tests run it on every build and a probe registered without a - /// seed, or a product change that leaves a seed behind, fails there and not only at the next local run. - /// - internal static List Problems(Ledger ledger) - { - var config = ledger.AxesBlock()["replay"] as JsonObject ?? []; - var include = Files.Strings(config["include"]).ToArray() is { Length: > 0 } inc ? inc : ["src"]; - var problems = new List(); - // The obligations each claim lists (or its one implicit obligation), and the seeds that name each. - var obligations = ledger.Claims().ToDictionary(c => Files.Str(c["id"]), Claim.ObligationIds, StringComparer.Ordinal); - var seeded = new Dictionary>(StringComparer.Ordinal); - foreach (var (probe, entry) in ledger.Registry()) - { - if (entry["retired"] is JsonValue retired && retired.TryGetValue(out var r) && r) continue; - var claim = Files.Str(entry["claim"]); - var seeds = SeedsOf(entry); - if (seeds.Count == 0) { problems.Add($"{probe}: no seed, its registry entry names none"); continue; } - foreach (var (obligation, seed) in seeds) - { - if (Validate(ledger, include, seed, ledger.Top) is { Refused: { } refused, Detail: var detail }) - problems.Add($"{Named(probe, obligation)}: {refused}, {detail}"); - var ids = obligations.GetValueOrDefault(claim) ?? [Claim.ImplicitObligation]; - // A seed naming no obligation proves the probe; it counts for the claim's one implicit obligation only. - var counts = obligation ?? (ids is [Claim.ImplicitObligation] ? Claim.ImplicitObligation : null); - if (counts is null) continue; - if (!ids.Contains(counts)) { problems.Add($"{Named(probe, obligation)}: names obligation {counts}, which claim {claim} does not list"); continue; } - (seeded.TryGetValue(claim, out var set) ? set : seeded[claim] = new(StringComparer.Ordinal)).Add(counts); - } - } - // Every listed obligation of a claim that has a probe is seeded by some probe of that claim (decision 104); - // the implicit obligation is covered by the rule above that every probe has a seed. - var claimed = ledger.Registry().Values.Select(e => Files.Str(e["claim"])).ToHashSet(StringComparer.Ordinal); - foreach (var (claim, ids) in obligations.OrderBy(kv => kv.Key, StringComparer.Ordinal)) - { - if (ids is [Claim.ImplicitObligation] || !claimed.Contains(claim)) continue; - foreach (var id in ids.Where(id => !(seeded.GetValueOrDefault(claim)?.Contains(id) ?? false))) - problems.Add($"{claim}: obligation {id} has no seed in any of its probes"); - } - return problems; - } - - /// The (row, obligation) of every check that ended as a violation, clean or not: what a seed's violations are measured against. - private static HashSet<(int Row, string Obligation)> Violating(Dictionary rows) => - [.. rows.SelectMany(kv => kv.Value.Checks.Where(c => c.Category == "violation").Select(c => (kv.Key, c.Obligation)))]; - - /// Runs one probe in every one of its variants and reads how each row ended. - private static Dictionary RunProbe(IProbeAdapter adapter, string worktree, Ledger.ProbeState s, Dictionary environment, - JsonObject? seedEnv, string results, string tag) - { - var rows = new Dictionary(); - var index = 0; - foreach (var variant in s.Variants.OfType()) - { - var name = $"{s.Probe}-{tag}-{index}"; - var (_, outcomes) = ProbeRun.Once(adapter, worktree, s.Filter, results, name, name + ".cobertura.xml", ProbeRun.Variant(environment, variant, seedEnv)); - foreach (var (idx, outcome) in outcomes) rows[idx] = outcome; - index++; - } - return rows; - } - - /// Puts the scratch worktree's tracked files back at HEAD; the copied corpus, untracked, stays. - private static void Reset(string worktree, string head) - { - if (Git.Run(worktree, "checkout", "-q", "--detach", head).ExitCode != 0 && Git.Run(worktree, "reset", "-q", "--hard", head).ExitCode != 0) - throw new Refusal($"could not move the scratch worktree {worktree} to {head[..7]}"); - Git.Run(worktree, "reset", "-q", "--hard", head); - } - - private static string Describe(JsonNode? seed) => - seed is JsonObject o - ? string.Join(" + ", new[] { Files.Str(o["patch"]), o["env"] is JsonObject env ? string.Join(",", env.Select(kv => $"{kv.Key}={Files.Str(kv.Value)}")) : "" }.Where(x => x.Length > 0)) - : "(none)"; - - private static Ran Timed(Func run, out double seconds) - { - var clock = Stopwatch.StartNew(); - var ran = run(); - seconds = clock.Elapsed.TotalSeconds; - return ran; - } - - private static string Seconds(double seconds) => seconds.ToString("F1", CultureInfo.InvariantCulture) + " s"; - - private static string Cut(string s) => string.Join(" ", s.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)) is var line && line.Length > 160 ? line[..160] + "..." : line; - - private static string FirstLine(Ran ran) => ProbeRun.FirstLine(ran); - - private static string FirstLine(string text) => text.Trim().Split('\n')[0].Trim(); -} diff --git a/tools/Prober/SourceLint.cs b/tools/Prober/SourceLint.cs deleted file mode 100644 index 6b34502b..00000000 --- a/tools/Prober/SourceLint.cs +++ /dev/null @@ -1,277 +0,0 @@ -using System.Text; -using System.Text.Json.Nodes; -using System.Text.RegularExpressions; - -namespace Prober; - -/// -/// What `register` and `publish` refuse in the staged probe sources before anything is built (decision 103): -/// a Note whose first argument is not a key of the catalogue (NoteKeys.Name, a member the -/// corpus or the staging declares), and a two-argument Skip (the harness's, not LINQ's) whose first -/// argument is not a string literal naming a finding this ledger holds open. The compiler refuses a key -/// outside the catalogue too; this says so by file and line before a build, and holds the finding ids, -/// which stay strings. A check's Inapplicable and Refused cite their reason by a Cite -/// factory with a string literal, and what it names exists: a not-promised entry of the probe's claim (of any -/// claim, for a harness source no one probe owns), a file of the docs, a knowledge entry under design/knowledge/, -/// a record under design/adr/. It reads code with comments and string literals blanked out, so a call written in -/// either does not count. -/// -internal static partial class SourceLint -{ - /// The catalogue's members, as harness/Row.cs declares them. - [GeneratedRegex(@"public\s+static\s+readonly\s+NoteKeys\s+(\w+)\s*=")] - private static partial Regex CatalogueMember(); - - [GeneratedRegex(@"\.\s*(Note|Skip|Inapplicable|Refused)\s*\(")] - private static partial Regex Call(); - - [GeneratedRegex("^Cite\\s*\\.\\s*(NotPromised|Docs|Fact|Record)\\s*\\(\\s*\"([^\"\\\\]*)\"\\s*\\)$")] - private static partial Regex CiteFactory(); - - [GeneratedRegex(@"^NoteKeys\s*\.\s*(\w+)$")] - private static partial Regex CatalogueKey(); - - [GeneratedRegex("^\"([^\"\\\\]*)\"$")] - private static partial Regex PlainLiteral(); - - /// Refuses, naming each file and line, while a staged source breaks a rule; does nothing without a staging. - public static void Refuse(Ledger l, string command) - { - var problems = Problems(l); - if (problems.Count > 0) - throw new Refusal($"{command}: the staged sources make a note, a skip or a citation that is not written out (decision 103):\n " + string.Join("\n ", problems)); - } - - /// Every problem in the staged harness and probe sources, with the finding ids checked against this ledger. - public static List Problems(Ledger l) - { - var staged = Sources(l.StagingDir).ToList(); - if (staged.Count == 0) return []; - var catalogue = new HashSet(StringComparer.Ordinal); - foreach (var file in Sources(l.CorpusDir).Concat(staged)) - foreach (Match m in CatalogueMember().Matches(Mask(File.ReadAllText(file)))) catalogue.Add(m.Groups[1].Value); - var findings = l.Findings(); - // The not-promised ids of each claim, and the claim of each registered probe: a probe's source cites its own - // claim's entries, a harness source that no one probe owns any claim's. - var notPromised = l.Claims().ToDictionary(c => Files.Str(c["id"]), - c => (IReadOnlySet)(c["not-promised"] as JsonArray ?? []).OfType().Select(n => Files.Str(n["id"])).ToHashSet(StringComparer.Ordinal), StringComparer.Ordinal); - // A probe's source is named for it, or for the class its filter selects (*C1Probe). - var claimOf = new Dictionary(StringComparer.Ordinal); - foreach (var (name, entry) in l.Registry()) - { - claimOf[name] = Files.Str(entry["claim"]); - if (Files.Str(entry["filter"]).TrimStart('*') is { Length: > 0 } cls) claimOf.TryAdd(cls, Files.Str(entry["claim"])); - } - var problems = new List(); - foreach (var file in staged) - { - // Relative to the checkout where the state lives in it, else in full (PROBE_STATE_DIR elsewhere). - var path = Path.GetRelativePath(l.Top, file) is var rel && !rel.StartsWith("..", StringComparison.Ordinal) ? rel.Replace('\\', '/') : file; - var cites = new Citations(l.Top, claimOf.TryGetValue(Path.GetFileNameWithoutExtension(file), out var claim) ? claim : null, notPromised); - foreach (var (line, problem, finding) in Scan(File.ReadAllText(file), catalogue, cites.Problem)) - { - if (problem is not null) problems.Add($"{path}:{line}: {problem}"); - else if (finding is not null && Status(findings, finding) is { } status) - problems.Add($"{path}:{line}: Skip(\"{finding}\", ...) names {status}; a skip steps around an open finding only"); - } - } - return problems; - } - - private static string? Status(Dictionary findings, string id) => - !findings.TryGetValue(id, out var f) ? "no finding this ledger has" - : Files.Str(f["status"]) is var status && status != "open" ? $"finding {id}, which is {status}" - : null; - - private static IEnumerable Sources(string root) => - new[] { "harness", "probes" }.Select(sub => Path.Combine(root, sub)).Where(Directory.Exists) - .SelectMany(dir => Directory.EnumerateFiles(dir, "*.cs")).Order(StringComparer.Ordinal); - - /// - /// What a citation of one source may name, and why one does not: takes the factory and its - /// literal and says what is missing, or null. is the claim of the probe the source is, or - /// null for a source that no one probe owns, which may cite any claim's not-promised entry. - /// - internal sealed record Citations(string Top, string? Claim, IReadOnlyDictionary> NotPromised) - { - public string? Problem(string factory, string value) - { - switch (factory) - { - case "NotPromised" when Claim is not null: - return NotPromised.GetValueOrDefault(Claim)?.Contains(value) ?? false ? null : $"names no not-promised entry of its claim {Claim}"; - case "NotPromised": - return NotPromised.Values.Any(ids => ids.Contains(value)) ? null : "names no not-promised entry of any claim"; - case "Docs": - var file = value.Split('#')[0]; - return file.Length > 0 && !file.Contains("..", StringComparison.Ordinal) && File.Exists(Path.Combine(Top, file)) ? null : $"names no file {file} in the repository"; - case "Fact": - return Named(Path.Combine(Top, "design", "knowledge"), value) ? null : "names no entry under design/knowledge/"; - default: - return Named(Path.Combine(Top, "design", "adr"), value) ? null : "names no record under design/adr/"; - } - } - - /// A file of the directory named for the id: KB-031-...md, 0094-...md. - private static bool Named(string directory, string id) => - id.Length > 0 && Directory.Exists(directory) - && Directory.EnumerateFiles(directory, "*.md").Any(f => Path.GetFileName(f).StartsWith(id + "-", StringComparison.Ordinal) || Path.GetFileName(f) == id + ".md"); - } - - /// - /// Each Note, two-argument Skip, and Inapplicable or Refused with an argument, of one - /// source: its line, the rule it breaks (or null), and for a skip written out, the finding it names. - /// says what a citation's factory and literal fail to name; without it only the form is held. - /// - internal static IEnumerable<(int Line, string? Problem, string? Finding)> Scan(string source, IReadOnlySet catalogue, Func? cite = null) - { - var masked = Mask(source); - foreach (Match m in Call().Matches(masked)) - { - var open = m.Index + m.Length - 1; - var arguments = Arguments(masked, source, open); - if (arguments is null) continue; - var line = 1 + masked.AsSpan(0, m.Index).Count('\n'); - var first = arguments.Count > 0 ? arguments[0] : ""; - if (m.Groups[1].Value is "Inapplicable" or "Refused") - { - if (arguments.Count == 0) continue; // an entry's own Refused() of the harness, not a check's - var call = m.Groups[1].Value; - var factory = CiteFactory().Match(first); - if (!factory.Success) yield return (line, $"{call}({Shown(first)}, ...) does not cite by a Cite factory with a string literal", null); - else if (cite?.Invoke(factory.Groups[1].Value, factory.Groups[2].Value) is { } missing) - yield return (line, $"{call}({Shown(first)}, ...) {missing}", null); - } - else if (m.Groups[1].Value == "Note") - { - var key = CatalogueKey().Match(first); - if (!key.Success) yield return (line, $"Note({Shown(first)}, ...) does not name a key of NoteKeys", null); - else if (!catalogue.Contains(key.Groups[1].Value)) yield return (line, $"Note({Shown(first)}, ...) names no key of NoteKeys", null); - } - else if (arguments.Count == 2) - { - var id = PlainLiteral().Match(first); - yield return id.Success ? (line, null, id.Groups[1].Value) : (line, $"Skip({Shown(first)}, ...) does not name its finding as a string literal", null); - } - } - } - - private static string Shown(string argument) => Regex.Replace(argument, @"\s+", " ") is var s && s.Length > 60 ? s[..60] + "..." : s; - - /// The top-level arguments of the call whose parenthesis opens at , from the original text; null if it never closes. - private static List? Arguments(string masked, string source, int open) - { - var arguments = new List(); - var depth = 0; var start = open + 1; - for (var i = open + 1; i < masked.Length; i++) - { - switch (masked[i]) - { - case '(' or '[' or '{': depth++; break; - case ')' or ']' or '}' when depth > 0: depth--; break; - case ',' when depth == 0: arguments.Add(source[start..i].Trim()); start = i + 1; break; - case ')': - var last = source[start..i].Trim(); - if (last.Length > 0 || arguments.Count > 0) arguments.Add(last); - return arguments; - } - } - return null; - } - - /// - /// The source with every comment and the content of every string and character literal blanked out (line - /// breaks kept, so lines still count), and the code of an interpolation hole kept as code. - /// - internal static string Mask(string s) - { - var m = new StringBuilder(s); - Code(s, m, 0, hole: false); - return m.ToString(); - } - - private static void Blank(StringBuilder m, int from, int to) - { - for (var i = from; i < to && i < m.Length; i++) if (m[i] != '\n') m[i] = ' '; - } - - /// Scans code from ; in a hole, returns the index after its closing brace. - private static int Code(string s, StringBuilder m, int i, bool hole) - { - var depth = 0; - while (i < s.Length) - { - var c = s[i]; - if (c == '/' && i + 1 < s.Length && s[i + 1] == '/') - { - var end = s.IndexOf('\n', i); end = end < 0 ? s.Length : end; - Blank(m, i, end); i = end; - } - else if (c == '/' && i + 1 < s.Length && s[i + 1] == '*') - { - var end = s.IndexOf("*/", i + 2, StringComparison.Ordinal); end = end < 0 ? s.Length : end + 2; - Blank(m, i, end); i = end; - } - else if (c == '\'') - { - var j = i + 1; - while (j < s.Length && s[j] != '\'' && s[j] != '\n') j += s[j] == '\\' ? 2 : 1; - Blank(m, i + 1, j); i = j + 1; - } - else if (c is '"' or '$' or '@' && StringStart(s, i) is { } literal) i = String(s, m, i, literal); - else if (hole && c == '{') { depth++; i++; } - else if (hole && c == '}') { if (depth-- == 0) return i + 1; i++; } - else i++; - } - return i; - } - - private sealed record Literal(int Dollars, bool Verbatim, int Quotes, int QuoteAt); - - /// The literal that starts at : its $ count, @, and run of quotes. - private static Literal? StringStart(string s, int i) - { - if (i > 0 && (char.IsLetterOrDigit(s[i - 1]) || s[i - 1] == '_') && s[i] != '"') return null; - var j = i; var dollars = 0; var verbatim = false; - while (j < s.Length && (s[j] == '$' || s[j] == '@')) { if (s[j] == '$') dollars++; else verbatim = true; j++; } - if (j >= s.Length || s[j] != '"') return null; - var quotes = 0; - while (j + quotes < s.Length && s[j + quotes] == '"') quotes++; - return new Literal(dollars, verbatim, quotes >= 3 ? quotes : 1, j); - } - - /// Blanks one literal's content, keeping its holes as code; returns the index after it. - private static int String(string s, StringBuilder m, int i, Literal literal) - { - var j = literal.QuoteAt + literal.Quotes; - if (literal.Quotes >= 3) - { - // A raw literal ends at a run of as many quotes as it opened with. Holes in it hold no note or skip - // (both return nothing), so it is blanked whole. - var close = new string('"', literal.Quotes); - var end = s.IndexOf(close, j, StringComparison.Ordinal); end = end < 0 ? s.Length : end; - Blank(m, j, end); - return end + literal.Quotes; - } - var from = j; - while (j < s.Length) - { - var c = s[j]; - if (literal.Verbatim && c == '"' && j + 1 < s.Length && s[j + 1] == '"') { j += 2; continue; } - if (!literal.Verbatim && c == '\\') { j += 2; continue; } - if (c == '"') { Blank(m, from, j); return j + 1; } - if (literal.Dollars > 0 && c == '{') - { - if (j + 1 < s.Length && s[j + 1] == '{') { j += 2; continue; } - Blank(m, from, j + 1); - j = Code(s, m, j + 1, hole: true); - from = j - 1; - continue; - } - if (!literal.Verbatim && c == '\n') break; - j++; - } - Blank(m, from, j); - return j; - } -} diff --git a/tools/Prober/TryProbe.cs b/tools/Prober/TryProbe.cs deleted file mode 100644 index ba6e36cc..00000000 --- a/tools/Prober/TryProbe.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System.Diagnostics; -using System.Globalization; -using System.Text.Json.Nodes; - -namespace Prober; - -/// -/// try: the writer's loop for one probe. It moves the replay's worktree to HEAD, overlays the corpus and -/// the branch's staging there as the replay does, builds once through the adapter (not with --no-build), -/// and runs the one probe in each of its registry variants; --rows keeps the rows given, run through the -/// harness's PROBE_ROWS one stretch of consecutive rows at a time, within the rows each variant's -/// PROBE_ONLY selects. It prints each run's first line (the verdict's) and how each row ended, from the -/// jsonl the harness writes. It records nothing, no evidence, footprint or run, so nothing under the state -/// directory changes, and takes no round lock, but it holds the replay's worktree () -/// while it runs: a try and a replay of the same checkout never build in it at once, and a try refuses while a -/// replay holds it. Its exit code is the probe's: the first run's that failed, else 0; 1 when the build fails. -/// -internal static class TryProbe -{ - public static int Run(Context ctx, Args a, IProbeAdapter? adapter = null) - { - var ledger = new Ledger(ctx, a.Get("branch")); - var config = ledger.AxesBlock()["replay"] as JsonObject ?? throw new Refusal("the axes file's block has no \"replay\" configuration"); - adapter ??= new DotNetAdapter(config); - var probe = a.Need("probe"); - if (!ledger.Registry().TryGetValue(probe, out var entry)) - throw new Refusal($"no probe {probe} in the registry, tracked or staged; `prober new` stages one", 2); - var claim = Files.Str(entry["claim"]); - var filter = Files.Str(entry["filter"]); - - // What each variant runs: the rows its PROBE_ONLY selects, those of --rows among them, in stretches. - var probeRows = ledger.RowsOf(probe); - var asked = Indices(a.Get("rows"), probeRows.Count); - var plan = new List<(JsonObject Variant, List Rows, List Stretches)>(); - foreach (var variant in Ledger.VariantsOf(entry).OfType()) - { - var only = Files.Str(variant["PROBE_ONLY"]); - var selected = Enumerable.Range(0, probeRows.Count).Where(i => Replay.Selected(Files.StringMap(probeRows[i]?["row"] ?? probeRows[i]), only)).ToList(); - if (asked is null) { plan.Add((variant, selected, [null])); continue; } - var rows = selected.Where(asked.Contains).ToList(); - if (rows.Count > 0) plan.Add((variant, rows, Stretches(rows))); - } - if (asked is not null && asked.Except(plan.SelectMany(p => p.Rows)).ToList() is { Count: > 0 } unrun) - throw new Refusal($"no variant of {probe} runs row(s) {string.Join(", ", unrun)}: each variant's PROBE_ONLY leaves them out", 2); - - var home = Replay.Home(ctx, ledger); - var worktree = Path.Combine(home, "wt"); - var noBuild = a.Flag("no-build"); - if (noBuild && !Directory.Exists(worktree)) throw new Refusal($"--no-build: the replay worktree {worktree} does not exist yet, so nothing is built; run without it", 2); - // The worktree is the replay's: held for the whole try, so neither moves it or builds in it under the other. - using var held = Replay.HoldWorktree(home) - ?? throw new Refusal($"a replay in this checkout is using {worktree}; run `prober try` when it ends"); - var head = ledger.Head; - var warmth = ProbeRun.Ready(ledger, worktree, head); - Replay.Overlay(ledger, config, worktree); - var environment = ProbeRun.Environment(ctx, adapter, home); - ctx.Out.WriteLine($"prober try: {probe} ({claim}) at {head[..7]} in {worktree}; nothing is recorded"); - if (!noBuild) - { - var clock = Stopwatch.StartNew(); - var build = adapter.Build(worktree, environment); - ctx.Out.WriteLine($" build ({warmth}): {Seconds(clock.Elapsed.TotalSeconds)}, {ProbeRun.FirstLine(build)}"); - if (build.ExitCode != 0) { ctx.Out.WriteLine(build.Stdout); return 1; } - } - - // The full logs of each run, kept until the next try; the results and the rows' jsonl are thrown away. - var logs = Path.Combine(home, "try-logs"); - try { if (Directory.Exists(logs)) Directory.Delete(logs, recursive: true); } catch (IOException) { } - var results = Directory.CreateTempSubdirectory("prober-try-").FullName; - var exit = 0; - var tally = new SortedDictionary(StringComparer.Ordinal); - try - { - var index = 0; - foreach (var (variant, rows, stretches) in plan) - { - var label = variant.Count > 0 ? " [" + string.Join(", ", variant.Select(kv => $"{kv.Key}={Files.Str(kv.Value)}")) + "]" : ""; - foreach (var stretch in stretches) - { - var name = $"{probe}-try-{index++}"; - var env = ProbeRun.Variant(environment, variant); - if (stretch is not null) env["PROBE_ROWS"] = stretch; - env["PROBE_LOG_DIR"] = Path.Combine(logs, name); - var clock = Stopwatch.StartNew(); - var (ran, outcomes) = ProbeRun.Once(adapter, worktree, filter, results, name, "", env); - ctx.Out.WriteLine($" {probe}{label}{(stretch is null ? "" : $" rows {stretch}")}: {ProbeRun.FirstLine(ran)} (exit {ran.ExitCode}, {Seconds(clock.Elapsed.TotalSeconds)})"); - foreach (var i in rows.Where(i => stretch is null || Within(i, stretch))) - { - var outcome = outcomes.GetValueOrDefault(i); - var category = outcome?.Category ?? "did not run"; - tally[category] = tally.GetValueOrDefault(category) + 1; - ctx.Out.WriteLine($" #{i} {category}{(outcome is { Category: not "pass", Reason.Length: > 0 } ? ": " + Cut(Unprefixed(outcome.Reason, category)) : "")}"); - } - if (ran.ExitCode != 0 && Directory.Exists(env["PROBE_LOG_DIR"])) ctx.Out.WriteLine($" log: {env["PROBE_LOG_DIR"]}"); - if (ran.ExitCode != 0 && exit == 0) exit = ran.ExitCode; - } - } - } - finally - { - try { Directory.Delete(results, recursive: true); } catch (IOException) { } - } - ctx.Out.WriteLine($"prober try: {probe}: {(tally.Count == 0 ? "no rows" : string.Join(", ", tally.Select(kv => $"{kv.Key} {kv.Value}")))}; exit {exit}"); - return exit; - } - - /// The row indices of --rows (i,j,..., a-b for a stretch), or null for every row; refuses one the claim does not have. - internal static SortedSet? Indices(string? given, int count) - { - if (given is null) return null; - var result = new SortedSet(); - foreach (var part in given.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) - { - var bounds = part.Split('-'); - if (bounds.Length > 2 || !bounds.All(x => int.TryParse(x, NumberStyles.None, CultureInfo.InvariantCulture, out _))) - throw new Refusal($"--rows takes row indices, i,j,... or a-b: not {part}", 2); - var lo = int.Parse(bounds[0], CultureInfo.InvariantCulture); var hi = int.Parse(bounds[^1], CultureInfo.InvariantCulture); - for (var i = lo; i <= hi; i++) result.Add(i); - } - if (result.Count == 0) throw new Refusal("--rows takes row indices, i,j,... or a-b", 2); - if (result.Max >= count) throw new Refusal($"--rows {result.Max}: the claim has {count} row(s), 0 to {count - 1}", 2); - return result; - } - - /// The consecutive stretches of sorted rows, as the harness's PROBE_ROWS spells one: lo-hi. - internal static List Stretches(List rows) - { - var result = new List(); - for (var i = 0; i < rows.Count;) - { - var j = i; - while (j + 1 < rows.Count && rows[j + 1] == rows[j] + 1) j++; - result.Add(string.Create(CultureInfo.InvariantCulture, $"{rows[i]}-{rows[j]}")); - i = j + 1; - } - return result; - } - - private static bool Within(int row, string stretch) - { - var bounds = stretch.Split('-'); - return row >= int.Parse(bounds[0], CultureInfo.InvariantCulture) && row <= int.Parse(bounds[1], CultureInfo.InvariantCulture); - } - - /// A reason without the category the harness puts in front of it (violation:...), which the line already names. - private static string Unprefixed(string reason, string category) => reason.StartsWith(category + ":", StringComparison.Ordinal) ? reason[(category.Length + 1)..] : reason; - - private static string Seconds(double seconds) => seconds.ToString("F1", CultureInfo.InvariantCulture) + " s"; - - private static string Cut(string s) => string.Join(" ", s.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)) is var line && line.Length > 160 ? line[..160] + "..." : line; -} diff --git a/tools/probe/corpus/claims.json b/tools/probe/corpus/claims.json deleted file mode 100644 index 301b96a8..00000000 --- a/tools/probe/corpus/claims.json +++ /dev/null @@ -1,16178 +0,0 @@ -[ - { - "id": "C01-decimal-spelling", - "source": "docs/json.md#decimals-in-one-spelling", - "surfaces": [ - "json" - ], - "axes": { - "converter": [ - "number", - "string" - ], - "input": [ - "7.80", - "7.8000", - "0.00", - "100", - "1E+2", - "max28digits", - "1e-28" - ] - }, - "narrow": { - "shape.wrapped": "Register either one once, and it covers every decimal: nullable ones, dictionary keys, and every value object over a decimal, since its JSON is the underlying type's.", - "input.json": "Both read a number, or a string holding one, and keep the scale they read." - }, - "obligations": [ - { - "id": "smallest-scale", - "sentence": "Two converters write the smallest scale instead:" - }, - { - "id": "number-is-a-number", - "sentence": "**`DecimalNumberJsonConverter`** writes it as a number.", - "axes": { - "converter": [ - "number" - ] - } - }, - { - "id": "string-is-a-string", - "sentence": "**`DecimalStringJsonConverter`** writes it as a string.", - "axes": { - "converter": [ - "string" - ] - } - }, - { - "id": "reads-a-number-or-a-string", - "sentence": "Both read a number, or a string holding one, and keep the scale they read." - }, - { - "id": "keeps-the-scale-it-reads", - "sentence": "Both read a number, or a string holding one, and keep the scale they read." - }, - { - "id": "covers-every-decimal", - "sentence": "Register either one once, and it covers every decimal: nullable ones, dictionary keys, and every value object over a decimal, since its JSON is the underlying type's." - } - ], - "exclude": [ - { - "reason": "A closed set wraps nothing, so it holds no decimal for a decimal converter to write (docs/json.md#decimals-in-one-spelling covers decimals, nullable ones, dictionary keys and value objects over a decimal).", - "pairs": [ - [ - "converter=number", - "shape.kind=closed-set" - ], - [ - "converter=string", - "shape.kind=closed-set" - ], - [ - "input=0.00", - "shape.kind=closed-set" - ], - [ - "input=100", - "shape.kind=closed-set" - ], - [ - "input=1E+2", - "shape.kind=closed-set" - ], - [ - "input=1e-28", - "shape.kind=closed-set" - ], - [ - "input=7.80", - "shape.kind=closed-set" - ], - [ - "input=7.8000", - "shape.kind=closed-set" - ], - [ - "input=max28digits", - "shape.kind=closed-set" - ], - [ - "json.AllowDuplicateProperties=default", - "shape.kind=closed-set" - ], - [ - "json.AllowDuplicateProperties=false", - "shape.kind=closed-set" - ], - [ - "json.AllowOutOfOrderMetadataProperties=default", - "shape.kind=closed-set" - ], - [ - "json.AllowOutOfOrderMetadataProperties=true", - "shape.kind=closed-set" - ], - [ - "json.AllowTrailingCommas=default", - "shape.kind=closed-set" - ], - [ - "json.AllowTrailingCommas=true", - "shape.kind=closed-set" - ], - [ - "json.Converters=JsonStringEnumConverter", - "shape.kind=closed-set" - ], - [ - "json.Converters=StoredJsonConverterFactory", - "shape.kind=closed-set" - ], - [ - "json.Converters=default", - "shape.kind=closed-set" - ], - [ - "json.Converters=own-for-value-object", - "shape.kind=closed-set" - ], - [ - "json.Converters=own-for-wrapped", - "shape.kind=closed-set" - ], - [ - "json.DefaultIgnoreCondition=WhenReading", - "shape.kind=closed-set" - ], - [ - "json.DefaultIgnoreCondition=WhenWriting", - "shape.kind=closed-set" - ], - [ - "json.DefaultIgnoreCondition=WhenWritingDefault", - "shape.kind=closed-set" - ], - [ - "json.DefaultIgnoreCondition=WhenWritingNull", - "shape.kind=closed-set" - ], - [ - "json.DefaultIgnoreCondition=default", - "shape.kind=closed-set" - ], - [ - "json.DictionaryKeyPolicy=CamelCase", - "shape.kind=closed-set" - ], - [ - "json.DictionaryKeyPolicy=KebabCaseUpper", - "shape.kind=closed-set" - ], - [ - "json.DictionaryKeyPolicy=SnakeCaseLower", - "shape.kind=closed-set" - ], - [ - "json.DictionaryKeyPolicy=default", - "shape.kind=closed-set" - ], - [ - "json.DictionaryKeyPolicy=own-upper-case", - "shape.kind=closed-set" - ], - [ - "json.Encoder=UnsafeRelaxedJsonEscaping", - "shape.kind=closed-set" - ], - [ - "json.Encoder=default", - "shape.kind=closed-set" - ], - [ - "json.IgnoreReadOnlyFields=default", - "shape.kind=closed-set" - ], - [ - "json.IgnoreReadOnlyFields=true", - "shape.kind=closed-set" - ], - [ - "json.IgnoreReadOnlyProperties=default", - "shape.kind=closed-set" - ], - [ - "json.IgnoreReadOnlyProperties=true", - "shape.kind=closed-set" - ], - [ - "json.IncludeFields=default", - "shape.kind=closed-set" - ], - [ - "json.IncludeFields=true", - "shape.kind=closed-set" - ], - [ - "json.MaxDepth=3", - "shape.kind=closed-set" - ], - [ - "json.MaxDepth=default", - "shape.kind=closed-set" - ], - [ - "json.NumberHandling=AllowNamedFloatingPointLiterals", - "shape.kind=closed-set" - ], - [ - "json.NumberHandling=AllowReadingFromString", - "shape.kind=closed-set" - ], - [ - "json.NumberHandling=WriteAsString", - "shape.kind=closed-set" - ], - [ - "json.NumberHandling=default", - "shape.kind=closed-set" - ], - [ - "json.PreferredObjectCreationHandling=Populate", - "shape.kind=closed-set" - ], - [ - "json.PreferredObjectCreationHandling=default", - "shape.kind=closed-set" - ], - [ - "json.PropertyNameCaseInsensitive=default", - "shape.kind=closed-set" - ], - [ - "json.PropertyNameCaseInsensitive=true", - "shape.kind=closed-set" - ], - [ - "json.PropertyNamingPolicy=CamelCase", - "shape.kind=closed-set" - ], - [ - "json.PropertyNamingPolicy=KebabCaseLower", - "shape.kind=closed-set" - ], - [ - "json.PropertyNamingPolicy=KebabCaseUpper", - "shape.kind=closed-set" - ], - [ - "json.PropertyNamingPolicy=SnakeCaseLower", - "shape.kind=closed-set" - ], - [ - "json.PropertyNamingPolicy=SnakeCaseUpper", - "shape.kind=closed-set" - ], - [ - "json.PropertyNamingPolicy=default", - "shape.kind=closed-set" - ], - [ - "json.PropertyNamingPolicy=own-upper-case", - "shape.kind=closed-set" - ], - [ - "json.ReadCommentHandling=Skip", - "shape.kind=closed-set" - ], - [ - "json.ReadCommentHandling=default", - "shape.kind=closed-set" - ], - [ - "json.ReferenceHandler=IgnoreCycles", - "shape.kind=closed-set" - ], - [ - "json.ReferenceHandler=Preserve", - "shape.kind=closed-set" - ], - [ - "json.ReferenceHandler=default", - "shape.kind=closed-set" - ], - [ - "json.RespectNullableAnnotations=default", - "shape.kind=closed-set" - ], - [ - "json.RespectNullableAnnotations=true", - "shape.kind=closed-set" - ], - [ - "json.RespectRequiredConstructorParameters=default", - "shape.kind=closed-set" - ], - [ - "json.RespectRequiredConstructorParameters=true", - "shape.kind=closed-set" - ], - [ - "json.TypeInfoResolver=default", - "shape.kind=closed-set" - ], - [ - "json.TypeInfoResolver=default-with-renaming-modifier", - "shape.kind=closed-set" - ], - [ - "json.TypeInfoResolver=source-generated-context", - "shape.kind=closed-set" - ], - [ - "json.TypeInfoResolverChain=context-then-default", - "shape.kind=closed-set" - ], - [ - "json.TypeInfoResolverChain=default", - "shape.kind=closed-set" - ], - [ - "json.UnknownTypeHandling=JsonNode", - "shape.kind=closed-set" - ], - [ - "json.UnknownTypeHandling=default", - "shape.kind=closed-set" - ], - [ - "json.UnmappedMemberHandling=Disallow", - "shape.kind=closed-set" - ], - [ - "json.UnmappedMemberHandling=default", - "shape.kind=closed-set" - ], - [ - "shape.containing=class", - "shape.kind=closed-set" - ], - [ - "shape.containing=generic-type", - "shape.kind=closed-set" - ], - [ - "shape.containing=record-class", - "shape.kind=closed-set" - ], - [ - "shape.containing=record-struct", - "shape.kind=closed-set" - ], - [ - "shape.containing=struct", - "shape.kind=closed-set" - ], - [ - "shape.kind=closed-set", - "shape.position=array-element" - ], - [ - "shape.kind=closed-set", - "shape.position=dictionary-key" - ], - [ - "shape.kind=closed-set", - "shape.position=dictionary-value" - ], - [ - "shape.kind=closed-set", - "shape.position=field" - ], - [ - "shape.kind=closed-set", - "shape.position=immutable-array-element" - ], - [ - "shape.kind=closed-set", - "shape.position=in-patch" - ], - [ - "shape.kind=closed-set", - "shape.position=list-element" - ], - [ - "shape.kind=closed-set", - "shape.position=nullable-property" - ], - [ - "shape.kind=closed-set", - "shape.position=polymorphic-derived-property" - ], - [ - "shape.kind=closed-set", - "shape.position=property" - ], - [ - "shape.kind=closed-set", - "shape.position=read-only-list-element" - ], - [ - "shape.kind=closed-set", - "shape.position=record-ctor-parameter" - ], - [ - "shape.kind=closed-set", - "shape.position=required-init" - ], - [ - "shape.kind=closed-set", - "shape.position=root" - ] - ] - }, - { - "reason": "A value at the root is not held by a containing type, so shape.containing has nothing to apply to.", - "pairs": [ - [ - "shape.containing=class", - "shape.position=root" - ], - [ - "shape.containing=generic-type", - "shape.position=root" - ], - [ - "shape.containing=record-class", - "shape.position=root" - ], - [ - "shape.containing=record-struct", - "shape.position=root" - ], - [ - "shape.containing=struct", - "shape.position=root" - ] - ] - }, - { - "reason": "System.Text.Json serializes a field only under IncludeFields (or [JsonInclude]), so without it the member is in no document and no converter runs in either direction, for the plain type alike.", - "pairs": [ - [ - "json.IncludeFields=default", - "shape.position=field" - ] - ] - }, - { - "reason": "System.Text.Json refuses JsonObjectCreationHandling.Populate together with a reference handler (InvalidOperationException when the options are used; harness/JsonRow.cs runs such a row without Populate), so no row can set both.", - "pairs": [ - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=IgnoreCycles" - ], - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=Preserve" - ] - ] - } - ] - }, - { - "id": "C02-optional-text", - "source": "docs/text.md#required-and-optional", - "surfaces": [ - "text" - ], - "axes": { - "entry": [ - "AsOptionalText", - "AsOptionalIdentifier", - "AsText", - "AsIdentifier" - ], - "input": [ - "null", - "empty", - "spaces", - "tab-newline", - "word", - "word+ZWSP", - "nbsp-only", - "ZWSP-only" - ] - }, - "narrow": { - "shape.kind": "`AsOptionalText()` and `AsOptionalIdentifier()` return a `Result, TextFault>`", - "shape.wrapped": "`AsOptionalText()` and `AsOptionalIdentifier()` return a `Result, TextFault>`", - "text.entry": "`AsOptionalText()` and `AsOptionalIdentifier()` return a `Result, TextFault>`" - }, - "obligations": [ - { - "id": "required-blank-is-an-error", - "sentence": "For a required field, `Blank` is an error.", - "axes": { - "entry": [ - "AsText", - "AsIdentifier" - ], - "input": [ - "null", - "empty", - "spaces", - "tab-newline", - "nbsp-only" - ] - } - }, - { - "id": "optional-blank-is-no-value", - "sentence": "For an optional one, a blank means \"no value\", and `AsOptionalText()` and `AsOptionalIdentifier()` return a `Result, TextFault>`.", - "axes": { - "entry": [ - "AsOptionalText", - "AsOptionalIdentifier" - ], - "input": [ - "null", - "empty", - "spaces", - "tab-newline", - "nbsp-only" - ] - } - }, - { - "id": "hidden-is-an-error-in-both", - "sentence": "`Hidden` is an error in both cases.", - "axes": { - "input": [ - "word+ZWSP", - "ZWSP-only" - ] - } - }, - { - "id": "null-is-blank", - "sentence": "A null string is `Blank`.", - "axes": { - "input": [ - "null" - ] - } - } - ], - "not-promised": [ - { - "id": "as-text-refused-in-globalization-invariant-mode", - "source": "docs/text.md#globalization-invariant-mode", - "sentence": "So `AsText()` throws `PlatformNotSupportedException` there, on its first call, whatever the string.", - "axes": { - "text.culture": [ - "globalization-invariant" - ], - "entry": [ - "AsText", - "AsOptionalText" - ] - } - } - ], - "exclude": [ - { - "reason": "Not promised (as-text-refused-in-globalization-invariant-mode): `AsText()` throws `PlatformNotSupportedException` in globalization-invariant mode (docs/text.md#globalization-invariant-mode), and `AsOptionalText()`, which calls it, throws with it, so C02OptionalTextProbe checks that refusal without credit and runs the row on the identifier counterpart; no row holds the pair.", - "pairs": [ - [ - "entry=AsText", - "text.culture=globalization-invariant" - ], - [ - "entry=AsOptionalText", - "text.culture=globalization-invariant" - ] - ] - } - ] - }, - { - "id": "C03-composed-faults", - "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#faults-of-several-types", - "surfaces": [ - "http" - ], - "axes": { - "arity": [ - "2", - "3" - ], - "error": [ - "first-set", - "last-set", - "with-detail", - "with-rule" - ] - }, - "obligations": [ - { - "id": "each-error-own-set", - "sentence": "Each error is answered as its own set maps it." - }, - { - "id": "detail-and-rule-intact", - "sentence": "`Fault` (from CodoMetis.TypeKit) holds one error of either set, with its detail and rule intact.", - "axes": { - "error": [ - "with-detail", - "with-rule" - ] - } - }, - { - "id": "statuses-ascending", - "sentence": "So its operation lists the statuses of both sets, in ascending order, with the problem types of both where they share a status." - }, - { - "id": "shared-status-types", - "sentence": "So its operation lists the statuses of both sets, in ascending order, with the problem types of both where they share a status." - }, - { - "id": "unmapped-set-stops-start", - "sentence": "The host doesn't start while either set is unmapped." - } - ], - "exclude": [ - { - "reason": "System.Text.Json refuses PreferredObjectCreationHandling.Populate together with a ReferenceHandler on the options object; JsonRow substitutes the default, so the pair never applies.", - "pairs": [ - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=Preserve" - ], - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=IgnoreCycles" - ] - ] - } - ] - }, - { - "id": "C04-without-faults", - "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#leaving-out-what-a-route-cannot-answer", - "surfaces": [ - "http" - ], - "axes": { - "scope": [ - "endpoint", - "group", - "controller" - ], - "combined": [ - "alone", - "with-WithFaultStatus-on-group" - ] - }, - "obligations": [ - { - "id": "doc-omits-left-out", - "sentence": "The operation then lists no status and no problem type for a member left out." - }, - { - "id": "reporting-left-out-throws", - "sentence": "Reporting a member left out anyway throws an `InvalidOperationException` naming the endpoint and the member, rather than answering with a status the document doesn't list." - }, - { - "id": "narrows-wider-status", - "sentence": "If a group gives `NotFound` a status and one endpoint inside leaves `NotFound` out, that endpoint drops it, and the group's other endpoints keep the status.", - "axes": { - "scope": [ - "endpoint", - "controller" - ], - "combined": [ - "with-WithFaultStatus-on-group" - ] - } - }, - { - "id": "left-out-everywhere-stops-start", - "sentence": "**These stop the host from starting**, as for a status: a value or a name no member has, a status that every endpoint it's set on leaves out (on the endpoint itself, or on a group whose endpoints all leave the member out), and a group or a controller none of whose endpoints answers with the type.", - "axes": { - "scope": [ - "group" - ], - "combined": [ - "with-WithFaultStatus-on-group" - ] - } - } - ], - "not-promised": [ - { - "id": "controller-scope-is-mvc", - "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#leaving-out-what-a-route-cannot-answer", - "sentence": "**In MVC**, an action or a controller says it with `[WithoutFaults(nameof(AccountFault.NotFound))]`.", - "axes": { - "scope": [ - "controller" - ], - "http.host": [ - "minimal", - "route-group" - ] - } - } - ], - "exclude": [ - { - "reason": "A minimal-API host has no controller to leave a member out on: the controller scope is MVC's (not-promised controller-scope-is-mvc).", - "pairs": [ - [ - "scope=controller", - "http.host=minimal" - ], - [ - "scope=controller", - "http.host=route-group" - ] - ] - }, - { - "reason": "System.Text.Json refuses PreferredObjectCreationHandling.Populate together with a ReferenceHandler on the options object; JsonRow substitutes the default, so the pair never applies.", - "pairs": [ - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=Preserve" - ], - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=IgnoreCycles" - ] - ] - } - ] - }, - { - "id": "C05-with-rule", - "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#the-rule-that-refused", - "surfaces": [ - "http" - ], - "axes": { - "ruletype": [ - "enum", - "closed-set", - "nested-enum", - "flags-combination", - "undefined-cast" - ], - "qualified": [ - "bare", - "QualifiedFaultNames" - ] - }, - "obligations": [ - { - "id": "rule-qualified", - "sentence": "The problem carries it as `rule`, qualified with its type's name, so two types' `Blank` can be told apart:", - "axes": { - "ruletype": [ - "enum", - "closed-set" - ] - } - }, - { - "id": "nested-chain", - "sentence": "A rule of a nested type is named after its nesting chain, whatever `QualifiedFaultNames` says.", - "axes": { - "ruletype": [ - "nested-enum" - ] - } - }, - { - "id": "stays-the-errors", - "sentence": "The status, the mapping and the document stay the error's.", - "axes": { - "ruletype": [ - "enum", - "closed-set", - "nested-enum" - ] - } - }, - { - "id": "unnamed-value-throws", - "sentence": "A value no member names, such as a combination of flags or a cast number, throws where it's given.", - "axes": { - "ruletype": [ - "flags-combination", - "undefined-cast" - ] - } - }, - { - "id": "no-namespace", - "sentence": "**The name carries no namespace**, which would publish your application's structure to every client.", - "axes": { - "ruletype": [ - "enum", - "closed-set", - "nested-enum" - ] - } - } - ], - "exclude": [ - { - "reason": "System.Text.Json refuses PreferredObjectCreationHandling.Populate together with a ReferenceHandler on the options object; JsonRow substitutes the default, so the pair never applies.", - "pairs": [ - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=Preserve" - ], - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=IgnoreCycles" - ] - ] - } - ] - }, - { - "id": "C06-fault-kinds", - "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#a-closed-set-as-the-fault-type", - "surfaces": [ - "http" - ], - "axes": { - "kind": [ - "PreconditionFailed", - "Malformed", - "Invalid", - "Unavailable" - ] - }, - "obligations": [ - { - "id": "kind-gives-status", - "sentence": "The kind gives the status:" - }, - { - "id": "description-as-detail", - "sentence": "The description becomes the problem's `detail`, so a client can show it." - }, - { - "id": "no-description-no-detail", - "sentence": "A member without one has no `detail`." - }, - { - "id": "member-must-have-kind", - "sentence": "**A member must have a kind.** A member whose kind is `default`, which is no kind, fails this call." - } - ], - "exclude": [ - { - "reason": "System.Text.Json refuses PreferredObjectCreationHandling.Populate together with a ReferenceHandler on the options object; JsonRow substitutes the default, so the pair never applies.", - "pairs": [ - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=Preserve" - ], - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=IgnoreCycles" - ] - ] - } - ] - }, - { - "id": "C07-fault-responses-blanket", - "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#every-fault-set-at-once", - "surfaces": [ - "http" - ], - "axes": { - "mapping": [ - "by-kind", - "own-IFault-mapping" - ], - "set": [ - "fault-set", - "own-mapped-set", - "enum-unmapped", - "member-without-kind", - "mapping-throws", - "status-not-error" - ] - }, - "obligations": [ - { - "id": "by-kind", - "sentence": "every fault set an endpoint answers with, by its kind", - "axes": { - "mapping": [ - "by-kind" - ] - } - }, - { - "id": "own-mapping", - "sentence": "The mapping can be your own. It reads each member's kind and description through `IFault`:", - "axes": { - "mapping": [ - "own-IFault-mapping" - ] - } - }, - { - "id": "own-set-keeps-mapping", - "sentence": "A set mapped on its own keeps its mapping.", - "axes": { - "set": [ - "own-mapped-set" - ] - } - }, - { - "id": "enum-needs-own-mapping", - "sentence": "Neither has a kind, and the host doesn't start while an endpoint answers with one that's unmapped.", - "axes": { - "set": [ - "enum-unmapped" - ] - } - }, - { - "id": "member-without-kind-stops-start", - "sentence": "A member with no kind stops the start, and so does one the mapping throws for or gives a status that isn't an error.", - "axes": { - "set": [ - "member-without-kind" - ] - } - }, - { - "id": "mapping-refusal-stops-start", - "sentence": "A member with no kind stops the start, and so does one the mapping throws for or gives a status that isn't an error.", - "axes": { - "set": [ - "mapping-throws", - "status-not-error" - ] - } - } - ], - "exclude": [ - { - "reason": "System.Text.Json refuses PreferredObjectCreationHandling.Populate together with a ReferenceHandler on the options object; JsonRow substitutes the default, so the pair never applies.", - "pairs": [ - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=Preserve" - ], - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=IgnoreCycles" - ] - ] - } - ] - }, - { - "id": "C08-same-name-warning", - "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#faults-of-several-types", - "surfaces": [ - "http" - ], - "axes": { - "naming": [ - "bare", - "qualified" - ], - "arity": [ - "2", - "3" - ], - "leave-out": [ - "none", - "WithoutFaults-one" - ] - }, - "obligations": [ - { - "id": "bare-names-warn", - "sentence": "With bare names, the default, that's `AccountFault.NotFound` beside an `EditFault.NotFound`.", - "axes": { - "naming": [ - "bare" - ] - } - }, - { - "id": "qualified-chain-warns", - "sentence": "With qualified names, it's two sets with one nesting chain in two namespaces, since the name carries no namespace.", - "axes": { - "naming": [ - "qualified" - ] - } - }, - { - "id": "warning-names-both", - "sentence": "A client reads the two errors as one, so the host logs a warning at startup naming both.", - "axes": { - "leave-out": [ - "none" - ] - } - }, - { - "id": "qualified-names-fix", - "sentence": "Fix it with `QualifiedFaultNames`, a rename, or by leaving one out of the route", - "axes": { - "naming": [ - "qualified" - ] - } - }, - { - "id": "leave-one-out-fixes", - "sentence": "Fix it with `QualifiedFaultNames`, a rename, or by leaving one out of the route", - "axes": { - "leave-out": [ - "WithoutFaults-one" - ] - } - } - ], - "exclude": [ - { - "reason": "System.Text.Json refuses PreferredObjectCreationHandling.Populate together with a ReferenceHandler on the options object; JsonRow substitutes the default, so the pair never applies.", - "pairs": [ - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=Preserve" - ], - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=IgnoreCycles" - ] - ] - } - ] - }, - { - "id": "C09-base-record-equality", - "source": "CHANGELOG.md#2.1.0-fixed", - "surfaces": [ - "generation" - ], - "axes": { - "base-equality": [ - "none", - "override-Equals", - "override-GetHashCode", - "explicit-IEquatable" - ], - "collection": [ - "HashSet", - "Dictionary-key", - "Distinct" - ] - }, - "narrow": { - "shape.position": "Over a base record, a value object's equality is its wrapped value's, in a collection of the base too, whatever equality the base declares.", - "shape.containing": "Over a base record, a value object's equality is its wrapped value's, in a collection of the base too, whatever equality the base declares." - }, - "obligations": [ - { - "id": "equal-by-the-wrapped-value", - "sentence": "Over a base record, a value object's equality is its wrapped value's" - }, - { - "id": "in-a-collection-of-the-base", - "sentence": "in a collection of the base too, whatever equality the base declares." - } - ], - "exclude": [ - { - "reason": "A record struct or a closed set has no base record: C# gives a struct no base type, and a closed set is a sealed class that is never a record (CMTK1020).", - "pairs": [ - [ - "base-equality=explicit-IEquatable", - "shape.kind=closed-set" - ], - [ - "base-equality=explicit-IEquatable", - "shape.kind=record-struct" - ], - [ - "base-equality=explicit-IEquatable", - "shape.kind=validated-struct" - ], - [ - "base-equality=none", - "shape.kind=closed-set" - ], - [ - "base-equality=none", - "shape.kind=record-struct" - ], - [ - "base-equality=none", - "shape.kind=validated-struct" - ], - [ - "base-equality=override-Equals", - "shape.kind=closed-set" - ], - [ - "base-equality=override-Equals", - "shape.kind=record-struct" - ], - [ - "base-equality=override-Equals", - "shape.kind=validated-struct" - ], - [ - "base-equality=override-GetHashCode", - "shape.kind=closed-set" - ], - [ - "base-equality=override-GetHashCode", - "shape.kind=record-struct" - ], - [ - "base-equality=override-GetHashCode", - "shape.kind=validated-struct" - ], - [ - "collection=Dictionary-key", - "shape.kind=closed-set" - ], - [ - "collection=Dictionary-key", - "shape.kind=record-struct" - ], - [ - "collection=Dictionary-key", - "shape.kind=validated-struct" - ], - [ - "collection=Distinct", - "shape.kind=closed-set" - ], - [ - "collection=Distinct", - "shape.kind=record-struct" - ], - [ - "collection=Distinct", - "shape.kind=validated-struct" - ], - [ - "collection=HashSet", - "shape.kind=closed-set" - ], - [ - "collection=HashSet", - "shape.kind=record-struct" - ], - [ - "collection=HashSet", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=base-depth-1", - "shape.kind=closed-set" - ], - [ - "shape.inheritance=base-depth-1", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-depth-1", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=base-depth-2", - "shape.kind=closed-set" - ], - [ - "shape.inheritance=base-depth-2", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-depth-2", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=base-depth-3", - "shape.kind=closed-set" - ], - [ - "shape.inheritance=base-depth-3", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-depth-3", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.kind=closed-set" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.kind=closed-set" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=none", - "shape.kind=closed-set" - ], - [ - "shape.inheritance=none", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=none", - "shape.kind=validated-struct" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.kind=closed-set", - "shape.site=in-generic-type" - ], - [ - "shape.kind=closed-set", - "shape.site=nested-in-class" - ], - [ - "shape.kind=closed-set", - "shape.site=nested-in-struct" - ], - [ - "shape.kind=closed-set", - "shape.site=nested-two-levels" - ], - [ - "shape.kind=closed-set", - "shape.site=top-level" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=DateOnly" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=DateTime" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=Guid" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=TimeOnly" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=TimeSpan" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=Uri" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=bool" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=byte" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=char" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=decimal" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=double" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=enum" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=flags-enum" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=int" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=long" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=own-record-class" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=string" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=ulong" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.kind=record-struct", - "shape.site=in-generic-type" - ], - [ - "shape.kind=record-struct", - "shape.site=nested-in-class" - ], - [ - "shape.kind=record-struct", - "shape.site=nested-in-struct" - ], - [ - "shape.kind=record-struct", - "shape.site=nested-two-levels" - ], - [ - "shape.kind=record-struct", - "shape.site=top-level" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=DateOnly" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=DateTime" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=Guid" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=TimeOnly" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=TimeSpan" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=Uri" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=bool" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=byte" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=char" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=decimal" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=double" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=enum" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=flags-enum" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=int" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=long" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=own-record-class" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=string" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=ulong" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.kind=validated-struct", - "shape.site=in-generic-type" - ], - [ - "shape.kind=validated-struct", - "shape.site=nested-in-class" - ], - [ - "shape.kind=validated-struct", - "shape.site=nested-in-struct" - ], - [ - "shape.kind=validated-struct", - "shape.site=nested-two-levels" - ], - [ - "shape.kind=validated-struct", - "shape.site=top-level" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=DateOnly" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=DateTime" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=Guid" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=TimeOnly" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=TimeSpan" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=Uri" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=bool" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=byte" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=char" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=decimal" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=double" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=enum" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=flags-enum" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=int" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=long" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=own-record-class" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=string" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=ulong" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=equality-operator (CMTK1011)" - ] - ] - }, - { - "reason": "A value object nested in a generic type fails the build with CMTK1005 (Generators README, Build errors), so it has no generated equality or parsing to check.", - "pairs": [ - [ - "base-equality=explicit-IEquatable", - "shape.site=in-generic-type" - ], - [ - "base-equality=none", - "shape.site=in-generic-type" - ], - [ - "base-equality=override-Equals", - "shape.site=in-generic-type" - ], - [ - "base-equality=override-GetHashCode", - "shape.site=in-generic-type" - ], - [ - "collection=Dictionary-key", - "shape.site=in-generic-type" - ], - [ - "collection=Distinct", - "shape.site=in-generic-type" - ], - [ - "collection=HashSet", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=base-depth-1", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=base-depth-2", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=base-depth-3", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=none", - "shape.site=in-generic-type" - ], - [ - "shape.kind=sealed-record-class", - "shape.site=in-generic-type" - ], - [ - "shape.kind=validated-class", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.site=in-generic-type" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=DateOnly" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=DateTime" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=Guid" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=TimeOnly" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=TimeSpan" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=Uri" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=bool" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=byte" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=char" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=decimal" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=double" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=enum" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=flags-enum" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=int" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=long" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=own-record-class" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=string" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.site=in-generic-type" - ] - ] - }, - { - "reason": "The claim is about a value object over a base record; with no base record it states nothing.", - "pairs": [ - [ - "base-equality=explicit-IEquatable", - "shape.inheritance=none" - ], - [ - "base-equality=none", - "shape.inheritance=none" - ], - [ - "base-equality=override-Equals", - "shape.inheritance=none" - ], - [ - "base-equality=override-GetHashCode", - "shape.inheritance=none" - ], - [ - "collection=Dictionary-key", - "shape.inheritance=none" - ], - [ - "collection=Distinct", - "shape.inheritance=none" - ], - [ - "collection=HashSet", - "shape.inheritance=none" - ], - [ - "shape.inheritance=none", - "shape.kind=sealed-record-class" - ], - [ - "shape.inheritance=none", - "shape.kind=validated-class" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.inheritance=none", - "shape.site=nested-in-class" - ], - [ - "shape.inheritance=none", - "shape.site=nested-in-struct" - ], - [ - "shape.inheritance=none", - "shape.site=nested-two-levels" - ], - [ - "shape.inheritance=none", - "shape.site=top-level" - ], - [ - "shape.inheritance=none", - "shape.wrapped=DateOnly" - ], - [ - "shape.inheritance=none", - "shape.wrapped=DateTime" - ], - [ - "shape.inheritance=none", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.inheritance=none", - "shape.wrapped=Guid" - ], - [ - "shape.inheritance=none", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.inheritance=none", - "shape.wrapped=TimeOnly" - ], - [ - "shape.inheritance=none", - "shape.wrapped=TimeSpan" - ], - [ - "shape.inheritance=none", - "shape.wrapped=Uri" - ], - [ - "shape.inheritance=none", - "shape.wrapped=bool" - ], - [ - "shape.inheritance=none", - "shape.wrapped=byte" - ], - [ - "shape.inheritance=none", - "shape.wrapped=char" - ], - [ - "shape.inheritance=none", - "shape.wrapped=decimal" - ], - [ - "shape.inheritance=none", - "shape.wrapped=double" - ], - [ - "shape.inheritance=none", - "shape.wrapped=enum" - ], - [ - "shape.inheritance=none", - "shape.wrapped=flags-enum" - ], - [ - "shape.inheritance=none", - "shape.wrapped=int" - ], - [ - "shape.inheritance=none", - "shape.wrapped=long" - ], - [ - "shape.inheritance=none", - "shape.wrapped=own-record-class" - ], - [ - "shape.inheritance=none", - "shape.wrapped=string" - ], - [ - "shape.inheritance=none", - "shape.wrapped=ulong" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=equality-operator (CMTK1011)" - ] - ] - }, - { - "reason": "A declaration of a refused kind fails the build (CMTK1005, CMTK1006), so it is generated into nothing; .claude/probe-axes.md: shape.refused-kind is taken only where the refusal is the claim.", - "pairs": [ - [ - "base-equality=explicit-IEquatable", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "base-equality=explicit-IEquatable", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "base-equality=explicit-IEquatable", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "base-equality=explicit-IEquatable", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "base-equality=none", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "base-equality=none", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "base-equality=none", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "base-equality=none", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "base-equality=override-Equals", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "base-equality=override-Equals", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "base-equality=override-Equals", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "base-equality=override-Equals", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "base-equality=override-GetHashCode", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "base-equality=override-GetHashCode", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "base-equality=override-GetHashCode", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "base-equality=override-GetHashCode", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "collection=Dictionary-key", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "collection=Dictionary-key", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "collection=Dictionary-key", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "collection=Dictionary-key", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "collection=Distinct", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "collection=Distinct", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "collection=Distinct", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "collection=Distinct", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "collection=HashSet", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "collection=HashSet", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "collection=HashSet", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "collection=HashSet", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=ulong" - ], - [ - "base-equality=explicit-IEquatable", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "base-equality=explicit-IEquatable", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "base-equality=explicit-IEquatable", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "base-equality=explicit-IEquatable", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "base-equality=explicit-IEquatable", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "base-equality=none", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "base-equality=none", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "base-equality=none", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "base-equality=none", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "base-equality=none", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "base-equality=override-Equals", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "base-equality=override-Equals", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "base-equality=override-Equals", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "base-equality=override-Equals", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "base-equality=override-Equals", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "base-equality=override-GetHashCode", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "base-equality=override-GetHashCode", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "base-equality=override-GetHashCode", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "base-equality=override-GetHashCode", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "base-equality=override-GetHashCode", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "collection=Dictionary-key", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "collection=Dictionary-key", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "collection=Dictionary-key", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "collection=Dictionary-key", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "collection=Dictionary-key", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "collection=Distinct", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "collection=Distinct", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "collection=Distinct", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "collection=Distinct", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "collection=Distinct", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "collection=HashSet", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "collection=HashSet", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "collection=HashSet", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "collection=HashSet", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "collection=HashSet", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=ulong" - ] - ] - } - ] - }, - { - "id": "C10-preserve-guard", - "source": "src/CodoMetis.TypeKit.Generators/README.md#json", - "surfaces": [ - "json" - ], - "axes": { - "handler": [ - "Preserve", - "IgnoreCycles", - "none" - ] - }, - "obligations": [ - { - "id": "refused-where-references-could-be-written", - "sentence": "**Preserved references** (`ReferenceHandler.Preserve`) throw a `NotSupportedException` where the serializer could write `$id` or `$ref` inside the underlying value.", - "axes": { - "handler": [ - "Preserve" - ], - "shape.wrapped": [ - "own-record-class" - ] - } - }, - { - "id": "value-converters-unaffected", - "sentence": "These are unaffected: a value the serializer writes with a value converter (a number, a string, a `Guid`, a `Uri`)", - "axes": { - "handler": [ - "Preserve" - ] - } - }, - { - "id": "own-converters-unaffected", - "sentence": "anything a converter of your own writes, on the options or on one property", - "axes": { - "json.Converters": [ - "own-for-wrapped", - "own-for-value-object" - ] - } - }, - { - "id": "ignore-cycles-unaffected", - "sentence": "a struct of such values, and `ReferenceHandler.IgnoreCycles`.", - "axes": { - "handler": [ - "IgnoreCycles" - ] - } - } - ], - "exclude": [ - { - "reason": "handler is this claim's name for JsonSerializerOptions.ReferenceHandler (Preserve, IgnoreCycles, none = default), the same setting as json.ReferenceHandler; one setting cannot hold two values in one row.", - "pairs": [ - [ - "handler=IgnoreCycles", - "json.ReferenceHandler=Preserve" - ], - [ - "handler=IgnoreCycles", - "json.ReferenceHandler=default" - ], - [ - "handler=Preserve", - "json.ReferenceHandler=IgnoreCycles" - ], - [ - "handler=Preserve", - "json.ReferenceHandler=default" - ], - [ - "handler=none", - "json.ReferenceHandler=IgnoreCycles" - ], - [ - "handler=none", - "json.ReferenceHandler=Preserve" - ] - ] - }, - { - "reason": "A closed set wraps nothing (Generators README#closed-sets: a sealed class whose instances are its members), so it has no shape.wrapped.", - "pairs": [ - [ - "shape.kind=closed-set", - "shape.wrapped=DateOnly" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=DateTime" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=Guid" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=TimeOnly" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=TimeSpan" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=Uri" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=bool" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=byte" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=char" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=decimal" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=double" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=enum" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=flags-enum" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=int" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=long" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=own-record-class" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=string" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=ulong" - ] - ] - }, - { - "reason": "A value at the root is not held by a containing type, so shape.containing has nothing to apply to.", - "pairs": [ - [ - "shape.containing=class", - "shape.position=root" - ], - [ - "shape.containing=generic-type", - "shape.position=root" - ], - [ - "shape.containing=record-class", - "shape.position=root" - ], - [ - "shape.containing=record-struct", - "shape.position=root" - ], - [ - "shape.containing=struct", - "shape.position=root" - ] - ] - }, - { - "reason": "System.Text.Json refuses JsonObjectCreationHandling.Populate together with a reference handler (InvalidOperationException when the options are used; harness/JsonRow.cs runs such a row without Populate), so no row can set both.", - "pairs": [ - [ - "handler=IgnoreCycles", - "json.PreferredObjectCreationHandling=Populate" - ], - [ - "handler=Preserve", - "json.PreferredObjectCreationHandling=Populate" - ], - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=IgnoreCycles" - ], - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=Preserve" - ] - ] - } - ] - }, - { - "id": "C11-parse-shapes-build", - "source": "CHANGELOG.md#2.1.0-fixed", - "surfaces": [ - "text", - "generation" - ], - "axes": { - "parse-shape": [ - "implicit-result", - "ref-text", - "abstract-string-ctor", - "ordinary" - ] - }, - "narrow": { - "shape.position": "A wrapped type's `Parse` whose result can't be assigned to it, or that takes its text by `ref`, or an abstract class's string constructor, no longer breaks the build; the value object has no parsing.", - "shape.containing": "A wrapped type's `Parse` whose result can't be assigned to it, or that takes its text by `ref`, or an abstract class's string constructor, no longer breaks the build; the value object has no parsing." - }, - "obligations": [ - { - "id": "implicit-result", - "sentence": "A wrapped type's `Parse` whose result can't be assigned to it", - "axes": { - "parse-shape": [ - "implicit-result" - ] - } - }, - { - "id": "ref-text", - "sentence": "or that takes its text by `ref`", - "axes": { - "parse-shape": [ - "ref-text" - ] - } - }, - { - "id": "abstract-string-constructor", - "sentence": "or an abstract class's string constructor, no longer breaks the build; the value object has no parsing.", - "axes": { - "parse-shape": [ - "abstract-string-ctor" - ] - } - }, - { - "id": "parsing-where-it-can-be-parsed", - "sentence": "`IParsable`/`ISpanParsable`/`IUtf8SpanParsable`, formatting in the invariant culture", - "axes": { - "parse-shape": [ - "ordinary" - ] - }, - "inapplicable": [ - { - "axes": { - "shape.wrapped": [ - "Uri", - "enum", - "flags-enum", - "NodaTime.Instant" - ], - "text.entry": [ - "ISpanParsable" - ] - }, - "cite": "not-promised:no-span-parsing-without-the-underlying-types" - } - ] - } - ], - "not-promised": [ - { - "id": "no-span-parsing-without-the-underlying-types", - "source": "src/CodoMetis.TypeKit.Generators/README.md#what-is-generated", - "sentence": "A `Uri`, an enum and a NodaTime type have neither.", - "axes": { - "shape.wrapped": [ - "Uri", - "enum", - "flags-enum", - "NodaTime.Instant" - ], - "text.entry": [ - "ISpanParsable" - ] - } - } - ], - "exclude": [ - { - "reason": "A closed set wraps no type, so it has no wrapped type's Parse to take for its parser.", - "pairs": [ - [ - "parse-shape=abstract-string-ctor", - "shape.kind=closed-set" - ], - [ - "parse-shape=implicit-result", - "shape.kind=closed-set" - ], - [ - "parse-shape=ordinary", - "shape.kind=closed-set" - ], - [ - "parse-shape=ref-text", - "shape.kind=closed-set" - ], - [ - "process.converter-lookup=none", - "shape.kind=closed-set" - ], - [ - "process.converter-lookup=wrapped-type-converter-looked-up-first", - "shape.kind=closed-set" - ], - [ - "shape.inheritance=base-depth-1", - "shape.kind=closed-set" - ], - [ - "shape.inheritance=base-depth-2", - "shape.kind=closed-set" - ], - [ - "shape.inheritance=base-depth-3", - "shape.kind=closed-set" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.kind=closed-set" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.kind=closed-set" - ], - [ - "shape.inheritance=none", - "shape.kind=closed-set" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.kind=closed-set", - "shape.site=in-generic-type" - ], - [ - "shape.kind=closed-set", - "shape.site=nested-in-class" - ], - [ - "shape.kind=closed-set", - "shape.site=nested-in-struct" - ], - [ - "shape.kind=closed-set", - "shape.site=nested-two-levels" - ], - [ - "shape.kind=closed-set", - "shape.site=top-level" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=DateOnly" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=DateTime" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=Guid" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=TimeOnly" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=TimeSpan" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=Uri" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=bool" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=byte" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=char" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=decimal" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=double" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=enum" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=flags-enum" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=int" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=long" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=own-record-class" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=string" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=ulong" - ], - [ - "shape.kind=closed-set", - "text.culture=ar-SA" - ], - [ - "shape.kind=closed-set", - "text.culture=de-DE" - ], - [ - "shape.kind=closed-set", - "text.culture=globalization-invariant" - ], - [ - "shape.kind=closed-set", - "text.culture=invariant" - ], - [ - "shape.kind=closed-set", - "text.culture=tr-TR" - ], - [ - "shape.kind=closed-set", - "text.entry=IParsable" - ], - [ - "shape.kind=closed-set", - "text.entry=ISpanParsable" - ], - [ - "shape.kind=closed-set", - "text.entry=Parse" - ], - [ - "shape.kind=closed-set", - "text.entry=ToString-format" - ], - [ - "shape.kind=closed-set", - "text.entry=TryFormat-short-span" - ], - [ - "shape.kind=closed-set", - "text.entry=TryFrom" - ], - [ - "shape.kind=closed-set", - "text.entry=TryParse" - ], - [ - "shape.kind=closed-set", - "text.entry=type-converter" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=equality-operator (CMTK1011)" - ] - ] - }, - { - "reason": "A value object nested in a generic type fails the build with CMTK1005 (Generators README, Build errors), so it has no generated equality or parsing to check.", - "pairs": [ - [ - "parse-shape=abstract-string-ctor", - "shape.site=in-generic-type" - ], - [ - "parse-shape=implicit-result", - "shape.site=in-generic-type" - ], - [ - "parse-shape=ordinary", - "shape.site=in-generic-type" - ], - [ - "parse-shape=ref-text", - "shape.site=in-generic-type" - ], - [ - "process.converter-lookup=none", - "shape.site=in-generic-type" - ], - [ - "process.converter-lookup=wrapped-type-converter-looked-up-first", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=base-depth-1", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=base-depth-2", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=base-depth-3", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=none", - "shape.site=in-generic-type" - ], - [ - "shape.kind=record-struct", - "shape.site=in-generic-type" - ], - [ - "shape.kind=sealed-record-class", - "shape.site=in-generic-type" - ], - [ - "shape.kind=validated-class", - "shape.site=in-generic-type" - ], - [ - "shape.kind=validated-struct", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.site=in-generic-type" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=DateOnly" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=DateTime" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=Guid" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=TimeOnly" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=TimeSpan" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=Uri" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=bool" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=byte" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=char" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=decimal" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=double" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=enum" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=flags-enum" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=int" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=long" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=own-record-class" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=string" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=ulong" - ], - [ - "shape.site=in-generic-type", - "text.culture=ar-SA" - ], - [ - "shape.site=in-generic-type", - "text.culture=de-DE" - ], - [ - "shape.site=in-generic-type", - "text.culture=globalization-invariant" - ], - [ - "shape.site=in-generic-type", - "text.culture=invariant" - ], - [ - "shape.site=in-generic-type", - "text.culture=tr-TR" - ], - [ - "shape.site=in-generic-type", - "text.entry=IParsable" - ], - [ - "shape.site=in-generic-type", - "text.entry=ISpanParsable" - ], - [ - "shape.site=in-generic-type", - "text.entry=Parse" - ], - [ - "shape.site=in-generic-type", - "text.entry=ToString-format" - ], - [ - "shape.site=in-generic-type", - "text.entry=TryFormat-short-span" - ], - [ - "shape.site=in-generic-type", - "text.entry=TryFrom" - ], - [ - "shape.site=in-generic-type", - "text.entry=TryParse" - ], - [ - "shape.site=in-generic-type", - "text.entry=type-converter" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.site=in-generic-type" - ] - ] - }, - { - "reason": "A record struct has no base type, so it cannot derive from a base record or inherit a base's explicit IEquatable.", - "pairs": [ - [ - "shape.inheritance=base-depth-1", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-depth-1", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=base-depth-2", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-depth-2", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=base-depth-3", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-depth-3", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.kind=validated-struct" - ] - ] - }, - { - "reason": "The parse shape is the wrapped type itself, a class of its own; a value object over one of the catalogue's wrapped types is the ordinary shape.", - "pairs": [ - [ - "parse-shape=abstract-string-ctor", - "shape.wrapped=DateOnly" - ], - [ - "parse-shape=abstract-string-ctor", - "shape.wrapped=DateTime" - ], - [ - "parse-shape=abstract-string-ctor", - "shape.wrapped=DateTimeOffset" - ], - [ - "parse-shape=abstract-string-ctor", - "shape.wrapped=Guid" - ], - [ - "parse-shape=abstract-string-ctor", - "shape.wrapped=NodaTime.Instant" - ], - [ - "parse-shape=abstract-string-ctor", - "shape.wrapped=TimeOnly" - ], - [ - "parse-shape=abstract-string-ctor", - "shape.wrapped=TimeSpan" - ], - [ - "parse-shape=abstract-string-ctor", - "shape.wrapped=Uri" - ], - [ - "parse-shape=abstract-string-ctor", - "shape.wrapped=bool" - ], - [ - "parse-shape=abstract-string-ctor", - "shape.wrapped=byte" - ], - [ - "parse-shape=abstract-string-ctor", - "shape.wrapped=char" - ], - [ - "parse-shape=abstract-string-ctor", - "shape.wrapped=decimal" - ], - [ - "parse-shape=abstract-string-ctor", - "shape.wrapped=double" - ], - [ - "parse-shape=abstract-string-ctor", - "shape.wrapped=enum" - ], - [ - "parse-shape=abstract-string-ctor", - "shape.wrapped=flags-enum" - ], - [ - "parse-shape=abstract-string-ctor", - "shape.wrapped=int" - ], - [ - "parse-shape=abstract-string-ctor", - "shape.wrapped=long" - ], - [ - "parse-shape=abstract-string-ctor", - "shape.wrapped=own-record-class" - ], - [ - "parse-shape=abstract-string-ctor", - "shape.wrapped=string" - ], - [ - "parse-shape=abstract-string-ctor", - "shape.wrapped=ulong" - ], - [ - "parse-shape=implicit-result", - "shape.wrapped=DateOnly" - ], - [ - "parse-shape=implicit-result", - "shape.wrapped=DateTime" - ], - [ - "parse-shape=implicit-result", - "shape.wrapped=DateTimeOffset" - ], - [ - "parse-shape=implicit-result", - "shape.wrapped=Guid" - ], - [ - "parse-shape=implicit-result", - "shape.wrapped=NodaTime.Instant" - ], - [ - "parse-shape=implicit-result", - "shape.wrapped=TimeOnly" - ], - [ - "parse-shape=implicit-result", - "shape.wrapped=TimeSpan" - ], - [ - "parse-shape=implicit-result", - "shape.wrapped=Uri" - ], - [ - "parse-shape=implicit-result", - "shape.wrapped=bool" - ], - [ - "parse-shape=implicit-result", - "shape.wrapped=byte" - ], - [ - "parse-shape=implicit-result", - "shape.wrapped=char" - ], - [ - "parse-shape=implicit-result", - "shape.wrapped=decimal" - ], - [ - "parse-shape=implicit-result", - "shape.wrapped=double" - ], - [ - "parse-shape=implicit-result", - "shape.wrapped=enum" - ], - [ - "parse-shape=implicit-result", - "shape.wrapped=flags-enum" - ], - [ - "parse-shape=implicit-result", - "shape.wrapped=int" - ], - [ - "parse-shape=implicit-result", - "shape.wrapped=long" - ], - [ - "parse-shape=implicit-result", - "shape.wrapped=own-record-class" - ], - [ - "parse-shape=implicit-result", - "shape.wrapped=string" - ], - [ - "parse-shape=implicit-result", - "shape.wrapped=ulong" - ], - [ - "parse-shape=ref-text", - "shape.wrapped=DateOnly" - ], - [ - "parse-shape=ref-text", - "shape.wrapped=DateTime" - ], - [ - "parse-shape=ref-text", - "shape.wrapped=DateTimeOffset" - ], - [ - "parse-shape=ref-text", - "shape.wrapped=Guid" - ], - [ - "parse-shape=ref-text", - "shape.wrapped=NodaTime.Instant" - ], - [ - "parse-shape=ref-text", - "shape.wrapped=TimeOnly" - ], - [ - "parse-shape=ref-text", - "shape.wrapped=TimeSpan" - ], - [ - "parse-shape=ref-text", - "shape.wrapped=Uri" - ], - [ - "parse-shape=ref-text", - "shape.wrapped=bool" - ], - [ - "parse-shape=ref-text", - "shape.wrapped=byte" - ], - [ - "parse-shape=ref-text", - "shape.wrapped=char" - ], - [ - "parse-shape=ref-text", - "shape.wrapped=decimal" - ], - [ - "parse-shape=ref-text", - "shape.wrapped=double" - ], - [ - "parse-shape=ref-text", - "shape.wrapped=enum" - ], - [ - "parse-shape=ref-text", - "shape.wrapped=flags-enum" - ], - [ - "parse-shape=ref-text", - "shape.wrapped=int" - ], - [ - "parse-shape=ref-text", - "shape.wrapped=long" - ], - [ - "parse-shape=ref-text", - "shape.wrapped=own-record-class" - ], - [ - "parse-shape=ref-text", - "shape.wrapped=string" - ], - [ - "parse-shape=ref-text", - "shape.wrapped=ulong" - ] - ] - }, - { - "reason": "A declaration of a refused kind fails the build (CMTK1005, CMTK1006), so it is generated into nothing; .claude/probe-axes.md: shape.refused-kind is taken only where the refusal is the claim.", - "pairs": [ - [ - "parse-shape=abstract-string-ctor", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "parse-shape=abstract-string-ctor", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "parse-shape=abstract-string-ctor", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "parse-shape=abstract-string-ctor", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "parse-shape=implicit-result", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "parse-shape=implicit-result", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "parse-shape=implicit-result", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "parse-shape=implicit-result", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "parse-shape=ordinary", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "parse-shape=ordinary", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "parse-shape=ordinary", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "parse-shape=ordinary", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "parse-shape=ref-text", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "parse-shape=ref-text", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "parse-shape=ref-text", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "parse-shape=ref-text", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "process.converter-lookup=none", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "process.converter-lookup=none", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "process.converter-lookup=none", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "process.converter-lookup=none", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "process.converter-lookup=wrapped-type-converter-looked-up-first", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "process.converter-lookup=wrapped-type-converter-looked-up-first", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "process.converter-lookup=wrapped-type-converter-looked-up-first", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "process.converter-lookup=wrapped-type-converter-looked-up-first", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "text.culture=ar-SA" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "text.culture=de-DE" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "text.culture=globalization-invariant" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "text.culture=invariant" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "text.culture=tr-TR" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "text.entry=IParsable" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "text.entry=ISpanParsable" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "text.entry=Parse" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "text.entry=ToString-format" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "text.entry=TryFormat-short-span" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "text.entry=TryFrom" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "text.entry=TryParse" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "text.entry=type-converter" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "text.culture=ar-SA" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "text.culture=de-DE" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "text.culture=globalization-invariant" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "text.culture=invariant" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "text.culture=tr-TR" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "text.entry=IParsable" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "text.entry=ISpanParsable" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "text.entry=Parse" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "text.entry=ToString-format" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "text.entry=TryFormat-short-span" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "text.entry=TryFrom" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "text.entry=TryParse" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "text.entry=type-converter" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "text.culture=ar-SA" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "text.culture=de-DE" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "text.culture=globalization-invariant" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "text.culture=invariant" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "text.culture=tr-TR" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "text.entry=IParsable" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "text.entry=ISpanParsable" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "text.entry=Parse" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "text.entry=ToString-format" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "text.entry=TryFormat-short-span" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "text.entry=TryFrom" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "text.entry=TryParse" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "text.entry=type-converter" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "text.culture=ar-SA" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "text.culture=de-DE" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "text.culture=globalization-invariant" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "text.culture=invariant" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "text.culture=tr-TR" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "text.entry=IParsable" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "text.entry=ISpanParsable" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "text.entry=Parse" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "text.entry=ToString-format" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "text.entry=TryFormat-short-span" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "text.entry=TryFrom" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "text.entry=TryParse" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "text.entry=type-converter" - ], - [ - "parse-shape=abstract-string-ctor", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "parse-shape=abstract-string-ctor", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "parse-shape=abstract-string-ctor", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "parse-shape=abstract-string-ctor", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "parse-shape=abstract-string-ctor", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "parse-shape=implicit-result", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "parse-shape=implicit-result", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "parse-shape=implicit-result", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "parse-shape=implicit-result", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "parse-shape=implicit-result", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "parse-shape=ordinary", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "parse-shape=ordinary", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "parse-shape=ordinary", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "parse-shape=ordinary", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "parse-shape=ordinary", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "parse-shape=ref-text", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "parse-shape=ref-text", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "parse-shape=ref-text", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "parse-shape=ref-text", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "parse-shape=ref-text", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "process.converter-lookup=none", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "process.converter-lookup=none", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "process.converter-lookup=none", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "process.converter-lookup=none", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "process.converter-lookup=none", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "process.converter-lookup=wrapped-type-converter-looked-up-first", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "process.converter-lookup=wrapped-type-converter-looked-up-first", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "process.converter-lookup=wrapped-type-converter-looked-up-first", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "process.converter-lookup=wrapped-type-converter-looked-up-first", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "process.converter-lookup=wrapped-type-converter-looked-up-first", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "text.culture=ar-SA" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "text.culture=ar-SA" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "text.culture=ar-SA" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "text.culture=ar-SA" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "text.culture=ar-SA" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "text.culture=de-DE" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "text.culture=de-DE" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "text.culture=de-DE" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "text.culture=de-DE" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "text.culture=de-DE" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "text.culture=globalization-invariant" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "text.culture=globalization-invariant" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "text.culture=globalization-invariant" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "text.culture=globalization-invariant" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "text.culture=globalization-invariant" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "text.culture=invariant" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "text.culture=invariant" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "text.culture=invariant" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "text.culture=invariant" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "text.culture=invariant" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "text.culture=tr-TR" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "text.culture=tr-TR" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "text.culture=tr-TR" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "text.culture=tr-TR" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "text.culture=tr-TR" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "text.entry=IParsable" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "text.entry=IParsable" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "text.entry=IParsable" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "text.entry=IParsable" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "text.entry=IParsable" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "text.entry=ISpanParsable" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "text.entry=ISpanParsable" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "text.entry=ISpanParsable" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "text.entry=ISpanParsable" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "text.entry=ISpanParsable" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "text.entry=Parse" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "text.entry=Parse" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "text.entry=Parse" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "text.entry=Parse" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "text.entry=Parse" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "text.entry=ToString-format" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "text.entry=ToString-format" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "text.entry=ToString-format" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "text.entry=ToString-format" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "text.entry=ToString-format" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "text.entry=TryFormat-short-span" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "text.entry=TryFormat-short-span" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "text.entry=TryFormat-short-span" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "text.entry=TryFormat-short-span" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "text.entry=TryFormat-short-span" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "text.entry=TryFrom" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "text.entry=TryFrom" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "text.entry=TryFrom" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "text.entry=TryFrom" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "text.entry=TryFrom" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "text.entry=TryParse" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "text.entry=TryParse" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "text.entry=TryParse" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "text.entry=TryParse" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "text.entry=TryParse" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "text.entry=type-converter" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "text.entry=type-converter" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "text.entry=type-converter" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "text.entry=type-converter" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "text.entry=type-converter" - ] - ] - }, - { - "reason": "A `Uri`, an enum and a NodaTime type have neither (Generators README, What is generated: no ISpanParsable or IUtf8SpanParsable where the underlying type has none, not-promised no-span-parsing-without-the-underlying-types), so parsing-where-it-can-be-parsed is inapplicable there and no check that reads shape.wrapped can credit them.", - "pairs": [ - [ - "shape.wrapped=NodaTime.Instant", - "text.entry=ISpanParsable" - ], - [ - "shape.wrapped=Uri", - "text.entry=ISpanParsable" - ], - [ - "shape.wrapped=enum", - "text.entry=ISpanParsable" - ], - [ - "shape.wrapped=flags-enum", - "text.entry=ISpanParsable" - ] - ] - } - ] - }, - { - "id": "C12-described-converter", - "source": "docs/json.md#describing-what-a-converter-writes", - "surfaces": [ - "http" - ], - "axes": { - "source": [ - "IDescribedJsonConverter", - "JsonSchemaDescriptions", - "decimal-converter" - ], - "http.host": [ - "minimal", - "mvc", - "both", - "route-group", - "controller-without-ApiController-visible" - ] - }, - "obligations": [ - { - "id": "own-converter-describes-itself", - "sentence": "**Your own converter** implements `IDescribedJsonConverter` and describes itself, as both decimal converters do.", - "axes": { - "source": [ - "IDescribedJsonConverter", - "decimal-converter" - ] - }, - "inapplicable": [ - { - "axes": { - "json.ReferenceHandler": [ - "Preserve" - ] - }, - "cite": "record:0099" - }, - { - "axes": { - "json.MaxDepth": [ - "3" - ], - "source": [ - "IDescribedJsonConverter", - "JsonSchemaDescriptions" - ] - }, - "cite": "record:0099" - } - ] - }, - { - "id": "options-describe", - "sentence": "**A converter someone else wrote**, or the serializer's own enum converter, is described in the options by `JsonSchemaDescriptions`.", - "axes": { - "source": [ - "JsonSchemaDescriptions" - ] - }, - "inapplicable": [ - { - "axes": { - "json.ReferenceHandler": [ - "Preserve" - ] - }, - "cite": "record:0099" - }, - { - "axes": { - "json.MaxDepth": [ - "3" - ], - "source": [ - "IDescribedJsonConverter", - "JsonSchemaDescriptions" - ] - }, - "cite": "record:0099" - } - ] - }, - { - "id": "every-position", - "sentence": "With CodoMetis.TypeKit.AspNetCore, `AddTypeKit()` writes the same descriptions into the OpenAPI document, in every position, a list's elements and a dictionary's values included.", - "inapplicable": [ - { - "axes": { - "json.ReferenceHandler": [ - "Preserve" - ] - }, - "cite": "record:0099" - }, - { - "axes": { - "json.MaxDepth": [ - "3" - ], - "source": [ - "IDescribedJsonConverter", - "JsonSchemaDescriptions" - ] - }, - "cite": "record:0099" - } - ] - }, - { - "id": "parameter-keeps-aspnet", - "sentence": "So it keeps the description ASP.NET gives the type without your converters", - "axes": { - "source": [ - "decimal-converter" - ] - }, - "inapplicable": [ - { - "axes": { - "json.ReferenceHandler": [ - "Preserve" - ] - }, - "cite": "record:0099" - }, - { - "axes": { - "json.MaxDepth": [ - "3" - ], - "source": [ - "IDescribedJsonConverter", - "JsonSchemaDescriptions" - ] - }, - "cite": "record:0099" - } - ] - } - ], - "exclude": [ - { - "reason": "System.Text.Json refuses PreferredObjectCreationHandling.Populate together with a ReferenceHandler on the options object; JsonRow substitutes the default, so the pair never applies.", - "pairs": [ - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=Preserve" - ], - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=IgnoreCycles" - ] - ] - } - ] - }, - { - "id": "C13-patch-required", - "source": "docs/merge-patches.md#required-constructor-parameters", - "surfaces": [ - "http" - ], - "axes": { - "declared": [ - "init", - "ctor-parameter" - ], - "http.host": [ - "minimal", - "mvc", - "both", - "route-group", - "controller-without-ApiController-visible" - ] - }, - "obligations": [ - { - "id": "doc-required-where-rejected", - "sentence": "So the document never calls a field optional when the endpoint rejects a request without it.", - "axes": { - "declared": [ - "ctor-parameter" - ], - "json.RespectRequiredConstructorParameters": [ - "true" - ] - }, - "inapplicable": [ - { - "axes": { - "json.ReferenceHandler": [ - "Preserve" - ] - }, - "cite": "record:0099" - } - ] - }, - { - "id": "doc-optional-where-absent", - "sentence": "`AddTypeKit()` leaves a patch out of `required` where the serializer lets it be absent, and keeps it there where the options reject a body without it.", - "inapplicable": [ - { - "axes": { - "json.ReferenceHandler": [ - "Preserve" - ] - }, - "cite": "record:0099" - } - ] - }, - { - "id": "absent-is-default", - "sentence": "a property the request leaves out keeps its default, which is `Absent`." - } - ], - "exclude": [ - { - "reason": "System.Text.Json refuses PreferredObjectCreationHandling.Populate together with a ReferenceHandler on the options object; JsonRow substitutes the default, so the pair never applies.", - "pairs": [ - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=Preserve" - ], - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=IgnoreCycles" - ] - ] - } - ] - }, - { - "id": "C14-declared-problem", - "source": "CHANGELOG.md#2.1.0-fixed", - "surfaces": [ - "http" - ], - "axes": { - "declared": [ - "ProducesProblem", - "ProducesResponseType", - "none" - ], - "http.host": [ - "minimal", - "mvc", - "both", - "route-group", - "controller-without-ApiController-visible" - ] - }, - "obligations": [ - { - "id": "declared-describes-fault", - "sentence": "OpenAPI: a problem response an endpoint declares itself (`ProducesProblem`, `[ProducesResponseType]`) describes `fault`, and names its problem types.", - "axes": { - "declared": [ - "ProducesProblem", - "ProducesResponseType" - ] - } - }, - { - "id": "declared-names-types", - "sentence": "OpenAPI: a problem response an endpoint declares itself (`ProducesProblem`, `[ProducesResponseType]`) describes `fault`, and names its problem types.", - "axes": { - "declared": [ - "ProducesProblem", - "ProducesResponseType" - ] - } - }, - { - "id": "ascending-with-declared", - "sentence": "OpenAPI: an operation with fault responses lists every status ascending, the ones its endpoint declares itself among them", - "axes": { - "declared": [ - "ProducesProblem", - "ProducesResponseType" - ] - } - }, - { - "id": "application-json-declared", - "sentence": "OpenAPI: a problem response an endpoint declares as `application/json`, such as `Produces(404)` or `[ProducesResponseType(404)]`, describes `fault` and `rule` and names its problem types, and keeps its media type", - "axes": { - "declared": [ - "ProducesResponseType" - ] - } - } - ], - "exclude": [ - { - "reason": "System.Text.Json refuses PreferredObjectCreationHandling.Populate together with a ReferenceHandler on the options object; JsonRow substitutes the default, so the pair never applies.", - "pairs": [ - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=Preserve" - ], - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=IgnoreCycles" - ] - ] - } - ] - }, - { - "id": "C15-mvc-fault-status-doc", - "source": "CHANGELOG.md#2.1.0-fixed", - "surfaces": [ - "http" - ], - "axes": { - "setter": [ - "MapControllers().WithFaultStatus", - "action-attribute", - "controller-attribute" - ], - "kind": [ - "status-only", - "WithoutFaults" - ], - "http.host": [ - "minimal", - "mvc", - "both", - "route-group", - "controller-without-ApiController-visible" - ] - }, - "obligations": [ - { - "id": "builder-reaches-doc", - "sentence": "OpenAPI: a `WithFaultStatus` on the builder `MapControllers()` returns reaches the document, as it reached the response." - }, - { - "id": "without-faults-leaves-doc", - "sentence": "**`WithoutFaults`** and `[WithoutFaults]`: a route leaves out of its document the members it never answers with, and reporting one throws" - }, - { - "id": "status-per-scope", - "sentence": "`WithFaultStatus`/`[FaultStatus]` per endpoint, group or controller" - } - ], - "exclude": [ - { - "reason": "System.Text.Json refuses PreferredObjectCreationHandling.Populate together with a ReferenceHandler on the options object; JsonRow substitutes the default, so the pair never applies.", - "pairs": [ - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=Preserve" - ], - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=IgnoreCycles" - ] - ] - } - ] - }, - { - "id": "C16-options-agreement", - "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says", - "surfaces": [ - "http" - ], - "axes": { - "difference": [ - "converter-only-http", - "naming-policy", - "number-handling" - ], - "http.host": [ - "minimal", - "mvc", - "both", - "route-group", - "controller-without-ApiController-visible" - ] - }, - "obligations": [ - { - "id": "differing-refused", - "sentence": "Otherwise building the document fails rather than describe JSON the controllers don't write.", - "axes": { - "difference": [ - "converter-only-http" - ], - "http.host": [ - "mvc", - "both", - "controller-without-ApiController-visible" - ] - }, - "inapplicable": [ - { - "axes": { - "json.ReferenceHandler": [ - "Preserve" - ] - }, - "cite": "record:0099" - } - ] - }, - { - "id": "error-names-type-and-descriptions", - "sentence": "The error names the type and what each set of options says of it, also for a type held only inside a patch.", - "axes": { - "difference": [ - "converter-only-http" - ], - "http.host": [ - "mvc", - "both", - "controller-without-ApiController-visible" - ] - }, - "inapplicable": [ - { - "axes": { - "json.ReferenceHandler": [ - "Preserve" - ] - }, - "cite": "record:0099" - } - ] - }, - { - "id": "patch-held-refused", - "sentence": "The error names the type and what each set of options says of it, also for a type held only inside a patch.", - "axes": { - "difference": [ - "converter-only-http" - ], - "http.host": [ - "mvc", - "both", - "controller-without-ApiController-visible" - ] - }, - "inapplicable": [ - { - "axes": { - "json.ReferenceHandler": [ - "Preserve" - ] - }, - "cite": "record:0099" - } - ] - }, - { - "id": "same-converters-build", - "sentence": "A host with controllers registers the same converters and descriptions on both, in the same order.", - "axes": { - "difference": [ - "naming-policy", - "number-handling" - ] - }, - "inapplicable": [ - { - "axes": { - "json.ReferenceHandler": [ - "Preserve" - ] - }, - "cite": "record:0099" - } - ] - }, - { - "id": "minimal-host-not-compared", - "sentence": "A host with controllers registers the same converters and descriptions on both, in the same order.", - "axes": { - "difference": [ - "converter-only-http" - ], - "http.host": [ - "minimal", - "route-group" - ] - }, - "inapplicable": [ - { - "axes": { - "json.ReferenceHandler": [ - "Preserve" - ] - }, - "cite": "record:0099" - } - ] - } - ], - "not-promised": [ - { - "id": "only-converters-and-descriptions-compared", - "source": "design/adr/0071-converters-say-what-they-write.md#0071-json-converters-say-what-they-write-decimals-get-one-spelling", - "sentence": "Where the host has a controller, building the document fails when the two option sets do not carry the same described converters and description sets, by type, naming each that differs; a host of minimal APIs alone is never compared.", - "axes": { - "difference": [ - "naming-policy", - "number-handling" - ] - } - } - ], - "exclude": [ - { - "reason": "System.Text.Json refuses PreferredObjectCreationHandling.Populate together with a ReferenceHandler on the options object; JsonRow substitutes the default, so the pair never applies.", - "pairs": [ - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=Preserve" - ], - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=IgnoreCycles" - ] - ] - } - ] - }, - { - "id": "C17-cmtk0014", - "source": "src/CodoMetis.TypeKit.Analyzers/README.md#cmtk0014", - "surfaces": [ - "analyzers" - ], - "axes": { - "value": [ - "(FaultKind)9", - "default", - "valid" - ], - "site": [ - "attribute-argument", - "cast", - "default-literal" - ] - }, - "obligations": [ - { - "id": "a-cast-naming-no-member", - "sentence": "A `FaultKind` or a `ConstructorAccessibility` that names no member: a cast", - "axes": { - "value": [ - "(FaultKind)9" - ] - } - }, - { - "id": "default-and-zero", - "sentence": "`default(FaultKind)`, a zero", - "axes": { - "value": [ - "default" - ] - } - }, - { - "id": "constructor-accessibility-too", - "sentence": "[RequireParameterlessConstructor((ConstructorAccessibility)7)] // CMTK0014: no member, read as public", - "axes": { - "site": [ - "attribute-argument" - ], - "value": [ - "(FaultKind)9" - ] - } - }, - { - "id": "reported-where-written", - "sentence": "Both are reported where the value is written:", - "axes": { - "value": [ - "(FaultKind)9", - "default" - ] - } - }, - { - "id": "a-member-is-silent", - "sentence": "public static readonly ImportFault Duplicate = new(FaultKind.Conflict);", - "axes": { - "value": [ - "valid" - ] - } - } - ], - "exclude": [ - { - "reason": "A default literal is the value 0, so it cannot spell (FaultKind)9 or a member: FaultKind's members start at 1.", - "pairs": [ - [ - "site=default-literal", - "value=(FaultKind)9" - ], - [ - "site=default-literal", - "value=valid" - ] - ] - }, - { - "reason": "A record struct has no base type, so it cannot derive from a base record or inherit a base's explicit IEquatable.", - "pairs": [ - [ - "shape.inheritance=base-depth-1", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-depth-1", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=base-depth-2", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-depth-2", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=base-depth-3", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-depth-3", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.kind=validated-struct" - ] - ] - }, - { - "reason": "A closed set wraps no type, so a closed set over a wrapped type cannot be declared.", - "pairs": [ - [ - "shape.kind=closed-set", - "shape.wrapped=DateOnly" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=DateTime" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=Guid" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=TimeOnly" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=TimeSpan" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=Uri" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=bool" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=byte" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=char" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=decimal" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=double" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=enum" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=flags-enum" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=int" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=long" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=own-record-class" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=string" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=ulong" - ] - ] - }, - { - "reason": "Declared as .claude/probe-axes.md says (as C22's harness, BuildRefusals.cs, declares it), a closed set named as one of its generated members leaves their writing to the generators, so in this compilation without them it does not implement IClosedSet's members: the compiler's own CS0535, and no row can hold it beside the analyzer's places.", - "pairs": [ - [ - "shape.containing=class", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.containing=generic-type", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.containing=record-class", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.containing=record-struct", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.containing=struct", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=array-element", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=dictionary-key", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=dictionary-value", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=field", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=immutable-array-element", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=in-patch", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=list-element", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=nullable-property", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=polymorphic-derived-property", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=property", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=read-only-list-element", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=record-ctor-parameter", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=required-init", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=root", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "site=attribute-argument" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "site=cast" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "site=default-literal" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "value=(FaultKind)9" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "value=default" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "value=valid" - ] - ] - }, - { - "reason": "A value object with a hand-written operator == and != between two of the type is C#'s own CS0111 without the generators (Generators README, Build errors: CMTK1011, 'which C# also reports as CS0111'), since a record already declares both, so this compilation without the generators cannot hold it.", - "pairs": [ - [ - "shape.containing=class", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.containing=generic-type", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.containing=record-class", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.containing=record-struct", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.containing=struct", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=array-element", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=dictionary-key", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=dictionary-value", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=field", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=immutable-array-element", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=in-patch", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=list-element", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=nullable-property", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=polymorphic-derived-property", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=property", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=read-only-list-element", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=record-ctor-parameter", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=required-init", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=root", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "site=attribute-argument" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "site=cast" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "site=default-literal" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "value=(FaultKind)9" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "value=default" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "value=valid" - ] - ] - } - ] - }, - { - "id": "C18-cmtk1023", - "source": "src/CodoMetis.TypeKit.Generators/README.md#build-errors", - "surfaces": [ - "generation" - ], - "axes": { - "base-member": [ - "Equals", - "GetHashCode", - "IEquatable" - ] - }, - "obligations": [ - { - "id": "names-each-base-member", - "sentence": "So a hand-written `Equals(TBase)`, `GetHashCode()` or `IEquatable.Equals` on a base record isn't used, and a warning names it (CMTK1023)." - }, - { - "id": "a-warning-and-generated", - "sentence": "A warning, and the value object is generated." - }, - { - "id": "generated-equality-never-calls-the-base", - "sentence": "Over a base record, the generated `Equals(TSelf)` and `GetHashCode()` compare the underlying value alone, and never call the base's." - } - ], - "exclude": [ - { - "reason": "A record struct or a closed set has no base record: C# gives a struct no base type, and a closed set is a sealed class that is never a record (CMTK1020). CMTK1023 is about a base record's equality.", - "pairs": [ - [ - "base-member=Equals", - "shape.kind=closed-set" - ], - [ - "base-member=Equals", - "shape.kind=record-struct" - ], - [ - "base-member=Equals", - "shape.kind=validated-struct" - ], - [ - "base-member=GetHashCode", - "shape.kind=closed-set" - ], - [ - "base-member=GetHashCode", - "shape.kind=record-struct" - ], - [ - "base-member=GetHashCode", - "shape.kind=validated-struct" - ], - [ - "base-member=IEquatable", - "shape.kind=closed-set" - ], - [ - "base-member=IEquatable", - "shape.kind=record-struct" - ], - [ - "base-member=IEquatable", - "shape.kind=validated-struct" - ], - [ - "shape.containing=class", - "shape.kind=closed-set" - ], - [ - "shape.containing=class", - "shape.kind=record-struct" - ], - [ - "shape.containing=class", - "shape.kind=validated-struct" - ], - [ - "shape.containing=generic-type", - "shape.kind=closed-set" - ], - [ - "shape.containing=generic-type", - "shape.kind=record-struct" - ], - [ - "shape.containing=generic-type", - "shape.kind=validated-struct" - ], - [ - "shape.containing=record-class", - "shape.kind=closed-set" - ], - [ - "shape.containing=record-class", - "shape.kind=record-struct" - ], - [ - "shape.containing=record-class", - "shape.kind=validated-struct" - ], - [ - "shape.containing=record-struct", - "shape.kind=closed-set" - ], - [ - "shape.containing=record-struct", - "shape.kind=record-struct" - ], - [ - "shape.containing=record-struct", - "shape.kind=validated-struct" - ], - [ - "shape.containing=struct", - "shape.kind=closed-set" - ], - [ - "shape.containing=struct", - "shape.kind=record-struct" - ], - [ - "shape.containing=struct", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=base-depth-1", - "shape.kind=closed-set" - ], - [ - "shape.inheritance=base-depth-1", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-depth-1", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=base-depth-2", - "shape.kind=closed-set" - ], - [ - "shape.inheritance=base-depth-2", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-depth-2", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=base-depth-3", - "shape.kind=closed-set" - ], - [ - "shape.inheritance=base-depth-3", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-depth-3", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.kind=closed-set" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.kind=closed-set" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=none", - "shape.kind=closed-set" - ], - [ - "shape.inheritance=none", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=none", - "shape.kind=validated-struct" - ], - [ - "shape.kind=closed-set", - "shape.position=array-element" - ], - [ - "shape.kind=closed-set", - "shape.position=dictionary-key" - ], - [ - "shape.kind=closed-set", - "shape.position=dictionary-value" - ], - [ - "shape.kind=closed-set", - "shape.position=field" - ], - [ - "shape.kind=closed-set", - "shape.position=immutable-array-element" - ], - [ - "shape.kind=closed-set", - "shape.position=in-patch" - ], - [ - "shape.kind=closed-set", - "shape.position=list-element" - ], - [ - "shape.kind=closed-set", - "shape.position=nullable-property" - ], - [ - "shape.kind=closed-set", - "shape.position=polymorphic-derived-property" - ], - [ - "shape.kind=closed-set", - "shape.position=property" - ], - [ - "shape.kind=closed-set", - "shape.position=read-only-list-element" - ], - [ - "shape.kind=closed-set", - "shape.position=record-ctor-parameter" - ], - [ - "shape.kind=closed-set", - "shape.position=required-init" - ], - [ - "shape.kind=closed-set", - "shape.position=root" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.kind=closed-set", - "shape.site=in-generic-type" - ], - [ - "shape.kind=closed-set", - "shape.site=nested-in-class" - ], - [ - "shape.kind=closed-set", - "shape.site=nested-in-struct" - ], - [ - "shape.kind=closed-set", - "shape.site=nested-two-levels" - ], - [ - "shape.kind=closed-set", - "shape.site=top-level" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=DateOnly" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=DateTime" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=Guid" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=TimeOnly" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=TimeSpan" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=Uri" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=bool" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=byte" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=char" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=decimal" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=double" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=enum" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=flags-enum" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=int" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=long" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=own-record-class" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=string" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=ulong" - ], - [ - "shape.kind=record-struct", - "shape.position=array-element" - ], - [ - "shape.kind=record-struct", - "shape.position=dictionary-key" - ], - [ - "shape.kind=record-struct", - "shape.position=dictionary-value" - ], - [ - "shape.kind=record-struct", - "shape.position=field" - ], - [ - "shape.kind=record-struct", - "shape.position=immutable-array-element" - ], - [ - "shape.kind=record-struct", - "shape.position=in-patch" - ], - [ - "shape.kind=record-struct", - "shape.position=list-element" - ], - [ - "shape.kind=record-struct", - "shape.position=nullable-property" - ], - [ - "shape.kind=record-struct", - "shape.position=polymorphic-derived-property" - ], - [ - "shape.kind=record-struct", - "shape.position=property" - ], - [ - "shape.kind=record-struct", - "shape.position=read-only-list-element" - ], - [ - "shape.kind=record-struct", - "shape.position=record-ctor-parameter" - ], - [ - "shape.kind=record-struct", - "shape.position=required-init" - ], - [ - "shape.kind=record-struct", - "shape.position=root" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.kind=record-struct", - "shape.site=in-generic-type" - ], - [ - "shape.kind=record-struct", - "shape.site=nested-in-class" - ], - [ - "shape.kind=record-struct", - "shape.site=nested-in-struct" - ], - [ - "shape.kind=record-struct", - "shape.site=nested-two-levels" - ], - [ - "shape.kind=record-struct", - "shape.site=top-level" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=DateOnly" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=DateTime" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=Guid" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=TimeOnly" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=TimeSpan" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=Uri" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=bool" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=byte" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=char" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=decimal" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=double" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=enum" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=flags-enum" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=int" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=long" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=own-record-class" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=string" - ], - [ - "shape.kind=record-struct", - "shape.wrapped=ulong" - ], - [ - "shape.kind=validated-struct", - "shape.position=array-element" - ], - [ - "shape.kind=validated-struct", - "shape.position=dictionary-key" - ], - [ - "shape.kind=validated-struct", - "shape.position=dictionary-value" - ], - [ - "shape.kind=validated-struct", - "shape.position=field" - ], - [ - "shape.kind=validated-struct", - "shape.position=immutable-array-element" - ], - [ - "shape.kind=validated-struct", - "shape.position=in-patch" - ], - [ - "shape.kind=validated-struct", - "shape.position=list-element" - ], - [ - "shape.kind=validated-struct", - "shape.position=nullable-property" - ], - [ - "shape.kind=validated-struct", - "shape.position=polymorphic-derived-property" - ], - [ - "shape.kind=validated-struct", - "shape.position=property" - ], - [ - "shape.kind=validated-struct", - "shape.position=read-only-list-element" - ], - [ - "shape.kind=validated-struct", - "shape.position=record-ctor-parameter" - ], - [ - "shape.kind=validated-struct", - "shape.position=required-init" - ], - [ - "shape.kind=validated-struct", - "shape.position=root" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.kind=validated-struct", - "shape.site=in-generic-type" - ], - [ - "shape.kind=validated-struct", - "shape.site=nested-in-class" - ], - [ - "shape.kind=validated-struct", - "shape.site=nested-in-struct" - ], - [ - "shape.kind=validated-struct", - "shape.site=nested-two-levels" - ], - [ - "shape.kind=validated-struct", - "shape.site=top-level" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=DateOnly" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=DateTime" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=Guid" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=TimeOnly" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=TimeSpan" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=Uri" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=bool" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=byte" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=char" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=decimal" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=double" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=enum" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=flags-enum" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=int" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=long" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=own-record-class" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=string" - ], - [ - "shape.kind=validated-struct", - "shape.wrapped=ulong" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=equality-operator (CMTK1011)" - ] - ] - }, - { - "reason": "A value object nested in a generic type fails the build with CMTK1005 (Generators README, Build errors), so it has no generated equality or parsing to check.", - "pairs": [ - [ - "base-member=Equals", - "shape.site=in-generic-type" - ], - [ - "base-member=GetHashCode", - "shape.site=in-generic-type" - ], - [ - "base-member=IEquatable", - "shape.site=in-generic-type" - ], - [ - "shape.containing=class", - "shape.site=in-generic-type" - ], - [ - "shape.containing=generic-type", - "shape.site=in-generic-type" - ], - [ - "shape.containing=record-class", - "shape.site=in-generic-type" - ], - [ - "shape.containing=record-struct", - "shape.site=in-generic-type" - ], - [ - "shape.containing=struct", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=base-depth-1", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=base-depth-2", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=base-depth-3", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=none", - "shape.site=in-generic-type" - ], - [ - "shape.kind=sealed-record-class", - "shape.site=in-generic-type" - ], - [ - "shape.kind=validated-class", - "shape.site=in-generic-type" - ], - [ - "shape.position=array-element", - "shape.site=in-generic-type" - ], - [ - "shape.position=dictionary-key", - "shape.site=in-generic-type" - ], - [ - "shape.position=dictionary-value", - "shape.site=in-generic-type" - ], - [ - "shape.position=field", - "shape.site=in-generic-type" - ], - [ - "shape.position=immutable-array-element", - "shape.site=in-generic-type" - ], - [ - "shape.position=in-patch", - "shape.site=in-generic-type" - ], - [ - "shape.position=list-element", - "shape.site=in-generic-type" - ], - [ - "shape.position=nullable-property", - "shape.site=in-generic-type" - ], - [ - "shape.position=polymorphic-derived-property", - "shape.site=in-generic-type" - ], - [ - "shape.position=property", - "shape.site=in-generic-type" - ], - [ - "shape.position=read-only-list-element", - "shape.site=in-generic-type" - ], - [ - "shape.position=record-ctor-parameter", - "shape.site=in-generic-type" - ], - [ - "shape.position=required-init", - "shape.site=in-generic-type" - ], - [ - "shape.position=root", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.site=in-generic-type" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=DateOnly" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=DateTime" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=Guid" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=TimeOnly" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=TimeSpan" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=Uri" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=bool" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=byte" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=char" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=decimal" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=double" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=enum" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=flags-enum" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=int" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=long" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=own-record-class" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=string" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.site=in-generic-type" - ] - ] - }, - { - "reason": "Without a base record there is no base to declare Equals, GetHashCode or IEquatable on.", - "pairs": [ - [ - "base-member=Equals", - "shape.inheritance=none" - ], - [ - "base-member=GetHashCode", - "shape.inheritance=none" - ], - [ - "base-member=IEquatable", - "shape.inheritance=none" - ], - [ - "shape.containing=class", - "shape.inheritance=none" - ], - [ - "shape.containing=generic-type", - "shape.inheritance=none" - ], - [ - "shape.containing=record-class", - "shape.inheritance=none" - ], - [ - "shape.containing=record-struct", - "shape.inheritance=none" - ], - [ - "shape.containing=struct", - "shape.inheritance=none" - ], - [ - "shape.inheritance=none", - "shape.kind=sealed-record-class" - ], - [ - "shape.inheritance=none", - "shape.kind=validated-class" - ], - [ - "shape.inheritance=none", - "shape.position=array-element" - ], - [ - "shape.inheritance=none", - "shape.position=dictionary-key" - ], - [ - "shape.inheritance=none", - "shape.position=dictionary-value" - ], - [ - "shape.inheritance=none", - "shape.position=field" - ], - [ - "shape.inheritance=none", - "shape.position=immutable-array-element" - ], - [ - "shape.inheritance=none", - "shape.position=in-patch" - ], - [ - "shape.inheritance=none", - "shape.position=list-element" - ], - [ - "shape.inheritance=none", - "shape.position=nullable-property" - ], - [ - "shape.inheritance=none", - "shape.position=polymorphic-derived-property" - ], - [ - "shape.inheritance=none", - "shape.position=property" - ], - [ - "shape.inheritance=none", - "shape.position=read-only-list-element" - ], - [ - "shape.inheritance=none", - "shape.position=record-ctor-parameter" - ], - [ - "shape.inheritance=none", - "shape.position=required-init" - ], - [ - "shape.inheritance=none", - "shape.position=root" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.inheritance=none", - "shape.site=nested-in-class" - ], - [ - "shape.inheritance=none", - "shape.site=nested-in-struct" - ], - [ - "shape.inheritance=none", - "shape.site=nested-two-levels" - ], - [ - "shape.inheritance=none", - "shape.site=top-level" - ], - [ - "shape.inheritance=none", - "shape.wrapped=DateOnly" - ], - [ - "shape.inheritance=none", - "shape.wrapped=DateTime" - ], - [ - "shape.inheritance=none", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.inheritance=none", - "shape.wrapped=Guid" - ], - [ - "shape.inheritance=none", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.inheritance=none", - "shape.wrapped=TimeOnly" - ], - [ - "shape.inheritance=none", - "shape.wrapped=TimeSpan" - ], - [ - "shape.inheritance=none", - "shape.wrapped=Uri" - ], - [ - "shape.inheritance=none", - "shape.wrapped=bool" - ], - [ - "shape.inheritance=none", - "shape.wrapped=byte" - ], - [ - "shape.inheritance=none", - "shape.wrapped=char" - ], - [ - "shape.inheritance=none", - "shape.wrapped=decimal" - ], - [ - "shape.inheritance=none", - "shape.wrapped=double" - ], - [ - "shape.inheritance=none", - "shape.wrapped=enum" - ], - [ - "shape.inheritance=none", - "shape.wrapped=flags-enum" - ], - [ - "shape.inheritance=none", - "shape.wrapped=int" - ], - [ - "shape.inheritance=none", - "shape.wrapped=long" - ], - [ - "shape.inheritance=none", - "shape.wrapped=own-record-class" - ], - [ - "shape.inheritance=none", - "shape.wrapped=string" - ], - [ - "shape.inheritance=none", - "shape.wrapped=ulong" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=equality-operator (CMTK1011)" - ] - ] - }, - { - "reason": "CMTK1023 is 'A warning, and the value object is generated' (Generators README, Build errors); a refused declaration is not generated. A declaration of a refused kind fails the build (CMTK1005, CMTK1006), so it is generated into nothing; .claude/probe-axes.md: shape.refused-kind is taken only where the refusal is the claim.", - "pairs": [ - [ - "base-member=Equals", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "base-member=Equals", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "base-member=Equals", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "base-member=Equals", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "base-member=GetHashCode", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "base-member=GetHashCode", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "base-member=GetHashCode", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "base-member=GetHashCode", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "base-member=IEquatable", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "base-member=IEquatable", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "base-member=IEquatable", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "base-member=IEquatable", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.containing=class", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.containing=class", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.containing=class", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.containing=class", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.containing=generic-type", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.containing=generic-type", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.containing=generic-type", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.containing=generic-type", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.containing=record-class", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.containing=record-class", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.containing=record-class", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.containing=record-class", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.containing=record-struct", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.containing=record-struct", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.containing=record-struct", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.containing=record-struct", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.containing=struct", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.containing=struct", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.containing=struct", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.containing=struct", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.position=array-element", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.position=array-element", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.position=array-element", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.position=array-element", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.position=dictionary-key", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.position=dictionary-key", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.position=dictionary-key", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.position=dictionary-key", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.position=dictionary-value", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.position=dictionary-value", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.position=dictionary-value", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.position=dictionary-value", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.position=field", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.position=field", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.position=field", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.position=field", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.position=immutable-array-element", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.position=immutable-array-element", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.position=immutable-array-element", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.position=immutable-array-element", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.position=in-patch", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.position=in-patch", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.position=in-patch", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.position=in-patch", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.position=list-element", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.position=list-element", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.position=list-element", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.position=list-element", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.position=nullable-property", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.position=nullable-property", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.position=nullable-property", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.position=nullable-property", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.position=polymorphic-derived-property", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.position=polymorphic-derived-property", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.position=polymorphic-derived-property", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.position=polymorphic-derived-property", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.position=property", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.position=property", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.position=property", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.position=property", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.position=read-only-list-element", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.position=read-only-list-element", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.position=read-only-list-element", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.position=read-only-list-element", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.position=record-ctor-parameter", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.position=record-ctor-parameter", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.position=record-ctor-parameter", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.position=record-ctor-parameter", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.position=required-init", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.position=required-init", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.position=required-init", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.position=required-init", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.position=root", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.position=root", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.position=root", - "shape.refused-kind=over-value-object (CMTK1005)" - ], - [ - "shape.position=root", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.wrapped=ulong" - ], - [ - "base-member=Equals", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "base-member=Equals", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "base-member=Equals", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "base-member=Equals", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "base-member=Equals", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "base-member=GetHashCode", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "base-member=GetHashCode", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "base-member=GetHashCode", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "base-member=GetHashCode", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "base-member=GetHashCode", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "base-member=IEquatable", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "base-member=IEquatable", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "base-member=IEquatable", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "base-member=IEquatable", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "base-member=IEquatable", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.containing=class", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.containing=class", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.containing=class", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.containing=class", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.containing=class", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.containing=generic-type", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.containing=generic-type", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.containing=generic-type", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.containing=generic-type", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.containing=generic-type", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.containing=record-class", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.containing=record-class", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.containing=record-class", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.containing=record-class", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.containing=record-class", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.containing=record-struct", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.containing=record-struct", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.containing=record-struct", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.containing=record-struct", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.containing=record-struct", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.containing=struct", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.containing=struct", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.containing=struct", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.containing=struct", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.containing=struct", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=array-element", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.position=array-element", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.position=array-element", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.position=array-element", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=array-element", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=dictionary-key", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.position=dictionary-key", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.position=dictionary-key", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.position=dictionary-key", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=dictionary-key", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=dictionary-value", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.position=dictionary-value", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.position=dictionary-value", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.position=dictionary-value", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=dictionary-value", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=field", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.position=field", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.position=field", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.position=field", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=field", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=immutable-array-element", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.position=immutable-array-element", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.position=immutable-array-element", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.position=immutable-array-element", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=immutable-array-element", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=in-patch", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.position=in-patch", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.position=in-patch", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.position=in-patch", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=in-patch", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=list-element", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.position=list-element", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.position=list-element", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.position=list-element", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=list-element", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=nullable-property", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.position=nullable-property", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.position=nullable-property", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.position=nullable-property", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=nullable-property", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=polymorphic-derived-property", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.position=polymorphic-derived-property", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.position=polymorphic-derived-property", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.position=polymorphic-derived-property", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=polymorphic-derived-property", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=property", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.position=property", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.position=property", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.position=property", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=property", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=read-only-list-element", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.position=read-only-list-element", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.position=read-only-list-element", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.position=read-only-list-element", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=read-only-list-element", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=record-ctor-parameter", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.position=record-ctor-parameter", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.position=record-ctor-parameter", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.position=record-ctor-parameter", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=record-ctor-parameter", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=required-init", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.position=required-init", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.position=required-init", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.position=required-init", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=required-init", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=root", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.position=root", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.position=root", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.position=root", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=root", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=ulong" - ] - ] - } - ] - }, - { - "id": "C19-cmtk0010-kinds", - "source": "docs/type-constraints.md#the-kind-of-a-type-argument", - "surfaces": [ - "analyzers" - ], - "axes": { - "attribute": [ - "Interface", - "Unsealed", - "ValueObject", - "ClosedSet", - "FlagsEnum", - "ReadOnlyStruct", - "Distinct", - "Unrelated" - ], - "site": [ - "call", - "foreach", - "await", - "Deconstruct", - "collection-expression" - ] - }, - "obligations": [ - { - "id": "interface-and-unsealed", - "sentence": "**`[Interface]` and `[Unsealed]`** are what a proxy needs: a contract to implement, or a class to derive from.", - "axes": { - "attribute": [ - "Interface", - "Unsealed" - ] - } - }, - { - "id": "value-object-and-closed-set", - "sentence": "**`[ValueObject]` and `[ClosedSet]`** accept their marker interface with any type argument.", - "axes": { - "attribute": [ - "ValueObject", - "ClosedSet" - ] - } - }, - { - "id": "flags-enum-and-read-only-struct", - "sentence": "**`[FlagsEnum]` and `[ReadOnlyStruct]`** narrow what `where T : Enum` and `where T : struct` admit.", - "axes": { - "attribute": [ - "FlagsEnum", - "ReadOnlyStruct" - ] - } - }, - { - "id": "a-pair-once-at-the-later-argument", - "sentence": "A breach is reported once, at the later argument.", - "axes": { - "attribute": [ - "Distinct", - "Unrelated" - ] - } - }, - { - "id": "methods-the-compiler-calls", - "sentence": "**Methods the compiler calls for you:** a `foreach`'s `GetEnumerator`, an `await`'s `GetAwaiter`, a `Deconstruct` (in `var (a, b) = x`, a `foreach`, or a pattern like `x is (1, 2)`), and a collection expression's `Add` or builder method.", - "axes": { - "site": [ - "foreach", - "await", - "Deconstruct", - "collection-expression" - ] - } - } - ], - "exclude": [ - { - "reason": "A record struct has no base type, so it cannot derive from a base record or inherit a base's explicit IEquatable.", - "pairs": [ - [ - "shape.inheritance=base-depth-1", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-depth-1", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=base-depth-2", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-depth-2", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=base-depth-3", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-depth-3", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.kind=validated-struct" - ] - ] - }, - { - "reason": "A closed set wraps no type, so a closed set over a wrapped type cannot be declared.", - "pairs": [ - [ - "shape.kind=closed-set", - "shape.wrapped=DateOnly" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=DateTime" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=Guid" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=TimeOnly" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=TimeSpan" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=Uri" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=bool" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=byte" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=char" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=decimal" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=double" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=enum" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=flags-enum" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=int" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=long" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=own-record-class" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=string" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=ulong" - ] - ] - }, - { - "reason": "Declared as .claude/probe-axes.md says (as C22's harness, BuildRefusals.cs, declares it), a closed set named as one of its generated members leaves their writing to the generators, so in this compilation without them it does not implement IClosedSet's members: the compiler's own CS0535, and no row can hold it beside the analyzer's places.", - "pairs": [ - [ - "attribute=ClosedSet", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "attribute=Distinct", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "attribute=FlagsEnum", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "attribute=Interface", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "attribute=ReadOnlyStruct", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "attribute=Unrelated", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "attribute=Unsealed", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "attribute=ValueObject", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.containing=class", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.containing=generic-type", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.containing=record-class", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.containing=record-struct", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.containing=struct", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=array-element", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=dictionary-key", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=dictionary-value", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=field", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=immutable-array-element", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=in-patch", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=list-element", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=nullable-property", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=polymorphic-derived-property", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=property", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=read-only-list-element", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=record-ctor-parameter", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=required-init", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.position=root", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "site=Deconstruct" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "site=await" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "site=call" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "site=collection-expression" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "site=foreach" - ] - ] - }, - { - "reason": "A value object with a hand-written operator == and != between two of the type is C#'s own CS0111 without the generators (Generators README, Build errors: CMTK1011, 'which C# also reports as CS0111'), since a record already declares both, so this compilation without the generators cannot hold it.", - "pairs": [ - [ - "attribute=ClosedSet", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "attribute=Distinct", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "attribute=FlagsEnum", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "attribute=Interface", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "attribute=ReadOnlyStruct", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "attribute=Unrelated", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "attribute=Unsealed", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "attribute=ValueObject", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.containing=class", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.containing=generic-type", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.containing=record-class", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.containing=record-struct", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.containing=struct", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.inheritance=none", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=array-element", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=dictionary-key", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=dictionary-value", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=field", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=immutable-array-element", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=in-patch", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=list-element", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=nullable-property", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=polymorphic-derived-property", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=property", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=read-only-list-element", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=record-ctor-parameter", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=required-init", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.position=root", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.site=nested-in-class" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.site=nested-in-struct" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.site=nested-two-levels" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.site=top-level" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "site=Deconstruct" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "site=await" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "site=call" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "site=collection-expression" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "site=foreach" - ] - ] - } - ] - }, - { - "id": "C20-testing-shouldhold", - "source": "src/CodoMetis.TypeKit.Testing/README.md", - "surfaces": [ - "option-result" - ], - "axes": { - "arity": [ - "2", - "3" - ], - "set": [ - "first", - "last" - ], - "wrong-reason": [ - "no", - "yes" - ] - }, - "obligations": [ - { - "id": "fails-unless-it-holds-that-reason", - "sentence": "`ShouldHold(reason)` fails unless the fault holds that reason, whichever of its types the reason belongs to." - }, - { - "id": "whichever-of-its-types", - "sentence": "whichever of its types the reason belongs to", - "axes": { - "set": [ - "first", - "last" - ], - "arity": [ - "2", - "3" - ] - } - }, - { - "id": "hands-back-detail-and-rule", - "sentence": "It hands back the `Fault` with its detail and rule intact.", - "axes": { - "wrong-reason": [ - "no" - ] - } - }, - { - "id": "uninitialized-fails", - "sentence": "**An uninitialized result fails every assertion.**", - "axes": { - "or.operation": [ - "default-instance" - ] - } - }, - { - "id": "a-failure-says-the-outcome", - "sentence": "**A failure says what the outcome was**", - "axes": { - "wrong-reason": [ - "yes" - ] - } - } - ], - "exclude": [ - { - "reason": "Result.Catch turns an exception into a Result, never an Option (src/CodoMetis.TypeKit/ResultExtensions.cs), so no row can catch into an Option.", - "pairs": [ - [ - "or.operation=Result.Catch", - "or.shape=Option" - ] - ] - }, - { - "reason": "Result has no Select or SelectMany, so no query expression produces one, and FirstOrNone returns an Option, never a Result (src/CodoMetis.TypeKit/ResultExtensions.cs and OptionExtensions.cs).", - "pairs": [ - [ - "or.operation=FirstOrNone", - "or.shape=Result-E" - ], - [ - "or.operation=query-syntax", - "or.shape=Result-E" - ] - ] - } - ] - }, - { - "id": "C21-openapi-schema-parity", - "source": "README.md#what-you-get", - "surfaces": [ - "http", - "json" - ], - "axes": { - "http.host": [ - "minimal", - "mvc", - "both", - "route-group", - "controller-without-ApiController-visible" - ] - }, - "obligations": [ - { - "id": "the-underlying-types-schema", - "sentence": "The JSON, the database column and the OpenAPI schema are the same as the underlying type's.", - "inapplicable": [ - { - "axes": { - "json.Converters": [ - "own-for-value-object" - ] - }, - "cite": "not-promised:own-converter-for-the-value-object" - }, - { - "axes": { - "json.Converters": [ - "own-for-wrapped" - ] - }, - "cite": "not-promised:own-converter-for-the-wrapped-type" - }, - { - "axes": { - "json.ReferenceHandler": [ - "Preserve" - ] - }, - "cite": "record:0099" - }, - { - "axes": { - "json.NumberHandling": [ - "WriteAsString" - ], - "shape.wrapped": [ - "char" - ] - }, - "cite": "fact:KB-042" - }, - { - "axes": { - "json.MaxDepth": [ - "3" - ], - "shape.wrapped": [ - "own-record-class" - ] - }, - "cite": "record:0099" - } - ] - }, - { - "id": "nullable-admits-null-as-the-wrapped-does", - "sentence": "Its OpenAPI schema is the one ASP.NET Core publishes for the underlying type.", - "axes": { - "http.nullability": [ - "nullable" - ] - }, - "inapplicable": [ - { - "axes": { - "json.Converters": [ - "own-for-value-object" - ] - }, - "cite": "not-promised:own-converter-for-the-value-object" - }, - { - "axes": { - "json.Converters": [ - "own-for-wrapped" - ] - }, - "cite": "not-promised:own-converter-for-the-wrapped-type" - }, - { - "axes": { - "json.ReferenceHandler": [ - "Preserve" - ] - }, - "cite": "record:0099" - }, - { - "axes": { - "json.NumberHandling": [ - "WriteAsString" - ], - "shape.wrapped": [ - "char" - ] - }, - "cite": "fact:KB-042" - }, - { - "axes": { - "json.MaxDepth": [ - "3" - ], - "shape.wrapped": [ - "own-record-class" - ] - }, - "cite": "record:0099" - } - ] - }, - { - "id": "container-elements", - "sentence": "Its OpenAPI schema is the one ASP.NET Core publishes for the underlying type.", - "axes": { - "shape.position": [ - "list-element", - "array-element", - "immutable-array-element", - "read-only-list-element", - "dictionary-value" - ] - }, - "inapplicable": [ - { - "axes": { - "json.Converters": [ - "own-for-value-object" - ] - }, - "cite": "not-promised:own-converter-for-the-value-object" - }, - { - "axes": { - "json.Converters": [ - "own-for-wrapped" - ] - }, - "cite": "not-promised:own-converter-for-the-wrapped-type" - }, - { - "axes": { - "json.ReferenceHandler": [ - "Preserve" - ] - }, - "cite": "record:0099" - }, - { - "axes": { - "json.NumberHandling": [ - "WriteAsString" - ], - "shape.wrapped": [ - "char" - ] - }, - "cite": "fact:KB-042" - }, - { - "axes": { - "json.MaxDepth": [ - "3" - ], - "shape.wrapped": [ - "own-record-class" - ] - }, - "cite": "record:0099" - } - ] - }, - { - "id": "dictionary-key", - "sentence": "Its OpenAPI schema is the one ASP.NET Core publishes for the underlying type.", - "axes": { - "shape.position": [ - "dictionary-key" - ] - }, - "inapplicable": [ - { - "axes": { - "json.Converters": [ - "own-for-value-object" - ] - }, - "cite": "not-promised:own-converter-for-the-value-object" - }, - { - "axes": { - "json.Converters": [ - "own-for-wrapped" - ] - }, - "cite": "not-promised:own-converter-for-the-wrapped-type" - }, - { - "axes": { - "json.ReferenceHandler": [ - "Preserve" - ] - }, - "cite": "record:0099" - }, - { - "axes": { - "json.NumberHandling": [ - "WriteAsString" - ], - "shape.wrapped": [ - "char" - ] - }, - "cite": "fact:KB-042" - }, - { - "axes": { - "json.MaxDepth": [ - "3" - ], - "shape.wrapped": [ - "own-record-class" - ] - }, - "cite": "record:0099" - } - ] - }, - { - "id": "every-document-version", - "sentence": "Its OpenAPI schema is the one ASP.NET Core publishes for the underlying type.", - "axes": { - "openapi.OpenApiVersion": [ - "OpenApi2_0", - "OpenApi3_0" - ] - }, - "inapplicable": [ - { - "axes": { - "json.Converters": [ - "own-for-value-object" - ] - }, - "cite": "not-promised:own-converter-for-the-value-object" - }, - { - "axes": { - "json.Converters": [ - "own-for-wrapped" - ] - }, - "cite": "not-promised:own-converter-for-the-wrapped-type" - }, - { - "axes": { - "json.ReferenceHandler": [ - "Preserve" - ] - }, - "cite": "record:0099" - }, - { - "axes": { - "json.NumberHandling": [ - "WriteAsString" - ], - "shape.wrapped": [ - "char" - ] - }, - "cite": "fact:KB-042" - }, - { - "axes": { - "json.MaxDepth": [ - "3" - ], - "shape.wrapped": [ - "own-record-class" - ] - }, - "cite": "record:0099" - } - ] - } - ], - "not-promised": [ - { - "id": "a-closed-set-is-a-string-enum", - "source": "README.md#what-you-get", - "sentence": "One line per member gives you parsing by name, JSON as the name, an OpenAPI string enum and a text column.", - "axes": { - "shape.kind": [ - "closed-set" - ] - } - }, - { - "id": "own-converter-for-the-value-object", - "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says", - "sentence": "That converter then writes it instead of the generated one, so it's described as that converter says (see \"A described type\" below).", - "axes": { - "json.Converters": [ - "own-for-value-object" - ] - } - }, - { - "id": "own-converter-for-the-wrapped-type", - "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says", - "sentence": "ASP.NET can't see through a JSON converter you write yourself. It writes the type as `{}`, or with only the `format` its CLR type maps to. It also drops a list's `items` and a dictionary's `additionalProperties` of that type.", - "axes": { - "json.Converters": [ - "own-for-wrapped" - ] - } - }, - { - "id": "a-class-value-object-admits-null", - "source": "design/adr/0036-nullable-value-object-admits-null-openapi.md#0036-a-nullable-value-object-admits-null-in-the-openapi-document-wherever-it-appears", - "sentence": "A nullable value object admits null in the OpenAPI document wherever it appears", - "axes": { - "shape.kind": [ - "sealed-record-class", - "validated-class" - ] - } - }, - { - "id": "a-nullable-value-object-admits-null", - "source": "design/adr/0036-nullable-value-object-admits-null-openapi.md#0036-a-nullable-value-object-admits-null-in-the-openapi-document-wherever-it-appears", - "sentence": "A nullable value object admits null in the OpenAPI document wherever it appears", - "axes": { - "http.nullability": [ - "nullable" - ] - } - }, - { - "id": "openapi-2-nullable-component-loses-its-type", - "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says", - "sentence": "So under 2.0 a nullable value-object property loses its type, where the underlying type's nullable property keeps it.", - "axes": { - "openapi.OpenApiVersion": [ - "OpenApi2_0" - ], - "openapi.CreateSchemaReferenceId": [ - "default", - "own-prefixing" - ] - } - }, - { - "id": "enum-component-is-its-first-use", - "source": "design/knowledge/KB-027-an-enum-component-is-its-first-use.md#kb-027-the-component-aspnet-makes-for-an-enum-written-by-name-is-the-schema-of-the-first-use-it-meets", - "sentence": "So whether a parameter of the wrapped `E?` admits null in a document with components depends on the endpoints and their order, not on the parameter.", - "axes": { - "json.Converters": [ - "JsonStringEnumConverter" - ], - "http.nullability": [ - "nullable" - ], - "shape.wrapped": [ - "enum" - ] - } - }, - { - "id": "openapi-2-nullable-body-keeps-only-its-null", - "source": "design/knowledge/KB-001-openapi-2-nullable-body-drops-pattern.md#kb-001-under-openapi-20-aspnet-writes-a-nullable-request-body-as-allof-x-nullable-true-and-drops-its-pattern", - "sentence": "A minimal API writes it the same, and a nullable enum body, which has no `format`, keeps nothing but the null:", - "axes": { - "openapi.OpenApiVersion": [ - "OpenApi2_0" - ], - "http.binding": [ - "body" - ], - "http.nullability": [ - "nullable" - ], - "shape.kind": [ - "sealed-record-class", - "validated-class" - ] - } - }, - { - "id": "enum-component-lists-the-null-of-its-first-use", - "source": "design/knowledge/KB-027-an-enum-component-is-its-first-use.md#kb-027-the-component-aspnet-makes-for-an-enum-written-by-name-is-the-schema-of-the-first-use-it-meets", - "sentence": "What the component says is whatever schema ASP.NET built for the first use it met: met first as a nullable parameter, the component lists null among its values, for its non-nullable uses too; met first as itself, it does not, for its nullable uses too.", - "axes": { - "json.Converters": [ - "JsonStringEnumConverter" - ], - "shape.wrapped": [ - "enum" - ] - } - }, - { - "id": "openapi-2-nullable-element-loses-its-type", - "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says", - "sentence": "A nullable element of a list or a dictionary loses its type the same way, since it's the component or null too.", - "axes": { - "openapi.OpenApiVersion": [ - "OpenApi2_0" - ], - "openapi.CreateSchemaReferenceId": [ - "default", - "own-prefixing" - ], - "shape.position": [ - "list-element", - "array-element", - "immutable-array-element", - "read-only-list-element", - "dictionary-value" - ] - } - }, - { - "id": "a-librarys-converter-for-the-wrapped-type", - "source": "src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says", - "sentence": "The same goes for a converter a library registers for its own type, such as NodaTime's for `Instant`.", - "axes": { - "shape.wrapped": [ - "NodaTime.Instant" - ] - } - } - ], - "exclude": [ - { - "reason": "System.Text.Json refuses PreferredObjectCreationHandling.Populate together with a ReferenceHandler on the options object; JsonRow substitutes the default, so the pair never applies.", - "pairs": [ - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=Preserve" - ], - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=IgnoreCycles" - ] - ] - }, - { - "reason": "Not promised (a-closed-set-is-a-string-enum): a closed set wraps nothing and is described as the string enum of its names, so it has no underlying type's schema to equal.", - "pairs": [ - [ - "http.binding=route", - "shape.kind=closed-set" - ], - [ - "http.binding=query", - "shape.kind=closed-set" - ], - [ - "http.binding=header", - "shape.kind=closed-set" - ], - [ - "http.binding=body", - "shape.kind=closed-set" - ], - [ - "http.binding=form", - "shape.kind=closed-set" - ], - [ - "http.binding=as-parameters", - "shape.kind=closed-set" - ], - [ - "http.host=minimal", - "shape.kind=closed-set" - ], - [ - "http.host=mvc", - "shape.kind=closed-set" - ], - [ - "http.host=both", - "shape.kind=closed-set" - ], - [ - "http.host=route-group", - "shape.kind=closed-set" - ], - [ - "http.host=controller-without-ApiController-visible", - "shape.kind=closed-set" - ], - [ - "http.nullability=as-declared", - "shape.kind=closed-set" - ], - [ - "http.nullability=nullable", - "shape.kind=closed-set" - ], - [ - "http.response=result-to-http-result", - "shape.kind=closed-set" - ], - [ - "http.response=composed-fault", - "shape.kind=closed-set" - ], - [ - "http.response=two-faults-same-name", - "shape.kind=closed-set" - ], - [ - "http.response=group-with-fault-status", - "shape.kind=closed-set" - ], - [ - "http.response=endpoint-without-faults", - "shape.kind=closed-set" - ], - [ - "http.response=declared-produces-problem", - "shape.kind=closed-set" - ], - [ - "input.json=valid", - "shape.kind=closed-set" - ], - [ - "input.json=refused-by-create", - "shape.kind=closed-set" - ], - [ - "input.json=wrong-token", - "shape.kind=closed-set" - ], - [ - "input.json=null", - "shape.kind=closed-set" - ], - [ - "input.json=empty-string", - "shape.kind=closed-set" - ], - [ - "input.json=at-type-limit", - "shape.kind=closed-set" - ], - [ - "input.json=quoted-number", - "shape.kind=closed-set" - ], - [ - "input.json=escape-sequence", - "shape.kind=closed-set" - ], - [ - "input.json=duplicate-property", - "shape.kind=closed-set" - ], - [ - "input.json=metadata-property", - "shape.kind=closed-set" - ], - [ - "json.AllowDuplicateProperties=default", - "shape.kind=closed-set" - ], - [ - "json.AllowDuplicateProperties=false", - "shape.kind=closed-set" - ], - [ - "json.AllowOutOfOrderMetadataProperties=default", - "shape.kind=closed-set" - ], - [ - "json.AllowOutOfOrderMetadataProperties=true", - "shape.kind=closed-set" - ], - [ - "json.AllowTrailingCommas=default", - "shape.kind=closed-set" - ], - [ - "json.AllowTrailingCommas=true", - "shape.kind=closed-set" - ], - [ - "json.Converters=default", - "shape.kind=closed-set" - ], - [ - "json.Converters=JsonStringEnumConverter", - "shape.kind=closed-set" - ], - [ - "json.Converters=StoredJsonConverterFactory", - "shape.kind=closed-set" - ], - [ - "json.DefaultIgnoreCondition=default", - "shape.kind=closed-set" - ], - [ - "json.DefaultIgnoreCondition=WhenWritingDefault", - "shape.kind=closed-set" - ], - [ - "json.DefaultIgnoreCondition=WhenWritingNull", - "shape.kind=closed-set" - ], - [ - "json.DefaultIgnoreCondition=WhenWriting", - "shape.kind=closed-set" - ], - [ - "json.DefaultIgnoreCondition=WhenReading", - "shape.kind=closed-set" - ], - [ - "json.DictionaryKeyPolicy=default", - "shape.kind=closed-set" - ], - [ - "json.DictionaryKeyPolicy=CamelCase", - "shape.kind=closed-set" - ], - [ - "json.DictionaryKeyPolicy=SnakeCaseLower", - "shape.kind=closed-set" - ], - [ - "json.DictionaryKeyPolicy=KebabCaseUpper", - "shape.kind=closed-set" - ], - [ - "json.DictionaryKeyPolicy=own-upper-case", - "shape.kind=closed-set" - ], - [ - "json.Encoder=default", - "shape.kind=closed-set" - ], - [ - "json.Encoder=UnsafeRelaxedJsonEscaping", - "shape.kind=closed-set" - ], - [ - "json.IgnoreReadOnlyFields=default", - "shape.kind=closed-set" - ], - [ - "json.IgnoreReadOnlyFields=true", - "shape.kind=closed-set" - ], - [ - "json.IgnoreReadOnlyProperties=default", - "shape.kind=closed-set" - ], - [ - "json.IgnoreReadOnlyProperties=true", - "shape.kind=closed-set" - ], - [ - "json.IncludeFields=default", - "shape.kind=closed-set" - ], - [ - "json.IncludeFields=true", - "shape.kind=closed-set" - ], - [ - "json.MaxDepth=default", - "shape.kind=closed-set" - ], - [ - "json.MaxDepth=3", - "shape.kind=closed-set" - ], - [ - "json.NumberHandling=default", - "shape.kind=closed-set" - ], - [ - "json.NumberHandling=AllowReadingFromString", - "shape.kind=closed-set" - ], - [ - "json.NumberHandling=WriteAsString", - "shape.kind=closed-set" - ], - [ - "json.NumberHandling=AllowNamedFloatingPointLiterals", - "shape.kind=closed-set" - ], - [ - "json.PreferredObjectCreationHandling=default", - "shape.kind=closed-set" - ], - [ - "json.PreferredObjectCreationHandling=Populate", - "shape.kind=closed-set" - ], - [ - "json.PropertyNameCaseInsensitive=default", - "shape.kind=closed-set" - ], - [ - "json.PropertyNameCaseInsensitive=true", - "shape.kind=closed-set" - ], - [ - "json.PropertyNamingPolicy=default", - "shape.kind=closed-set" - ], - [ - "json.PropertyNamingPolicy=CamelCase", - "shape.kind=closed-set" - ], - [ - "json.PropertyNamingPolicy=SnakeCaseLower", - "shape.kind=closed-set" - ], - [ - "json.PropertyNamingPolicy=SnakeCaseUpper", - "shape.kind=closed-set" - ], - [ - "json.PropertyNamingPolicy=KebabCaseLower", - "shape.kind=closed-set" - ], - [ - "json.PropertyNamingPolicy=KebabCaseUpper", - "shape.kind=closed-set" - ], - [ - "json.PropertyNamingPolicy=own-upper-case", - "shape.kind=closed-set" - ], - [ - "json.ReadCommentHandling=default", - "shape.kind=closed-set" - ], - [ - "json.ReadCommentHandling=Skip", - "shape.kind=closed-set" - ], - [ - "json.ReferenceHandler=default", - "shape.kind=closed-set" - ], - [ - "json.ReferenceHandler=Preserve", - "shape.kind=closed-set" - ], - [ - "json.ReferenceHandler=IgnoreCycles", - "shape.kind=closed-set" - ], - [ - "json.RespectNullableAnnotations=default", - "shape.kind=closed-set" - ], - [ - "json.RespectNullableAnnotations=true", - "shape.kind=closed-set" - ], - [ - "json.RespectRequiredConstructorParameters=default", - "shape.kind=closed-set" - ], - [ - "json.RespectRequiredConstructorParameters=true", - "shape.kind=closed-set" - ], - [ - "json.TypeInfoResolver=default", - "shape.kind=closed-set" - ], - [ - "json.TypeInfoResolver=source-generated-context", - "shape.kind=closed-set" - ], - [ - "json.TypeInfoResolver=default-with-renaming-modifier", - "shape.kind=closed-set" - ], - [ - "json.TypeInfoResolverChain=default", - "shape.kind=closed-set" - ], - [ - "json.TypeInfoResolverChain=context-then-default", - "shape.kind=closed-set" - ], - [ - "json.UnknownTypeHandling=default", - "shape.kind=closed-set" - ], - [ - "json.UnknownTypeHandling=JsonNode", - "shape.kind=closed-set" - ], - [ - "json.UnmappedMemberHandling=default", - "shape.kind=closed-set" - ], - [ - "json.UnmappedMemberHandling=Disallow", - "shape.kind=closed-set" - ], - [ - "mvc.AllowEmptyInputInBodyModelBinding=default", - "shape.kind=closed-set" - ], - [ - "mvc.AllowEmptyInputInBodyModelBinding=true", - "shape.kind=closed-set" - ], - [ - "mvc.Conventions=default", - "shape.kind=closed-set" - ], - [ - "mvc.Conventions=own-route-prefix", - "shape.kind=closed-set" - ], - [ - "mvc.InputFormatters=default", - "shape.kind=closed-set" - ], - [ - "mvc.InputFormatters=xml-added", - "shape.kind=closed-set" - ], - [ - "mvc.MaxModelBindingCollectionSize=default", - "shape.kind=closed-set" - ], - [ - "mvc.MaxModelBindingCollectionSize=2", - "shape.kind=closed-set" - ], - [ - "mvc.MaxModelValidationErrors=default", - "shape.kind=closed-set" - ], - [ - "mvc.MaxModelValidationErrors=1", - "shape.kind=closed-set" - ], - [ - "mvc.MaxValidationDepth=default", - "shape.kind=closed-set" - ], - [ - "mvc.MaxValidationDepth=1", - "shape.kind=closed-set" - ], - [ - "mvc.ModelBinderProviders=default", - "shape.kind=closed-set" - ], - [ - "mvc.ModelBinderProviders=own-provider-first", - "shape.kind=closed-set" - ], - [ - "mvc.OutputFormatters=default", - "shape.kind=closed-set" - ], - [ - "mvc.OutputFormatters=xml-added", - "shape.kind=closed-set" - ], - [ - "mvc.RespectBrowserAcceptHeader=default", - "shape.kind=closed-set" - ], - [ - "mvc.RespectBrowserAcceptHeader=true", - "shape.kind=closed-set" - ], - [ - "mvc.ReturnHttpNotAcceptable=default", - "shape.kind=closed-set" - ], - [ - "mvc.ReturnHttpNotAcceptable=true", - "shape.kind=closed-set" - ], - [ - "mvc.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes=default", - "shape.kind=closed-set" - ], - [ - "mvc.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes=true", - "shape.kind=closed-set" - ], - [ - "mvc.ValidateComplexTypesIfChildValidationFails=default", - "shape.kind=closed-set" - ], - [ - "mvc.ValidateComplexTypesIfChildValidationFails=true", - "shape.kind=closed-set" - ], - [ - "openapi.CreateSchemaReferenceId=default", - "shape.kind=closed-set" - ], - [ - "openapi.CreateSchemaReferenceId=own-prefixing", - "shape.kind=closed-set" - ], - [ - "openapi.CreateSchemaReferenceId=null-inlines-all", - "shape.kind=closed-set" - ], - [ - "openapi.OpenApiVersion=default", - "shape.kind=closed-set" - ], - [ - "openapi.OpenApiVersion=OpenApi2_0", - "shape.kind=closed-set" - ], - [ - "openapi.OpenApiVersion=OpenApi3_0", - "shape.kind=closed-set" - ], - [ - "openapi.transformers=default", - "shape.kind=closed-set" - ], - [ - "openapi.transformers=own-schema-transformer", - "shape.kind=closed-set" - ], - [ - "openapi.transformers=own-operation-transformer", - "shape.kind=closed-set" - ], - [ - "openapi.transformers=own-document-transformer", - "shape.kind=closed-set" - ], - [ - "process.converter-lookup=none", - "shape.kind=closed-set" - ], - [ - "process.converter-lookup=wrapped-type-converter-looked-up-first", - "shape.kind=closed-set" - ], - [ - "shape.containing=class", - "shape.kind=closed-set" - ], - [ - "shape.containing=record-class", - "shape.kind=closed-set" - ], - [ - "shape.containing=struct", - "shape.kind=closed-set" - ], - [ - "shape.containing=record-struct", - "shape.kind=closed-set" - ], - [ - "shape.containing=generic-type", - "shape.kind=closed-set" - ], - [ - "shape.kind=closed-set", - "shape.position=root" - ], - [ - "shape.kind=closed-set", - "shape.position=property" - ], - [ - "shape.kind=closed-set", - "shape.position=nullable-property" - ], - [ - "shape.kind=closed-set", - "shape.position=field" - ], - [ - "shape.kind=closed-set", - "shape.position=list-element" - ], - [ - "shape.kind=closed-set", - "shape.position=array-element" - ], - [ - "shape.kind=closed-set", - "shape.position=immutable-array-element" - ], - [ - "shape.kind=closed-set", - "shape.position=read-only-list-element" - ], - [ - "shape.kind=closed-set", - "shape.position=dictionary-key" - ], - [ - "shape.kind=closed-set", - "shape.position=dictionary-value" - ], - [ - "shape.kind=closed-set", - "shape.position=record-ctor-parameter" - ], - [ - "shape.kind=closed-set", - "shape.position=required-init" - ], - [ - "shape.kind=closed-set", - "shape.position=in-patch" - ], - [ - "shape.kind=closed-set", - "shape.position=polymorphic-derived-property" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=string" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=Guid" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=int" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=long" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=byte" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=ulong" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=decimal" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=double" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=bool" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=char" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=DateTime" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=DateOnly" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=TimeOnly" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=TimeSpan" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=enum" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=flags-enum" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=Uri" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=own-record-class" - ] - ] - } - ] - }, - { - "id": "E01-column-parity", - "source": "docs/ef-core.md#the-model", - "surfaces": [ - "ef" - ], - "axes": { - "ef.mapping": [ - "property", - "nullable", - "key", - "owned", - "complex" - ], - "ef.facet": [ - "none", - "HasMaxLength", - "HavePrecision", - "HasColumnType" - ], - "shape.wrapped": [ - "string", - "Guid", - "int", - "long", - "byte", - "ulong", - "decimal", - "double", - "bool", - "char", - "DateTime", - "DateTimeOffset", - "DateOnly", - "TimeOnly", - "TimeSpan", - "enum", - "flags-enum", - "Uri" - ], - "shape.kind": [ - "record-struct", - "sealed-record-class", - "validated-struct", - "validated-class" - ] - }, - "narrow": { - "ef.query": "Every property maps as a column of the underlying type, nullable where the property is." - }, - "obligations": [ - { - "id": "column-type", - "sentence": "Every property maps as a column of the underlying type, nullable where the property is.", - "axes": { - "ef.mapping": [ - "property", - "key", - "owned", - "complex" - ] - } - }, - { - "id": "nullable", - "sentence": "nullable where the property is", - "axes": { - "ef.mapping": [ - "nullable" - ] - } - }, - { - "id": "facet-kept", - "sentence": "A facet you configure on it is kept: a precision, a length, a column type.", - "axes": { - "ef.facet": [ - "HasMaxLength", - "HavePrecision", - "HasColumnType" - ] - } - } - ], - "not-promised": [ - { - "id": "char-key-not-generated", - "source": "docs/ef-core.md#keys", - "sentence": "A key over `char` is always yours to assign", - "axes": { - "ef.mapping": [ - "key" - ], - "shape.wrapped": [ - "char" - ] - } - }, - { - "id": "guid-key-not-generated", - "source": "docs/ef-core.md#keys", - "sentence": "A key over a `Guid` is yours to assign", - "axes": { - "ef.mapping": [ - "key" - ], - "shape.wrapped": [ - "Guid" - ] - } - } - ] - }, - { - "id": "E02-value-translates-to-column", - "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#queries", - "surfaces": [ - "ef" - ], - "axes": { - "ef.query": [ - "where-equals", - "where-value-compare", - "order-by", - "contains", - "select", - "group-by" - ], - "ef.accessor": [ - "Value", - "GetValue", - "ValueOrNull" - ], - "shape.wrapped": [ - "string", - "Guid", - "int", - "long", - "byte", - "ulong", - "decimal", - "double", - "bool", - "char", - "DateTime", - "DateTimeOffset", - "DateOnly", - "TimeOnly", - "TimeSpan", - "enum", - "flags-enum", - "Uri" - ], - "shape.kind": [ - "record-struct", - "sealed-record-class", - "validated-struct", - "validated-class" - ] - }, - "narrow": { - "ef.mapping": "`.Value`, `GetValue()` and `ValueOrNull()` translate to the column itself." - }, - "obligations": [ - { - "id": "value", - "sentence": "`.Value`, `GetValue()` and `ValueOrNull()` translate to the column itself.", - "axes": { - "ef.accessor": [ - "Value" - ] - }, - "inapplicable": [ - { - "axes": { - "shape.wrapped": [ - "DateTimeOffset", - "TimeSpan", - "ulong" - ], - "ef.query": [ - "where-value-compare", - "order-by" - ] - }, - "cite": "fact:KB-039" - } - ] - }, - { - "id": "get-value", - "sentence": "db.Orders.Where(o => o.Total.GetValue() > 100m);", - "axes": { - "ef.accessor": [ - "GetValue" - ] - }, - "inapplicable": [ - { - "axes": { - "shape.wrapped": [ - "DateTimeOffset", - "TimeSpan", - "ulong" - ], - "ef.query": [ - "where-value-compare", - "order-by" - ] - }, - "cite": "fact:KB-039" - } - ] - }, - { - "id": "value-or-null", - "sentence": "Use `ValueOrNull()` for an optional value object, where `.Value` would first unwrap the `Nullable`.", - "axes": { - "ef.accessor": [ - "ValueOrNull" - ] - }, - "inapplicable": [ - { - "axes": { - "shape.wrapped": [ - "DateTimeOffset", - "TimeSpan", - "ulong" - ], - "ef.query": [ - "where-value-compare", - "order-by" - ] - }, - "cite": "fact:KB-039" - } - ] - }, - { - "id": "column-not-cast", - "sentence": "A table's column is re-typed, not cast, so an index on it still serves the query.", - "inapplicable": [ - { - "axes": { - "shape.wrapped": [ - "DateTimeOffset", - "TimeSpan", - "ulong" - ], - "ef.query": [ - "where-value-compare", - "order-by" - ] - }, - "cite": "fact:KB-039" - } - ] - } - ], - "not-promised": [ - { - "id": "null-passes-no-comparison", - "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#queries", - "sentence": "A null passes no `<`, `<=`, `>` or `>=` in the database.", - "axes": { - "ef.accessor": [ - "ValueOrNull" - ], - "ef.query": [ - "where-value-compare" - ] - } - }, - { - "id": "collation-orders", - "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#queries", - "sentence": "Ordering in SQL follows the database's collation", - "axes": { - "ef.query": [ - "order-by" - ], - "shape.wrapped": [ - "string" - ] - } - } - ], - "exclude": [ - { - "reason": "C# defines no `>=` for a `string`, a `bool` or a `Uri`, so no query compares their `.Value` with it; E02ValueTranslatesProbe writes where-equals in its place there, so no row holds the pair.", - "pairs": [ - [ - "ef.query=where-value-compare", - "shape.wrapped=string" - ], - [ - "ef.query=where-value-compare", - "shape.wrapped=bool" - ], - [ - "ef.query=where-value-compare", - "shape.wrapped=Uri" - ] - ] - } - ] - }, - { - "id": "E03-column-round-trip", - "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#what-maps", - "surfaces": [ - "ef" - ], - "axes": { - "ef.mapping": [ - "property", - "nullable", - "key", - "owned", - "complex" - ], - "shape.wrapped": [ - "string", - "Guid", - "int", - "long", - "byte", - "ulong", - "decimal", - "double", - "bool", - "char", - "DateTime", - "DateTimeOffset", - "DateOnly", - "TimeOnly", - "TimeSpan", - "enum", - "flags-enum", - "Uri" - ], - "shape.kind": [ - "record-struct", - "sealed-record-class", - "validated-struct", - "validated-class" - ] - }, - "narrow": { - "ef.query": "Keys, foreign keys, nullable properties, primitive collections and query parameters all map as plain columns of the underlying type." - }, - "obligations": [ - { - "id": "stored-as-underlying", - "sentence": "Every value object is stored in a plain column of its underlying type", - "axes": { - "ef.mapping": [ - "property", - "owned", - "complex" - ] - } - }, - { - "id": "keys", - "sentence": "Keys, foreign keys, nullable properties, primitive collections and query parameters all map as plain columns of the underlying type.", - "axes": { - "ef.mapping": [ - "key" - ] - } - }, - { - "id": "nullable-properties", - "sentence": "Keys, foreign keys, nullable properties, primitive collections and query parameters all map as plain columns of the underlying type.", - "axes": { - "ef.mapping": [ - "nullable" - ] - } - } - ] - }, - { - "id": "E04-integer-key-generated", - "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#keys", - "surfaces": [ - "ef" - ], - "axes": { - "shape.wrapped": [ - "int", - "long", - "byte", - "ulong", - "char", - "Guid", - "string" - ], - "shape.kind": [ - "record-struct", - "sealed-record-class", - "validated-struct", - "validated-class" - ], - "ef.key-config": [ - "default", - "ValueGeneratedOnAdd", - "ValueGeneratedNever" - ] - }, - "narrow": { - "ef.query": "A single-column key over an integer is generated on add, like a key of that integer type.", - "ef.mapping": "A single-column key over an integer is generated on add, like a key of that integer type.", - "shape.site": "A single-column key over an integer is generated on add, like a key of that integer type." - }, - "obligations": [ - { - "id": "generated-on-add", - "sentence": "A single-column key over an integer is generated on add, like a key of that integer type.", - "axes": { - "shape.wrapped": [ - "int", - "long", - "ulong" - ] - } - }, - { - "id": "byte-not-generated", - "sentence": "That's EF's own list: `int`, `long`, `short`, `sbyte`, `uint`, `ulong` and `ushort`, and not `byte`.", - "axes": { - "shape.wrapped": [ - "byte" - ] - } - }, - { - "id": "char-key-yours", - "sentence": "A key over `char` is yours to assign.", - "axes": { - "shape.wrapped": [ - "char" - ], - "ef.key-config": [ - "default" - ] - } - }, - { - "id": "guid-key-yours", - "sentence": "A key over a `Guid` is yours to assign", - "axes": { - "shape.wrapped": [ - "Guid" - ], - "ef.key-config": [ - "default" - ] - } - }, - { - "id": "not-on-efs-list", - "sentence": "That's EF's own list: `int`, `long`, `short`, `sbyte`, `uint`, `ulong` and `ushort`, and not `byte`.", - "axes": { - "shape.wrapped": [ - "string" - ], - "ef.key-config": [ - "default" - ] - } - }, - { - "id": "own-configuration-wins", - "sentence": "A configuration you write yourself always wins over these defaults.", - "axes": { - "shape.wrapped": [ - "char", - "Guid", - "string" - ], - "ef.key-config": [ - "ValueGeneratedOnAdd", - "ValueGeneratedNever" - ] - } - } - ], - "not-promised": [ - { - "id": "meaningful-char-key-unchecked", - "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#keys", - "sentence": "TypeKit stops that invented value, but it doesn't check that you set a meaningful one.", - "axes": { - "shape.wrapped": [ - "char" - ], - "ef.key-config": [ - "default" - ] - } - }, - { - "id": "ulong-key-on-a-decimal-column", - "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#keys", - "sentence": "A default key over `ulong` throws at `Add` where the column is a `decimal`", - "axes": { - "shape.wrapped": [ - "ulong" - ], - "provider": [ - "SQL Server", - "PostgreSQL" - ] - } - } - ] - }, - { - "id": "E05-own-compareto-rejected", - "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#queries", - "surfaces": [ - "ef" - ], - "axes": { - "ef.op": [ - "lt", - "gt", - "le", - "ge", - "CompareTo", - "OrderBy", - "OrderByDescending", - "ThenBy", - "Order", - "Min", - "Max", - "equals", - "HasQueryFilter", - "named-query-filter", - "value-order" - ], - "ef.vo": [ - "int-struct", - "string-struct", - "decimal-struct", - "DateTime-struct", - "int-class", - "string-class" - ] - }, - "narrow": { - "ef.query": "Your own `CompareTo(TSelf)` can't run in the database.", - "ef.mapping": "Your own `CompareTo(TSelf)` can't run in the database.", - "shape.kind": "Your own `CompareTo(TSelf)` can't run in the database.", - "shape.wrapped": "Your own `CompareTo(TSelf)` can't run in the database.", - "shape.site": "Your own `CompareTo(TSelf)` can't run in the database." - }, - "obligations": [ - { - "id": "comparison", - "sentence": "a comparison (`<`, `>`, `<=`, `>=`, `CompareTo`);", - "axes": { - "ef.op": [ - "lt", - "gt", - "le", - "ge", - "CompareTo" - ] - } - }, - { - "id": "ordering", - "sentence": "an ordering (`OrderBy`, `ThenBy`, their descending forms, `Order`);", - "axes": { - "ef.op": [ - "OrderBy", - "OrderByDescending", - "ThenBy", - "Order" - ] - } - }, - { - "id": "min-max", - "sentence": "`Min` or `Max`.", - "axes": { - "ef.op": [ - "Min", - "Max" - ] - } - }, - { - "id": "equality-allowed", - "sentence": "Equality isn't ordering, and stays allowed.", - "axes": { - "ef.op": [ - "equals" - ] - } - }, - { - "id": "value-order-allowed", - "sentence": "Compare or order by `.Value` where the underlying value's order is what you mean", - "axes": { - "ef.op": [ - "value-order" - ] - } - }, - { - "id": "query-filter", - "sentence": "A query filter (`HasQueryFilter`, named or not) that does the same fails when the model is built, naming the entity type, since it applies to every query of that type.", - "axes": { - "ef.op": [ - "HasQueryFilter", - "named-query-filter" - ] - } - } - ], - "not-promised": [ - { - "id": "collation-orders", - "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#queries", - "sentence": "Ordering in SQL follows the database's collation", - "axes": { - "ef.op": [ - "value-order" - ], - "ef.vo": [ - "string-struct", - "string-class" - ] - } - } - ] - }, - { - "id": "E06-own-converter-no-value", - "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#queries", - "surfaces": [ - "ef" - ], - "axes": { - "ef.converter": [ - "HasConversion-lambda", - "converter-type", - "ConfigureConventions" - ], - "ef.op": [ - "value-access", - "vo-equality" - ], - "shape.wrapped": [ - "string", - "int", - "Guid", - "decimal" - ] - }, - "narrow": { - "ef.query": "A property your application converts itself", - "ef.mapping": "A property your application converts itself", - "shape.kind": "A property your application converts itself", - "shape.site": "A property your application converts itself" - }, - "obligations": [ - { - "id": "value-not-translated", - "sentence": "So a filter, a sort, a grouping or an aggregate that reads `.Value` there fails with EF's \"could not be translated\", rather than comparing against what the column really holds.", - "axes": { - "ef.op": [ - "value-access" - ] - } - }, - { - "id": "value-object-comparison", - "sentence": "Comparing the value object itself (`o.Code == code`) goes through that converter and still works.", - "axes": { - "ef.op": [ - "vo-equality" - ] - } - } - ] - }, - { - "id": "E07-closed-set-stored-as-name", - "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#closed-sets", - "surfaces": [ - "ef" - ], - "axes": { - "ef.site": [ - "table", - "primitive-collection", - "json-column" - ], - "ef.event": [ - "round-trip", - "unknown-name-on-read", - "non-member-on-write" - ] - }, - "narrow": { - "ef.query": "A closed set (`IClosedSet`) is stored as a text column of the member's name, and read back as the member itself.", - "ef.mapping": "A closed set (`IClosedSet`) is stored as a text column of the member's name, and read back as the member itself.", - "shape.kind": "A closed set (`IClosedSet`) is stored as a text column of the member's name, and read back as the member itself.", - "shape.wrapped": "A closed set (`IClosedSet`) is stored as a text column of the member's name, and read back as the member itself.", - "shape.site": "A closed set (`IClosedSet`) is stored as a text column of the member's name, and read back as the member itself." - }, - "obligations": [ - { - "id": "stored-as-name", - "sentence": "A closed set (`IClosedSet`) is stored as a text column of the member's name, and read back as the member itself.", - "axes": { - "ef.event": [ - "round-trip" - ] - } - }, - { - "id": "in-collections-and-json", - "sentence": "That holds in a table, in a primitive collection of names (`text[]` on PostgreSQL, a JSON array elsewhere) and in a JSON column alike, with nothing registered per set.", - "axes": { - "ef.site": [ - "primitive-collection", - "json-column" - ], - "ef.event": [ - "round-trip" - ] - } - }, - { - "id": "unknown-name-throws", - "sentence": "A name the set no longer has throws when it's read, rather than reading as null.", - "axes": { - "ef.event": [ - "unknown-name-on-read" - ] - } - }, - { - "id": "non-member-throws", - "sentence": "An instance that isn't a member throws when it's written.", - "axes": { - "ef.event": [ - "non-member-on-write" - ] - } - } - ] - }, - { - "id": "E08-option-result-not-columns", - "source": "docs/ef-core.md#option-and-result", - "surfaces": [ - "ef" - ], - "axes": { - "ef.type": [ - "Option-int", - "Option-VO", - "Result-int-E", - "Result-VO-E", - "Result-E" - ], - "ef.position": [ - "property", - "key", - "collection", - "owned-property" - ], - "shape.wrapped": [ - "string", - "int", - "Guid" - ] - }, - "narrow": { - "ef.query": "EF can't map an `Option` or a `Result` property, and building the model fails with an error that says so.", - "ef.mapping": "EF can't map an `Option` or a `Result` property, and building the model fails with an error that says so.", - "shape.kind": "EF can't map an `Option` or a `Result` property, and building the model fails with an error that says so.", - "shape.site": "EF can't map an `Option` or a `Result` property, and building the model fails with an error that says so." - }, - "obligations": [ - { - "id": "model-fails", - "sentence": "EF can't map an `Option` or a `Result` property" - }, - { - "id": "error-says-so", - "sentence": "building the model fails with an error that says so" - } - ] - }, - { - "id": "E09-or-none-in-database", - "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#an-option-for-nothing-found", - "surfaces": [ - "ef" - ], - "axes": { - "ef.operator": [ - "FirstOrNoneAsync", - "FirstOrNone", - "SingleOrNoneAsync", - "SingleOrNone", - "ElementAtOrNone", - "FirstOrError" - ], - "ef.data": [ - "empty", - "one-match", - "many-match" - ], - "ef.condition": [ - "lambda", - "expression-variable" - ], - "shape.wrapped": [ - "int", - "Guid", - "string" - ] - }, - "narrow": { - "ef.query": "These ask the database for one row and let it run the condition:", - "ef.mapping": "These ask the database for one row and let it run the condition:", - "shape.kind": "These ask the database for one row and let it run the condition:", - "shape.site": "These ask the database for one row and let it run the condition:" - }, - "obligations": [ - { - "id": "first-reads-one-row", - "sentence": "read one row: `WHERE ... LIMIT @p`.", - "axes": { - "ef.operator": [ - "FirstOrNoneAsync", - "FirstOrNone", - "FirstOrError" - ] - } - }, - { - "id": "single-asks-for-two", - "sentence": "ask for two rows, and throw for more than one.", - "axes": { - "ef.operator": [ - "SingleOrNoneAsync", - "SingleOrNone" - ] - } - }, - { - "id": "element-at-skips", - "sentence": "skips to its row: `OFFSET @p`.", - "axes": { - "ef.operator": [ - "ElementAtOrNone" - ] - } - } - ], - "not-promised": [ - { - "id": "func-condition-runs-in-memory", - "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#an-option-for-nothing-found", - "sentence": "Then every row is read and the condition runs in memory, without a warning.", - "axes": { - "ef.condition": [ - "func-variable", - "method-group" - ] - } - }, - { - "id": "last-on-a-query-does-not-compile", - "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#an-option-for-nothing-found", - "sentence": "`LastOrNone` and `LastOrError` on a query don't compile.", - "axes": { - "ef.operator": [ - "LastOrNone", - "LastOrError" - ] - } - }, - { - "id": "not-precompiled-under-native-aot", - "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#an-option-for-nothing-found", - "sentence": "Under Native AOT these throw \"Query wasn't precompiled\"", - "axes": { - "process": [ - "native-aot" - ] - } - } - ] - }, - { - "id": "E10-read-back-skips-validation", - "source": "docs/ef-core.md#reading-back-without-validation", - "surfaces": [ - "ef" - ], - "axes": { - "ef.mapping": [ - "property", - "key", - "nullable", - "owned", - "complex" - ], - "shape.wrapped": [ - "string", - "int", - "Guid", - "decimal", - "DateTime", - "Uri" - ], - "shape.kind": [ - "validated-struct", - "validated-class" - ] - }, - "narrow": { - "ef.query": "EF reads a column back without validation, through the value object's `Materialize` rather than `Create`.", - "shape.site": "EF reads a column back without validation, through the value object's `Materialize` rather than `Create`." - }, - "obligations": [ - { - "id": "reads-without-create", - "sentence": "EF reads a column back without validation, through the value object's `Materialize` rather than `Create`." - }, - { - "id": "revalidate-finds", - "sentence": "`Revalidate()` finds the rows that today's rules reject, as above." - } - ] - }, - { - "id": "E11-usetypekit-order", - "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#setup", - "surfaces": [ - "ef" - ], - "axes": { - "ef.order": [ - "before-provider", - "after-provider" - ], - "ef.mapping": [ - "property", - "key", - "integer-key", - "nullable", - "owned", - "complex", - "closed-set" - ] - }, - "narrow": { - "ef.query": "it doesn't matter whether you call it before or after `UseNpgsql` or `UseSqlite`", - "shape.kind": "it doesn't matter whether you call it before or after `UseNpgsql` or `UseSqlite`", - "shape.site": "it doesn't matter whether you call it before or after `UseNpgsql` or `UseSqlite`", - "shape.wrapped": "it doesn't matter whether you call it before or after `UseNpgsql` or `UseSqlite`" - }, - "obligations": [ - { - "id": "order-does-not-matter", - "sentence": "it doesn't matter whether you call it before or after `UseNpgsql` or `UseSqlite`" - } - ], - "not-promised": [ - { - "id": "in-memory-provider-maps-nothing", - "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#setup", - "sentence": "EF's in-memory provider has no relational type mapping, so nothing is mapped there.", - "axes": { - "ef.provider": [ - "InMemory" - ] - } - } - ] - }, - { - "id": "E12-convention-by-vo-type", - "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#model-wide-conventions-follow-the-propertys-type", - "surfaces": [ - "ef" - ], - "axes": { - "ef.convention": [ - "HaveMaxLength", - "HavePrecision", - "HaveColumnType" - ], - "ef.convention-type": [ - "value-object", - "underlying" - ], - "shape.wrapped": [ - "string", - "decimal", - "Uri", - "int" - ] - }, - "narrow": { - "ef.query": "Configure the value object instead:", - "ef.mapping": "Configure the value object instead:", - "shape.site": "Configure the value object instead:", - "shape.kind": "Configure the value object instead:" - }, - "obligations": [ - { - "id": "value-object-type-reaches", - "sentence": "Configure the value object instead: `configurationBuilder.Properties().HavePrecision(18, 2)`.", - "axes": { - "ef.convention-type": [ - "value-object" - ] - } - }, - { - "id": "underlying-type-does-not-reach", - "sentence": "So `configurationBuilder.Properties().HavePrecision(18, 2)` reaches `decimal` properties, and not an `Amount` that wraps one.", - "axes": { - "ef.convention-type": [ - "underlying" - ] - } - } - ], - "not-promised": [ - { - "id": "underlying-type-convention", - "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#model-wide-conventions-follow-the-propertys-type", - "sentence": "EF applies `ConfigureConventions` by CLR type.", - "axes": { - "ef.convention-type": [ - "underlying" - ] - } - } - ] - }, - { - "id": "E13-primitive-collections", - "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#queries", - "surfaces": [ - "ef" - ], - "axes": { - "ef.query": [ - "contains", - "any-value-equals" - ], - "shape.wrapped": [ - "string", - "int", - "Guid", - "long", - "decimal" - ] - }, - "narrow": { - "ef.mapping": "db.Orders.Where(o => o.Tags.Contains(tag));", - "shape.site": "db.Orders.Where(o => o.Tags.Contains(tag));", - "shape.kind": "db.Orders.Where(o => o.Tags.Contains(tag));" - }, - "obligations": [ - { - "id": "contains", - "sentence": "db.Orders.Where(o => o.Tags.Contains(tag));", - "axes": { - "ef.query": [ - "contains" - ] - } - }, - { - "id": "element-value", - "sentence": "db.Orders.Where(o => o.Tags.Any(t => t.Value == \"x\"));", - "axes": { - "ef.query": [ - "any-value-equals" - ] - } - } - ], - "not-promised": [ - { - "id": "element-converted-may-cast", - "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#queries", - "sentence": "These are converted instead, which works everywhere and may add a cast:", - "axes": { - "ef.query": [ - "any-value-equals" - ] - } - } - ] - }, - { - "id": "E14-inheritance-key-generation", - "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#keys", - "surfaces": [ - "ef" - ], - "axes": { - "ef.inheritance": [ - "TPH", - "TPT", - "TPC" - ], - "shape.wrapped": [ - "int", - "long", - "Guid" - ] - }, - "narrow": { - "ef.query": "The root of an inheritance hierarchy follows the same rules under TPH and TPT.", - "ef.mapping": "The root of an inheritance hierarchy follows the same rules under TPH and TPT.", - "shape.site": "The root of an inheritance hierarchy follows the same rules under TPH and TPT.", - "shape.kind": "The root of an inheritance hierarchy follows the same rules under TPH and TPT." - }, - "obligations": [ - { - "id": "tph-tpt-root", - "sentence": "The root of an inheritance hierarchy follows the same rules under TPH and TPT.", - "axes": { - "ef.inheritance": [ - "TPH", - "TPT" - ] - } - }, - { - "id": "tpc-assign-on-sqlite", - "sentence": "SQLite has no such sequence, so there you assign it.", - "axes": { - "ef.inheritance": [ - "TPC" - ] - } - } - ], - "not-promised": [ - { - "id": "guid-key-not-generated", - "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#keys", - "sentence": "A key over a `Guid` is yours to assign", - "axes": { - "shape.wrapped": [ - "Guid" - ] - } - } - ] - }, - { - "id": "E15-composite-key", - "source": "docs/ef-core.md#keys", - "surfaces": [ - "ef" - ], - "axes": { - "shape.wrapped": [ - "int", - "Guid", - "string", - "long" - ], - "ef.key-position": [ - "first", - "second" - ] - }, - "narrow": { - "ef.query": "A composite key of value objects is `HasKey(x => new { x.Order, x.Line })`, as for primitives.", - "ef.mapping": "A composite key of value objects is `HasKey(x => new { x.Order, x.Line })`, as for primitives.", - "shape.site": "A composite key of value objects is `HasKey(x => new { x.Order, x.Line })`, as for primitives.", - "shape.kind": "A composite key of value objects is `HasKey(x => new { x.Order, x.Line })`, as for primitives." - }, - "obligations": [ - { - "id": "as-for-primitives", - "sentence": "A composite key of value objects is `HasKey(x => new { x.Order, x.Line })`, as for primitives." - } - ] - }, - { - "id": "E16-own-configuration-wins", - "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#keys", - "surfaces": [ - "ef" - ], - "axes": { - "ef.config": [ - "ValueGeneratedNever", - "ValueGeneratedOnAdd", - "HasConversion", - "HasColumnType", - "IsRequired-false" - ], - "shape.wrapped": [ - "int", - "long", - "char", - "Guid", - "string" - ] - }, - "narrow": { - "ef.query": "A configuration you write yourself always wins over these defaults.", - "ef.mapping": "A configuration you write yourself always wins over these defaults.", - "shape.site": "A configuration you write yourself always wins over these defaults.", - "shape.kind": "A configuration you write yourself always wins over these defaults." - }, - "obligations": [ - { - "id": "never", - "sentence": "A configuration you write yourself always wins over these defaults.", - "axes": { - "ef.config": [ - "ValueGeneratedNever" - ] - } - }, - { - "id": "on-add", - "sentence": "A configuration you write yourself always wins over these defaults.", - "axes": { - "ef.config": [ - "ValueGeneratedOnAdd" - ] - } - }, - { - "id": "column-type", - "sentence": "A facet you configure on the property, such as a maximum length or a column type, is kept.", - "axes": { - "ef.config": [ - "HasColumnType" - ] - } - }, - { - "id": "conversion", - "sentence": "A configuration you write yourself always wins over these defaults.", - "axes": { - "ef.config": [ - "HasConversion" - ] - } - }, - { - "id": "optional", - "sentence": "A configuration you write yourself always wins over these defaults.", - "axes": { - "ef.config": [ - "IsRequired-false" - ] - } - } - ], - "not-promised": [ - { - "id": "char-key-not-generated", - "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#keys", - "sentence": "A key over `char` is yours to assign.", - "axes": { - "ef.config": [ - "HasColumnType", - "HasConversion" - ], - "shape.wrapped": [ - "char" - ] - } - }, - { - "id": "guid-key-not-generated", - "source": "src/CodoMetis.TypeKit.EntityFrameworkCore/README.md#keys", - "sentence": "A key over a `Guid` is yours to assign", - "axes": { - "ef.config": [ - "HasColumnType", - "HasConversion" - ], - "shape.wrapped": [ - "Guid" - ] - } - } - ] - }, - { - "id": "L01-json-bytes", - "source": "README.md#what-typekit-guarantees", - "surfaces": [ - "json" - ], - "obligations": [ - { - "id": "the-underlying-types-json", - "sentence": "Its JSON is the underlying type's under your options", - "inapplicable": [ - { - "axes": { - "json.Converters": [ - "own-for-value-object" - ] - }, - "cite": "docs:src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says" - }, - { - "axes": { - "shape.kind": [ - "record-struct", - "sealed-record-class", - "validated-struct", - "validated-class" - ], - "shape.wrapped": [ - "byte" - ], - "shape.position": [ - "array-element" - ] - }, - "cite": "not-promised:a-byte-array-is-base64" - } - ] - }, - { - "id": "as-a-dictionary-key-too", - "sentence": "Its JSON is the underlying type's under your options", - "axes": { - "shape.position": [ - "dictionary-key" - ] - }, - "inapplicable": [ - { - "axes": { - "json.Converters": [ - "own-for-value-object" - ] - }, - "cite": "docs:src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says" - }, - { - "axes": { - "shape.kind": [ - "closed-set" - ], - "shape.position": [ - "dictionary-key" - ], - "json.DictionaryKeyPolicy": [ - "CamelCase", - "SnakeCaseLower", - "KebabCaseUpper", - "own-upper-case" - ] - }, - "cite": "not-promised:a-closed-set-key-is-its-name" - } - ] - }, - { - "id": "read-as-the-underlying-type-reads", - "sentence": "**Swapping a primitive for a value object keeps the JSON, the column and the schema.**", - "inapplicable": [ - { - "axes": { - "shape.kind": [ - "record-struct", - "sealed-record-class", - "validated-struct", - "validated-class" - ], - "shape.wrapped": [ - "byte" - ], - "shape.position": [ - "array-element" - ] - }, - "cite": "not-promised:a-byte-array-is-base64" - }, - { - "axes": { - "shape.kind": [ - "closed-set" - ], - "shape.position": [ - "dictionary-key" - ], - "json.DictionaryKeyPolicy": [ - "CamelCase", - "SnakeCaseLower", - "KebabCaseUpper", - "own-upper-case" - ] - }, - "cite": "not-promised:a-closed-set-key-is-its-name" - } - ] - } - ], - "not-promised": [ - { - "id": "a-datetime-is-written-as-utc", - "source": "README.md#what-typekit-guarantees", - "sentence": "Its JSON is the underlying type's under your options, with two exceptions: a `DateTime` is written as UTC", - "axes": { - "shape.wrapped": [ - "DateTime" - ] - } - }, - { - "id": "number-handling-on-the-containing-type", - "source": "README.md#what-typekit-guarantees", - "sentence": "and no converter sees number handling set by an attribute on the containing type" - }, - { - "id": "a-struct-refuses-a-json-null", - "source": "README.md#what-typekit-guarantees", - "sentence": "What changes is what the value object rejects, a JSON `null` for a struct value object included.", - "axes": { - "input.json": [ - "null" - ], - "shape.kind": [ - "record-struct", - "validated-struct" - ] - } - }, - { - "id": "a-validated-value-object-refuses-what-create-refuses", - "source": "README.md#what-typekit-guarantees", - "sentence": "What changes is what the value object rejects", - "axes": { - "input.json": [ - "refused-by-create" - ], - "shape.kind": [ - "validated-struct", - "validated-class" - ] - } - }, - { - "id": "an-enum-under-a-source-generated-context", - "source": "README.md#what-typekit-guarantees", - "sentence": "And under a source-generated context, a value object over an enum needs its enum listed", - "axes": { - "json.TypeInfoResolver": [ - "source-generated-context" - ], - "shape.wrapped": [ - "enum", - "flags-enum" - ] - } - }, - { - "id": "preserved-references-over-a-walked-class", - "source": "src/CodoMetis.TypeKit.Generators/README.md#json", - "sentence": "**Preserved references** (`ReferenceHandler.Preserve`) throw a `NotSupportedException` where the serializer could write `$id` or `$ref` inside the underlying value.", - "axes": { - "json.ReferenceHandler": [ - "Preserve" - ], - "shape.wrapped": [ - "own-record-class" - ] - } - }, - { - "id": "a-closed-set-key-is-its-name", - "source": "docs/closed-sets.md#send-it-as-json", - "sentence": "No naming policy renames it, because a renamed name wouldn't read back.", - "axes": { - "shape.kind": [ - "closed-set" - ], - "shape.position": [ - "dictionary-key" - ] - } - }, - { - "id": "a-byte-array-is-base64", - "source": "docs/json.md#the-wrapped-types-json", - "sentence": "An array of value objects over `byte` is a JSON array of numbers.", - "axes": { - "shape.wrapped": [ - "byte" - ], - "shape.position": [ - "array-element" - ] - } - } - ], - "exclude": [ - { - "reason": "System.Text.Json refuses PreferredObjectCreationHandling.Populate together with a ReferenceHandler on the options object; JsonRow substitutes the default, so the pair never applies.", - "pairs": [ - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=Preserve" - ], - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=IgnoreCycles" - ] - ] - } - ] - }, - { - "id": "L02-no-quote", - "source": "README.md#what-typekit-guarantees", - "surfaces": [ - "json", - "text", - "logging" - ], - "obligations": [ - { - "id": "parsing-never-repeats", - "sentence": "**Rejections never repeat the input.**", - "axes": { - "text.entry": [ - "Parse", - "TryParse", - "IParsable", - "ISpanParsable", - "type-converter" - ] - }, - "inapplicable": [ - { - "axes": { - "shape.kind": [ - "record-struct", - "sealed-record-class", - "validated-struct", - "validated-class" - ], - "shape.wrapped": [ - "Uri", - "enum", - "flags-enum", - "NodaTime.Instant" - ], - "text.entry": [ - "ISpanParsable" - ] - }, - "cite": "docs:src/CodoMetis.TypeKit.Generators/README.md#what-is-generated" - }, - { - "axes": { - "shape.kind": [ - "closed-set" - ], - "text.entry": [ - "ISpanParsable" - ] - }, - "cite": "docs:src/CodoMetis.TypeKit.Generators/README.md#closed-sets" - }, - { - "axes": { - "shape.kind": [ - "record-struct", - "sealed-record-class", - "validated-struct", - "validated-class" - ], - "shape.wrapped": [ - "own-record-class" - ], - "text.entry": [ - "Parse", - "TryParse", - "IParsable", - "ISpanParsable", - "type-converter" - ] - }, - "cite": "docs:src/CodoMetis.TypeKit.Generators/README.md#what-is-generated" - }, - { - "axes": { - "text.entry": [ - "TryFrom", - "ToString-format", - "TryFormat-short-span" - ] - }, - "cite": "docs:src/CodoMetis.TypeKit.Generators/README.md#when-text-is-rejected" - } - ] - }, - { - "id": "json-never-repeats", - "sentence": "A JSON or parsing rejection names the value object and the rule, or the underlying type it could not read.", - "inapplicable": [ - { - "axes": { - "json.Converters": [ - "own-for-wrapped", - "own-for-value-object" - ] - }, - "cite": "not-promised:the-applications-own-converter" - } - ] - }, - { - "id": "no-inner-that-quotes", - "sentence": "It carries no inner exception that quotes the text.", - "axes": { - "text.entry": [ - "Parse", - "TryParse", - "IParsable", - "ISpanParsable", - "type-converter" - ] - }, - "inapplicable": [ - { - "axes": { - "shape.kind": [ - "record-struct", - "sealed-record-class", - "validated-struct", - "validated-class" - ], - "shape.wrapped": [ - "Uri", - "enum", - "flags-enum", - "NodaTime.Instant" - ], - "text.entry": [ - "ISpanParsable" - ] - }, - "cite": "docs:src/CodoMetis.TypeKit.Generators/README.md#what-is-generated" - }, - { - "axes": { - "shape.kind": [ - "closed-set" - ], - "text.entry": [ - "ISpanParsable" - ] - }, - "cite": "docs:src/CodoMetis.TypeKit.Generators/README.md#closed-sets" - }, - { - "axes": { - "shape.kind": [ - "record-struct", - "sealed-record-class", - "validated-struct", - "validated-class" - ], - "shape.wrapped": [ - "own-record-class" - ], - "text.entry": [ - "Parse", - "TryParse", - "IParsable", - "ISpanParsable", - "type-converter" - ] - }, - "cite": "docs:src/CodoMetis.TypeKit.Generators/README.md#what-is-generated" - }, - { - "axes": { - "text.entry": [ - "TryFrom", - "ToString-format", - "TryFormat-short-span" - ] - }, - "cite": "docs:src/CodoMetis.TypeKit.Generators/README.md#when-text-is-rejected" - } - ] - } - ], - "not-promised": [ - { - "id": "a-rejected-dictionary-key-in-the-path", - "source": "README.md#what-typekit-guarantees", - "sentence": "One exception comes from System.Text.Json: a rejected dictionary key is in the `JsonException`'s `Path`, and so in MVC's 400", - "axes": { - "shape.position": [ - "dictionary-key" - ] - } - }, - { - "id": "the-applications-own-converter", - "source": "README.md#what-typekit-guarantees", - "sentence": "So a secret doesn't end up in a message or a log through this package, as long as your fault type doesn't carry it.", - "axes": { - "json.Converters": [ - "own-for-wrapped", - "own-for-value-object" - ] - } - } - ], - "exclude": [ - { - "reason": "System.Text.Json refuses PreferredObjectCreationHandling.Populate together with a ReferenceHandler on the options object; JsonRow substitutes the default, so the pair never applies.", - "pairs": [ - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=Preserve" - ], - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=IgnoreCycles" - ] - ] - }, - { - "reason": "System.Text.Json writes no dictionary key of a record class but through a converter of the application's (NotSupportedException for the wrapped type alike, L01's rows), where the JSON half is not promised (not-promised the-applications-own-converter), and a value object over a record class has no parsing (Generators README#what-is-generated), so no check of the claim can read the pair.", - "pairs": [ - [ - "shape.position=dictionary-key", - "shape.wrapped=own-record-class" - ] - ] - } - ] - }, - { - "id": "L03-one-rule", - "source": "README.md#what-typekit-guarantees", - "surfaces": [ - "text", - "json" - ], - "narrow": { - "input.json": "The generated `TryFrom`, `FromKnownGood`, JSON converter, parsing and type converter all run it, and each reports the fault you return from `Create`" - }, - "obligations": [ - { - "id": "tryfrom-runs-create", - "sentence": "The generated `TryFrom`, `FromKnownGood`, JSON converter, parsing and type converter all run it", - "axes": { - "text.entry": [ - "TryFrom" - ] - }, - "inapplicable": [ - { - "axes": { - "shape.kind": [ - "closed-set" - ], - "text.entry": [ - "TryFrom" - ] - }, - "cite": "docs:src/CodoMetis.TypeKit.Generators/README.md#closed-sets" - } - ] - }, - { - "id": "parsing-runs-create", - "sentence": "The generated `TryFrom`, `FromKnownGood`, JSON converter, parsing and type converter all run it", - "axes": { - "text.entry": [ - "Parse", - "TryParse", - "IParsable", - "ISpanParsable", - "type-converter" - ] - }, - "inapplicable": [ - { - "axes": { - "shape.kind": [ - "record-struct", - "sealed-record-class", - "validated-struct", - "validated-class" - ], - "shape.wrapped": [ - "Uri", - "enum", - "flags-enum", - "NodaTime.Instant" - ], - "text.entry": [ - "ISpanParsable" - ] - }, - "cite": "docs:src/CodoMetis.TypeKit.Generators/README.md#what-is-generated" - }, - { - "axes": { - "shape.kind": [ - "closed-set" - ], - "text.entry": [ - "ISpanParsable" - ] - }, - "cite": "docs:src/CodoMetis.TypeKit.Generators/README.md#closed-sets" - }, - { - "axes": { - "shape.kind": [ - "record-struct", - "sealed-record-class", - "validated-struct", - "validated-class" - ], - "shape.wrapped": [ - "own-record-class" - ], - "text.entry": [ - "Parse", - "TryParse", - "IParsable", - "ISpanParsable", - "type-converter" - ] - }, - "cite": "docs:src/CodoMetis.TypeKit.Generators/README.md#what-is-generated" - } - ] - }, - { - "id": "json-runs-create", - "sentence": "The generated `TryFrom`, `FromKnownGood`, JSON converter, parsing and type converter all run it" - }, - { - "id": "refusals-name-the-fault", - "sentence": "the `JsonException`, the `FormatException` and the `FromKnownGood` exception all name your `EmailFault.NoAt`" - } - ], - "not-promised": [ - { - "id": "stored-json-skips-validation", - "source": "README.md#what-typekit-guarantees", - "sentence": "`StoredJsonConverterFactory` reads JSON the application stored itself.", - "axes": { - "json.Converters": [ - "StoredJsonConverterFactory" - ] - } - } - ], - "exclude": [ - { - "reason": "System.Text.Json refuses PreferredObjectCreationHandling.Populate together with a ReferenceHandler on the options object; JsonRow substitutes the default, so the pair never applies.", - "pairs": [ - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=Preserve" - ], - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=IgnoreCycles" - ] - ] - } - ] - }, - { - "id": "L04-round-trip", - "source": "src/CodoMetis.TypeKit.Generators/README.md#parsing-and-formatting", - "surfaces": [ - "text", - "json" - ], - "obligations": [ - { - "id": "invariant-whatever-the-culture", - "sentence": "A null `IFormatProvider` means the invariant culture in every generated `Parse`, `TryParse`, `ToString` and `TryFormat`, so the text doesn't depend on the culture the process runs in." - }, - { - "id": "the-type-converter-writes-the-same", - "sentence": "`ToString()` is the underlying type's own default text under the invariant culture, and the type converter and `$\"{x}\"` write the same.", - "axes": { - "text.entry": [ - "ToString-format", - "TryFormat-short-span", - "type-converter" - ] - }, - "inapplicable": [ - { - "axes": { - "shape.kind": [ - "closed-set" - ], - "text.entry": [ - "ToString-format", - "TryFormat-short-span" - ] - }, - "cite": "docs:src/CodoMetis.TypeKit.Generators/README.md#closed-sets" - }, - { - "axes": { - "shape.kind": [ - "record-struct", - "sealed-record-class", - "validated-struct", - "validated-class" - ], - "shape.wrapped": [ - "own-record-class" - ], - "text.entry": [ - "type-converter" - ] - }, - "cite": "docs:src/CodoMetis.TypeKit.Generators/README.md#what-is-generated" - } - ] - }, - { - "id": "text-reads-back", - "sentence": "That text reads back as the same value wherever the underlying type's own does", - "inapplicable": [ - { - "axes": { - "shape.kind": [ - "closed-set" - ], - "text.entry": [ - "TryFrom", - "ISpanParsable", - "ToString-format", - "TryFormat-short-span" - ] - }, - "cite": "docs:src/CodoMetis.TypeKit.Generators/README.md#closed-sets" - }, - { - "axes": { - "shape.kind": [ - "record-struct", - "sealed-record-class", - "validated-struct", - "validated-class" - ], - "shape.wrapped": [ - "own-record-class" - ], - "text.entry": [ - "Parse", - "TryParse", - "IParsable", - "ISpanParsable", - "type-converter", - "ToString-format", - "TryFormat-short-span" - ] - }, - "cite": "docs:src/CodoMetis.TypeKit.Generators/README.md#what-is-generated" - }, - { - "axes": { - "shape.kind": [ - "record-struct", - "sealed-record-class", - "validated-struct", - "validated-class" - ], - "shape.wrapped": [ - "Uri", - "enum", - "flags-enum", - "NodaTime.Instant" - ], - "text.entry": [ - "ISpanParsable" - ] - }, - "cite": "docs:src/CodoMetis.TypeKit.Generators/README.md#what-is-generated" - }, - { - "axes": { - "shape.kind": [ - "record-struct", - "sealed-record-class", - "validated-struct", - "validated-class" - ], - "shape.wrapped": [ - "TimeOnly", - "DateTime", - "DateTimeOffset" - ], - "text.entry": [ - "type-converter" - ] - }, - "cite": "not-promised:dates-and-times-lose-detail-as-text" - } - ] - }, - { - "id": "json-refusal-is-a-json-exception", - "sentence": "Input it can't read throws a `JsonException` naming the value object and the underlying type" - } - ], - "not-promised": [ - { - "id": "dates-and-times-lose-detail-as-text", - "source": "src/CodoMetis.TypeKit.Generators/README.md#parsing-and-formatting", - "sentence": "**`TimeOnly`, `DateTime` and `DateTimeOffset` lose detail as text.**", - "axes": { - "shape.wrapped": [ - "TimeOnly", - "DateTime", - "DateTimeOffset" - ] - } - } - ], - "exclude": [ - { - "reason": "System.Text.Json refuses PreferredObjectCreationHandling.Populate together with a ReferenceHandler on the options object; JsonRow substitutes the default, so the pair never applies.", - "pairs": [ - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=Preserve" - ], - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=IgnoreCycles" - ] - ] - } - ] - }, - { - "id": "L05-equality", - "source": "src/CodoMetis.TypeKit.Generators/README.md#equality", - "surfaces": [ - "generation" - ], - "narrow": { - "shape.refused-kind": "A declaration that can't be generated fails the build, so no type is left half-generated." - }, - "obligations": [ - { - "id": "equality-is-the-underlying-values", - "sentence": "Equality is the underlying value's, like the ordering, the JSON and the column." - }, - { - "id": "ordering-follows-the-underlying-type", - "sentence": "`CompareTo` and the operators follow the underlying type's ordering, ordinal for a string." - }, - { - "id": "through-every-base", - "sentence": "The value object implements each base's `IEquatable` again (`IReimplementedEquatable`), so a collection of the base compares it the way the value object does.", - "axes": { - "shape.inheritance": [ - "base-depth-1", - "base-depth-2", - "base-depth-3", - "base-explicit-IEquatable" - ] - } - } - ], - "exclude": [ - { - "reason": "A struct cannot derive from a base type (C# structs inherit only from ValueType), so it has no base record.", - "pairs": [ - [ - "shape.inheritance=base-depth-1", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-depth-1", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=base-depth-2", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-depth-2", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=base-depth-3", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-depth-3", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.kind=validated-struct" - ] - ] - }, - { - "reason": "A closed set wraps nothing (Generators README#closed-sets: a sealed class whose instances are its members), so it has no shape.wrapped.", - "pairs": [ - [ - "shape.kind=closed-set", - "shape.wrapped=DateOnly" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=DateTime" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=Guid" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=TimeOnly" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=TimeSpan" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=Uri" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=bool" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=byte" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=char" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=decimal" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=double" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=enum" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=flags-enum" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=int" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=long" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=own-record-class" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=string" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=ulong" - ] - ] - }, - { - "reason": "A value object or closed set nested in a generic type fails the build (CMTK1005, CMTK1018; Generators README#build-errors), so no such type exists to compare.", - "pairs": [ - [ - "shape.containing=class", - "shape.site=in-generic-type" - ], - [ - "shape.containing=generic-type", - "shape.site=in-generic-type" - ], - [ - "shape.containing=record-class", - "shape.site=in-generic-type" - ], - [ - "shape.containing=record-struct", - "shape.site=in-generic-type" - ], - [ - "shape.containing=struct", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=base-depth-1", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=base-depth-2", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=base-depth-3", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=none", - "shape.site=in-generic-type" - ], - [ - "shape.kind=closed-set", - "shape.site=in-generic-type" - ], - [ - "shape.kind=record-struct", - "shape.site=in-generic-type" - ], - [ - "shape.kind=sealed-record-class", - "shape.site=in-generic-type" - ], - [ - "shape.kind=validated-class", - "shape.site=in-generic-type" - ], - [ - "shape.kind=validated-struct", - "shape.site=in-generic-type" - ], - [ - "shape.position=array-element", - "shape.site=in-generic-type" - ], - [ - "shape.position=dictionary-key", - "shape.site=in-generic-type" - ], - [ - "shape.position=dictionary-value", - "shape.site=in-generic-type" - ], - [ - "shape.position=field", - "shape.site=in-generic-type" - ], - [ - "shape.position=immutable-array-element", - "shape.site=in-generic-type" - ], - [ - "shape.position=in-patch", - "shape.site=in-generic-type" - ], - [ - "shape.position=list-element", - "shape.site=in-generic-type" - ], - [ - "shape.position=nullable-property", - "shape.site=in-generic-type" - ], - [ - "shape.position=polymorphic-derived-property", - "shape.site=in-generic-type" - ], - [ - "shape.position=property", - "shape.site=in-generic-type" - ], - [ - "shape.position=read-only-list-element", - "shape.site=in-generic-type" - ], - [ - "shape.position=record-ctor-parameter", - "shape.site=in-generic-type" - ], - [ - "shape.position=required-init", - "shape.site=in-generic-type" - ], - [ - "shape.position=root", - "shape.site=in-generic-type" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=DateOnly" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=DateTime" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=Guid" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=TimeOnly" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=TimeSpan" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=Uri" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=bool" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=byte" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=char" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=decimal" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=double" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=enum" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=flags-enum" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=int" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=long" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=own-record-class" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=string" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=ulong" - ] - ] - }, - { - "reason": "A value at the root is not held by a containing type, so shape.containing has nothing to apply to.", - "pairs": [ - [ - "shape.containing=class", - "shape.position=root" - ], - [ - "shape.containing=generic-type", - "shape.position=root" - ], - [ - "shape.containing=record-class", - "shape.position=root" - ], - [ - "shape.containing=record-struct", - "shape.position=root" - ], - [ - "shape.containing=struct", - "shape.position=root" - ] - ] - } - ] - }, - { - "id": "L06-off-the-wire", - "source": "README.md#what-typekit-guarantees", - "surfaces": [ - "json", - "option-result" - ], - "obligations": [ - { - "id": "never-written", - "sentence": "System.Text.Json throws `NotSupportedException` for either, in both directions, instead of writing `{}` that reads back as `None`." - }, - { - "id": "never-read", - "sentence": "System.Text.Json throws `NotSupportedException` for either, in both directions, instead of writing `{}` that reads back as `None`." - } - ], - "not-promised": [ - { - "id": "a-source-generated-context-refuses-a-nullable", - "source": "src/CodoMetis.TypeKit/README.md#not-wire-types", - "sentence": "**A source-generated context can't describe an `Option?` or `Result<...>?` property.**", - "axes": { - "json.TypeInfoResolver": [ - "source-generated-context" - ], - "shape.position": [ - "nullable-property" - ] - } - }, - { - "id": "a-missing-property-reaches-no-converter", - "source": "src/CodoMetis.TypeKit/README.md#not-wire-types", - "sentence": "**A missing property reaches no converter.**" - } - ], - "exclude": [ - { - "reason": "A closed set wraps nothing (Generators README#closed-sets: a sealed class whose instances are its members), so it has no shape.wrapped.", - "pairs": [ - [ - "shape.kind=closed-set", - "shape.wrapped=DateOnly" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=DateTime" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=Guid" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=TimeOnly" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=TimeSpan" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=Uri" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=bool" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=byte" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=char" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=decimal" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=double" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=enum" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=flags-enum" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=int" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=long" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=own-record-class" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=string" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=ulong" - ] - ] - }, - { - "reason": "A value at the root is not held by a containing type, so shape.containing has nothing to apply to.", - "pairs": [ - [ - "shape.containing=class", - "shape.position=root" - ], - [ - "shape.containing=generic-type", - "shape.position=root" - ], - [ - "shape.containing=record-class", - "shape.position=root" - ], - [ - "shape.containing=record-struct", - "shape.position=root" - ], - [ - "shape.containing=struct", - "shape.position=root" - ] - ] - }, - { - "reason": "Result has no Select or SelectMany, so no query expression produces one, and FirstOrNone returns an Option, never a Result (src/CodoMetis.TypeKit/ResultExtensions.cs and OptionExtensions.cs).", - "pairs": [ - [ - "or.operation=FirstOrNone", - "or.shape=Result-E" - ], - [ - "or.operation=query-syntax", - "or.shape=Result-E" - ] - ] - }, - { - "reason": "System.Text.Json serializes a field only under IncludeFields (or [JsonInclude]), so without it the member is in no document and no converter runs in either direction, for the plain type alike.", - "pairs": [ - [ - "json.IncludeFields=default", - "shape.position=field" - ] - ] - }, - { - "reason": "System.Text.Json refuses JsonObjectCreationHandling.Populate together with a reference handler (InvalidOperationException when the options are used; harness/JsonRow.cs runs such a row without Populate), so no row can set both.", - "pairs": [ - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=IgnoreCycles" - ], - [ - "json.PreferredObjectCreationHandling=Populate", - "json.ReferenceHandler=Preserve" - ] - ] - } - ] - }, - { - "id": "C22-generator-refusals", - "source": "src/CodoMetis.TypeKit.Generators/README.md#build-errors", - "surfaces": [ - "generation", - "analyzers" - ], - "axes": { - "build-host": [ - "sdk-build" - ] - }, - "narrow": { - "shape.position": "A declaration that can't be generated fails the build, so no type is left half-generated.", - "shape.containing": "A declaration that can't be generated fails the build, so no type is left half-generated." - }, - "obligations": [ - { - "id": "no-type-half-generated", - "sentence": "A declaration that can't be generated fails the build, so no type is left half-generated." - }, - { - "id": "every-error-links-the-table", - "sentence": "Every error ends with a link to this table." - }, - { - "id": "unsealed-record-class", - "sentence": "A record class not declared `sealed`.", - "axes": { - "shape.refused-kind": [ - "unsealed-record-class (CMTK1006)" - ] - } - }, - { - "id": "over-value-object", - "sentence": "or a value object (itself included). Wrap what the other value object wraps instead.", - "axes": { - "shape.refused-kind": [ - "over-value-object (CMTK1005)" - ] - } - }, - { - "id": "generic-wrapped", - "sentence": "A generic underlying type, a tuple included, fails the build (CMTK1005).", - "axes": { - "shape.refused-kind": [ - "generic-wrapped (CMTK1005)" - ] - } - }, - { - "id": "abstract", - "sentence": "An abstract type with a value-object marker. It would get no member, and a value held as it, of a type derived from it, would have no `Value` and be written as `{}`.", - "axes": { - "shape.refused-kind": [ - "abstract (CMTK1005)" - ] - } - }, - { - "id": "over-object", - "sentence": "or wrapping `object`, `System.Enum`, an array, a pointer, a nullable type, a generic type (a tuple included) or a value object (itself included).", - "axes": { - "shape.refused-kind": [ - "over-object (CMTK1005)" - ] - } - }, - { - "id": "over-system-enum", - "sentence": "or wrapping `object`, `System.Enum`, an array, a pointer, a nullable type, a generic type (a tuple included) or a value object (itself included).", - "axes": { - "shape.refused-kind": [ - "over-System.Enum (CMTK1005)" - ] - } - }, - { - "id": "named-as-generated-member", - "sentence": "named `Value` or as another member the generators add (`From`, `TryFrom`, `FromKnownGood`, `Revalidate`, `Parse`, `TryParse`, `TryFormat`, `MinValue`, `MaxValue`, `CompareTo`)", - "axes": { - "shape.refused-kind": [ - "named-as-generated-member (CMTK1005)" - ] - } - }, - { - "id": "closed-set-named-as-generated-member", - "sentence": "a set itself named as one of them (a member can't have the name of its type)", - "axes": { - "shape.refused-kind": [ - "closed-set-named-as-generated-member (CMTK1021)" - ] - } - }, - { - "id": "equality-operator", - "sentence": "So a hand-written `Equals(TSelf)`, `GetHashCode()`, `IEquatable.Equals`, or `operator ==` or `!=` between two of the type, fails the build (CMTK1011).", - "axes": { - "shape.refused-kind": [ - "equality-operator (CMTK1011)" - ] - } - } - ], - "exclude": [ - { - "reason": "A declaration nested in a generic type is refused for that alone (Generators README, Build errors: CMTK1005, \"Generic or nested in a generic type\"; the axes file's refused list), so it cannot show which refusal of its own the generators report.", - "pairs": [ - [ - "build-host=sdk-build", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=base-depth-1", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=base-depth-2", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=base-depth-3", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=explicit-interface-member", - "shape.site=in-generic-type" - ], - [ - "shape.inheritance=none", - "shape.site=in-generic-type" - ], - [ - "shape.kind=closed-set", - "shape.site=in-generic-type" - ], - [ - "shape.kind=record-struct", - "shape.site=in-generic-type" - ], - [ - "shape.kind=sealed-record-class", - "shape.site=in-generic-type" - ], - [ - "shape.kind=validated-class", - "shape.site=in-generic-type" - ], - [ - "shape.kind=validated-struct", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=abstract (CMTK1005)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=equality-operator (CMTK1011)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=generic-wrapped (CMTK1005)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=named-as-generated-member (CMTK1005)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=over-value-object (CMTK1005)", - "shape.site=in-generic-type" - ], - [ - "shape.refused-kind=unsealed-record-class (CMTK1006)", - "shape.site=in-generic-type" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=DateOnly" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=DateTime" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=Guid" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=TimeOnly" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=TimeSpan" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=Uri" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=bool" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=byte" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=char" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=decimal" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=double" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=enum" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=flags-enum" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=int" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=long" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=own-record-class" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=string" - ], - [ - "shape.site=in-generic-type", - "shape.wrapped=ulong" - ] - ] - }, - { - "reason": "C# seals every struct and refuses an abstract one, so a record struct can be neither an unsealed nor an abstract record class; a closed set is never a record (CMTK1020), and an unsealed or abstract one is CMTK1014, a refusal of its own.", - "pairs": [ - [ - "shape.kind=closed-set", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.kind=record-struct", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=abstract (CMTK1005)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=unsealed-record-class (CMTK1006)" - ] - ] - }, - { - "reason": "A closed set wraps no type and has no equality of the underlying value to write by hand (Generators README, Closed sets), so it cannot be declared over a type or with a value object's equality; its own name clash is closed-set-named-as-generated-member (CMTK1021).", - "pairs": [ - [ - "shape.kind=closed-set", - "shape.refused-kind=equality-operator (CMTK1011)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=generic-wrapped (CMTK1005)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=named-as-generated-member (CMTK1005)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=over-System.Enum (CMTK1005)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=over-object (CMTK1005)" - ], - [ - "shape.kind=closed-set", - "shape.refused-kind=over-value-object (CMTK1005)" - ] - ] - }, - { - "reason": "CMTK1021 is a closed set's refusal; a value object named as a generated member is named-as-generated-member (CMTK1005).", - "pairs": [ - [ - "shape.kind=record-struct", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=sealed-record-class", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=validated-class", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.kind=validated-struct", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ] - ] - }, - { - "reason": "The refused kind fixes what is wrapped: object, System.Enum; and a closed set wraps no type (Generators README, Closed sets), so shape.wrapped has nothing to apply to.", - "pairs": [ - [ - "shape.kind=closed-set", - "shape.wrapped=DateOnly" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=DateTime" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=Guid" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=TimeOnly" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=TimeSpan" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=Uri" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=bool" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=byte" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=char" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=decimal" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=double" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=enum" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=flags-enum" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=int" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=long" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=own-record-class" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=string" - ], - [ - "shape.kind=closed-set", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=over-System.Enum (CMTK1005)", - "shape.wrapped=ulong" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=DateOnly" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=DateTime" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=DateTimeOffset" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=Guid" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=NodaTime.Instant" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=TimeOnly" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=TimeSpan" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=Uri" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=bool" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=byte" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=char" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=decimal" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=double" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=enum" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=flags-enum" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=int" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=long" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=own-record-class" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=string" - ], - [ - "shape.refused-kind=over-object (CMTK1005)", - "shape.wrapped=ulong" - ] - ] - }, - { - "reason": "A record struct has no base type, and a closed set is a sealed class that is never a record (CMTK1020), so neither derives from the base records of shape.inheritance; an explicit member of another interface they implement themselves.", - "pairs": [ - [ - "shape.inheritance=base-depth-1", - "shape.kind=closed-set" - ], - [ - "shape.inheritance=base-depth-1", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-depth-1", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=base-depth-1", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=base-depth-2", - "shape.kind=closed-set" - ], - [ - "shape.inheritance=base-depth-2", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-depth-2", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=base-depth-2", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=base-depth-3", - "shape.kind=closed-set" - ], - [ - "shape.inheritance=base-depth-3", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-depth-3", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=base-depth-3", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.kind=closed-set" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.kind=record-struct" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.kind=validated-struct" - ], - [ - "shape.inheritance=base-explicit-IEquatable", - "shape.refused-kind=closed-set-named-as-generated-member (CMTK1021)" - ] - ] - } - ] - } -] diff --git a/tools/probe/corpus/dismissed.jsonl b/tools/probe/corpus/dismissed.jsonl deleted file mode 100644 index 4327b389..00000000 --- a/tools/probe/corpus/dismissed.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"id": "G1", "reason": "Not a TypeKit bug: the MVC [FromBody] T v of a class is nullable, so ASP.NET makes the body optional, and its OpenAPI 2.0 writer turns any nullable body into allOf:[{x-nullable:true}] without pattern; a plain int? without TypeKit gets the same bytes. The harness compared a nullable class value object with a non-nullable int (fix-f2-g1-openapi, measured at 1ee02fc).", "date": "2026-10-15"} diff --git a/tools/probe/corpus/harness/BaseProbe.cs b/tools/probe/corpus/harness/BaseProbe.cs deleted file mode 100644 index 13b5e370..00000000 --- a/tools/probe/corpus/harness/BaseProbe.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Calibration of the shared HTTP host (not a claim): runs the baseline's wire half over its own rows of claim C07 to find harness mistakes -// before the claims use it. The schema half is claim C21's (C21OpenapiSchemaParityProbe). Each comparison is a check of its own (HttpBaseline.Run), -// of the implicit obligation claim: C07 promises nothing of the wire, and its seed names no obligation. -using Probes.Shapes; -namespace Probes; -public sealed class BaseProbe -{ - public static TheoryData Data() => Rows.Data("BaseProbe"); - [Theory, MemberData(nameof(Data))] - public Task Row(string probe, int idx) => Rec.Run("BASE", idx, async r => await HttpBaseline.Run(r, Registry.Of(r), schema: false), probe); -} diff --git a/tools/probe/corpus/harness/BuildConsumer.cs b/tools/probe/corpus/harness/BuildConsumer.cs deleted file mode 100644 index 15dddbc5..00000000 --- a/tools/probe/corpus/harness/BuildConsumer.cs +++ /dev/null @@ -1,125 +0,0 @@ -// A throwaway consumer of the generators, built once per probe run with every row's declarations in a file of its own, for what -// only a build can show: the generators' diagnostics (CMTK1xxx). It builds as test/CodoMetis.TypeKit.Generators.Tests' BuildOutcomeTests -// does: under artifacts/, against the projects this run already built (--no-dependencies), outside the repository's build settings. -using System.Diagnostics; -using System.Reflection; -using System.Reflection.Metadata; -using System.Reflection.PortableExecutable; -using System.Text.RegularExpressions; -namespace Probes.Build; - -public sealed record BuildDiagnostic(string File, string Severity, string Id, string Message); - -public sealed record ConsumerOutcome(int ExitCode, string Output, IReadOnlyList Diagnostics, IReadOnlyDictionary> Methods) -{ - /// The diagnostics reported in one of the consumer's files. - public IEnumerable In(string file) => Diagnostics.Where(d => Path.GetFileName(d.File) == file); -} - -public static partial class Consumer -{ - static readonly Dictionary>> Builds = new(); - - /// Builds the consumer of once per in this process. - public static Task Build(string key, Func> files) - { - lock (Builds) - { - if (!Builds.TryGetValue(key, out var build)) Builds[key] = build = new(() => Run(files())); - return build.Value; - } - } - - public static string RepositoryRoot() - { - for (var d = new DirectoryInfo(AppContext.BaseDirectory); d is not null; d = d.Parent) - if (File.Exists(Path.Combine(d.FullName, "CodoMetis.TypeKit.slnx"))) return d.FullName; - throw new InvalidOperationException($"no CodoMetis.TypeKit.slnx above {AppContext.BaseDirectory}"); - } - - static string Configuration => typeof(Consumer).Assembly.GetCustomAttribute()?.Configuration - ?? throw new InvalidOperationException("the probe assembly carries no AssemblyConfigurationAttribute"); - - static async Task Run(IReadOnlyDictionary files) - { - var root = RepositoryRoot(); - var dir = Path.Combine(root, "artifacts", $"probe-consumer-{Guid.NewGuid():N}"); - Directory.CreateDirectory(dir); - try - { - var src = Path.Combine(root, "src"); - await File.WriteAllTextAsync(Path.Combine(dir, "Consumer.csproj"), $""" - - - net10.0 - enable - disable - ProbeConsumer - - - - {typeof(NodaTime.Instant).Assembly.Location} - - - """); - await File.WriteAllTextAsync(Path.Combine(dir, "Directory.Build.props"), ""); - await File.WriteAllTextAsync(Path.Combine(dir, "Directory.Build.targets"), ""); - await File.WriteAllTextAsync(Path.Combine(dir, "Directory.Packages.props"), - "false"); - foreach (var (name, text) in files) await File.WriteAllTextAsync(Path.Combine(dir, name), text); - - var dotnet = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH") is { Length: > 0 } host ? host : "dotnet"; - var info = new ProcessStartInfo(dotnet, ["build", dir, "-c", Configuration, "--no-dependencies", "-p:RestoreRecursive=false", "-nodeReuse:false", "-clp:NoSummary", "-v:q", "-nologo"]) - { - RedirectStandardOutput = true, RedirectStandardError = true, WorkingDirectory = dir, - }; - // The run's code coverage instruments this process through its environment; the build is not the probe's code. - foreach (var k in info.Environment.Keys.ToList()) - if (k.StartsWith("CORECLR_", StringComparison.OrdinalIgnoreCase) || k.StartsWith("COR_", StringComparison.OrdinalIgnoreCase) - || k.Contains("CODECOVERAGE", StringComparison.OrdinalIgnoreCase) || k.StartsWith("MicrosoftInstrumentationEngine", StringComparison.OrdinalIgnoreCase) - || k.Equals("DOTNET_STARTUP_HOOKS", StringComparison.OrdinalIgnoreCase)) - info.Environment.Remove(k); - info.Environment["DOTNET_CLI_UI_LANGUAGE"] = "en"; - using var process = Process.Start(info)!; - var stdout = process.StandardOutput.ReadToEndAsync(); - var stderr = process.StandardError.ReadToEndAsync(); - using (var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(10))) await process.WaitForExitAsync(timeout.Token); - var output = await stdout + await stderr; - var diagnostics = Line().Matches(output) - .Select(m => new BuildDiagnostic(m.Groups["file"].Value.Trim(), m.Groups["sev"].Value, m.Groups["id"].Value, m.Groups["msg"].Value.Trim())) - .Distinct().ToList(); - var dll = Path.Combine(dir, "bin", Configuration, "net10.0", "ProbeConsumer.dll"); - return new ConsumerOutcome(process.ExitCode, output, diagnostics, File.Exists(dll) ? Methods(dll) : new Dictionary>()); - } - finally - { - try { Directory.Delete(dir, recursive: true); } catch (IOException) { } catch (UnauthorizedAccessException) { } - } - } - - /// The methods each type of the built assembly declares, by the type's metadata name (nesting with '+'), read without loading it. - static Dictionary> Methods(string dll) - { - using var stream = File.OpenRead(dll); - using var pe = new PEReader(stream); - var md = pe.GetMetadataReader(); - string Name(TypeDefinition t) - { - var name = md.GetString(t.Name); - var outer = t.GetDeclaringType(); - if (!outer.IsNil) return Name(md.GetTypeDefinition(outer)) + "+" + name; - var ns = md.GetString(t.Namespace); - return ns.Length == 0 ? name : ns + "." + name; - } - var result = new Dictionary>(); - foreach (var h in md.TypeDefinitions) - { - var t = md.GetTypeDefinition(h); - result[Name(t)] = [.. t.GetMethods().Select(m => md.GetString(md.GetMethodDefinition(m).Name))]; - } - return result; - } - - [GeneratedRegex(@"^(?[^\r\n(]+)\((?\d+),(?\d+)\): (?warning|error) (?[A-Z]+\d+): (?.*?)(?: \[[^\]\r\n]*\])?\r?$", RegexOptions.Multiline)] - private static partial Regex Line(); -} diff --git a/tools/probe/corpus/harness/BuildDecls.cs b/tools/probe/corpus/harness/BuildDecls.cs deleted file mode 100644 index 7efdb659..00000000 --- a/tools/probe/corpus/harness/BuildDecls.cs +++ /dev/null @@ -1,91 +0,0 @@ -// The hand half of C09Shapes.g.cs and C11Shapes.g.cs (generated from the rows, one value object per row): the tables' rows, and the -// wrapped types of C11's parse shapes. -namespace Probes.Build; - -/// One C09 row as declared: the effective settings, the value object, its bases from the direct one up, and its factory. -public sealed record C09Case(int Index, string Key, string Kind, string Site, string Inheritance, Type Vo, Type[] Bases, Func Make, (string Axis, string Value)[] Substituted); - -/// -/// One C11 row as declared: the effective settings, the value object and the type it wraps, and for a parse shape its control: a value -/// object of the same kind, site and inheritance over the nearest type that does parse. -/// -public sealed record C11Case(int Index, string Key, string Kind, string Site, string Inheritance, Type Vo, Type Wrapped, Type? Control, Type? ControlWrapped, (string Axis, string Value)[] Substituted); - -public static class Decls -{ - /// - /// The row's settings as the generator keyed them; a table generated from other rows is a harness error, not a pass. Read from - /// the cached copy of the row, so the run's own row marks no axis read. - /// - public static string Key(string probe, int index) => - string.Join(";", Probes.Shapes.Rows.Get(probe, index).All.OrderBy(kv => kv.Key, StringComparer.Ordinal).Select(kv => $"{kv.Key}={kv.Value}")); -} - -// C11's parse shapes, each the whole wrapped type (CHANGELOG 2.1.0, Fixed): "A wrapped type's `Parse` whose result can't be assigned -// to it, or that takes its text by `ref`, or an abstract class's string constructor". - -/// implicit-result: a static Parse whose result, an int, cannot be assigned to the type. -public sealed class C11ParsesToInt -{ - public static int Parse(string text) => text.Length; - public override string ToString() => "parses-to-int"; -} - -/// ref-text: a static Parse that takes its text by ref. -public sealed class C11ParsesByRef -{ - public static C11ParsesByRef Parse(ref string text) { text = ""; return new(); } - public override string ToString() => "parses-by-ref"; -} - -/// ref-text: a string constructor that takes its text by ref. -public sealed class C11ConstructedByRef -{ - public C11ConstructedByRef(ref string text) => text = ""; - private C11ConstructedByRef() { } - public static C11ConstructedByRef Sample() => new(); - public override string ToString() => "constructed-by-ref"; -} - -/// abstract-string-ctor: an abstract class with a public string constructor, which only a derived class can call. -public abstract class C11AbstractText -{ - public C11AbstractText(string text) => Text = text; - public string Text { get; } - public override string ToString() => Text; -} - -public sealed class C11ConcreteText(string text) : C11AbstractText(text); - -// The controls, each the nearest type that can be parsed (Generators README, "What you can wrap": "Parsing where it can be parsed"): -// a Parse whose result converts to the type by a user-defined implicit operator, so it can be assigned; a Parse taking its text by in, -// which a call passes as it is; a sealed class's public string constructor. - -/// Control of implicit-result: Parse gives a type that converts to this one implicitly. -public sealed class C11ParsedShape -{ - public string Text { get; init; } = ""; - public static C11ParsedCircle Parse(string text) => new() { Text = text }; - public override string ToString() => Text; -} - -public sealed class C11ParsedCircle -{ - public string Text { get; init; } = ""; - public static implicit operator C11ParsedShape(C11ParsedCircle circle) => new() { Text = circle.Text }; -} - -/// Control of ref-text: Parse takes its text by in. -public sealed class C11ParsesFromIn -{ - public string Text { get; private init; } = ""; - public static C11ParsesFromIn Parse(in string text) => new() { Text = text }; - public override string ToString() => Text; -} - -/// Control of abstract-string-ctor: a sealed class with a public string constructor. -public sealed class C11ConstructedConcrete(string text) -{ - public string Text { get; } = text; - public override string ToString() => Text; -} diff --git a/tools/probe/corpus/harness/BuildRefusals.cs b/tools/probe/corpus/harness/BuildRefusals.cs deleted file mode 100644 index be3d79da..00000000 --- a/tools/probe/corpus/harness/BuildRefusals.cs +++ /dev/null @@ -1,280 +0,0 @@ -// The declarations the generators refuse (shape.refused-kind), written as C# per row, and the consumer build that shows what the -// generators did with them (C22). The build is BuildConsumer.cs's (a throwaway consumer under artifacts/, against the projects this run -// already built, outside the repository's build settings) with Metalama's transformed files kept, since a refused declaration fails the -// build and leaves no assembly to read: the transformed source is where a member the generators added to a refused type would show. -using System.Diagnostics; -using System.Reflection; -using System.Text.RegularExpressions; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Probes.Build; - -/// A consumer build: its exit code, its output, its diagnostics, and Metalama's transformed source of each file. -public sealed record RefusalOutcome(int ExitCode, string Output, IReadOnlyList Diagnostics, IReadOnlyDictionary Transformed) -{ - /// The diagnostics reported in one of the consumer's files. - public IEnumerable In(string file) => Diagnostics.Where(d => Path.GetFileName(d.File) == file); -} - -/// -/// One refusal of the Generators README's "Build errors" table, as a row declares it: the id the table gives it, the obligation of C22 -/// that states it, the consumer it is built in, and the compiler errors that may stand beside it in its own file. -/// -/// -/// A compiler error in a consumer hid every LAMA0611 of the other files (e4b751c), so the declarations whose refusal comes with a -/// compiler error of its own are built apart: a refused closed set leaves its interface's members unimplemented (CS0535, as -/// BuildOutcomeTests allows it), and a hand-written equality operator of a record is also C#'s CS0111 (the README says so). -/// -public sealed record Refusal(string Kind, string Id, string Obligation, string Group, IReadOnlyList Companions); - -/// One row's declarations: the refused type, as the diagnostics name it, and a control the generators must generate. -public sealed record RefusedDeclaration(int Index, Refusal Refusal, string Name, string Display, string Control, string File, string Source); - -public static partial class Refusals -{ - /// The refusals C22 declares, by their shape.refused-kind value; any other value is the harness's mistake, never a pass. - public static readonly IReadOnlyDictionary Table = new Refusal[] - { - new("unsealed-record-class (CMTK1006)", "CMTK1006", "unsealed-record-class", "value-objects", []), - new("over-value-object (CMTK1005)", "CMTK1005", "over-value-object", "value-objects", []), - new("generic-wrapped (CMTK1005)", "CMTK1005", "generic-wrapped", "value-objects", []), - // The label keeps the id it had when C09's and C11's rows were generated; an abstract value object is CMTK1022 since 1ef30be. - new("abstract (CMTK1005)", "CMTK1022", "abstract", "value-objects", []), - new("over-object (CMTK1005)", "CMTK1005", "over-object", "value-objects", []), - new("over-System.Enum (CMTK1005)", "CMTK1005", "over-system-enum", "value-objects", []), - new("named-as-generated-member (CMTK1005)", "CMTK1005", "named-as-generated-member", "value-objects", []), - new("closed-set-named-as-generated-member (CMTK1021)", "CMTK1021", "closed-set-named-as-generated-member", "closed-sets", ["CS0535"]), - new("equality-operator (CMTK1011)", "CMTK1011", "equality-operator", "operators", ["CS0111"]), - }.ToDictionary(r => r.Kind); - - /// - /// The value-object names CMTK1005 refuses (the README's list), and the closed-set names CMTK1021 refuses; a row takes one by its - /// index. C17's and C19's probes name their named-as-generated-member declaration from the same list. - /// - public static readonly string[] ValueObjectNames = ["Value", "From", "TryFrom", "FromKnownGood", "Revalidate", "Parse", "TryParse", "TryFormat", "MinValue", "MaxValue", "CompareTo"]; - static readonly string[] ClosedSetNames = ["Name", "QualifiedName", "All", "Match", "Parse", "TryParse"]; - - /// The link every generator error ends with (Generators README, Build errors: "Every error ends with a link to this table"). - public const string TableLink = "https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.Generators/README.md#build-errors"; - - static readonly string[] Structs = ["record-struct", "validated-struct"]; - - /// - /// The row's declarations. Reads shape.refused-kind, shape.kind, shape.site, shape.inheritance and build-host, and shape.wrapped where - /// the refused kind wraps it. A refused kind the table does not hold, a combination the claim excludes, and a refusal the README's - /// Build errors table no longer lists are harness errors: the row would show nothing about a refusal the README states. - /// - public static RefusedDeclaration Declare(Probes.Shapes.Row r) - { - if (r["build-host"] != "sdk-build") throw new InvalidOperationException("build-host " + r["build-host"] + " has no build here"); - var kindValue = r["shape.refused-kind"]; - if (!Table.TryGetValue(kindValue, out var refusal)) throw new InvalidOperationException($"shape.refused-kind '{kindValue}' is no refusal this harness declares"); - if (!ReadmeLists(refusal.Id)) throw new InvalidOperationException($"the Generators README's Build errors table has no row for {refusal.Id}"); - var kind = r["shape.kind"]; var site = r["shape.site"]; var inheritance = r["shape.inheritance"]; - var n = r.Index; var id = n.ToString(); - var closedSet = kind == "closed-set"; - if (closedSet != (refusal.Group == "closed-sets")) throw new InvalidOperationException($"{kindValue} cannot be declared as a {kind} (excluded in claims.json)"); - if (Structs.Contains(kind) && refusal.Obligation is "unsealed-record-class" or "abstract") throw new InvalidOperationException($"a {kind} cannot be {refusal.Obligation} (excluded in claims.json)"); - if ((Structs.Contains(kind) || closedSet) && inheritance is not ("none" or "explicit-interface-member")) throw new InvalidOperationException($"a {kind} has no base record for {inheritance} (excluded in claims.json)"); - if (site == "in-generic-type") throw new InvalidOperationException("in-generic-type is a refusal of its own (excluded in claims.json)"); - - var name = refusal.Obligation switch - { - "named-as-generated-member" => ValueObjectNames[n % ValueObjectNames.Length], - "closed-set-named-as-generated-member" => ClosedSetNames[n % ClosedSetNames.Length], - _ => $"V{id}", - }; - var control = closedSet ? $"S{id}" : $"K{id}"; - var body = new System.Text.StringBuilder(); - body.AppendLine($"public enum VF{id} {{ No }}"); - var explicitMember = inheritance == "explicit-interface-member"; - if (explicitMember) body.AppendLine($"public interface IMark{id} {{ int Mark(); }}"); - - // The bases, from the topmost down: plain abstract records, the direct one with an explicit IEquatable of its own or an explicit - // member of another interface where shape.inheritance says so. A struct or a closed set implements that interface itself. - var depth = inheritance switch { "base-depth-2" => 2, "base-depth-3" => 3, "none" => 0, _ => 1 }; - if (closedSet || Structs.Contains(kind)) depth = 0; - for (var d = depth; d >= 1; d--) - { - var b = $"B{id}_{d}"; - var parents = new List(); - if (d < depth) parents.Add($"B{id}_{d + 1}"); - var members = ""; - if (d == 1 && inheritance == "base-explicit-IEquatable") { parents.Add($"IEquatable<{b}>"); members = $"bool IEquatable<{b}>.Equals({b}? other) => ReferenceEquals(this, other);"; } - if (d == 1 && explicitMember) { parents.Add($"IMark{id}"); members = $"int IMark{id}.Mark() => 0;"; } - body.AppendLine($"public abstract record {b}{(parents.Count > 0 ? " : " + string.Join(", ", parents) : "")} {{ {members} }}"); - } - var baseList = depth > 0 ? $"B{id}_1, " : ""; - var ownMark = explicitMember && depth == 0; - var markList = ownMark ? $", IMark{id}" : ""; - var markMember = ownMark ? $" int IMark{id}.Mark() => 0;" : ""; - - if (closedSet) - { - body.AppendLine($"public sealed partial class {name} : IClosedSet<{name}>{markList} {{ public static readonly {name} A{id} = new();{markMember} }}"); - body.AppendLine($"public sealed partial class {control} : IClosedSet<{control}>{markList} {{ public static readonly {control} A{id} = new();{markMember} }}"); - } - else - { - var w = refusal.Obligation is "over-object" or "over-system-enum" ? null : Src.Wrapped(r["shape.wrapped"], id); - var wrapped = refusal.Obligation switch - { - "over-value-object" => $"I{id}", - "generic-wrapped" => $"List<{w}>", - "over-object" => "object", - "over-system-enum" => "System.Enum", - _ => w!, - }; - if (refusal.Obligation == "over-value-object") body.AppendLine($"public readonly partial record struct I{id} : IValue<{w}>;"); - var modifier = refusal.Obligation switch { "unsealed-record-class" => "", "abstract" => "abstract ", _ => "sealed " }; - var operators = refusal.Obligation == "equality-operator"; - body.AppendLine(ValueObject(kind, name, modifier, baseList, wrapped, id, markList, markMember, operators)); - body.AppendLine(ValueObject(kind, control, "sealed ", baseList, w ?? "int", id, markList, markMember, operators: false)); - } - - var source = Src.Usings + $"namespace C22R{id};\n" + Src.WrappedDeclarations(id) + Src.AtSite(site, id, body.ToString()) + "\n"; - return new(n, refusal, name, Nesting(site, id) + name, control, $"R{id}.cs", source); - } - - static string ValueObject(string kind, string name, string modifier, string baseList, string wrapped, string id, string markList, string markMember, bool operators) - { - var validated = kind.StartsWith("validated", StringComparison.Ordinal); - var marker = validated ? $"IValidatedValue<{name}, {wrapped}, VF{id}>" : $"IValue<{wrapped}>"; - var head = Structs.Contains(kind) - ? $"public readonly partial record struct {name} : {marker}{markList}" - : $"public {modifier}partial record {name} : {baseList}{marker}{markList}"; - var members = new List(); - if (validated) members.Add($"public static Result<{name}, VF{id}> Create({wrapped} value) => Result.Error(VF{id}.No);"); - if (operators) - { - var t = Structs.Contains(kind) ? name : name + "?"; - members.Add($"public static bool operator ==({t} left, {t} right) => true;"); - members.Add($"public static bool operator !=({t} left, {t} right) => false;"); - } - if (markMember.Length > 0) members.Add(markMember.Trim()); - return members.Count == 0 ? head + ";" : $"{head}\n{{\n {string.Join("\n ", members)}\n}}"; - } - - /// shape.site's nesting as a diagnostic names a type: its containing types, dotted. - static string Nesting(string site, string id) => site switch - { - "top-level" => "", "nested-in-class" or "nested-in-struct" => $"O{id}.", "nested-two-levels" => $"O{id}.P{id}.", - _ => throw new ArgumentException("shape.site " + site), - }; - - static readonly Lazy Readme = new(() => File.ReadAllText(Path.Combine(Consumer.RepositoryRoot(), "src", "CodoMetis.TypeKit.Generators", "README.md"))); - - /// Whether the README's Build errors table has a row for . - public static bool ReadmeLists(string id) => - Readme.Value.IndexOf("\n## Build errors", StringComparison.Ordinal) is var at and >= 0 - && Readme.Value.IndexOf($"\n| {id} |", at, StringComparison.Ordinal) >= 0; - - /// Whether the README still has the heading the table link points at. - public static bool ReadmeHasBuildErrorsHeading() => Readme.Value.Split('\n').Any(l => l.TrimEnd('\r') == "## Build errors"); - - /// - /// How the generators left a type in Metalama's transformed source: null where its declaration is as it was written (no base - /// type and no member added, and no {Name}Extensions companion), else what they added. Throws where the type is not there. - /// - public static string? Added(RefusalOutcome build, RefusedDeclaration d, string name) - { - if (!build.Transformed.TryGetValue(d.File, out var transformed)) throw new InvalidOperationException($"Metalama wrote no transformed {d.File}"); - var written = One(CSharpSyntaxTree.ParseText(d.Source).GetRoot(), name, "the row's source"); - var woven = One(CSharpSyntaxTree.ParseText(transformed).GetRoot(), name, "the transformed " + d.File); - var added = new List(); - var bw = written.BaseList?.Types.Count ?? 0; var bt = woven.BaseList?.Types.Count ?? 0; - if (bt != bw) added.Add($"{bt - bw} base type(s)"); - if (woven.Members.Count != written.Members.Count) added.Add($"{woven.Members.Count - written.Members.Count} member(s)"); - var companion = name + "Extensions"; - foreach (var (file, text) in build.Transformed) - if (text.Contains($"namespace C22R{d.Index}", StringComparison.Ordinal) && text.Contains(companion, StringComparison.Ordinal) - && CSharpSyntaxTree.ParseText(text).GetRoot().DescendantNodes().OfType().Any(c => c.Identifier.Text == companion)) - added.Add($"the companion {companion} ({file})"); - return added.Count == 0 ? null : string.Join(", ", added); - } - - static TypeDeclarationSyntax One(Microsoft.CodeAnalysis.SyntaxNode root, string name, string where) - { - var found = root.DescendantNodes().OfType().Where(t => t.Identifier.Text == name).ToList(); - return found.Count == 1 ? found[0] : throw new InvalidOperationException($"{found.Count} declarations of {name} in {where}"); - } - - static readonly Dictionary>> Builds = new(); - - /// Builds the consumer of once per in this process. - public static Task Build(string key, Func> files) - { - lock (Builds) - { - if (!Builds.TryGetValue(key, out var build)) Builds[key] = build = new(() => Run(files())); - return build.Value; - } - } - - static string Configuration => typeof(Refusals).Assembly.GetCustomAttribute()?.Configuration - ?? throw new InvalidOperationException("the probe assembly carries no AssemblyConfigurationAttribute"); - - static async Task Run(IReadOnlyDictionary files) - { - var root = Consumer.RepositoryRoot(); - var dir = Path.Combine(root, "artifacts", $"probe-refusals-{Guid.NewGuid():N}"); - Directory.CreateDirectory(dir); - try - { - var src = Path.Combine(root, "src"); - await File.WriteAllTextAsync(Path.Combine(dir, "Consumer.csproj"), $""" - - - net10.0 - enable - disable - ProbeRefusals - true - - - - {typeof(NodaTime.Instant).Assembly.Location} - - - """); - await File.WriteAllTextAsync(Path.Combine(dir, "Directory.Build.props"), ""); - await File.WriteAllTextAsync(Path.Combine(dir, "Directory.Build.targets"), ""); - await File.WriteAllTextAsync(Path.Combine(dir, "Directory.Packages.props"), - "false"); - foreach (var (name, text) in files) await File.WriteAllTextAsync(Path.Combine(dir, name), text); - - var dotnet = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH") is { Length: > 0 } host ? host : "dotnet"; - var info = new ProcessStartInfo(dotnet, ["build", dir, "-c", Configuration, "--no-dependencies", "-p:RestoreRecursive=false", "-nodeReuse:false", "-clp:NoSummary", "-v:q", "-nologo"]) - { - RedirectStandardOutput = true, RedirectStandardError = true, WorkingDirectory = dir, - }; - // The run's code coverage instruments this process through its environment; the build is not the probe's code. - foreach (var k in info.Environment.Keys.ToList()) - if (k.StartsWith("CORECLR_", StringComparison.OrdinalIgnoreCase) || k.StartsWith("COR_", StringComparison.OrdinalIgnoreCase) - || k.Contains("CODECOVERAGE", StringComparison.OrdinalIgnoreCase) || k.StartsWith("MicrosoftInstrumentationEngine", StringComparison.OrdinalIgnoreCase) - || k.Equals("DOTNET_STARTUP_HOOKS", StringComparison.OrdinalIgnoreCase)) - info.Environment.Remove(k); - info.Environment["DOTNET_CLI_UI_LANGUAGE"] = "en"; - using var process = Process.Start(info)!; - var stdout = process.StandardOutput.ReadToEndAsync(); - var stderr = process.StandardError.ReadToEndAsync(); - using (var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(10))) await process.WaitForExitAsync(timeout.Token); - var output = await stdout + await stderr; - var diagnostics = Line().Matches(output) - .Select(m => new BuildDiagnostic(m.Groups["file"].Value.Trim(), m.Groups["sev"].Value, m.Groups["id"].Value, m.Groups["msg"].Value.Trim())) - .Distinct().ToList(); - var woven = Path.Combine(dir, "obj", Configuration, "net10.0", "metalama"); - var transformed = Directory.Exists(woven) - ? Directory.GetFiles(woven, "*.cs").ToDictionary(f => Path.GetFileName(f), f => File.ReadAllText(f)) - : new Dictionary(); - return new RefusalOutcome(process.ExitCode, output, diagnostics, transformed); - } - finally - { - try { Directory.Delete(dir, recursive: true); } catch (IOException) { } catch (UnauthorizedAccessException) { } - } - } - - [GeneratedRegex(@"^(?[^\r\n(]+)\((?\d+),(?\d+)\): (?warning|error) (?[A-Z]+\d+): (?.*?)(?: \[[^\]\r\n]*\])?\r?$", RegexOptions.Multiline)] - private static partial Regex Line(); -} diff --git a/tools/probe/corpus/harness/BuildRoslyn.cs b/tools/probe/corpus/harness/BuildRoslyn.cs deleted file mode 100644 index 2f3584d1..00000000 --- a/tools/probe/corpus/harness/BuildRoslyn.cs +++ /dev/null @@ -1,167 +0,0 @@ -// The analyzer probes' compiler (C17 CMTK0014, C19 CMTK0010): a CSharpCompilation over the real CodoMetis.TypeKit and the -// running framework, with the CMTK analyzer that reports the claim's id attached, as test/CodoMetis.TypeKit.Analyzers.Tests -// runs it. Sources carry markup {|#label:...|}; a marked span is where the probe expects (or forbids) a report. -using System.Collections.Immutable; -using System.Reflection; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Text; -namespace Probes.Build; - -/// One marked span of a source file, by its label. -public sealed record Mark(string Label, string File, TextSpan Span, string Text); - -/// A source file whose markup was taken out, with the spans it marked. -public sealed record Marked(string File, string Text, IReadOnlyList Marks) -{ - /// Strips {|#label:…|} (not nested) and records each span in the stripped text. - public static Marked Parse(string file, string source) - { - var sb = new System.Text.StringBuilder(); var marks = new List(); var i = 0; - while (i < source.Length) - { - var open = source.IndexOf("{|#", i, StringComparison.Ordinal); - if (open < 0) { sb.Append(source, i, source.Length - i); break; } - sb.Append(source, i, open - i); - var colon = source.IndexOf(':', open); - var label = source[(open + 3)..colon]; - var close = source.IndexOf("|}", colon, StringComparison.Ordinal); - if (close < 0) throw new InvalidOperationException($"unclosed markup {label} in {file}"); - var inner = source[(colon + 1)..close]; - if (inner.Contains("{|", StringComparison.Ordinal)) throw new InvalidOperationException($"nested markup in {label} of {file}"); - marks.Add(new Mark(label, file, new TextSpan(sb.Length, inner.Length), inner)); - sb.Append(inner); - i = close + 2; - } - return new Marked(file, sb.ToString(), marks); - } -} - -/// What one compilation reported: compiler errors, the analyzer's diagnostics, analyzer failures. -public sealed record Analysis(IReadOnlyList Errors, IReadOnlyList Reported, IReadOnlyList AnalyzerFailures, IReadOnlyList Files) -{ - public IEnumerable Marks => Files.SelectMany(f => f.Marks); - public static string Where(Diagnostic d) => $"{d.Location.SourceTree?.FilePath}:{d.Location.SourceSpan} '{d.Location.SourceTree?.GetText().ToString(d.Location.SourceSpan)}'"; - public string ErrorText() => string.Join("; ", Errors.Take(5).Select(e => $"{e.Id} {Where(e)}: {e.GetMessage()}")); -} - -public static class Rz -{ - static readonly Lazy> Refs = new(() => - { - // The running shared framework (implementation assemblies, as the probe's own process binds), plus the real base package and NodaTime. - var framework = Path.GetDirectoryName(typeof(object).Assembly.Location)!; - var tpa = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!).Split(Path.PathSeparator) - .Where(p => Path.GetDirectoryName(p) == framework && Path.GetFileName(p) is var n && (n.StartsWith("System.") || n.StartsWith("Microsoft.") || n is "mscorlib.dll" or "netstandard.dll")); - return [.. tpa.Concat([typeof(CodoMetis.TypeKit.Option<>).Assembly.Location, typeof(NodaTime.Instant).Assembly.Location]) - .Distinct().Select(p => (MetadataReference)MetadataReference.CreateFromFile(p))]; - }); - - /// The CMTK analyzers that report , found by the id they declare, never by type name. - public static ImmutableArray AnalyzersFor(string id) => - [ - .. typeof(CodoMetis.TypeKit.Analyzers.TypeArgumentConstraintAnalyzer).Assembly.GetTypes() - .Where(t => !t.IsAbstract && typeof(DiagnosticAnalyzer).IsAssignableFrom(t) && t.GetCustomAttribute() is not null) - .Select(t => (DiagnosticAnalyzer)Activator.CreateInstance(t)!) - .Where(a => a.SupportedDiagnostics.Any(d => d.Id == id)), - ]; - - public static async Task Analyze(string id, IEnumerable<(string File, string Source)> sources) - { - var files = sources.Select(s => Marked.Parse(s.File, s.Source)).ToList(); - var parse = new CSharpParseOptions(LanguageVersion.Latest); - var trees = files.Select(f => CSharpSyntaxTree.ParseText(f.Text, parse, path: f.File)).ToList(); - var compilation = CSharpCompilation.Create("ProbeBuild" + Guid.NewGuid().ToString("N"), trees, Refs.Value, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable)); - var analyzers = AnalyzersFor(id); - if (analyzers.Length == 0) throw new InvalidOperationException($"no CMTK analyzer declares {id}"); - var failures = new List(); - var options = new CompilationWithAnalyzersOptions(new AnalyzerOptions([]), (ex, analyzer, diagnostic) => { lock (failures) failures.Add(diagnostic); }, - concurrentAnalysis: false, logAnalyzerExecutionTime: false); - var with = compilation.WithAnalyzers(analyzers, options); - var analyzerDiagnostics = await with.GetAnalyzerDiagnosticsAsync(CancellationToken.None); - var errors = compilation.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error).ToList(); - failures.AddRange(analyzerDiagnostics.Where(d => d.Id == "AD0001")); - return new Analysis(errors, [.. analyzerDiagnostics.Where(d => d.Id == id)], failures, files); - } - - /// - /// Compares the reports with the marks: every mark in reported exactly once at its span, nothing - /// reported anywhere else. Null when they agree, else what differs. - /// - public static string? Agree(Analysis a, IReadOnlySet expected, Func? message = null) => - Agree(a, expected, _ => true, outside: true, message); - - /// - /// The same over the marks selects, one check's share of a compilation: each of them in - /// reported exactly once at its span, the others silent, and with , nothing - /// reported outside every mark. A report at a mark the check does not judge is another check's. - /// - public static string? Agree(Analysis a, IReadOnlySet expected, Func judged, bool outside, Func? message = null) - { - var marks = a.Marks.ToList(); - foreach (var label in expected) if (!marks.Any(m => m.Label == label)) throw new InvalidOperationException($"no mark {label}"); - var problems = new List(); - foreach (var m in marks.Where(judged)) - { - var at = At(a, m); - if (expected.Contains(m.Label)) - { - if (at.Count != 1) problems.Add($"{m.Label} '{m.Text}': reported {at.Count} times, expected once"); - else if (message?.Invoke(at[0], m) is { } bad) problems.Add($"{m.Label} '{m.Text}': {bad}"); - } - else if (at.Count != 0) problems.Add($"{m.Label} '{m.Text}': reported, expected silent ({at[0].GetMessage()})"); - } - if (outside) problems.AddRange(Outside(a).Select(d => $"reported outside every mark at {Analysis.Where(d)}: {d.GetMessage()}")); - return problems.Count == 0 ? null : string.Join("; ", problems); - } - - /// The reports exactly at a mark's span. - public static List At(Analysis a, Mark m) => - [.. a.Reported.Where(d => d.Location.SourceTree?.FilePath == m.File && d.Location.SourceSpan == m.Span)]; - - /// The reports whose span holds a mark's: at it, or at the syntax around it. - public static List Around(Analysis a, Mark m) => - [.. a.Reported.Where(d => d.Location.SourceTree?.FilePath == m.File && d.Location.SourceSpan.Contains(m.Span))]; - - /// The reports whose span holds no mark. - public static List Outside(Analysis a) => - [.. a.Reported.Where(d => !a.Marks.Any(m => d.Location.SourceTree?.FilePath == m.File && d.Location.SourceSpan.Contains(m.Span)))]; - - /// - /// Whether a value is reported, wherever around it: each mark names that is in - /// has exactly one report at or around it, the others none. Where the report sits is 's question, so a - /// report moved to the syntax around its value still counts here. Null when they agree, else what differs. - /// - public static string? Reported(Analysis a, IEnumerable judged, IReadOnlySet expected, Func? message = null) - { - var problems = new List(); - foreach (var label in judged) - { - var m = a.Marks.SingleOrDefault(x => x.Label == label) ?? throw new InvalidOperationException($"no mark {label}"); - var around = Around(a, m); - if (expected.Contains(label)) - { - if (around.Count != 1) problems.Add($"{m.Label} '{m.Text}': reported {around.Count} times, expected once"); - else if (message?.Invoke(around[0], m) is { } bad) problems.Add($"{m.Label} '{m.Text}': {bad}"); - } - else if (around.Count != 0) problems.Add($"{m.Label} '{m.Text}': reported, expected silent ({around[0].GetMessage()})"); - } - return problems.Count == 0 ? null : string.Join("; ", problems); - } - - /// - /// Where the reports sit: every report at or around a mark in is at the mark's own span, the value as - /// written, and none is outside every mark. Null when so, else what differs. - /// - public static string? WhereWritten(Analysis a, IReadOnlySet expected) - { - var problems = new List(); - foreach (var m in a.Marks.Where(m => expected.Contains(m.Label))) - foreach (var d in Around(a, m).Where(d => d.Location.SourceSpan != m.Span)) - problems.Add($"{m.Label} '{m.Text}': reported at {Analysis.Where(d)}, not at the value"); - problems.AddRange(Outside(a).Select(d => $"reported outside every mark at {Analysis.Where(d)}: {d.GetMessage()}")); - return problems.Count == 0 ? null : string.Join("; ", problems); - } -} diff --git a/tools/probe/corpus/harness/BuildSource.cs b/tools/probe/corpus/harness/BuildSource.cs deleted file mode 100644 index bc37edfc..00000000 --- a/tools/probe/corpus/harness/BuildSource.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Source text for the build family's probes (C17, C18, C19): a row's shape.* axes written as C#, for a compilation the probe -// makes itself (an analyzer run, or the consumer build of C18). Each piece names its types after a suffix, so the pieces of -// several rows can sit in one compilation. -namespace Probes.Build; - -public static class Src -{ - public const string Usings = - """ - #nullable enable - using System; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Threading.Tasks; - using CodoMetis.TypeKit; - using CodoMetis.TypeKit.ClosedSets; - using CodoMetis.TypeKit.ValueObjects; - - """; - - /// shape.site: declared at the top level, nested in a class or a struct, two levels down, or in a generic type. - public static string AtSite(string site, string id, string inner) => site switch - { - "top-level" => inner, - "nested-in-class" => $"public static partial class O{id}\n{{\n{inner}\n}}", - "nested-in-struct" => $"public partial struct O{id}\n{{\n{inner}\n}}", - "nested-two-levels" => $"public static partial class O{id}\n{{\npublic sealed partial class P{id}\n{{\n{inner}\n}}\n}}", - "in-generic-type" => $"public static partial class O{id}\n{{\n{inner}\n}}", - _ => throw new ArgumentException("shape.site " + site), - }; - - /// shape.wrapped as a type name; enum, flags-enum and own-record-class are declared by . - public static string Wrapped(string wrapped, string id) => wrapped switch - { - "string" or "int" or "long" or "byte" or "ulong" or "decimal" or "double" or "bool" or "char" => wrapped, - "Guid" or "DateTime" or "DateTimeOffset" or "DateOnly" or "TimeOnly" or "TimeSpan" or "Uri" => "System." + wrapped, - "enum" => $"WEnum{id}", "flags-enum" => $"WFlags{id}", "NodaTime.Instant" => "global::NodaTime.Instant", "own-record-class" => $"WRec{id}", - _ => throw new ArgumentException("shape.wrapped " + wrapped), - }; - - public static string WrappedDeclarations(string id) => - $"public enum WEnum{id} {{ A, B }}\n[Flags] public enum WFlags{id} {{ None = 0, P = 1, Q = 2 }}\npublic sealed record WRec{id} {{ public int N {{ get; init; }} }}\n"; - - public static bool IsStructKind(string kind) => kind is "record-struct" or "validated-struct"; - - /// The type head of shape.containing, and the type as written from inside it (generic-type is closed over int outside). - public static string ContainerHead(string containing, string name, string primary = "") => containing switch - { - "class" => $"public sealed partial class {name}{primary}", - "record-class" => $"public sealed partial record {name}{primary}", - "struct" => $"public partial struct {name}{primary}", - "record-struct" => $"public partial record struct {name}{primary}", - "generic-type" => $"public sealed partial class {name}{primary}", - _ => throw new ArgumentException("shape.containing " + containing), - }; - - public static string ContainerSelf(string containing, string name) => containing == "generic-type" ? $"{name}" : name; -} diff --git a/tools/probe/corpus/harness/C09Shapes.g.cs b/tools/probe/corpus/harness/C09Shapes.g.cs deleted file mode 100644 index 2a365946..00000000 --- a/tools/probe/corpus/harness/C09Shapes.g.cs +++ /dev/null @@ -1,1031 +0,0 @@ -// by tools/probe/generators/decls.cs from rows/C09BaseRecordEqualityProbe.json.gz: one value object per row. Do not edit; regenerate with the rows. -#nullable enable -// CMTK1023: the bases' own equality is the point (C09) or beside it (C11); CS8851: a base's Equals without GetHashCode, on purpose. -#pragma warning disable CMTK1023, CS8851, CS1591 -using CodoMetis.TypeKit; -using CodoMetis.TypeKit.ValueObjects; -namespace Probes.Build.C09; - -// row 0 -public static partial class O0 -{ -public sealed partial class P0 -{ -public abstract record B0_1 : System.IEquatable { bool System.IEquatable.Equals(B0_1? other) => ReferenceEquals(this, other); } -public sealed partial record V0 : B0_1, IValidatedValue -{ - public static Result Create(global::Probes.Shapes.PRec value) => new V0(value); -} -} -} - -// row 1 -public static partial class O1 -{ -public sealed partial class P1 -{ -public abstract record B1_3 { public virtual bool Equals(B1_3? other) => ReferenceEquals(this, other); } -public abstract record B1_2 : B1_3 { } -public abstract record B1_1 : B1_2 { } -public sealed partial record V1 : B1_1, IValue; -} -} - -// row 2 -public partial struct O2 -{ -public abstract record B2_1 { public override int GetHashCode() => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this); } -public sealed partial record V2 : B2_1, IValidatedValue -{ - public static Result Create(long value) => new V2(value); -} -} - -// row 3 -public abstract record B3_1 { } -public sealed partial record V3 : B3_1, IValue; - -// row 4 -public interface IMark4 { int Mark(); } -public abstract record B4_1 : IMark4 { public override int GetHashCode() => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this); int IMark4.Mark() => 1; } -public sealed partial record V4 : B4_1, IValidatedValue -{ - public static Result Create(string value) => new V4(value); -} - -// row 5 -public static partial class O5 -{ -public interface IMark5 { int Mark(); } -public abstract record B5_1 : System.IEquatable, IMark5 { bool System.IEquatable.Equals(B5_1? other) => ReferenceEquals(this, other); int IMark5.Mark() => 1; } -public sealed partial record V5 : B5_1, IValue; -} - -// row 6 -public static partial class O6 -{ -public abstract record B6_2 { } -public abstract record B6_1 : B6_2 { } -public sealed partial record V6 : B6_1, IValidatedValue -{ - public static Result Create(global::Probes.Shapes.PFlags value) => new V6(value); -} -} - -// row 7 -public partial struct O7 -{ -public abstract record B7_2 { public override int GetHashCode() => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this); } -public abstract record B7_1 : B7_2 { } -public sealed partial record V7 : B7_1, IValue; -} - -// row 8 -public partial struct O8 -{ -public abstract record B8_1 : System.IEquatable { bool System.IEquatable.Equals(B8_1? other) => ReferenceEquals(this, other); } -public sealed partial record V8 : B8_1, IValue; -} - -// row 9 -public static partial class O9 -{ -public sealed partial class P9 -{ -public interface IMark9 { int Mark(); } -public abstract record B9_1 : IMark9 { int IMark9.Mark() => 1; } -public sealed partial record V9 : B9_1, IValue; -} -} - -// row 10 -public abstract record B10_2 { public virtual bool Equals(B10_2? other) => ReferenceEquals(this, other); } -public abstract record B10_1 : B10_2 { } -public sealed partial record V10 : B10_1, IValidatedValue -{ - public static Result Create(global::System.DateOnly value) => new V10(value); -} - -// row 11 -public static partial class O11 -{ -public abstract record B11_1 { public virtual bool Equals(B11_1? other) => ReferenceEquals(this, other); } -public sealed partial record V11 : B11_1, IValidatedValue -{ - public static Result Create(global::System.Guid value) => new V11(value); -} -} - -// row 12 -public partial struct O12 -{ -public abstract record B12_2 : System.IEquatable { bool System.IEquatable.Equals(B12_2? other) => ReferenceEquals(this, other); } -public abstract record B12_1 : B12_2 { } -public sealed partial record V12 : B12_1, IValidatedValue -{ - public static Result Create(ulong value) => new V12(value); -} -} - -// row 13 -public partial struct O13 -{ -public abstract record B13_3 { public virtual bool Equals(B13_3? other) => ReferenceEquals(this, other); } -public abstract record B13_2 : B13_3 { } -public abstract record B13_1 : B13_2 { } -public sealed partial record V13 : B13_1, IValidatedValue -{ - public static Result Create(decimal value) => new V13(value); -} -} - -// row 14 -public static partial class O14 -{ -public abstract record B14_3 { public override int GetHashCode() => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this); } -public abstract record B14_2 : B14_3 { } -public abstract record B14_1 : B14_2 { } -public sealed partial record V14 : B14_1, IValue; -} - -// row 15 -public partial struct O15 -{ -public interface IMark15 { int Mark(); } -public abstract record B15_1 : IMark15 { public virtual bool Equals(B15_1? other) => ReferenceEquals(this, other); int IMark15.Mark() => 1; } -public sealed partial record V15 : B15_1, IValidatedValue -{ - public static Result Create(global::NodaTime.Instant value) => new V15(value); -} -} - -// row 16 -public abstract record B16_3 : System.IEquatable { bool System.IEquatable.Equals(B16_3? other) => ReferenceEquals(this, other); } -public abstract record B16_2 : B16_3 { } -public abstract record B16_1 : B16_2 { } -public sealed partial record V16 : B16_1, IValue; - -// row 17 -public static partial class O17 -{ -public abstract record B17_1 : System.IEquatable { public virtual bool Equals(B17_1? other) => ReferenceEquals(this, other); bool System.IEquatable.Equals(B17_1? other) => ReferenceEquals(this, other); } -public sealed partial record V17 : B17_1, IValue; -} - -// row 18 -public static partial class O18 -{ -public sealed partial class P18 -{ -public abstract record B18_1 { public override int GetHashCode() => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this); } -public sealed partial record V18 : B18_1, IValue; -} -} - -// row 19 -public abstract record B19_1 : System.IEquatable { public override int GetHashCode() => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this); bool System.IEquatable.Equals(B19_1? other) => ReferenceEquals(this, other); } -public sealed partial record V19 : B19_1, IValidatedValue -{ - public static Result Create(char value) => new V19(value); -} - -// row 20 -public abstract record B20_3 { } -public abstract record B20_2 : B20_3 { } -public abstract record B20_1 : B20_2 { } -public sealed partial record V20 : B20_1, IValidatedValue -{ - public static Result Create(global::System.DateTime value) => new V20(value); -} - -// row 21 -public static partial class O21 -{ -public abstract record B21_2 : System.IEquatable { bool System.IEquatable.Equals(B21_2? other) => ReferenceEquals(this, other); } -public abstract record B21_1 : B21_2 { } -public sealed partial record V21 : B21_1, IValue; -} - -// row 22 -public static partial class O22 -{ -public sealed partial class P22 -{ -public abstract record B22_2 { } -public abstract record B22_1 : B22_2 { } -public sealed partial record V22 : B22_1, IValue; -} -} - -// row 23 -public abstract record B23_1 : System.IEquatable { bool System.IEquatable.Equals(B23_1? other) => ReferenceEquals(this, other); } -public sealed partial record V23 : B23_1, IValidatedValue -{ - public static Result Create(global::System.TimeOnly value) => new V23(value); -} - -// row 24 -public static partial class O24 -{ -public sealed partial class P24 -{ -public abstract record B24_1 { } -public sealed partial record V24 : B24_1, IValidatedValue -{ - public static Result Create(global::System.Uri value) => new V24(value); -} -} -} - -// row 25 -public static partial class O25 -{ -public abstract record B25_1 { } -public sealed partial record V25 : B25_1, IValidatedValue -{ - public static Result Create(bool value) => new V25(value); -} -} - -// row 26 -public static partial class O26 -{ -public sealed partial class P26 -{ -public abstract record B26_3 { public override int GetHashCode() => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this); } -public abstract record B26_2 : B26_3 { } -public abstract record B26_1 : B26_2 { } -public sealed partial record V26 : B26_1, IValidatedValue -{ - public static Result Create(global::Probes.Shapes.PEnum value) => new V26(value); -} -} -} - -// row 27 -public abstract record B27_2 { } -public abstract record B27_1 : B27_2 { } -public sealed partial record V27 : B27_1, IValue; - -// row 28 -public partial struct O28 -{ -public abstract record B28_1 { } -public sealed partial record V28 : B28_1, IValue; -} - -// row 29 -public partial struct O29 -{ -public interface IMark29 { int Mark(); } -public abstract record B29_1 : System.IEquatable, IMark29 { bool System.IEquatable.Equals(B29_1? other) => ReferenceEquals(this, other); int IMark29.Mark() => 1; } -public sealed partial record V29 : B29_1, IValue; -} - -// row 30 -public abstract record B30_2 : System.IEquatable { bool System.IEquatable.Equals(B30_2? other) => ReferenceEquals(this, other); } -public abstract record B30_1 : B30_2 { } -public sealed partial record V30 : B30_1, IValue; - -// row 31 -public static partial class O31 -{ -public sealed partial class P31 -{ -public abstract record B31_3 { } -public abstract record B31_2 : B31_3 { } -public abstract record B31_1 : B31_2 { } -public sealed partial record V31 : B31_1, IValue; -} -} - -// row 32 -public partial struct O32 -{ -public abstract record B32_1 : System.IEquatable { bool System.IEquatable.Equals(B32_1? other) => ReferenceEquals(this, other); } -public sealed partial record V32 : B32_1, IValidatedValue -{ - public static Result Create(double value) => new V32(value); -} -} - -// row 33 -public static partial class O33 -{ -public sealed partial class P33 -{ -public abstract record B33_1 : System.IEquatable { public virtual bool Equals(B33_1? other) => ReferenceEquals(this, other); bool System.IEquatable.Equals(B33_1? other) => ReferenceEquals(this, other); } -public sealed partial record V33 : B33_1, IValue; -} -} - -// row 34 -public partial struct O34 -{ -public abstract record B34_1 { public override int GetHashCode() => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this); } -public sealed partial record V34 : B34_1, IValidatedValue -{ - public static Result Create(global::System.DateTimeOffset value) => new V34(value); -} -} - -// row 35 -public abstract record B35_3 { public override int GetHashCode() => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this); } -public abstract record B35_2 : B35_3 { } -public abstract record B35_1 : B35_2 { } -public sealed partial record V35 : B35_1, IValue; - -// row 36 -public static partial class O36 -{ -public abstract record B36_3 { } -public abstract record B36_2 : B36_3 { } -public abstract record B36_1 : B36_2 { } -public sealed partial record V36 : B36_1, IValue; -} - -// row 37 -public static partial class O37 -{ -public abstract record B37_1 : System.IEquatable { bool System.IEquatable.Equals(B37_1? other) => ReferenceEquals(this, other); } -public sealed partial record V37 : B37_1, IValue; -} - -// row 38 -public partial struct O38 -{ -public abstract record B38_2 : System.IEquatable { bool System.IEquatable.Equals(B38_2? other) => ReferenceEquals(this, other); } -public abstract record B38_1 : B38_2 { } -public sealed partial record V38 : B38_1, IValue; -} - -// row 39 -public abstract record B39_2 { public virtual bool Equals(B39_2? other) => ReferenceEquals(this, other); } -public abstract record B39_1 : B39_2 { } -public sealed partial record V39 : B39_1, IValidatedValue -{ - public static Result Create(byte value) => new V39(value); -} - -// row 40 -public partial struct O40 -{ -public abstract record B40_3 { public virtual bool Equals(B40_3? other) => ReferenceEquals(this, other); } -public abstract record B40_2 : B40_3 { } -public abstract record B40_1 : B40_2 { } -public sealed partial record V40 : B40_1, IValidatedValue -{ - public static Result Create(global::Probes.Shapes.PFlags value) => new V40(value); -} -} - -// row 41 -public abstract record B41_3 { } -public abstract record B41_2 : B41_3 { } -public abstract record B41_1 : B41_2 { } -public sealed partial record V41 : B41_1, IValidatedValue -{ - public static Result Create(global::System.TimeSpan value) => new V41(value); -} - -// row 42 -public partial struct O42 -{ -public interface IMark42 { int Mark(); } -public abstract record B42_1 : IMark42 { public virtual bool Equals(B42_1? other) => ReferenceEquals(this, other); int IMark42.Mark() => 1; } -public sealed partial record V42 : B42_1, IValue; -} - -// row 43 -public partial struct O43 -{ -public abstract record B43_1 { public virtual bool Equals(B43_1? other) => ReferenceEquals(this, other); } -public sealed partial record V43 : B43_1, IValue; -} - -// row 44 -public partial struct O44 -{ -public abstract record B44_1 : System.IEquatable { public virtual bool Equals(B44_1? other) => ReferenceEquals(this, other); bool System.IEquatable.Equals(B44_1? other) => ReferenceEquals(this, other); } -public sealed partial record V44 : B44_1, IValue; -} - -// row 45 -public static partial class O45 -{ -public abstract record B45_1 { } -public sealed partial record V45 : B45_1, IValidatedValue -{ - public static Result Create(global::Probes.Shapes.PEnum value) => new V45(value); -} -} - -// row 46 -public static partial class O46 -{ -public interface IMark46 { int Mark(); } -public abstract record B46_1 : System.IEquatable, IMark46 { bool System.IEquatable.Equals(B46_1? other) => ReferenceEquals(this, other); int IMark46.Mark() => 1; } -public sealed partial record V46 : B46_1, IValidatedValue -{ - public static Result Create(long value) => new V46(value); -} -} - -// row 47 -public abstract record B47_3 { public override int GetHashCode() => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this); } -public abstract record B47_2 : B47_3 { } -public abstract record B47_1 : B47_2 { } -public sealed partial record V47 : B47_1, IValue; - -// row 48 -public partial struct O48 -{ -public abstract record B48_3 { } -public abstract record B48_2 : B48_3 { } -public abstract record B48_1 : B48_2 { } -public sealed partial record V48 : B48_1, IValue; -} - -// row 49 -public interface IMark49 { int Mark(); } -public abstract record B49_1 : IMark49 { public override int GetHashCode() => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this); int IMark49.Mark() => 1; } -public sealed partial record V49 : B49_1, IValue; - -// row 50 -public static partial class O50 -{ -public sealed partial class P50 -{ -public abstract record B50_3 { public override int GetHashCode() => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this); } -public abstract record B50_2 : B50_3 { } -public abstract record B50_1 : B50_2 { } -public sealed partial record V50 : B50_1, IValue; -} -} - -// row 51 -public abstract record B51_1 : System.IEquatable { public override int GetHashCode() => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this); bool System.IEquatable.Equals(B51_1? other) => ReferenceEquals(this, other); } -public sealed partial record V51 : B51_1, IValidatedValue -{ - public static Result Create(bool value) => new V51(value); -} - -// row 52 -public abstract record B52_2 { } -public abstract record B52_1 : B52_2 { } -public sealed partial record V52 : B52_1, IValue; - -// row 53 -public partial struct O53 -{ -public interface IMark53 { int Mark(); } -public abstract record B53_1 : IMark53 { int IMark53.Mark() => 1; } -public sealed partial record V53 : B53_1, IValue; -} - -// row 54 -public partial struct O54 -{ -public abstract record B54_1 : System.IEquatable { public override int GetHashCode() => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this); bool System.IEquatable.Equals(B54_1? other) => ReferenceEquals(this, other); } -public sealed partial record V54 : B54_1, IValidatedValue -{ - public static Result Create(global::System.DateTime value) => new V54(value); -} -} - -// row 55 -public partial struct O55 -{ -public abstract record B55_3 : System.IEquatable { bool System.IEquatable.Equals(B55_3? other) => ReferenceEquals(this, other); } -public abstract record B55_2 : B55_3 { } -public abstract record B55_1 : B55_2 { } -public sealed partial record V55 : B55_1, IValue; -} - -// row 56 -public interface IMark56 { int Mark(); } -public abstract record B56_1 : IMark56 { int IMark56.Mark() => 1; } -public sealed partial record V56 : B56_1, IValidatedValue -{ - public static Result Create(decimal value) => new V56(value); -} - -// row 57 -public static partial class O57 -{ -public abstract record B57_1 { public virtual bool Equals(B57_1? other) => ReferenceEquals(this, other); } -public sealed partial record V57 : B57_1, IValidatedValue -{ - public static Result Create(double value) => new V57(value); -} -} - -// row 58 -public interface IMark58 { int Mark(); } -public abstract record B58_1 : IMark58 { public virtual bool Equals(B58_1? other) => ReferenceEquals(this, other); int IMark58.Mark() => 1; } -public sealed partial record V58 : B58_1, IValidatedValue -{ - public static Result Create(global::System.TimeOnly value) => new V58(value); -} - -// row 59 -public partial struct O59 -{ -public abstract record B59_1 : System.IEquatable { bool System.IEquatable.Equals(B59_1? other) => ReferenceEquals(this, other); } -public sealed partial record V59 : B59_1, IValue; -} - -// row 60 -public abstract record B60_1 : System.IEquatable { public virtual bool Equals(B60_1? other) => ReferenceEquals(this, other); bool System.IEquatable.Equals(B60_1? other) => ReferenceEquals(this, other); } -public sealed partial record V60 : B60_1, IValidatedValue -{ - public static Result Create(global::Probes.Shapes.PEnum value) => new V60(value); -} - -// row 61 -public abstract record B61_1 { public override int GetHashCode() => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this); } -public sealed partial record V61 : B61_1, IValue; - -// row 62 -public static partial class O62 -{ -public abstract record B62_2 { public virtual bool Equals(B62_2? other) => ReferenceEquals(this, other); } -public abstract record B62_1 : B62_2 { } -public sealed partial record V62 : B62_1, IValue; -} - -// row 63 -public static partial class O63 -{ -public sealed partial class P63 -{ -public abstract record B63_1 : System.IEquatable { public virtual bool Equals(B63_1? other) => ReferenceEquals(this, other); bool System.IEquatable.Equals(B63_1? other) => ReferenceEquals(this, other); } -public sealed partial record V63 : B63_1, IValidatedValue -{ - public static Result Create(long value) => new V63(value); -} -} -} - -// row 64 -public static partial class O64 -{ -public sealed partial class P64 -{ -public abstract record B64_1 : System.IEquatable { bool System.IEquatable.Equals(B64_1? other) => ReferenceEquals(this, other); } -public sealed partial record V64 : B64_1, IValidatedValue -{ - public static Result Create(int value) => new V64(value); -} -} -} - -// row 65 -public static partial class O65 -{ -public abstract record B65_1 : System.IEquatable { bool System.IEquatable.Equals(B65_1? other) => ReferenceEquals(this, other); } -public sealed partial record V65 : B65_1, IValue; -} - -// row 66 -public static partial class O66 -{ -public sealed partial class P66 -{ -public abstract record B66_1 : System.IEquatable { bool System.IEquatable.Equals(B66_1? other) => ReferenceEquals(this, other); } -public sealed partial record V66 : B66_1, IValidatedValue -{ - public static Result Create(global::System.TimeSpan value) => new V66(value); -} -} -} - -// row 67 -public static partial class O67 -{ -public sealed partial class P67 -{ -public abstract record B67_1 { public virtual bool Equals(B67_1? other) => ReferenceEquals(this, other); } -public sealed partial record V67 : B67_1, IValue; -} -} - -// row 68 -public static partial class O68 -{ -public abstract record B68_1 { public virtual bool Equals(B68_1? other) => ReferenceEquals(this, other); } -public sealed partial record V68 : B68_1, IValidatedValue -{ - public static Result Create(ulong value) => new V68(value); -} -} - -// row 69 -public static partial class O69 -{ -public sealed partial class P69 -{ -public abstract record B69_2 { public virtual bool Equals(B69_2? other) => ReferenceEquals(this, other); } -public abstract record B69_1 : B69_2 { } -public sealed partial record V69 : B69_1, IValue; -} -} - -// row 70 -public abstract record B70_2 { public override int GetHashCode() => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this); } -public abstract record B70_1 : B70_2 { } -public sealed partial record V70 : B70_1, IValidatedValue -{ - public static Result Create(global::System.Uri value) => new V70(value); -} - -// row 71 -public interface IMark71 { int Mark(); } -public abstract record B71_1 : System.IEquatable, IMark71 { bool System.IEquatable.Equals(B71_1? other) => ReferenceEquals(this, other); int IMark71.Mark() => 1; } -public sealed partial record V71 : B71_1, IValidatedValue -{ - public static Result Create(global::Probes.Shapes.PFlags value) => new V71(value); -} - -// row 72 -public static partial class O72 -{ -public abstract record B72_1 { public override int GetHashCode() => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this); } -public sealed partial record V72 : B72_1, IValue; -} - -// row 73 -public static partial class O73 -{ -public sealed partial class P73 -{ -public abstract record B73_1 : System.IEquatable { bool System.IEquatable.Equals(B73_1? other) => ReferenceEquals(this, other); } -public sealed partial record V73 : B73_1, IValue; -} -} - -// row 74 -public static partial class O74 -{ -public sealed partial class P74 -{ -public abstract record B74_2 { public override int GetHashCode() => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this); } -public abstract record B74_1 : B74_2 { } -public sealed partial record V74 : B74_1, IValidatedValue -{ - public static Result Create(double value) => new V74(value); -} -} -} - -// row 75 -public static partial class O75 -{ -public abstract record B75_1 : System.IEquatable { bool System.IEquatable.Equals(B75_1? other) => ReferenceEquals(this, other); } -public sealed partial record V75 : B75_1, IValue; -} - -// row 76 -public static partial class O76 -{ -public interface IMark76 { int Mark(); } -public abstract record B76_1 : System.IEquatable, IMark76 { bool System.IEquatable.Equals(B76_1? other) => ReferenceEquals(this, other); int IMark76.Mark() => 1; } -public sealed partial record V76 : B76_1, IValidatedValue -{ - public static Result Create(global::System.DateTimeOffset value) => new V76(value); -} -} - -// row 77 -public static partial class O77 -{ -public sealed partial class P77 -{ -public abstract record B77_2 : System.IEquatable { bool System.IEquatable.Equals(B77_2? other) => ReferenceEquals(this, other); } -public abstract record B77_1 : B77_2 { } -public sealed partial record V77 : B77_1, IValidatedValue -{ - public static Result Create(char value) => new V77(value); -} -} -} - -// row 78 -public static partial class O78 -{ -public sealed partial class P78 -{ -public abstract record B78_1 { public override int GetHashCode() => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this); } -public sealed partial record V78 : B78_1, IValue; -} -} - -// row 79 -public static partial class O79 -{ -public abstract record B79_2 { public override int GetHashCode() => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this); } -public abstract record B79_1 : B79_2 { } -public sealed partial record V79 : B79_1, IValue; -} - -// row 80 -public static partial class O80 -{ -public sealed partial class P80 -{ -public abstract record B80_3 { public virtual bool Equals(B80_3? other) => ReferenceEquals(this, other); } -public abstract record B80_2 : B80_3 { } -public abstract record B80_1 : B80_2 { } -public sealed partial record V80 : B80_1, IValidatedValue -{ - public static Result Create(global::System.TimeOnly value) => new V80(value); -} -} -} - -// row 81 -public static partial class O81 -{ -public interface IMark81 { int Mark(); } -public abstract record B81_1 : System.IEquatable, IMark81 { bool System.IEquatable.Equals(B81_1? other) => ReferenceEquals(this, other); int IMark81.Mark() => 1; } -public sealed partial record V81 : B81_1, IValue; -} - -// row 82 -public static partial class O82 -{ -public interface IMark82 { int Mark(); } -public abstract record B82_1 : IMark82 { public override int GetHashCode() => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this); int IMark82.Mark() => 1; } -public sealed partial record V82 : B82_1, IValue; -} - -// row 83 -public abstract record B83_1 : System.IEquatable { bool System.IEquatable.Equals(B83_1? other) => ReferenceEquals(this, other); } -public sealed partial record V83 : B83_1, IValue; - -// row 84 -public abstract record B84_1 { public override int GetHashCode() => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this); } -public sealed partial record V84 : B84_1, IValidatedValue -{ - public static Result Create(char value) => new V84(value); -} - -// row 85 -public static partial class O85 -{ -public interface IMark85 { int Mark(); } -public abstract record B85_1 : System.IEquatable, IMark85 { bool System.IEquatable.Equals(B85_1? other) => ReferenceEquals(this, other); int IMark85.Mark() => 1; } -public sealed partial record V85 : B85_1, IValue; -} - -// row 86 -public partial struct O86 -{ -public abstract record B86_2 { public override int GetHashCode() => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this); } -public abstract record B86_1 : B86_2 { } -public sealed partial record V86 : B86_1, IValue; -} - -// row 87 -public partial struct O87 -{ -public abstract record B87_1 : System.IEquatable { bool System.IEquatable.Equals(B87_1? other) => ReferenceEquals(this, other); } -public sealed partial record V87 : B87_1, IValidatedValue -{ - public static Result Create(global::System.DateTimeOffset value) => new V87(value); -} -} - -// row 88 -public static partial class O88 -{ -public abstract record B88_2 : System.IEquatable { bool System.IEquatable.Equals(B88_2? other) => ReferenceEquals(this, other); } -public abstract record B88_1 : B88_2 { } -public sealed partial record V88 : B88_1, IValidatedValue -{ - public static Result Create(global::System.Guid value) => new V88(value); -} -} - -// row 89 -public static partial class O89 -{ -public abstract record B89_1 : System.IEquatable { bool System.IEquatable.Equals(B89_1? other) => ReferenceEquals(this, other); } -public sealed partial record V89 : B89_1, IValue; -} - -// row 90 -public abstract record B90_3 : System.IEquatable { bool System.IEquatable.Equals(B90_3? other) => ReferenceEquals(this, other); } -public abstract record B90_2 : B90_3 { } -public abstract record B90_1 : B90_2 { } -public sealed partial record V90 : B90_1, IValidatedValue -{ - public static Result Create(long value) => new V90(value); -} - -// row 91 -public static partial class O91 -{ -public sealed partial class P91 -{ -public abstract record B91_2 : System.IEquatable { bool System.IEquatable.Equals(B91_2? other) => ReferenceEquals(this, other); } -public abstract record B91_1 : B91_2 { } -public sealed partial record V91 : B91_1, IValue; -} -} - -// row 92 -public abstract record B92_3 : System.IEquatable { bool System.IEquatable.Equals(B92_3? other) => ReferenceEquals(this, other); } -public abstract record B92_2 : B92_3 { } -public abstract record B92_1 : B92_2 { } -public sealed partial record V92 : B92_1, IValidatedValue -{ - public static Result Create(bool value) => new V92(value); -} - -// row 93 -public abstract record B93_1 : System.IEquatable { bool System.IEquatable.Equals(B93_1? other) => ReferenceEquals(this, other); } -public sealed partial record V93 : B93_1, IValidatedValue -{ - public static Result Create(global::NodaTime.Instant value) => new V93(value); -} - -// row 94 -public static partial class O94 -{ -public sealed partial class P94 -{ -public interface IMark94 { int Mark(); } -public abstract record B94_1 : System.IEquatable, IMark94 { bool System.IEquatable.Equals(B94_1? other) => ReferenceEquals(this, other); int IMark94.Mark() => 1; } -public sealed partial record V94 : B94_1, IValue; -} -} - -// row 95 -public static partial class O95 -{ -public abstract record B95_1 : System.IEquatable { bool System.IEquatable.Equals(B95_1? other) => ReferenceEquals(this, other); } -public sealed partial record V95 : B95_1, IValidatedValue -{ - public static Result Create(global::Probes.Shapes.PFlags value) => new V95(value); -} -} - -// row 96 -public partial struct O96 -{ -public interface IMark96 { int Mark(); } -public abstract record B96_1 : IMark96 { public override int GetHashCode() => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this); int IMark96.Mark() => 1; } -public sealed partial record V96 : B96_1, IValue; -} - -// row 97 -public static partial class O97 -{ -public sealed partial class P97 -{ -public abstract record B97_3 { } -public abstract record B97_2 : B97_3 { } -public abstract record B97_1 : B97_2 { } -public sealed partial record V97 : B97_1, IValue; -} -} - -// row 98 -public static partial class O98 -{ -public interface IMark98 { int Mark(); } -public abstract record B98_1 : System.IEquatable, IMark98 { bool System.IEquatable.Equals(B98_1? other) => ReferenceEquals(this, other); int IMark98.Mark() => 1; } -public sealed partial record V98 : B98_1, IValue; -} - -// row 99 -public static partial class O99 -{ -public interface IMark99 { int Mark(); } -public abstract record B99_1 : IMark99 { public override int GetHashCode() => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this); int IMark99.Mark() => 1; } -public sealed partial record V99 : B99_1, IValidatedValue -{ - public static Result Create(double value) => new V99(value); -} -} - -// row 100 -public static partial class O100 -{ -public abstract record B100_3 : System.IEquatable { bool System.IEquatable.Equals(B100_3? other) => ReferenceEquals(this, other); } -public abstract record B100_2 : B100_3 { } -public abstract record B100_1 : B100_2 { } -public sealed partial record V100 : B100_1, IValidatedValue -{ - public static Result Create(global::System.Uri value) => new V100(value); -} -} - -public static partial class C09Table -{ - public static readonly C09Case[] Cases = - [ - new(0, "base-equality=explicit-IEquatable;collection=HashSet;shape.inheritance=base-explicit-IEquatable;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-two-levels;shape.wrapped=own-record-class", "validated-class", "nested-two-levels", "base-explicit-IEquatable", typeof(O0.P0.V0), [typeof(O0.P0.B0_1)], x => O0.P0.V0.FromKnownGood((global::Probes.Shapes.PRec)x), []), - new(1, "base-equality=override-Equals;collection=Distinct;shape.inheritance=base-depth-3;shape.kind=sealed-record-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-two-levels;shape.wrapped=DateTimeOffset", "sealed-record-class", "nested-two-levels", "base-depth-3", typeof(O1.P1.V1), [typeof(O1.P1.B1_1), typeof(O1.P1.B1_2), typeof(O1.P1.B1_3)], x => O1.P1.V1.From((global::System.DateTimeOffset)x), []), - new(2, "base-equality=override-GetHashCode;collection=Dictionary-key;shape.inheritance=base-depth-1;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-struct;shape.wrapped=long", "validated-class", "nested-in-struct", "base-depth-1", typeof(O2.V2), [typeof(O2.B2_1)], x => O2.V2.FromKnownGood((long)x), []), - new(3, "base-equality=none;collection=HashSet;shape.inheritance=base-depth-1;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=DateOnly", "sealed-record-class", "top-level", "base-depth-1", typeof(V3), [typeof(B3_1)], x => V3.From((global::System.DateOnly)x), []), - new(4, "base-equality=override-GetHashCode;collection=Distinct;shape.inheritance=explicit-interface-member;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=top-level;shape.wrapped=string", "validated-class", "top-level", "explicit-interface-member", typeof(V4), [typeof(B4_1)], x => V4.FromKnownGood((string)x), []), - new(5, "base-equality=explicit-IEquatable;collection=Dictionary-key;shape.inheritance=explicit-interface-member;shape.kind=sealed-record-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-class;shape.wrapped=Uri", "sealed-record-class", "nested-in-class", "explicit-interface-member", typeof(O5.V5), [typeof(O5.B5_1)], x => O5.V5.From((global::System.Uri)x), []), - new(6, "base-equality=none;collection=HashSet;shape.inheritance=base-depth-2;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-class;shape.wrapped=flags-enum", "validated-class", "nested-in-class", "base-depth-2", typeof(O6.V6), [typeof(O6.B6_1), typeof(O6.B6_2)], x => O6.V6.FromKnownGood((global::Probes.Shapes.PFlags)x), []), - new(7, "base-equality=override-GetHashCode;collection=HashSet;shape.inheritance=base-depth-2;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-struct;shape.wrapped=TimeSpan", "sealed-record-class", "nested-in-struct", "base-depth-2", typeof(O7.V7), [typeof(O7.B7_1), typeof(O7.B7_2)], x => O7.V7.From((global::System.TimeSpan)x), []), - new(8, "base-equality=none;collection=Distinct;shape.inheritance=base-explicit-IEquatable;shape.kind=sealed-record-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-struct;shape.wrapped=TimeOnly", "sealed-record-class", "nested-in-struct", "base-explicit-IEquatable", typeof(O8.V8), [typeof(O8.B8_1)], x => O8.V8.From((global::System.TimeOnly)x), []), - new(9, "base-equality=none;collection=Dictionary-key;shape.inheritance=explicit-interface-member;shape.kind=sealed-record-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-two-levels;shape.wrapped=byte", "sealed-record-class", "nested-two-levels", "explicit-interface-member", typeof(O9.P9.V9), [typeof(O9.P9.B9_1)], x => O9.P9.V9.From((byte)x), []), - new(10, "base-equality=override-Equals;collection=Dictionary-key;shape.inheritance=base-depth-2;shape.kind=validated-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=top-level;shape.wrapped=DateOnly", "validated-class", "top-level", "base-depth-2", typeof(V10), [typeof(B10_1), typeof(B10_2)], x => V10.FromKnownGood((global::System.DateOnly)x), []), - new(11, "base-equality=override-Equals;collection=Distinct;shape.inheritance=base-depth-1;shape.kind=validated-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-class;shape.wrapped=Guid", "validated-class", "nested-in-class", "base-depth-1", typeof(O11.V11), [typeof(O11.B11_1)], x => O11.V11.FromKnownGood((global::System.Guid)x), []), - new(12, "base-equality=explicit-IEquatable;collection=Distinct;shape.inheritance=base-depth-2;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-struct;shape.wrapped=ulong", "validated-class", "nested-in-struct", "base-depth-2", typeof(O12.V12), [typeof(O12.B12_1), typeof(O12.B12_2)], x => O12.V12.FromKnownGood((ulong)x), []), - new(13, "base-equality=override-Equals;collection=HashSet;shape.inheritance=base-depth-3;shape.kind=validated-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-struct;shape.wrapped=decimal", "validated-class", "nested-in-struct", "base-depth-3", typeof(O13.V13), [typeof(O13.B13_1), typeof(O13.B13_2), typeof(O13.B13_3)], x => O13.V13.FromKnownGood((decimal)x), []), - new(14, "base-equality=override-GetHashCode;collection=Dictionary-key;shape.inheritance=base-depth-3;shape.kind=closed-set;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-class;shape.wrapped=int", "sealed-record-class", "nested-in-class", "base-depth-3", typeof(O14.V14), [typeof(O14.B14_1), typeof(O14.B14_2), typeof(O14.B14_3)], x => O14.V14.From((int)x), [("shape.kind", "sealed-record-class")]), - new(15, "base-equality=override-Equals;collection=HashSet;shape.inheritance=explicit-interface-member;shape.kind=validated-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-struct;shape.wrapped=NodaTime.Instant", "validated-class", "nested-in-struct", "explicit-interface-member", typeof(O15.V15), [typeof(O15.B15_1)], x => O15.V15.FromKnownGood((global::NodaTime.Instant)x), []), - new(16, "base-equality=explicit-IEquatable;collection=Distinct;shape.inheritance=base-depth-3;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=double", "sealed-record-class", "top-level", "base-depth-3", typeof(V16), [typeof(B16_1), typeof(B16_2), typeof(B16_3)], x => V16.From((double)x), []), - new(17, "base-equality=override-Equals;collection=Dictionary-key;shape.inheritance=base-explicit-IEquatable;shape.kind=record-struct;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-class;shape.wrapped=TimeSpan", "sealed-record-class", "nested-in-class", "base-explicit-IEquatable", typeof(O17.V17), [typeof(O17.B17_1)], x => O17.V17.From((global::System.TimeSpan)x), [("shape.kind", "sealed-record-class")]), - new(18, "base-equality=override-GetHashCode;collection=Dictionary-key;shape.inheritance=base-depth-1;shape.kind=sealed-record-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-two-levels;shape.wrapped=flags-enum", "sealed-record-class", "nested-two-levels", "base-depth-1", typeof(O18.P18.V18), [typeof(O18.P18.B18_1)], x => O18.P18.V18.From((global::Probes.Shapes.PFlags)x), []), - new(19, "base-equality=override-GetHashCode;collection=Dictionary-key;shape.inheritance=base-explicit-IEquatable;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=top-level;shape.wrapped=char", "validated-class", "top-level", "base-explicit-IEquatable", typeof(V19), [typeof(B19_1)], x => V19.FromKnownGood((char)x), []), - new(20, "base-equality=none;collection=Dictionary-key;shape.inheritance=base-depth-3;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=top-level;shape.wrapped=DateTime", "validated-class", "top-level", "base-depth-3", typeof(V20), [typeof(B20_1), typeof(B20_2), typeof(B20_3)], x => V20.FromKnownGood((global::System.DateTime)x), []), - new(21, "base-equality=explicit-IEquatable;collection=HashSet;shape.inheritance=base-depth-2;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-class;shape.wrapped=DateTime", "sealed-record-class", "nested-in-class", "base-depth-2", typeof(O21.V21), [typeof(O21.B21_1), typeof(O21.B21_2)], x => O21.V21.From((global::System.DateTime)x), []), - new(22, "base-equality=none;collection=Dictionary-key;shape.inheritance=base-depth-2;shape.kind=sealed-record-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-two-levels;shape.wrapped=NodaTime.Instant", "sealed-record-class", "nested-two-levels", "base-depth-2", typeof(O22.P22.V22), [typeof(O22.P22.B22_1), typeof(O22.P22.B22_2)], x => O22.P22.V22.From((global::NodaTime.Instant)x), []), - new(23, "base-equality=explicit-IEquatable;collection=Dictionary-key;shape.inheritance=base-depth-1;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=top-level;shape.wrapped=TimeOnly", "validated-class", "top-level", "base-depth-1", typeof(V23), [typeof(B23_1)], x => V23.FromKnownGood((global::System.TimeOnly)x), []), - new(24, "base-equality=none;collection=Distinct;shape.inheritance=base-depth-1;shape.kind=validated-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-two-levels;shape.wrapped=Uri", "validated-class", "nested-two-levels", "base-depth-1", typeof(O24.P24.V24), [typeof(O24.P24.B24_1)], x => O24.P24.V24.FromKnownGood((global::System.Uri)x), []), - new(25, "base-equality=none;collection=Dictionary-key;shape.inheritance=base-depth-1;shape.kind=validated-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-class;shape.wrapped=bool", "validated-class", "nested-in-class", "base-depth-1", typeof(O25.V25), [typeof(O25.B25_1)], x => O25.V25.FromKnownGood((bool)x), []), - new(26, "base-equality=override-GetHashCode;collection=Dictionary-key;shape.inheritance=base-depth-3;shape.kind=validated-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-two-levels;shape.wrapped=enum", "validated-class", "nested-two-levels", "base-depth-3", typeof(O26.P26.V26), [typeof(O26.P26.B26_1), typeof(O26.P26.B26_2), typeof(O26.P26.B26_3)], x => O26.P26.V26.FromKnownGood((global::Probes.Shapes.PEnum)x), []), - new(27, "base-equality=none;collection=Distinct;shape.inheritance=base-depth-2;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=long", "sealed-record-class", "top-level", "base-depth-2", typeof(V27), [typeof(B27_1), typeof(B27_2)], x => V27.From((long)x), []), - new(28, "base-equality=none;collection=Distinct;shape.inheritance=base-depth-1;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-struct;shape.wrapped=own-record-class", "sealed-record-class", "nested-in-struct", "base-depth-1", typeof(O28.V28), [typeof(O28.B28_1)], x => O28.V28.From((global::Probes.Shapes.PRec)x), []), - new(29, "base-equality=explicit-IEquatable;collection=Distinct;shape.inheritance=explicit-interface-member;shape.kind=sealed-record-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-struct;shape.wrapped=bool", "sealed-record-class", "nested-in-struct", "explicit-interface-member", typeof(O29.V29), [typeof(O29.B29_1)], x => O29.V29.From((bool)x), []), - new(30, "base-equality=explicit-IEquatable;collection=Distinct;shape.inheritance=base-depth-2;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=int", "sealed-record-class", "top-level", "base-depth-2", typeof(V30), [typeof(B30_1), typeof(B30_2)], x => V30.From((int)x), []), - new(31, "base-equality=none;collection=Dictionary-key;shape.inheritance=base-depth-3;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-two-levels;shape.wrapped=ulong", "sealed-record-class", "nested-two-levels", "base-depth-3", typeof(O31.P31.V31), [typeof(O31.P31.B31_1), typeof(O31.P31.B31_2), typeof(O31.P31.B31_3)], x => O31.P31.V31.From((ulong)x), []), - new(32, "base-equality=none;collection=Dictionary-key;shape.inheritance=base-explicit-IEquatable;shape.kind=validated-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-struct;shape.wrapped=double", "validated-class", "nested-in-struct", "base-explicit-IEquatable", typeof(O32.V32), [typeof(O32.B32_1)], x => O32.V32.FromKnownGood((double)x), []), - new(33, "base-equality=override-Equals;collection=HashSet;shape.inheritance=base-explicit-IEquatable;shape.kind=sealed-record-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-two-levels;shape.wrapped=string", "sealed-record-class", "nested-two-levels", "base-explicit-IEquatable", typeof(O33.P33.V33), [typeof(O33.P33.B33_1)], x => O33.P33.V33.From((string)x), []), - new(34, "base-equality=override-GetHashCode;collection=HashSet;shape.inheritance=base-depth-1;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-struct;shape.wrapped=DateTimeOffset", "validated-class", "nested-in-struct", "base-depth-1", typeof(O34.V34), [typeof(O34.B34_1)], x => O34.V34.FromKnownGood((global::System.DateTimeOffset)x), []), - new(35, "base-equality=override-GetHashCode;collection=Dictionary-key;shape.inheritance=base-depth-3;shape.kind=sealed-record-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=top-level;shape.wrapped=Guid", "sealed-record-class", "top-level", "base-depth-3", typeof(V35), [typeof(B35_1), typeof(B35_2), typeof(B35_3)], x => V35.From((global::System.Guid)x), []), - new(36, "base-equality=none;collection=Distinct;shape.inheritance=base-depth-3;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-class;shape.wrapped=char", "sealed-record-class", "nested-in-class", "base-depth-3", typeof(O36.V36), [typeof(O36.B36_1), typeof(O36.B36_2), typeof(O36.B36_3)], x => O36.V36.From((char)x), []), - new(37, "base-equality=explicit-IEquatable;collection=Dictionary-key;shape.inheritance=base-explicit-IEquatable;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-class;shape.wrapped=decimal", "sealed-record-class", "nested-in-class", "base-explicit-IEquatable", typeof(O37.V37), [typeof(O37.B37_1)], x => O37.V37.From((decimal)x), []), - new(38, "base-equality=explicit-IEquatable;collection=Distinct;shape.inheritance=base-depth-2;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-struct;shape.wrapped=enum", "sealed-record-class", "nested-in-struct", "base-depth-2", typeof(O38.V38), [typeof(O38.B38_1), typeof(O38.B38_2)], x => O38.V38.From((global::Probes.Shapes.PEnum)x), []), - new(39, "base-equality=override-Equals;collection=HashSet;shape.inheritance=base-depth-2;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=top-level;shape.wrapped=byte", "validated-class", "top-level", "base-depth-2", typeof(V39), [typeof(B39_1), typeof(B39_2)], x => V39.FromKnownGood((byte)x), []), - new(40, "base-equality=override-Equals;collection=Distinct;shape.inheritance=base-depth-3;shape.kind=validated-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-struct;shape.wrapped=flags-enum", "validated-class", "nested-in-struct", "base-depth-3", typeof(O40.V40), [typeof(O40.B40_1), typeof(O40.B40_2), typeof(O40.B40_3)], x => O40.V40.FromKnownGood((global::Probes.Shapes.PFlags)x), []), - new(41, "base-equality=none;collection=Distinct;shape.inheritance=base-depth-3;shape.kind=validated-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=top-level;shape.wrapped=TimeSpan", "validated-class", "top-level", "base-depth-3", typeof(V41), [typeof(B41_1), typeof(B41_2), typeof(B41_3)], x => V41.FromKnownGood((global::System.TimeSpan)x), []), - new(42, "base-equality=override-Equals;collection=HashSet;shape.inheritance=explicit-interface-member;shape.kind=sealed-record-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-struct;shape.wrapped=char", "sealed-record-class", "nested-in-struct", "explicit-interface-member", typeof(O42.V42), [typeof(O42.B42_1)], x => O42.V42.From((char)x), []), - new(43, "base-equality=override-Equals;collection=HashSet;shape.inheritance=base-depth-1;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-struct;shape.wrapped=int", "sealed-record-class", "nested-in-struct", "base-depth-1", typeof(O43.V43), [typeof(O43.B43_1)], x => O43.V43.From((int)x), []), - new(44, "base-equality=override-Equals;collection=HashSet;shape.inheritance=base-explicit-IEquatable;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-struct;shape.wrapped=Uri", "sealed-record-class", "nested-in-struct", "base-explicit-IEquatable", typeof(O44.V44), [typeof(O44.B44_1)], x => O44.V44.From((global::System.Uri)x), []), - new(45, "base-equality=none;collection=HashSet;shape.inheritance=base-depth-1;shape.kind=validated-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-class;shape.wrapped=enum", "validated-class", "nested-in-class", "base-depth-1", typeof(O45.V45), [typeof(O45.B45_1)], x => O45.V45.FromKnownGood((global::Probes.Shapes.PEnum)x), []), - new(46, "base-equality=explicit-IEquatable;collection=HashSet;shape.inheritance=explicit-interface-member;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-class;shape.wrapped=long", "validated-class", "nested-in-class", "explicit-interface-member", typeof(O46.V46), [typeof(O46.B46_1)], x => O46.V46.FromKnownGood((long)x), []), - new(47, "base-equality=override-GetHashCode;collection=Distinct;shape.inheritance=base-depth-3;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=NodaTime.Instant", "sealed-record-class", "top-level", "base-depth-3", typeof(V47), [typeof(B47_1), typeof(B47_2), typeof(B47_3)], x => V47.From((global::NodaTime.Instant)x), []), - new(48, "base-equality=none;collection=Dictionary-key;shape.inheritance=base-depth-3;shape.kind=sealed-record-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-struct;shape.wrapped=string", "sealed-record-class", "nested-in-struct", "base-depth-3", typeof(O48.V48), [typeof(O48.B48_1), typeof(O48.B48_2), typeof(O48.B48_3)], x => O48.V48.From((string)x), []), - new(49, "base-equality=override-GetHashCode;collection=Dictionary-key;shape.inheritance=explicit-interface-member;shape.kind=record-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=top-level;shape.wrapped=own-record-class", "sealed-record-class", "top-level", "explicit-interface-member", typeof(V49), [typeof(B49_1)], x => V49.From((global::Probes.Shapes.PRec)x), [("shape.kind", "sealed-record-class")]), - new(50, "base-equality=override-GetHashCode;collection=Distinct;shape.inheritance=base-depth-3;shape.kind=sealed-record-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-two-levels;shape.wrapped=DateOnly", "sealed-record-class", "nested-two-levels", "base-depth-3", typeof(O50.P50.V50), [typeof(O50.P50.B50_1), typeof(O50.P50.B50_2), typeof(O50.P50.B50_3)], x => O50.P50.V50.From((global::System.DateOnly)x), []), - new(51, "base-equality=override-GetHashCode;collection=HashSet;shape.inheritance=base-explicit-IEquatable;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=top-level;shape.wrapped=bool", "validated-class", "top-level", "base-explicit-IEquatable", typeof(V51), [typeof(B51_1)], x => V51.FromKnownGood((bool)x), []), - new(52, "base-equality=none;collection=Dictionary-key;shape.inheritance=base-depth-2;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=top-level;shape.wrapped=DateTimeOffset", "sealed-record-class", "top-level", "base-depth-2", typeof(V52), [typeof(B52_1), typeof(B52_2)], x => V52.From((global::System.DateTimeOffset)x), []), - new(53, "base-equality=none;collection=HashSet;shape.inheritance=explicit-interface-member;shape.kind=sealed-record-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-struct;shape.wrapped=Guid", "sealed-record-class", "nested-in-struct", "explicit-interface-member", typeof(O53.V53), [typeof(O53.B53_1)], x => O53.V53.From((global::System.Guid)x), []), - new(54, "base-equality=override-GetHashCode;collection=Distinct;shape.inheritance=base-explicit-IEquatable;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-struct;shape.wrapped=DateTime", "validated-class", "nested-in-struct", "base-explicit-IEquatable", typeof(O54.V54), [typeof(O54.B54_1)], x => O54.V54.FromKnownGood((global::System.DateTime)x), []), - new(55, "base-equality=explicit-IEquatable;collection=Distinct;shape.inheritance=base-depth-3;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-struct;shape.wrapped=byte", "sealed-record-class", "nested-in-struct", "base-depth-3", typeof(O55.V55), [typeof(O55.B55_1), typeof(O55.B55_2), typeof(O55.B55_3)], x => O55.V55.From((byte)x), []), - new(56, "base-equality=none;collection=Distinct;shape.inheritance=explicit-interface-member;shape.kind=validated-struct;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=top-level;shape.wrapped=decimal", "validated-class", "top-level", "explicit-interface-member", typeof(V56), [typeof(B56_1)], x => V56.FromKnownGood((decimal)x), [("shape.kind", "validated-class")]), - new(57, "base-equality=override-Equals;collection=HashSet;shape.inheritance=base-depth-1;shape.kind=validated-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-class;shape.wrapped=double", "validated-class", "nested-in-class", "base-depth-1", typeof(O57.V57), [typeof(O57.B57_1)], x => O57.V57.FromKnownGood((double)x), []), - new(58, "base-equality=override-Equals;collection=HashSet;shape.inheritance=explicit-interface-member;shape.kind=validated-struct;shape.refused-kind=over-value-object (CMTK1005);shape.site=top-level;shape.wrapped=TimeOnly", "validated-class", "top-level", "explicit-interface-member", typeof(V58), [typeof(B58_1)], x => V58.FromKnownGood((global::System.TimeOnly)x), [("shape.kind", "validated-class")]), - new(59, "base-equality=explicit-IEquatable;collection=HashSet;shape.inheritance=base-explicit-IEquatable;shape.kind=closed-set;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-struct;shape.wrapped=DateOnly", "sealed-record-class", "nested-in-struct", "base-explicit-IEquatable", typeof(O59.V59), [typeof(O59.B59_1)], x => O59.V59.From((global::System.DateOnly)x), [("shape.kind", "sealed-record-class")]), - new(60, "base-equality=override-Equals;collection=Distinct;shape.inheritance=base-explicit-IEquatable;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=top-level;shape.wrapped=enum", "validated-class", "top-level", "base-explicit-IEquatable", typeof(V60), [typeof(B60_1)], x => V60.FromKnownGood((global::Probes.Shapes.PEnum)x), []), - new(61, "base-equality=override-GetHashCode;collection=HashSet;shape.inheritance=base-depth-1;shape.kind=sealed-record-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=top-level;shape.wrapped=ulong", "sealed-record-class", "top-level", "base-depth-1", typeof(V61), [typeof(B61_1)], x => V61.From((ulong)x), []), - new(62, "base-equality=override-Equals;collection=Distinct;shape.inheritance=base-depth-2;shape.kind=closed-set;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-class;shape.wrapped=own-record-class", "sealed-record-class", "nested-in-class", "base-depth-2", typeof(O62.V62), [typeof(O62.B62_1), typeof(O62.B62_2)], x => O62.V62.From((global::Probes.Shapes.PRec)x), [("shape.kind", "sealed-record-class")]), - new(63, "base-equality=override-Equals;collection=HashSet;shape.inheritance=base-explicit-IEquatable;shape.kind=validated-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-two-levels;shape.wrapped=long", "validated-class", "nested-two-levels", "base-explicit-IEquatable", typeof(O63.P63.V63), [typeof(O63.P63.B63_1)], x => O63.P63.V63.FromKnownGood((long)x), []), - new(64, "base-equality=none;collection=Distinct;shape.inheritance=base-explicit-IEquatable;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-two-levels;shape.wrapped=int", "validated-class", "nested-two-levels", "base-explicit-IEquatable", typeof(O64.P64.V64), [typeof(O64.P64.B64_1)], x => O64.P64.V64.FromKnownGood((int)x), []), - new(65, "base-equality=explicit-IEquatable;collection=HashSet;shape.inheritance=base-depth-1;shape.kind=sealed-record-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-class;shape.wrapped=string", "sealed-record-class", "nested-in-class", "base-depth-1", typeof(O65.V65), [typeof(O65.B65_1)], x => O65.V65.From((string)x), []), - new(66, "base-equality=explicit-IEquatable;collection=HashSet;shape.inheritance=base-depth-1;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-two-levels;shape.wrapped=TimeSpan", "validated-class", "nested-two-levels", "base-depth-1", typeof(O66.P66.V66), [typeof(O66.P66.B66_1)], x => O66.P66.V66.FromKnownGood((global::System.TimeSpan)x), []), - new(67, "base-equality=override-Equals;collection=Distinct;shape.inheritance=base-depth-1;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-two-levels;shape.wrapped=DateTime", "sealed-record-class", "nested-two-levels", "base-depth-1", typeof(O67.P67.V67), [typeof(O67.P67.B67_1)], x => O67.P67.V67.From((global::System.DateTime)x), []), - new(68, "base-equality=override-Equals;collection=Dictionary-key;shape.inheritance=base-depth-1;shape.kind=validated-struct;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-class;shape.wrapped=ulong", "validated-class", "nested-in-class", "base-depth-1", typeof(O68.V68), [typeof(O68.B68_1)], x => O68.V68.FromKnownGood((ulong)x), [("shape.kind", "validated-class")]), - new(69, "base-equality=override-Equals;collection=Dictionary-key;shape.inheritance=base-depth-2;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-two-levels;shape.wrapped=bool", "sealed-record-class", "nested-two-levels", "base-depth-2", typeof(O69.P69.V69), [typeof(O69.P69.B69_1), typeof(O69.P69.B69_2)], x => O69.P69.V69.From((bool)x), []), - new(70, "base-equality=override-GetHashCode;collection=Dictionary-key;shape.inheritance=base-depth-2;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=top-level;shape.wrapped=Uri", "validated-class", "top-level", "base-depth-2", typeof(V70), [typeof(B70_1), typeof(B70_2)], x => V70.FromKnownGood((global::System.Uri)x), []), - new(71, "base-equality=explicit-IEquatable;collection=HashSet;shape.inheritance=explicit-interface-member;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=top-level;shape.wrapped=flags-enum", "validated-class", "top-level", "explicit-interface-member", typeof(V71), [typeof(B71_1)], x => V71.FromKnownGood((global::Probes.Shapes.PFlags)x), []), - new(72, "base-equality=override-GetHashCode;collection=HashSet;shape.inheritance=base-depth-1;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-class;shape.wrapped=byte", "sealed-record-class", "nested-in-class", "base-depth-1", typeof(O72.V72), [typeof(O72.B72_1)], x => O72.V72.From((byte)x), []), - new(73, "base-equality=explicit-IEquatable;collection=Distinct;shape.inheritance=base-explicit-IEquatable;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-two-levels;shape.wrapped=Guid", "sealed-record-class", "nested-two-levels", "base-explicit-IEquatable", typeof(O73.P73.V73), [typeof(O73.P73.B73_1)], x => O73.P73.V73.From((global::System.Guid)x), []), - new(74, "base-equality=override-GetHashCode;collection=HashSet;shape.inheritance=base-depth-2;shape.kind=validated-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-two-levels;shape.wrapped=double", "validated-class", "nested-two-levels", "base-depth-2", typeof(O74.P74.V74), [typeof(O74.P74.B74_1), typeof(O74.P74.B74_2)], x => O74.P74.V74.FromKnownGood((double)x), []), - new(75, "base-equality=explicit-IEquatable;collection=Distinct;shape.inheritance=base-explicit-IEquatable;shape.kind=sealed-record-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-class;shape.wrapped=NodaTime.Instant", "sealed-record-class", "nested-in-class", "base-explicit-IEquatable", typeof(O75.V75), [typeof(O75.B75_1)], x => O75.V75.From((global::NodaTime.Instant)x), []), - new(76, "base-equality=explicit-IEquatable;collection=Dictionary-key;shape.inheritance=explicit-interface-member;shape.kind=validated-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-class;shape.wrapped=DateTimeOffset", "validated-class", "nested-in-class", "explicit-interface-member", typeof(O76.V76), [typeof(O76.B76_1)], x => O76.V76.FromKnownGood((global::System.DateTimeOffset)x), []), - new(77, "base-equality=explicit-IEquatable;collection=HashSet;shape.inheritance=base-depth-2;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-two-levels;shape.wrapped=char", "validated-class", "nested-two-levels", "base-depth-2", typeof(O77.P77.V77), [typeof(O77.P77.B77_1), typeof(O77.P77.B77_2)], x => O77.P77.V77.FromKnownGood((char)x), []), - new(78, "base-equality=override-GetHashCode;collection=HashSet;shape.inheritance=base-depth-1;shape.kind=record-struct;shape.refused-kind=abstract (CMTK1005);shape.site=nested-two-levels;shape.wrapped=decimal", "sealed-record-class", "nested-two-levels", "base-depth-1", typeof(O78.P78.V78), [typeof(O78.P78.B78_1)], x => O78.P78.V78.From((decimal)x), [("shape.kind", "sealed-record-class")]), - new(79, "base-equality=override-GetHashCode;collection=Dictionary-key;shape.inheritance=base-depth-2;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-class;shape.wrapped=TimeOnly", "sealed-record-class", "nested-in-class", "base-depth-2", typeof(O79.V79), [typeof(O79.B79_1), typeof(O79.B79_2)], x => O79.V79.From((global::System.TimeOnly)x), []), - new(80, "base-equality=override-Equals;collection=Distinct;shape.inheritance=base-depth-3;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-two-levels;shape.wrapped=TimeOnly", "validated-class", "nested-two-levels", "base-depth-3", typeof(O80.P80.V80), [typeof(O80.P80.B80_1), typeof(O80.P80.B80_2), typeof(O80.P80.B80_3)], x => O80.P80.V80.FromKnownGood((global::System.TimeOnly)x), []), - new(81, "base-equality=explicit-IEquatable;collection=Dictionary-key;shape.inheritance=explicit-interface-member;shape.kind=sealed-record-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-class;shape.wrapped=ulong", "sealed-record-class", "nested-in-class", "explicit-interface-member", typeof(O81.V81), [typeof(O81.B81_1)], x => O81.V81.From((ulong)x), []), - new(82, "base-equality=override-GetHashCode;collection=Dictionary-key;shape.inheritance=explicit-interface-member;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-class;shape.wrapped=DateOnly", "sealed-record-class", "nested-in-class", "explicit-interface-member", typeof(O82.V82), [typeof(O82.B82_1)], x => O82.V82.From((global::System.DateOnly)x), []), - new(83, "base-equality=explicit-IEquatable;collection=HashSet;shape.inheritance=base-explicit-IEquatable;shape.kind=sealed-record-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=top-level;shape.wrapped=ulong", "sealed-record-class", "top-level", "base-explicit-IEquatable", typeof(V83), [typeof(B83_1)], x => V83.From((ulong)x), []), - new(84, "base-equality=override-GetHashCode;collection=Distinct;shape.inheritance=base-depth-1;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=top-level;shape.wrapped=char", "validated-class", "top-level", "base-depth-1", typeof(V84), [typeof(B84_1)], x => V84.FromKnownGood((char)x), []), - new(85, "base-equality=explicit-IEquatable;collection=Dictionary-key;shape.inheritance=explicit-interface-member;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-class;shape.wrapped=TimeSpan", "sealed-record-class", "nested-in-class", "explicit-interface-member", typeof(O85.V85), [typeof(O85.B85_1)], x => O85.V85.From((global::System.TimeSpan)x), []), - new(86, "base-equality=override-GetHashCode;collection=Distinct;shape.inheritance=base-depth-2;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-struct;shape.wrapped=decimal", "sealed-record-class", "nested-in-struct", "base-depth-2", typeof(O86.V86), [typeof(O86.B86_1), typeof(O86.B86_2)], x => O86.V86.From((decimal)x), []), - new(87, "base-equality=none;collection=HashSet;shape.inheritance=base-explicit-IEquatable;shape.kind=validated-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-struct;shape.wrapped=DateTimeOffset", "validated-class", "nested-in-struct", "base-explicit-IEquatable", typeof(O87.V87), [typeof(O87.B87_1)], x => O87.V87.FromKnownGood((global::System.DateTimeOffset)x), []), - new(88, "base-equality=explicit-IEquatable;collection=Distinct;shape.inheritance=base-depth-2;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-class;shape.wrapped=Guid", "validated-class", "nested-in-class", "base-depth-2", typeof(O88.V88), [typeof(O88.B88_1), typeof(O88.B88_2)], x => O88.V88.FromKnownGood((global::System.Guid)x), []), - new(89, "base-equality=explicit-IEquatable;collection=Distinct;shape.inheritance=base-explicit-IEquatable;shape.kind=sealed-record-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=in-generic-type;shape.wrapped=byte", "sealed-record-class", "nested-in-class", "base-explicit-IEquatable", typeof(O89.V89), [typeof(O89.B89_1)], x => O89.V89.From((byte)x), [("shape.site", "nested-in-class")]), - new(90, "base-equality=explicit-IEquatable;collection=Distinct;shape.inheritance=base-depth-3;shape.kind=validated-class;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=long", "validated-class", "top-level", "base-depth-3", typeof(V90), [typeof(B90_1), typeof(B90_2), typeof(B90_3)], x => V90.FromKnownGood((long)x), []), - new(91, "base-equality=explicit-IEquatable;collection=HashSet;shape.inheritance=base-depth-2;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-two-levels;shape.wrapped=string", "sealed-record-class", "nested-two-levels", "base-depth-2", typeof(O91.P91.V91), [typeof(O91.P91.B91_1), typeof(O91.P91.B91_2)], x => O91.P91.V91.From((string)x), []), - new(92, "base-equality=explicit-IEquatable;collection=HashSet;shape.inheritance=base-depth-3;shape.kind=validated-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=top-level;shape.wrapped=bool", "validated-class", "top-level", "base-depth-3", typeof(V92), [typeof(B92_1), typeof(B92_2), typeof(B92_3)], x => V92.FromKnownGood((bool)x), [("shape.kind", "validated-class")]), - new(93, "base-equality=explicit-IEquatable;collection=Dictionary-key;shape.inheritance=base-depth-1;shape.kind=validated-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=top-level;shape.wrapped=NodaTime.Instant", "validated-class", "top-level", "base-depth-1", typeof(V93), [typeof(B93_1)], x => V93.FromKnownGood((global::NodaTime.Instant)x), []), - new(94, "base-equality=explicit-IEquatable;collection=Distinct;shape.inheritance=explicit-interface-member;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-two-levels;shape.wrapped=int", "sealed-record-class", "nested-two-levels", "explicit-interface-member", typeof(O94.P94.V94), [typeof(O94.P94.B94_1)], x => O94.P94.V94.From((int)x), []), - new(95, "base-equality=none;collection=Distinct;shape.inheritance=base-explicit-IEquatable;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-class;shape.wrapped=flags-enum", "validated-class", "nested-in-class", "base-explicit-IEquatable", typeof(O95.V95), [typeof(O95.B95_1)], x => O95.V95.FromKnownGood((global::Probes.Shapes.PFlags)x), []), - new(96, "base-equality=override-GetHashCode;collection=Dictionary-key;shape.inheritance=explicit-interface-member;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-struct;shape.wrapped=enum", "sealed-record-class", "nested-in-struct", "explicit-interface-member", typeof(O96.V96), [typeof(O96.B96_1)], x => O96.V96.From((global::Probes.Shapes.PEnum)x), []), - new(97, "base-equality=none;collection=Dictionary-key;shape.inheritance=base-depth-3;shape.kind=sealed-record-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-two-levels;shape.wrapped=own-record-class", "sealed-record-class", "nested-two-levels", "base-depth-3", typeof(O97.P97.V97), [typeof(O97.P97.B97_1), typeof(O97.P97.B97_2), typeof(O97.P97.B97_3)], x => O97.P97.V97.From((global::Probes.Shapes.PRec)x), []), - new(98, "base-equality=explicit-IEquatable;collection=HashSet;shape.inheritance=explicit-interface-member;shape.kind=sealed-record-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-class;shape.wrapped=DateTime", "sealed-record-class", "nested-in-class", "explicit-interface-member", typeof(O98.V98), [typeof(O98.B98_1)], x => O98.V98.From((global::System.DateTime)x), []), - new(99, "base-equality=override-GetHashCode;collection=Dictionary-key;shape.inheritance=explicit-interface-member;shape.kind=validated-class;shape.refused-kind=abstract (CMTK1005);shape.site=in-generic-type;shape.wrapped=double", "validated-class", "nested-in-class", "explicit-interface-member", typeof(O99.V99), [typeof(O99.B99_1)], x => O99.V99.FromKnownGood((double)x), [("shape.site", "nested-in-class")]), - new(100, "base-equality=explicit-IEquatable;collection=Dictionary-key;shape.inheritance=base-depth-3;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=in-generic-type;shape.wrapped=Uri", "validated-class", "nested-in-class", "base-depth-3", typeof(O100.V100), [typeof(O100.B100_1), typeof(O100.B100_2), typeof(O100.B100_3)], x => O100.V100.FromKnownGood((global::System.Uri)x), [("shape.site", "nested-in-class")]), - ]; -} diff --git a/tools/probe/corpus/harness/C11Shapes.g.cs b/tools/probe/corpus/harness/C11Shapes.g.cs deleted file mode 100644 index 992834fa..00000000 --- a/tools/probe/corpus/harness/C11Shapes.g.cs +++ /dev/null @@ -1,2596 +0,0 @@ -// by tools/probe/generators/decls.cs from rows/C11ParseShapesBuildProbe.json.gz: one value object per row. Do not edit; regenerate with the rows. -#nullable enable -// CMTK1023: the bases' own equality is the point (C09) or beside it (C11); CS8851: a base's Equals without GetHashCode, on purpose. -#pragma warning disable CMTK1023, CS8851, CS1591 -using CodoMetis.TypeKit; -using CodoMetis.TypeKit.ValueObjects; -namespace Probes.Build.C11; - -// row 0 -public static partial class O0 -{ -public interface IMark0 { int Mark(); } -public sealed partial record V0 : IValue, IMark0 { int IMark0.Mark() => 1; } -} - -// row 1 -public sealed partial record V1 : IValidatedValue -{ - public static Result Create(global::System.TimeSpan value) => new V1(value); -} - -// row 2 -public partial struct O2 -{ -public abstract record B2_2 { } -public abstract record B2_1 : B2_2 { } -public sealed partial record V2 : B2_1, IValue { } -public abstract record BC2_2 { } -public abstract record BC2_1 : BC2_2 { } -public sealed partial record VC2 : BC2_1, IValue { } -} - -// row 3 -public static partial class O3 -{ -public sealed partial class P3 -{ -public readonly partial record struct V3 : IValidatedValue -{ - public static Result Create(global::Probes.Build.C11AbstractText value) => new V3(value); -} -public readonly partial record struct VC3 : IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ConstructedConcrete value) => new VC3(value); -} -} -} - -// row 4 -public interface IMark4 { int Mark(); } -public readonly partial record struct V4 : IValue, IMark4 { int IMark4.Mark() => 1; } -public readonly partial record struct VC4 : IValue, IMark4 { int IMark4.Mark() => 1; } - -// row 5 -public partial struct O5 -{ -public abstract record B5_1 { } -public sealed partial record V5 : B5_1, IValidatedValue -{ - public static Result Create(global::Probes.Build.C11AbstractText value) => new V5(value); -} -public abstract record BC5_1 { } -public sealed partial record VC5 : BC5_1, IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ConstructedConcrete value) => new VC5(value); -} -} - -// row 6 -public static partial class O6 -{ -public readonly partial record struct V6 : IValue { } -public readonly partial record struct VC6 : IValue { } -} - -// row 7 -public static partial class O7 -{ -public sealed partial class P7 -{ -public abstract record B7_1 { } -public sealed partial record V7 : B7_1, IValue { } -public abstract record BC7_1 { } -public sealed partial record VC7 : BC7_1, IValue { } -} -} - -// row 8 -public static partial class O8 -{ -public sealed partial class P8 -{ -public abstract record B8_1 : System.IEquatable { bool System.IEquatable.Equals(B8_1? other) => ReferenceEquals(this, other); } -public sealed partial record V8 : B8_1, IValue { } -} -} - -// row 9 -public partial struct O9 -{ -public abstract record B9_3 { } -public abstract record B9_2 : B9_3 { } -public abstract record B9_1 : B9_2 { } -public sealed partial record V9 : B9_1, IValidatedValue -{ - public static Result Create(global::NodaTime.Instant value) => new V9(value); -} -} - -// row 10 -public abstract record B10_1 : System.IEquatable { bool System.IEquatable.Equals(B10_1? other) => ReferenceEquals(this, other); } -public sealed partial record V10 : B10_1, IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsesToInt value) => new V10(value); -} -public abstract record BC10_1 : System.IEquatable { bool System.IEquatable.Equals(BC10_1? other) => ReferenceEquals(this, other); } -public sealed partial record VC10 : BC10_1, IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsedShape value) => new VC10(value); -} - -// row 11 -public static partial class O11 -{ -public interface IMark11 { int Mark(); } -public readonly partial record struct V11 : IValidatedValue, IMark11 -{ - public static Result Create(global::Probes.Build.C11ConstructedByRef value) => new V11(value); int IMark11.Mark() => 1; -} -public readonly partial record struct VC11 : IValidatedValue, IMark11 -{ - public static Result Create(global::Probes.Build.C11ParsesFromIn value) => new VC11(value); int IMark11.Mark() => 1; -} -} - -// row 12 -public abstract record B12_2 { } -public abstract record B12_1 : B12_2 { } -public sealed partial record V12 : B12_1, IValue { } - -// row 13 -public static partial class O13 -{ -public abstract record B13_3 { } -public abstract record B13_2 : B13_3 { } -public abstract record B13_1 : B13_2 { } -public sealed partial record V13 : B13_1, IValidatedValue -{ - public static Result Create(global::Probes.Build.C11AbstractText value) => new V13(value); -} -public abstract record BC13_3 { } -public abstract record BC13_2 : BC13_3 { } -public abstract record BC13_1 : BC13_2 { } -public sealed partial record VC13 : BC13_1, IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ConstructedConcrete value) => new VC13(value); -} -} - -// row 14 -public partial struct O14 -{ -public readonly partial record struct V14 : IValidatedValue -{ - public static Result Create(global::System.DateOnly value) => new V14(value); -} -} - -// row 15 -public partial struct O15 -{ -public readonly partial record struct V15 : IValue { } -public readonly partial record struct VC15 : IValue { } -} - -// row 16 -public static partial class O16 -{ -public sealed partial class P16 -{ -public interface IMark16 { int Mark(); } -public readonly partial record struct V16 : IValue, IMark16 { int IMark16.Mark() => 1; } -public readonly partial record struct VC16 : IValue, IMark16 { int IMark16.Mark() => 1; } -} -} - -// row 17 -public readonly partial record struct V17 : IValue { } - -// row 18 -public static partial class O18 -{ -public sealed partial class P18 -{ -public abstract record B18_2 { } -public abstract record B18_1 : B18_2 { } -public sealed partial record V18 : B18_1, IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsesToInt value) => new V18(value); -} -public abstract record BC18_2 { } -public abstract record BC18_1 : BC18_2 { } -public sealed partial record VC18 : BC18_1, IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsedShape value) => new VC18(value); -} -} -} - -// row 19 -public static partial class O19 -{ -public abstract record B19_1 : System.IEquatable { bool System.IEquatable.Equals(B19_1? other) => ReferenceEquals(this, other); } -public sealed partial record V19 : B19_1, IValue { } -public abstract record BC19_1 : System.IEquatable { bool System.IEquatable.Equals(BC19_1? other) => ReferenceEquals(this, other); } -public sealed partial record VC19 : BC19_1, IValue { } -} - -// row 20 -public partial struct O20 -{ -public interface IMark20 { int Mark(); } -public readonly partial record struct V20 : IValidatedValue, IMark20 -{ - public static Result Create(global::Probes.Build.C11ParsesByRef value) => new V20(value); int IMark20.Mark() => 1; -} -public readonly partial record struct VC20 : IValidatedValue, IMark20 -{ - public static Result Create(global::Probes.Build.C11ParsesFromIn value) => new VC20(value); int IMark20.Mark() => 1; -} -} - -// row 21 -public static partial class O21 -{ -public readonly partial record struct V21 : IValidatedValue -{ - public static Result Create(global::System.DateTimeOffset value) => new V21(value); -} -} - -// row 22 -public static partial class O22 -{ -public sealed partial class P22 -{ -public interface IMark22 { int Mark(); } -public readonly partial record struct V22 : IValue, IMark22 { int IMark22.Mark() => 1; } -public readonly partial record struct VC22 : IValue, IMark22 { int IMark22.Mark() => 1; } -} -} - -// row 23 -public static partial class O23 -{ -public sealed partial class P23 -{ -public abstract record B23_3 { } -public abstract record B23_2 : B23_3 { } -public abstract record B23_1 : B23_2 { } -public sealed partial record V23 : B23_1, IValue { } -} -} - -// row 24 -public static partial class O24 -{ -public readonly partial record struct V24 : IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsesToInt value) => new V24(value); -} -public readonly partial record struct VC24 : IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsedShape value) => new VC24(value); -} -} - -// row 25 -public interface IMark25 { int Mark(); } -public readonly partial record struct V25 : IValidatedValue, IMark25 -{ - public static Result Create(global::Probes.Build.C11AbstractText value) => new V25(value); int IMark25.Mark() => 1; -} -public readonly partial record struct VC25 : IValidatedValue, IMark25 -{ - public static Result Create(global::Probes.Build.C11ConstructedConcrete value) => new VC25(value); int IMark25.Mark() => 1; -} - -// row 26 -public partial struct O26 -{ -public abstract record B26_1 : System.IEquatable { bool System.IEquatable.Equals(B26_1? other) => ReferenceEquals(this, other); } -public sealed partial record V26 : B26_1, IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsesByRef value) => new V26(value); -} -public abstract record BC26_1 : System.IEquatable { bool System.IEquatable.Equals(BC26_1? other) => ReferenceEquals(this, other); } -public sealed partial record VC26 : BC26_1, IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsesFromIn value) => new VC26(value); -} -} - -// row 27 -public static partial class O27 -{ -public readonly partial record struct V27 : IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsesToInt value) => new V27(value); -} -public readonly partial record struct VC27 : IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsedShape value) => new VC27(value); -} -} - -// row 28 -public readonly partial record struct V28 : IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsesByRef value) => new V28(value); -} -public readonly partial record struct VC28 : IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsesFromIn value) => new VC28(value); -} - -// row 29 -public partial struct O29 -{ -public interface IMark29 { int Mark(); } -public sealed partial record V29 : IValidatedValue, IMark29 -{ - public static Result Create(global::Probes.Build.C11ConstructedByRef value) => new V29(value); int IMark29.Mark() => 1; -} -public sealed partial record VC29 : IValidatedValue, IMark29 -{ - public static Result Create(global::Probes.Build.C11ParsesFromIn value) => new VC29(value); int IMark29.Mark() => 1; -} -} - -// row 30 -public abstract record B30_2 { } -public abstract record B30_1 : B30_2 { } -public sealed partial record V30 : B30_1, IValue { } -public abstract record BC30_2 { } -public abstract record BC30_1 : BC30_2 { } -public sealed partial record VC30 : BC30_1, IValue { } - -// row 31 -public static partial class O31 -{ -public readonly partial record struct V31 : IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsesToInt value) => new V31(value); -} -public readonly partial record struct VC31 : IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsedShape value) => new VC31(value); -} -} - -// row 32 -public partial struct O32 -{ -public sealed partial record V32 : IValue { } -public sealed partial record VC32 : IValue { } -} - -// row 33 -public static partial class O33 -{ -public abstract record B33_1 { } -public sealed partial record V33 : B33_1, IValidatedValue -{ - public static Result Create(long value) => new V33(value); -} -} - -// row 34 -public abstract record B34_3 { } -public abstract record B34_2 : B34_3 { } -public abstract record B34_1 : B34_2 { } -public sealed partial record V34 : B34_1, IValue { } -public abstract record BC34_3 { } -public abstract record BC34_2 : BC34_3 { } -public abstract record BC34_1 : BC34_2 { } -public sealed partial record VC34 : BC34_1, IValue { } - -// row 35 -public abstract record B35_2 { } -public abstract record B35_1 : B35_2 { } -public sealed partial record V35 : B35_1, IValue { } - -// row 36 -public partial struct O36 -{ -public abstract record B36_2 { } -public abstract record B36_1 : B36_2 { } -public sealed partial record V36 : B36_1, IValue { } -public abstract record BC36_2 { } -public abstract record BC36_1 : BC36_2 { } -public sealed partial record VC36 : BC36_1, IValue { } -} - -// row 37 -public static partial class O37 -{ -public sealed partial class P37 -{ -public sealed partial record V37 : IValue { } -} -} - -// row 38 -public partial struct O38 -{ -public abstract record B38_2 { } -public abstract record B38_1 : B38_2 { } -public sealed partial record V38 : B38_1, IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsesByRef value) => new V38(value); -} -public abstract record BC38_2 { } -public abstract record BC38_1 : BC38_2 { } -public sealed partial record VC38 : BC38_1, IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsesFromIn value) => new VC38(value); -} -} - -// row 39 -public partial struct O39 -{ -public abstract record B39_3 { } -public abstract record B39_2 : B39_3 { } -public abstract record B39_1 : B39_2 { } -public sealed partial record V39 : B39_1, IValue { } -} - -// row 40 -public partial struct O40 -{ -public readonly partial record struct V40 : IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsesToInt value) => new V40(value); -} -public readonly partial record struct VC40 : IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsedShape value) => new VC40(value); -} -} - -// row 41 -public partial struct O41 -{ -public abstract record B41_1 : System.IEquatable { bool System.IEquatable.Equals(B41_1? other) => ReferenceEquals(this, other); } -public sealed partial record V41 : B41_1, IValue { } -} - -// row 42 -public static partial class O42 -{ -public sealed partial class P42 -{ -public abstract record B42_1 : System.IEquatable { bool System.IEquatable.Equals(B42_1? other) => ReferenceEquals(this, other); } -public sealed partial record V42 : B42_1, IValue { } -} -} - -// row 43 -public partial struct O43 -{ -public readonly partial record struct V43 : IValue { } -public readonly partial record struct VC43 : IValue { } -} - -// row 44 -public static partial class O44 -{ -public sealed partial class P44 -{ -public readonly partial record struct V44 : IValue { } -public readonly partial record struct VC44 : IValue { } -} -} - -// row 45 -public readonly partial record struct V45 : IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ConstructedByRef value) => new V45(value); -} -public readonly partial record struct VC45 : IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsesFromIn value) => new VC45(value); -} - -// row 46 -public static partial class O46 -{ -public readonly partial record struct V46 : IValidatedValue -{ - public static Result Create(global::Probes.Shapes.PFlags value) => new V46(value); -} -} - -// row 47 -public static partial class O47 -{ -public readonly partial record struct V47 : IValidatedValue -{ - public static Result Create(int value) => new V47(value); -} -} - -// row 48 -public static partial class O48 -{ -public readonly partial record struct V48 : IValue { } -public readonly partial record struct VC48 : IValue { } -} - -// row 49 -public static partial class O49 -{ -public abstract record B49_1 : System.IEquatable { bool System.IEquatable.Equals(B49_1? other) => ReferenceEquals(this, other); } -public sealed partial record V49 : B49_1, IValidatedValue -{ - public static Result Create(bool value) => new V49(value); -} -} - -// row 50 -public abstract record B50_1 : System.IEquatable { bool System.IEquatable.Equals(B50_1? other) => ReferenceEquals(this, other); } -public sealed partial record V50 : B50_1, IValue { } -public abstract record BC50_1 : System.IEquatable { bool System.IEquatable.Equals(BC50_1? other) => ReferenceEquals(this, other); } -public sealed partial record VC50 : BC50_1, IValue { } - -// row 51 -public static partial class O51 -{ -public abstract record B51_3 { } -public abstract record B51_2 : B51_3 { } -public abstract record B51_1 : B51_2 { } -public sealed partial record V51 : B51_1, IValidatedValue -{ - public static Result Create(global::System.DateOnly value) => new V51(value); -} -} - -// row 52 -public partial struct O52 -{ -public sealed partial record V52 : IValue { } -} - -// row 53 -public static partial class O53 -{ -public sealed partial class P53 -{ -public abstract record B53_3 { } -public abstract record B53_2 : B53_3 { } -public abstract record B53_1 : B53_2 { } -public sealed partial record V53 : B53_1, IValidatedValue -{ - public static Result Create(string value) => new V53(value); -} -} -} - -// row 54 -public static partial class O54 -{ -public sealed partial class P54 -{ -public abstract record B54_3 { } -public abstract record B54_2 : B54_3 { } -public abstract record B54_1 : B54_2 { } -public sealed partial record V54 : B54_1, IValidatedValue -{ - public static Result Create(global::Probes.Build.C11AbstractText value) => new V54(value); -} -public abstract record BC54_3 { } -public abstract record BC54_2 : BC54_3 { } -public abstract record BC54_1 : BC54_2 { } -public sealed partial record VC54 : BC54_1, IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ConstructedConcrete value) => new VC54(value); -} -} -} - -// row 55 -public static partial class O55 -{ -public sealed partial class P55 -{ -public abstract record B55_1 { } -public sealed partial record V55 : B55_1, IValue { } -} -} - -// row 56 -public static partial class O56 -{ -public sealed partial class P56 -{ -public abstract record B56_1 { } -public sealed partial record V56 : B56_1, IValidatedValue -{ - public static Result Create(decimal value) => new V56(value); -} -} -} - -// row 57 -public abstract record B57_1 { } -public sealed partial record V57 : B57_1, IValidatedValue -{ - public static Result Create(global::Probes.Shapes.PEnum value) => new V57(value); -} - -// row 58 -public readonly partial record struct V58 : IValue { } - -// row 59 -public static partial class O59 -{ -public sealed partial class P59 -{ -public abstract record B59_3 { } -public abstract record B59_2 : B59_3 { } -public abstract record B59_1 : B59_2 { } -public sealed partial record V59 : B59_1, IValidatedValue -{ - public static Result Create(double value) => new V59(value); -} -} -} - -// row 60 -public static partial class O60 -{ -public interface IMark60 { int Mark(); } -public sealed partial record V60 : IValue, IMark60 { int IMark60.Mark() => 1; } -} - -// row 61 -public partial struct O61 -{ -public abstract record B61_1 { } -public sealed partial record V61 : B61_1, IValue { } -public abstract record BC61_1 { } -public sealed partial record VC61 : BC61_1, IValue { } -} - -// row 62 -public static partial class O62 -{ -public sealed partial class P62 -{ -public readonly partial record struct V62 : IValidatedValue -{ - public static Result Create(global::System.DateTime value) => new V62(value); -} -} -} - -// row 63 -public interface IMark63 { int Mark(); } -public readonly partial record struct V63 : IValue, IMark63 { int IMark63.Mark() => 1; } - -// row 64 -public static partial class O64 -{ -public readonly partial record struct V64 : IValidatedValue -{ - public static Result Create(global::Probes.Shapes.PEnum value) => new V64(value); -} -} - -// row 65 -public abstract record B65_2 { } -public abstract record B65_1 : B65_2 { } -public sealed partial record V65 : B65_1, IValidatedValue -{ - public static Result Create(global::System.Guid value) => new V65(value); -} - -// row 66 -public abstract record B66_1 : System.IEquatable { bool System.IEquatable.Equals(B66_1? other) => ReferenceEquals(this, other); } -public sealed partial record V66 : B66_1, IValidatedValue -{ - public static Result Create(global::Probes.Shapes.PRec value) => new V66(value); -} - -// row 67 -public static partial class O67 -{ -public sealed partial class P67 -{ -public readonly partial record struct V67 : IValidatedValue -{ - public static Result Create(global::Probes.Build.C11AbstractText value) => new V67(value); -} -public readonly partial record struct VC67 : IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ConstructedConcrete value) => new VC67(value); -} -} -} - -// row 68 -public static partial class O68 -{ -public sealed partial class P68 -{ -public readonly partial record struct V68 : IValue { } -} -} - -// row 69 -public partial struct O69 -{ -public interface IMark69 { int Mark(); } -public readonly partial record struct V69 : IValidatedValue, IMark69 -{ - public static Result Create(long value) => new V69(value); int IMark69.Mark() => 1; -} -} - -// row 70 -public static partial class O70 -{ -public sealed partial class P70 -{ -public interface IMark70 { int Mark(); } -public readonly partial record struct V70 : IValue, IMark70 { int IMark70.Mark() => 1; } -} -} - -// row 71 -public static partial class O71 -{ -public sealed partial class P71 -{ -public readonly partial record struct V71 : IValue { } -} -} - -// row 72 -public partial struct O72 -{ -public readonly partial record struct V72 : IValue { } -} - -// row 73 -public static partial class O73 -{ -public interface IMark73 { int Mark(); } -public readonly partial record struct V73 : IValidatedValue, IMark73 -{ - public static Result Create(global::Probes.Build.C11ConstructedByRef value) => new V73(value); int IMark73.Mark() => 1; -} -public readonly partial record struct VC73 : IValidatedValue, IMark73 -{ - public static Result Create(global::Probes.Build.C11ParsesFromIn value) => new VC73(value); int IMark73.Mark() => 1; -} -} - -// row 74 -public static partial class O74 -{ -public readonly partial record struct V74 : IValue { } -} - -// row 75 -public partial struct O75 -{ -public abstract record B75_1 { } -public sealed partial record V75 : B75_1, IValidatedValue -{ - public static Result Create(int value) => new V75(value); -} -} - -// row 76 -public static partial class O76 -{ -public interface IMark76 { int Mark(); } -public sealed partial record V76 : IValue, IMark76 { int IMark76.Mark() => 1; } -} - -// row 77 -public readonly partial record struct V77 : IValue { } -public readonly partial record struct VC77 : IValue { } - -// row 78 -public static partial class O78 -{ -public sealed partial class P78 -{ -public readonly partial record struct V78 : IValue { } -public readonly partial record struct VC78 : IValue { } -} -} - -// row 79 -public abstract record B79_1 : System.IEquatable { bool System.IEquatable.Equals(B79_1? other) => ReferenceEquals(this, other); } -public sealed partial record V79 : B79_1, IValue { } -public abstract record BC79_1 : System.IEquatable { bool System.IEquatable.Equals(BC79_1? other) => ReferenceEquals(this, other); } -public sealed partial record VC79 : BC79_1, IValue { } - -// row 80 -public static partial class O80 -{ -public readonly partial record struct V80 : IValidatedValue -{ - public static Result Create(global::NodaTime.Instant value) => new V80(value); -} -} - -// row 81 -public static partial class O81 -{ -public abstract record B81_1 : System.IEquatable { bool System.IEquatable.Equals(B81_1? other) => ReferenceEquals(this, other); } -public sealed partial record V81 : B81_1, IValue { } -public abstract record BC81_1 : System.IEquatable { bool System.IEquatable.Equals(BC81_1? other) => ReferenceEquals(this, other); } -public sealed partial record VC81 : BC81_1, IValue { } -} - -// row 82 -public static partial class O82 -{ -public sealed partial class P82 -{ -public interface IMark82 { int Mark(); } -public readonly partial record struct V82 : IValue, IMark82 { int IMark82.Mark() => 1; } -} -} - -// row 83 -public abstract record B83_2 { } -public abstract record B83_1 : B83_2 { } -public sealed partial record V83 : B83_1, IValidatedValue -{ - public static Result Create(string value) => new V83(value); -} - -// row 84 -public static partial class O84 -{ -public abstract record B84_2 { } -public abstract record B84_1 : B84_2 { } -public sealed partial record V84 : B84_1, IValidatedValue -{ - public static Result Create(long value) => new V84(value); -} -} - -// row 85 -public partial struct O85 -{ -public abstract record B85_1 : System.IEquatable { bool System.IEquatable.Equals(B85_1? other) => ReferenceEquals(this, other); } -public sealed partial record V85 : B85_1, IValue { } -} - -// row 86 -public static partial class O86 -{ -public sealed partial class P86 -{ -public abstract record B86_2 { } -public abstract record B86_1 : B86_2 { } -public sealed partial record V86 : B86_1, IValue { } -public abstract record BC86_2 { } -public abstract record BC86_1 : BC86_2 { } -public sealed partial record VC86 : BC86_1, IValue { } -} -} - -// row 87 -public static partial class O87 -{ -public sealed partial class P87 -{ -public sealed partial record V87 : IValue { } -} -} - -// row 88 -public sealed partial record V88 : IValue { } -public sealed partial record VC88 : IValue { } - -// row 89 -public abstract record B89_2 { } -public abstract record B89_1 : B89_2 { } -public sealed partial record V89 : B89_1, IValidatedValue -{ - public static Result Create(ulong value) => new V89(value); -} - -// row 90 -public readonly partial record struct V90 : IValue { } - -// row 91 -public static partial class O91 -{ -public sealed partial class P91 -{ -public abstract record B91_1 { } -public sealed partial record V91 : B91_1, IValue { } -public abstract record BC91_1 { } -public sealed partial record VC91 : BC91_1, IValue { } -} -} - -// row 92 -public static partial class O92 -{ -public abstract record B92_2 { } -public abstract record B92_1 : B92_2 { } -public sealed partial record V92 : B92_1, IValidatedValue -{ - public static Result Create(global::System.TimeSpan value) => new V92(value); -} -} - -// row 93 -public abstract record B93_3 { } -public abstract record B93_2 : B93_3 { } -public abstract record B93_1 : B93_2 { } -public sealed partial record V93 : B93_1, IValue { } - -// row 94 -public partial struct O94 -{ -public abstract record B94_2 { } -public abstract record B94_1 : B94_2 { } -public sealed partial record V94 : B94_1, IValue { } -} - -// row 95 -public abstract record B95_3 { } -public abstract record B95_2 : B95_3 { } -public abstract record B95_1 : B95_2 { } -public sealed partial record V95 : B95_1, IValidatedValue -{ - public static Result Create(char value) => new V95(value); -} - -// row 96 -public partial struct O96 -{ -public abstract record B96_3 { } -public abstract record B96_2 : B96_3 { } -public abstract record B96_1 : B96_2 { } -public sealed partial record V96 : B96_1, IValidatedValue -{ - public static Result Create(global::Probes.Shapes.PRec value) => new V96(value); -} -} - -// row 97 -public abstract record B97_1 { } -public sealed partial record V97 : B97_1, IValidatedValue -{ - public static Result Create(global::Probes.Shapes.PFlags value) => new V97(value); -} - -// row 98 -public abstract record B98_2 { } -public abstract record B98_1 : B98_2 { } -public sealed partial record V98 : B98_1, IValue { } - -// row 99 -public interface IMark99 { int Mark(); } -public readonly partial record struct V99 : IValidatedValue, IMark99 -{ - public static Result Create(string value) => new V99(value); int IMark99.Mark() => 1; -} - -// row 100 -public abstract record B100_1 { } -public sealed partial record V100 : B100_1, IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsesToInt value) => new V100(value); -} -public abstract record BC100_1 { } -public sealed partial record VC100 : BC100_1, IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsedShape value) => new VC100(value); -} - -// row 101 -public static partial class O101 -{ -public interface IMark101 { int Mark(); } -public readonly partial record struct V101 : IValidatedValue, IMark101 -{ - public static Result Create(global::System.DateOnly value) => new V101(value); int IMark101.Mark() => 1; -} -} - -// row 102 -public static partial class O102 -{ -public sealed partial class P102 -{ -public interface IMark102 { int Mark(); } -public sealed partial record V102 : IValue, IMark102 { int IMark102.Mark() => 1; } -} -} - -// row 103 -public readonly partial record struct V103 : IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsesToInt value) => new V103(value); -} -public readonly partial record struct VC103 : IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsedShape value) => new VC103(value); -} - -// row 104 -public static partial class O104 -{ -public sealed partial class P104 -{ -public readonly partial record struct V104 : IValue { } -} -} - -// row 105 -public interface IMark105 { int Mark(); } -public readonly partial record struct V105 : IValue, IMark105 { int IMark105.Mark() => 1; } - -// row 106 -public static partial class O106 -{ -public sealed partial class P106 -{ -public abstract record B106_1 : System.IEquatable { bool System.IEquatable.Equals(B106_1? other) => ReferenceEquals(this, other); } -public sealed partial record V106 : B106_1, IValue { } -public abstract record BC106_1 : System.IEquatable { bool System.IEquatable.Equals(BC106_1? other) => ReferenceEquals(this, other); } -public sealed partial record VC106 : BC106_1, IValue { } -} -} - -// row 107 -public static partial class O107 -{ -public sealed partial class P107 -{ -public readonly partial record struct V107 : IValue { } -public readonly partial record struct VC107 : IValue { } -} -} - -// row 108 -public static partial class O108 -{ -public sealed partial class P108 -{ -public readonly partial record struct V108 : IValidatedValue -{ - public static Result Create(bool value) => new V108(value); -} -} -} - -// row 109 -public static partial class O109 -{ -public abstract record B109_2 { } -public abstract record B109_1 : B109_2 { } -public sealed partial record V109 : B109_1, IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ConstructedByRef value) => new V109(value); -} -public abstract record BC109_2 { } -public abstract record BC109_1 : BC109_2 { } -public sealed partial record VC109 : BC109_1, IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsesFromIn value) => new VC109(value); -} -} - -// row 110 -public static partial class O110 -{ -public sealed partial class P110 -{ -public abstract record B110_1 : System.IEquatable { bool System.IEquatable.Equals(B110_1? other) => ReferenceEquals(this, other); } -public sealed partial record V110 : B110_1, IValue { } -} -} - -// row 111 -public static partial class O111 -{ -public readonly partial record struct V111 : IValue { } -public readonly partial record struct VC111 : IValue { } -} - -// row 112 -public partial struct O112 -{ -public interface IMark112 { int Mark(); } -public readonly partial record struct V112 : IValidatedValue, IMark112 -{ - public static Result Create(int value) => new V112(value); int IMark112.Mark() => 1; -} -} - -// row 113 -public partial struct O113 -{ -public abstract record B113_3 { } -public abstract record B113_2 : B113_3 { } -public abstract record B113_1 : B113_2 { } -public sealed partial record V113 : B113_1, IValue { } -} - -// row 114 -public partial struct O114 -{ -public abstract record B114_2 { } -public abstract record B114_1 : B114_2 { } -public sealed partial record V114 : B114_1, IValidatedValue -{ - public static Result Create(double value) => new V114(value); -} -} - -// row 115 -public static partial class O115 -{ -public readonly partial record struct V115 : IValidatedValue -{ - public static Result Create(global::System.DateTime value) => new V115(value); -} -} - -// row 116 -public static partial class O116 -{ -public abstract record B116_2 { } -public abstract record B116_1 : B116_2 { } -public sealed partial record V116 : B116_1, IValue { } -} - -// row 117 -public static partial class O117 -{ -public abstract record B117_1 { } -public sealed partial record V117 : B117_1, IValue { } -public abstract record BC117_1 { } -public sealed partial record VC117 : BC117_1, IValue { } -} - -// row 118 -public partial struct O118 -{ -public abstract record B118_1 : System.IEquatable { bool System.IEquatable.Equals(B118_1? other) => ReferenceEquals(this, other); } -public sealed partial record V118 : B118_1, IValue { } -} - -// row 119 -public static partial class O119 -{ -public sealed partial class P119 -{ -public abstract record B119_1 { } -public sealed partial record V119 : B119_1, IValue { } -} -} - -// row 120 -public partial struct O120 -{ -public abstract record B120_2 { } -public abstract record B120_1 : B120_2 { } -public sealed partial record V120 : B120_1, IValidatedValue -{ - public static Result Create(global::NodaTime.Instant value) => new V120(value); -} -} - -// row 121 -public abstract record B121_3 { } -public abstract record B121_2 : B121_3 { } -public abstract record B121_1 : B121_2 { } -public sealed partial record V121 : B121_1, IValue { } -public abstract record BC121_3 { } -public abstract record BC121_2 : BC121_3 { } -public abstract record BC121_1 : BC121_2 { } -public sealed partial record VC121 : BC121_1, IValue { } - -// row 122 -public static partial class O122 -{ -public readonly partial record struct V122 : IValue { } -} - -// row 123 -public partial struct O123 -{ -public interface IMark123 { int Mark(); } -public sealed partial record V123 : IValue, IMark123 { int IMark123.Mark() => 1; } -public sealed partial record VC123 : IValue, IMark123 { int IMark123.Mark() => 1; } -} - -// row 124 -public partial struct O124 -{ -public readonly partial record struct V124 : IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsesByRef value) => new V124(value); -} -public readonly partial record struct VC124 : IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsesFromIn value) => new VC124(value); -} -} - -// row 125 -public static partial class O125 -{ -public interface IMark125 { int Mark(); } -public readonly partial record struct V125 : IValidatedValue, IMark125 -{ - public static Result Create(global::System.Uri value) => new V125(value); int IMark125.Mark() => 1; -} -} - -// row 126 -public static partial class O126 -{ -public sealed partial class P126 -{ -public abstract record B126_1 : System.IEquatable { bool System.IEquatable.Equals(B126_1? other) => ReferenceEquals(this, other); } -public sealed partial record V126 : B126_1, IValidatedValue -{ - public static Result Create(decimal value) => new V126(value); -} -} -} - -// row 127 -public static partial class O127 -{ -public abstract record B127_1 : System.IEquatable { bool System.IEquatable.Equals(B127_1? other) => ReferenceEquals(this, other); } -public sealed partial record V127 : B127_1, IValue { } -} - -// row 128 -public static partial class O128 -{ -public abstract record B128_1 : System.IEquatable { bool System.IEquatable.Equals(B128_1? other) => ReferenceEquals(this, other); } -public sealed partial record V128 : B128_1, IValidatedValue -{ - public static Result Create(global::System.DateTime value) => new V128(value); -} -} - -// row 129 -public readonly partial record struct V129 : IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ConstructedByRef value) => new V129(value); -} -public readonly partial record struct VC129 : IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsesFromIn value) => new VC129(value); -} - -// row 130 -public static partial class O130 -{ -public interface IMark130 { int Mark(); } -public readonly partial record struct V130 : IValue, IMark130 { int IMark130.Mark() => 1; } -} - -// row 131 -public partial struct O131 -{ -public interface IMark131 { int Mark(); } -public readonly partial record struct V131 : IValue, IMark131 { int IMark131.Mark() => 1; } -} - -// row 132 -public interface IMark132 { int Mark(); } -public readonly partial record struct V132 : IValidatedValue, IMark132 -{ - public static Result Create(global::Probes.Build.C11AbstractText value) => new V132(value); int IMark132.Mark() => 1; -} -public readonly partial record struct VC132 : IValidatedValue, IMark132 -{ - public static Result Create(global::Probes.Build.C11ConstructedConcrete value) => new VC132(value); int IMark132.Mark() => 1; -} - -// row 133 -public static partial class O133 -{ -public abstract record B133_2 { } -public abstract record B133_1 : B133_2 { } -public sealed partial record V133 : B133_1, IValue { } -} - -// row 134 -public abstract record B134_2 { } -public abstract record B134_1 : B134_2 { } -public sealed partial record V134 : B134_1, IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsesByRef value) => new V134(value); -} -public abstract record BC134_2 { } -public abstract record BC134_1 : BC134_2 { } -public sealed partial record VC134 : BC134_1, IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsesFromIn value) => new VC134(value); -} - -// row 135 -public partial struct O135 -{ -public abstract record B135_3 { } -public abstract record B135_2 : B135_3 { } -public abstract record B135_1 : B135_2 { } -public sealed partial record V135 : B135_1, IValue { } -} - -// row 136 -public partial struct O136 -{ -public interface IMark136 { int Mark(); } -public readonly partial record struct V136 : IValue, IMark136 { int IMark136.Mark() => 1; } -} - -// row 137 -public static partial class O137 -{ -public abstract record B137_1 { } -public sealed partial record V137 : B137_1, IValue { } -public abstract record BC137_1 { } -public sealed partial record VC137 : BC137_1, IValue { } -} - -// row 138 -public static partial class O138 -{ -public readonly partial record struct V138 : IValue { } -public readonly partial record struct VC138 : IValue { } -} - -// row 139 -public static partial class O139 -{ -public sealed partial class P139 -{ -public abstract record B139_1 : System.IEquatable { bool System.IEquatable.Equals(B139_1? other) => ReferenceEquals(this, other); } -public sealed partial record V139 : B139_1, IValue { } -} -} - -// row 140 -public static partial class O140 -{ -public abstract record B140_2 { } -public abstract record B140_1 : B140_2 { } -public sealed partial record V140 : B140_1, IValue { } -} - -// row 141 -public static partial class O141 -{ -public interface IMark141 { int Mark(); } -public readonly partial record struct V141 : IValue, IMark141 { int IMark141.Mark() => 1; } -public readonly partial record struct VC141 : IValue, IMark141 { int IMark141.Mark() => 1; } -} - -// row 142 -public static partial class O142 -{ -public readonly partial record struct V142 : IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsesToInt value) => new V142(value); -} -public readonly partial record struct VC142 : IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsedShape value) => new VC142(value); -} -} - -// row 143 -public sealed partial record V143 : IValue { } -public sealed partial record VC143 : IValue { } - -// row 144 -public static partial class O144 -{ -public sealed partial class P144 -{ -public interface IMark144 { int Mark(); } -public readonly partial record struct V144 : IValue, IMark144 { int IMark144.Mark() => 1; } -public readonly partial record struct VC144 : IValue, IMark144 { int IMark144.Mark() => 1; } -} -} - -// row 145 -public static partial class O145 -{ -public sealed partial class P145 -{ -public readonly partial record struct V145 : IValidatedValue -{ - public static Result Create(ulong value) => new V145(value); -} -} -} - -// row 146 -public static partial class O146 -{ -public readonly partial record struct V146 : IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsesToInt value) => new V146(value); -} -public readonly partial record struct VC146 : IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsedShape value) => new VC146(value); -} -} - -// row 147 -public static partial class O147 -{ -public sealed partial class P147 -{ -public interface IMark147 { int Mark(); } -public readonly partial record struct V147 : IValidatedValue, IMark147 -{ - public static Result Create(global::Probes.Build.C11AbstractText value) => new V147(value); int IMark147.Mark() => 1; -} -public readonly partial record struct VC147 : IValidatedValue, IMark147 -{ - public static Result Create(global::Probes.Build.C11ConstructedConcrete value) => new VC147(value); int IMark147.Mark() => 1; -} -} -} - -// row 148 -public static partial class O148 -{ -public sealed partial record V148 : IValidatedValue -{ - public static Result Create(global::Probes.Shapes.PEnum value) => new V148(value); -} -} - -// row 149 -public partial struct O149 -{ -public interface IMark149 { int Mark(); } -public sealed partial record V149 : IValue, IMark149 { int IMark149.Mark() => 1; } -public sealed partial record VC149 : IValue, IMark149 { int IMark149.Mark() => 1; } -} - -// row 150 -public static partial class O150 -{ -public sealed partial class P150 -{ -public abstract record B150_3 { } -public abstract record B150_2 : B150_3 { } -public abstract record B150_1 : B150_2 { } -public sealed partial record V150 : B150_1, IValidatedValue -{ - public static Result Create(decimal value) => new V150(value); -} -} -} - -// row 151 -public static partial class O151 -{ -public sealed partial record V151 : IValidatedValue -{ - public static Result Create(ulong value) => new V151(value); -} -} - -// row 152 -public static partial class O152 -{ -public interface IMark152 { int Mark(); } -public sealed partial record V152 : IValue, IMark152 { int IMark152.Mark() => 1; } -public sealed partial record VC152 : IValue, IMark152 { int IMark152.Mark() => 1; } -} - -// row 153 -public static partial class O153 -{ -public sealed partial record V153 : IValue { } -public sealed partial record VC153 : IValue { } -} - -// row 154 -public static partial class O154 -{ -public sealed partial class P154 -{ -public abstract record B154_1 : System.IEquatable { bool System.IEquatable.Equals(B154_1? other) => ReferenceEquals(this, other); } -public sealed partial record V154 : B154_1, IValue { } -public abstract record BC154_1 : System.IEquatable { bool System.IEquatable.Equals(BC154_1? other) => ReferenceEquals(this, other); } -public sealed partial record VC154 : BC154_1, IValue { } -} -} - -// row 155 -public sealed partial record V155 : IValue { } -public sealed partial record VC155 : IValue { } - -// row 156 -public interface IMark156 { int Mark(); } -public readonly partial record struct V156 : IValidatedValue, IMark156 -{ - public static Result Create(global::System.DateTimeOffset value) => new V156(value); int IMark156.Mark() => 1; -} - -// row 157 -public static partial class O157 -{ -public sealed partial class P157 -{ -public abstract record B157_1 : System.IEquatable { bool System.IEquatable.Equals(B157_1? other) => ReferenceEquals(this, other); } -public sealed partial record V157 : B157_1, IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsesToInt value) => new V157(value); -} -public abstract record BC157_1 : System.IEquatable { bool System.IEquatable.Equals(BC157_1? other) => ReferenceEquals(this, other); } -public sealed partial record VC157 : BC157_1, IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsedShape value) => new VC157(value); -} -} -} - -// row 158 -public static partial class O158 -{ -public sealed partial class P158 -{ -public abstract record B158_3 { } -public abstract record B158_2 : B158_3 { } -public abstract record B158_1 : B158_2 { } -public sealed partial record V158 : B158_1, IValidatedValue -{ - public static Result Create(string value) => new V158(value); -} -} -} - -// row 159 -public static partial class O159 -{ -public sealed partial class P159 -{ -public abstract record B159_2 { } -public abstract record B159_1 : B159_2 { } -public sealed partial record V159 : B159_1, IValue { } -} -} - -// row 160 -public partial struct O160 -{ -public interface IMark160 { int Mark(); } -public sealed partial record V160 : IValue, IMark160 { int IMark160.Mark() => 1; } -} - -// row 161 -public static partial class O161 -{ -public readonly partial record struct V161 : IValue { } -} - -// row 162 -public partial struct O162 -{ -public abstract record B162_3 { } -public abstract record B162_2 : B162_3 { } -public abstract record B162_1 : B162_2 { } -public sealed partial record V162 : B162_1, IValue { } -} - -// row 163 -public partial struct O163 -{ -public abstract record B163_1 : System.IEquatable { bool System.IEquatable.Equals(B163_1? other) => ReferenceEquals(this, other); } -public sealed partial record V163 : B163_1, IValue { } -} - -// row 164 -public partial struct O164 -{ -public interface IMark164 { int Mark(); } -public readonly partial record struct V164 : IValidatedValue, IMark164 -{ - public static Result Create(byte value) => new V164(value); int IMark164.Mark() => 1; -} -} - -// row 165 -public abstract record B165_1 : System.IEquatable { bool System.IEquatable.Equals(B165_1? other) => ReferenceEquals(this, other); } -public sealed partial record V165 : B165_1, IValue { } - -// row 166 -public static partial class O166 -{ -public readonly partial record struct V166 : IValue { } -} - -// row 167 -public static partial class O167 -{ -public readonly partial record struct V167 : IValue { } -} - -// row 168 -public static partial class O168 -{ -public readonly partial record struct V168 : IValue { } -} - -// row 169 -public partial struct O169 -{ -public interface IMark169 { int Mark(); } -public readonly partial record struct V169 : IValue, IMark169 { int IMark169.Mark() => 1; } -} - -// row 170 -public static partial class O170 -{ -public sealed partial class P170 -{ -public readonly partial record struct V170 : IValidatedValue -{ - public static Result Create(global::Probes.Shapes.PRec value) => new V170(value); -} -} -} - -// row 171 -public static partial class O171 -{ -public sealed partial record V171 : IValidatedValue -{ - public static Result Create(byte value) => new V171(value); -} -} - -// row 172 -public static partial class O172 -{ -public sealed partial class P172 -{ -public abstract record B172_2 { } -public abstract record B172_1 : B172_2 { } -public sealed partial record V172 : B172_1, IValue { } -} -} - -// row 173 -public abstract record B173_3 { } -public abstract record B173_2 : B173_3 { } -public abstract record B173_1 : B173_2 { } -public sealed partial record V173 : B173_1, IValidatedValue -{ - public static Result Create(global::System.TimeOnly value) => new V173(value); -} - -// row 174 -public static partial class O174 -{ -public abstract record B174_3 { } -public abstract record B174_2 : B174_3 { } -public abstract record B174_1 : B174_2 { } -public sealed partial record V174 : B174_1, IValidatedValue -{ - public static Result Create(global::System.Uri value) => new V174(value); -} -} - -// row 175 -public partial struct O175 -{ -public readonly partial record struct V175 : IValidatedValue -{ - public static Result Create(double value) => new V175(value); -} -} - -// row 176 -public partial struct O176 -{ -public abstract record B176_1 : System.IEquatable { bool System.IEquatable.Equals(B176_1? other) => ReferenceEquals(this, other); } -public sealed partial record V176 : B176_1, IValue { } -} - -// row 177 -public static partial class O177 -{ -public abstract record B177_3 { } -public abstract record B177_2 : B177_3 { } -public abstract record B177_1 : B177_2 { } -public sealed partial record V177 : B177_1, IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ConstructedByRef value) => new V177(value); -} -public abstract record BC177_3 { } -public abstract record BC177_2 : BC177_3 { } -public abstract record BC177_1 : BC177_2 { } -public sealed partial record VC177 : BC177_1, IValidatedValue -{ - public static Result Create(global::Probes.Build.C11ParsesFromIn value) => new VC177(value); -} -} - -// row 178 -public partial struct O178 -{ -public interface IMark178 { int Mark(); } -public readonly partial record struct V178 : IValidatedValue, IMark178 -{ - public static Result Create(global::System.Guid value) => new V178(value); int IMark178.Mark() => 1; -} -} - -// row 179 -public static partial class O179 -{ -public readonly partial record struct V179 : IValidatedValue -{ - public static Result Create(global::System.TimeOnly value) => new V179(value); -} -} - -// row 180 -public static partial class O180 -{ -public sealed partial class P180 -{ -public interface IMark180 { int Mark(); } -public readonly partial record struct V180 : IValue, IMark180 { int IMark180.Mark() => 1; } -} -} - -// row 181 -public interface IMark181 { int Mark(); } -public readonly partial record struct V181 : IValidatedValue, IMark181 -{ - public static Result Create(global::System.TimeSpan value) => new V181(value); int IMark181.Mark() => 1; -} - -// row 182 -public static partial class O182 -{ -public readonly partial record struct V182 : IValidatedValue -{ - public static Result Create(decimal value) => new V182(value); -} -} - -// row 183 -public partial struct O183 -{ -public abstract record B183_3 { } -public abstract record B183_2 : B183_3 { } -public abstract record B183_1 : B183_2 { } -public sealed partial record V183 : B183_1, IValidatedValue -{ - public static Result Create(global::Probes.Shapes.PFlags value) => new V183(value); -} -} - -// row 184 -public abstract record B184_1 { } -public sealed partial record V184 : B184_1, IValidatedValue -{ - public static Result Create(global::System.DateTime value) => new V184(value); -} - -// row 185 -public static partial class O185 -{ -public sealed partial class P185 -{ -public readonly partial record struct V185 : IValue { } -} -} - -// row 186 -public abstract record B186_3 { } -public abstract record B186_2 : B186_3 { } -public abstract record B186_1 : B186_2 { } -public sealed partial record V186 : B186_1, IValue { } - -// row 187 -public static partial class O187 -{ -public abstract record B187_1 { } -public sealed partial record V187 : B187_1, IValidatedValue -{ - public static Result Create(global::System.DateOnly value) => new V187(value); -} -} - -// row 188 -public static partial class O188 -{ -public sealed partial class P188 -{ -public interface IMark188 { int Mark(); } -public readonly partial record struct V188 : IValue, IMark188 { int IMark188.Mark() => 1; } -} -} - -// row 189 -public static partial class O189 -{ -public sealed partial class P189 -{ -public interface IMark189 { int Mark(); } -public sealed partial record V189 : IValidatedValue, IMark189 -{ - public static Result Create(global::NodaTime.Instant value) => new V189(value); int IMark189.Mark() => 1; -} -} -} - -// row 190 -public partial struct O190 -{ -public abstract record B190_3 { } -public abstract record B190_2 : B190_3 { } -public abstract record B190_1 : B190_2 { } -public sealed partial record V190 : B190_1, IValue { } -} - -// row 191 -public static partial class O191 -{ -public sealed partial class P191 -{ -public interface IMark191 { int Mark(); } -public sealed partial record V191 : IValue, IMark191 { int IMark191.Mark() => 1; } -} -} - -// row 192 -public abstract record B192_1 : System.IEquatable { bool System.IEquatable.Equals(B192_1? other) => ReferenceEquals(this, other); } -public sealed partial record V192 : B192_1, IValidatedValue -{ - public static Result Create(ulong value) => new V192(value); -} - -// row 193 -public abstract record B193_2 { } -public abstract record B193_1 : B193_2 { } -public sealed partial record V193 : B193_1, IValue { } - -// row 194 -public static partial class O194 -{ -public abstract record B194_2 { } -public abstract record B194_1 : B194_2 { } -public sealed partial record V194 : B194_1, IValidatedValue -{ - public static Result Create(byte value) => new V194(value); -} -} - -// row 195 -public interface IMark195 { int Mark(); } -public sealed partial record V195 : IValue, IMark195 { int IMark195.Mark() => 1; } - -// row 196 -public abstract record B196_1 : System.IEquatable { bool System.IEquatable.Equals(B196_1? other) => ReferenceEquals(this, other); } -public sealed partial record V196 : B196_1, IValidatedValue -{ - public static Result Create(double value) => new V196(value); -} - -// row 197 -public static partial class O197 -{ -public abstract record B197_2 { } -public abstract record B197_1 : B197_2 { } -public sealed partial record V197 : B197_1, IValue { } -} - -// row 198 -public partial struct O198 -{ -public readonly partial record struct V198 : IValidatedValue -{ - public static Result Create(byte value) => new V198(value); -} -} - -// row 199 -public static partial class O199 -{ -public sealed partial class P199 -{ -public interface IMark199 { int Mark(); } -public readonly partial record struct V199 : IValidatedValue, IMark199 -{ - public static Result Create(char value) => new V199(value); int IMark199.Mark() => 1; -} -} -} - -// row 200 -public partial struct O200 -{ -public abstract record B200_1 : System.IEquatable { bool System.IEquatable.Equals(B200_1? other) => ReferenceEquals(this, other); } -public sealed partial record V200 : B200_1, IValue { } -} - -// row 201 -public partial struct O201 -{ -public abstract record B201_3 { } -public abstract record B201_2 : B201_3 { } -public abstract record B201_1 : B201_2 { } -public sealed partial record V201 : B201_1, IValue { } -} - -// row 202 -public partial struct O202 -{ -public abstract record B202_2 { } -public abstract record B202_1 : B202_2 { } -public sealed partial record V202 : B202_1, IValue { } -} - -// row 203 -public static partial class O203 -{ -public sealed partial class P203 -{ -public abstract record B203_1 { } -public sealed partial record V203 : B203_1, IValue { } -} -} - -// row 204 -public partial struct O204 -{ -public abstract record B204_1 { } -public sealed partial record V204 : B204_1, IValidatedValue -{ - public static Result Create(global::System.TimeOnly value) => new V204(value); -} -} - -// row 205 -public partial struct O205 -{ -public abstract record B205_1 { } -public sealed partial record V205 : B205_1, IValue { } -} - -// row 206 -public static partial class O206 -{ -public sealed partial class P206 -{ -public abstract record B206_1 { } -public sealed partial record V206 : B206_1, IValidatedValue -{ - public static Result Create(global::System.Uri value) => new V206(value); -} -} -} - -// row 207 -public abstract record B207_1 { } -public sealed partial record V207 : B207_1, IValue { } - -// row 208 -public abstract record B208_2 { } -public abstract record B208_1 : B208_2 { } -public sealed partial record V208 : B208_1, IValidatedValue -{ - public static Result Create(long value) => new V208(value); -} - -// row 209 -public static partial class O209 -{ -public sealed partial class P209 -{ -public interface IMark209 { int Mark(); } -public sealed partial record V209 : IValidatedValue, IMark209 -{ - public static Result Create(ulong value) => new V209(value); int IMark209.Mark() => 1; -} -} -} - -// row 210 -public static partial class O210 -{ -public abstract record B210_1 { } -public sealed partial record V210 : B210_1, IValidatedValue -{ - public static Result Create(bool value) => new V210(value); -} -} - -// row 211 -public abstract record B211_1 : System.IEquatable { bool System.IEquatable.Equals(B211_1? other) => ReferenceEquals(this, other); } -public sealed partial record V211 : B211_1, IValidatedValue -{ - public static Result Create(global::System.TimeSpan value) => new V211(value); -} - -// row 212 -public static partial class O212 -{ -public abstract record B212_3 { } -public abstract record B212_2 : B212_3 { } -public abstract record B212_1 : B212_2 { } -public sealed partial record V212 : B212_1, IValidatedValue -{ - public static Result Create(int value) => new V212(value); -} -} - -// row 213 -public static partial class O213 -{ -public abstract record B213_1 : System.IEquatable { bool System.IEquatable.Equals(B213_1? other) => ReferenceEquals(this, other); } -public sealed partial record V213 : B213_1, IValue { } -} - -// row 214 -public abstract record B214_1 { } -public sealed partial record V214 : B214_1, IValidatedValue -{ - public static Result Create(decimal value) => new V214(value); -} - -// row 215 -public static partial class O215 -{ -public sealed partial class P215 -{ -public abstract record B215_1 : System.IEquatable { bool System.IEquatable.Equals(B215_1? other) => ReferenceEquals(this, other); } -public sealed partial record V215 : B215_1, IValidatedValue -{ - public static Result Create(byte value) => new V215(value); -} -} -} - -// row 216 -public static partial class O216 -{ -public sealed partial class P216 -{ -public abstract record B216_1 { } -public sealed partial record V216 : B216_1, IValidatedValue -{ - public static Result Create(global::Probes.Shapes.PRec value) => new V216(value); -} -} -} - -// row 217 -public partial struct O217 -{ -public readonly partial record struct V217 : IValidatedValue -{ - public static Result Create(int value) => new V217(value); -} -} - -// row 218 -public static partial class O218 -{ -public abstract record B218_1 : System.IEquatable { bool System.IEquatable.Equals(B218_1? other) => ReferenceEquals(this, other); } -public sealed partial record V218 : B218_1, IValue { } -} - -// row 219 -public static partial class O219 -{ -public sealed partial class P219 -{ -public abstract record B219_2 { } -public abstract record B219_1 : B219_2 { } -public sealed partial record V219 : B219_1, IValidatedValue -{ - public static Result Create(global::System.Guid value) => new V219(value); -} -} -} - -// row 220 -public static partial class O220 -{ -public sealed partial class P220 -{ -public abstract record B220_1 { } -public sealed partial record V220 : B220_1, IValue { } -} -} - -// row 221 -public partial struct O221 -{ -public sealed partial record V221 : IValue { } -} - -// row 222 -public partial struct O222 -{ -public abstract record B222_1 { } -public sealed partial record V222 : B222_1, IValue { } -} - -// row 223 -public sealed partial record V223 : IValue { } - -// row 224 -public partial struct O224 -{ -public abstract record B224_1 { } -public sealed partial record V224 : B224_1, IValidatedValue -{ - public static Result Create(global::NodaTime.Instant value) => new V224(value); -} -} - -// row 225 -public static partial class O225 -{ -public sealed partial class P225 -{ -public readonly partial record struct V225 : IValue { } -} -} - -// row 226 -public static partial class O226 -{ -public sealed partial class P226 -{ -public readonly partial record struct V226 : IValidatedValue -{ - public static Result Create(global::System.DateTimeOffset value) => new V226(value); -} -} -} - -// row 227 -public static partial class O227 -{ -public abstract record B227_3 { } -public abstract record B227_2 : B227_3 { } -public abstract record B227_1 : B227_2 { } -public sealed partial record V227 : B227_1, IValue { } -} - -// row 228 -public static partial class O228 -{ -public readonly partial record struct V228 : IValidatedValue -{ - public static Result Create(global::System.TimeSpan value) => new V228(value); -} -} - -// row 229 -public static partial class O229 -{ -public sealed partial class P229 -{ -public abstract record B229_1 : System.IEquatable { bool System.IEquatable.Equals(B229_1? other) => ReferenceEquals(this, other); } -public sealed partial record V229 : B229_1, IValidatedValue -{ - public static Result Create(global::System.DateTime value) => new V229(value); -} -} -} - -// row 230 -public partial struct O230 -{ -public abstract record B230_1 : System.IEquatable { bool System.IEquatable.Equals(B230_1? other) => ReferenceEquals(this, other); } -public sealed partial record V230 : B230_1, IValidatedValue -{ - public static Result Create(double value) => new V230(value); -} -} - -// row 231 -public static partial class O231 -{ -public interface IMark231 { int Mark(); } -public readonly partial record struct V231 : IValidatedValue, IMark231 -{ - public static Result Create(global::System.Uri value) => new V231(value); int IMark231.Mark() => 1; -} -} - -// row 232 -public partial struct O232 -{ -public interface IMark232 { int Mark(); } -public readonly partial record struct V232 : IValue, IMark232 { int IMark232.Mark() => 1; } -} - -// row 233 -public static partial class O233 -{ -public sealed partial class P233 -{ -public readonly partial record struct V233 : IValue { } -} -} - -// row 234 -public readonly partial record struct V234 : IValue { } - -// row 235 -public abstract record B235_3 { } -public abstract record B235_2 : B235_3 { } -public abstract record B235_1 : B235_2 { } -public sealed partial record V235 : B235_1, IValidatedValue -{ - public static Result Create(global::System.DateTimeOffset value) => new V235(value); -} - -// row 236 -public abstract record B236_3 { } -public abstract record B236_2 : B236_3 { } -public abstract record B236_1 : B236_2 { } -public sealed partial record V236 : B236_1, IValidatedValue -{ - public static Result Create(bool value) => new V236(value); -} - -// row 237 -public static partial class O237 -{ -public abstract record B237_2 { } -public abstract record B237_1 : B237_2 { } -public sealed partial record V237 : B237_1, IValue { } -} - -// row 238 -public static partial class O238 -{ -public sealed partial class P238 -{ -public abstract record B238_1 { } -public sealed partial record V238 : B238_1, IValue { } -} -} - -// row 239 -public abstract record B239_1 { } -public sealed partial record V239 : B239_1, IValue { } - -// row 240 -public abstract record B240_1 { } -public sealed partial record V240 : B240_1, IValue { } - -// row 241 -public static partial class O241 -{ -public sealed partial class P241 -{ -public interface IMark241 { int Mark(); } -public sealed partial record V241 : IValidatedValue, IMark241 -{ - public static Result Create(global::Probes.Shapes.PEnum value) => new V241(value); int IMark241.Mark() => 1; -} -} -} - -// row 242 -public partial struct O242 -{ -public abstract record B242_3 { } -public abstract record B242_2 : B242_3 { } -public abstract record B242_1 : B242_2 { } -public sealed partial record V242 : B242_1, IValue { } -} - -// row 243 -public readonly partial record struct V243 : IValue { } - -// row 244 -public static partial class O244 -{ -public sealed partial class P244 -{ -public readonly partial record struct V244 : IValidatedValue -{ - public static Result Create(global::NodaTime.Instant value) => new V244(value); -} -} -} - -// row 245 -public partial struct O245 -{ -public interface IMark245 { int Mark(); } -public readonly partial record struct V245 : IValue, IMark245 { int IMark245.Mark() => 1; } -} - -// row 246 -public static partial class O246 -{ -public sealed partial class P246 -{ -public abstract record B246_3 { } -public abstract record B246_2 : B246_3 { } -public abstract record B246_1 : B246_2 { } -public sealed partial record V246 : B246_1, IValidatedValue -{ - public static Result Create(global::System.Uri value) => new V246(value); -} -} -} - -public static partial class C11Table -{ - public static readonly C11Case[] Cases = - [ - new(0, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=explicit-interface-member;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-class;shape.wrapped=double;text.culture=invariant;text.entry=TryFrom", "sealed-record-class", "nested-in-class", "explicit-interface-member", typeof(O0.V0), typeof(double), null, null, []), - new(1, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=none;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=top-level;shape.wrapped=TimeSpan;text.culture=de-DE;text.entry=ISpanParsable", "validated-class", "top-level", "none", typeof(V1), typeof(global::System.TimeSpan), null, null, []), - new(2, "parse-shape=implicit-result;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-2;shape.kind=sealed-record-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-struct;shape.wrapped=decimal;text.culture=tr-TR;text.entry=TryFormat-short-span", "sealed-record-class", "nested-in-struct", "base-depth-2", typeof(O2.V2), typeof(global::Probes.Build.C11ParsesToInt), typeof(O2.VC2), typeof(global::Probes.Build.C11ParsedShape), []), - new(3, "parse-shape=abstract-string-ctor;process.converter-lookup=none;shape.inheritance=none;shape.kind=validated-struct;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-two-levels;shape.wrapped=TimeOnly;text.culture=tr-TR;text.entry=Parse", "validated-struct", "nested-two-levels", "none", typeof(O3.P3.V3), typeof(global::Probes.Build.C11AbstractText), typeof(O3.P3.VC3), typeof(global::Probes.Build.C11ConstructedConcrete), []), - new(4, "parse-shape=ref-text;process.converter-lookup=none;shape.inheritance=explicit-interface-member;shape.kind=record-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=top-level;shape.wrapped=decimal;text.culture=ar-SA;text.entry=ToString-format", "record-struct", "top-level", "explicit-interface-member", typeof(V4), typeof(global::Probes.Build.C11ParsesByRef), typeof(VC4), typeof(global::Probes.Build.C11ParsesFromIn), []), - new(5, "parse-shape=abstract-string-ctor;process.converter-lookup=none;shape.inheritance=base-depth-1;shape.kind=validated-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-struct;shape.wrapped=char;text.culture=globalization-invariant;text.entry=IParsable", "validated-class", "nested-in-struct", "base-depth-1", typeof(O5.V5), typeof(global::Probes.Build.C11AbstractText), typeof(O5.VC5), typeof(global::Probes.Build.C11ConstructedConcrete), []), - new(6, "parse-shape=implicit-result;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=none;shape.kind=record-struct;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-class;shape.wrapped=string;text.culture=globalization-invariant;text.entry=TryParse", "record-struct", "nested-in-class", "none", typeof(O6.V6), typeof(global::Probes.Build.C11ParsesToInt), typeof(O6.VC6), typeof(global::Probes.Build.C11ParsedShape), []), - new(7, "parse-shape=ref-text;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-1;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-two-levels;shape.wrapped=NodaTime.Instant;text.culture=invariant;text.entry=type-converter", "sealed-record-class", "nested-two-levels", "base-depth-1", typeof(O7.P7.V7), typeof(global::Probes.Build.C11ConstructedByRef), typeof(O7.P7.VC7), typeof(global::Probes.Build.C11ParsesFromIn), []), - new(8, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-explicit-IEquatable;shape.kind=sealed-record-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-two-levels;shape.wrapped=long;text.culture=ar-SA;text.entry=IParsable", "sealed-record-class", "nested-two-levels", "base-explicit-IEquatable", typeof(O8.P8.V8), typeof(long), null, null, []), - new(9, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-3;shape.kind=validated-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-struct;shape.wrapped=NodaTime.Instant;text.culture=de-DE;text.entry=TryParse", "validated-class", "nested-in-struct", "base-depth-3", typeof(O9.V9), typeof(global::NodaTime.Instant), null, null, []), - new(10, "parse-shape=implicit-result;process.converter-lookup=none;shape.inheritance=base-explicit-IEquatable;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=top-level;shape.wrapped=ulong;text.culture=invariant;text.entry=TryFormat-short-span", "validated-class", "top-level", "base-explicit-IEquatable", typeof(V10), typeof(global::Probes.Build.C11ParsesToInt), typeof(VC10), typeof(global::Probes.Build.C11ParsedShape), []), - new(11, "parse-shape=ref-text;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=explicit-interface-member;shape.kind=validated-struct;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-class;shape.wrapped=Guid;text.culture=de-DE;text.entry=Parse", "validated-struct", "nested-in-class", "explicit-interface-member", typeof(O11.V11), typeof(global::Probes.Build.C11ConstructedByRef), typeof(O11.VC11), typeof(global::Probes.Build.C11ParsesFromIn), []), - new(12, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-2;shape.kind=sealed-record-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=top-level;shape.wrapped=bool;text.culture=globalization-invariant;text.entry=type-converter", "sealed-record-class", "top-level", "base-depth-2", typeof(V12), typeof(bool), null, null, []), - new(13, "parse-shape=abstract-string-ctor;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-3;shape.kind=validated-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-class;shape.wrapped=DateTime;text.culture=tr-TR;text.entry=ToString-format", "validated-class", "nested-in-class", "base-depth-3", typeof(O13.V13), typeof(global::Probes.Build.C11AbstractText), typeof(O13.VC13), typeof(global::Probes.Build.C11ConstructedConcrete), []), - new(14, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-1;shape.kind=validated-struct;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-struct;shape.wrapped=DateOnly;text.culture=ar-SA;text.entry=ISpanParsable", "validated-struct", "nested-in-struct", "none", typeof(O14.V14), typeof(global::System.DateOnly), null, null, [("shape.inheritance", "none")]), - new(15, "parse-shape=ref-text;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=none;shape.kind=record-struct;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-struct;shape.wrapped=int;text.culture=invariant;text.entry=TryFrom", "record-struct", "nested-in-struct", "none", typeof(O15.V15), typeof(global::Probes.Build.C11ConstructedByRef), typeof(O15.VC15), typeof(global::Probes.Build.C11ParsesFromIn), []), - new(16, "parse-shape=abstract-string-ctor;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=explicit-interface-member;shape.kind=record-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-two-levels;shape.wrapped=enum;text.culture=globalization-invariant;text.entry=TryFormat-short-span", "record-struct", "nested-two-levels", "explicit-interface-member", typeof(O16.P16.V16), typeof(global::Probes.Build.C11AbstractText), typeof(O16.P16.VC16), typeof(global::Probes.Build.C11ConstructedConcrete), []), - new(17, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-1;shape.kind=record-struct;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=top-level;shape.wrapped=Uri;text.culture=tr-TR;text.entry=Parse", "record-struct", "top-level", "none", typeof(V17), typeof(global::System.Uri), null, null, [("shape.inheritance", "none")]), - new(18, "parse-shape=implicit-result;process.converter-lookup=none;shape.inheritance=base-depth-2;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-two-levels;shape.wrapped=TimeOnly;text.culture=ar-SA;text.entry=TryFrom", "validated-class", "nested-two-levels", "base-depth-2", typeof(O18.P18.V18), typeof(global::Probes.Build.C11ParsesToInt), typeof(O18.P18.VC18), typeof(global::Probes.Build.C11ParsedShape), []), - new(19, "parse-shape=abstract-string-ctor;process.converter-lookup=none;shape.inheritance=base-explicit-IEquatable;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-class;shape.wrapped=string;text.culture=de-DE;text.entry=ISpanParsable", "sealed-record-class", "nested-in-class", "base-explicit-IEquatable", typeof(O19.V19), typeof(global::Probes.Build.C11AbstractText), typeof(O19.VC19), typeof(global::Probes.Build.C11ConstructedConcrete), []), - new(20, "parse-shape=ref-text;process.converter-lookup=none;shape.inheritance=explicit-interface-member;shape.kind=validated-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-struct;shape.wrapped=TimeSpan;text.culture=tr-TR;text.entry=IParsable", "validated-struct", "nested-in-struct", "explicit-interface-member", typeof(O20.V20), typeof(global::Probes.Build.C11ParsesByRef), typeof(O20.VC20), typeof(global::Probes.Build.C11ParsesFromIn), []), - new(21, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=none;shape.kind=validated-struct;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-class;shape.wrapped=DateTimeOffset;text.culture=ar-SA;text.entry=TryFormat-short-span", "validated-struct", "nested-in-class", "none", typeof(O21.V21), typeof(global::System.DateTimeOffset), null, null, []), - new(22, "parse-shape=implicit-result;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=explicit-interface-member;shape.kind=record-struct;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-two-levels;shape.wrapped=ulong;text.culture=de-DE;text.entry=ISpanParsable", "record-struct", "nested-two-levels", "explicit-interface-member", typeof(O22.P22.V22), typeof(global::Probes.Build.C11ParsesToInt), typeof(O22.P22.VC22), typeof(global::Probes.Build.C11ParsedShape), []), - new(23, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-3;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-two-levels;shape.wrapped=byte;text.culture=invariant;text.entry=ToString-format", "sealed-record-class", "nested-two-levels", "base-depth-3", typeof(O23.P23.V23), typeof(byte), null, null, []), - new(24, "parse-shape=implicit-result;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-2;shape.kind=validated-struct;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-class;shape.wrapped=own-record-class;text.culture=de-DE;text.entry=ToString-format", "validated-struct", "nested-in-class", "none", typeof(O24.V24), typeof(global::Probes.Build.C11ParsesToInt), typeof(O24.VC24), typeof(global::Probes.Build.C11ParsedShape), [("shape.inheritance", "none")]), - new(25, "parse-shape=abstract-string-ctor;process.converter-lookup=none;shape.inheritance=explicit-interface-member;shape.kind=validated-struct;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=char;text.culture=ar-SA;text.entry=TryParse", "validated-struct", "top-level", "explicit-interface-member", typeof(V25), typeof(global::Probes.Build.C11AbstractText), typeof(VC25), typeof(global::Probes.Build.C11ConstructedConcrete), []), - new(26, "parse-shape=ref-text;process.converter-lookup=none;shape.inheritance=base-explicit-IEquatable;shape.kind=validated-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-struct;shape.wrapped=byte;text.culture=globalization-invariant;text.entry=Parse", "validated-class", "nested-in-struct", "base-explicit-IEquatable", typeof(O26.V26), typeof(global::Probes.Build.C11ParsesByRef), typeof(O26.VC26), typeof(global::Probes.Build.C11ParsesFromIn), []), - new(27, "parse-shape=implicit-result;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-1;shape.kind=validated-struct;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-class;shape.wrapped=ulong;text.culture=ar-SA;text.entry=type-converter", "validated-struct", "nested-in-class", "none", typeof(O27.V27), typeof(global::Probes.Build.C11ParsesToInt), typeof(O27.VC27), typeof(global::Probes.Build.C11ParsedShape), [("shape.inheritance", "none")]), - new(28, "parse-shape=ref-text;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-3;shape.kind=validated-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=top-level;shape.wrapped=TimeOnly;text.culture=globalization-invariant;text.entry=TryFrom", "validated-struct", "top-level", "none", typeof(V28), typeof(global::Probes.Build.C11ParsesByRef), typeof(VC28), typeof(global::Probes.Build.C11ParsesFromIn), [("shape.inheritance", "none")]), - new(29, "parse-shape=ref-text;process.converter-lookup=none;shape.inheritance=explicit-interface-member;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-struct;shape.wrapped=flags-enum;text.culture=de-DE;text.entry=type-converter", "validated-class", "nested-in-struct", "explicit-interface-member", typeof(O29.V29), typeof(global::Probes.Build.C11ConstructedByRef), typeof(O29.VC29), typeof(global::Probes.Build.C11ParsesFromIn), []), - new(30, "parse-shape=ref-text;process.converter-lookup=none;shape.inheritance=base-depth-2;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=DateOnly;text.culture=invariant;text.entry=TryParse", "sealed-record-class", "top-level", "base-depth-2", typeof(V30), typeof(global::Probes.Build.C11ParsesByRef), typeof(VC30), typeof(global::Probes.Build.C11ParsesFromIn), []), - new(31, "parse-shape=implicit-result;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=none;shape.kind=validated-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-class;shape.wrapped=decimal;text.culture=invariant;text.entry=IParsable", "validated-struct", "nested-in-class", "none", typeof(O31.V31), typeof(global::Probes.Build.C11ParsesToInt), typeof(O31.VC31), typeof(global::Probes.Build.C11ParsedShape), []), - new(32, "parse-shape=abstract-string-ctor;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=none;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-struct;shape.wrapped=Guid;text.culture=globalization-invariant;text.entry=ToString-format", "sealed-record-class", "nested-in-struct", "none", typeof(O32.V32), typeof(global::Probes.Build.C11AbstractText), typeof(O32.VC32), typeof(global::Probes.Build.C11ConstructedConcrete), []), - new(33, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-1;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-class;shape.wrapped=long;text.culture=de-DE;text.entry=TryFrom", "validated-class", "nested-in-class", "base-depth-1", typeof(O33.V33), typeof(long), null, null, []), - new(34, "parse-shape=implicit-result;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-3;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=top-level;shape.wrapped=flags-enum;text.culture=globalization-invariant;text.entry=Parse", "sealed-record-class", "top-level", "base-depth-3", typeof(V34), typeof(global::Probes.Build.C11ParsesToInt), typeof(VC34), typeof(global::Probes.Build.C11ParsedShape), []), - new(35, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-2;shape.kind=sealed-record-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=top-level;shape.wrapped=int;text.culture=globalization-invariant;text.entry=IParsable", "sealed-record-class", "top-level", "base-depth-2", typeof(V35), typeof(int), null, null, []), - new(36, "parse-shape=abstract-string-ctor;process.converter-lookup=none;shape.inheritance=base-depth-2;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-struct;shape.wrapped=DateTime;text.culture=invariant;text.entry=Parse", "sealed-record-class", "nested-in-struct", "base-depth-2", typeof(O36.V36), typeof(global::Probes.Build.C11AbstractText), typeof(O36.VC36), typeof(global::Probes.Build.C11ConstructedConcrete), []), - new(37, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=none;shape.kind=sealed-record-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-two-levels;shape.wrapped=char;text.culture=tr-TR;text.entry=type-converter", "sealed-record-class", "nested-two-levels", "none", typeof(O37.P37.V37), typeof(char), null, null, []), - new(38, "parse-shape=ref-text;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-2;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-struct;shape.wrapped=DateTimeOffset;text.culture=globalization-invariant;text.entry=ISpanParsable", "validated-class", "nested-in-struct", "base-depth-2", typeof(O38.V38), typeof(global::Probes.Build.C11ParsesByRef), typeof(O38.VC38), typeof(global::Probes.Build.C11ParsesFromIn), []), - new(39, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-3;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-struct;shape.wrapped=enum;text.culture=ar-SA;text.entry=Parse", "sealed-record-class", "nested-in-struct", "base-depth-3", typeof(O39.V39), typeof(global::Probes.Shapes.PEnum), null, null, []), - new(40, "parse-shape=implicit-result;process.converter-lookup=none;shape.inheritance=base-depth-3;shape.kind=validated-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-struct;shape.wrapped=Uri;text.culture=invariant;text.entry=ISpanParsable", "validated-struct", "nested-in-struct", "none", typeof(O40.V40), typeof(global::Probes.Build.C11ParsesToInt), typeof(O40.VC40), typeof(global::Probes.Build.C11ParsedShape), [("shape.inheritance", "none")]), - new(41, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-explicit-IEquatable;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-struct;shape.wrapped=own-record-class;text.culture=tr-TR;text.entry=ISpanParsable", "sealed-record-class", "nested-in-struct", "base-explicit-IEquatable", typeof(O41.V41), typeof(global::Probes.Shapes.PRec), null, null, []), - new(42, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-explicit-IEquatable;shape.kind=sealed-record-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-two-levels;shape.wrapped=Guid;text.culture=tr-TR;text.entry=TryParse", "sealed-record-class", "nested-two-levels", "base-explicit-IEquatable", typeof(O42.P42.V42), typeof(global::System.Guid), null, null, []), - new(43, "parse-shape=abstract-string-ctor;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-2;shape.kind=record-struct;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-struct;shape.wrapped=bool;text.culture=de-DE;text.entry=IParsable", "record-struct", "nested-in-struct", "none", typeof(O43.V43), typeof(global::Probes.Build.C11AbstractText), typeof(O43.VC43), typeof(global::Probes.Build.C11ConstructedConcrete), [("shape.inheritance", "none")]), - new(44, "parse-shape=ref-text;process.converter-lookup=none;shape.inheritance=base-depth-1;shape.kind=record-struct;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-two-levels;shape.wrapped=TimeOnly;text.culture=de-DE;text.entry=TryFormat-short-span", "record-struct", "nested-two-levels", "none", typeof(O44.P44.V44), typeof(global::Probes.Build.C11ParsesByRef), typeof(O44.P44.VC44), typeof(global::Probes.Build.C11ParsesFromIn), [("shape.inheritance", "none")]), - new(45, "parse-shape=ref-text;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=none;shape.kind=validated-struct;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=double;text.culture=ar-SA;text.entry=TryFormat-short-span", "validated-struct", "top-level", "none", typeof(V45), typeof(global::Probes.Build.C11ConstructedByRef), typeof(VC45), typeof(global::Probes.Build.C11ParsesFromIn), []), - new(46, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-explicit-IEquatable;shape.kind=validated-struct;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-class;shape.wrapped=flags-enum;text.culture=invariant;text.entry=ToString-format", "validated-struct", "nested-in-class", "none", typeof(O46.V46), typeof(global::Probes.Shapes.PFlags), null, null, [("shape.inheritance", "none")]), - new(47, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-3;shape.kind=validated-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-class;shape.wrapped=int;text.culture=de-DE;text.entry=type-converter", "validated-struct", "nested-in-class", "none", typeof(O47.V47), typeof(int), null, null, [("shape.inheritance", "none")]), - new(48, "parse-shape=abstract-string-ctor;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-3;shape.kind=record-struct;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=in-generic-type;shape.wrapped=Guid;text.culture=invariant;text.entry=type-converter", "record-struct", "nested-in-class", "none", typeof(O48.V48), typeof(global::Probes.Build.C11AbstractText), typeof(O48.VC48), typeof(global::Probes.Build.C11ConstructedConcrete), [("shape.site", "nested-in-class"), ("shape.inheritance", "none")]), - new(49, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-explicit-IEquatable;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-class;shape.wrapped=bool;text.culture=tr-TR;text.entry=TryFrom", "validated-class", "nested-in-class", "base-explicit-IEquatable", typeof(O49.V49), typeof(bool), null, null, []), - new(50, "parse-shape=ref-text;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-explicit-IEquatable;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=DateTimeOffset;text.culture=invariant;text.entry=type-converter", "sealed-record-class", "top-level", "base-explicit-IEquatable", typeof(V50), typeof(global::Probes.Build.C11ParsesByRef), typeof(VC50), typeof(global::Probes.Build.C11ParsesFromIn), []), - new(51, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-3;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-class;shape.wrapped=DateOnly;text.culture=globalization-invariant;text.entry=IParsable", "validated-class", "nested-in-class", "base-depth-3", typeof(O51.V51), typeof(global::System.DateOnly), null, null, []), - new(52, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=none;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-struct;shape.wrapped=ulong;text.culture=tr-TR;text.entry=TryFrom", "sealed-record-class", "nested-in-struct", "none", typeof(O52.V52), typeof(ulong), null, null, []), - new(53, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-3;shape.kind=validated-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-two-levels;shape.wrapped=string;text.culture=invariant;text.entry=TryFormat-short-span", "validated-class", "nested-two-levels", "base-depth-3", typeof(O53.P53.V53), typeof(string), null, null, []), - new(54, "parse-shape=abstract-string-ctor;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-3;shape.kind=validated-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-two-levels;shape.wrapped=Uri;text.culture=ar-SA;text.entry=TryFrom", "validated-class", "nested-two-levels", "base-depth-3", typeof(O54.P54.V54), typeof(global::Probes.Build.C11AbstractText), typeof(O54.P54.VC54), typeof(global::Probes.Build.C11ConstructedConcrete), []), - new(55, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-1;shape.kind=sealed-record-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-two-levels;shape.wrapped=TimeSpan;text.culture=ar-SA;text.entry=TryParse", "sealed-record-class", "nested-two-levels", "base-depth-1", typeof(O55.P55.V55), typeof(global::System.TimeSpan), null, null, []), - new(56, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-1;shape.kind=validated-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-two-levels;shape.wrapped=decimal;text.culture=globalization-invariant;text.entry=TryParse", "validated-class", "nested-two-levels", "base-depth-1", typeof(O56.P56.V56), typeof(decimal), null, null, []), - new(57, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-1;shape.kind=validated-class;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=enum;text.culture=de-DE;text.entry=ToString-format", "validated-class", "top-level", "base-depth-1", typeof(V57), typeof(global::Probes.Shapes.PEnum), null, null, []), - new(58, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=none;shape.kind=record-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=top-level;shape.wrapped=byte;text.culture=tr-TR;text.entry=TryParse", "record-struct", "top-level", "none", typeof(V58), typeof(byte), null, null, []), - new(59, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-3;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-two-levels;shape.wrapped=double;text.culture=tr-TR;text.entry=TryParse", "validated-class", "nested-two-levels", "base-depth-3", typeof(O59.P59.V59), typeof(double), null, null, []), - new(60, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=explicit-interface-member;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-class;shape.wrapped=TimeOnly;text.culture=invariant;text.entry=ToString-format", "sealed-record-class", "nested-in-class", "explicit-interface-member", typeof(O60.V60), typeof(global::System.TimeOnly), null, null, []), - new(61, "parse-shape=implicit-result;process.converter-lookup=none;shape.inheritance=base-depth-1;shape.kind=sealed-record-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-struct;shape.wrapped=string;text.culture=tr-TR;text.entry=Parse", "sealed-record-class", "nested-in-struct", "base-depth-1", typeof(O61.V61), typeof(global::Probes.Build.C11ParsesToInt), typeof(O61.VC61), typeof(global::Probes.Build.C11ParsedShape), []), - new(62, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=none;shape.kind=validated-struct;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-two-levels;shape.wrapped=DateTime;text.culture=ar-SA;text.entry=ISpanParsable", "validated-struct", "nested-two-levels", "none", typeof(O62.P62.V62), typeof(global::System.DateTime), null, null, []), - new(63, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=explicit-interface-member;shape.kind=record-struct;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=NodaTime.Instant;text.culture=tr-TR;text.entry=TryFrom", "record-struct", "top-level", "explicit-interface-member", typeof(V63), typeof(global::NodaTime.Instant), null, null, []), - new(64, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=none;shape.kind=validated-struct;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-class;shape.wrapped=enum;text.culture=tr-TR;text.entry=type-converter", "validated-struct", "nested-in-class", "none", typeof(O64.V64), typeof(global::Probes.Shapes.PEnum), null, null, []), - new(65, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-2;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=top-level;shape.wrapped=Guid;text.culture=ar-SA;text.entry=TryFormat-short-span", "validated-class", "top-level", "base-depth-2", typeof(V65), typeof(global::System.Guid), null, null, []), - new(66, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-explicit-IEquatable;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=top-level;shape.wrapped=own-record-class;text.culture=ar-SA;text.entry=IParsable", "validated-class", "top-level", "base-explicit-IEquatable", typeof(V66), typeof(global::Probes.Shapes.PRec), null, null, []), - new(67, "parse-shape=abstract-string-ctor;process.converter-lookup=none;shape.inheritance=base-depth-3;shape.kind=validated-struct;shape.refused-kind=abstract (CMTK1005);shape.site=nested-two-levels;shape.wrapped=bool;text.culture=ar-SA;text.entry=ISpanParsable", "validated-struct", "nested-two-levels", "none", typeof(O67.P67.V67), typeof(global::Probes.Build.C11AbstractText), typeof(O67.P67.VC67), typeof(global::Probes.Build.C11ConstructedConcrete), [("shape.inheritance", "none")]), - new(68, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-explicit-IEquatable;shape.kind=record-struct;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-two-levels;shape.wrapped=DateOnly;text.culture=tr-TR;text.entry=ToString-format", "record-struct", "nested-two-levels", "none", typeof(O68.P68.V68), typeof(global::System.DateOnly), null, null, [("shape.inheritance", "none")]), - new(69, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=explicit-interface-member;shape.kind=validated-struct;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-struct;shape.wrapped=long;text.culture=invariant;text.entry=TryParse", "validated-struct", "nested-in-struct", "explicit-interface-member", typeof(O69.V69), typeof(long), null, null, []), - new(70, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=explicit-interface-member;shape.kind=record-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-two-levels;shape.wrapped=DateTimeOffset;text.culture=tr-TR;text.entry=Parse", "record-struct", "nested-two-levels", "explicit-interface-member", typeof(O70.P70.V70), typeof(global::System.DateTimeOffset), null, null, []), - new(71, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=none;shape.kind=record-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-two-levels;shape.wrapped=flags-enum;text.culture=ar-SA;text.entry=TryFormat-short-span", "record-struct", "nested-two-levels", "none", typeof(O71.P71.V71), typeof(global::Probes.Shapes.PFlags), null, null, []), - new(72, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-explicit-IEquatable;shape.kind=record-struct;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-struct;shape.wrapped=double;text.culture=globalization-invariant;text.entry=ToString-format", "record-struct", "nested-in-struct", "none", typeof(O72.V72), typeof(double), null, null, [("shape.inheritance", "none")]), - new(73, "parse-shape=ref-text;process.converter-lookup=none;shape.inheritance=explicit-interface-member;shape.kind=validated-struct;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-class;shape.wrapped=byte;text.culture=de-DE;text.entry=ISpanParsable", "validated-struct", "nested-in-class", "explicit-interface-member", typeof(O73.V73), typeof(global::Probes.Build.C11ConstructedByRef), typeof(O73.VC73), typeof(global::Probes.Build.C11ParsesFromIn), []), - new(74, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=none;shape.kind=record-struct;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-class;shape.wrapped=char;text.culture=de-DE;text.entry=TryFormat-short-span", "record-struct", "nested-in-class", "none", typeof(O74.V74), typeof(char), null, null, []), - new(75, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-1;shape.kind=validated-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-struct;shape.wrapped=int;text.culture=ar-SA;text.entry=TryFormat-short-span", "validated-class", "nested-in-struct", "base-depth-1", typeof(O75.V75), typeof(int), null, null, []), - new(76, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=explicit-interface-member;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-class;shape.wrapped=Uri;text.culture=de-DE;text.entry=TryParse", "sealed-record-class", "nested-in-class", "explicit-interface-member", typeof(O76.V76), typeof(global::System.Uri), null, null, []), - new(77, "parse-shape=abstract-string-ctor;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=none;shape.kind=record-struct;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=long;text.culture=globalization-invariant;text.entry=ToString-format", "record-struct", "top-level", "none", typeof(V77), typeof(global::Probes.Build.C11AbstractText), typeof(VC77), typeof(global::Probes.Build.C11ConstructedConcrete), []), - new(78, "parse-shape=implicit-result;process.converter-lookup=none;shape.inheritance=none;shape.kind=record-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-two-levels;shape.wrapped=own-record-class;text.culture=invariant;text.entry=TryParse", "record-struct", "nested-two-levels", "none", typeof(O78.P78.V78), typeof(global::Probes.Build.C11ParsesToInt), typeof(O78.P78.VC78), typeof(global::Probes.Build.C11ParsedShape), []), - new(79, "parse-shape=implicit-result;process.converter-lookup=none;shape.inheritance=base-explicit-IEquatable;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=DateTime;text.culture=globalization-invariant;text.entry=TryFrom", "sealed-record-class", "top-level", "base-explicit-IEquatable", typeof(V79), typeof(global::Probes.Build.C11ParsesToInt), typeof(VC79), typeof(global::Probes.Build.C11ParsedShape), []), - new(80, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-1;shape.kind=validated-struct;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-class;shape.wrapped=NodaTime.Instant;text.culture=globalization-invariant;text.entry=IParsable", "validated-struct", "nested-in-class", "none", typeof(O80.V80), typeof(global::NodaTime.Instant), null, null, [("shape.inheritance", "none")]), - new(81, "parse-shape=abstract-string-ctor;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-explicit-IEquatable;shape.kind=closed-set;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-class;shape.wrapped=TimeSpan;text.culture=ar-SA;text.entry=Parse", "sealed-record-class", "nested-in-class", "base-explicit-IEquatable", typeof(O81.V81), typeof(global::Probes.Build.C11AbstractText), typeof(O81.VC81), typeof(global::Probes.Build.C11ConstructedConcrete), [("shape.kind", "sealed-record-class")]), - new(82, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=explicit-interface-member;shape.kind=record-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-two-levels;shape.wrapped=DateTime;text.culture=de-DE;text.entry=IParsable", "record-struct", "nested-two-levels", "explicit-interface-member", typeof(O82.P82.V82), typeof(global::System.DateTime), null, null, []), - new(83, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-2;shape.kind=validated-class;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=string;text.culture=ar-SA;text.entry=ToString-format", "validated-class", "top-level", "base-depth-2", typeof(V83), typeof(string), null, null, []), - new(84, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-2;shape.kind=validated-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-class;shape.wrapped=long;text.culture=tr-TR;text.entry=ISpanParsable", "validated-class", "nested-in-class", "base-depth-2", typeof(O84.V84), typeof(long), null, null, []), - new(85, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-explicit-IEquatable;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-struct;shape.wrapped=TimeOnly;text.culture=invariant;text.entry=type-converter", "sealed-record-class", "nested-in-struct", "base-explicit-IEquatable", typeof(O85.V85), typeof(global::System.TimeOnly), null, null, []), - new(86, "parse-shape=implicit-result;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-2;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-two-levels;shape.wrapped=byte;text.culture=ar-SA;text.entry=TryFrom", "sealed-record-class", "nested-two-levels", "base-depth-2", typeof(O86.P86.V86), typeof(global::Probes.Build.C11ParsesToInt), typeof(O86.P86.VC86), typeof(global::Probes.Build.C11ParsedShape), []), - new(87, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=none;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-two-levels;shape.wrapped=int;text.culture=tr-TR;text.entry=ToString-format", "sealed-record-class", "nested-two-levels", "none", typeof(O87.P87.V87), typeof(int), null, null, []), - new(88, "parse-shape=abstract-string-ctor;process.converter-lookup=none;shape.inheritance=none;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=top-level;shape.wrapped=DateOnly;text.culture=de-DE;text.entry=type-converter", "sealed-record-class", "top-level", "none", typeof(V88), typeof(global::Probes.Build.C11AbstractText), typeof(VC88), typeof(global::Probes.Build.C11ConstructedConcrete), []), - new(89, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-2;shape.kind=validated-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=top-level;shape.wrapped=ulong;text.culture=globalization-invariant;text.entry=TryParse", "validated-class", "top-level", "base-depth-2", typeof(V89), typeof(ulong), null, null, []), - new(90, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=none;shape.kind=record-struct;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=NodaTime.Instant;text.culture=ar-SA;text.entry=TryFormat-short-span", "record-struct", "top-level", "none", typeof(V90), typeof(global::NodaTime.Instant), null, null, []), - new(91, "parse-shape=implicit-result;process.converter-lookup=none;shape.inheritance=base-depth-1;shape.kind=closed-set;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-two-levels;shape.wrapped=DateTimeOffset;text.culture=de-DE;text.entry=ToString-format", "sealed-record-class", "nested-two-levels", "base-depth-1", typeof(O91.P91.V91), typeof(global::Probes.Build.C11ParsesToInt), typeof(O91.P91.VC91), typeof(global::Probes.Build.C11ParsedShape), [("shape.kind", "sealed-record-class")]), - new(92, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-2;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=in-generic-type;shape.wrapped=TimeSpan;text.culture=invariant;text.entry=type-converter", "validated-class", "nested-in-class", "base-depth-2", typeof(O92.V92), typeof(global::System.TimeSpan), null, null, [("shape.site", "nested-in-class")]), - new(93, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-3;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=top-level;shape.wrapped=decimal;text.culture=de-DE;text.entry=ISpanParsable", "sealed-record-class", "top-level", "base-depth-3", typeof(V93), typeof(decimal), null, null, []), - new(94, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-2;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-struct;shape.wrapped=Uri;text.culture=globalization-invariant;text.entry=IParsable", "sealed-record-class", "nested-in-struct", "base-depth-2", typeof(O94.V94), typeof(global::System.Uri), null, null, []), - new(95, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-3;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=top-level;shape.wrapped=char;text.culture=invariant;text.entry=TryFrom", "validated-class", "top-level", "base-depth-3", typeof(V95), typeof(char), null, null, []), - new(96, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-3;shape.kind=validated-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-struct;shape.wrapped=own-record-class;text.culture=globalization-invariant;text.entry=type-converter", "validated-class", "nested-in-struct", "base-depth-3", typeof(O96.V96), typeof(global::Probes.Shapes.PRec), null, null, []), - new(97, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-1;shape.kind=validated-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=top-level;shape.wrapped=flags-enum;text.culture=tr-TR;text.entry=TryFrom", "validated-class", "top-level", "base-depth-1", typeof(V97), typeof(global::Probes.Shapes.PFlags), null, null, []), - new(98, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-2;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=enum;text.culture=invariant;text.entry=TryParse", "sealed-record-class", "top-level", "base-depth-2", typeof(V98), typeof(global::Probes.Shapes.PEnum), null, null, []), - new(99, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=explicit-interface-member;shape.kind=validated-struct;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=string;text.culture=invariant;text.entry=TryFrom", "validated-struct", "top-level", "explicit-interface-member", typeof(V99), typeof(string), null, null, []), - new(100, "parse-shape=implicit-result;process.converter-lookup=none;shape.inheritance=base-depth-1;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=top-level;shape.wrapped=double;text.culture=de-DE;text.entry=ISpanParsable", "validated-class", "top-level", "base-depth-1", typeof(V100), typeof(global::Probes.Build.C11ParsesToInt), typeof(VC100), typeof(global::Probes.Build.C11ParsedShape), []), - new(101, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=explicit-interface-member;shape.kind=validated-struct;shape.refused-kind=over-value-object (CMTK1005);shape.site=in-generic-type;shape.wrapped=DateOnly;text.culture=tr-TR;text.entry=Parse", "validated-struct", "nested-in-class", "explicit-interface-member", typeof(O101.V101), typeof(global::System.DateOnly), null, null, [("shape.site", "nested-in-class")]), - new(102, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=explicit-interface-member;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-two-levels;shape.wrapped=bool;text.culture=invariant;text.entry=TryParse", "sealed-record-class", "nested-two-levels", "explicit-interface-member", typeof(O102.P102.V102), typeof(bool), null, null, []), - new(103, "parse-shape=implicit-result;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-1;shape.kind=validated-struct;shape.refused-kind=over-value-object (CMTK1005);shape.site=top-level;shape.wrapped=bool;text.culture=ar-SA;text.entry=ToString-format", "validated-struct", "top-level", "none", typeof(V103), typeof(global::Probes.Build.C11ParsesToInt), typeof(VC103), typeof(global::Probes.Build.C11ParsedShape), [("shape.inheritance", "none")]), - new(104, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=none;shape.kind=record-struct;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-two-levels;shape.wrapped=TimeSpan;text.culture=globalization-invariant;text.entry=TryFormat-short-span", "record-struct", "nested-two-levels", "none", typeof(O104.P104.V104), typeof(global::System.TimeSpan), null, null, []), - new(105, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=explicit-interface-member;shape.kind=record-struct;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=own-record-class;text.culture=de-DE;text.entry=TryFormat-short-span", "record-struct", "top-level", "explicit-interface-member", typeof(V105), typeof(global::Probes.Shapes.PRec), null, null, []), - new(106, "parse-shape=implicit-result;process.converter-lookup=none;shape.inheritance=base-explicit-IEquatable;shape.kind=sealed-record-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-two-levels;shape.wrapped=enum;text.culture=globalization-invariant;text.entry=ISpanParsable", "sealed-record-class", "nested-two-levels", "base-explicit-IEquatable", typeof(O106.P106.V106), typeof(global::Probes.Build.C11ParsesToInt), typeof(O106.P106.VC106), typeof(global::Probes.Build.C11ParsedShape), []), - new(107, "parse-shape=implicit-result;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=none;shape.kind=record-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-two-levels;shape.wrapped=Uri;text.culture=tr-TR;text.entry=ToString-format", "record-struct", "nested-two-levels", "none", typeof(O107.P107.V107), typeof(global::Probes.Build.C11ParsesToInt), typeof(O107.P107.VC107), typeof(global::Probes.Build.C11ParsedShape), []), - new(108, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=none;shape.kind=validated-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-two-levels;shape.wrapped=bool;text.culture=tr-TR;text.entry=TryFormat-short-span", "validated-struct", "nested-two-levels", "none", typeof(O108.P108.V108), typeof(bool), null, null, []), - new(109, "parse-shape=ref-text;process.converter-lookup=none;shape.inheritance=base-depth-2;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-class;shape.wrapped=flags-enum;text.culture=ar-SA;text.entry=ISpanParsable", "validated-class", "nested-in-class", "base-depth-2", typeof(O109.V109), typeof(global::Probes.Build.C11ConstructedByRef), typeof(O109.VC109), typeof(global::Probes.Build.C11ParsesFromIn), []), - new(110, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-explicit-IEquatable;shape.kind=sealed-record-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-two-levels;shape.wrapped=Uri;text.culture=ar-SA;text.entry=type-converter", "sealed-record-class", "nested-two-levels", "base-explicit-IEquatable", typeof(O110.P110.V110), typeof(global::System.Uri), null, null, []), - new(111, "parse-shape=implicit-result;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-explicit-IEquatable;shape.kind=record-struct;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-class;shape.wrapped=int;text.culture=de-DE;text.entry=ISpanParsable", "record-struct", "nested-in-class", "none", typeof(O111.V111), typeof(global::Probes.Build.C11ParsesToInt), typeof(O111.VC111), typeof(global::Probes.Build.C11ParsedShape), [("shape.inheritance", "none")]), - new(112, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=explicit-interface-member;shape.kind=validated-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-struct;shape.wrapped=int;text.culture=ar-SA;text.entry=TryParse", "validated-struct", "nested-in-struct", "explicit-interface-member", typeof(O112.V112), typeof(int), null, null, []), - new(113, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-3;shape.kind=sealed-record-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-struct;shape.wrapped=ulong;text.culture=globalization-invariant;text.entry=Parse", "sealed-record-class", "nested-in-struct", "base-depth-3", typeof(O113.V113), typeof(ulong), null, null, []), - new(114, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-2;shape.kind=validated-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-struct;shape.wrapped=double;text.culture=de-DE;text.entry=IParsable", "validated-class", "nested-in-struct", "base-depth-2", typeof(O114.V114), typeof(double), null, null, []), - new(115, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-1;shape.kind=validated-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-class;shape.wrapped=DateTime;text.culture=tr-TR;text.entry=TryParse", "validated-struct", "nested-in-class", "none", typeof(O115.V115), typeof(global::System.DateTime), null, null, [("shape.inheritance", "none")]), - new(116, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-2;shape.kind=closed-set;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=in-generic-type;shape.wrapped=char;text.culture=globalization-invariant;text.entry=ISpanParsable", "sealed-record-class", "nested-in-class", "base-depth-2", typeof(O116.V116), typeof(char), null, null, [("shape.kind", "sealed-record-class"), ("shape.site", "nested-in-class")]), - new(117, "parse-shape=implicit-result;process.converter-lookup=none;shape.inheritance=base-depth-1;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-class;shape.wrapped=byte;text.culture=tr-TR;text.entry=TryFormat-short-span", "sealed-record-class", "nested-in-class", "base-depth-1", typeof(O117.V117), typeof(global::Probes.Build.C11ParsesToInt), typeof(O117.VC117), typeof(global::Probes.Build.C11ParsedShape), []), - new(118, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-explicit-IEquatable;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-struct;shape.wrapped=char;text.culture=ar-SA;text.entry=Parse", "sealed-record-class", "nested-in-struct", "base-explicit-IEquatable", typeof(O118.V118), typeof(char), null, null, []), - new(119, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-1;shape.kind=sealed-record-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-two-levels;shape.wrapped=Guid;text.culture=tr-TR;text.entry=IParsable", "sealed-record-class", "nested-two-levels", "base-depth-1", typeof(O119.P119.V119), typeof(global::System.Guid), null, null, []), - new(120, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-2;shape.kind=validated-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-struct;shape.wrapped=NodaTime.Instant;text.culture=invariant;text.entry=ToString-format", "validated-class", "nested-in-struct", "base-depth-2", typeof(O120.V120), typeof(global::NodaTime.Instant), null, null, []), - new(121, "parse-shape=abstract-string-ctor;process.converter-lookup=none;shape.inheritance=base-depth-3;shape.kind=closed-set;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=top-level;shape.wrapped=TimeSpan;text.culture=invariant;text.entry=TryFrom", "sealed-record-class", "top-level", "base-depth-3", typeof(V121), typeof(global::Probes.Build.C11AbstractText), typeof(VC121), typeof(global::Probes.Build.C11ConstructedConcrete), [("shape.kind", "sealed-record-class")]), - new(122, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-3;shape.kind=record-struct;shape.refused-kind=abstract (CMTK1005);shape.site=in-generic-type;shape.wrapped=long;text.culture=globalization-invariant;text.entry=type-converter", "record-struct", "nested-in-class", "none", typeof(O122.V122), typeof(long), null, null, [("shape.site", "nested-in-class"), ("shape.inheritance", "none")]), - new(123, "parse-shape=ref-text;process.converter-lookup=none;shape.inheritance=explicit-interface-member;shape.kind=closed-set;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-struct;shape.wrapped=DateOnly;text.culture=de-DE;text.entry=TryFrom", "sealed-record-class", "nested-in-struct", "explicit-interface-member", typeof(O123.V123), typeof(global::Probes.Build.C11ConstructedByRef), typeof(O123.VC123), typeof(global::Probes.Build.C11ParsesFromIn), [("shape.kind", "sealed-record-class")]), - new(124, "parse-shape=ref-text;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-3;shape.kind=validated-struct;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-struct;shape.wrapped=DateTimeOffset;text.culture=tr-TR;text.entry=TryFrom", "validated-struct", "nested-in-struct", "none", typeof(O124.V124), typeof(global::Probes.Build.C11ParsesByRef), typeof(O124.VC124), typeof(global::Probes.Build.C11ParsesFromIn), [("shape.inheritance", "none")]), - new(125, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=explicit-interface-member;shape.kind=validated-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-class;shape.wrapped=Uri;text.culture=tr-TR;text.entry=TryFormat-short-span", "validated-struct", "nested-in-class", "explicit-interface-member", typeof(O125.V125), typeof(global::System.Uri), null, null, []), - new(126, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-explicit-IEquatable;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-two-levels;shape.wrapped=decimal;text.culture=de-DE;text.entry=Parse", "validated-class", "nested-two-levels", "base-explicit-IEquatable", typeof(O126.P126.V126), typeof(decimal), null, null, []), - new(127, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-explicit-IEquatable;shape.kind=closed-set;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-class;shape.wrapped=NodaTime.Instant;text.culture=ar-SA;text.entry=Parse", "sealed-record-class", "nested-in-class", "base-explicit-IEquatable", typeof(O127.V127), typeof(global::NodaTime.Instant), null, null, [("shape.kind", "sealed-record-class")]), - new(128, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-explicit-IEquatable;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-class;shape.wrapped=DateTime;text.culture=de-DE;text.entry=type-converter", "validated-class", "nested-in-class", "base-explicit-IEquatable", typeof(O128.V128), typeof(global::System.DateTime), null, null, []), - new(129, "parse-shape=ref-text;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-1;shape.kind=validated-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=top-level;shape.wrapped=own-record-class;text.culture=ar-SA;text.entry=TryFrom", "validated-struct", "top-level", "none", typeof(V129), typeof(global::Probes.Build.C11ConstructedByRef), typeof(VC129), typeof(global::Probes.Build.C11ParsesFromIn), [("shape.inheritance", "none")]), - new(130, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=explicit-interface-member;shape.kind=record-struct;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-class;shape.wrapped=char;text.culture=globalization-invariant;text.entry=ToString-format", "record-struct", "nested-in-class", "explicit-interface-member", typeof(O130.V130), typeof(char), null, null, []), - new(131, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=explicit-interface-member;shape.kind=record-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-struct;shape.wrapped=long;text.culture=globalization-invariant;text.entry=TryFormat-short-span", "record-struct", "nested-in-struct", "explicit-interface-member", typeof(O131.V131), typeof(long), null, null, []), - new(132, "parse-shape=abstract-string-ctor;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=explicit-interface-member;shape.kind=validated-struct;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=double;text.culture=invariant;text.entry=Parse", "validated-struct", "top-level", "explicit-interface-member", typeof(V132), typeof(global::Probes.Build.C11AbstractText), typeof(VC132), typeof(global::Probes.Build.C11ConstructedConcrete), []), - new(133, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-2;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-class;shape.wrapped=own-record-class;text.culture=de-DE;text.entry=Parse", "sealed-record-class", "nested-in-class", "base-depth-2", typeof(O133.V133), typeof(global::Probes.Shapes.PRec), null, null, []), - new(134, "parse-shape=ref-text;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-2;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=top-level;shape.wrapped=NodaTime.Instant;text.culture=invariant;text.entry=ISpanParsable", "validated-class", "top-level", "base-depth-2", typeof(V134), typeof(global::Probes.Build.C11ParsesByRef), typeof(VC134), typeof(global::Probes.Build.C11ParsesFromIn), []), - new(135, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-3;shape.kind=closed-set;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-struct;shape.wrapped=TimeSpan;text.culture=globalization-invariant;text.entry=ToString-format", "sealed-record-class", "nested-in-struct", "base-depth-3", typeof(O135.V135), typeof(global::System.TimeSpan), null, null, [("shape.kind", "sealed-record-class")]), - new(136, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=explicit-interface-member;shape.kind=record-struct;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-struct;shape.wrapped=TimeOnly;text.culture=de-DE;text.entry=TryParse", "record-struct", "nested-in-struct", "explicit-interface-member", typeof(O136.V136), typeof(global::System.TimeOnly), null, null, []), - new(137, "parse-shape=implicit-result;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-1;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-class;shape.wrapped=byte;text.culture=ar-SA;text.entry=IParsable", "sealed-record-class", "nested-in-class", "base-depth-1", typeof(O137.V137), typeof(global::Probes.Build.C11ParsesToInt), typeof(O137.VC137), typeof(global::Probes.Build.C11ParsedShape), []), - new(138, "parse-shape=abstract-string-ctor;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=none;shape.kind=record-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-class;shape.wrapped=TimeOnly;text.culture=invariant;text.entry=ISpanParsable", "record-struct", "nested-in-class", "none", typeof(O138.V138), typeof(global::Probes.Build.C11AbstractText), typeof(O138.VC138), typeof(global::Probes.Build.C11ConstructedConcrete), []), - new(139, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-explicit-IEquatable;shape.kind=closed-set;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-two-levels;shape.wrapped=flags-enum;text.culture=de-DE;text.entry=TryParse", "sealed-record-class", "nested-two-levels", "base-explicit-IEquatable", typeof(O139.P139.V139), typeof(global::Probes.Shapes.PFlags), null, null, [("shape.kind", "sealed-record-class")]), - new(140, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-2;shape.kind=sealed-record-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-class;shape.wrapped=bool;text.culture=globalization-invariant;text.entry=Parse", "sealed-record-class", "nested-in-class", "base-depth-2", typeof(O140.V140), typeof(bool), null, null, []), - new(141, "parse-shape=abstract-string-ctor;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=explicit-interface-member;shape.kind=record-struct;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=in-generic-type;shape.wrapped=DateTimeOffset;text.culture=de-DE;text.entry=IParsable", "record-struct", "nested-in-class", "explicit-interface-member", typeof(O141.V141), typeof(global::Probes.Build.C11AbstractText), typeof(O141.VC141), typeof(global::Probes.Build.C11ConstructedConcrete), [("shape.site", "nested-in-class")]), - new(142, "parse-shape=implicit-result;process.converter-lookup=none;shape.inheritance=none;shape.kind=validated-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=in-generic-type;shape.wrapped=TimeOnly;text.culture=invariant;text.entry=IParsable", "validated-struct", "nested-in-class", "none", typeof(O142.V142), typeof(global::Probes.Build.C11ParsesToInt), typeof(O142.VC142), typeof(global::Probes.Build.C11ParsedShape), [("shape.site", "nested-in-class")]), - new(143, "parse-shape=ref-text;process.converter-lookup=none;shape.inheritance=none;shape.kind=closed-set;shape.refused-kind=over-value-object (CMTK1005);shape.site=top-level;shape.wrapped=byte;text.culture=tr-TR;text.entry=type-converter", "sealed-record-class", "top-level", "none", typeof(V143), typeof(global::Probes.Build.C11ConstructedByRef), typeof(VC143), typeof(global::Probes.Build.C11ParsesFromIn), [("shape.kind", "sealed-record-class")]), - new(144, "parse-shape=abstract-string-ctor;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=explicit-interface-member;shape.kind=record-struct;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-two-levels;shape.wrapped=enum;text.culture=de-DE;text.entry=TryFrom", "record-struct", "nested-two-levels", "explicit-interface-member", typeof(O144.P144.V144), typeof(global::Probes.Build.C11AbstractText), typeof(O144.P144.VC144), typeof(global::Probes.Build.C11ConstructedConcrete), []), - new(145, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=none;shape.kind=validated-struct;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-two-levels;shape.wrapped=ulong;text.culture=de-DE;text.entry=ToString-format", "validated-struct", "nested-two-levels", "none", typeof(O145.P145.V145), typeof(ulong), null, null, []), - new(146, "parse-shape=implicit-result;process.converter-lookup=none;shape.inheritance=none;shape.kind=validated-struct;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-class;shape.wrapped=int;text.culture=de-DE;text.entry=Parse", "validated-struct", "nested-in-class", "none", typeof(O146.V146), typeof(global::Probes.Build.C11ParsesToInt), typeof(O146.VC146), typeof(global::Probes.Build.C11ParsedShape), []), - new(147, "parse-shape=abstract-string-ctor;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=explicit-interface-member;shape.kind=validated-struct;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-two-levels;shape.wrapped=Guid;text.culture=ar-SA;text.entry=ISpanParsable", "validated-struct", "nested-two-levels", "explicit-interface-member", typeof(O147.P147.V147), typeof(global::Probes.Build.C11AbstractText), typeof(O147.P147.VC147), typeof(global::Probes.Build.C11ConstructedConcrete), []), - new(148, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=none;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=in-generic-type;shape.wrapped=enum;text.culture=globalization-invariant;text.entry=IParsable", "validated-class", "nested-in-class", "none", typeof(O148.V148), typeof(global::Probes.Shapes.PEnum), null, null, [("shape.site", "nested-in-class")]), - new(149, "parse-shape=implicit-result;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=explicit-interface-member;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-struct;shape.wrapped=DateTime;text.culture=invariant;text.entry=TryFormat-short-span", "sealed-record-class", "nested-in-struct", "explicit-interface-member", typeof(O149.V149), typeof(global::Probes.Build.C11ParsesToInt), typeof(O149.VC149), typeof(global::Probes.Build.C11ParsedShape), []), - new(150, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-3;shape.kind=validated-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-two-levels;shape.wrapped=decimal;text.culture=de-DE;text.entry=TryFrom", "validated-class", "nested-two-levels", "base-depth-3", typeof(O150.P150.V150), typeof(decimal), null, null, []), - new(151, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=none;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-class;shape.wrapped=ulong;text.culture=invariant;text.entry=IParsable", "validated-class", "nested-in-class", "none", typeof(O151.V151), typeof(ulong), null, null, []), - new(152, "parse-shape=abstract-string-ctor;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=explicit-interface-member;shape.kind=sealed-record-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-class;shape.wrapped=double;text.culture=ar-SA;text.entry=type-converter", "sealed-record-class", "nested-in-class", "explicit-interface-member", typeof(O152.V152), typeof(global::Probes.Build.C11AbstractText), typeof(O152.VC152), typeof(global::Probes.Build.C11ConstructedConcrete), []), - new(153, "parse-shape=implicit-result;process.converter-lookup=none;shape.inheritance=none;shape.kind=closed-set;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=in-generic-type;shape.wrapped=Guid;text.culture=ar-SA;text.entry=TryFrom", "sealed-record-class", "nested-in-class", "none", typeof(O153.V153), typeof(global::Probes.Build.C11ParsesToInt), typeof(O153.VC153), typeof(global::Probes.Build.C11ParsedShape), [("shape.kind", "sealed-record-class"), ("shape.site", "nested-in-class")]), - new(154, "parse-shape=implicit-result;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-explicit-IEquatable;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-two-levels;shape.wrapped=long;text.culture=invariant;text.entry=Parse", "sealed-record-class", "nested-two-levels", "base-explicit-IEquatable", typeof(O154.P154.V154), typeof(global::Probes.Build.C11ParsesToInt), typeof(O154.P154.VC154), typeof(global::Probes.Build.C11ParsedShape), []), - new(155, "parse-shape=ref-text;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=none;shape.kind=closed-set;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=top-level;shape.wrapped=string;text.culture=globalization-invariant;text.entry=type-converter", "sealed-record-class", "top-level", "none", typeof(V155), typeof(global::Probes.Build.C11ConstructedByRef), typeof(VC155), typeof(global::Probes.Build.C11ParsesFromIn), [("shape.kind", "sealed-record-class")]), - new(156, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=explicit-interface-member;shape.kind=validated-struct;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=DateTimeOffset;text.culture=de-DE;text.entry=TryParse", "validated-struct", "top-level", "explicit-interface-member", typeof(V156), typeof(global::System.DateTimeOffset), null, null, []), - new(157, "parse-shape=implicit-result;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-explicit-IEquatable;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-two-levels;shape.wrapped=DateOnly;text.culture=globalization-invariant;text.entry=TryFormat-short-span", "validated-class", "nested-two-levels", "base-explicit-IEquatable", typeof(O157.P157.V157), typeof(global::Probes.Build.C11ParsesToInt), typeof(O157.P157.VC157), typeof(global::Probes.Build.C11ParsedShape), []), - new(158, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-3;shape.kind=validated-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-two-levels;shape.wrapped=string;text.culture=invariant;text.entry=IParsable", "validated-class", "nested-two-levels", "base-depth-3", typeof(O158.P158.V158), typeof(string), null, null, []), - new(159, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-2;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-two-levels;shape.wrapped=flags-enum;text.culture=globalization-invariant;text.entry=IParsable", "sealed-record-class", "nested-two-levels", "base-depth-2", typeof(O159.P159.V159), typeof(global::Probes.Shapes.PFlags), null, null, []), - new(160, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=explicit-interface-member;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-struct;shape.wrapped=decimal;text.culture=invariant;text.entry=type-converter", "sealed-record-class", "nested-in-struct", "explicit-interface-member", typeof(O160.V160), typeof(decimal), null, null, []), - new(161, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=none;shape.kind=record-struct;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-class;shape.wrapped=Guid;text.culture=invariant;text.entry=type-converter", "record-struct", "nested-in-class", "none", typeof(O161.V161), typeof(global::System.Guid), null, null, []), - new(162, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-3;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-struct;shape.wrapped=DateTime;text.culture=invariant;text.entry=TryFormat-short-span", "sealed-record-class", "nested-in-struct", "base-depth-3", typeof(O162.V162), typeof(global::System.DateTime), null, null, []), - new(163, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-explicit-IEquatable;shape.kind=sealed-record-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-struct;shape.wrapped=DateTimeOffset;text.culture=invariant;text.entry=ToString-format", "sealed-record-class", "nested-in-struct", "base-explicit-IEquatable", typeof(O163.V163), typeof(global::System.DateTimeOffset), null, null, []), - new(164, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=explicit-interface-member;shape.kind=validated-struct;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-struct;shape.wrapped=byte;text.culture=ar-SA;text.entry=IParsable", "validated-struct", "nested-in-struct", "explicit-interface-member", typeof(O164.V164), typeof(byte), null, null, []), - new(165, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-explicit-IEquatable;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=top-level;shape.wrapped=DateOnly;text.culture=de-DE;text.entry=ToString-format", "sealed-record-class", "top-level", "base-explicit-IEquatable", typeof(V165), typeof(global::System.DateOnly), null, null, []), - new(166, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=none;shape.kind=record-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-class;shape.wrapped=decimal;text.culture=ar-SA;text.entry=ToString-format", "record-struct", "nested-in-class", "none", typeof(O166.V166), typeof(decimal), null, null, []), - new(167, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=none;shape.kind=record-struct;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-class;shape.wrapped=string;text.culture=de-DE;text.entry=Parse", "record-struct", "nested-in-class", "none", typeof(O167.V167), typeof(string), null, null, []), - new(168, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-1;shape.kind=record-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-class;shape.wrapped=ulong;text.culture=ar-SA;text.entry=TryFormat-short-span", "record-struct", "nested-in-class", "none", typeof(O168.V168), typeof(ulong), null, null, [("shape.inheritance", "none")]), - new(169, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=explicit-interface-member;shape.kind=record-struct;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-struct;shape.wrapped=bool;text.culture=de-DE;text.entry=ISpanParsable", "record-struct", "nested-in-struct", "explicit-interface-member", typeof(O169.V169), typeof(bool), null, null, []), - new(170, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=none;shape.kind=validated-struct;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-two-levels;shape.wrapped=own-record-class;text.culture=invariant;text.entry=ToString-format", "validated-struct", "nested-two-levels", "none", typeof(O170.P170.V170), typeof(global::Probes.Shapes.PRec), null, null, []), - new(171, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=none;shape.kind=validated-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-class;shape.wrapped=byte;text.culture=globalization-invariant;text.entry=ISpanParsable", "validated-class", "nested-in-class", "none", typeof(O171.V171), typeof(byte), null, null, []), - new(172, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-2;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-two-levels;shape.wrapped=TimeOnly;text.culture=ar-SA;text.entry=Parse", "sealed-record-class", "nested-two-levels", "base-depth-2", typeof(O172.P172.V172), typeof(global::System.TimeOnly), null, null, []), - new(173, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-3;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=top-level;shape.wrapped=TimeOnly;text.culture=tr-TR;text.entry=TryFormat-short-span", "validated-class", "top-level", "base-depth-3", typeof(V173), typeof(global::System.TimeOnly), null, null, []), - new(174, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-3;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-class;shape.wrapped=Uri;text.culture=invariant;text.entry=ISpanParsable", "validated-class", "nested-in-class", "base-depth-3", typeof(O174.V174), typeof(global::System.Uri), null, null, []), - new(175, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-1;shape.kind=validated-struct;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-struct;shape.wrapped=double;text.culture=ar-SA;text.entry=ISpanParsable", "validated-struct", "nested-in-struct", "none", typeof(O175.V175), typeof(double), null, null, [("shape.inheritance", "none")]), - new(176, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-explicit-IEquatable;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-struct;shape.wrapped=string;text.culture=tr-TR;text.entry=ToString-format", "sealed-record-class", "nested-in-struct", "base-explicit-IEquatable", typeof(O176.V176), typeof(string), null, null, []), - new(177, "parse-shape=ref-text;process.converter-lookup=none;shape.inheritance=base-depth-3;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-class;shape.wrapped=DateTimeOffset;text.culture=globalization-invariant;text.entry=TryFrom", "validated-class", "nested-in-class", "base-depth-3", typeof(O177.V177), typeof(global::Probes.Build.C11ConstructedByRef), typeof(O177.VC177), typeof(global::Probes.Build.C11ParsesFromIn), []), - new(178, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=explicit-interface-member;shape.kind=validated-struct;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-struct;shape.wrapped=Guid;text.culture=tr-TR;text.entry=ToString-format", "validated-struct", "nested-in-struct", "explicit-interface-member", typeof(O178.V178), typeof(global::System.Guid), null, null, []), - new(179, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=none;shape.kind=validated-struct;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-class;shape.wrapped=TimeOnly;text.culture=globalization-invariant;text.entry=TryFrom", "validated-struct", "nested-in-class", "none", typeof(O179.V179), typeof(global::System.TimeOnly), null, null, []), - new(180, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=explicit-interface-member;shape.kind=record-struct;shape.refused-kind=abstract (CMTK1005);shape.site=nested-two-levels;shape.wrapped=enum;text.culture=globalization-invariant;text.entry=ISpanParsable", "record-struct", "nested-two-levels", "explicit-interface-member", typeof(O180.P180.V180), typeof(global::Probes.Shapes.PEnum), null, null, []), - new(181, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=explicit-interface-member;shape.kind=validated-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=top-level;shape.wrapped=TimeSpan;text.culture=tr-TR;text.entry=IParsable", "validated-struct", "top-level", "explicit-interface-member", typeof(V181), typeof(global::System.TimeSpan), null, null, []), - new(182, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-2;shape.kind=validated-struct;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-class;shape.wrapped=decimal;text.culture=tr-TR;text.entry=TryFormat-short-span", "validated-struct", "nested-in-class", "none", typeof(O182.V182), typeof(decimal), null, null, [("shape.inheritance", "none")]), - new(183, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-3;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-struct;shape.wrapped=flags-enum;text.culture=invariant;text.entry=ISpanParsable", "validated-class", "nested-in-struct", "base-depth-3", typeof(O183.V183), typeof(global::Probes.Shapes.PFlags), null, null, []), - new(184, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-1;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=top-level;shape.wrapped=DateTime;text.culture=globalization-invariant;text.entry=type-converter", "validated-class", "top-level", "base-depth-1", typeof(V184), typeof(global::System.DateTime), null, null, []), - new(185, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-3;shape.kind=record-struct;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-two-levels;shape.wrapped=bool;text.culture=ar-SA;text.entry=IParsable", "record-struct", "nested-two-levels", "none", typeof(O185.P185.V185), typeof(bool), null, null, [("shape.inheritance", "none")]), - new(186, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-3;shape.kind=sealed-record-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=top-level;shape.wrapped=long;text.culture=globalization-invariant;text.entry=ToString-format", "sealed-record-class", "top-level", "base-depth-3", typeof(V186), typeof(long), null, null, []), - new(187, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-1;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=in-generic-type;shape.wrapped=DateOnly;text.culture=invariant;text.entry=TryFrom", "validated-class", "nested-in-class", "base-depth-1", typeof(O187.V187), typeof(global::System.DateOnly), null, null, [("shape.site", "nested-in-class")]), - new(188, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=explicit-interface-member;shape.kind=record-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-two-levels;shape.wrapped=int;text.culture=invariant;text.entry=Parse", "record-struct", "nested-two-levels", "explicit-interface-member", typeof(O188.P188.V188), typeof(int), null, null, []), - new(189, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=explicit-interface-member;shape.kind=validated-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-two-levels;shape.wrapped=NodaTime.Instant;text.culture=invariant;text.entry=ISpanParsable", "validated-class", "nested-two-levels", "explicit-interface-member", typeof(O189.P189.V189), typeof(global::NodaTime.Instant), null, null, []), - new(190, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-3;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-struct;shape.wrapped=Guid;text.culture=de-DE;text.entry=TryFrom", "sealed-record-class", "nested-in-struct", "base-depth-3", typeof(O190.V190), typeof(global::System.Guid), null, null, []), - new(191, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=explicit-interface-member;shape.kind=sealed-record-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-two-levels;shape.wrapped=string;text.culture=globalization-invariant;text.entry=TryParse", "sealed-record-class", "nested-two-levels", "explicit-interface-member", typeof(O191.P191.V191), typeof(string), null, null, []), - new(192, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-explicit-IEquatable;shape.kind=validated-class;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=ulong;text.culture=globalization-invariant;text.entry=type-converter", "validated-class", "top-level", "base-explicit-IEquatable", typeof(V192), typeof(ulong), null, null, []), - new(193, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-2;shape.kind=sealed-record-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=top-level;shape.wrapped=DateOnly;text.culture=globalization-invariant;text.entry=TryParse", "sealed-record-class", "top-level", "base-depth-2", typeof(V193), typeof(global::System.DateOnly), null, null, []), - new(194, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-2;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-class;shape.wrapped=byte;text.culture=de-DE;text.entry=TryFrom", "validated-class", "nested-in-class", "base-depth-2", typeof(O194.V194), typeof(byte), null, null, []), - new(195, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=explicit-interface-member;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=top-level;shape.wrapped=flags-enum;text.culture=globalization-invariant;text.entry=Parse", "sealed-record-class", "top-level", "explicit-interface-member", typeof(V195), typeof(global::Probes.Shapes.PFlags), null, null, []), - new(196, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-explicit-IEquatable;shape.kind=validated-class;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=double;text.culture=ar-SA;text.entry=type-converter", "validated-class", "top-level", "base-explicit-IEquatable", typeof(V196), typeof(double), null, null, []), - new(197, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-2;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-class;shape.wrapped=DateTime;text.culture=invariant;text.entry=TryFrom", "sealed-record-class", "nested-in-class", "base-depth-2", typeof(O197.V197), typeof(global::System.DateTime), null, null, []), - new(198, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-1;shape.kind=validated-struct;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-struct;shape.wrapped=byte;text.culture=invariant;text.entry=type-converter", "validated-struct", "nested-in-struct", "none", typeof(O198.V198), typeof(byte), null, null, [("shape.inheritance", "none")]), - new(199, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=explicit-interface-member;shape.kind=validated-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-two-levels;shape.wrapped=char;text.culture=invariant;text.entry=IParsable", "validated-struct", "nested-two-levels", "explicit-interface-member", typeof(O199.P199.V199), typeof(char), null, null, []), - new(200, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-explicit-IEquatable;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-struct;shape.wrapped=enum;text.culture=ar-SA;text.entry=TryFormat-short-span", "sealed-record-class", "nested-in-struct", "base-explicit-IEquatable", typeof(O200.V200), typeof(global::Probes.Shapes.PEnum), null, null, []), - new(201, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-3;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-struct;shape.wrapped=Guid;text.culture=globalization-invariant;text.entry=ISpanParsable", "sealed-record-class", "nested-in-struct", "base-depth-3", typeof(O201.V201), typeof(global::System.Guid), null, null, []), - new(202, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-2;shape.kind=sealed-record-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-struct;shape.wrapped=DateTimeOffset;text.culture=de-DE;text.entry=IParsable", "sealed-record-class", "nested-in-struct", "base-depth-2", typeof(O202.V202), typeof(global::System.DateTimeOffset), null, null, []), - new(203, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-1;shape.kind=sealed-record-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-two-levels;shape.wrapped=char;text.culture=invariant;text.entry=TryParse", "sealed-record-class", "nested-two-levels", "base-depth-1", typeof(O203.P203.V203), typeof(char), null, null, []), - new(204, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-1;shape.kind=validated-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-struct;shape.wrapped=TimeOnly;text.culture=invariant;text.entry=ISpanParsable", "validated-class", "nested-in-struct", "base-depth-1", typeof(O204.V204), typeof(global::System.TimeOnly), null, null, []), - new(205, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-1;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-struct;shape.wrapped=string;text.culture=tr-TR;text.entry=ISpanParsable", "sealed-record-class", "nested-in-struct", "base-depth-1", typeof(O205.V205), typeof(string), null, null, []), - new(206, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-1;shape.kind=validated-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-two-levels;shape.wrapped=Uri;text.culture=tr-TR;text.entry=TryFrom", "validated-class", "nested-two-levels", "base-depth-1", typeof(O206.P206.V206), typeof(global::System.Uri), null, null, []), - new(207, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-1;shape.kind=sealed-record-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=top-level;shape.wrapped=DateTimeOffset;text.culture=ar-SA;text.entry=ISpanParsable", "sealed-record-class", "top-level", "base-depth-1", typeof(V207), typeof(global::System.DateTimeOffset), null, null, []), - new(208, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-2;shape.kind=validated-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=top-level;shape.wrapped=long;text.culture=ar-SA;text.entry=Parse", "validated-class", "top-level", "base-depth-2", typeof(V208), typeof(long), null, null, []), - new(209, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=explicit-interface-member;shape.kind=validated-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-two-levels;shape.wrapped=ulong;text.culture=de-DE;text.entry=ISpanParsable", "validated-class", "nested-two-levels", "explicit-interface-member", typeof(O209.P209.V209), typeof(ulong), null, null, []), - new(210, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-1;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-class;shape.wrapped=bool;text.culture=invariant;text.entry=ToString-format", "validated-class", "nested-in-class", "base-depth-1", typeof(O210.V210), typeof(bool), null, null, []), - new(211, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-explicit-IEquatable;shape.kind=validated-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=top-level;shape.wrapped=TimeSpan;text.culture=tr-TR;text.entry=Parse", "validated-class", "top-level", "base-explicit-IEquatable", typeof(V211), typeof(global::System.TimeSpan), null, null, []), - new(212, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-3;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-class;shape.wrapped=int;text.culture=globalization-invariant;text.entry=TryFrom", "validated-class", "nested-in-class", "base-depth-3", typeof(O212.V212), typeof(int), null, null, []), - new(213, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-explicit-IEquatable;shape.kind=sealed-record-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=in-generic-type;shape.wrapped=int;text.culture=invariant;text.entry=ToString-format", "sealed-record-class", "nested-in-class", "base-explicit-IEquatable", typeof(O213.V213), typeof(int), null, null, [("shape.site", "nested-in-class")]), - new(214, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-1;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=top-level;shape.wrapped=decimal;text.culture=tr-TR;text.entry=IParsable", "validated-class", "top-level", "base-depth-1", typeof(V214), typeof(decimal), null, null, []), - new(215, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-explicit-IEquatable;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-two-levels;shape.wrapped=byte;text.culture=de-DE;text.entry=TryFormat-short-span", "validated-class", "nested-two-levels", "base-explicit-IEquatable", typeof(O215.P215.V215), typeof(byte), null, null, []), - new(216, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-1;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-two-levels;shape.wrapped=own-record-class;text.culture=ar-SA;text.entry=TryParse", "validated-class", "nested-two-levels", "base-depth-1", typeof(O216.P216.V216), typeof(global::Probes.Shapes.PRec), null, null, []), - new(217, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=none;shape.kind=validated-struct;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-struct;shape.wrapped=int;text.culture=invariant;text.entry=ISpanParsable", "validated-struct", "nested-in-struct", "none", typeof(O217.V217), typeof(int), null, null, []), - new(218, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-explicit-IEquatable;shape.kind=sealed-record-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=in-generic-type;shape.wrapped=string;text.culture=globalization-invariant;text.entry=type-converter", "sealed-record-class", "nested-in-class", "base-explicit-IEquatable", typeof(O218.V218), typeof(string), null, null, [("shape.site", "nested-in-class")]), - new(219, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-2;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-two-levels;shape.wrapped=Guid;text.culture=invariant;text.entry=Parse", "validated-class", "nested-two-levels", "base-depth-2", typeof(O219.P219.V219), typeof(global::System.Guid), null, null, []), - new(220, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-1;shape.kind=sealed-record-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-two-levels;shape.wrapped=flags-enum;text.culture=globalization-invariant;text.entry=type-converter", "sealed-record-class", "nested-two-levels", "base-depth-1", typeof(O220.P220.V220), typeof(global::Probes.Shapes.PFlags), null, null, []), - new(221, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=none;shape.kind=sealed-record-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-struct;shape.wrapped=byte;text.culture=invariant;text.entry=Parse", "sealed-record-class", "nested-in-struct", "none", typeof(O221.V221), typeof(byte), null, null, []), - new(222, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-1;shape.kind=sealed-record-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-struct;shape.wrapped=DateTime;text.culture=tr-TR;text.entry=Parse", "sealed-record-class", "nested-in-struct", "base-depth-1", typeof(O222.V222), typeof(global::System.DateTime), null, null, []), - new(223, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=none;shape.kind=sealed-record-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=top-level;shape.wrapped=own-record-class;text.culture=tr-TR;text.entry=TryFrom", "sealed-record-class", "top-level", "none", typeof(V223), typeof(global::Probes.Shapes.PRec), null, null, []), - new(224, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-1;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-struct;shape.wrapped=NodaTime.Instant;text.culture=tr-TR;text.entry=type-converter", "validated-class", "nested-in-struct", "base-depth-1", typeof(O224.V224), typeof(global::NodaTime.Instant), null, null, []), - new(225, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-2;shape.kind=record-struct;shape.refused-kind=abstract (CMTK1005);shape.site=nested-two-levels;shape.wrapped=DateOnly;text.culture=de-DE;text.entry=type-converter", "record-struct", "nested-two-levels", "none", typeof(O225.P225.V225), typeof(global::System.DateOnly), null, null, [("shape.inheritance", "none")]), - new(226, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-3;shape.kind=validated-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-two-levels;shape.wrapped=DateTimeOffset;text.culture=invariant;text.entry=type-converter", "validated-struct", "nested-two-levels", "none", typeof(O226.P226.V226), typeof(global::System.DateTimeOffset), null, null, [("shape.inheritance", "none")]), - new(227, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-3;shape.kind=sealed-record-class;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-class;shape.wrapped=TimeOnly;text.culture=ar-SA;text.entry=IParsable", "sealed-record-class", "nested-in-class", "base-depth-3", typeof(O227.V227), typeof(global::System.TimeOnly), null, null, []), - new(228, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-explicit-IEquatable;shape.kind=validated-struct;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-class;shape.wrapped=TimeSpan;text.culture=globalization-invariant;text.entry=TryFrom", "validated-struct", "nested-in-class", "none", typeof(O228.V228), typeof(global::System.TimeSpan), null, null, [("shape.inheritance", "none")]), - new(229, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-explicit-IEquatable;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-two-levels;shape.wrapped=DateTime;text.culture=tr-TR;text.entry=ToString-format", "validated-class", "nested-two-levels", "base-explicit-IEquatable", typeof(O229.P229.V229), typeof(global::System.DateTime), null, null, []), - new(230, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-explicit-IEquatable;shape.kind=validated-class;shape.refused-kind=generic-wrapped (CMTK1005);shape.site=nested-in-struct;shape.wrapped=double;text.culture=globalization-invariant;text.entry=TryFormat-short-span", "validated-class", "nested-in-struct", "base-explicit-IEquatable", typeof(O230.V230), typeof(double), null, null, []), - new(231, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=explicit-interface-member;shape.kind=validated-struct;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-class;shape.wrapped=Uri;text.culture=ar-SA;text.entry=ToString-format", "validated-struct", "nested-in-class", "explicit-interface-member", typeof(O231.V231), typeof(global::System.Uri), null, null, []), - new(232, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=explicit-interface-member;shape.kind=record-struct;shape.refused-kind=over-value-object (CMTK1005);shape.site=nested-in-struct;shape.wrapped=double;text.culture=ar-SA;text.entry=Parse", "record-struct", "nested-in-struct", "explicit-interface-member", typeof(O232.V232), typeof(double), null, null, []), - new(233, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-3;shape.kind=record-struct;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-two-levels;shape.wrapped=DateOnly;text.culture=tr-TR;text.entry=TryFormat-short-span", "record-struct", "nested-two-levels", "none", typeof(O233.P233.V233), typeof(global::System.DateOnly), null, null, [("shape.inheritance", "none")]), - new(234, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=none;shape.kind=record-struct;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=top-level;shape.wrapped=enum;text.culture=invariant;text.entry=TryFrom", "record-struct", "top-level", "none", typeof(V234), typeof(global::Probes.Shapes.PEnum), null, null, []), - new(235, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-3;shape.kind=validated-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=top-level;shape.wrapped=DateTimeOffset;text.culture=globalization-invariant;text.entry=TryFrom", "validated-class", "top-level", "base-depth-3", typeof(V235), typeof(global::System.DateTimeOffset), null, null, []), - new(236, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-3;shape.kind=validated-class;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=bool;text.culture=de-DE;text.entry=Parse", "validated-class", "top-level", "base-depth-3", typeof(V236), typeof(bool), null, null, []), - new(237, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-2;shape.kind=sealed-record-class;shape.refused-kind=unsealed-record-class (CMTK1006);shape.site=nested-in-class;shape.wrapped=decimal;text.culture=tr-TR;text.entry=TryFormat-short-span", "sealed-record-class", "nested-in-class", "base-depth-2", typeof(O237.V237), typeof(decimal), null, null, []), - new(238, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-1;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-two-levels;shape.wrapped=ulong;text.culture=de-DE;text.entry=TryFormat-short-span", "sealed-record-class", "nested-two-levels", "base-depth-1", typeof(O238.P238.V238), typeof(ulong), null, null, []), - new(239, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-1;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=double;text.culture=de-DE;text.entry=IParsable", "sealed-record-class", "top-level", "base-depth-1", typeof(V239), typeof(double), null, null, []), - new(240, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-1;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=top-level;shape.wrapped=byte;text.culture=ar-SA;text.entry=TryFrom", "sealed-record-class", "top-level", "base-depth-1", typeof(V240), typeof(byte), null, null, []), - new(241, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=explicit-interface-member;shape.kind=validated-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-two-levels;shape.wrapped=enum;text.culture=de-DE;text.entry=IParsable", "validated-class", "nested-two-levels", "explicit-interface-member", typeof(O241.P241.V241), typeof(global::Probes.Shapes.PEnum), null, null, []), - new(242, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-3;shape.kind=sealed-record-class;shape.refused-kind=abstract (CMTK1005);shape.site=nested-in-struct;shape.wrapped=flags-enum;text.culture=ar-SA;text.entry=TryFrom", "sealed-record-class", "nested-in-struct", "base-depth-3", typeof(O242.V242), typeof(global::Probes.Shapes.PFlags), null, null, []), - new(243, "parse-shape=ordinary;process.converter-lookup=wrapped-type-converter-looked-up-first;shape.inheritance=base-depth-3;shape.kind=record-struct;shape.refused-kind=closed-set-named-as-generated-member (CMTK1021);shape.site=top-level;shape.wrapped=Uri;text.culture=ar-SA;text.entry=IParsable", "record-struct", "top-level", "none", typeof(V243), typeof(global::System.Uri), null, null, [("shape.inheritance", "none")]), - new(244, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=none;shape.kind=validated-struct;shape.refused-kind=equality-operator (CMTK1011);shape.site=nested-two-levels;shape.wrapped=NodaTime.Instant;text.culture=ar-SA;text.entry=TryFormat-short-span", "validated-struct", "nested-two-levels", "none", typeof(O244.P244.V244), typeof(global::NodaTime.Instant), null, null, []), - new(245, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=explicit-interface-member;shape.kind=record-struct;shape.refused-kind=over-object (CMTK1005);shape.site=nested-in-struct;shape.wrapped=Uri;text.culture=invariant;text.entry=TryFormat-short-span", "record-struct", "nested-in-struct", "explicit-interface-member", typeof(O245.V245), typeof(global::System.Uri), null, null, []), - new(246, "parse-shape=ordinary;process.converter-lookup=none;shape.inheritance=base-depth-3;shape.kind=validated-class;shape.refused-kind=over-System.Enum (CMTK1005);shape.site=nested-two-levels;shape.wrapped=Uri;text.culture=ar-SA;text.entry=TryParse", "validated-class", "nested-two-levels", "base-depth-3", typeof(O246.P246.V246), typeof(global::System.Uri), null, null, []), - ]; -} diff --git a/tools/probe/corpus/harness/Cmp.cs b/tools/probe/corpus/harness/Cmp.cs deleted file mode 100644 index a435030b..00000000 --- a/tools/probe/corpus/harness/Cmp.cs +++ /dev/null @@ -1,128 +0,0 @@ -// Composed-fault errors for C03/C05 (harness). e: 1 first set, 2 last set, 3 last set with detail, 4/5/6 first set with a rule (enum / closed set / nested enum). -using CodoMetis.TypeKit; -using CodoMetis.TypeKit.ClosedSets; -using CodoMetis.TypeKit.AspNetCore; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; -namespace Probes; - -public sealed partial class KFault : IFaultSet -{ - public static readonly KFault Clash = new(FaultKind.Conflict, null); - public static readonly KFault Lost = new(FaultKind.NotFound, null); - private KFault(FaultKind kind, string? description) { Kind = kind; Description = description; } - public FaultKind Kind { get; } - public string? Description { get; } -} - -public static class Cmp -{ - public const string Detail = "It is at version 3, not 2."; - static Fault Rule(int e) => e switch - { - 4 => Fault.Of(HFault.Clash).WithRule(PRule.A), - 5 => Fault.Of(HFault.Clash).WithRule(RuleSet.X), - 6 => Fault.Of(HFault.Clash).WithRule(Holder.NestedRule.N), - _ => throw new ArgumentOutOfRangeException(nameof(e)), - }; - public static Result> Two(int e) => e switch - { - 1 => Fault.Of(HFault.Missing), - 2 => Fault.Of(GFault.Raced), - 3 => Fault.Of(Fault.Of(GFault.Old, Detail)), - >= 4 and <= 6 => Fault.Of(Rule(e)), - _ => Result>.Success(), - }; - public static Result> TwoK(int e) => e switch - { - 1 => Fault.Of(HFault.Missing), - 2 => Fault.Of(KFault.Clash), - 3 => Fault.Of(Fault.Of(KFault.Lost, Detail)), - >= 4 and <= 6 => Fault.Of(Rule(e)), - _ => Result>.Success(), - }; - public static Result> Three(int e) => e switch - { - 1 => Fault.Of(HFault.Missing), - 2 => Fault.Of(SFault.First), - 3 => Fault.Of(Fault.Of(SFault.Second, Detail)), - >= 4 and <= 6 => Fault.Of(Rule(e)), - _ => Result>.Success(), - }; - public static Result> ThreeK(int e) => e switch - { - 1 => Fault.Of(HFault.Missing), - 2 => Fault.Of(SFault.First), - 3 => Fault.Of(Fault.Of(SFault.Second, Detail)), - >= 4 and <= 6 => Fault.Of(Rule(e)), - _ => Result>.Success(), - }; - - // ---- expectations - /// (fault type name, member name, status) of the error e answers, by arity and variant; variant group moves HFault.Clash to 422. - public static (string Type, string Member, int Status) Expect(int arity, string variant, int e) - { - var k = variant == "dup"; - int clash = variant == "group" ? 422 : 409; - return e switch - { - 1 => ("HFault", "Missing", 404), - 2 => arity == 2 ? (k ? ("KFault", "Clash", 409) : ("GFault", "Raced", 409)) : ("SFault", "First", 409), - 3 => arity == 2 ? (k ? ("KFault", "Lost", 404) : ("GFault", "Old", 412)) : ("SFault", "Second", 410), - _ => ("HFault", "Clash", clash), - }; - } - /// The statuses >= 400 the operation lists: those of every member of every answered type, minus the left-out ones, plus the declared 429. - public static SortedSet DocStatuses(int arity, string variant) - { - var s = new SortedSet(); - var h = new (string, int)[] { ("Missing", 404), ("Clash", variant == "group" ? 422 : 409), ("Bad", 422), ("Denied", 403), ("Down", 503), ("Stale", 412), ("Garbled", 400) }; - foreach (var (m, st) in h) if (!(variant == "wo" && m == "Bad")) s.Add(st); - if (variant == "dup") { s.Add(409); s.Add(404); } else { s.Add(404); s.Add(409); s.Add(412); } - if (arity == 3) { s.Add(409); s.Add(410); } - if (variant == "declared") s.Add(429); - return s; - } - - static readonly Dictionary Kinds = new(StringComparer.Ordinal) - { - ["HFault.Missing"] = "NotFound", ["HFault.Clash"] = "Conflict", ["HFault.Bad"] = "Invalid", ["HFault.Denied"] = "Forbidden", - ["HFault.Down"] = "Unavailable", ["HFault.Stale"] = "PreconditionFailed", ["HFault.Garbled"] = "Malformed", - ["GFault.Gone"] = "NotFound", ["GFault.Raced"] = "Conflict", ["GFault.Old"] = "PreconditionFailed", - ["KFault.Clash"] = "Conflict", ["KFault.Lost"] = "NotFound", - }; - /// The kind of a fault-set member, or null for a member of the enum SFault, which has none. - public static string? KindOf(string type, string member) => Kinds.GetValueOrDefault(type + "." + member); - /// The problem types the operation's response for names: every member answered with it, of every set. - public static HashSet TypesAt(int arity, string variant, int status) - { - var members = new List<(string Type, string Member, int Status)> - { - ("HFault", "Missing", 404), ("HFault", "Clash", variant == "group" ? 422 : 409), ("HFault", "Denied", 403), ("HFault", "Down", 503), - ("HFault", "Stale", 412), ("HFault", "Garbled", 400), - }; - if (variant != "wo") members.Add(("HFault", "Bad", 422)); - if (variant == "dup") members.AddRange([("KFault", "Clash", 409), ("KFault", "Lost", 404)]); - else members.AddRange([("GFault", "Gone", 404), ("GFault", "Raced", 409), ("GFault", "Old", 412)]); - // arity 3 adds SFault, an enum mapped to 409 and 410 with no type - return [.. members.Where(m => m.Status == status).Select(m => ComposedClaims.TypeOf(m.Type, KindOf(m.Type, m.Member)!))]; - } - - public static RouteHandlerBuilder Decorate(RouteHandlerBuilder rb, string variant) => variant switch - { - "wo" => rb.WithoutFaults(HFault.Bad), - "group" => rb.WithFaultStatus(HFault.Clash, 422), - "declared" => rb.ProducesProblem(429), - _ => rb, - }; - public static void MapMinimal(IEndpointRouteBuilder b) - { - foreach (var v in new[] { "plain", "wo", "group", "declared", "dup" }) - { - var k = v == "dup"; - Decorate(k ? b.MapGet($"/m/2/{v}", (int e) => TwoK(e).ToHttpResult()) : b.MapGet($"/m/2/{v}", (int e) => Two(e).ToHttpResult()), v); - Decorate(k ? b.MapGet($"/m/3/{v}", (int e) => ThreeK(e).ToHttpResult()) : b.MapGet($"/m/3/{v}", (int e) => Three(e).ToHttpResult()), v); - } - } -} diff --git a/tools/probe/corpus/harness/ComposedProbes.cs b/tools/probe/corpus/harness/ComposedProbes.cs deleted file mode 100644 index 56532242..00000000 --- a/tools/probe/corpus/harness/ComposedProbes.cs +++ /dev/null @@ -1,228 +0,0 @@ -// Claims C03-composed-faults (AspNetCore README#faults-of-several-types) and C05-with-rule (README#the-rule-that-refused). -// C03: a Fault is "answered as its own set maps it" (status, `fault`, problem type, detail, rule intact); "its operation lists the -// statuses of both sets, in ascending order, with the problem types of both where they share a status"; "the host doesn't start while either -// set is unmapped". The host maps every fault set with a problem type of its set and kind (TypeOf), so two sets sharing a status name two types. -// C05: WithRule(member) is written as `rule`, "qualified with its type's name", a nested type after its nesting chain "whatever QualifiedFaultNames -// says"; "The status, the mapping and the document stay the error's"; a value no member names "throws where it's given"; the document describes `rule`. -// Oracle: claim (README text) + differential wire/document. Expected: see Cmp.Expect / Cmp.DocStatuses. Each obligation is a check of its -// own (ComposedChecks.C03, C05), every check on one host. -using System.Text.Json.Nodes; -using CodoMetis.TypeKit; -using CodoMetis.TypeKit.AspNetCore; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using Probes.Faults; -using Probes.Shapes; -namespace Probes; - -public static class ComposedClaims -{ - /// The application's own string converter (json.Converters=own-for-wrapped) wraps every string it writes: take it off again. - static string? Un(Row r, string? s) => r.Is("json.Converters", "own-for-wrapped") && s is not null && s.StartsWith("own:") ? s[4..].Trim('"') : s; - static string Cut(string? s) => s is null ? "(null)" : s.Length > 200 ? s[..200] + "..." : s; - public static string Variant(Row r) => r["http.response"] switch - { - "endpoint-without-faults" => "wo", "group-with-fault-status" => "group", "declared-produces-problem" => "declared", "two-faults-same-name" => "dup", _ => "plain", - }; - static HostCfg Cfg(Row r) => new() - { - FaultControllers = true, Controllers = Hx.Plain(r) ? [typeof(RulePlainController)] : [typeof(RuleApiController)], - Minimal = Cmp.MapMinimal, Qualified = r.Is("qualified", "QualifiedFaultNames"), - Mapping = "own-per-set-type", - Services = s => s.AddFaultResponses(f => FaultResponse.For(f.Kind).WithType(new Uri(TypeOf(f.GetType().Name, f.Kind.ToString())))), - }; - - /// The problem type the host maps a fault-set member to: its set's and its kind's, so two sets sharing a status name two types. - public static string TypeOf(string set, string kind) => $"https://p.example/{set}/{kind}"; - - /// The problem's rule, an aspect of its own beside 's. - public const string Rule = "rule"; - - /// - /// The wire and the document of one endpoint family under the row, one check (Row.Check) per obligation names an - /// aspect for ('s and ), against ; every check runs on the one host, with - /// the answers fetched once. e: the error code of Cmp. - /// - public static async Task WireAndDoc(Row r, Entry en, int arity, int e, string rule, Func of, Baseline against) - { - var variant = Variant(r); - var obligations = Aspect.Answer.Append(Rule).Concat(Aspect.Doc).SelectMany(of).Distinct().ToList(); - WebApplication? app = null; Exception? down = null; - try { app = await Hx.Start(r, en, Cfg(r)); } - catch (Exception ex) when (ex is not Sides.InputsNotEquivalent) { down = ex; } - await using var _ = app; - string? failed = null; - if (down is not null) - { - foreach (var o in obligations) { var f = r.Check(o, against, _ => "host did not start: " + down.GetType().Name + ": " + Cut(down.Message)); failed ??= f; } - return failed; - } - var qualified = r.Is("qualified", "QualifiedFaultNames"); - var sides = new List(); - if (Hx.HasMinimal(r)) sides.Add(Hx.M(r, $"/m/{arity}/{variant}")); - if (Hx.HasMvc(r)) sides.Add(Hx.C(r, $"/c/{arity}/{variant}")); - JsonNode? doc = null; Exception? docFailed = null; - try { doc = await Hx.Doc(app!); } - catch (Exception ex) { docFailed = ex; } - var wires = new Dictionary(); - foreach (var path in sides) wires[path] = await Hx.Get(app!, $"{path}?e={e}"); - var (type, member, status) = Cmp.Expect(arity, variant, e); - - foreach (var o in obligations) - { - var f = await r.Check(o, against, async c => - { - bool want(string aspect) => of(aspect).Contains(o); - if (Aspect.Answer.Any(want) || want(Rule)) - foreach (var path in sides) - { - var w = wires[path]; - if (w.Thrown is not null) return $"{path} e={e}: request threw {w.Thrown}"; - if (want(Aspect.Status) && (int)w.Status != status) return $"{path} e={e}: status {(int)w.Status}, expected {status}: {Cut(w.Body)}"; - JsonNode? body; try { body = JsonNode.Parse(w.Body); } catch { return $"{path} e={e}: body is not JSON: {Cut(w.Body)}"; } - var fault = Un(r, body?["fault"]?.GetValue()); - var wantFault = qualified ? $"{type}.{member}" : member; - if (want(Aspect.Fault) && fault != wantFault) return $"{path} e={e}: fault {fault}, expected {wantFault}"; - if (want(Aspect.Detail) && e == 3 && Un(r, body?["detail"]?.GetValue()) != Cmp.Detail) return $"{path} e={e}: detail {body?["detail"]}, expected the occurrence's"; - var gotRule = Un(r, body?["rule"]?.GetValue()); - if (want(Rule) && gotRule != (rule == "" ? null : rule)) return $"{path} e={e}: rule {gotRule ?? "(none)"}, expected {(rule == "" ? "(none)" : rule)}"; - // an enum (SFault) is mapped to a status alone, and ASP.NET writes its own default type there - if (want(Aspect.Type) && Cmp.KindOf(type, member) is { } kind && Un(r, body?["type"]?.GetValue()) is var gotType && gotType != TypeOf(type, kind)) - return $"{path} e={e}: type {gotType ?? "(none)"}, expected {TypeOf(type, kind)}"; - if (want(Aspect.Status) && int.Parse(body?["status"]?.ToString() is { } st ? Un(r, st)!.Trim('"') : "0") != status) return $"{path} e={e}: problem status field {body?["status"]}"; - } - if (!Aspect.Doc.Any(want)) return null; - // the document: one ASP.NET alone refuses the same way has nothing to check (noted there) - if (docFailed is not null) - return await Hx.DocRefusedByAspNetAlone(r, en, Cfg(r), docFailed, (Aspect.Answer.Any(want) || want(Rule)) && sides.Count > 0) is { } alone - ? alone + " @ " + string.Join(" < ", (docFailed.StackTrace ?? "").Split('\n').Take(4).Select(x => x.Trim().Split('(')[0])) - : null; - foreach (var path in sides) - { - var ops = doc!["paths"]?[path]?["get"]; - if (ops is null && !Hx.InDocument(r) && path.Contains("/c/")) { c.Note(NoteKeys.DocControllerNotInApiExplorer); continue; } - if (ops is null) return $"document has no GET {path}"; - var keys = ops["responses"]!.AsObject().Select(kv => kv.Key).Where(k => int.TryParse(k, out var n) && n >= 400).Select(int.Parse).ToList(); - var listed = Cmp.DocStatuses(arity, variant); - // A status the endpoint declares itself (the 429) takes its place among the sets' statuses (fixed in 8d1ebd8). - if ((want(Aspect.DocListed) || want(Aspect.DocOrder)) && !keys.SequenceEqual(listed)) return $"{path}: document lists {string.Join(",", keys)}, expected {string.Join(",", listed)} (ascending)"; - foreach (var k in keys) - { - if (variant == "declared" && k == 429) continue; // a declared problem is ASP.NET's, not an answered fault - var txt = Hx.Deep(doc!, ops["responses"]![k.ToString()])?.ToJsonString() ?? ""; - if (want(Aspect.DocDescribes) && !txt.Contains("\"fault\"")) return $"{path} {k}: response schema does not describe fault: {Cut(txt)}"; - if (want(Aspect.DocDescribes) && !txt.Contains("\"rule\"")) return $"{path} {k}: response schema does not describe rule: {Cut(txt)}"; - var desc = ops["responses"]![k.ToString()]?["description"]?.GetValue() ?? ""; - const string Lead = "Problem types: "; - var at = desc.IndexOf(Lead, StringComparison.Ordinal); - var types = at < 0 ? [] : desc[(at + Lead.Length)..].Split(", ").ToHashSet(); - var wantTypes = Cmp.TypesAt(arity, variant, k); - if (want(Aspect.DocTypes) && !types.SetEquals(wantTypes)) - return $"{path} {k}: description \"{desc}\" names problem types {{{string.Join(", ", types)}}}, expected {{{string.Join(", ", wantTypes)}}}"; - } - } - return null; - }); - failed ??= f; - } - return failed; - } - - /// "The host doesn't start while either set is unmapped." - public static async Task UnmappedDoesNotStart(Row r, Entry en, int arity, int e) - { - var cfg = Cfg(r); cfg.MapBlanketMapping = false; - var unmapped = e == 1 ? "HFault" : arity == 2 ? "GFault" : "GFault"; - var variant = Variant(r); - if (variant == "dup") unmapped = e == 1 ? "HFault" : "KFault"; - cfg.Services = s => - { - if (unmapped != "HFault") s.AddFaultResponses(); - if (unmapped != "GFault" && variant != "dup") s.AddFaultResponses(); - if (unmapped != "KFault" && variant == "dup") s.AddFaultResponses(); - if (arity == 3) s.AddFaultResponses(f => f switch { SFault.First => 409, _ => 410 }); - }; - WebApplication? app = null; - try { app = await Hx.Start(r, en, cfg); } - catch (Exception ex) when (ex is not Sides.InputsNotEquivalent) - { - return ex.ToString().Contains(unmapped) ? null : $"host refused to start but not naming {unmapped}: {ex.GetType().Name}: {Cut(ex.Message)}"; - } - await using var _ = app; - return $"host started with {unmapped} unmapped (arity {arity}, variant {variant}); answered only at request time?"; - } -} - -public static class ComposedChecks -{ - static void Process(Row r, Entry en) { if (r.Is("process.converter-lookup", "wrapped-type-converter-looked-up-first")) _ = System.ComponentModel.TypeDescriptor.GetConverter(Make.Held(en.Plain)); } - /// - /// C03, a check per obligation: each-error-own-set (status, fault, problem type, and the rule where the error names none), - /// detail-and-rule-intact (error with-detail or with-rule: the detail and the rule), statuses-ascending (the statuses each operation - /// lists, in order, and their problem schemas), shared-status-types (the problem types each status's description names), and - /// unmapped-set-stops-start (a host with one set unmapped, against the refusal) in every row. - /// - public static async Task C03(Row r) - { - var en = Registry.Of(r); Process(r, en); - var arity = int.Parse(r["arity"]); - var e = r["error"] switch { "first-set" => 1, "last-set" => 2, "with-detail" => 3, _ => 4 }; - var intact = r["error"] is "with-detail" or "with-rule"; - string[] Of(string aspect) => aspect switch - { - Aspect.Detail or ComposedClaims.Rule when intact => ["detail-and-rule-intact"], - Aspect.DocListed or Aspect.DocOrder or Aspect.DocDescribes => ["statuses-ascending"], - Aspect.DocTypes => ["shared-status-types"], - _ => ["each-error-own-set"], - }; - var answered = await ComposedClaims.WireAndDoc(r, en, arity, e, e == 4 ? "PRule.A" : "", Of, - Baseline.Docs("src/CodoMetis.TypeKit.AspNetCore/README.md#faults-of-several-types")); - var unmapped = await r.Check("unmapped-set-stops-start", Baseline.Refusal, _ => ComposedClaims.UnmappedDoesNotStart(r, en, arity, e)); - return answered ?? unmapped; - } - /// - /// C05, a check per obligation: unnamed-value-throws (ruletype flags-combination or undefined-cast, against the refusal); - /// rule-qualified (enum, closed-set) or nested-chain (nested-enum), and no-namespace, for the rule on the wire; stays-the-errors for - /// the status, fault, problem type and the document, and in the rows of an unnamed value for the error answered without a rule. - /// - public static async Task C05(Row r) - { - var en = Registry.Of(r); Process(r, en); - var ruletype = r["ruletype"]; - // a value no member names throws where it is given (never a rule on the wire) - var thrown = ruletype switch - { - "flags-combination" => r.Check("unnamed-value-throws", Baseline.Refusal, c => - { - try { _ = Fault.Of(HFault.Clash).WithRule((FRule)3); return "WithRule(flags combination) did not throw"; } catch (ArgumentException) { return null; } - }), - "undefined-cast" => r.Check("unnamed-value-throws", Baseline.Refusal, c => - { - try { _ = Fault.Of(HFault.Clash).WithRule((PRule)9); return "WithRule(undefined cast) did not throw"; } catch (ArgumentException) { return null; } - }), - _ => null, - }; - var (e, rule) = ruletype switch { "enum" => (4, "PRule.A"), "closed-set" => (5, "RuleSet.X"), "nested-enum" => (6, "Holder.NestedRule.N"), _ => (1, "") }; - var named = ruletype switch { "enum" or "closed-set" => "rule-qualified", "nested-enum" => "nested-chain", _ => null }; - string[] Of(string aspect) => aspect == ComposedClaims.Rule && named is not null ? [named, "no-namespace"] : ["stays-the-errors"]; - var answered = await ComposedClaims.WireAndDoc(r, en, 3, e, rule, Of, Baseline.Docs("src/CodoMetis.TypeKit.AspNetCore/README.md#the-rule-that-refused")); - return thrown ?? answered; - } -} -public static class ComposedShrinkInit -{ - [System.Runtime.CompilerServices.ModuleInitializer] - public static void Init() { ShrinkChecks.All["C03"] = ComposedChecks.C03; ShrinkChecks.All["C05"] = ComposedChecks.C05; } -} -public sealed class C03ComposedProbe -{ - public static TheoryData Data() => Rows.Data("C03ComposedProbe"); - [Theory, MemberData(nameof(Data))] - public Task Row(string probe, int idx) => Rec.Run("C03", idx, ComposedChecks.C03, probe); -} -public sealed class C05RuleProbe -{ - public static TheoryData Data() => Rows.Data("C05RuleProbe"); - [Theory, MemberData(nameof(Data))] - public Task Row(string probe, int idx) => Rec.Run("C05", idx, ComposedChecks.C05, probe); -} diff --git a/tools/probe/corpus/harness/Containers.g.cs b/tools/probe/corpus/harness/Containers.g.cs deleted file mode 100644 index d79627ed..00000000 --- a/tools/probe/corpus/harness/Containers.g.cs +++ /dev/null @@ -1,25 +0,0 @@ -// by tools/probe/generators/containers.cs -#nullable disable -#pragma warning disable CS8618, CS1591, CS0649 -using System.Text.Json.Serialization; -namespace Probes.Shapes; -public class Cn_class_P { public M P { get; set; } } -public class Cn_class_F { public M F; } -public class Cn_class_C { public M P { get; } [JsonConstructor] public Cn_class_C(M p) { P = p; } } -public class Cn_class_R { public required M P { get; init; } } -public record class Cn_record_class_P { public M P { get; set; } } -public record class Cn_record_class_F { public M F; } -public record class Cn_record_class_C(M P); -public record class Cn_record_class_R { public required M P { get; init; } } -public struct Cn_struct_P { public M P { get; set; } } -public struct Cn_struct_F { public M F; } -public struct Cn_struct_C { public M P { get; } [JsonConstructor] public Cn_struct_C(M p) { P = p; } } -public struct Cn_struct_R { public required M P { get; init; } } -public record struct Cn_record_struct_P { public M P { get; set; } } -public record struct Cn_record_struct_F { public M F; } -public record struct Cn_record_struct_C { public M P { get; } [JsonConstructor] public Cn_record_struct_C(M p) { P = p; } } -public record struct Cn_record_struct_R { public required M P { get; init; } } -public class Cn_generic_type_P { public M P { get; set; } } -public class Cn_generic_type_F { public M F; } -public class Cn_generic_type_C { public M P { get; } [JsonConstructor] public Cn_generic_type_C(M p) { P = p; } } -public class Cn_generic_type_R { public required M P { get; init; } } diff --git a/tools/probe/corpus/harness/DecimalSpelling.cs b/tools/probe/corpus/harness/DecimalSpelling.cs deleted file mode 100644 index 11a7c617..00000000 --- a/tools/probe/corpus/harness/DecimalSpelling.cs +++ /dev/null @@ -1,230 +0,0 @@ -// Claim C01-decimal-spelling (docs/json.md#decimals-in-one-spelling): DecimalNumberJsonConverter writes the smallest scale as a -// number, DecimalStringJsonConverter as a string; "Register either one once, and it covers every decimal: nullable ones, dictionary -// keys, and every value object over a decimal, since its JSON is the underlying type's." "Both read a number, or a string holding one, -// and keep the scale they read." -// Oracle: claim (the minimal spelling, computed here independently of the product) plus differential (the value object's document is the -// plain decimal's under the same options), with the options' own leaving-out shown by a baseline without the converter in the same row. -using System.Globalization; -using System.Text.Json; -using System.Text.Json.Serialization; -using CodoMetis.TypeKit; -using Probes.Shapes; -namespace Probes.Json; - -public static class DecimalSpelling -{ - static readonly CultureInfo Inv = CultureInfo.InvariantCulture; - const char Bs = (char)92; - - /// The input axis as decimal text: max28digits is 7.8 at scale 28, the largest scale a decimal has. - public static string Text(string input) => input == "max28digits" ? "7.8000000000000000000000000000" : input; - - /// The minimal spelling, worked out without the product: invariant digits, trailing fractional zeros dropped, a zero written 0. - public static string Canonical(decimal d) - { - if (d == 0m) return "0"; - var s = d.ToString(Inv); - return s.Contains('.') ? s.TrimEnd('0').TrimEnd('.') : s; - } - - static bool IsDecimal(JTok t, decimal d) => t.Type switch - { - JsonTokenType.Number => decimal.TryParse(t.Raw, NumberStyles.Float, Inv, out var x) && x == d, - JsonTokenType.String or JsonTokenType.PropertyName => t.Text is { Length: > 0 } s && s.Trim() == s && decimal.TryParse(s, NumberStyles.Float, Inv, out var y) && y == d, - _ => false, - }; - - /// The decimals a read value holds, as the serializer writes them by default: with the scale they hold. - static List Held(object? v, Type declared, decimal d) - { - if (v is null) return []; - var json = JsonSerializer.Serialize(v, declared, Jx.Plain); - return Jx.Tokens(json).Where(t => IsDecimal(t, d)).Select(t => decimal.Parse(t.Type == JsonTokenType.Number ? t.Raw : t.Text!, NumberStyles.Float, Inv)).ToList(); - } - static bool Same(decimal a, decimal b) => decimal.GetBits(a).SequenceEqual(decimal.GetBits(b)); - static string Show(List l) => string.Join(",", l.Select(x => x.ToString(Inv))); - - /// What one form of the document read as: the decimal's and the value object's refusals and the decimals each holds. - sealed record Form(string Name, string Json, Exception? Ep, Exception? Ev, List Cp, List Cv, bool BothRefused); - - /// - /// C01's checks, one per obligation, over one write and one read of the row's shape: the converter's spelling of each value - /// (smallest-scale; number-is-a-number or string-is-a-string, by the row's converter) against the docs' minimal spelling worked out - /// here; every decimal of the document, a key's included, and the value object's document against the plain decimal's - /// (covers-every-decimal, the twin); each form of the input read (reads-a-number-or-a-string) with the scale it carries - /// (keeps-the-scale-it-reads). The options' own leaving-out is shown by a baseline without the converter in the same row: a - /// direction it leaves out has nothing to check. Every check runs; returns the first failure. - /// - public static string? Check(Row r) - { - var kind = r["shape.kind"]; - if (kind == "closed-set") { r.Outcome("harness-error", "C01 excludes closed-set: a closed set holds no decimal"); return null; } - var e = Registry.Get(kind, "decimal"); - var text = Text(r["input"]); - var d = decimal.Parse(text, NumberStyles.Float, Inv); - var canon = Canonical(d); - var number = r["converter"] == "number"; - JsonConverter conv = number ? new DecimalNumberJsonConverter() : new DecimalStringJsonConverter(); - var position = r["shape.position"]; - var containing = position == "root" ? "class" : r["shape.containing"]; - var (hp, hv) = Sides.Twins(position, containing, e, d, Make.Vo(e, d)); - var o = JCtx.Apply(r, e, hp.Declared, hv.Declared); - var ob = new JsonSerializerOptions(o); // the baseline: the same options, the serializer's own decimal - foreach (var c in ob.Converters.Where(c => c.CanConvert(typeof(decimal))).ToList()) ob.Converters.Remove(c); - o.Converters.Insert(0, conv); // registered first: an application converter for decimals (own-for-wrapped) comes after it - var ownVo = r.Is("json.Converters", "own-for-value-object"); - var docs = Baseline.Docs("docs/json.md#decimals-in-one-spelling"); - const string Smallest = "smallest-scale", Covers = "covers-every-decimal", Reads = "reads-a-number-or-a-string", Keeps = "keeps-the-scale-it-reads"; - var kindOf = number ? "number-is-a-number" : "string-is-a-string"; - - // Writing: one spelling, the smallest scale, as a number or a string; the value object's document is the decimal's. - var (jp, xp) = Jx.Ser(hp, o); var (jv, xv) = Jx.Ser(hv, o); var (jb, xb) = Jx.Ser(hp, ob); - if (xb is not null && xp is not null && (xv is not null || ownVo) && xp.GetType() == xb.GetType()) - { - // the options refuse the shape for the serializer's own decimal too: nothing is written or read to compare. Where the value - // object is refused alike, at the same stage with the same exception type, the decimal without TypeKit's converter is side (1) - // of the three-sided differential (Sides), and each check is the framework's refusal; otherwise none can tell. - var one = new Sides.Answer("write", null, xb); var three = new Sides.Answer("write", null, xv); - if (Sides.Alike(one, three)) - { - var framework = Sides.Measured(one, "the serializer's own decimal, without TypeKit's converter"); - foreach (var o2 in new[] { Smallest, kindOf, Covers, Reads, Keeps }) r.Check(o2, framework.Baseline, c => Sides.Refuses(c, framework, three)); - return null; - } - var why = "the options refuse the shape for the serializer's own decimal too: " + Jx.Ex(xb); - string? None(Check c) => c.Inconclusive(why); - foreach (var ob2 in new[] { Smallest, kindOf }) r.Check(ob2, docs, None); - r.Check(Covers, Baseline.Twin, None); r.Check(Reads, docs, None); r.Check(Keeps, docs, None); - return null; - } - var tp = xp is null ? Jx.Tokens(jp!).Where(t => IsDecimal(t, d)).ToList() : []; - var tb = xb is null ? Jx.Tokens(jb!).Where(t => IsDecimal(t, d)).ToList() : null; - var writeLeftOut = tb is { Count: 0 } && xp is null && tp.Count == 0; - string? WriteLeftOut(Check c) { c.Note(NoteKeys.JsonMemberLeftOutByOptions, "write: " + Jx.Cut(jb)); return c.Inconclusive("write: the options leave the member out, for the serializer's own decimal too: " + Jx.Cut(jb)); } - var want = number ? JsonTokenType.Number : JsonTokenType.String; - var f1 = r.Check(Smallest, docs, c => - { - if (xp is not null) return $"write: the decimal with {conv.GetType().Name} threw {Jx.Ex(xp)}; without it the serializer wrote {Jx.Cut(jb)}"; - if (tb is { Count: 0 } && tp.Count != 0) return $"write: the options leave the member out without the converter ({Jx.Cut(jb)}) but it wrote {Jx.Cut(jp)}"; - if (writeLeftOut) return WriteLeftOut(c); - if (tp.Count == 0) return $"write: no decimal in {Jx.Cut(jp)}; the serializer alone wrote {Jx.Cut(jb)}"; - foreach (var t in tp.Where(t => t.Type != JsonTokenType.PropertyName)) - if (t.Raw != canon) return $"write: {t.Type} {t.Raw}, expected the smallest scale {canon}, in {Jx.Cut(jp)}"; - c.Note(NoteKeys.Compared, $"write {text} -> {canon}"); - return null; - }); - var f2 = r.Check(kindOf, docs, c => - { - if (xp is not null) return c.Inconclusive("write: the decimal with the converter threw, judged by smallest-scale"); - if (writeLeftOut) return WriteLeftOut(c); - var values = tp.Where(t => t.Type != JsonTokenType.PropertyName).ToList(); - if (values.Count == 0) return c.Inconclusive($"write: no decimal value in {Jx.Cut(jp)}, only keys, which are names"); - foreach (var t in values) - if (t.Type != want) return $"write: {t.Type} {t.Raw}, expected the {want} {canon}, in {Jx.Cut(jp)}"; - c.Note(NoteKeys.Compared, $"write as a {want}"); - return null; - }); - - // Reading: a number, a string holding one, and an escaped string all read, the scale they carry kept, on both sides alike. - var full = Jx.Full(o); - var (jf, xf) = Jx.Ser(hp, full); - if (xf is not null) { r.Outcome("harness-error", "the document with every member written threw " + Jx.Ex(xf)); return f1 ?? f2; } - var at = Jx.Tokens(jf!).Where(t => IsDecimal(t, d)).ToList(); - string? readNothing = null; - var forms = new List
(); - var leftOut = false; - if (at.Count == 0) - { - // Nothing to read either: the options leave the member out of every document (a field without IncludeFields). - var (jfb, _) = Jx.Ser(hp, Jx.Full(ob)); - if (jfb is null || Jx.Tokens(jfb).Any(t => IsDecimal(t, d))) { r.Outcome("harness-error", "no decimal in the document with every member written: " + jf); return f1 ?? f2; } - readNothing = "read: the options leave the member out of every document, for the serializer's own decimal too: " + Jx.Cut(jf); - } - else - { - var key = at.Any(t => t.Type == JsonTokenType.PropertyName); - var texts = new List<(string Name, string Json)>(); - if (!key) texts.Add(("number", Jx.Rewrite(jf!, at, _ => text))); - texts.Add(("string", Jx.Rewrite(jf!, at, _ => "\"" + text + "\""))); - texts.Add(("escaped string", Jx.Rewrite(jf!, at, _ => "\"" + Bs + "u00" + ((int)text[0]).ToString("x2") + text[1..] + "\""))); - // The baseline reads the first form with the serializer's own decimal: it shows whether these options read the member at all. - var (vb, eb) = Jx.De(texts[0].Json, hp.Declared, ob); - leftOut = eb is null && Held(vb, hp.Declared, d).Count == 0; - foreach (var (name, json) in texts) - { - var (vp, ep) = Jx.De(json, hp.Declared, o); - var (vv, ev) = ownVo ? (null, null) : Jx.De(json, hv.Declared, o); - var both = ep is not null && eb is not null && (ownVo || ev is not null) && ep.GetType() == eb.GetType(); - var cp = ep is null ? Held(vp, hp.Declared, d) : []; var cv = ownVo ? cp : ev is null ? Held(vv, hv.Declared, d) : []; - forms.Add(new(name, json, ep, ev, cp, cv, both)); - } - if (leftOut && forms.All(f => f.Ep is null && f.Ev is null && f.Cp.Count == 0 && f.Cv.Count == 0)) - readNothing = "read: the options read no member, for the serializer's own decimal too"; - } - string? ReadLeftOut(Check c) { c.Note(NoteKeys.JsonMemberLeftOutByOptions, "read"); return c.Inconclusive(readNothing!); } - // The forms both sides refuse alike under these options (the serializer's own decimal too) have nothing to compare. - var compared = forms.Where(f => !f.BothRefused).ToList(); - string? AllRefused(Check c) - { - foreach (var f in forms.Where(f => f.BothRefused)) c.Log.AppendLine($"read {f.Name}: both refuse: {Jx.Ex(f.Ep!)}"); - return c.Inconclusive("read: every form is refused for the serializer's own decimal too: " + Jx.Ex(forms[0].Ep!)); - } - var f4 = r.Check(Reads, docs, c => - { - if (readNothing is not null) return ReadLeftOut(c); - foreach (var f in forms) - { - if (f.BothRefused) continue; - if (f.Ep is not null) return $"read {f.Name}: the decimal with {conv.GetType().Name} threw {Jx.Ex(f.Ep)}\n text={Jx.Cut(f.Json)}"; - if (leftOut) { if (f.Cp.Count != 0) return $"read {f.Name}: the options read no member for the serializer's own decimal, yet it read {Show(f.Cp)}"; continue; } - if (f.Cp.Count == 0) return $"read {f.Name}: the decimal read nothing\n text={Jx.Cut(f.Json)}"; - } - if (compared.Count == 0) return AllRefused(c); - c.Note(NoteKeys.Compared, "read " + string.Join(", ", compared.Select(f => f.Name))); - return null; - }); - var f5 = r.Check(Keeps, docs, c => - { - if (readNothing is not null) return ReadLeftOut(c); - if (compared.Count == 0) return AllRefused(c); - var kept = 0; - foreach (var f in compared) - { - if (f.Cp.Count == 0) continue; // read nothing, or refused: reads-a-number-or-a-string judges it - if (f.Cp.Any(x => !Same(x, d))) return $"read {f.Name}: read {Show(f.Cp)}, expected {d.ToString(Inv)} with its scale\n text={Jx.Cut(f.Json)}"; - kept++; - } - if (kept == 0) return c.Inconclusive("read: no form read a decimal to hold its scale to"); - c.Note(NoteKeys.Compared, $"read with the scale of {text}"); - return null; - }); - var f3 = r.Check(Covers, ownVo ? Baseline.RoundTrip : Baseline.Twin, c => - { - if (xv is not null && !ownVo) return $"write: the value object threw {Jx.Ex(xv)} where the decimal wrote {Jx.Cut(jp)}"; - if (xp is not null) return c.Inconclusive("write: the decimal with the converter threw, judged by smallest-scale"); - if (tb is not null && tb.Count != 0 && tp.Count != tb.Count) return $"write: {tp.Count} decimals in {Jx.Cut(jp)}, {tb.Count} in {Jx.Cut(jb)}"; - foreach (var t in tp.Where(t => t.Type == JsonTokenType.PropertyName)) - if (t.Raw != canon) return $"write: key {t.Raw}, expected {canon}, in {Jx.Cut(jp)}"; - if (ownVo) - { - // "Your own converter wins" (AspNetCore README#what-the-document-says): the application's converter writes the value object, - // so the decimal's document is none of its; its own document reads back under the same options - c.Note(NoteKeys.OwnVo); - var (_, xr) = Jx.De(jv!, hv.Declared, o); - if (xr is not null) return "own converter for the value object: its document does not read back: " + Jx.Ex(xr); - return null; - } - if (jv != jp) return $"write: the value object wrote {Jx.Cut(jv)}, the decimal {Jx.Cut(jp)}"; - foreach (var f in compared) - { - if (f.Ep is not null) continue; // the decimal refused it: reads-a-number-or-a-string judges it - if (f.Ev is not null) return $"read {f.Name}: the value object threw {Jx.Ex(f.Ev)} where the decimal read it\n text={Jx.Cut(f.Json)}"; - if (f.Cp.Count != f.Cv.Count || f.Cp.Zip(f.Cv).Any(z => !Same(z.First, z.Second))) return $"read {f.Name}: the value object read {Show(f.Cv)}, the decimal {Show(f.Cp)}\n text={Jx.Cut(f.Json)}"; - } - if (writeLeftOut && (readNothing is not null || compared.Count == 0)) return WriteLeftOut(c); - c.Note(NoteKeys.Compared, $"{(number ? "number" : "string")} {text} -> {canon}, the value object's document the decimal's"); - return null; - }); - return f1 ?? f2 ?? f3 ?? f4 ?? f5; - } -} diff --git a/tools/probe/corpus/harness/EfExtra.cs b/tools/probe/corpus/harness/EfExtra.cs deleted file mode 100644 index 26ef7def..00000000 --- a/tools/probe/corpus/harness/EfExtra.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Entity shapes and helpers for the EF claims E07, E13, E14, E15 (reuse EfHarness.cs: EfDb, EfModel, Q, EfRegistry, KeyRegistry). -#nullable enable -using System.Linq.Expressions; -using System.Reflection; -using Microsoft.EntityFrameworkCore; -using Probes.Shapes; -namespace Probes.Ef; - -public class EntL where T : notnull { public int Id { get; set; } public List Items { get; set; } = []; } -public class EntComp where TA : notnull where TB : notnull { public TA A { get; set; } = default!; public TB B { get; set; } = default!; public string Tag { get; set; } = ""; } -public abstract class RootE where T : notnull { public T Id { get; set; } = default!; public string Name { get; set; } = ""; } -public class LeafA : RootE where T : notnull { public int X { get; set; } } -public class LeafB : RootE where T : notnull { public int Y { get; set; } } - -public static class EfX -{ - /// The i-th distinct plain value of a wrapped type. - public static object Sample(string wrapped, int i) => wrapped switch - { - "string" => "val-" + (char)('a' + i), - "int" => 10 + i, - "long" => 4_000_000_000L + i, - "Guid" => new Guid(i + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11), - "decimal" => 1.5m + i, - _ => throw new ArgumentException(wrapped), - }; - public static EfEntry Top(string kind, string wrapped) => EfRegistry.All.FirstOrDefault(x => x.Kind == kind && x.Wrapped == wrapped && x.Site == "top-level") - ?? EfRegistry.All.First(x => (x.Kind is "record-struct" or "sealed-record-class") && x.Wrapped == wrapped && x.Site == "top-level"); - public static readonly string[] Kinds = ["record-struct", "sealed-record-class", "validated-class", "validated-struct"]; - public static object New(Type t) => Activator.CreateInstance(t)!; - public static void Set(object o, string prop, object? v) => o.GetType().GetProperty(prop)!.SetValue(o, v); - public static object? Get(object o, string prop) => o.GetType().GetProperty(prop)!.GetValue(o); - public static string Plainify(object? k) - { - if (k is not null && k.GetType().Namespace == "Probes.Ef") k = k.GetType().GetProperty("Value")!.GetValue(k); - return Convert.ToString(k, System.Globalization.CultureInfo.InvariantCulture) ?? "null"; - } - public static IEnumerable Enumerate(IQueryable q) { foreach (var o in q) yield return o!; } - public static async Task RawScalar(EfDb db, string sql) - { - await using var cmd = db.Connection.CreateCommand(); cmd.CommandText = sql; - return Convert.ToString(await cmd.ExecuteScalarAsync(), System.Globalization.CultureInfo.InvariantCulture); - } - public static async Task Raw(EfDb db, string sql) - { - await using var cmd = db.Connection.CreateCommand(); cmd.CommandText = sql; await cmd.ExecuteNonQueryAsync(); - } -} diff --git a/tools/probe/corpus/harness/EfExtra2.cs b/tools/probe/corpus/harness/EfExtra2.cs deleted file mode 100644 index 2cffd4cf..00000000 --- a/tools/probe/corpus/harness/EfExtra2.cs +++ /dev/null @@ -1,82 +0,0 @@ -// Shared by the EF claims E06, E11, E12, E16 (after EfHarness.cs / EfExtra.cs): a context with ConventionS and the order of UseTypeKit, -// and an application-owned converter ("C:" + the plain value as text) for any type. -#nullable enable -using System.Globalization; -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Probes.Shapes; -namespace Probes.Ef; - -public sealed class DynCtx2(DbContextOptions options, Action configure, Action? conventions) : DbContext(options) -{ - protected override void OnModelCreating(ModelBuilder modelBuilder) => configure(modelBuilder); - protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) => conventions?.Invoke(configurationBuilder); -} - -public sealed class EfDb2 : IAsyncDisposable -{ - public SqliteConnection Connection { get; } - public DynCtx2 Context { get; } - // order: "none" (no UseTypeKit), "before-provider" or "after-provider". - public EfDb2(Action configure, string order, Action? conventions = null) - { - Connection = new SqliteConnection("Data Source=:memory:"); - Connection.Open(); - var b = new DbContextOptionsBuilder(); - if (order == "before-provider") b.UseTypeKit(); - b.UseSqlite(Connection); - if (order == "after-provider") b.UseTypeKit(); - b.ReplaceService(); - Context = new DynCtx2(b.Options, configure, conventions); - } - public async ValueTask DisposeAsync() { await Context.DisposeAsync(); await Connection.DisposeAsync(); } -} - -/// An application-owned conversion of T to text: "C:" + the plain value. T is the value object or its wrapped type. -public static class OwnConv -{ - public static Func ToF = _ => throw new InvalidOperationException("OwnConv not set up"); - public static Func FromF = _ => throw new InvalidOperationException("OwnConv not set up"); - public static string To(T v) => ToF(v); - public static T From(string s) => FromF(s); - public static void Setup(string wrapped, Func make, Func plain) - { - ToF = v => "C:" + Fmt(plain(v!)); - FromF = s => (T)make(Parse(wrapped, s[2..])); - } - public static string Fmt(object p) => Convert.ToString(p, CultureInfo.InvariantCulture)!; - public static object Parse(string wrapped, string s) => wrapped switch - { - "string" => s, "int" => int.Parse(s, CultureInfo.InvariantCulture), "long" => long.Parse(s, CultureInfo.InvariantCulture), - "Guid" => Guid.Parse(s), "decimal" => decimal.Parse(s, CultureInfo.InvariantCulture), "char" => s[0], - _ => throw new ArgumentException(wrapped), - }; -} - -public sealed class OwnConverter() : ValueConverter(v => OwnConv.To(v), s => OwnConv.From(s)); - -public static class OwnConvs -{ - /// Sets OwnConv up for the value object (voSide) or for its wrapped type, returns the converter type. - public static Type Setup(EfEntry e, bool vo) - { - if (vo) - { - typeof(OwnConv<>).MakeGenericType(e.Vo).GetMethod("Setup")!.Invoke(null, [e.Wrapped, (Func)e.MakeVo, (Func)(v => v.GetType().GetProperty("Value")!.GetValue(v)!)]); - return typeof(OwnConverter<>).MakeGenericType(e.Vo); - } - typeof(OwnConv<>).MakeGenericType(e.Plain).GetMethod("Setup")!.Invoke(null, [e.Wrapped, (Func)(p => p), (Func)(p => p)]); - return typeof(OwnConverter<>).MakeGenericType(e.Plain); - } -} - -public static class EfX2 -{ - public static async Task RawScalar(SqliteConnection c, string sql) - { - await using var cmd = c.CreateCommand(); cmd.CommandText = sql; - return Convert.ToString(await cmd.ExecuteScalarAsync(), CultureInfo.InvariantCulture); - } -} diff --git a/tools/probe/corpus/harness/EfHarness.cs b/tools/probe/corpus/harness/EfHarness.cs deleted file mode 100644 index c6de75a1..00000000 --- a/tools/probe/corpus/harness/EfHarness.cs +++ /dev/null @@ -1,191 +0,0 @@ -// Shared EF row-applying code for the EF satellite claims (surface ef): the entity shapes, a DbContext built from a -// configuration delegate, the model descriptor and the query helpers. Written once; later EF claims reuse it. -#nullable enable -using System.Linq.Expressions; -using System.Reflection; -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Probes.Shapes; -namespace Probes.Ef; - -public sealed record EfEntry(string Kind, string Wrapped, string Site, Type Vo, Type Plain, Func Valid) -{ - /// The value object around a plain value, by its public seams: From, else FromKnownGood. - public object MakeVo(object plain) - { - var from = Vo.GetMethod("From", BindingFlags.Public | BindingFlags.Static, [Plain]); - if (from is not null) return from.Invoke(null, [plain])!; - var known = Vo.GetMethod("FromKnownGood", BindingFlags.Public | BindingFlags.Static, [Plain, typeof(string)]); - return known is null ? throw new InvalidOperationException($"{Vo} has neither From nor FromKnownGood") : known.Invoke(null, [plain, null])!; - } - - /// The generated companion extension GetValue / ValueOrNull of this value object (a static class in its namespace; the first parameter is the value object, or its Nullable for ValueOrNull). - public MethodInfo? Companion(string name) - { - var first = name == "ValueOrNull" && Vo.IsValueType ? typeof(Nullable<>).MakeGenericType(Vo) : Vo; - return Vo.Assembly.GetTypes().Where(t => t.IsAbstract && t.IsSealed && t.Namespace == Vo.Namespace) - .SelectMany(t => t.GetMethods(BindingFlags.Public | BindingFlags.Static)) - .FirstOrDefault(m => m.Name == name && m.GetParameters() is [var p] && p.ParameterType == first); - } -} - -public static partial class EfRegistry -{ - public static readonly List All = Build(); - static List Build() { var e = new List(); Fill(e); return e; } - static partial void Fill(List e); - public static EfEntry Of(Row r) { var k = r["shape.kind"]; var w = r["shape.wrapped"]; var s = r["shape.site"]; return All.First(x => x.Kind == k && x.Wrapped == w && x.Site == s); } -} - -public class EntP where T : notnull { public int Id { get; set; } public T P { get; set; } = default!; } -public class EntNC where T : class { public int Id { get; set; } public T? P { get; set; } } -public class EntK where T : notnull { public T Id { get; set; } = default!; public string Tag { get; set; } = ""; } -public class Own where T : notnull { public T P { get; set; } = default!; } -public class EntO where T : notnull { public int Id { get; set; } public Own O { get; set; } = new(); } -public class EntC where T : notnull { public int Id { get; set; } public Own C { get; set; } = new(); } -public sealed class Box { public T Value = default!; } - -public sealed class UniqueModelKey : Microsoft.EntityFrameworkCore.Infrastructure.IModelCacheKeyFactory -{ - public object Create(DbContext context, bool designTime) => new object(); -} - -public sealed class DynCtx(DbContextOptions options, Action configure) : DbContext(options) -{ - protected override void OnModelCreating(ModelBuilder modelBuilder) => configure(modelBuilder); -} - -public sealed class EfDb : IAsyncDisposable -{ - public SqliteConnection Connection { get; } - public DynCtx Context { get; } - public EfDb(Action configure, bool typeKit, Action? extra = null) - { - Connection = new SqliteConnection("Data Source=:memory:"); - Connection.Open(); - var b = new DbContextOptionsBuilder().UseSqlite(Connection).ReplaceService(); - if (typeKit) b.UseTypeKit(); - extra?.Invoke(b); - Context = new DynCtx(b.Options, configure); - } - public async ValueTask DisposeAsync() { await Context.DisposeAsync(); await Connection.DisposeAsync(); } -} - -public static class EfModel -{ - public static Type Entity(string mapping, Type t) => mapping switch - { - "property" => typeof(EntP<>).MakeGenericType(t), - "nullable" => t.IsValueType ? typeof(EntP<>).MakeGenericType(typeof(Nullable<>).MakeGenericType(t)) : typeof(EntNC<>).MakeGenericType(t), - "key" => typeof(EntK<>).MakeGenericType(t), - "owned" => typeof(EntO<>).MakeGenericType(t), - "complex" => typeof(EntC<>).MakeGenericType(t), - _ => throw new ArgumentException("mapping " + mapping), - }; - - public static readonly Dictionary ColumnTypes = new() - { - ["string"] = "VARCHAR(40)", ["Uri"] = "VARCHAR(40)", ["Guid"] = "UNIQUEIDENTIFIER", ["int"] = "BIGINT", ["long"] = "BIGINT", ["byte"] = "TINYINT", - ["ulong"] = "BIGINT", ["decimal"] = "NUMERIC(10,2)", ["double"] = "FLOAT", ["bool"] = "BIT", ["char"] = "CHAR(1)", ["DateTime"] = "DATETIME", - ["DateTimeOffset"] = "DATETIMEOFFSET", ["DateOnly"] = "DATE", ["TimeOnly"] = "TIME", ["TimeSpan"] = "TIME", ["enum"] = "SMALLINT", ["flags-enum"] = "SMALLINT", - }; - - /// Maps the entity of a mapping in table "t", applying a facet to the column property (and UseAutoincrement to an integer key when asked). - public static Action Configure(string mapping, Type entity, Action? facet, bool autoincrement) => mb => - { - var eb = mb.Entity(entity); - eb.ToTable("t"); - var t = entity.GetGenericArguments()[0]; - switch (mapping) - { - case "property" or "nullable": eb.HasKey("Id"); if (facet is not null) facet(eb.Property("P").Metadata); break; - case "key": - eb.HasKey("Id"); - var pb = eb.Property("Id"); - facet?.Invoke(pb.Metadata); - if (autoincrement) Microsoft.EntityFrameworkCore.SqlitePropertyBuilderExtensions.UseAutoincrement(pb); - break; - case "owned": - eb.HasKey("Id"); - eb.OwnsOne(typeof(Own<>).MakeGenericType(t), "O", ob => { if (facet is not null) facet(ob.Property("P").Metadata); }); - break; - case "complex": - eb.HasKey("Id"); - eb.ComplexProperty("C", cb => { if (facet is not null) facet(cb.Property("P").Metadata); }); - break; - } - }; - - /// What the model says about every mapped column: store type, nullability, facets, key and generation. - public static string Describe(DbContext db) - { - var model = db.GetService().Model; - var lines = new List(); - string Line(string table, string prefix, IProperty p) - { - string col = "?"; try { col = ((IReadOnlyProperty)p).GetColumnType() ?? "?"; } catch { try { col = Microsoft.EntityFrameworkCore.RelationalPropertyExtensions.GetRelationalTypeMapping(p).StoreType; } catch { col = "?"; } } - string nul; try { nul = p.IsColumnNullable().ToString(); } catch { nul = p.IsNullable.ToString(); } - return $"{table}.{prefix}{p.Name}: type={col} nullable={nul} max={p.GetMaxLength()} prec={p.GetPrecision()} scale={p.GetScale()} unicode={p.IsUnicode()} pk={p.IsPrimaryKey()} gen={p.ValueGenerated}"; - } - foreach (var et in model.GetEntityTypes()) - { - var table = et.GetTableName() ?? "(none)"; - foreach (var p in et.GetProperties()) lines.Add(Line(table, "", p)); - foreach (var cp in et.GetComplexProperties()) foreach (var p in cp.ComplexType.GetProperties()) lines.Add(Line(table, cp.Name + ".", p)); - } - lines.Sort(StringComparer.Ordinal); - return string.Join("\n", lines); - } - - public static string FirstDiff(string a, string b) - { - var x = a.Split('\n'); var y = b.Split('\n'); - for (var i = 0; i < Math.Max(x.Length, y.Length); i++) - if ((i < x.Length ? x[i] : "") != (i < y.Length ? y[i] : "")) return $"VO: {(i < x.Length ? x[i] : "")} | plain: {(i < y.Length ? y[i] : "")}"; - return ""; - } -} - -/// -/// How a check of a two-sided EF row ends where a side threw: the value object's context under UseTypeKit() and the -/// wrapped twin's under plain EF ran the same step, named by what. Neither threw: false, compare on. The value object -/// alone: its violation. Both, with exceptions of one type: nothing to compare, inconclusive, since no fact or sentence says why -/// the framework refuses it. Both, otherwise: a violation, since the value object did not fail as its twin does. The twin alone: -/// inconclusive, no twin to compare with. -/// -public static class EfSides -{ - public static bool Threw(Check c, Exception? vo, Exception? plain, string what, out string? failure) - { - failure = null; - if (vo is null && plain is null) return false; - if (vo is not null && plain is not null) - failure = vo.GetType() == plain.GetType() - ? c.Inconclusive($"both sides refuse {what} alike: {vo.GetType().Name}: {Short(vo.Message)}") - : $"{what}: the value object's side threw {vo.GetType().Name}: {Short(vo.Message)}; the wrapped type's {plain.GetType().Name}: {Short(plain.Message)}"; - else if (plain is not null) failure = c.Inconclusive($"the wrapped type's side refuses {what}, so there is no twin to compare with: {plain.GetType().Name}: {Short(plain.Message)}"); - else failure = $"{what} with the value object threw {vo!.GetType().Name}: {Short(vo.Message)}; the wrapped type's did not"; - return true; - } - - static string Short(string s) { s = s.Split('\n')[0]; return s.Length > 300 ? s[..300] : s; } -} - -public static class Q -{ - public static IQueryable Call(string name, Type[] typeArgs, IQueryable src, params Expression[] args) - { - static bool OneParamLambda(ParameterInfo p) => - p.ParameterType.IsGenericType && p.ParameterType.GetGenericTypeDefinition() == typeof(Expression<>) && p.ParameterType.GetGenericArguments()[0].GetGenericArguments().Length == 2; - var m = typeof(Queryable).GetMethods().First(x => x.Name == name && x.IsGenericMethodDefinition && x.GetGenericArguments().Length == typeArgs.Length - && x.GetParameters().Length == args.Length + 1 && x.GetParameters().Skip(1).All(OneParamLambda)); - return (IQueryable)m.MakeGenericMethod(typeArgs).Invoke(null, [src, .. args.Select(a => (object)a)])!; - } - public static IQueryable SetOf(DbContext db, Type entity) => - (IQueryable)typeof(DbContext).GetMethods().First(m => m.Name == "Set" && m.GetParameters().Length == 0).MakeGenericMethod(entity).Invoke(db, null)!; - public static object NewBox(Type t, object? value) { var b = Activator.CreateInstance(typeof(Box<>).MakeGenericType(t))!; typeof(Box<>).MakeGenericType(t).GetField("Value")!.SetValue(b, value); return b; } - public static string Sql(IQueryable q) => q.ToQueryString(); -} diff --git a/tools/probe/corpus/harness/EfShapes.g.cs b/tools/probe/corpus/harness/EfShapes.g.cs deleted file mode 100644 index 95fa592f..00000000 --- a/tools/probe/corpus/harness/EfShapes.g.cs +++ /dev/null @@ -1,962 +0,0 @@ -// by tools/probe/generators/ef.cs catalogue; the EF probes' shape catalogue (shape.kind x shape.wrapped x shape.site). Do not edit. -#nullable enable -#pragma warning disable CMTK0014, CS8618, CS1591, CMTK0005, CMTK1000, CMTK0003, CMTK0006, CA1000 -using CodoMetis.TypeKit; -using CodoMetis.TypeKit.ValueObjects; -using Probes.Shapes; -namespace Probes.Ef; -public struct S_E_record_struct_DateOnly -{ -public readonly partial record struct V : IValue; -} -public static class C2_E_record_struct_DateOnly -{ - public static class Inner - { -public readonly partial record struct V : IValue; - } -} -public readonly partial record struct E_record_struct_DateOnly : IValue; -public static class C_E_record_struct_DateTime -{ -public readonly partial record struct V : IValue; -} -public static class C2_E_record_struct_DateTime -{ - public static class Inner - { -public readonly partial record struct V : IValue; - } -} -public readonly partial record struct E_record_struct_DateTime : IValue; -public readonly partial record struct E_record_struct_DateTimeOffset : IValue; -public static class C_E_record_struct_Guid -{ -public readonly partial record struct V : IValue; -} -public static class C2_E_record_struct_Guid -{ - public static class Inner - { -public readonly partial record struct V : IValue; - } -} -public readonly partial record struct E_record_struct_Guid : IValue; -public static class C2_E_record_struct_TimeOnly -{ - public static class Inner - { -public readonly partial record struct V : IValue; - } -} -public static class C_E_record_struct_TimeSpan -{ -public readonly partial record struct V : IValue; -} -public struct S_E_record_struct_TimeSpan -{ -public readonly partial record struct V : IValue; -} -public static class C_E_record_struct_Uri -{ -public readonly partial record struct V : IValue; -} -public static class C2_E_record_struct_Uri -{ - public static class Inner - { -public readonly partial record struct V : IValue; - } -} -public readonly partial record struct E_record_struct_Uri : IValue; -public struct S_E_record_struct_bool -{ -public readonly partial record struct V : IValue; -} -public readonly partial record struct E_record_struct_bool : IValue; -public static class C2_E_record_struct_byte -{ - public static class Inner - { -public readonly partial record struct V : IValue; - } -} -public readonly partial record struct E_record_struct_byte : IValue; -public static class C_E_record_struct_char -{ -public readonly partial record struct V : IValue; -} -public struct S_E_record_struct_char -{ -public readonly partial record struct V : IValue; -} -public readonly partial record struct E_record_struct_char : IValue; -public struct S_E_record_struct_decimal -{ -public readonly partial record struct V : IValue; -} -public static class C2_E_record_struct_decimal -{ - public static class Inner - { -public readonly partial record struct V : IValue; - } -} -public readonly partial record struct E_record_struct_decimal : IValue; -public struct S_E_record_struct_double -{ -public readonly partial record struct V : IValue; -} -public static class C2_E_record_struct_enum -{ - public static class Inner - { -public readonly partial record struct V : IValue; - } -} -public readonly partial record struct E_record_struct_enum : IValue; -public static class C2_E_record_struct_flags_enum -{ - public static class Inner - { -public readonly partial record struct V : IValue; - } -} -public readonly partial record struct E_record_struct_flags_enum : IValue; -public static class C2_E_record_struct_int -{ - public static class Inner - { -public readonly partial record struct V : IValue; - } -} -public readonly partial record struct E_record_struct_int : IValue; -public static class C2_E_record_struct_long -{ - public static class Inner - { -public readonly partial record struct V : IValue; - } -} -public readonly partial record struct E_record_struct_long : IValue; -public static class C_E_record_struct_string -{ -public readonly partial record struct V : IValue; -} -public struct S_E_record_struct_ulong -{ -public readonly partial record struct V : IValue; -} -public struct S_E_sealed_record_class_DateOnly -{ -public sealed partial record class V : IValue; -} -public static class C2_E_sealed_record_class_DateOnly -{ - public static class Inner - { -public sealed partial record class V : IValue; - } -} -public struct S_E_sealed_record_class_DateTime -{ -public sealed partial record class V : IValue; -} -public static class C2_E_sealed_record_class_DateTime -{ - public static class Inner - { -public sealed partial record class V : IValue; - } -} -public static class C_E_sealed_record_class_DateTimeOffset -{ -public sealed partial record class V : IValue; -} -public struct S_E_sealed_record_class_DateTimeOffset -{ -public sealed partial record class V : IValue; -} -public static class C2_E_sealed_record_class_DateTimeOffset -{ - public static class Inner - { -public sealed partial record class V : IValue; - } -} -public static class C2_E_sealed_record_class_Guid -{ - public static class Inner - { -public sealed partial record class V : IValue; - } -} -public static class C_E_sealed_record_class_TimeOnly -{ -public sealed partial record class V : IValue; -} -public struct S_E_sealed_record_class_TimeOnly -{ -public sealed partial record class V : IValue; -} -public static class C2_E_sealed_record_class_TimeOnly -{ - public static class Inner - { -public sealed partial record class V : IValue; - } -} -public sealed partial record class E_sealed_record_class_TimeSpan : IValue; -public static class C_E_sealed_record_class_Uri -{ -public sealed partial record class V : IValue; -} -public struct S_E_sealed_record_class_Uri -{ -public sealed partial record class V : IValue; -} -public static class C2_E_sealed_record_class_Uri -{ - public static class Inner - { -public sealed partial record class V : IValue; - } -} -public static class C2_E_sealed_record_class_bool -{ - public static class Inner - { -public sealed partial record class V : IValue; - } -} -public static class C_E_sealed_record_class_byte -{ -public sealed partial record class V : IValue; -} -public struct S_E_sealed_record_class_byte -{ -public sealed partial record class V : IValue; -} -public struct S_E_sealed_record_class_char -{ -public sealed partial record class V : IValue; -} -public sealed partial record class E_sealed_record_class_char : IValue; -public static class C_E_sealed_record_class_decimal -{ -public sealed partial record class V : IValue; -} -public static class C2_E_sealed_record_class_decimal -{ - public static class Inner - { -public sealed partial record class V : IValue; - } -} -public static class C2_E_sealed_record_class_double -{ - public static class Inner - { -public sealed partial record class V : IValue; - } -} -public sealed partial record class E_sealed_record_class_double : IValue; -public sealed partial record class E_sealed_record_class_enum : IValue; -public static class C_E_sealed_record_class_flags_enum -{ -public sealed partial record class V : IValue; -} -public sealed partial record class E_sealed_record_class_flags_enum : IValue; -public static class C_E_sealed_record_class_int -{ -public sealed partial record class V : IValue; -} -public struct S_E_sealed_record_class_int -{ -public sealed partial record class V : IValue; -} -public static class C_E_sealed_record_class_long -{ -public sealed partial record class V : IValue; -} -public static class C_E_sealed_record_class_string -{ -public sealed partial record class V : IValue; -} -public struct S_E_sealed_record_class_string -{ -public sealed partial record class V : IValue; -} -public sealed partial record class E_sealed_record_class_string : IValue; -public static class C_E_sealed_record_class_ulong -{ -public sealed partial record class V : IValue; -} -public struct S_E_validated_class_DateOnly -{ -public sealed partial record class V : IValidatedValue -{ - public static Result Create(DateOnly value) => - EqualityComparer.Default.Equals(value, DateOnly.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public struct S_E_validated_class_DateTimeOffset -{ -public sealed partial record class V : IValidatedValue -{ - public static Result Create(DateTimeOffset value) => - EqualityComparer.Default.Equals(value, DateTimeOffset.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public static class C_E_validated_class_Guid -{ -public sealed partial record class V : IValidatedValue -{ - public static Result Create(Guid value) => - EqualityComparer.Default.Equals(value, Guid.Empty) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public struct S_E_validated_class_Guid -{ -public sealed partial record class V : IValidatedValue -{ - public static Result Create(Guid value) => - EqualityComparer.Default.Equals(value, Guid.Empty) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public sealed partial record class E_validated_class_Guid : IValidatedValue -{ - public static Result Create(Guid value) => - EqualityComparer.Default.Equals(value, Guid.Empty) ? Result.Error(ShapeFault.Refused) : new E_validated_class_Guid(value); -} -public static class C_E_validated_class_TimeOnly -{ -public sealed partial record class V : IValidatedValue -{ - public static Result Create(TimeOnly value) => - EqualityComparer.Default.Equals(value, TimeOnly.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public sealed partial record class E_validated_class_TimeOnly : IValidatedValue -{ - public static Result Create(TimeOnly value) => - EqualityComparer.Default.Equals(value, TimeOnly.MinValue) ? Result.Error(ShapeFault.Refused) : new E_validated_class_TimeOnly(value); -} -public static class C2_E_validated_class_TimeSpan -{ - public static class Inner - { -public sealed partial record class V : IValidatedValue -{ - public static Result Create(TimeSpan value) => - EqualityComparer.Default.Equals(value, TimeSpan.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); -} - } -} -public struct S_E_validated_class_Uri -{ -public sealed partial record class V : IValidatedValue -{ - public static Result Create(Uri value) => - EqualityComparer.Default.Equals(value, new Uri("https://example.com/refused")) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public static class C_E_validated_class_bool -{ -public sealed partial record class V : IValidatedValue -{ - public static Result Create(bool value) => - EqualityComparer.Default.Equals(value, false) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public struct S_E_validated_class_bool -{ -public sealed partial record class V : IValidatedValue -{ - public static Result Create(bool value) => - EqualityComparer.Default.Equals(value, false) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public sealed partial record class E_validated_class_bool : IValidatedValue -{ - public static Result Create(bool value) => - EqualityComparer.Default.Equals(value, false) ? Result.Error(ShapeFault.Refused) : new E_validated_class_bool(value); -} -public static class C_E_validated_class_byte -{ -public sealed partial record class V : IValidatedValue -{ - public static Result Create(byte value) => - EqualityComparer.Default.Equals(value, (byte)255) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public sealed partial record class E_validated_class_byte : IValidatedValue -{ - public static Result Create(byte value) => - EqualityComparer.Default.Equals(value, (byte)255) ? Result.Error(ShapeFault.Refused) : new E_validated_class_byte(value); -} -public static class C_E_validated_class_char -{ -public sealed partial record class V : IValidatedValue -{ - public static Result Create(char value) => - EqualityComparer.Default.Equals(value, 'x') ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public static class C2_E_validated_class_char -{ - public static class Inner - { -public sealed partial record class V : IValidatedValue -{ - public static Result Create(char value) => - EqualityComparer.Default.Equals(value, 'x') ? Result.Error(ShapeFault.Refused) : new V(value); -} - } -} -public static class C_E_validated_class_double -{ -public sealed partial record class V : IValidatedValue -{ - public static Result Create(double value) => - EqualityComparer.Default.Equals(value, -1.0) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public static class C2_E_validated_class_double -{ - public static class Inner - { -public sealed partial record class V : IValidatedValue -{ - public static Result Create(double value) => - EqualityComparer.Default.Equals(value, -1.0) ? Result.Error(ShapeFault.Refused) : new V(value); -} - } -} -public struct S_E_validated_class_enum -{ -public sealed partial record class V : IValidatedValue -{ - public static Result Create(PEnum value) => - EqualityComparer.Default.Equals(value, (PEnum)99) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public static class C2_E_validated_class_enum -{ - public static class Inner - { -public sealed partial record class V : IValidatedValue -{ - public static Result Create(PEnum value) => - EqualityComparer.Default.Equals(value, (PEnum)99) ? Result.Error(ShapeFault.Refused) : new V(value); -} - } -} -public static class C_E_validated_class_flags_enum -{ -public sealed partial record class V : IValidatedValue -{ - public static Result Create(PFlags value) => - EqualityComparer.Default.Equals(value, (PFlags)0) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public struct S_E_validated_class_flags_enum -{ -public sealed partial record class V : IValidatedValue -{ - public static Result Create(PFlags value) => - EqualityComparer.Default.Equals(value, (PFlags)0) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public static class C2_E_validated_class_flags_enum -{ - public static class Inner - { -public sealed partial record class V : IValidatedValue -{ - public static Result Create(PFlags value) => - EqualityComparer.Default.Equals(value, (PFlags)0) ? Result.Error(ShapeFault.Refused) : new V(value); -} - } -} -public static class C2_E_validated_class_int -{ - public static class Inner - { -public sealed partial record class V : IValidatedValue -{ - public static Result Create(int value) => - EqualityComparer.Default.Equals(value, -1) ? Result.Error(ShapeFault.Refused) : new V(value); -} - } -} -public sealed partial record class E_validated_class_int : IValidatedValue -{ - public static Result Create(int value) => - EqualityComparer.Default.Equals(value, -1) ? Result.Error(ShapeFault.Refused) : new E_validated_class_int(value); -} -public struct S_E_validated_class_long -{ -public sealed partial record class V : IValidatedValue -{ - public static Result Create(long value) => - EqualityComparer.Default.Equals(value, -1L) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public static class C2_E_validated_class_long -{ - public static class Inner - { -public sealed partial record class V : IValidatedValue -{ - public static Result Create(long value) => - EqualityComparer.Default.Equals(value, -1L) ? Result.Error(ShapeFault.Refused) : new V(value); -} - } -} -public struct S_E_validated_class_string -{ -public sealed partial record class V : IValidatedValue -{ - public static Result Create(string value) => - EqualityComparer.Default.Equals(value, "SECRET-7f3a") ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public static class C2_E_validated_class_string -{ - public static class Inner - { -public sealed partial record class V : IValidatedValue -{ - public static Result Create(string value) => - EqualityComparer.Default.Equals(value, "SECRET-7f3a") ? Result.Error(ShapeFault.Refused) : new V(value); -} - } -} -public struct S_E_validated_class_ulong -{ -public sealed partial record class V : IValidatedValue -{ - public static Result Create(ulong value) => - EqualityComparer.Default.Equals(value, 0UL) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public sealed partial record class E_validated_class_ulong : IValidatedValue -{ - public static Result Create(ulong value) => - EqualityComparer.Default.Equals(value, 0UL) ? Result.Error(ShapeFault.Refused) : new E_validated_class_ulong(value); -} -public static class C_E_validated_struct_DateOnly -{ -public readonly partial record struct V : IValidatedValue -{ - public static Result Create(DateOnly value) => - EqualityComparer.Default.Equals(value, DateOnly.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public readonly partial record struct E_validated_struct_DateOnly : IValidatedValue -{ - public static Result Create(DateOnly value) => - EqualityComparer.Default.Equals(value, DateOnly.MinValue) ? Result.Error(ShapeFault.Refused) : new E_validated_struct_DateOnly(value); -} -public static class C_E_validated_struct_DateTime -{ -public readonly partial record struct V : IValidatedValue -{ - public static Result Create(DateTime value) => - EqualityComparer.Default.Equals(value, DateTime.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public struct S_E_validated_struct_DateTime -{ -public readonly partial record struct V : IValidatedValue -{ - public static Result Create(DateTime value) => - EqualityComparer.Default.Equals(value, DateTime.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public static class C_E_validated_struct_DateTimeOffset -{ -public readonly partial record struct V : IValidatedValue -{ - public static Result Create(DateTimeOffset value) => - EqualityComparer.Default.Equals(value, DateTimeOffset.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public static class C2_E_validated_struct_DateTimeOffset -{ - public static class Inner - { -public readonly partial record struct V : IValidatedValue -{ - public static Result Create(DateTimeOffset value) => - EqualityComparer.Default.Equals(value, DateTimeOffset.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); -} - } -} -public static class C_E_validated_struct_Guid -{ -public readonly partial record struct V : IValidatedValue -{ - public static Result Create(Guid value) => - EqualityComparer.Default.Equals(value, Guid.Empty) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public struct S_E_validated_struct_Guid -{ -public readonly partial record struct V : IValidatedValue -{ - public static Result Create(Guid value) => - EqualityComparer.Default.Equals(value, Guid.Empty) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public struct S_E_validated_struct_TimeOnly -{ -public readonly partial record struct V : IValidatedValue -{ - public static Result Create(TimeOnly value) => - EqualityComparer.Default.Equals(value, TimeOnly.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public readonly partial record struct E_validated_struct_TimeOnly : IValidatedValue -{ - public static Result Create(TimeOnly value) => - EqualityComparer.Default.Equals(value, TimeOnly.MinValue) ? Result.Error(ShapeFault.Refused) : new E_validated_struct_TimeOnly(value); -} -public static class C_E_validated_struct_TimeSpan -{ -public readonly partial record struct V : IValidatedValue -{ - public static Result Create(TimeSpan value) => - EqualityComparer.Default.Equals(value, TimeSpan.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public readonly partial record struct E_validated_struct_Uri : IValidatedValue -{ - public static Result Create(Uri value) => - EqualityComparer.Default.Equals(value, new Uri("https://example.com/refused")) ? Result.Error(ShapeFault.Refused) : new E_validated_struct_Uri(value); -} -public static class C_E_validated_struct_bool -{ -public readonly partial record struct V : IValidatedValue -{ - public static Result Create(bool value) => - EqualityComparer.Default.Equals(value, false) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public struct S_E_validated_struct_bool -{ -public readonly partial record struct V : IValidatedValue -{ - public static Result Create(bool value) => - EqualityComparer.Default.Equals(value, false) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public static class C2_E_validated_struct_bool -{ - public static class Inner - { -public readonly partial record struct V : IValidatedValue -{ - public static Result Create(bool value) => - EqualityComparer.Default.Equals(value, false) ? Result.Error(ShapeFault.Refused) : new V(value); -} - } -} -public struct S_E_validated_struct_byte -{ -public readonly partial record struct V : IValidatedValue -{ - public static Result Create(byte value) => - EqualityComparer.Default.Equals(value, (byte)255) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public static class C2_E_validated_struct_byte -{ - public static class Inner - { -public readonly partial record struct V : IValidatedValue -{ - public static Result Create(byte value) => - EqualityComparer.Default.Equals(value, (byte)255) ? Result.Error(ShapeFault.Refused) : new V(value); -} - } -} -public static class C2_E_validated_struct_char -{ - public static class Inner - { -public readonly partial record struct V : IValidatedValue -{ - public static Result Create(char value) => - EqualityComparer.Default.Equals(value, 'x') ? Result.Error(ShapeFault.Refused) : new V(value); -} - } -} -public readonly partial record struct E_validated_struct_char : IValidatedValue -{ - public static Result Create(char value) => - EqualityComparer.Default.Equals(value, 'x') ? Result.Error(ShapeFault.Refused) : new E_validated_struct_char(value); -} -public static class C_E_validated_struct_decimal -{ -public readonly partial record struct V : IValidatedValue -{ - public static Result Create(decimal value) => - EqualityComparer.Default.Equals(value, -1m) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public struct S_E_validated_struct_decimal -{ -public readonly partial record struct V : IValidatedValue -{ - public static Result Create(decimal value) => - EqualityComparer.Default.Equals(value, -1m) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public static class C2_E_validated_struct_decimal -{ - public static class Inner - { -public readonly partial record struct V : IValidatedValue -{ - public static Result Create(decimal value) => - EqualityComparer.Default.Equals(value, -1m) ? Result.Error(ShapeFault.Refused) : new V(value); -} - } -} -public readonly partial record struct E_validated_struct_decimal : IValidatedValue -{ - public static Result Create(decimal value) => - EqualityComparer.Default.Equals(value, -1m) ? Result.Error(ShapeFault.Refused) : new E_validated_struct_decimal(value); -} -public static class C_E_validated_struct_double -{ -public readonly partial record struct V : IValidatedValue -{ - public static Result Create(double value) => - EqualityComparer.Default.Equals(value, -1.0) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public readonly partial record struct E_validated_struct_double : IValidatedValue -{ - public static Result Create(double value) => - EqualityComparer.Default.Equals(value, -1.0) ? Result.Error(ShapeFault.Refused) : new E_validated_struct_double(value); -} -public static class C_E_validated_struct_enum -{ -public readonly partial record struct V : IValidatedValue -{ - public static Result Create(PEnum value) => - EqualityComparer.Default.Equals(value, (PEnum)99) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public struct S_E_validated_struct_flags_enum -{ -public readonly partial record struct V : IValidatedValue -{ - public static Result Create(PFlags value) => - EqualityComparer.Default.Equals(value, (PFlags)0) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public static class C_E_validated_struct_int -{ -public readonly partial record struct V : IValidatedValue -{ - public static Result Create(int value) => - EqualityComparer.Default.Equals(value, -1) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public struct S_E_validated_struct_int -{ -public readonly partial record struct V : IValidatedValue -{ - public static Result Create(int value) => - EqualityComparer.Default.Equals(value, -1) ? Result.Error(ShapeFault.Refused) : new V(value); -} -} -public static class C2_E_validated_struct_long -{ - public static class Inner - { -public readonly partial record struct V : IValidatedValue -{ - public static Result Create(long value) => - EqualityComparer.Default.Equals(value, -1L) ? Result.Error(ShapeFault.Refused) : new V(value); -} - } -} -public readonly partial record struct E_validated_struct_long : IValidatedValue -{ - public static Result Create(long value) => - EqualityComparer.Default.Equals(value, -1L) ? Result.Error(ShapeFault.Refused) : new E_validated_struct_long(value); -} -public static class C2_E_validated_struct_string -{ - public static class Inner - { -public readonly partial record struct V : IValidatedValue -{ - public static Result Create(string value) => - EqualityComparer.Default.Equals(value, "SECRET-7f3a") ? Result.Error(ShapeFault.Refused) : new V(value); -} - } -} -public readonly partial record struct E_validated_struct_string : IValidatedValue -{ - public static Result Create(string value) => - EqualityComparer.Default.Equals(value, "SECRET-7f3a") ? Result.Error(ShapeFault.Refused) : new E_validated_struct_string(value); -} -public static class C2_E_validated_struct_ulong -{ - public static class Inner - { -public readonly partial record struct V : IValidatedValue -{ - public static Result Create(ulong value) => - EqualityComparer.Default.Equals(value, 0UL) ? Result.Error(ShapeFault.Refused) : new V(value); -} - } -} -public static partial class EfRegistry -{ - static partial void Fill(List e) - { - e.Add(new EfEntry("record-struct", "DateOnly", "nested-in-struct", typeof(S_E_record_struct_DateOnly.V), typeof(DateOnly), () => new DateOnly(2026,10,15))); - e.Add(new EfEntry("record-struct", "DateOnly", "nested-two-levels", typeof(C2_E_record_struct_DateOnly.Inner.V), typeof(DateOnly), () => new DateOnly(2026,10,15))); - e.Add(new EfEntry("record-struct", "DateOnly", "top-level", typeof(E_record_struct_DateOnly), typeof(DateOnly), () => new DateOnly(2026,10,15))); - e.Add(new EfEntry("record-struct", "DateTime", "nested-in-class", typeof(C_E_record_struct_DateTime.V), typeof(DateTime), () => new DateTime(2026,10,15,8,30,0,DateTimeKind.Utc))); - e.Add(new EfEntry("record-struct", "DateTime", "nested-two-levels", typeof(C2_E_record_struct_DateTime.Inner.V), typeof(DateTime), () => new DateTime(2026,10,15,8,30,0,DateTimeKind.Utc))); - e.Add(new EfEntry("record-struct", "DateTime", "top-level", typeof(E_record_struct_DateTime), typeof(DateTime), () => new DateTime(2026,10,15,8,30,0,DateTimeKind.Utc))); - e.Add(new EfEntry("record-struct", "DateTimeOffset", "top-level", typeof(E_record_struct_DateTimeOffset), typeof(DateTimeOffset), () => new DateTimeOffset(2026,10,15,8,30,0,TimeSpan.Zero))); - e.Add(new EfEntry("record-struct", "Guid", "nested-in-class", typeof(C_E_record_struct_Guid.V), typeof(Guid), () => Guid.Parse("11111111-1111-1111-1111-111111111111"))); - e.Add(new EfEntry("record-struct", "Guid", "nested-two-levels", typeof(C2_E_record_struct_Guid.Inner.V), typeof(Guid), () => Guid.Parse("11111111-1111-1111-1111-111111111111"))); - e.Add(new EfEntry("record-struct", "Guid", "top-level", typeof(E_record_struct_Guid), typeof(Guid), () => Guid.Parse("11111111-1111-1111-1111-111111111111"))); - e.Add(new EfEntry("record-struct", "TimeOnly", "nested-two-levels", typeof(C2_E_record_struct_TimeOnly.Inner.V), typeof(TimeOnly), () => new TimeOnly(8,30,15))); - e.Add(new EfEntry("record-struct", "TimeSpan", "nested-in-class", typeof(C_E_record_struct_TimeSpan.V), typeof(TimeSpan), () => TimeSpan.FromMinutes(90))); - e.Add(new EfEntry("record-struct", "TimeSpan", "nested-in-struct", typeof(S_E_record_struct_TimeSpan.V), typeof(TimeSpan), () => TimeSpan.FromMinutes(90))); - e.Add(new EfEntry("record-struct", "Uri", "nested-in-class", typeof(C_E_record_struct_Uri.V), typeof(Uri), () => new Uri("https://example.com/a"))); - e.Add(new EfEntry("record-struct", "Uri", "nested-two-levels", typeof(C2_E_record_struct_Uri.Inner.V), typeof(Uri), () => new Uri("https://example.com/a"))); - e.Add(new EfEntry("record-struct", "Uri", "top-level", typeof(E_record_struct_Uri), typeof(Uri), () => new Uri("https://example.com/a"))); - e.Add(new EfEntry("record-struct", "bool", "nested-in-struct", typeof(S_E_record_struct_bool.V), typeof(bool), () => true)); - e.Add(new EfEntry("record-struct", "bool", "top-level", typeof(E_record_struct_bool), typeof(bool), () => true)); - e.Add(new EfEntry("record-struct", "byte", "nested-two-levels", typeof(C2_E_record_struct_byte.Inner.V), typeof(byte), () => (byte)7)); - e.Add(new EfEntry("record-struct", "byte", "top-level", typeof(E_record_struct_byte), typeof(byte), () => (byte)7)); - e.Add(new EfEntry("record-struct", "char", "nested-in-class", typeof(C_E_record_struct_char.V), typeof(char), () => 'a')); - e.Add(new EfEntry("record-struct", "char", "nested-in-struct", typeof(S_E_record_struct_char.V), typeof(char), () => 'a')); - e.Add(new EfEntry("record-struct", "char", "top-level", typeof(E_record_struct_char), typeof(char), () => 'a')); - e.Add(new EfEntry("record-struct", "decimal", "nested-in-struct", typeof(S_E_record_struct_decimal.V), typeof(decimal), () => 7.80m)); - e.Add(new EfEntry("record-struct", "decimal", "nested-two-levels", typeof(C2_E_record_struct_decimal.Inner.V), typeof(decimal), () => 7.80m)); - e.Add(new EfEntry("record-struct", "decimal", "top-level", typeof(E_record_struct_decimal), typeof(decimal), () => 7.80m)); - e.Add(new EfEntry("record-struct", "double", "nested-in-struct", typeof(S_E_record_struct_double.V), typeof(double), () => 1.5)); - e.Add(new EfEntry("record-struct", "enum", "nested-two-levels", typeof(C2_E_record_struct_enum.Inner.V), typeof(PEnum), () => PEnum.B)); - e.Add(new EfEntry("record-struct", "enum", "top-level", typeof(E_record_struct_enum), typeof(PEnum), () => PEnum.B)); - e.Add(new EfEntry("record-struct", "flags-enum", "nested-two-levels", typeof(C2_E_record_struct_flags_enum.Inner.V), typeof(PFlags), () => PFlags.P | PFlags.Q)); - e.Add(new EfEntry("record-struct", "flags-enum", "top-level", typeof(E_record_struct_flags_enum), typeof(PFlags), () => PFlags.P | PFlags.Q)); - e.Add(new EfEntry("record-struct", "int", "nested-two-levels", typeof(C2_E_record_struct_int.Inner.V), typeof(int), () => 42)); - e.Add(new EfEntry("record-struct", "int", "top-level", typeof(E_record_struct_int), typeof(int), () => 42)); - e.Add(new EfEntry("record-struct", "long", "nested-two-levels", typeof(C2_E_record_struct_long.Inner.V), typeof(long), () => 4200000000L)); - e.Add(new EfEntry("record-struct", "long", "top-level", typeof(E_record_struct_long), typeof(long), () => 4200000000L)); - e.Add(new EfEntry("record-struct", "string", "nested-in-class", typeof(C_E_record_struct_string.V), typeof(string), () => "alpha")); - e.Add(new EfEntry("record-struct", "ulong", "nested-in-struct", typeof(S_E_record_struct_ulong.V), typeof(ulong), () => 4200000000UL)); - e.Add(new EfEntry("sealed-record-class", "DateOnly", "nested-in-struct", typeof(S_E_sealed_record_class_DateOnly.V), typeof(DateOnly), () => new DateOnly(2026,10,15))); - e.Add(new EfEntry("sealed-record-class", "DateOnly", "nested-two-levels", typeof(C2_E_sealed_record_class_DateOnly.Inner.V), typeof(DateOnly), () => new DateOnly(2026,10,15))); - e.Add(new EfEntry("sealed-record-class", "DateTime", "nested-in-struct", typeof(S_E_sealed_record_class_DateTime.V), typeof(DateTime), () => new DateTime(2026,10,15,8,30,0,DateTimeKind.Utc))); - e.Add(new EfEntry("sealed-record-class", "DateTime", "nested-two-levels", typeof(C2_E_sealed_record_class_DateTime.Inner.V), typeof(DateTime), () => new DateTime(2026,10,15,8,30,0,DateTimeKind.Utc))); - e.Add(new EfEntry("sealed-record-class", "DateTimeOffset", "nested-in-class", typeof(C_E_sealed_record_class_DateTimeOffset.V), typeof(DateTimeOffset), () => new DateTimeOffset(2026,10,15,8,30,0,TimeSpan.Zero))); - e.Add(new EfEntry("sealed-record-class", "DateTimeOffset", "nested-in-struct", typeof(S_E_sealed_record_class_DateTimeOffset.V), typeof(DateTimeOffset), () => new DateTimeOffset(2026,10,15,8,30,0,TimeSpan.Zero))); - e.Add(new EfEntry("sealed-record-class", "DateTimeOffset", "nested-two-levels", typeof(C2_E_sealed_record_class_DateTimeOffset.Inner.V), typeof(DateTimeOffset), () => new DateTimeOffset(2026,10,15,8,30,0,TimeSpan.Zero))); - e.Add(new EfEntry("sealed-record-class", "Guid", "nested-two-levels", typeof(C2_E_sealed_record_class_Guid.Inner.V), typeof(Guid), () => Guid.Parse("11111111-1111-1111-1111-111111111111"))); - e.Add(new EfEntry("sealed-record-class", "TimeOnly", "nested-in-class", typeof(C_E_sealed_record_class_TimeOnly.V), typeof(TimeOnly), () => new TimeOnly(8,30,15))); - e.Add(new EfEntry("sealed-record-class", "TimeOnly", "nested-in-struct", typeof(S_E_sealed_record_class_TimeOnly.V), typeof(TimeOnly), () => new TimeOnly(8,30,15))); - e.Add(new EfEntry("sealed-record-class", "TimeOnly", "nested-two-levels", typeof(C2_E_sealed_record_class_TimeOnly.Inner.V), typeof(TimeOnly), () => new TimeOnly(8,30,15))); - e.Add(new EfEntry("sealed-record-class", "TimeSpan", "top-level", typeof(E_sealed_record_class_TimeSpan), typeof(TimeSpan), () => TimeSpan.FromMinutes(90))); - e.Add(new EfEntry("sealed-record-class", "Uri", "nested-in-class", typeof(C_E_sealed_record_class_Uri.V), typeof(Uri), () => new Uri("https://example.com/a"))); - e.Add(new EfEntry("sealed-record-class", "Uri", "nested-in-struct", typeof(S_E_sealed_record_class_Uri.V), typeof(Uri), () => new Uri("https://example.com/a"))); - e.Add(new EfEntry("sealed-record-class", "Uri", "nested-two-levels", typeof(C2_E_sealed_record_class_Uri.Inner.V), typeof(Uri), () => new Uri("https://example.com/a"))); - e.Add(new EfEntry("sealed-record-class", "bool", "nested-two-levels", typeof(C2_E_sealed_record_class_bool.Inner.V), typeof(bool), () => true)); - e.Add(new EfEntry("sealed-record-class", "byte", "nested-in-class", typeof(C_E_sealed_record_class_byte.V), typeof(byte), () => (byte)7)); - e.Add(new EfEntry("sealed-record-class", "byte", "nested-in-struct", typeof(S_E_sealed_record_class_byte.V), typeof(byte), () => (byte)7)); - e.Add(new EfEntry("sealed-record-class", "char", "nested-in-struct", typeof(S_E_sealed_record_class_char.V), typeof(char), () => 'a')); - e.Add(new EfEntry("sealed-record-class", "char", "top-level", typeof(E_sealed_record_class_char), typeof(char), () => 'a')); - e.Add(new EfEntry("sealed-record-class", "decimal", "nested-in-class", typeof(C_E_sealed_record_class_decimal.V), typeof(decimal), () => 7.80m)); - e.Add(new EfEntry("sealed-record-class", "decimal", "nested-two-levels", typeof(C2_E_sealed_record_class_decimal.Inner.V), typeof(decimal), () => 7.80m)); - e.Add(new EfEntry("sealed-record-class", "double", "nested-two-levels", typeof(C2_E_sealed_record_class_double.Inner.V), typeof(double), () => 1.5)); - e.Add(new EfEntry("sealed-record-class", "double", "top-level", typeof(E_sealed_record_class_double), typeof(double), () => 1.5)); - e.Add(new EfEntry("sealed-record-class", "enum", "top-level", typeof(E_sealed_record_class_enum), typeof(PEnum), () => PEnum.B)); - e.Add(new EfEntry("sealed-record-class", "flags-enum", "nested-in-class", typeof(C_E_sealed_record_class_flags_enum.V), typeof(PFlags), () => PFlags.P | PFlags.Q)); - e.Add(new EfEntry("sealed-record-class", "flags-enum", "top-level", typeof(E_sealed_record_class_flags_enum), typeof(PFlags), () => PFlags.P | PFlags.Q)); - e.Add(new EfEntry("sealed-record-class", "int", "nested-in-class", typeof(C_E_sealed_record_class_int.V), typeof(int), () => 42)); - e.Add(new EfEntry("sealed-record-class", "int", "nested-in-struct", typeof(S_E_sealed_record_class_int.V), typeof(int), () => 42)); - e.Add(new EfEntry("sealed-record-class", "long", "nested-in-class", typeof(C_E_sealed_record_class_long.V), typeof(long), () => 4200000000L)); - e.Add(new EfEntry("sealed-record-class", "string", "nested-in-class", typeof(C_E_sealed_record_class_string.V), typeof(string), () => "alpha")); - e.Add(new EfEntry("sealed-record-class", "string", "nested-in-struct", typeof(S_E_sealed_record_class_string.V), typeof(string), () => "alpha")); - e.Add(new EfEntry("sealed-record-class", "string", "top-level", typeof(E_sealed_record_class_string), typeof(string), () => "alpha")); - e.Add(new EfEntry("sealed-record-class", "ulong", "nested-in-class", typeof(C_E_sealed_record_class_ulong.V), typeof(ulong), () => 4200000000UL)); - e.Add(new EfEntry("validated-class", "DateOnly", "nested-in-struct", typeof(S_E_validated_class_DateOnly.V), typeof(DateOnly), () => new DateOnly(2026,10,15))); - e.Add(new EfEntry("validated-class", "DateTimeOffset", "nested-in-struct", typeof(S_E_validated_class_DateTimeOffset.V), typeof(DateTimeOffset), () => new DateTimeOffset(2026,10,15,8,30,0,TimeSpan.Zero))); - e.Add(new EfEntry("validated-class", "Guid", "nested-in-class", typeof(C_E_validated_class_Guid.V), typeof(Guid), () => Guid.Parse("11111111-1111-1111-1111-111111111111"))); - e.Add(new EfEntry("validated-class", "Guid", "nested-in-struct", typeof(S_E_validated_class_Guid.V), typeof(Guid), () => Guid.Parse("11111111-1111-1111-1111-111111111111"))); - e.Add(new EfEntry("validated-class", "Guid", "top-level", typeof(E_validated_class_Guid), typeof(Guid), () => Guid.Parse("11111111-1111-1111-1111-111111111111"))); - e.Add(new EfEntry("validated-class", "TimeOnly", "nested-in-class", typeof(C_E_validated_class_TimeOnly.V), typeof(TimeOnly), () => new TimeOnly(8,30,15))); - e.Add(new EfEntry("validated-class", "TimeOnly", "top-level", typeof(E_validated_class_TimeOnly), typeof(TimeOnly), () => new TimeOnly(8,30,15))); - e.Add(new EfEntry("validated-class", "TimeSpan", "nested-two-levels", typeof(C2_E_validated_class_TimeSpan.Inner.V), typeof(TimeSpan), () => TimeSpan.FromMinutes(90))); - e.Add(new EfEntry("validated-class", "Uri", "nested-in-struct", typeof(S_E_validated_class_Uri.V), typeof(Uri), () => new Uri("https://example.com/a"))); - e.Add(new EfEntry("validated-class", "bool", "nested-in-class", typeof(C_E_validated_class_bool.V), typeof(bool), () => true)); - e.Add(new EfEntry("validated-class", "bool", "nested-in-struct", typeof(S_E_validated_class_bool.V), typeof(bool), () => true)); - e.Add(new EfEntry("validated-class", "bool", "top-level", typeof(E_validated_class_bool), typeof(bool), () => true)); - e.Add(new EfEntry("validated-class", "byte", "nested-in-class", typeof(C_E_validated_class_byte.V), typeof(byte), () => (byte)7)); - e.Add(new EfEntry("validated-class", "byte", "top-level", typeof(E_validated_class_byte), typeof(byte), () => (byte)7)); - e.Add(new EfEntry("validated-class", "char", "nested-in-class", typeof(C_E_validated_class_char.V), typeof(char), () => 'a')); - e.Add(new EfEntry("validated-class", "char", "nested-two-levels", typeof(C2_E_validated_class_char.Inner.V), typeof(char), () => 'a')); - e.Add(new EfEntry("validated-class", "double", "nested-in-class", typeof(C_E_validated_class_double.V), typeof(double), () => 1.5)); - e.Add(new EfEntry("validated-class", "double", "nested-two-levels", typeof(C2_E_validated_class_double.Inner.V), typeof(double), () => 1.5)); - e.Add(new EfEntry("validated-class", "enum", "nested-in-struct", typeof(S_E_validated_class_enum.V), typeof(PEnum), () => PEnum.B)); - e.Add(new EfEntry("validated-class", "enum", "nested-two-levels", typeof(C2_E_validated_class_enum.Inner.V), typeof(PEnum), () => PEnum.B)); - e.Add(new EfEntry("validated-class", "flags-enum", "nested-in-class", typeof(C_E_validated_class_flags_enum.V), typeof(PFlags), () => PFlags.P | PFlags.Q)); - e.Add(new EfEntry("validated-class", "flags-enum", "nested-in-struct", typeof(S_E_validated_class_flags_enum.V), typeof(PFlags), () => PFlags.P | PFlags.Q)); - e.Add(new EfEntry("validated-class", "flags-enum", "nested-two-levels", typeof(C2_E_validated_class_flags_enum.Inner.V), typeof(PFlags), () => PFlags.P | PFlags.Q)); - e.Add(new EfEntry("validated-class", "int", "nested-two-levels", typeof(C2_E_validated_class_int.Inner.V), typeof(int), () => 42)); - e.Add(new EfEntry("validated-class", "int", "top-level", typeof(E_validated_class_int), typeof(int), () => 42)); - e.Add(new EfEntry("validated-class", "long", "nested-in-struct", typeof(S_E_validated_class_long.V), typeof(long), () => 4200000000L)); - e.Add(new EfEntry("validated-class", "long", "nested-two-levels", typeof(C2_E_validated_class_long.Inner.V), typeof(long), () => 4200000000L)); - e.Add(new EfEntry("validated-class", "string", "nested-in-struct", typeof(S_E_validated_class_string.V), typeof(string), () => "alpha")); - e.Add(new EfEntry("validated-class", "string", "nested-two-levels", typeof(C2_E_validated_class_string.Inner.V), typeof(string), () => "alpha")); - e.Add(new EfEntry("validated-class", "ulong", "nested-in-struct", typeof(S_E_validated_class_ulong.V), typeof(ulong), () => 4200000000UL)); - e.Add(new EfEntry("validated-class", "ulong", "top-level", typeof(E_validated_class_ulong), typeof(ulong), () => 4200000000UL)); - e.Add(new EfEntry("validated-struct", "DateOnly", "nested-in-class", typeof(C_E_validated_struct_DateOnly.V), typeof(DateOnly), () => new DateOnly(2026,10,15))); - e.Add(new EfEntry("validated-struct", "DateOnly", "top-level", typeof(E_validated_struct_DateOnly), typeof(DateOnly), () => new DateOnly(2026,10,15))); - e.Add(new EfEntry("validated-struct", "DateTime", "nested-in-class", typeof(C_E_validated_struct_DateTime.V), typeof(DateTime), () => new DateTime(2026,10,15,8,30,0,DateTimeKind.Utc))); - e.Add(new EfEntry("validated-struct", "DateTime", "nested-in-struct", typeof(S_E_validated_struct_DateTime.V), typeof(DateTime), () => new DateTime(2026,10,15,8,30,0,DateTimeKind.Utc))); - e.Add(new EfEntry("validated-struct", "DateTimeOffset", "nested-in-class", typeof(C_E_validated_struct_DateTimeOffset.V), typeof(DateTimeOffset), () => new DateTimeOffset(2026,10,15,8,30,0,TimeSpan.Zero))); - e.Add(new EfEntry("validated-struct", "DateTimeOffset", "nested-two-levels", typeof(C2_E_validated_struct_DateTimeOffset.Inner.V), typeof(DateTimeOffset), () => new DateTimeOffset(2026,10,15,8,30,0,TimeSpan.Zero))); - e.Add(new EfEntry("validated-struct", "Guid", "nested-in-class", typeof(C_E_validated_struct_Guid.V), typeof(Guid), () => Guid.Parse("11111111-1111-1111-1111-111111111111"))); - e.Add(new EfEntry("validated-struct", "Guid", "nested-in-struct", typeof(S_E_validated_struct_Guid.V), typeof(Guid), () => Guid.Parse("11111111-1111-1111-1111-111111111111"))); - e.Add(new EfEntry("validated-struct", "TimeOnly", "nested-in-struct", typeof(S_E_validated_struct_TimeOnly.V), typeof(TimeOnly), () => new TimeOnly(8,30,15))); - e.Add(new EfEntry("validated-struct", "TimeOnly", "top-level", typeof(E_validated_struct_TimeOnly), typeof(TimeOnly), () => new TimeOnly(8,30,15))); - e.Add(new EfEntry("validated-struct", "TimeSpan", "nested-in-class", typeof(C_E_validated_struct_TimeSpan.V), typeof(TimeSpan), () => TimeSpan.FromMinutes(90))); - e.Add(new EfEntry("validated-struct", "Uri", "top-level", typeof(E_validated_struct_Uri), typeof(Uri), () => new Uri("https://example.com/a"))); - e.Add(new EfEntry("validated-struct", "bool", "nested-in-class", typeof(C_E_validated_struct_bool.V), typeof(bool), () => true)); - e.Add(new EfEntry("validated-struct", "bool", "nested-in-struct", typeof(S_E_validated_struct_bool.V), typeof(bool), () => true)); - e.Add(new EfEntry("validated-struct", "bool", "nested-two-levels", typeof(C2_E_validated_struct_bool.Inner.V), typeof(bool), () => true)); - e.Add(new EfEntry("validated-struct", "byte", "nested-in-struct", typeof(S_E_validated_struct_byte.V), typeof(byte), () => (byte)7)); - e.Add(new EfEntry("validated-struct", "byte", "nested-two-levels", typeof(C2_E_validated_struct_byte.Inner.V), typeof(byte), () => (byte)7)); - e.Add(new EfEntry("validated-struct", "char", "nested-two-levels", typeof(C2_E_validated_struct_char.Inner.V), typeof(char), () => 'a')); - e.Add(new EfEntry("validated-struct", "char", "top-level", typeof(E_validated_struct_char), typeof(char), () => 'a')); - e.Add(new EfEntry("validated-struct", "decimal", "nested-in-class", typeof(C_E_validated_struct_decimal.V), typeof(decimal), () => 7.80m)); - e.Add(new EfEntry("validated-struct", "decimal", "nested-in-struct", typeof(S_E_validated_struct_decimal.V), typeof(decimal), () => 7.80m)); - e.Add(new EfEntry("validated-struct", "decimal", "nested-two-levels", typeof(C2_E_validated_struct_decimal.Inner.V), typeof(decimal), () => 7.80m)); - e.Add(new EfEntry("validated-struct", "decimal", "top-level", typeof(E_validated_struct_decimal), typeof(decimal), () => 7.80m)); - e.Add(new EfEntry("validated-struct", "double", "nested-in-class", typeof(C_E_validated_struct_double.V), typeof(double), () => 1.5)); - e.Add(new EfEntry("validated-struct", "double", "top-level", typeof(E_validated_struct_double), typeof(double), () => 1.5)); - e.Add(new EfEntry("validated-struct", "enum", "nested-in-class", typeof(C_E_validated_struct_enum.V), typeof(PEnum), () => PEnum.B)); - e.Add(new EfEntry("validated-struct", "flags-enum", "nested-in-struct", typeof(S_E_validated_struct_flags_enum.V), typeof(PFlags), () => PFlags.P | PFlags.Q)); - e.Add(new EfEntry("validated-struct", "int", "nested-in-class", typeof(C_E_validated_struct_int.V), typeof(int), () => 42)); - e.Add(new EfEntry("validated-struct", "int", "nested-in-struct", typeof(S_E_validated_struct_int.V), typeof(int), () => 42)); - e.Add(new EfEntry("validated-struct", "long", "nested-two-levels", typeof(C2_E_validated_struct_long.Inner.V), typeof(long), () => 4200000000L)); - e.Add(new EfEntry("validated-struct", "long", "top-level", typeof(E_validated_struct_long), typeof(long), () => 4200000000L)); - e.Add(new EfEntry("validated-struct", "string", "nested-two-levels", typeof(C2_E_validated_struct_string.Inner.V), typeof(string), () => "alpha")); - e.Add(new EfEntry("validated-struct", "string", "top-level", typeof(E_validated_struct_string), typeof(string), () => "alpha")); - e.Add(new EfEntry("validated-struct", "ulong", "nested-two-levels", typeof(C2_E_validated_struct_ulong.Inner.V), typeof(ulong), () => 4200000000UL)); - } -} diff --git a/tools/probe/corpus/harness/EfShapes3.cs b/tools/probe/corpus/harness/EfShapes3.cs deleted file mode 100644 index b4b846b7..00000000 --- a/tools/probe/corpus/harness/EfShapes3.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Lookup of the top-level shapes of EfShapes3.g.cs as EfEntry (so MakeVo/Companion work): the complete kind x wrapped grid. -#nullable enable -namespace Probes.Ef3; -public static class E3 -{ - public static Probes.Ef.EfEntry Find(string kind, string wrapped) - { - var (vo, plain) = Reg3.All[(kind, wrapped)]; - var src = Probes.Ef.EfRegistry.All.Where(x => x.Wrapped == wrapped && x.Kind == (kind.StartsWith("validated") ? "validated-struct" : "record-struct")).FirstOrDefault() - ?? Probes.Ef.EfRegistry.All.First(x => x.Wrapped == wrapped); - return new Probes.Ef.EfEntry(kind, wrapped, "top-level", vo, plain, src.Valid); - } -} -public static class E4 -{ - /// The shape at a site; in-generic-type (CMTK1005, refused at build) runs at top-level and says so. - public static Probes.Ef.EfEntry At(Probes.Shapes.Row r, string kind, string wrapped, string site) - { - if (site == "in-generic-type") { r.Substituted("shape.site", "top-level", "CMTK1005: a value object nested in a generic type does not build"); site = "top-level"; } - if (site == "top-level") return E3.Find(kind, wrapped); - var (vo, plain) = Probes.Ef4.Reg4.All[(kind, wrapped, site)]; - var src = Probes.Ef.EfRegistry.All.Where(x => x.Wrapped == wrapped && x.Kind == (kind.StartsWith("validated") ? "validated-struct" : "record-struct")).FirstOrDefault() - ?? Probes.Ef.EfRegistry.All.First(x => x.Wrapped == wrapped); - return new Probes.Ef.EfEntry(kind, wrapped, site, vo, plain, src.Valid); - } -} diff --git a/tools/probe/corpus/harness/EfShapes3.g.cs b/tools/probe/corpus/harness/EfShapes3.g.cs deleted file mode 100644 index ef335f76..00000000 --- a/tools/probe/corpus/harness/EfShapes3.g.cs +++ /dev/null @@ -1,155 +0,0 @@ -// by tools/probe/generators/ef.cs top-level: top-level shapes for every (kind, wrapped) the E03/E09/E10 probes need. Do not edit. -#nullable enable -#pragma warning disable CMTK0014, CS8618, CS1591, CMTK0005, CMTK1000, CMTK0003, CMTK0006, CA1000 -using CodoMetis.TypeKit; -using CodoMetis.TypeKit.ValueObjects; -using Probes.Shapes; -namespace Probes.Ef3; -public readonly partial record struct X_rs_DateOnly : IValue; -public sealed partial record class X_rc_DateOnly : IValue; -public readonly partial record struct X_vs_DateOnly : IValidatedValue { public static Result Create(DateOnly value) => EqualityComparer.Default.Equals(value, DateOnly.MinValue) ? Result.Error(ShapeFault.Refused) : new X_vs_DateOnly(value); } -public sealed partial record class X_vc_DateOnly : IValidatedValue { public static Result Create(DateOnly value) => EqualityComparer.Default.Equals(value, DateOnly.MinValue) ? Result.Error(ShapeFault.Refused) : new X_vc_DateOnly(value); } -public readonly partial record struct X_rs_DateTime : IValue; -public sealed partial record class X_rc_DateTime : IValue; -public readonly partial record struct X_vs_DateTime : IValidatedValue { public static Result Create(DateTime value) => EqualityComparer.Default.Equals(value, DateTime.MinValue) ? Result.Error(ShapeFault.Refused) : new X_vs_DateTime(value); } -public sealed partial record class X_vc_DateTime : IValidatedValue { public static Result Create(DateTime value) => EqualityComparer.Default.Equals(value, DateTime.MinValue) ? Result.Error(ShapeFault.Refused) : new X_vc_DateTime(value); } -public readonly partial record struct X_rs_DateTimeOffset : IValue; -public sealed partial record class X_rc_DateTimeOffset : IValue; -public readonly partial record struct X_vs_DateTimeOffset : IValidatedValue { public static Result Create(DateTimeOffset value) => EqualityComparer.Default.Equals(value, DateTimeOffset.MinValue) ? Result.Error(ShapeFault.Refused) : new X_vs_DateTimeOffset(value); } -public sealed partial record class X_vc_DateTimeOffset : IValidatedValue { public static Result Create(DateTimeOffset value) => EqualityComparer.Default.Equals(value, DateTimeOffset.MinValue) ? Result.Error(ShapeFault.Refused) : new X_vc_DateTimeOffset(value); } -public readonly partial record struct X_rs_Guid : IValue; -public sealed partial record class X_rc_Guid : IValue; -public readonly partial record struct X_vs_Guid : IValidatedValue { public static Result Create(Guid value) => EqualityComparer.Default.Equals(value, Guid.Empty) ? Result.Error(ShapeFault.Refused) : new X_vs_Guid(value); } -public sealed partial record class X_vc_Guid : IValidatedValue { public static Result Create(Guid value) => EqualityComparer.Default.Equals(value, Guid.Empty) ? Result.Error(ShapeFault.Refused) : new X_vc_Guid(value); } -public readonly partial record struct X_rs_TimeOnly : IValue; -public sealed partial record class X_rc_TimeOnly : IValue; -public readonly partial record struct X_vs_TimeOnly : IValidatedValue { public static Result Create(TimeOnly value) => EqualityComparer.Default.Equals(value, TimeOnly.MinValue) ? Result.Error(ShapeFault.Refused) : new X_vs_TimeOnly(value); } -public sealed partial record class X_vc_TimeOnly : IValidatedValue { public static Result Create(TimeOnly value) => EqualityComparer.Default.Equals(value, TimeOnly.MinValue) ? Result.Error(ShapeFault.Refused) : new X_vc_TimeOnly(value); } -public readonly partial record struct X_rs_TimeSpan : IValue; -public sealed partial record class X_rc_TimeSpan : IValue; -public readonly partial record struct X_vs_TimeSpan : IValidatedValue { public static Result Create(TimeSpan value) => EqualityComparer.Default.Equals(value, TimeSpan.MinValue) ? Result.Error(ShapeFault.Refused) : new X_vs_TimeSpan(value); } -public sealed partial record class X_vc_TimeSpan : IValidatedValue { public static Result Create(TimeSpan value) => EqualityComparer.Default.Equals(value, TimeSpan.MinValue) ? Result.Error(ShapeFault.Refused) : new X_vc_TimeSpan(value); } -public readonly partial record struct X_rs_Uri : IValue; -public sealed partial record class X_rc_Uri : IValue; -public readonly partial record struct X_vs_Uri : IValidatedValue { public static Result Create(Uri value) => EqualityComparer.Default.Equals(value, new Uri("https://example.com/refused")) ? Result.Error(ShapeFault.Refused) : new X_vs_Uri(value); } -public sealed partial record class X_vc_Uri : IValidatedValue { public static Result Create(Uri value) => EqualityComparer.Default.Equals(value, new Uri("https://example.com/refused")) ? Result.Error(ShapeFault.Refused) : new X_vc_Uri(value); } -public readonly partial record struct X_rs_bool : IValue; -public sealed partial record class X_rc_bool : IValue; -public readonly partial record struct X_vs_bool : IValidatedValue { public static Result Create(bool value) => EqualityComparer.Default.Equals(value, false) ? Result.Error(ShapeFault.Refused) : new X_vs_bool(value); } -public sealed partial record class X_vc_bool : IValidatedValue { public static Result Create(bool value) => EqualityComparer.Default.Equals(value, false) ? Result.Error(ShapeFault.Refused) : new X_vc_bool(value); } -public readonly partial record struct X_rs_byte : IValue; -public sealed partial record class X_rc_byte : IValue; -public readonly partial record struct X_vs_byte : IValidatedValue { public static Result Create(byte value) => EqualityComparer.Default.Equals(value, (byte)255) ? Result.Error(ShapeFault.Refused) : new X_vs_byte(value); } -public sealed partial record class X_vc_byte : IValidatedValue { public static Result Create(byte value) => EqualityComparer.Default.Equals(value, (byte)255) ? Result.Error(ShapeFault.Refused) : new X_vc_byte(value); } -public readonly partial record struct X_rs_char : IValue; -public sealed partial record class X_rc_char : IValue; -public readonly partial record struct X_vs_char : IValidatedValue { public static Result Create(char value) => EqualityComparer.Default.Equals(value, 'x') ? Result.Error(ShapeFault.Refused) : new X_vs_char(value); } -public sealed partial record class X_vc_char : IValidatedValue { public static Result Create(char value) => EqualityComparer.Default.Equals(value, 'x') ? Result.Error(ShapeFault.Refused) : new X_vc_char(value); } -public readonly partial record struct X_rs_decimal : IValue; -public sealed partial record class X_rc_decimal : IValue; -public readonly partial record struct X_vs_decimal : IValidatedValue { public static Result Create(decimal value) => EqualityComparer.Default.Equals(value, -1m) ? Result.Error(ShapeFault.Refused) : new X_vs_decimal(value); } -public sealed partial record class X_vc_decimal : IValidatedValue { public static Result Create(decimal value) => EqualityComparer.Default.Equals(value, -1m) ? Result.Error(ShapeFault.Refused) : new X_vc_decimal(value); } -public readonly partial record struct X_rs_double : IValue; -public sealed partial record class X_rc_double : IValue; -public readonly partial record struct X_vs_double : IValidatedValue { public static Result Create(double value) => EqualityComparer.Default.Equals(value, -1.0) ? Result.Error(ShapeFault.Refused) : new X_vs_double(value); } -public sealed partial record class X_vc_double : IValidatedValue { public static Result Create(double value) => EqualityComparer.Default.Equals(value, -1.0) ? Result.Error(ShapeFault.Refused) : new X_vc_double(value); } -public readonly partial record struct X_rs_enum : IValue; -public sealed partial record class X_rc_enum : IValue; -public readonly partial record struct X_vs_enum : IValidatedValue { public static Result Create(PEnum value) => EqualityComparer.Default.Equals(value, (PEnum)99) ? Result.Error(ShapeFault.Refused) : new X_vs_enum(value); } -public sealed partial record class X_vc_enum : IValidatedValue { public static Result Create(PEnum value) => EqualityComparer.Default.Equals(value, (PEnum)99) ? Result.Error(ShapeFault.Refused) : new X_vc_enum(value); } -public readonly partial record struct X_rs_flags_enum : IValue; -public sealed partial record class X_rc_flags_enum : IValue; -public readonly partial record struct X_vs_flags_enum : IValidatedValue { public static Result Create(PFlags value) => EqualityComparer.Default.Equals(value, (PFlags)0) ? Result.Error(ShapeFault.Refused) : new X_vs_flags_enum(value); } -public sealed partial record class X_vc_flags_enum : IValidatedValue { public static Result Create(PFlags value) => EqualityComparer.Default.Equals(value, (PFlags)0) ? Result.Error(ShapeFault.Refused) : new X_vc_flags_enum(value); } -public readonly partial record struct X_rs_int : IValue; -public sealed partial record class X_rc_int : IValue; -public readonly partial record struct X_vs_int : IValidatedValue { public static Result Create(int value) => EqualityComparer.Default.Equals(value, -1) ? Result.Error(ShapeFault.Refused) : new X_vs_int(value); } -public sealed partial record class X_vc_int : IValidatedValue { public static Result Create(int value) => EqualityComparer.Default.Equals(value, -1) ? Result.Error(ShapeFault.Refused) : new X_vc_int(value); } -public readonly partial record struct X_rs_long : IValue; -public sealed partial record class X_rc_long : IValue; -public readonly partial record struct X_vs_long : IValidatedValue { public static Result Create(long value) => EqualityComparer.Default.Equals(value, -1L) ? Result.Error(ShapeFault.Refused) : new X_vs_long(value); } -public sealed partial record class X_vc_long : IValidatedValue { public static Result Create(long value) => EqualityComparer.Default.Equals(value, -1L) ? Result.Error(ShapeFault.Refused) : new X_vc_long(value); } -public readonly partial record struct X_rs_string : IValue; -public sealed partial record class X_rc_string : IValue; -public readonly partial record struct X_vs_string : IValidatedValue { public static Result Create(string value) => EqualityComparer.Default.Equals(value, "SECRET-7f3a") ? Result.Error(ShapeFault.Refused) : new X_vs_string(value); } -public sealed partial record class X_vc_string : IValidatedValue { public static Result Create(string value) => EqualityComparer.Default.Equals(value, "SECRET-7f3a") ? Result.Error(ShapeFault.Refused) : new X_vc_string(value); } -public readonly partial record struct X_rs_ulong : IValue; -public sealed partial record class X_rc_ulong : IValue; -public readonly partial record struct X_vs_ulong : IValidatedValue { public static Result Create(ulong value) => EqualityComparer.Default.Equals(value, 0UL) ? Result.Error(ShapeFault.Refused) : new X_vs_ulong(value); } -public sealed partial record class X_vc_ulong : IValidatedValue { public static Result Create(ulong value) => EqualityComparer.Default.Equals(value, 0UL) ? Result.Error(ShapeFault.Refused) : new X_vc_ulong(value); } -public static class Reg3 { - public static readonly Dictionary<(string Kind, string Wrapped), (Type Vo, Type Plain)> All = new() { - [("record-struct", "DateOnly")] = (typeof(X_rs_DateOnly), typeof(DateOnly)), - [("sealed-record-class", "DateOnly")] = (typeof(X_rc_DateOnly), typeof(DateOnly)), - [("validated-struct", "DateOnly")] = (typeof(X_vs_DateOnly), typeof(DateOnly)), - [("validated-class", "DateOnly")] = (typeof(X_vc_DateOnly), typeof(DateOnly)), - [("record-struct", "DateTime")] = (typeof(X_rs_DateTime), typeof(DateTime)), - [("sealed-record-class", "DateTime")] = (typeof(X_rc_DateTime), typeof(DateTime)), - [("validated-struct", "DateTime")] = (typeof(X_vs_DateTime), typeof(DateTime)), - [("validated-class", "DateTime")] = (typeof(X_vc_DateTime), typeof(DateTime)), - [("record-struct", "DateTimeOffset")] = (typeof(X_rs_DateTimeOffset), typeof(DateTimeOffset)), - [("sealed-record-class", "DateTimeOffset")] = (typeof(X_rc_DateTimeOffset), typeof(DateTimeOffset)), - [("validated-struct", "DateTimeOffset")] = (typeof(X_vs_DateTimeOffset), typeof(DateTimeOffset)), - [("validated-class", "DateTimeOffset")] = (typeof(X_vc_DateTimeOffset), typeof(DateTimeOffset)), - [("record-struct", "Guid")] = (typeof(X_rs_Guid), typeof(Guid)), - [("sealed-record-class", "Guid")] = (typeof(X_rc_Guid), typeof(Guid)), - [("validated-struct", "Guid")] = (typeof(X_vs_Guid), typeof(Guid)), - [("validated-class", "Guid")] = (typeof(X_vc_Guid), typeof(Guid)), - [("record-struct", "TimeOnly")] = (typeof(X_rs_TimeOnly), typeof(TimeOnly)), - [("sealed-record-class", "TimeOnly")] = (typeof(X_rc_TimeOnly), typeof(TimeOnly)), - [("validated-struct", "TimeOnly")] = (typeof(X_vs_TimeOnly), typeof(TimeOnly)), - [("validated-class", "TimeOnly")] = (typeof(X_vc_TimeOnly), typeof(TimeOnly)), - [("record-struct", "TimeSpan")] = (typeof(X_rs_TimeSpan), typeof(TimeSpan)), - [("sealed-record-class", "TimeSpan")] = (typeof(X_rc_TimeSpan), typeof(TimeSpan)), - [("validated-struct", "TimeSpan")] = (typeof(X_vs_TimeSpan), typeof(TimeSpan)), - [("validated-class", "TimeSpan")] = (typeof(X_vc_TimeSpan), typeof(TimeSpan)), - [("record-struct", "Uri")] = (typeof(X_rs_Uri), typeof(Uri)), - [("sealed-record-class", "Uri")] = (typeof(X_rc_Uri), typeof(Uri)), - [("validated-struct", "Uri")] = (typeof(X_vs_Uri), typeof(Uri)), - [("validated-class", "Uri")] = (typeof(X_vc_Uri), typeof(Uri)), - [("record-struct", "bool")] = (typeof(X_rs_bool), typeof(bool)), - [("sealed-record-class", "bool")] = (typeof(X_rc_bool), typeof(bool)), - [("validated-struct", "bool")] = (typeof(X_vs_bool), typeof(bool)), - [("validated-class", "bool")] = (typeof(X_vc_bool), typeof(bool)), - [("record-struct", "byte")] = (typeof(X_rs_byte), typeof(byte)), - [("sealed-record-class", "byte")] = (typeof(X_rc_byte), typeof(byte)), - [("validated-struct", "byte")] = (typeof(X_vs_byte), typeof(byte)), - [("validated-class", "byte")] = (typeof(X_vc_byte), typeof(byte)), - [("record-struct", "char")] = (typeof(X_rs_char), typeof(char)), - [("sealed-record-class", "char")] = (typeof(X_rc_char), typeof(char)), - [("validated-struct", "char")] = (typeof(X_vs_char), typeof(char)), - [("validated-class", "char")] = (typeof(X_vc_char), typeof(char)), - [("record-struct", "decimal")] = (typeof(X_rs_decimal), typeof(decimal)), - [("sealed-record-class", "decimal")] = (typeof(X_rc_decimal), typeof(decimal)), - [("validated-struct", "decimal")] = (typeof(X_vs_decimal), typeof(decimal)), - [("validated-class", "decimal")] = (typeof(X_vc_decimal), typeof(decimal)), - [("record-struct", "double")] = (typeof(X_rs_double), typeof(double)), - [("sealed-record-class", "double")] = (typeof(X_rc_double), typeof(double)), - [("validated-struct", "double")] = (typeof(X_vs_double), typeof(double)), - [("validated-class", "double")] = (typeof(X_vc_double), typeof(double)), - [("record-struct", "enum")] = (typeof(X_rs_enum), typeof(PEnum)), - [("sealed-record-class", "enum")] = (typeof(X_rc_enum), typeof(PEnum)), - [("validated-struct", "enum")] = (typeof(X_vs_enum), typeof(PEnum)), - [("validated-class", "enum")] = (typeof(X_vc_enum), typeof(PEnum)), - [("record-struct", "flags-enum")] = (typeof(X_rs_flags_enum), typeof(PFlags)), - [("sealed-record-class", "flags-enum")] = (typeof(X_rc_flags_enum), typeof(PFlags)), - [("validated-struct", "flags-enum")] = (typeof(X_vs_flags_enum), typeof(PFlags)), - [("validated-class", "flags-enum")] = (typeof(X_vc_flags_enum), typeof(PFlags)), - [("record-struct", "int")] = (typeof(X_rs_int), typeof(int)), - [("sealed-record-class", "int")] = (typeof(X_rc_int), typeof(int)), - [("validated-struct", "int")] = (typeof(X_vs_int), typeof(int)), - [("validated-class", "int")] = (typeof(X_vc_int), typeof(int)), - [("record-struct", "long")] = (typeof(X_rs_long), typeof(long)), - [("sealed-record-class", "long")] = (typeof(X_rc_long), typeof(long)), - [("validated-struct", "long")] = (typeof(X_vs_long), typeof(long)), - [("validated-class", "long")] = (typeof(X_vc_long), typeof(long)), - [("record-struct", "string")] = (typeof(X_rs_string), typeof(string)), - [("sealed-record-class", "string")] = (typeof(X_rc_string), typeof(string)), - [("validated-struct", "string")] = (typeof(X_vs_string), typeof(string)), - [("validated-class", "string")] = (typeof(X_vc_string), typeof(string)), - [("record-struct", "ulong")] = (typeof(X_rs_ulong), typeof(ulong)), - [("sealed-record-class", "ulong")] = (typeof(X_rc_ulong), typeof(ulong)), - [("validated-struct", "ulong")] = (typeof(X_vs_ulong), typeof(ulong)), - [("validated-class", "ulong")] = (typeof(X_vc_ulong), typeof(ulong)), - }; -} diff --git a/tools/probe/corpus/harness/EfShapes4.g.cs b/tools/probe/corpus/harness/EfShapes4.g.cs deleted file mode 100644 index 27234211..00000000 --- a/tools/probe/corpus/harness/EfShapes4.g.cs +++ /dev/null @@ -1,443 +0,0 @@ -// by tools/probe/generators/ef.cs nested: nested-site shapes (nested-in-class, nested-in-struct, nested-two-levels) for every kind x wrapped. Do not edit. -#nullable enable -#pragma warning disable CMTK0014, CS8618, CS1591, CMTK0005, CMTK1000, CMTK0003, CMTK0006, CA1000 -using CodoMetis.TypeKit; -using CodoMetis.TypeKit.ValueObjects; -using Probes.Shapes; -namespace Probes.Ef4; -public static class NC_rs_DateOnly { public readonly partial record struct V : IValue; } -public struct NS_rs_DateOnly { public readonly partial record struct V : IValue; } -public static class N2_rs_DateOnly { public static class Inner { public readonly partial record struct V : IValue; } } -public static class NC_rc_DateOnly { public sealed partial record class V : IValue; } -public struct NS_rc_DateOnly { public sealed partial record class V : IValue; } -public static class N2_rc_DateOnly { public static class Inner { public sealed partial record class V : IValue; } } -public static class NC_vs_DateOnly { public readonly partial record struct V : IValidatedValue { public static Result Create(DateOnly value) => EqualityComparer.Default.Equals(value, DateOnly.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vs_DateOnly { public readonly partial record struct V : IValidatedValue { public static Result Create(DateOnly value) => EqualityComparer.Default.Equals(value, DateOnly.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vs_DateOnly { public static class Inner { public readonly partial record struct V : IValidatedValue { public static Result Create(DateOnly value) => EqualityComparer.Default.Equals(value, DateOnly.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_vc_DateOnly { public sealed partial record class V : IValidatedValue { public static Result Create(DateOnly value) => EqualityComparer.Default.Equals(value, DateOnly.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vc_DateOnly { public sealed partial record class V : IValidatedValue { public static Result Create(DateOnly value) => EqualityComparer.Default.Equals(value, DateOnly.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vc_DateOnly { public static class Inner { public sealed partial record class V : IValidatedValue { public static Result Create(DateOnly value) => EqualityComparer.Default.Equals(value, DateOnly.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_rs_DateTime { public readonly partial record struct V : IValue; } -public struct NS_rs_DateTime { public readonly partial record struct V : IValue; } -public static class N2_rs_DateTime { public static class Inner { public readonly partial record struct V : IValue; } } -public static class NC_rc_DateTime { public sealed partial record class V : IValue; } -public struct NS_rc_DateTime { public sealed partial record class V : IValue; } -public static class N2_rc_DateTime { public static class Inner { public sealed partial record class V : IValue; } } -public static class NC_vs_DateTime { public readonly partial record struct V : IValidatedValue { public static Result Create(DateTime value) => EqualityComparer.Default.Equals(value, DateTime.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vs_DateTime { public readonly partial record struct V : IValidatedValue { public static Result Create(DateTime value) => EqualityComparer.Default.Equals(value, DateTime.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vs_DateTime { public static class Inner { public readonly partial record struct V : IValidatedValue { public static Result Create(DateTime value) => EqualityComparer.Default.Equals(value, DateTime.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_vc_DateTime { public sealed partial record class V : IValidatedValue { public static Result Create(DateTime value) => EqualityComparer.Default.Equals(value, DateTime.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vc_DateTime { public sealed partial record class V : IValidatedValue { public static Result Create(DateTime value) => EqualityComparer.Default.Equals(value, DateTime.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vc_DateTime { public static class Inner { public sealed partial record class V : IValidatedValue { public static Result Create(DateTime value) => EqualityComparer.Default.Equals(value, DateTime.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_rs_DateTimeOffset { public readonly partial record struct V : IValue; } -public struct NS_rs_DateTimeOffset { public readonly partial record struct V : IValue; } -public static class N2_rs_DateTimeOffset { public static class Inner { public readonly partial record struct V : IValue; } } -public static class NC_rc_DateTimeOffset { public sealed partial record class V : IValue; } -public struct NS_rc_DateTimeOffset { public sealed partial record class V : IValue; } -public static class N2_rc_DateTimeOffset { public static class Inner { public sealed partial record class V : IValue; } } -public static class NC_vs_DateTimeOffset { public readonly partial record struct V : IValidatedValue { public static Result Create(DateTimeOffset value) => EqualityComparer.Default.Equals(value, DateTimeOffset.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vs_DateTimeOffset { public readonly partial record struct V : IValidatedValue { public static Result Create(DateTimeOffset value) => EqualityComparer.Default.Equals(value, DateTimeOffset.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vs_DateTimeOffset { public static class Inner { public readonly partial record struct V : IValidatedValue { public static Result Create(DateTimeOffset value) => EqualityComparer.Default.Equals(value, DateTimeOffset.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_vc_DateTimeOffset { public sealed partial record class V : IValidatedValue { public static Result Create(DateTimeOffset value) => EqualityComparer.Default.Equals(value, DateTimeOffset.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vc_DateTimeOffset { public sealed partial record class V : IValidatedValue { public static Result Create(DateTimeOffset value) => EqualityComparer.Default.Equals(value, DateTimeOffset.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vc_DateTimeOffset { public static class Inner { public sealed partial record class V : IValidatedValue { public static Result Create(DateTimeOffset value) => EqualityComparer.Default.Equals(value, DateTimeOffset.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_rs_Guid { public readonly partial record struct V : IValue; } -public struct NS_rs_Guid { public readonly partial record struct V : IValue; } -public static class N2_rs_Guid { public static class Inner { public readonly partial record struct V : IValue; } } -public static class NC_rc_Guid { public sealed partial record class V : IValue; } -public struct NS_rc_Guid { public sealed partial record class V : IValue; } -public static class N2_rc_Guid { public static class Inner { public sealed partial record class V : IValue; } } -public static class NC_vs_Guid { public readonly partial record struct V : IValidatedValue { public static Result Create(Guid value) => EqualityComparer.Default.Equals(value, Guid.Empty) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vs_Guid { public readonly partial record struct V : IValidatedValue { public static Result Create(Guid value) => EqualityComparer.Default.Equals(value, Guid.Empty) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vs_Guid { public static class Inner { public readonly partial record struct V : IValidatedValue { public static Result Create(Guid value) => EqualityComparer.Default.Equals(value, Guid.Empty) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_vc_Guid { public sealed partial record class V : IValidatedValue { public static Result Create(Guid value) => EqualityComparer.Default.Equals(value, Guid.Empty) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vc_Guid { public sealed partial record class V : IValidatedValue { public static Result Create(Guid value) => EqualityComparer.Default.Equals(value, Guid.Empty) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vc_Guid { public static class Inner { public sealed partial record class V : IValidatedValue { public static Result Create(Guid value) => EqualityComparer.Default.Equals(value, Guid.Empty) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_rs_TimeOnly { public readonly partial record struct V : IValue; } -public struct NS_rs_TimeOnly { public readonly partial record struct V : IValue; } -public static class N2_rs_TimeOnly { public static class Inner { public readonly partial record struct V : IValue; } } -public static class NC_rc_TimeOnly { public sealed partial record class V : IValue; } -public struct NS_rc_TimeOnly { public sealed partial record class V : IValue; } -public static class N2_rc_TimeOnly { public static class Inner { public sealed partial record class V : IValue; } } -public static class NC_vs_TimeOnly { public readonly partial record struct V : IValidatedValue { public static Result Create(TimeOnly value) => EqualityComparer.Default.Equals(value, TimeOnly.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vs_TimeOnly { public readonly partial record struct V : IValidatedValue { public static Result Create(TimeOnly value) => EqualityComparer.Default.Equals(value, TimeOnly.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vs_TimeOnly { public static class Inner { public readonly partial record struct V : IValidatedValue { public static Result Create(TimeOnly value) => EqualityComparer.Default.Equals(value, TimeOnly.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_vc_TimeOnly { public sealed partial record class V : IValidatedValue { public static Result Create(TimeOnly value) => EqualityComparer.Default.Equals(value, TimeOnly.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vc_TimeOnly { public sealed partial record class V : IValidatedValue { public static Result Create(TimeOnly value) => EqualityComparer.Default.Equals(value, TimeOnly.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vc_TimeOnly { public static class Inner { public sealed partial record class V : IValidatedValue { public static Result Create(TimeOnly value) => EqualityComparer.Default.Equals(value, TimeOnly.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_rs_TimeSpan { public readonly partial record struct V : IValue; } -public struct NS_rs_TimeSpan { public readonly partial record struct V : IValue; } -public static class N2_rs_TimeSpan { public static class Inner { public readonly partial record struct V : IValue; } } -public static class NC_rc_TimeSpan { public sealed partial record class V : IValue; } -public struct NS_rc_TimeSpan { public sealed partial record class V : IValue; } -public static class N2_rc_TimeSpan { public static class Inner { public sealed partial record class V : IValue; } } -public static class NC_vs_TimeSpan { public readonly partial record struct V : IValidatedValue { public static Result Create(TimeSpan value) => EqualityComparer.Default.Equals(value, TimeSpan.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vs_TimeSpan { public readonly partial record struct V : IValidatedValue { public static Result Create(TimeSpan value) => EqualityComparer.Default.Equals(value, TimeSpan.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vs_TimeSpan { public static class Inner { public readonly partial record struct V : IValidatedValue { public static Result Create(TimeSpan value) => EqualityComparer.Default.Equals(value, TimeSpan.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_vc_TimeSpan { public sealed partial record class V : IValidatedValue { public static Result Create(TimeSpan value) => EqualityComparer.Default.Equals(value, TimeSpan.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vc_TimeSpan { public sealed partial record class V : IValidatedValue { public static Result Create(TimeSpan value) => EqualityComparer.Default.Equals(value, TimeSpan.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vc_TimeSpan { public static class Inner { public sealed partial record class V : IValidatedValue { public static Result Create(TimeSpan value) => EqualityComparer.Default.Equals(value, TimeSpan.MinValue) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_rs_Uri { public readonly partial record struct V : IValue; } -public struct NS_rs_Uri { public readonly partial record struct V : IValue; } -public static class N2_rs_Uri { public static class Inner { public readonly partial record struct V : IValue; } } -public static class NC_rc_Uri { public sealed partial record class V : IValue; } -public struct NS_rc_Uri { public sealed partial record class V : IValue; } -public static class N2_rc_Uri { public static class Inner { public sealed partial record class V : IValue; } } -public static class NC_vs_Uri { public readonly partial record struct V : IValidatedValue { public static Result Create(Uri value) => EqualityComparer.Default.Equals(value, new Uri("https://example.com/refused")) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vs_Uri { public readonly partial record struct V : IValidatedValue { public static Result Create(Uri value) => EqualityComparer.Default.Equals(value, new Uri("https://example.com/refused")) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vs_Uri { public static class Inner { public readonly partial record struct V : IValidatedValue { public static Result Create(Uri value) => EqualityComparer.Default.Equals(value, new Uri("https://example.com/refused")) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_vc_Uri { public sealed partial record class V : IValidatedValue { public static Result Create(Uri value) => EqualityComparer.Default.Equals(value, new Uri("https://example.com/refused")) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vc_Uri { public sealed partial record class V : IValidatedValue { public static Result Create(Uri value) => EqualityComparer.Default.Equals(value, new Uri("https://example.com/refused")) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vc_Uri { public static class Inner { public sealed partial record class V : IValidatedValue { public static Result Create(Uri value) => EqualityComparer.Default.Equals(value, new Uri("https://example.com/refused")) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_rs_bool { public readonly partial record struct V : IValue; } -public struct NS_rs_bool { public readonly partial record struct V : IValue; } -public static class N2_rs_bool { public static class Inner { public readonly partial record struct V : IValue; } } -public static class NC_rc_bool { public sealed partial record class V : IValue; } -public struct NS_rc_bool { public sealed partial record class V : IValue; } -public static class N2_rc_bool { public static class Inner { public sealed partial record class V : IValue; } } -public static class NC_vs_bool { public readonly partial record struct V : IValidatedValue { public static Result Create(bool value) => EqualityComparer.Default.Equals(value, false) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vs_bool { public readonly partial record struct V : IValidatedValue { public static Result Create(bool value) => EqualityComparer.Default.Equals(value, false) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vs_bool { public static class Inner { public readonly partial record struct V : IValidatedValue { public static Result Create(bool value) => EqualityComparer.Default.Equals(value, false) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_vc_bool { public sealed partial record class V : IValidatedValue { public static Result Create(bool value) => EqualityComparer.Default.Equals(value, false) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vc_bool { public sealed partial record class V : IValidatedValue { public static Result Create(bool value) => EqualityComparer.Default.Equals(value, false) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vc_bool { public static class Inner { public sealed partial record class V : IValidatedValue { public static Result Create(bool value) => EqualityComparer.Default.Equals(value, false) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_rs_byte { public readonly partial record struct V : IValue; } -public struct NS_rs_byte { public readonly partial record struct V : IValue; } -public static class N2_rs_byte { public static class Inner { public readonly partial record struct V : IValue; } } -public static class NC_rc_byte { public sealed partial record class V : IValue; } -public struct NS_rc_byte { public sealed partial record class V : IValue; } -public static class N2_rc_byte { public static class Inner { public sealed partial record class V : IValue; } } -public static class NC_vs_byte { public readonly partial record struct V : IValidatedValue { public static Result Create(byte value) => EqualityComparer.Default.Equals(value, (byte)255) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vs_byte { public readonly partial record struct V : IValidatedValue { public static Result Create(byte value) => EqualityComparer.Default.Equals(value, (byte)255) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vs_byte { public static class Inner { public readonly partial record struct V : IValidatedValue { public static Result Create(byte value) => EqualityComparer.Default.Equals(value, (byte)255) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_vc_byte { public sealed partial record class V : IValidatedValue { public static Result Create(byte value) => EqualityComparer.Default.Equals(value, (byte)255) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vc_byte { public sealed partial record class V : IValidatedValue { public static Result Create(byte value) => EqualityComparer.Default.Equals(value, (byte)255) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vc_byte { public static class Inner { public sealed partial record class V : IValidatedValue { public static Result Create(byte value) => EqualityComparer.Default.Equals(value, (byte)255) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_rs_char { public readonly partial record struct V : IValue; } -public struct NS_rs_char { public readonly partial record struct V : IValue; } -public static class N2_rs_char { public static class Inner { public readonly partial record struct V : IValue; } } -public static class NC_rc_char { public sealed partial record class V : IValue; } -public struct NS_rc_char { public sealed partial record class V : IValue; } -public static class N2_rc_char { public static class Inner { public sealed partial record class V : IValue; } } -public static class NC_vs_char { public readonly partial record struct V : IValidatedValue { public static Result Create(char value) => EqualityComparer.Default.Equals(value, 'x') ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vs_char { public readonly partial record struct V : IValidatedValue { public static Result Create(char value) => EqualityComparer.Default.Equals(value, 'x') ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vs_char { public static class Inner { public readonly partial record struct V : IValidatedValue { public static Result Create(char value) => EqualityComparer.Default.Equals(value, 'x') ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_vc_char { public sealed partial record class V : IValidatedValue { public static Result Create(char value) => EqualityComparer.Default.Equals(value, 'x') ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vc_char { public sealed partial record class V : IValidatedValue { public static Result Create(char value) => EqualityComparer.Default.Equals(value, 'x') ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vc_char { public static class Inner { public sealed partial record class V : IValidatedValue { public static Result Create(char value) => EqualityComparer.Default.Equals(value, 'x') ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_rs_decimal { public readonly partial record struct V : IValue; } -public struct NS_rs_decimal { public readonly partial record struct V : IValue; } -public static class N2_rs_decimal { public static class Inner { public readonly partial record struct V : IValue; } } -public static class NC_rc_decimal { public sealed partial record class V : IValue; } -public struct NS_rc_decimal { public sealed partial record class V : IValue; } -public static class N2_rc_decimal { public static class Inner { public sealed partial record class V : IValue; } } -public static class NC_vs_decimal { public readonly partial record struct V : IValidatedValue { public static Result Create(decimal value) => EqualityComparer.Default.Equals(value, -1m) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vs_decimal { public readonly partial record struct V : IValidatedValue { public static Result Create(decimal value) => EqualityComparer.Default.Equals(value, -1m) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vs_decimal { public static class Inner { public readonly partial record struct V : IValidatedValue { public static Result Create(decimal value) => EqualityComparer.Default.Equals(value, -1m) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_vc_decimal { public sealed partial record class V : IValidatedValue { public static Result Create(decimal value) => EqualityComparer.Default.Equals(value, -1m) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vc_decimal { public sealed partial record class V : IValidatedValue { public static Result Create(decimal value) => EqualityComparer.Default.Equals(value, -1m) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vc_decimal { public static class Inner { public sealed partial record class V : IValidatedValue { public static Result Create(decimal value) => EqualityComparer.Default.Equals(value, -1m) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_rs_double { public readonly partial record struct V : IValue; } -public struct NS_rs_double { public readonly partial record struct V : IValue; } -public static class N2_rs_double { public static class Inner { public readonly partial record struct V : IValue; } } -public static class NC_rc_double { public sealed partial record class V : IValue; } -public struct NS_rc_double { public sealed partial record class V : IValue; } -public static class N2_rc_double { public static class Inner { public sealed partial record class V : IValue; } } -public static class NC_vs_double { public readonly partial record struct V : IValidatedValue { public static Result Create(double value) => EqualityComparer.Default.Equals(value, -1.0) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vs_double { public readonly partial record struct V : IValidatedValue { public static Result Create(double value) => EqualityComparer.Default.Equals(value, -1.0) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vs_double { public static class Inner { public readonly partial record struct V : IValidatedValue { public static Result Create(double value) => EqualityComparer.Default.Equals(value, -1.0) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_vc_double { public sealed partial record class V : IValidatedValue { public static Result Create(double value) => EqualityComparer.Default.Equals(value, -1.0) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vc_double { public sealed partial record class V : IValidatedValue { public static Result Create(double value) => EqualityComparer.Default.Equals(value, -1.0) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vc_double { public static class Inner { public sealed partial record class V : IValidatedValue { public static Result Create(double value) => EqualityComparer.Default.Equals(value, -1.0) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_rs_enum { public readonly partial record struct V : IValue; } -public struct NS_rs_enum { public readonly partial record struct V : IValue; } -public static class N2_rs_enum { public static class Inner { public readonly partial record struct V : IValue; } } -public static class NC_rc_enum { public sealed partial record class V : IValue; } -public struct NS_rc_enum { public sealed partial record class V : IValue; } -public static class N2_rc_enum { public static class Inner { public sealed partial record class V : IValue; } } -public static class NC_vs_enum { public readonly partial record struct V : IValidatedValue { public static Result Create(PEnum value) => EqualityComparer.Default.Equals(value, (PEnum)99) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vs_enum { public readonly partial record struct V : IValidatedValue { public static Result Create(PEnum value) => EqualityComparer.Default.Equals(value, (PEnum)99) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vs_enum { public static class Inner { public readonly partial record struct V : IValidatedValue { public static Result Create(PEnum value) => EqualityComparer.Default.Equals(value, (PEnum)99) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_vc_enum { public sealed partial record class V : IValidatedValue { public static Result Create(PEnum value) => EqualityComparer.Default.Equals(value, (PEnum)99) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vc_enum { public sealed partial record class V : IValidatedValue { public static Result Create(PEnum value) => EqualityComparer.Default.Equals(value, (PEnum)99) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vc_enum { public static class Inner { public sealed partial record class V : IValidatedValue { public static Result Create(PEnum value) => EqualityComparer.Default.Equals(value, (PEnum)99) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_rs_flags_enum { public readonly partial record struct V : IValue; } -public struct NS_rs_flags_enum { public readonly partial record struct V : IValue; } -public static class N2_rs_flags_enum { public static class Inner { public readonly partial record struct V : IValue; } } -public static class NC_rc_flags_enum { public sealed partial record class V : IValue; } -public struct NS_rc_flags_enum { public sealed partial record class V : IValue; } -public static class N2_rc_flags_enum { public static class Inner { public sealed partial record class V : IValue; } } -public static class NC_vs_flags_enum { public readonly partial record struct V : IValidatedValue { public static Result Create(PFlags value) => EqualityComparer.Default.Equals(value, (PFlags)0) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vs_flags_enum { public readonly partial record struct V : IValidatedValue { public static Result Create(PFlags value) => EqualityComparer.Default.Equals(value, (PFlags)0) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vs_flags_enum { public static class Inner { public readonly partial record struct V : IValidatedValue { public static Result Create(PFlags value) => EqualityComparer.Default.Equals(value, (PFlags)0) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_vc_flags_enum { public sealed partial record class V : IValidatedValue { public static Result Create(PFlags value) => EqualityComparer.Default.Equals(value, (PFlags)0) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vc_flags_enum { public sealed partial record class V : IValidatedValue { public static Result Create(PFlags value) => EqualityComparer.Default.Equals(value, (PFlags)0) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vc_flags_enum { public static class Inner { public sealed partial record class V : IValidatedValue { public static Result Create(PFlags value) => EqualityComparer.Default.Equals(value, (PFlags)0) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_rs_int { public readonly partial record struct V : IValue; } -public struct NS_rs_int { public readonly partial record struct V : IValue; } -public static class N2_rs_int { public static class Inner { public readonly partial record struct V : IValue; } } -public static class NC_rc_int { public sealed partial record class V : IValue; } -public struct NS_rc_int { public sealed partial record class V : IValue; } -public static class N2_rc_int { public static class Inner { public sealed partial record class V : IValue; } } -public static class NC_vs_int { public readonly partial record struct V : IValidatedValue { public static Result Create(int value) => EqualityComparer.Default.Equals(value, -1) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vs_int { public readonly partial record struct V : IValidatedValue { public static Result Create(int value) => EqualityComparer.Default.Equals(value, -1) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vs_int { public static class Inner { public readonly partial record struct V : IValidatedValue { public static Result Create(int value) => EqualityComparer.Default.Equals(value, -1) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_vc_int { public sealed partial record class V : IValidatedValue { public static Result Create(int value) => EqualityComparer.Default.Equals(value, -1) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vc_int { public sealed partial record class V : IValidatedValue { public static Result Create(int value) => EqualityComparer.Default.Equals(value, -1) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vc_int { public static class Inner { public sealed partial record class V : IValidatedValue { public static Result Create(int value) => EqualityComparer.Default.Equals(value, -1) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_rs_long { public readonly partial record struct V : IValue; } -public struct NS_rs_long { public readonly partial record struct V : IValue; } -public static class N2_rs_long { public static class Inner { public readonly partial record struct V : IValue; } } -public static class NC_rc_long { public sealed partial record class V : IValue; } -public struct NS_rc_long { public sealed partial record class V : IValue; } -public static class N2_rc_long { public static class Inner { public sealed partial record class V : IValue; } } -public static class NC_vs_long { public readonly partial record struct V : IValidatedValue { public static Result Create(long value) => EqualityComparer.Default.Equals(value, -1L) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vs_long { public readonly partial record struct V : IValidatedValue { public static Result Create(long value) => EqualityComparer.Default.Equals(value, -1L) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vs_long { public static class Inner { public readonly partial record struct V : IValidatedValue { public static Result Create(long value) => EqualityComparer.Default.Equals(value, -1L) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_vc_long { public sealed partial record class V : IValidatedValue { public static Result Create(long value) => EqualityComparer.Default.Equals(value, -1L) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vc_long { public sealed partial record class V : IValidatedValue { public static Result Create(long value) => EqualityComparer.Default.Equals(value, -1L) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vc_long { public static class Inner { public sealed partial record class V : IValidatedValue { public static Result Create(long value) => EqualityComparer.Default.Equals(value, -1L) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_rs_string { public readonly partial record struct V : IValue; } -public struct NS_rs_string { public readonly partial record struct V : IValue; } -public static class N2_rs_string { public static class Inner { public readonly partial record struct V : IValue; } } -public static class NC_rc_string { public sealed partial record class V : IValue; } -public struct NS_rc_string { public sealed partial record class V : IValue; } -public static class N2_rc_string { public static class Inner { public sealed partial record class V : IValue; } } -public static class NC_vs_string { public readonly partial record struct V : IValidatedValue { public static Result Create(string value) => EqualityComparer.Default.Equals(value, "SECRET-7f3a") ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vs_string { public readonly partial record struct V : IValidatedValue { public static Result Create(string value) => EqualityComparer.Default.Equals(value, "SECRET-7f3a") ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vs_string { public static class Inner { public readonly partial record struct V : IValidatedValue { public static Result Create(string value) => EqualityComparer.Default.Equals(value, "SECRET-7f3a") ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_vc_string { public sealed partial record class V : IValidatedValue { public static Result Create(string value) => EqualityComparer.Default.Equals(value, "SECRET-7f3a") ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vc_string { public sealed partial record class V : IValidatedValue { public static Result Create(string value) => EqualityComparer.Default.Equals(value, "SECRET-7f3a") ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vc_string { public static class Inner { public sealed partial record class V : IValidatedValue { public static Result Create(string value) => EqualityComparer.Default.Equals(value, "SECRET-7f3a") ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_rs_ulong { public readonly partial record struct V : IValue; } -public struct NS_rs_ulong { public readonly partial record struct V : IValue; } -public static class N2_rs_ulong { public static class Inner { public readonly partial record struct V : IValue; } } -public static class NC_rc_ulong { public sealed partial record class V : IValue; } -public struct NS_rc_ulong { public sealed partial record class V : IValue; } -public static class N2_rc_ulong { public static class Inner { public sealed partial record class V : IValue; } } -public static class NC_vs_ulong { public readonly partial record struct V : IValidatedValue { public static Result Create(ulong value) => EqualityComparer.Default.Equals(value, 0UL) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vs_ulong { public readonly partial record struct V : IValidatedValue { public static Result Create(ulong value) => EqualityComparer.Default.Equals(value, 0UL) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vs_ulong { public static class Inner { public readonly partial record struct V : IValidatedValue { public static Result Create(ulong value) => EqualityComparer.Default.Equals(value, 0UL) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class NC_vc_ulong { public sealed partial record class V : IValidatedValue { public static Result Create(ulong value) => EqualityComparer.Default.Equals(value, 0UL) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public struct NS_vc_ulong { public sealed partial record class V : IValidatedValue { public static Result Create(ulong value) => EqualityComparer.Default.Equals(value, 0UL) ? Result.Error(ShapeFault.Refused) : new V(value); } } -public static class N2_vc_ulong { public static class Inner { public sealed partial record class V : IValidatedValue { public static Result Create(ulong value) => EqualityComparer.Default.Equals(value, 0UL) ? Result.Error(ShapeFault.Refused) : new V(value); } } } -public static class Reg4 { - public static readonly Dictionary<(string Kind, string Wrapped, string Site), (Type Vo, Type Plain)> All = new() { - [("record-struct", "DateOnly", "nested-in-class")] = (typeof(NC_rs_DateOnly.V), typeof(DateOnly)), - [("record-struct", "DateOnly", "nested-in-struct")] = (typeof(NS_rs_DateOnly.V), typeof(DateOnly)), - [("record-struct", "DateOnly", "nested-two-levels")] = (typeof(N2_rs_DateOnly.Inner.V), typeof(DateOnly)), - [("sealed-record-class", "DateOnly", "nested-in-class")] = (typeof(NC_rc_DateOnly.V), typeof(DateOnly)), - [("sealed-record-class", "DateOnly", "nested-in-struct")] = (typeof(NS_rc_DateOnly.V), typeof(DateOnly)), - [("sealed-record-class", "DateOnly", "nested-two-levels")] = (typeof(N2_rc_DateOnly.Inner.V), typeof(DateOnly)), - [("validated-struct", "DateOnly", "nested-in-class")] = (typeof(NC_vs_DateOnly.V), typeof(DateOnly)), - [("validated-struct", "DateOnly", "nested-in-struct")] = (typeof(NS_vs_DateOnly.V), typeof(DateOnly)), - [("validated-struct", "DateOnly", "nested-two-levels")] = (typeof(N2_vs_DateOnly.Inner.V), typeof(DateOnly)), - [("validated-class", "DateOnly", "nested-in-class")] = (typeof(NC_vc_DateOnly.V), typeof(DateOnly)), - [("validated-class", "DateOnly", "nested-in-struct")] = (typeof(NS_vc_DateOnly.V), typeof(DateOnly)), - [("validated-class", "DateOnly", "nested-two-levels")] = (typeof(N2_vc_DateOnly.Inner.V), typeof(DateOnly)), - [("record-struct", "DateTime", "nested-in-class")] = (typeof(NC_rs_DateTime.V), typeof(DateTime)), - [("record-struct", "DateTime", "nested-in-struct")] = (typeof(NS_rs_DateTime.V), typeof(DateTime)), - [("record-struct", "DateTime", "nested-two-levels")] = (typeof(N2_rs_DateTime.Inner.V), typeof(DateTime)), - [("sealed-record-class", "DateTime", "nested-in-class")] = (typeof(NC_rc_DateTime.V), typeof(DateTime)), - [("sealed-record-class", "DateTime", "nested-in-struct")] = (typeof(NS_rc_DateTime.V), typeof(DateTime)), - [("sealed-record-class", "DateTime", "nested-two-levels")] = (typeof(N2_rc_DateTime.Inner.V), typeof(DateTime)), - [("validated-struct", "DateTime", "nested-in-class")] = (typeof(NC_vs_DateTime.V), typeof(DateTime)), - [("validated-struct", "DateTime", "nested-in-struct")] = (typeof(NS_vs_DateTime.V), typeof(DateTime)), - [("validated-struct", "DateTime", "nested-two-levels")] = (typeof(N2_vs_DateTime.Inner.V), typeof(DateTime)), - [("validated-class", "DateTime", "nested-in-class")] = (typeof(NC_vc_DateTime.V), typeof(DateTime)), - [("validated-class", "DateTime", "nested-in-struct")] = (typeof(NS_vc_DateTime.V), typeof(DateTime)), - [("validated-class", "DateTime", "nested-two-levels")] = (typeof(N2_vc_DateTime.Inner.V), typeof(DateTime)), - [("record-struct", "DateTimeOffset", "nested-in-class")] = (typeof(NC_rs_DateTimeOffset.V), typeof(DateTimeOffset)), - [("record-struct", "DateTimeOffset", "nested-in-struct")] = (typeof(NS_rs_DateTimeOffset.V), typeof(DateTimeOffset)), - [("record-struct", "DateTimeOffset", "nested-two-levels")] = (typeof(N2_rs_DateTimeOffset.Inner.V), typeof(DateTimeOffset)), - [("sealed-record-class", "DateTimeOffset", "nested-in-class")] = (typeof(NC_rc_DateTimeOffset.V), typeof(DateTimeOffset)), - [("sealed-record-class", "DateTimeOffset", "nested-in-struct")] = (typeof(NS_rc_DateTimeOffset.V), typeof(DateTimeOffset)), - [("sealed-record-class", "DateTimeOffset", "nested-two-levels")] = (typeof(N2_rc_DateTimeOffset.Inner.V), typeof(DateTimeOffset)), - [("validated-struct", "DateTimeOffset", "nested-in-class")] = (typeof(NC_vs_DateTimeOffset.V), typeof(DateTimeOffset)), - [("validated-struct", "DateTimeOffset", "nested-in-struct")] = (typeof(NS_vs_DateTimeOffset.V), typeof(DateTimeOffset)), - [("validated-struct", "DateTimeOffset", "nested-two-levels")] = (typeof(N2_vs_DateTimeOffset.Inner.V), typeof(DateTimeOffset)), - [("validated-class", "DateTimeOffset", "nested-in-class")] = (typeof(NC_vc_DateTimeOffset.V), typeof(DateTimeOffset)), - [("validated-class", "DateTimeOffset", "nested-in-struct")] = (typeof(NS_vc_DateTimeOffset.V), typeof(DateTimeOffset)), - [("validated-class", "DateTimeOffset", "nested-two-levels")] = (typeof(N2_vc_DateTimeOffset.Inner.V), typeof(DateTimeOffset)), - [("record-struct", "Guid", "nested-in-class")] = (typeof(NC_rs_Guid.V), typeof(Guid)), - [("record-struct", "Guid", "nested-in-struct")] = (typeof(NS_rs_Guid.V), typeof(Guid)), - [("record-struct", "Guid", "nested-two-levels")] = (typeof(N2_rs_Guid.Inner.V), typeof(Guid)), - [("sealed-record-class", "Guid", "nested-in-class")] = (typeof(NC_rc_Guid.V), typeof(Guid)), - [("sealed-record-class", "Guid", "nested-in-struct")] = (typeof(NS_rc_Guid.V), typeof(Guid)), - [("sealed-record-class", "Guid", "nested-two-levels")] = (typeof(N2_rc_Guid.Inner.V), typeof(Guid)), - [("validated-struct", "Guid", "nested-in-class")] = (typeof(NC_vs_Guid.V), typeof(Guid)), - [("validated-struct", "Guid", "nested-in-struct")] = (typeof(NS_vs_Guid.V), typeof(Guid)), - [("validated-struct", "Guid", "nested-two-levels")] = (typeof(N2_vs_Guid.Inner.V), typeof(Guid)), - [("validated-class", "Guid", "nested-in-class")] = (typeof(NC_vc_Guid.V), typeof(Guid)), - [("validated-class", "Guid", "nested-in-struct")] = (typeof(NS_vc_Guid.V), typeof(Guid)), - [("validated-class", "Guid", "nested-two-levels")] = (typeof(N2_vc_Guid.Inner.V), typeof(Guid)), - [("record-struct", "TimeOnly", "nested-in-class")] = (typeof(NC_rs_TimeOnly.V), typeof(TimeOnly)), - [("record-struct", "TimeOnly", "nested-in-struct")] = (typeof(NS_rs_TimeOnly.V), typeof(TimeOnly)), - [("record-struct", "TimeOnly", "nested-two-levels")] = (typeof(N2_rs_TimeOnly.Inner.V), typeof(TimeOnly)), - [("sealed-record-class", "TimeOnly", "nested-in-class")] = (typeof(NC_rc_TimeOnly.V), typeof(TimeOnly)), - [("sealed-record-class", "TimeOnly", "nested-in-struct")] = (typeof(NS_rc_TimeOnly.V), typeof(TimeOnly)), - [("sealed-record-class", "TimeOnly", "nested-two-levels")] = (typeof(N2_rc_TimeOnly.Inner.V), typeof(TimeOnly)), - [("validated-struct", "TimeOnly", "nested-in-class")] = (typeof(NC_vs_TimeOnly.V), typeof(TimeOnly)), - [("validated-struct", "TimeOnly", "nested-in-struct")] = (typeof(NS_vs_TimeOnly.V), typeof(TimeOnly)), - [("validated-struct", "TimeOnly", "nested-two-levels")] = (typeof(N2_vs_TimeOnly.Inner.V), typeof(TimeOnly)), - [("validated-class", "TimeOnly", "nested-in-class")] = (typeof(NC_vc_TimeOnly.V), typeof(TimeOnly)), - [("validated-class", "TimeOnly", "nested-in-struct")] = (typeof(NS_vc_TimeOnly.V), typeof(TimeOnly)), - [("validated-class", "TimeOnly", "nested-two-levels")] = (typeof(N2_vc_TimeOnly.Inner.V), typeof(TimeOnly)), - [("record-struct", "TimeSpan", "nested-in-class")] = (typeof(NC_rs_TimeSpan.V), typeof(TimeSpan)), - [("record-struct", "TimeSpan", "nested-in-struct")] = (typeof(NS_rs_TimeSpan.V), typeof(TimeSpan)), - [("record-struct", "TimeSpan", "nested-two-levels")] = (typeof(N2_rs_TimeSpan.Inner.V), typeof(TimeSpan)), - [("sealed-record-class", "TimeSpan", "nested-in-class")] = (typeof(NC_rc_TimeSpan.V), typeof(TimeSpan)), - [("sealed-record-class", "TimeSpan", "nested-in-struct")] = (typeof(NS_rc_TimeSpan.V), typeof(TimeSpan)), - [("sealed-record-class", "TimeSpan", "nested-two-levels")] = (typeof(N2_rc_TimeSpan.Inner.V), typeof(TimeSpan)), - [("validated-struct", "TimeSpan", "nested-in-class")] = (typeof(NC_vs_TimeSpan.V), typeof(TimeSpan)), - [("validated-struct", "TimeSpan", "nested-in-struct")] = (typeof(NS_vs_TimeSpan.V), typeof(TimeSpan)), - [("validated-struct", "TimeSpan", "nested-two-levels")] = (typeof(N2_vs_TimeSpan.Inner.V), typeof(TimeSpan)), - [("validated-class", "TimeSpan", "nested-in-class")] = (typeof(NC_vc_TimeSpan.V), typeof(TimeSpan)), - [("validated-class", "TimeSpan", "nested-in-struct")] = (typeof(NS_vc_TimeSpan.V), typeof(TimeSpan)), - [("validated-class", "TimeSpan", "nested-two-levels")] = (typeof(N2_vc_TimeSpan.Inner.V), typeof(TimeSpan)), - [("record-struct", "Uri", "nested-in-class")] = (typeof(NC_rs_Uri.V), typeof(Uri)), - [("record-struct", "Uri", "nested-in-struct")] = (typeof(NS_rs_Uri.V), typeof(Uri)), - [("record-struct", "Uri", "nested-two-levels")] = (typeof(N2_rs_Uri.Inner.V), typeof(Uri)), - [("sealed-record-class", "Uri", "nested-in-class")] = (typeof(NC_rc_Uri.V), typeof(Uri)), - [("sealed-record-class", "Uri", "nested-in-struct")] = (typeof(NS_rc_Uri.V), typeof(Uri)), - [("sealed-record-class", "Uri", "nested-two-levels")] = (typeof(N2_rc_Uri.Inner.V), typeof(Uri)), - [("validated-struct", "Uri", "nested-in-class")] = (typeof(NC_vs_Uri.V), typeof(Uri)), - [("validated-struct", "Uri", "nested-in-struct")] = (typeof(NS_vs_Uri.V), typeof(Uri)), - [("validated-struct", "Uri", "nested-two-levels")] = (typeof(N2_vs_Uri.Inner.V), typeof(Uri)), - [("validated-class", "Uri", "nested-in-class")] = (typeof(NC_vc_Uri.V), typeof(Uri)), - [("validated-class", "Uri", "nested-in-struct")] = (typeof(NS_vc_Uri.V), typeof(Uri)), - [("validated-class", "Uri", "nested-two-levels")] = (typeof(N2_vc_Uri.Inner.V), typeof(Uri)), - [("record-struct", "bool", "nested-in-class")] = (typeof(NC_rs_bool.V), typeof(bool)), - [("record-struct", "bool", "nested-in-struct")] = (typeof(NS_rs_bool.V), typeof(bool)), - [("record-struct", "bool", "nested-two-levels")] = (typeof(N2_rs_bool.Inner.V), typeof(bool)), - [("sealed-record-class", "bool", "nested-in-class")] = (typeof(NC_rc_bool.V), typeof(bool)), - [("sealed-record-class", "bool", "nested-in-struct")] = (typeof(NS_rc_bool.V), typeof(bool)), - [("sealed-record-class", "bool", "nested-two-levels")] = (typeof(N2_rc_bool.Inner.V), typeof(bool)), - [("validated-struct", "bool", "nested-in-class")] = (typeof(NC_vs_bool.V), typeof(bool)), - [("validated-struct", "bool", "nested-in-struct")] = (typeof(NS_vs_bool.V), typeof(bool)), - [("validated-struct", "bool", "nested-two-levels")] = (typeof(N2_vs_bool.Inner.V), typeof(bool)), - [("validated-class", "bool", "nested-in-class")] = (typeof(NC_vc_bool.V), typeof(bool)), - [("validated-class", "bool", "nested-in-struct")] = (typeof(NS_vc_bool.V), typeof(bool)), - [("validated-class", "bool", "nested-two-levels")] = (typeof(N2_vc_bool.Inner.V), typeof(bool)), - [("record-struct", "byte", "nested-in-class")] = (typeof(NC_rs_byte.V), typeof(byte)), - [("record-struct", "byte", "nested-in-struct")] = (typeof(NS_rs_byte.V), typeof(byte)), - [("record-struct", "byte", "nested-two-levels")] = (typeof(N2_rs_byte.Inner.V), typeof(byte)), - [("sealed-record-class", "byte", "nested-in-class")] = (typeof(NC_rc_byte.V), typeof(byte)), - [("sealed-record-class", "byte", "nested-in-struct")] = (typeof(NS_rc_byte.V), typeof(byte)), - [("sealed-record-class", "byte", "nested-two-levels")] = (typeof(N2_rc_byte.Inner.V), typeof(byte)), - [("validated-struct", "byte", "nested-in-class")] = (typeof(NC_vs_byte.V), typeof(byte)), - [("validated-struct", "byte", "nested-in-struct")] = (typeof(NS_vs_byte.V), typeof(byte)), - [("validated-struct", "byte", "nested-two-levels")] = (typeof(N2_vs_byte.Inner.V), typeof(byte)), - [("validated-class", "byte", "nested-in-class")] = (typeof(NC_vc_byte.V), typeof(byte)), - [("validated-class", "byte", "nested-in-struct")] = (typeof(NS_vc_byte.V), typeof(byte)), - [("validated-class", "byte", "nested-two-levels")] = (typeof(N2_vc_byte.Inner.V), typeof(byte)), - [("record-struct", "char", "nested-in-class")] = (typeof(NC_rs_char.V), typeof(char)), - [("record-struct", "char", "nested-in-struct")] = (typeof(NS_rs_char.V), typeof(char)), - [("record-struct", "char", "nested-two-levels")] = (typeof(N2_rs_char.Inner.V), typeof(char)), - [("sealed-record-class", "char", "nested-in-class")] = (typeof(NC_rc_char.V), typeof(char)), - [("sealed-record-class", "char", "nested-in-struct")] = (typeof(NS_rc_char.V), typeof(char)), - [("sealed-record-class", "char", "nested-two-levels")] = (typeof(N2_rc_char.Inner.V), typeof(char)), - [("validated-struct", "char", "nested-in-class")] = (typeof(NC_vs_char.V), typeof(char)), - [("validated-struct", "char", "nested-in-struct")] = (typeof(NS_vs_char.V), typeof(char)), - [("validated-struct", "char", "nested-two-levels")] = (typeof(N2_vs_char.Inner.V), typeof(char)), - [("validated-class", "char", "nested-in-class")] = (typeof(NC_vc_char.V), typeof(char)), - [("validated-class", "char", "nested-in-struct")] = (typeof(NS_vc_char.V), typeof(char)), - [("validated-class", "char", "nested-two-levels")] = (typeof(N2_vc_char.Inner.V), typeof(char)), - [("record-struct", "decimal", "nested-in-class")] = (typeof(NC_rs_decimal.V), typeof(decimal)), - [("record-struct", "decimal", "nested-in-struct")] = (typeof(NS_rs_decimal.V), typeof(decimal)), - [("record-struct", "decimal", "nested-two-levels")] = (typeof(N2_rs_decimal.Inner.V), typeof(decimal)), - [("sealed-record-class", "decimal", "nested-in-class")] = (typeof(NC_rc_decimal.V), typeof(decimal)), - [("sealed-record-class", "decimal", "nested-in-struct")] = (typeof(NS_rc_decimal.V), typeof(decimal)), - [("sealed-record-class", "decimal", "nested-two-levels")] = (typeof(N2_rc_decimal.Inner.V), typeof(decimal)), - [("validated-struct", "decimal", "nested-in-class")] = (typeof(NC_vs_decimal.V), typeof(decimal)), - [("validated-struct", "decimal", "nested-in-struct")] = (typeof(NS_vs_decimal.V), typeof(decimal)), - [("validated-struct", "decimal", "nested-two-levels")] = (typeof(N2_vs_decimal.Inner.V), typeof(decimal)), - [("validated-class", "decimal", "nested-in-class")] = (typeof(NC_vc_decimal.V), typeof(decimal)), - [("validated-class", "decimal", "nested-in-struct")] = (typeof(NS_vc_decimal.V), typeof(decimal)), - [("validated-class", "decimal", "nested-two-levels")] = (typeof(N2_vc_decimal.Inner.V), typeof(decimal)), - [("record-struct", "double", "nested-in-class")] = (typeof(NC_rs_double.V), typeof(double)), - [("record-struct", "double", "nested-in-struct")] = (typeof(NS_rs_double.V), typeof(double)), - [("record-struct", "double", "nested-two-levels")] = (typeof(N2_rs_double.Inner.V), typeof(double)), - [("sealed-record-class", "double", "nested-in-class")] = (typeof(NC_rc_double.V), typeof(double)), - [("sealed-record-class", "double", "nested-in-struct")] = (typeof(NS_rc_double.V), typeof(double)), - [("sealed-record-class", "double", "nested-two-levels")] = (typeof(N2_rc_double.Inner.V), typeof(double)), - [("validated-struct", "double", "nested-in-class")] = (typeof(NC_vs_double.V), typeof(double)), - [("validated-struct", "double", "nested-in-struct")] = (typeof(NS_vs_double.V), typeof(double)), - [("validated-struct", "double", "nested-two-levels")] = (typeof(N2_vs_double.Inner.V), typeof(double)), - [("validated-class", "double", "nested-in-class")] = (typeof(NC_vc_double.V), typeof(double)), - [("validated-class", "double", "nested-in-struct")] = (typeof(NS_vc_double.V), typeof(double)), - [("validated-class", "double", "nested-two-levels")] = (typeof(N2_vc_double.Inner.V), typeof(double)), - [("record-struct", "enum", "nested-in-class")] = (typeof(NC_rs_enum.V), typeof(PEnum)), - [("record-struct", "enum", "nested-in-struct")] = (typeof(NS_rs_enum.V), typeof(PEnum)), - [("record-struct", "enum", "nested-two-levels")] = (typeof(N2_rs_enum.Inner.V), typeof(PEnum)), - [("sealed-record-class", "enum", "nested-in-class")] = (typeof(NC_rc_enum.V), typeof(PEnum)), - [("sealed-record-class", "enum", "nested-in-struct")] = (typeof(NS_rc_enum.V), typeof(PEnum)), - [("sealed-record-class", "enum", "nested-two-levels")] = (typeof(N2_rc_enum.Inner.V), typeof(PEnum)), - [("validated-struct", "enum", "nested-in-class")] = (typeof(NC_vs_enum.V), typeof(PEnum)), - [("validated-struct", "enum", "nested-in-struct")] = (typeof(NS_vs_enum.V), typeof(PEnum)), - [("validated-struct", "enum", "nested-two-levels")] = (typeof(N2_vs_enum.Inner.V), typeof(PEnum)), - [("validated-class", "enum", "nested-in-class")] = (typeof(NC_vc_enum.V), typeof(PEnum)), - [("validated-class", "enum", "nested-in-struct")] = (typeof(NS_vc_enum.V), typeof(PEnum)), - [("validated-class", "enum", "nested-two-levels")] = (typeof(N2_vc_enum.Inner.V), typeof(PEnum)), - [("record-struct", "flags-enum", "nested-in-class")] = (typeof(NC_rs_flags_enum.V), typeof(PFlags)), - [("record-struct", "flags-enum", "nested-in-struct")] = (typeof(NS_rs_flags_enum.V), typeof(PFlags)), - [("record-struct", "flags-enum", "nested-two-levels")] = (typeof(N2_rs_flags_enum.Inner.V), typeof(PFlags)), - [("sealed-record-class", "flags-enum", "nested-in-class")] = (typeof(NC_rc_flags_enum.V), typeof(PFlags)), - [("sealed-record-class", "flags-enum", "nested-in-struct")] = (typeof(NS_rc_flags_enum.V), typeof(PFlags)), - [("sealed-record-class", "flags-enum", "nested-two-levels")] = (typeof(N2_rc_flags_enum.Inner.V), typeof(PFlags)), - [("validated-struct", "flags-enum", "nested-in-class")] = (typeof(NC_vs_flags_enum.V), typeof(PFlags)), - [("validated-struct", "flags-enum", "nested-in-struct")] = (typeof(NS_vs_flags_enum.V), typeof(PFlags)), - [("validated-struct", "flags-enum", "nested-two-levels")] = (typeof(N2_vs_flags_enum.Inner.V), typeof(PFlags)), - [("validated-class", "flags-enum", "nested-in-class")] = (typeof(NC_vc_flags_enum.V), typeof(PFlags)), - [("validated-class", "flags-enum", "nested-in-struct")] = (typeof(NS_vc_flags_enum.V), typeof(PFlags)), - [("validated-class", "flags-enum", "nested-two-levels")] = (typeof(N2_vc_flags_enum.Inner.V), typeof(PFlags)), - [("record-struct", "int", "nested-in-class")] = (typeof(NC_rs_int.V), typeof(int)), - [("record-struct", "int", "nested-in-struct")] = (typeof(NS_rs_int.V), typeof(int)), - [("record-struct", "int", "nested-two-levels")] = (typeof(N2_rs_int.Inner.V), typeof(int)), - [("sealed-record-class", "int", "nested-in-class")] = (typeof(NC_rc_int.V), typeof(int)), - [("sealed-record-class", "int", "nested-in-struct")] = (typeof(NS_rc_int.V), typeof(int)), - [("sealed-record-class", "int", "nested-two-levels")] = (typeof(N2_rc_int.Inner.V), typeof(int)), - [("validated-struct", "int", "nested-in-class")] = (typeof(NC_vs_int.V), typeof(int)), - [("validated-struct", "int", "nested-in-struct")] = (typeof(NS_vs_int.V), typeof(int)), - [("validated-struct", "int", "nested-two-levels")] = (typeof(N2_vs_int.Inner.V), typeof(int)), - [("validated-class", "int", "nested-in-class")] = (typeof(NC_vc_int.V), typeof(int)), - [("validated-class", "int", "nested-in-struct")] = (typeof(NS_vc_int.V), typeof(int)), - [("validated-class", "int", "nested-two-levels")] = (typeof(N2_vc_int.Inner.V), typeof(int)), - [("record-struct", "long", "nested-in-class")] = (typeof(NC_rs_long.V), typeof(long)), - [("record-struct", "long", "nested-in-struct")] = (typeof(NS_rs_long.V), typeof(long)), - [("record-struct", "long", "nested-two-levels")] = (typeof(N2_rs_long.Inner.V), typeof(long)), - [("sealed-record-class", "long", "nested-in-class")] = (typeof(NC_rc_long.V), typeof(long)), - [("sealed-record-class", "long", "nested-in-struct")] = (typeof(NS_rc_long.V), typeof(long)), - [("sealed-record-class", "long", "nested-two-levels")] = (typeof(N2_rc_long.Inner.V), typeof(long)), - [("validated-struct", "long", "nested-in-class")] = (typeof(NC_vs_long.V), typeof(long)), - [("validated-struct", "long", "nested-in-struct")] = (typeof(NS_vs_long.V), typeof(long)), - [("validated-struct", "long", "nested-two-levels")] = (typeof(N2_vs_long.Inner.V), typeof(long)), - [("validated-class", "long", "nested-in-class")] = (typeof(NC_vc_long.V), typeof(long)), - [("validated-class", "long", "nested-in-struct")] = (typeof(NS_vc_long.V), typeof(long)), - [("validated-class", "long", "nested-two-levels")] = (typeof(N2_vc_long.Inner.V), typeof(long)), - [("record-struct", "string", "nested-in-class")] = (typeof(NC_rs_string.V), typeof(string)), - [("record-struct", "string", "nested-in-struct")] = (typeof(NS_rs_string.V), typeof(string)), - [("record-struct", "string", "nested-two-levels")] = (typeof(N2_rs_string.Inner.V), typeof(string)), - [("sealed-record-class", "string", "nested-in-class")] = (typeof(NC_rc_string.V), typeof(string)), - [("sealed-record-class", "string", "nested-in-struct")] = (typeof(NS_rc_string.V), typeof(string)), - [("sealed-record-class", "string", "nested-two-levels")] = (typeof(N2_rc_string.Inner.V), typeof(string)), - [("validated-struct", "string", "nested-in-class")] = (typeof(NC_vs_string.V), typeof(string)), - [("validated-struct", "string", "nested-in-struct")] = (typeof(NS_vs_string.V), typeof(string)), - [("validated-struct", "string", "nested-two-levels")] = (typeof(N2_vs_string.Inner.V), typeof(string)), - [("validated-class", "string", "nested-in-class")] = (typeof(NC_vc_string.V), typeof(string)), - [("validated-class", "string", "nested-in-struct")] = (typeof(NS_vc_string.V), typeof(string)), - [("validated-class", "string", "nested-two-levels")] = (typeof(N2_vc_string.Inner.V), typeof(string)), - [("record-struct", "ulong", "nested-in-class")] = (typeof(NC_rs_ulong.V), typeof(ulong)), - [("record-struct", "ulong", "nested-in-struct")] = (typeof(NS_rs_ulong.V), typeof(ulong)), - [("record-struct", "ulong", "nested-two-levels")] = (typeof(N2_rs_ulong.Inner.V), typeof(ulong)), - [("sealed-record-class", "ulong", "nested-in-class")] = (typeof(NC_rc_ulong.V), typeof(ulong)), - [("sealed-record-class", "ulong", "nested-in-struct")] = (typeof(NS_rc_ulong.V), typeof(ulong)), - [("sealed-record-class", "ulong", "nested-two-levels")] = (typeof(N2_rc_ulong.Inner.V), typeof(ulong)), - [("validated-struct", "ulong", "nested-in-class")] = (typeof(NC_vs_ulong.V), typeof(ulong)), - [("validated-struct", "ulong", "nested-in-struct")] = (typeof(NS_vs_ulong.V), typeof(ulong)), - [("validated-struct", "ulong", "nested-two-levels")] = (typeof(N2_vs_ulong.Inner.V), typeof(ulong)), - [("validated-class", "ulong", "nested-in-class")] = (typeof(NC_vc_ulong.V), typeof(ulong)), - [("validated-class", "ulong", "nested-in-struct")] = (typeof(NS_vc_ulong.V), typeof(ulong)), - [("validated-class", "ulong", "nested-two-levels")] = (typeof(N2_vc_ulong.Inner.V), typeof(ulong)), - }; -} diff --git a/tools/probe/corpus/harness/Equality.cs b/tools/probe/corpus/harness/Equality.cs deleted file mode 100644 index 7cabd460..00000000 --- a/tools/probe/corpus/harness/Equality.cs +++ /dev/null @@ -1,211 +0,0 @@ -// Claim L05-equality (README.md#what-typekit-guarantees; Generators README "Equality", "Comparison"; oracle "Equality (metamorphic)" in -// .claude/probe-axes.md): equal value objects have equal hash codes, CompareTo is zero exactly when Equals is true, and both agree with the -// wrapped type's (ordinal for a string), through every base in the inheritance chain, and wherever the value object is held. A closed set's -// members are singletons, so its equality is reference equality (docs/closed-sets.md). -// Oracle: metamorphic. Two value objects over values the wrapped type calls equal in different forms (7.8m and 7.80m, 0.0 and -0.0, one -// instant at two offsets, two string instances), and one over a value it calls unequal and orders after. -using System.Collections; -using System.Reflection; -using Probes.Shapes; -namespace Probes.Json; - -public sealed record L05Entry(string Kind, string Wrapped, string Inheritance, string Site, Type Type, Func A1, Func A2, Func B); - -public abstract record L05PolyBase; -public sealed record L05PolyDerived : L05PolyBase { public T Value { get; set; } = default!; } - -public static partial class L05Registry -{ - static readonly List All = []; - static L05Registry() => Fill(); - static partial void Fill(); - static void Add(string kind, string wrapped, string inheritance, string site, Type type, Func a1, Func a2, Func b) => - All.Add(new(kind, wrapped, inheritance, site, type, a1, a2, b)); - public static L05Entry? Get(string kind, string wrapped, string inheritance, string site) => - All.FirstOrDefault(x => x.Kind == kind && x.Wrapped == wrapped && x.Inheritance == inheritance && x.Site == site); -} - -public static class Equality -{ - static IEqualityComparer Comparer(Type t) => (IEqualityComparer)typeof(EqualityComparer<>).MakeGenericType(t).GetProperty("Default")!.GetValue(null)!; - static int Sign(int x) => Math.Sign(x); - - static object Make(Type v, Type w, object value) - { - var m = v.GetMethods(BindingFlags.Public | BindingFlags.Static) - .First(x => x.Name is "From" or "FromKnownGood" && x.GetParameters() is [var p, ..] && p.ParameterType == w); - var args = m.GetParameters().Select((p, i) => i == 0 ? value : Type.Missing).ToArray(); - return m.Invoke(null, BindingFlags.OptionalParamBinding | BindingFlags.Default, null, args, null)!; - } - - static object? Op(Type t, string name, object a, object b) => - t.GetMethod(name, BindingFlags.Public | BindingFlags.Static, [t, t]) is { } m ? m.Invoke(null, [a, b]) : null; - - public static string? Check(Row r) - { - var kind = r["shape.kind"]; var inh = r["shape.inheritance"]; var site = r["shape.site"]; - var closed = kind == "closed-set"; - // claims.json excludes the pairs no type can have; a row the covering array made with one anyway runs the nearest shape that exists - if (site == "in-generic-type") { r.Substituted("shape.site", "top-level", "CMTK1005/CMTK1018: nothing nests in a generic type"); site = "top-level"; } - if (kind is "record-struct" or "validated-struct" && inh.StartsWith("base-")) { r.Substituted("shape.inheritance", "none", "a struct has no base type"); inh = "none"; } - var wrapped = closed ? "-" : r["shape.wrapped"]; - var en = L05Registry.Get(kind, wrapped, inh, site); - if (en is null) { r.Outcome("harness-error", $"no L05 shape for {kind}/{wrapped}/{inh}/{site}: run harness/gen_json_family.py"); return null; } - var v = en.Type; - object x1, x2, y; bool eq12; - Type? w = null; object? a1 = null, b = null; - if (closed) - { - x1 = en.A1(); x2 = en.A2(); y = en.B(); - if (!ReferenceEquals(x1, x2)) return "closed set: the member found in All is not the member itself"; - eq12 = true; - } - else - { - w = v.GetProperty("Value")!.PropertyType; - a1 = en.A1(); var a2 = en.A2(); b = en.B(); - var cw = Comparer(w); - if (!cw.Equals(a1, a2) || cw.Equals(a1, b)) { r.Outcome("harness-error", $"samples of {wrapped}: {a1} and {a2} must be equal, {b} not"); return null; } - eq12 = true; - x1 = Make(v, w, a1); x2 = Make(v, w, a2); y = Make(v, w, b); - if (!cw.Equals(v.GetProperty("Value")!.GetValue(x1), a1)) return $"Value of {v.Name} is not the value it was made from"; - } - // each failure goes to the obligation it breaks: the value object's own equality and wherever it is held, its ordering, its bases - List eqF = [], ordF = [], baseF = []; - var fails = eqF; - void Expect(bool got, bool want, string what) { if (got != want) fails.Add($"{what}: {got}, expected {want}"); } - - // the value object's own equality - Expect(x1.Equals(x2), eq12, "Equals(object) of equal values"); - Expect(x1.Equals(y), false, "Equals(object) of unequal values"); - Expect(x1.Equals(null), false, "Equals(null)"); - Expect(Comparer(v).Equals(x1, x2), eq12, "EqualityComparer of equal values"); - Expect(Comparer(v).Equals(x1, y), false, "EqualityComparer of unequal values"); - if (x1!.GetHashCode() != x2!.GetHashCode()) fails.Add($"equal values hash apart: {x1.GetHashCode()} and {x2.GetHashCode()}"); - if (Comparer(v).GetHashCode(x1) != Comparer(v).GetHashCode(x2)) fails.Add("EqualityComparer hashes equal values apart"); - if (Op(v, "op_Equality", x1, x2) is { } e12) Expect((bool)e12, eq12, "== of equal values"); else if (!closed) fails.Add("no operator =="); - if (Op(v, "op_Equality", x1, y) is { } e1b) Expect((bool)e1b, false, "== of unequal values"); - if (Op(v, "op_Inequality", x1, x2) is { } n12) Expect((bool)n12, !eq12, "!= of equal values"); - - // ordering: generated where the wrapped type is comparable, ordinal for a string, zero exactly when equal - fails = ordF; - var icv = typeof(IComparable<>).MakeGenericType(v); - if (!closed) - { - var comparableW = typeof(IComparable<>).MakeGenericType(w!).IsAssignableFrom(w) || typeof(IComparable).IsAssignableFrom(w); // an enum is IComparable only - if (icv.IsAssignableFrom(v) != comparableW) fails.Add($"IComparable<{v.Name}> is {(icv.IsAssignableFrom(v) ? "" : "not ")}implemented, the wrapped {w!.Name} is {(comparableW ? "" : "not ")}comparable"); - } - if (icv.IsAssignableFrom(v)) - { - var cmp = icv.GetMethod("CompareTo")!; - var c12 = (int)cmp.Invoke(x1, [x2])!; var c1b = (int)cmp.Invoke(x1, [y])!; var cb1 = (int)cmp.Invoke(y, [x1])!; - Expect(c12 == 0, eq12, "CompareTo of equal values is zero"); - if (c1b == 0) fails.Add("CompareTo of unequal values is zero"); - if (Sign(c1b) != -Sign(cb1)) fails.Add($"CompareTo is not antisymmetric: {c1b} and {cb1}"); - if (!closed) - { - var want = w == typeof(string) ? string.CompareOrdinal((string)a1!, (string)b!) : ((IComparer)typeof(Comparer<>).MakeGenericType(w!).GetProperty("Default")!.GetValue(null)!).Compare(a1, b); - if (Sign(c1b) != Sign(want)) fails.Add($"CompareTo orders {c1b}, the wrapped type {want}"); - } - if (x1 is IComparable nc) { if (Sign(nc.CompareTo(y)) != Sign(c1b) || nc.CompareTo(x2) != 0 == eq12) fails.Add("CompareTo(object) disagrees with CompareTo(TSelf)"); } - if (Op(v, "op_LessThan", x1, y) is { } lt) Expect((bool)lt, c1b < 0, "<"); - if (Op(v, "op_GreaterThan", x1, y) is { } gt) Expect((bool)gt, c1b > 0, ">"); - if (Op(v, "op_LessThanOrEqual", x1, x2) is { } le) Expect((bool)le, true, "<= of equal values"); - if (Op(v, "op_GreaterThanOrEqual", x1, x2) is { } ge) Expect((bool)ge, true, ">= of equal values"); - if (Op(v, "op_LessThan", x1, x2) is { } lt2) Expect((bool)lt2, false, "< of equal values"); - } - - // through every base, and every IEquatable it implements - var bases = 0; - for (var t = v.BaseType; t is not null; t = t.BaseType) - { - // object and ValueType are every value object's, a base of none: held as one, it is its own equality - var own = t == typeof(object) || t == typeof(ValueType); - if (!own) bases++; - fails = own ? eqF : baseF; - var c = Comparer(t); - Expect(c.Equals(x1, x2), eq12, $"EqualityComparer<{t.Name}> of equal values"); - Expect(c.Equals(x1, y), false, $"EqualityComparer<{t.Name}> of unequal values"); - if (c.GetHashCode(x1) != c.GetHashCode(x2)) fails.Add($"EqualityComparer<{t.Name}> hashes equal values apart"); - if (Op(t, "op_Equality", x1, x2) is { } be) Expect((bool)be, eq12, $"{t.Name}'s == of equal values"); - var set = (IEnumerable)Activator.CreateInstance(typeof(HashSet<>).MakeGenericType(t))!; - var add = set.GetType().GetMethod("Add")!; add.Invoke(set, [x1]); add.Invoke(set, [x2]); add.Invoke(set, [y]); - var count = set.Cast().Count(); - if (count != 2) fails.Add($"HashSet<{t.Name}> of two equal values and one other holds {count}"); - } - foreach (var i in v.GetInterfaces().Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEquatable<>))) - { - fails = i.GetGenericArguments()[0] == v ? eqF : baseF; // IEquatable is its own equality, IEquatable a base's - var m = i.GetMethod("Equals")!; - Expect((bool)m.Invoke(x1, [x2])!, eq12, $"{Jx.Name(i)}.Equals of equal values"); - Expect((bool)m.Invoke(x1, [y])!, false, $"{Jx.Name(i)}.Equals of unequal values"); - } - - // wherever it is held - fails = eqF; - var position = r["shape.position"]; - var containing = position == "root" ? "class" : r["shape.containing"]; - var e = new Entry(kind, wrapped, v, v, () => x1, () => null, typeof(L05PolyBase), typeof(L05PolyDerived<>).MakeGenericType(v), typeof(L05PolyBase), typeof(L05PolyDerived<>).MakeGenericType(v)); - var h1 = Pos.Build(position, containing, e, true, x1); var h2 = Pos.Build(position, containing, e, true, x2); var h3 = Pos.Build(position, containing, e, true, y); - object? Member(Held h) => position == "root" ? h.Value : h.Declared.GetProperty("P") is { } p ? p.GetValue(h.Value) : h.Declared.GetField("F")!.GetValue(h.Value); - switch (position) - { - case "root": break; - case "list-element" or "array-element" or "immutable-array-element" or "read-only-list-element": - { - var l = (IList)Member(h1)!; - Expect(l.Contains(x2), eq12, $"{Jx.Name(l.GetType())}.Contains of an equal value"); - Expect(l.Contains(y), false, $"{Jx.Name(l.GetType())}.Contains of an unequal value"); - if (l.IndexOf(x2) != 0) fails.Add("IndexOf of an equal value is " + l.IndexOf(x2)); - break; - } - case "dictionary-key": - { - var d = (IDictionary)Member(h1)!; - Expect(d.Contains(x2), eq12, "dictionary lookup by an equal key"); - Expect(d.Contains(y), false, "dictionary lookup by an unequal key"); - break; - } - case "dictionary-value": - { - var d = Member(h1)!; var cv = d.GetType().GetMethod("ContainsValue")!; - Expect((bool)cv.Invoke(d, [x2])!, eq12, "ContainsValue of an equal value"); - Expect((bool)cv.Invoke(d, [y])!, false, "ContainsValue of an unequal value"); - break; - } - default: - { - // a container with value equality compares through the member; one with reference equality is compared by its member - var byValue = containing is "record-class" or "record-struct" or "struct"; - var (p1, p2, p3) = byValue ? (h1.Value, h2.Value, h3.Value) : (Member(h1), Member(h2), Member(h3)); - var what = byValue ? Jx.Name(h1.Declared) : $"the {position} of {Jx.Name(h1.Declared)}"; - Expect(Equals(p1, p2), eq12, what + " holding equal values"); - Expect(Equals(p1, p3), false, what + " holding unequal values"); - if (p1 is not null && p2 is not null && p1.GetHashCode() != p2.GetHashCode()) fails.Add(what + " holding equal values hashes them apart"); - break; - } - } - string What(List f) => $"{Jx.Name(v)} ({kind}, {wrapped}, {inh}, {site}, {position} in {containing}): " + string.Join("; ", f); - var f1 = r.Check("equality-is-the-underlying-values", Baseline.Twin, c => - { - if (eqF.Count > 0) return What(eqF); - c.Note(NoteKeys.Compared, $"{v.Name} {position}: equality, against EqualityComparer<{(w is null ? v.Name : w.Name)}>"); - return null; - }); - var f2 = r.Check("ordering-follows-the-underlying-type", Baseline.Twin, c => - { - if (ordF.Count > 0) return What(ordF); - c.Note(NoteKeys.Compared, $"{v.Name}: ordering{(icv.IsAssignableFrom(v) ? "" : ", none generated where the wrapped type has none")}"); - return null; - }); - // a value object with a base record: each base compares it as it does (the chain is shape.inheritance's) - var f3 = bases == 0 ? null : r.Check("through-every-base", Baseline.Twin, c => - { - _ = c["shape.inheritance"]; - if (baseF.Count > 0) return What(baseF); - c.Note(NoteKeys.Compared, $"{v.Name}: {bases} base type(s)"); - return null; - }); - return f1 ?? f2 ?? f3; - } -} diff --git a/tools/probe/corpus/harness/FaultClaim.cs b/tools/probe/corpus/harness/FaultClaim.cs deleted file mode 100644 index 03018dd1..00000000 --- a/tools/probe/corpus/harness/FaultClaim.cs +++ /dev/null @@ -1,412 +0,0 @@ -// The shared runner of the fault claims C04-without-faults, C06-fault-kinds, C14-declared-problem and C15-mvc-fault-status-doc -// (AspNetCore README "A closed set as the fault type", "A status per endpoint", "Leaving out what a route cannot answer"; CHANGELOG 2.1.0 Fixed). -// A claim is a Spec: its minimal endpoints as a tree of groups, the MVC controllers of FxCtl (FaultClaimControllers.g.cs), what -// MapControllers()'s builder adds, and the errors each endpoint reports. Each declaration is listed where it is made, so an endpoint's -// declarations come in ASP.NET's metadata order: outer group, inner group, endpoint; or controller, action, then MapControllers()'s builder. -// The oracle is the README's rules applied to those declarations, never the product's own reading of them: -// - a member is answered with the last status its endpoint's metadata sets for it, else the mapped one ("When several statuses name one -// member, the last in ASP.NET's metadata order wins"); the mapping is by kind (README table), or its own with a problem type per kind; -// - a member left out anywhere in that metadata is listed nowhere in the operation, and reporting it throws an InvalidOperationException -// naming the endpoint and the member ("Leave out only what the route can't report"); -// - the operation lists every status of every remaining member of every answered type, the endpoint's own declared ones among them, all its -// responses ascending; each fault status's problem schema describes fault and rule, and its description names exactly the problem types -// of the members answered with it ("names its problem types", CHANGELOG 2.1.0); -// - a status that every endpoint of its scope leaves out stops the host, naming the member and the status. -// Each comparison is about one Aspect, and a claim's Spec.Of names the obligations it is checked for: the row runs one check (Row.Check) per -// obligation it reaches, against the README's rules (Spec.Against) or, for a refusal, the documented one (Spec.Refusals), every check on the -// one host and the answers fetched once. -// http.response picks what every endpoint of the row answers (one fault type, a composed fault, two types writing one name) and what the -// outermost scope adds (a status for another member, a member left out, a problem the endpoint declares itself). -using System.Text.Json.Nodes; -using CodoMetis.TypeKit; -using CodoMetis.TypeKit.AspNetCore; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; -using Probes.Shapes; -namespace Probes.Faults; - -/// One declaration about one member: a status (WithFaultStatus, [FaultStatus]) or, with status 0, the member left out. -public sealed record Decl(string Type, string Member, int Status) -{ - public bool Out => Status == 0; - public bool Names(Mem m) => Type == m.Type && Member == m.Name; - public override string ToString() => Out ? $"WithoutFaults({Type}.{Member})" : $"WithFaultStatus({Type}.{Member}, {Status})"; -} -/// An MVC action of FxCtl: its route (before the result-type segment), what it declares, the problem statuses it declares itself. -public sealed record Act(string Route, Decl[] Decls, int[] Declared); -/// An MVC controller of FxCtl: the [ApiController] and the plain copy, what the class declares, its actions. -public sealed record Ctl(Type Api, Type Plain, Decl[] Decls, Act[] Actions); -public static partial class FxCtl -{ - public static readonly Dictionary All = Build(); - static Dictionary Build() { var d = new Dictionary(StringComparer.Ordinal); Fill(d); return d; } - static partial void Fill(Dictionary d); -} - -/// A minimal endpoint, relative to its group; How: "pp" = ProducesProblem(each Declared), "prt" = [ProducesResponseType<ProblemDetails>(404)]. -public sealed record MEp(string Route, Decl[] Decls, int[]? Declared = null, string? How = null); -/// A route group: its prefix (never empty), what it declares, its inner groups and endpoints. -public sealed record MGrp(string Prefix, Decl[] Decls, MGrp[] Groups, MEp[] Eps); - -/// A member of a fault type the endpoints answer with, its kind and the status its kind maps to (README table). -public sealed record Mem(string Type, string Name, string Kind, int Status) { public string Q => Type + "." + Name; } - -public static class FxErr -{ - public static readonly Mem[] H = - [ - new("HFault", "Missing", "NotFound", 404), new("HFault", "Clash", "Conflict", 409), new("HFault", "Bad", "Invalid", 422), - new("HFault", "Denied", "Forbidden", 403), new("HFault", "Down", "Unavailable", 503), new("HFault", "Stale", "PreconditionFailed", 412), - new("HFault", "Garbled", "Malformed", 400), - ]; - static readonly Mem[] G = [new("GFault", "Gone", "NotFound", 404), new("GFault", "Raced", "Conflict", 409), new("GFault", "Old", "PreconditionFailed", 412)]; - static readonly Mem[] K = [new("KFault", "Clash", "Conflict", 409), new("KFault", "Lost", "NotFound", 404)]; - /// Every member of every type an endpoint answering answers with. - public static Mem[] Answered(string t) => t == "two" ? [.. H, .. G] : t == "dup" ? [.. H, .. K] : H; - /// e: 1-based index into H; 0 is a success. - static HFault Of(int e) => (HFault)typeof(HFault).GetField(H[e - 1].Name)!.GetValue(null)!; - public static Result> One(int e) { if (e == 0) return Result>.Success(); return Fault.Of(Of(e)); } - public static Result> Two(int e) => e == 0 ? Result>.Success() : Fault.Of(Of(e)); - public static Result> Dup(int e) => e == 0 ? Result>.Success() : Fault.Of(Of(e)); - public static int E(string member) => Array.FindIndex(H, m => m.Name == member) + 1; -} - -/// One endpoint as the oracle sees it: its URL in this host, the result it answers, its declarations in metadata order. -public sealed record Ep(bool Mvc, string Url, string T, Decl[] Decls, int[] Declared, Func Map) -{ - public bool LeftOut(Mem m) => Decls.Any(d => d.Out && d.Names(m)); - public int StatusOf(Mem m) => Decls.LastOrDefault(d => !d.Out && d.Names(m))?.Status ?? Map(m).Status; - public SortedSet FaultStatuses() => new(FxErr.Answered(T).Where(m => !LeftOut(m)).Select(StatusOf)); - public SortedSet DocStatuses() { var s = FaultStatuses(); foreach (var d in Declared) s.Add(d); return s; } - public HashSet TypesAt(int status) => - [.. FxErr.Answered(T).Where(m => !LeftOut(m) && StatusOf(m) == status).Select(m => Map(m).Type).OfType()]; -} - -/// What one comparison of is about; a claim's names the obligations each is checked for. -public static class Aspect -{ - /// The host starts, or refuses to as the README says. - public const string Start = "start"; - /// Reporting a member left out throws, naming the endpoint and the member. - public const string LeftOut = "left-out"; - /// A member's status, on the wire and in the problem's status field. - public const string Status = "status"; - /// The problem's fault, its type and its detail. - public const string Fault = "fault", Type = "type", Detail = "detail"; - /// The operation's statuses (which), their order, the problem schema's fault and rule, and the problem types its description names. - public const string DocListed = "doc-listed", DocOrder = "doc-order", DocDescribes = "doc-describes", DocTypes = "doc-types"; - public static readonly string[] Answer = [Status, Fault, Type, Detail]; - public static readonly string[] Doc = [DocListed, DocOrder, DocDescribes, DocTypes]; -} - -/// What one claim's row asks: see the header. -public sealed class Spec -{ - /// The obligations a comparison of an aspect is checked for, given the member it is about (none for the start and the document). - public Func Of = (_, _) => ["claim"]; - /// What the README's rules are compared as: the sentence of the claim's source. - public Baseline Against = Baseline.Model; - /// The obligations whose check holds a refusal by its type and what it names. - public string[] Refusals = []; - public MGrp? Minimal; // under the outer group /m, where the host has minimal APIs - public string[] Controllers = []; // FxCtl keys, where it has MVC - public Decl[] Builder = []; // added to MapControllers()'s builder, after the response's own - public int[] Errors = []; // the HFault members each endpoint reports (FxErr.E) - public bool Typed = true; // the host's mapping gives a problem type per kind; else AddFaultResponses() by kind - public string[]? Refused; // the host must not start, with an InvalidOperationException naming each of these - public Func? RefusedAs; // or: the host must not start, and this judges the exception (null: as expected) - public Func? Map; // how the host maps each member; default by kind, with a type per kind where Typed - public string? Mapping; // HostCfg.Mapping in place of Typed's - public Action? Services; - public Action? Extra; // more minimal endpoints, at the root - public Type[] ExtraControllers = []; -} - -/// What http.response makes every endpoint of the row answer, and what the outermost scope adds. -public sealed record Resp(string T, Decl[] Outer, int[] Declared) -{ - public static Resp Of(Row r) => r["http.response"] switch - { - "composed-fault" => new("two", [], []), - "two-faults-same-name" => new("dup", [], []), - "group-with-fault-status" => new("one", [new("HFault", "Clash", 422)], []), - "endpoint-without-faults" => new("one", [new("HFault", "Missing", 0)], []), - "declared-produces-problem" => new("decl", [], [429]), - _ => new("one", [], []), - }; -} - -/// The baseline's problem response in MVC (see FaultClaim.DocRefusedByAspNetAlone). -[ApiController] -internal sealed class FxProblemBaseline : ControllerBase -{ - [HttpGet("baseline-problem/c"), ProducesResponseType(404, "application/problem+json")] public IActionResult Get() => NoContent(); -} - -public static class FaultClaim -{ - public static string TypeOf(string kind) => "https://p.example/" + kind; - static string? Un(Row r, string? s) => r.Is("json.Converters", "own-for-wrapped") && s is not null && s.StartsWith("own:") ? s[4..].Trim('"') : s; - static string Cut(string? s) => s is null ? "(null)" : s.Length > 300 ? s[..300] + "..." : s; - static void Process(Row r, Entry en) { if (r.Is("process.converter-lookup", "wrapped-type-converter-looked-up-first")) _ = System.ComponentModel.TypeDescriptor.GetConverter(Make.Held(en.Plain)); } - - static T Member(string name) => (T)typeof(T).GetField(name)!.GetValue(null)!; - static TB Apply(TB b, Decl d) where TB : IEndpointConventionBuilder => (d.Type, d.Out) switch - { - ("HFault", true) => b.WithoutFaults(Member(d.Member)), - ("HFault", false) => b.WithFaultStatus(Member(d.Member), d.Status), - ("GFault", true) => b.WithoutFaults(Member(d.Member)), - ("GFault", false) => b.WithFaultStatus(Member(d.Member), d.Status), - ("KFault", true) => b.WithoutFaults(Member(d.Member)), - _ => b.WithFaultStatus(Member(d.Member), d.Status), - }; - - static RouteHandlerBuilder MapT(IEndpointRouteBuilder g, string path, string t, string? how) => (t, how) switch - { - ("two", "prt") => g.MapGet(path, [ProducesResponseType(404)] (int e) => FxErr.Two(e).ToHttpResult()), - ("dup", "prt") => g.MapGet(path, [ProducesResponseType(404)] (int e) => FxErr.Dup(e).ToHttpResult()), - (_, "prt") => g.MapGet(path, [ProducesResponseType(404)] (int e) => FxErr.One(e).ToHttpResult()), - ("two", _) => g.MapGet(path, (int e) => FxErr.Two(e).ToHttpResult()), - ("dup", _) => g.MapGet(path, (int e) => FxErr.Dup(e).ToHttpResult()), - _ => g.MapGet(path, (int e) => FxErr.One(e).ToHttpResult()), - }; - - /// Walks a minimal tree: maps it where is given, and lists its endpoints with their declarations in metadata order. - static void Walk(IEndpointRouteBuilder? b, MGrp g, Decl[] above, string prefix, Resp resp, List<(string Path, Decl[] Decls, int[] Declared)> eps) - { - RouteGroupBuilder? grp = null; - if (b is not null) { grp = b.MapGroup(g.Prefix); foreach (var d in g.Decls) Apply(grp, d); } - Decl[] decls = [.. above, .. g.Decls]; - var pre = prefix + g.Prefix; - foreach (var sub in g.Groups) Walk(grp, sub, decls, pre, resp, eps); - foreach (var ep in g.Eps) - { - var path = "/" + ep.Route + "/" + resp.T; - if (grp is not null) - { - var rb = MapT(grp, path, resp.T, ep.How); - foreach (var d in ep.Decls) Apply(rb, d); - if (ep.How == "pp") foreach (var s in ep.Declared ?? []) rb.ProducesProblem(s); - if (resp.T == "decl") rb.ProducesProblem(429); - } - eps.Add((pre + path, [.. decls, .. ep.Decls], [.. ep.Declared ?? [], .. resp.Declared])); - } - } - - static (List Eps, HostCfg Cfg) Plan(Row r, Spec s, Resp resp) - { - var eps = new List(); - var map = s.Map ?? (m => (m.Status, s.Typed ? TypeOf(m.Kind) : (string?)null)); - MGrp? root = s.Minimal is null ? null : new MGrp("/m", resp.Outer, [s.Minimal], []); - if (root is not null && Hx.HasMinimal(r)) - { - var found = new List<(string Path, Decl[] Decls, int[] Declared)>(); - Walk(null, root, [], "", resp, found); - eps.AddRange(found.Select(x => new Ep(false, Hx.M(r, x.Path), resp.T, x.Decls, x.Declared, map))); - } - var controllers = new List(); - Decl[] builder = [.. resp.Outer, .. s.Builder]; - if (Hx.HasMvc(r)) - foreach (var key in s.Controllers) - { - var c = FxCtl.All[key]; - controllers.Add(Hx.Plain(r) ? c.Plain : c.Api); - foreach (var a in c.Actions) - eps.Add(new Ep(true, Hx.C(r, "/c/" + a.Route + "/" + resp.T), resp.T, [.. c.Decls, .. a.Decls, .. builder], [.. a.Declared, .. resp.Declared], map)); - } - if (Hx.HasMvc(r)) controllers.AddRange(s.ExtraControllers); - var cfg = new HostCfg - { - FaultControllers = true, Controllers = [.. controllers], - Mapping = s.Mapping ?? (s.Typed ? "own-IFault-mapping" : "per-set"), - Services = sv => { if (s.Mapping is null && !s.Typed) sv.AddFaultResponses(); s.Services?.Invoke(sv); }, - Minimal = b => { if (root is not null) Walk(b, root, [], "", resp, []); s.Extra?.Invoke(b); }, - ControllerBuilder = cb => { foreach (var d in builder) Apply(cb, d); return cb; }, - }; - return (eps, cfg); - } - - public static async Task Run(Row r, Spec s) - { - var en = Registry.Of(r); Process(r, en); - var resp = Resp.Of(r); - var (eps, cfg) = Plan(r, s, resp); - if (eps.Count == 0) return "harness: the row maps no endpoint of the claim (an excluded pair?)"; - WebApplication? app = null; Exception? down = null; - try { app = await Hx.Start(r, en, cfg); } - catch (Exception ex) when (ex is not Sides.InputsNotEquivalent) { down = ex; } - await using var _ = app; - Baseline Against(string o) => s.Refusals.Contains(o) ? Baseline.Refusal : s.Against; - string? failed = null; - - // A host the README says must not start: that is the row's one comparison. - if (s.Refused is not null || s.RefusedAs is not null) - { - foreach (var o in s.Of(Aspect.Start, null)) - { - var f = r.Check(o, Against(o), _ => down is null - ? (s.RefusedAs is not null ? "host started, though the README says this set stops the start" - : $"host started, though a status every endpoint of its scope leaves out must stop it ({string.Join(", ", s.Refused!)})") - : StartRefused(s, down)); - failed ??= f; - } - return failed; - } - - var members = s.Errors.Select(e => FxErr.H[e - 1]).ToList(); - var obligations = Aspect.Answer.Append(Aspect.LeftOut).SelectMany(a => members.SelectMany(m => s.Of(a, m))) - .Concat(Aspect.Doc.SelectMany(a => s.Of(a, null))).Distinct().ToList(); - if (down is not null) - { - foreach (var o in obligations) { var f = r.Check(o, Against(o), _ => "host did not start: " + down.GetType().Name + ": " + Cut(down.Message)); failed ??= f; } - return failed; - } - - var display = app!.Services.GetRequiredService().Endpoints.OfType() - .GroupBy(e => "/" + (e.RoutePattern.RawText ?? "").TrimStart('/'), StringComparer.OrdinalIgnoreCase) - .ToDictionary(g => g.Key, g => g.First().DisplayName ?? "", StringComparer.OrdinalIgnoreCase); - JsonNode? doc = null; Exception? docFailed = null; - try { doc = await Hx.Doc(app); } - catch (Exception ex) { docFailed = ex; } - var wires = new Dictionary<(string Url, int E), Hx.Wire>(); - foreach (var ep in eps) - foreach (var e in s.Errors) - wires[(ep.Url, e)] = await Hx.Get(app, $"{ep.Url}?e={e}"); - - foreach (var o in obligations) - { - var f = await r.Check(o, Against(o), async c => - { - var wire = false; // whether the check compared an answer on the wire before the document - bool want(string aspect, Mem? m) - { - var wanted = s.Of(aspect, m).Contains(o); - if (wanted && !Aspect.Doc.Contains(aspect)) wire = true; - return wanted; - } - foreach (var ep in eps) - foreach (var e in s.Errors) - if (Wire(r, ep, FxErr.H[e - 1], wires[(ep.Url, e)], display, want) is { } fail) return fail; - if (!Aspect.Doc.Any(a => want(a, null))) return null; - // A document ASP.NET alone refuses too has nothing to check (noted there); any other failure is returned. - if (docFailed is not null) return await DocRefusedByAspNetAlone(c, r, en, cfg, docFailed, wire); - foreach (var ep in eps) - if (DocOf(r, doc!, ep, want) is { } fail) return fail; - return null; - }); - failed ??= f; - } - return failed; - } - - /// The host refused to start: as the README says, with an InvalidOperationException naming each of , or as judges. - static string? StartRefused(Spec s, Exception ex) - { - if (s.RefusedAs is { } judge) return judge(ex); - var names = s.Refused!; - var ioe = ex as InvalidOperationException ?? ex.InnerException as InvalidOperationException; - if (ioe is null) return $"host refused to start, but with {ex.GetType().Name}, not InvalidOperationException: {Cut(ex.Message)}"; - foreach (var name in names) - if (!ioe.Message.Contains(name, StringComparison.Ordinal)) return $"host refused to start, but not naming {name}: {Cut(ioe.Message)}"; - return null; - } - - /// - /// Hx.DocRefusedByAspNetAlone, with a baseline that has a problem response: the same host without AddTypeKit(), plus one endpoint (and one - /// action, where the host has MVC) declaring ProducesProblem(404). AddTypeKit() adds its problem responses "as ProducesProblem would" - /// (README "A Result as a response"), so ASP.NET alone writing a document of the same host without any problem response says nothing about - /// them: under json.MaxDepth=3 System.Text.Json refuses the problem schema of ASP.NET's own ProducesProblem too (decision 99). - /// - static async Task DocRefusedByAspNetAlone(Check c, Row r, Entry en, HostCfg cfg, Exception withTypeKit, bool comparedBefore) - { - static string Say(Exception x) => x.GetType().Name + ": " + Cut(x.Message); - var alone = cfg.Copy(); alone.TypeKitOpenApi = false; - alone.Controllers = [.. cfg.Controllers ?? [], typeof(FxProblemBaseline)]; - alone.Minimal = b => { cfg.Minimal?.Invoke(b); b.MapGet("/baseline-problem", () => Results.NoContent()).ProducesProblem(404); }; - try - { - await using var app = await Hx.Start(r, en, alone); - await Hx.Doc(app); - } - // the same refusal by type and stage, never by its words (Hx.DocAlike) - catch (Exception ex) when (Hx.DocAlike(ex, withTypeKit)) - { - Hx.DocRefused(r, ex, comparedBefore); - return null; - } - catch (Exception ex) when (r.Is("json.MaxDepth", "3") && Depth(ex) && Depth(withTypeKit)) - { - // Both refuse the problem schema under MaxDepth=3, by System.Text.Json's own depth checks, but on different paths (TypeKit's - // transformer through the schema exporter, ProducesProblem through the document writer): a refusal of STJ's own, nothing to check - // (decision 99: under MaxDepth=3 System.Text.Json refuses the OpenAPI document of a problem response for plain ASP.NET too). - return c.Refused(Cite.Record("0099"), $"{Say(withTypeKit)} / {Say(ex)}"); - } - catch (Exception ex) { return $"document: AddTypeKit() fails ({Say(withTypeKit)}), ASP.NET alone with a ProducesProblem fails otherwise ({Say(ex)})"; } - return $"document: AddTypeKit() fails ({Say(withTypeKit)}), ASP.NET alone with a ProducesProblem writes it"; - } - - static bool Depth(Exception x) => x is InvalidOperationException or System.Text.Json.JsonException; - - /// One endpoint's answer to reporting , judged on the aspects names. - static string? Wire(Row r, Ep ep, Mem m, Hx.Wire w, Dictionary display, Func want) - { - var at = $"{ep.Url} reporting {m.Q}"; - if (ep.LeftOut(m)) - { - if (!want(Aspect.LeftOut, m)) return null; - if (w.Thrown is null) return $"{at}: left out of its document, but answered {(int)w.Status}: {Cut(w.Body)}"; - if (!w.Thrown.StartsWith(nameof(InvalidOperationException) + ":", StringComparison.Ordinal)) return $"{at}: left out, threw {Cut(w.Thrown)}, expected an InvalidOperationException"; - if (!display.TryGetValue(ep.Url, out var name) || name.Length == 0) return $"harness: no endpoint named at {ep.Url}"; - if (!w.Thrown.Contains(name, StringComparison.Ordinal) || !w.Thrown.Contains(m.Q, StringComparison.Ordinal)) - return $"{at}: the refusal does not name the endpoint ({name}) and the member ({m.Q}): {Cut(w.Thrown)}"; - return null; - } - if (!Aspect.Answer.Any(a => want(a, m))) return null; - if (w.Thrown is not null) return $"{at}: request threw {Cut(w.Thrown)}"; - var status = ep.StatusOf(m); - if (want(Aspect.Status, m) && (int)w.Status != status) return $"{at}: status {(int)w.Status}, expected {status}: {Cut(w.Body)}"; - JsonNode? body; try { body = JsonNode.Parse(w.Body); } catch { return $"{at}: body is not JSON: {Cut(w.Body)}"; } - var fault = Un(r, body?["fault"]?.GetValue()); - if (want(Aspect.Fault, m) && fault != m.Name) return $"{at}: fault {fault ?? "(none)"}, expected {m.Name}"; - if (want(Aspect.Status, m) && int.Parse(body?["status"]?.ToString() is { } st ? Un(r, st)!.Trim('"') : "0") != status) return $"{at}: problem status field {body?["status"]}, expected {status}"; - if (want(Aspect.Type, m) && ep.Map(m).Type is { } type && Un(r, body?["type"]?.GetValue()) != type) return $"{at}: type {body?["type"]}, expected {type}"; - var detail = Un(r, body?["detail"]?.GetValue()); - var wantDetail = m.Q == "HFault.Missing" ? "Missing." : null; - if (want(Aspect.Detail, m) && detail != wantDetail) return $"{at}: detail {detail ?? "(none)"}, expected {wantDetail ?? "none (the member has no description)"}"; - return null; - } - - /// One endpoint's operation in the document, judged on the aspects names. - static string? DocOf(Row r, JsonNode doc, Ep ep, Func want) - { - var op = doc["paths"]?[ep.Url]?["get"]; - if (op is null && ep.Mvc && !Hx.InDocument(r)) { r.Note(NoteKeys.DocControllerNotInApiExplorer); return null; } - if (op is null) return $"document has no GET {ep.Url}"; - var responses = op["responses"]?.AsObject(); - if (responses is null) return $"{ep.Url}: operation has no responses"; - var nums = responses.Select(kv => kv.Key).Where(k => int.TryParse(k, out _)).Select(int.Parse).ToList(); - if (want(Aspect.DocOrder, null) && !nums.SequenceEqual(nums.Order())) return $"{ep.Url}: responses {string.Join(",", responses.Select(kv => kv.Key))} are not in ascending order"; - var got = nums.Where(n => n >= 400).ToList(); - var listed = ep.DocStatuses(); - // which statuses are listed, as a set: their order is DocOrder's, above - if (want(Aspect.DocListed, null) && !got.Order().SequenceEqual(listed)) return $"{ep.Url}: document lists {string.Join(",", got)}, expected {string.Join(",", listed)} ({string.Join(" ", ep.Decls)})"; - foreach (var status in ep.FaultStatuses()) - { - var resp = responses[status.ToString()]; - var txt = Hx.Deep(doc, resp)?.ToJsonString() ?? ""; - if (want(Aspect.DocDescribes, null) && !txt.Contains("\"fault\"")) return $"{ep.Url} {status}: response schema does not describe fault: {Cut(txt)}"; - if (want(Aspect.DocDescribes, null) && !txt.Contains("\"rule\"")) return $"{ep.Url} {status}: response schema does not describe rule: {Cut(txt)}"; - var desc = resp?["description"]?.GetValue() ?? ""; - const string Lead = "Problem types: "; - var i = desc.IndexOf(Lead, StringComparison.Ordinal); - var types = i < 0 ? [] : desc[(i + Lead.Length)..].Split(", ").ToHashSet(); - var wantTypes = ep.TypesAt(status); - if (want(Aspect.DocTypes, null) && !types.SetEquals(wantTypes)) - return $"{ep.Url} {status}: description \"{desc}\" names problem types {{{string.Join(", ", types)}}}, expected {{{string.Join(", ", wantTypes)}}}"; - } - return null; - } -} diff --git a/tools/probe/corpus/harness/FaultClaimControllers.g.cs b/tools/probe/corpus/harness/FaultClaimControllers.g.cs deleted file mode 100644 index afec71e4..00000000 --- a/tools/probe/corpus/harness/FaultClaimControllers.g.cs +++ /dev/null @@ -1,206 +0,0 @@ -// Generated by tools/probe/generators/faults.cs (the fault claims' MVC controllers, C04 C06 C14 C15): do not edit by hand. -// Every action answers FxErr's errors; its route ends in the result type it answers (one, two, dup) or decl (one, with a -// [ProducesResponseType(429)] of its own). The table FxCtl.All restates what each class and action declares, -// in ASP.NET's metadata order (controller, then action), as the input FaultClaim computes the expected response and document from. -using CodoMetis.TypeKit; -using CodoMetis.TypeKit.AspNetCore; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.HttpResults; -using Microsoft.AspNetCore.Mvc; -namespace Probes.Faults; - -internal abstract class FxC04EpBase : ControllerBase -{ - [HttpGet("c/fx/ep/wo/one"), WithoutFaults("Down")] public Results> FxEpWoOne(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/fx/ep/wo/two"), WithoutFaults("Down")] public Results, FaultProblem> FxEpWoTwo(int e) => FxErr.Two(e).ToHttpResult(); - [HttpGet("c/fx/ep/wo/dup"), WithoutFaults("Down")] public Results, FaultProblem> FxEpWoDup(int e) => FxErr.Dup(e).ToHttpResult(); - [HttpGet("c/fx/ep/wo/decl"), WithoutFaults("Down"), ProducesResponseType(429)] public Results> FxEpWoDecl(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/fx/ep/keep/one")] public Results> FxEpKeepOne(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/fx/ep/keep/two")] public Results, FaultProblem> FxEpKeepTwo(int e) => FxErr.Two(e).ToHttpResult(); - [HttpGet("c/fx/ep/keep/dup")] public Results, FaultProblem> FxEpKeepDup(int e) => FxErr.Dup(e).ToHttpResult(); - [HttpGet("c/fx/ep/keep/decl"), ProducesResponseType(429)] public Results> FxEpKeepDecl(int e) => FxErr.One(e).ToHttpResult(); -} -[ApiController] internal sealed class FxC04EpApi : FxC04EpBase { } -internal sealed class FxC04EpPlain : FxC04EpBase { } -[ApiController, FaultStatus("Down", 507)] internal sealed class FxC04EpSApi : FxC04EpBase { } -[FaultStatus("Down", 507)] internal sealed class FxC04EpSPlain : FxC04EpBase { } - -internal abstract class FxC04CtWoBase : ControllerBase -{ - [HttpGet("c/fx/ct/wo/a/one")] public Results> FxCtWoAOne(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/fx/ct/wo/a/two")] public Results, FaultProblem> FxCtWoATwo(int e) => FxErr.Two(e).ToHttpResult(); - [HttpGet("c/fx/ct/wo/a/dup")] public Results, FaultProblem> FxCtWoADup(int e) => FxErr.Dup(e).ToHttpResult(); - [HttpGet("c/fx/ct/wo/a/decl"), ProducesResponseType(429)] public Results> FxCtWoADecl(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/fx/ct/wo/b/one")] public Results> FxCtWoBOne(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/fx/ct/wo/b/two")] public Results, FaultProblem> FxCtWoBTwo(int e) => FxErr.Two(e).ToHttpResult(); - [HttpGet("c/fx/ct/wo/b/dup")] public Results, FaultProblem> FxCtWoBDup(int e) => FxErr.Dup(e).ToHttpResult(); - [HttpGet("c/fx/ct/wo/b/decl"), ProducesResponseType(429)] public Results> FxCtWoBDecl(int e) => FxErr.One(e).ToHttpResult(); -} -[ApiController, WithoutFaults("Down")] internal sealed class FxC04CtWoApi : FxC04CtWoBase { } -[WithoutFaults("Down")] internal sealed class FxC04CtWoPlain : FxC04CtWoBase { } - -internal abstract class FxC04CtKeepBase : ControllerBase -{ - [HttpGet("c/fx/ct/keep/one")] public Results> FxCtKeepOne(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/fx/ct/keep/two")] public Results, FaultProblem> FxCtKeepTwo(int e) => FxErr.Two(e).ToHttpResult(); - [HttpGet("c/fx/ct/keep/dup")] public Results, FaultProblem> FxCtKeepDup(int e) => FxErr.Dup(e).ToHttpResult(); - [HttpGet("c/fx/ct/keep/decl"), ProducesResponseType(429)] public Results> FxCtKeepDecl(int e) => FxErr.One(e).ToHttpResult(); -} -[ApiController] internal sealed class FxC04CtKeepApi : FxC04CtKeepBase { } -internal sealed class FxC04CtKeepPlain : FxC04CtKeepBase { } - -internal abstract class FxC04GrBase : ControllerBase -{ - [HttpGet("c/fx/gr/a/one")] public Results> FxGrAOne(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/fx/gr/a/two")] public Results, FaultProblem> FxGrATwo(int e) => FxErr.Two(e).ToHttpResult(); - [HttpGet("c/fx/gr/a/dup")] public Results, FaultProblem> FxGrADup(int e) => FxErr.Dup(e).ToHttpResult(); - [HttpGet("c/fx/gr/a/decl"), ProducesResponseType(429)] public Results> FxGrADecl(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/fx/gr/b/one")] public Results> FxGrBOne(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/fx/gr/b/two")] public Results, FaultProblem> FxGrBTwo(int e) => FxErr.Two(e).ToHttpResult(); - [HttpGet("c/fx/gr/b/dup")] public Results, FaultProblem> FxGrBDup(int e) => FxErr.Dup(e).ToHttpResult(); - [HttpGet("c/fx/gr/b/decl"), ProducesResponseType(429)] public Results> FxGrBDecl(int e) => FxErr.One(e).ToHttpResult(); -} -[ApiController] internal sealed class FxC04GrApi : FxC04GrBase { } -internal sealed class FxC04GrPlain : FxC04GrBase { } - -internal abstract class FxC06KBase : ControllerBase -{ - [HttpGet("c/k/one")] public Results> KOne(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/k/two")] public Results, FaultProblem> KTwo(int e) => FxErr.Two(e).ToHttpResult(); - [HttpGet("c/k/dup")] public Results, FaultProblem> KDup(int e) => FxErr.Dup(e).ToHttpResult(); - [HttpGet("c/k/decl"), ProducesResponseType(429)] public Results> KDecl(int e) => FxErr.One(e).ToHttpResult(); -} -[ApiController] internal sealed class FxC06KApi : FxC06KBase { } -internal sealed class FxC06KPlain : FxC06KBase { } - -internal abstract class FxC14PpBase : ControllerBase -{ - [HttpGet("c/d/pp/one"), ProducesResponseType(404, "application/problem+json")] public Results> DPpOne(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/d/pp/two"), ProducesResponseType(404, "application/problem+json")] public Results, FaultProblem> DPpTwo(int e) => FxErr.Two(e).ToHttpResult(); - [HttpGet("c/d/pp/dup"), ProducesResponseType(404, "application/problem+json")] public Results, FaultProblem> DPpDup(int e) => FxErr.Dup(e).ToHttpResult(); - [HttpGet("c/d/pp/decl"), ProducesResponseType(404, "application/problem+json"), ProducesResponseType(429)] public Results> DPpDecl(int e) => FxErr.One(e).ToHttpResult(); -} -[ApiController] internal sealed class FxC14PpApi : FxC14PpBase { } -internal sealed class FxC14PpPlain : FxC14PpBase { } - -internal abstract class FxC14PrtBase : ControllerBase -{ - [HttpGet("c/d/prt/one"), ProducesResponseType(404)] public Results> DPrtOne(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/d/prt/two"), ProducesResponseType(404)] public Results, FaultProblem> DPrtTwo(int e) => FxErr.Two(e).ToHttpResult(); - [HttpGet("c/d/prt/dup"), ProducesResponseType(404)] public Results, FaultProblem> DPrtDup(int e) => FxErr.Dup(e).ToHttpResult(); - [HttpGet("c/d/prt/decl"), ProducesResponseType(404), ProducesResponseType(429)] public Results> DPrtDecl(int e) => FxErr.One(e).ToHttpResult(); -} -[ApiController] internal sealed class FxC14PrtApi : FxC14PrtBase { } -internal sealed class FxC14PrtPlain : FxC14PrtBase { } - -internal abstract class FxC14NoneBase : ControllerBase -{ - [HttpGet("c/d/none/one")] public Results> DNoneOne(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/d/none/two")] public Results, FaultProblem> DNoneTwo(int e) => FxErr.Two(e).ToHttpResult(); - [HttpGet("c/d/none/dup")] public Results, FaultProblem> DNoneDup(int e) => FxErr.Dup(e).ToHttpResult(); - [HttpGet("c/d/none/decl"), ProducesResponseType(429)] public Results> DNoneDecl(int e) => FxErr.One(e).ToHttpResult(); -} -[ApiController] internal sealed class FxC14NoneApi : FxC14NoneBase { } -internal sealed class FxC14NonePlain : FxC14NoneBase { } - -internal abstract class FxC15MBase : ControllerBase -{ - [HttpGet("c/s/a/one")] public Results> SAOne(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/s/a/two")] public Results, FaultProblem> SATwo(int e) => FxErr.Two(e).ToHttpResult(); - [HttpGet("c/s/a/dup")] public Results, FaultProblem> SADup(int e) => FxErr.Dup(e).ToHttpResult(); - [HttpGet("c/s/a/decl"), ProducesResponseType(429)] public Results> SADecl(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/s/p/one"), FaultStatus("Denied", 452)] public Results> SPOne(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/s/p/two"), FaultStatus("Denied", 452)] public Results, FaultProblem> SPTwo(int e) => FxErr.Two(e).ToHttpResult(); - [HttpGet("c/s/p/dup"), FaultStatus("Denied", 452)] public Results, FaultProblem> SPDup(int e) => FxErr.Dup(e).ToHttpResult(); - [HttpGet("c/s/p/decl"), FaultStatus("Denied", 452), ProducesResponseType(429)] public Results> SPDecl(int e) => FxErr.One(e).ToHttpResult(); -} -[ApiController] internal sealed class FxC15MApi : FxC15MBase { } -internal sealed class FxC15MPlain : FxC15MBase { } - -internal abstract class FxC15ASBase : ControllerBase -{ - [HttpGet("c/s/a/one"), FaultStatus("Denied", 451)] public Results> SAOne(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/s/a/two"), FaultStatus("Denied", 451)] public Results, FaultProblem> SATwo(int e) => FxErr.Two(e).ToHttpResult(); - [HttpGet("c/s/a/dup"), FaultStatus("Denied", 451)] public Results, FaultProblem> SADup(int e) => FxErr.Dup(e).ToHttpResult(); - [HttpGet("c/s/a/decl"), FaultStatus("Denied", 451), ProducesResponseType(429)] public Results> SADecl(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/s/k/one")] public Results> SKOne(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/s/k/two")] public Results, FaultProblem> SKTwo(int e) => FxErr.Two(e).ToHttpResult(); - [HttpGet("c/s/k/dup")] public Results, FaultProblem> SKDup(int e) => FxErr.Dup(e).ToHttpResult(); - [HttpGet("c/s/k/decl"), ProducesResponseType(429)] public Results> SKDecl(int e) => FxErr.One(e).ToHttpResult(); -} -[ApiController] internal sealed class FxC15ASApi : FxC15ASBase { } -internal sealed class FxC15ASPlain : FxC15ASBase { } - -internal abstract class FxC15AWBase : ControllerBase -{ - [HttpGet("c/s/a/one"), WithoutFaults("Down")] public Results> SAOne(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/s/a/two"), WithoutFaults("Down")] public Results, FaultProblem> SATwo(int e) => FxErr.Two(e).ToHttpResult(); - [HttpGet("c/s/a/dup"), WithoutFaults("Down")] public Results, FaultProblem> SADup(int e) => FxErr.Dup(e).ToHttpResult(); - [HttpGet("c/s/a/decl"), WithoutFaults("Down"), ProducesResponseType(429)] public Results> SADecl(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/s/k/one")] public Results> SKOne(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/s/k/two")] public Results, FaultProblem> SKTwo(int e) => FxErr.Two(e).ToHttpResult(); - [HttpGet("c/s/k/dup")] public Results, FaultProblem> SKDup(int e) => FxErr.Dup(e).ToHttpResult(); - [HttpGet("c/s/k/decl"), ProducesResponseType(429)] public Results> SKDecl(int e) => FxErr.One(e).ToHttpResult(); -} -[ApiController] internal sealed class FxC15AWApi : FxC15AWBase { } -internal sealed class FxC15AWPlain : FxC15AWBase { } - -internal abstract class FxC15CSBase : ControllerBase -{ - [HttpGet("c/s/a/one")] public Results> SAOne(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/s/a/two")] public Results, FaultProblem> SATwo(int e) => FxErr.Two(e).ToHttpResult(); - [HttpGet("c/s/a/dup")] public Results, FaultProblem> SADup(int e) => FxErr.Dup(e).ToHttpResult(); - [HttpGet("c/s/a/decl"), ProducesResponseType(429)] public Results> SADecl(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/s/p/one"), FaultStatus("Denied", 452)] public Results> SPOne(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/s/p/two"), FaultStatus("Denied", 452)] public Results, FaultProblem> SPTwo(int e) => FxErr.Two(e).ToHttpResult(); - [HttpGet("c/s/p/dup"), FaultStatus("Denied", 452)] public Results, FaultProblem> SPDup(int e) => FxErr.Dup(e).ToHttpResult(); - [HttpGet("c/s/p/decl"), FaultStatus("Denied", 452), ProducesResponseType(429)] public Results> SPDecl(int e) => FxErr.One(e).ToHttpResult(); -} -[ApiController, FaultStatus("Denied", 451)] internal sealed class FxC15CSApi : FxC15CSBase { } -[FaultStatus("Denied", 451)] internal sealed class FxC15CSPlain : FxC15CSBase { } - -internal abstract class FxC15CWBase : ControllerBase -{ - [HttpGet("c/s/a/one")] public Results> SAOne(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/s/a/two")] public Results, FaultProblem> SATwo(int e) => FxErr.Two(e).ToHttpResult(); - [HttpGet("c/s/a/dup")] public Results, FaultProblem> SADup(int e) => FxErr.Dup(e).ToHttpResult(); - [HttpGet("c/s/a/decl"), ProducesResponseType(429)] public Results> SADecl(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/s/p/one"), FaultStatus("Denied", 452)] public Results> SPOne(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/s/p/two"), FaultStatus("Denied", 452)] public Results, FaultProblem> SPTwo(int e) => FxErr.Two(e).ToHttpResult(); - [HttpGet("c/s/p/dup"), FaultStatus("Denied", 452)] public Results, FaultProblem> SPDup(int e) => FxErr.Dup(e).ToHttpResult(); - [HttpGet("c/s/p/decl"), FaultStatus("Denied", 452), ProducesResponseType(429)] public Results> SPDecl(int e) => FxErr.One(e).ToHttpResult(); -} -[ApiController, WithoutFaults("Down")] internal sealed class FxC15CWApi : FxC15CWBase { } -[WithoutFaults("Down")] internal sealed class FxC15CWPlain : FxC15CWBase { } - -internal abstract class FxC15KBase : ControllerBase -{ - [HttpGet("c/s/k/one")] public Results> SKOne(int e) => FxErr.One(e).ToHttpResult(); - [HttpGet("c/s/k/two")] public Results, FaultProblem> SKTwo(int e) => FxErr.Two(e).ToHttpResult(); - [HttpGet("c/s/k/dup")] public Results, FaultProblem> SKDup(int e) => FxErr.Dup(e).ToHttpResult(); - [HttpGet("c/s/k/decl"), ProducesResponseType(429)] public Results> SKDecl(int e) => FxErr.One(e).ToHttpResult(); -} -[ApiController] internal sealed class FxC15KApi : FxC15KBase { } -internal sealed class FxC15KPlain : FxC15KBase { } - -public static partial class FxCtl -{ - static partial void Fill(Dictionary d) - { - d["C04Ep"] = new(typeof(FxC04EpApi), typeof(FxC04EpPlain), [], [new("fx/ep/wo", [new("HFault", "Down", 0)], []), new("fx/ep/keep", [], [])]); - d["C04EpS"] = new(typeof(FxC04EpSApi), typeof(FxC04EpSPlain), [new("HFault", "Down", 507)], [new("fx/ep/wo", [new("HFault", "Down", 0)], []), new("fx/ep/keep", [], [])]); - d["C04CtWo"] = new(typeof(FxC04CtWoApi), typeof(FxC04CtWoPlain), [new("HFault", "Down", 0)], [new("fx/ct/wo/a", [], []), new("fx/ct/wo/b", [], [])]); - d["C04CtKeep"] = new(typeof(FxC04CtKeepApi), typeof(FxC04CtKeepPlain), [], [new("fx/ct/keep", [], [])]); - d["C04Gr"] = new(typeof(FxC04GrApi), typeof(FxC04GrPlain), [], [new("fx/gr/a", [], []), new("fx/gr/b", [], [])]); - d["C06K"] = new(typeof(FxC06KApi), typeof(FxC06KPlain), [], [new("k", [], [])]); - d["C14Pp"] = new(typeof(FxC14PpApi), typeof(FxC14PpPlain), [], [new("d/pp", [], [404])]); - d["C14Prt"] = new(typeof(FxC14PrtApi), typeof(FxC14PrtPlain), [], [new("d/prt", [], [404])]); - d["C14None"] = new(typeof(FxC14NoneApi), typeof(FxC14NonePlain), [], [new("d/none", [], [])]); - d["C15M"] = new(typeof(FxC15MApi), typeof(FxC15MPlain), [], [new("s/a", [], []), new("s/p", [new("HFault", "Denied", 452)], [])]); - d["C15AS"] = new(typeof(FxC15ASApi), typeof(FxC15ASPlain), [], [new("s/a", [new("HFault", "Denied", 451)], []), new("s/k", [], [])]); - d["C15AW"] = new(typeof(FxC15AWApi), typeof(FxC15AWPlain), [], [new("s/a", [new("HFault", "Down", 0)], []), new("s/k", [], [])]); - d["C15CS"] = new(typeof(FxC15CSApi), typeof(FxC15CSPlain), [new("HFault", "Denied", 451)], [new("s/a", [], []), new("s/p", [new("HFault", "Denied", 452)], [])]); - d["C15CW"] = new(typeof(FxC15CWApi), typeof(FxC15CWPlain), [new("HFault", "Down", 0)], [new("s/a", [], []), new("s/p", [new("HFault", "Denied", 452)], [])]); - d["C15K"] = new(typeof(FxC15KApi), typeof(FxC15KPlain), [], [new("s/k", [], [])]); - } -} diff --git a/tools/probe/corpus/harness/FaultClaimSets.cs b/tools/probe/corpus/harness/FaultClaimSets.cs deleted file mode 100644 index 30111c23..00000000 --- a/tools/probe/corpus/harness/FaultClaimSets.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Fault types for C07-fault-responses-blanket (AspNetCore README#every-fault-set-at-once), each answered by one endpoint of its own beside the -// claim's: an enum (which no mapping for every set covers), a fault set with a member of no kind, and two fault sets the host's mapping -// refuses (one it throws for, one it gives a status that is no error). -using CodoMetis.TypeKit; -using CodoMetis.TypeKit.AspNetCore; -using CodoMetis.TypeKit.ClosedSets; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.HttpResults; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -namespace Probes.Faults; - -public enum FxEnum { Lost, Taken } -public sealed partial class FxNoKind : IFaultSet -{ - public static readonly FxNoKind Fine = new(FaultKind.NotFound, null); -#pragma warning disable CMTK0014 // a member of no kind, on purpose: the claim is that it stops the start - public static readonly FxNoKind Blank = new(default, null); -#pragma warning restore CMTK0014 - private FxNoKind(FaultKind kind, string? description) { Kind = kind; Description = description; } - public FaultKind Kind { get; } - public string? Description { get; } -} -public sealed partial class FxThrows : IFaultSet -{ - public static readonly FxThrows Odd = new(FaultKind.Conflict, null); - private FxThrows(FaultKind kind, string? description) { Kind = kind; Description = description; } - public FaultKind Kind { get; } - public string? Description { get; } -} -public sealed partial class FxStatus : IFaultSet -{ - public static readonly FxStatus Moved = new(FaultKind.NotFound, null); - private FxStatus(FaultKind kind, string? description) { Kind = kind; Description = description; } - public FaultKind Kind { get; } - public string? Description { get; } -} -/// What the application's mapping throws (set=mapping-throws). -public sealed class FxMappingRefused() : Exception("the mapping refuses FxThrows"); - -public static class FxSets -{ - public static Result> Enum(int e) { if (e == 0) return Result>.Success(); return Fault.Of(FxEnum.Lost); } - public static Result> NoKind(int e) { if (e == 0) return Result>.Success(); return Fault.Of(FxNoKind.Fine); } - public static Result> Throws(int e) { if (e == 0) return Result>.Success(); return Fault.Of(FxThrows.Odd); } - public static Result> Status(int e) { if (e == 0) return Result>.Success(); return Fault.Of(FxStatus.Moved); } - - /// The minimal endpoint answering the row's set, at /c7x. - public static void Map(IEndpointRouteBuilder b, string set) - { - switch (set) - { - case "enum-unmapped": b.MapGet("/c7x", (int e) => Enum(e).ToHttpResult()); break; - case "member-without-kind": b.MapGet("/c7x", (int e) => NoKind(e).ToHttpResult()); break; - case "mapping-throws": b.MapGet("/c7x", (int e) => Throws(e).ToHttpResult()); break; - case "status-not-error": b.MapGet("/c7x", (int e) => Status(e).ToHttpResult()); break; - } - } - /// The MVC controller answering the row's set. - public static Type[] Controllers(string set) => set switch - { - "enum-unmapped" => [typeof(FxC07Enum)], "member-without-kind" => [typeof(FxC07NoKind)], - "mapping-throws" => [typeof(FxC07Throws)], "status-not-error" => [typeof(FxC07Status)], _ => [], - }; -} -[ApiController] internal sealed class FxC07Enum : ControllerBase { [HttpGet("c/c7x/enum")] public Results> Get(int e) => FxSets.Enum(e).ToHttpResult(); } -[ApiController] internal sealed class FxC07NoKind : ControllerBase { [HttpGet("c/c7x/nokind")] public Results> Get(int e) => FxSets.NoKind(e).ToHttpResult(); } -[ApiController] internal sealed class FxC07Throws : ControllerBase { [HttpGet("c/c7x/throws")] public Results> Get(int e) => FxSets.Throws(e).ToHttpResult(); } -[ApiController] internal sealed class FxC07Status : ControllerBase { [HttpGet("c/c7x/status")] public Results> Get(int e) => FxSets.Status(e).ToHttpResult(); } diff --git a/tools/probe/corpus/harness/FaultControllers.cs b/tools/probe/corpus/harness/FaultControllers.cs deleted file mode 100644 index f93ff5dc..00000000 --- a/tools/probe/corpus/harness/FaultControllers.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Controllers answering with faults; the Plain* copies have no [ApiController] (http.host=controller-without-ApiController, and -// controller-without-ApiController-visible, where a convention puts them into the document). -using CodoMetis.TypeKit; -using CodoMetis.TypeKit.AspNetCore; -using Microsoft.AspNetCore.Http.HttpResults; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -namespace Probes; - -[ApiController] -public sealed class MvcAController : ControllerBase -{ - [HttpGet("c/two")] public Results, FaultProblem> Two(int e) => Errors.Two(e).ToHttpResult(); - [HttpGet("c/wo"), WithoutFaults(nameof(HFault.Missing), nameof(HFault.Bad))] - public Results, FaultProblem> Wo(int e) => Errors.Two(e).ToHttpResult(); - [HttpGet("c/fs"), FaultStatus(nameof(HFault.Clash), 422)] - public Results, FaultProblem> Fs(int e) => Errors.Two(e).ToHttpResult(); - [HttpGet("c/three")] public Results, FaultProblem, FaultProblem> Three(int e) => Errors.Three(e).ToHttpResult(); - [HttpGet("c/declared404"), ProducesResponseType(404)] - public Results, FaultProblem> Declared404(int e) => Errors.Two(e).ToHttpResult(); - [HttpGet("c/declared"), ProducesResponseType(429)] - public Results, FaultProblem> Declared(int e) => Errors.Two(e).ToHttpResult(); -} -[ApiController, FaultStatus(nameof(HFault.Denied), 451), WithoutFaults(nameof(HFault.Down))] -public sealed class MvcBController : ControllerBase -{ - [HttpGet("d/two")] public Results, FaultProblem> Two(int e) => Errors.Two(e).ToHttpResult(); -} - - -public sealed class PlainAController : ControllerBase -{ - [HttpGet("c/two")] public Results, FaultProblem> Two(int e) => Errors.Two(e).ToHttpResult(); - [HttpGet("c/wo"), WithoutFaults(nameof(HFault.Missing), nameof(HFault.Bad))] - public Results, FaultProblem> Wo(int e) => Errors.Two(e).ToHttpResult(); - [HttpGet("c/fs"), FaultStatus(nameof(HFault.Clash), 422)] - public Results, FaultProblem> Fs(int e) => Errors.Two(e).ToHttpResult(); - [HttpGet("c/three")] public Results, FaultProblem, FaultProblem> Three(int e) => Errors.Three(e).ToHttpResult(); - [HttpGet("c/declared404"), ProducesResponseType(404)] - public Results, FaultProblem> Declared404(int e) => Errors.Two(e).ToHttpResult(); - [HttpGet("c/declared"), ProducesResponseType(429)] - public Results, FaultProblem> Declared(int e) => Errors.Two(e).ToHttpResult(); -} -[FaultStatus(nameof(HFault.Denied), 451), WithoutFaults(nameof(HFault.Down))] -public sealed class PlainBController : ControllerBase -{ - [HttpGet("d/two")] public Results, FaultProblem> Two(int e) => Errors.Two(e).ToHttpResult(); -} - diff --git a/tools/probe/corpus/harness/Faults.cs b/tools/probe/corpus/harness/Faults.cs deleted file mode 100644 index eac16d31..00000000 --- a/tools/probe/corpus/harness/Faults.cs +++ /dev/null @@ -1,106 +0,0 @@ -// Fault types, error tables and shared logging for the HTTP probes (harness; see HttpHost.cs). -using CodoMetis.TypeKit; -using CodoMetis.TypeKit.ValueObjects; -using CodoMetis.TypeKit.ClosedSets; -using Microsoft.AspNetCore.Mvc.ApplicationParts; -using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.Extensions.Logging; -using System.Reflection; -namespace Probes; - -public sealed partial class HFault : IFaultSet -{ - public static readonly HFault Missing = new(FaultKind.NotFound, "Missing."); - public static readonly HFault Clash = new(FaultKind.Conflict, null); - public static readonly HFault Bad = new(FaultKind.Invalid, null); - public static readonly HFault Denied = new(FaultKind.Forbidden, null); - public static readonly HFault Down = new(FaultKind.Unavailable, null); - public static readonly HFault Stale = new(FaultKind.PreconditionFailed, null); - public static readonly HFault Garbled = new(FaultKind.Malformed, null); - private HFault(FaultKind kind, string? description) { Kind = kind; Description = description; } - public FaultKind Kind { get; } - public string? Description { get; } -} -public sealed partial class GFault : IFaultSet -{ - public static readonly GFault Gone = new(FaultKind.NotFound, null); - public static readonly GFault Raced = new(FaultKind.Conflict, null); - public static readonly GFault Old = new(FaultKind.PreconditionFailed, null); - private GFault(FaultKind kind, string? description) { Kind = kind; Description = description; } - public FaultKind Kind { get; } - public string? Description { get; } -} -public enum SFault { First, Second } -public enum PRule { A, B } -[Flags] public enum FRule { P = 1, Q = 2 } -public static class Holder { public enum NestedRule { N } } -public sealed partial class RuleSet : IClosedSet { public static readonly RuleSet X = new(); public static readonly RuleSet Y = new(); } - -public static class Errors -{ - public static Result> Two(int e) => e switch - { - 0 => Result>.Success(), - 1 => Fault.Of(HFault.Missing), - 2 => Fault.Of(HFault.Clash), - 3 => Fault.Of(HFault.Bad), - 4 => Fault.Of(HFault.Denied), - 5 => Fault.Of(HFault.Down), - 6 => Fault.Of(HFault.Stale), - 7 => Fault.Of(HFault.Garbled), - 8 => Fault.Of(GFault.Gone), - 9 => Fault.Of(GFault.Raced), - 10 => Fault.Of(GFault.Old), - _ => throw new ArgumentOutOfRangeException(nameof(e)), - }; - public static Result> Three(int e) => e switch - { - 0 => Result>.Success(), - 1 => Fault.Of(HFault.Missing), - 8 => Fault.Of(GFault.Gone), - 11 => Fault.Of(SFault.First), - 12 => Fault.Of(Fault.Of(SFault.Second, "second detail").WithRule(PRule.B)), - 13 => Fault.Of(Fault.Of(HFault.Clash).WithRule(RuleSet.X)), - 14 => Fault.Of(Fault.Of(GFault.Raced).WithRule(Holder.NestedRule.N)), - _ => throw new ArgumentOutOfRangeException(nameof(e)), - }; - public static Result> One(int e) => e switch - { - 0 => Result>.Success(), - 1 => Fault.Of(HFault.Missing), - 2 => Fault.Of(HFault.Clash), - 4 => Fault.Of(HFault.Denied), - 5 => Fault.Of(HFault.Down), - _ => throw new ArgumentOutOfRangeException(nameof(e)), - }; - public static Result> OneG(int e) => e switch - { - 0 => Result>.Success(), - 8 => Fault.Of(GFault.Gone), - 9 => Fault.Of(GFault.Raced), - _ => throw new ArgumentOutOfRangeException(nameof(e)), - }; -} - -public sealed class ListedLogs : ILoggerProvider -{ - public List Lines { get; } = []; - public ILogger CreateLogger(string categoryName) => new L(this); - public void Dispose() { } - private sealed class L(ListedLogs o) : ILogger - { - public IDisposable? BeginScope(TState state) where TState : notnull => null; - public bool IsEnabled(LogLevel logLevel) => true; - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) => - o.Lines.Add($"{logLevel}: {formatter(state, exception)}"); - } -} - -public sealed class OnlyControllers(Type[] controllers) : IApplicationFeatureProvider -{ - public void PopulateFeature(IEnumerable parts, ControllerFeature feature) - { - foreach (var c in controllers) feature.Controllers.Add(c.GetTypeInfo()); - } -} - diff --git a/tools/probe/corpus/harness/HandOrdered.cs b/tools/probe/corpus/harness/HandOrdered.cs deleted file mode 100644 index c1c72cfb..00000000 --- a/tools/probe/corpus/harness/HandOrdered.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Value objects that declare their own CompareTo(TSelf) (a descending order), for the EF claims about hand-written ordering. -#nullable enable -#pragma warning disable CMTK0014, CS8618, CS1591, CMTK0005, CMTK1000, CMTK0003, CMTK0006 -using CodoMetis.TypeKit.ValueObjects; -namespace Probes.Ef; - -public readonly partial record struct HO_int_struct : IValue { public int CompareTo(HO_int_struct other) => other.Value.CompareTo(Value); } -public readonly partial record struct HO_string_struct : IValue { public int CompareTo(HO_string_struct other) => string.CompareOrdinal(other.Value, Value); } -public readonly partial record struct HO_decimal_struct : IValue { public int CompareTo(HO_decimal_struct other) => other.Value.CompareTo(Value); } -public readonly partial record struct HO_DateTime_struct : IValue { public int CompareTo(HO_DateTime_struct other) => other.Value.CompareTo(Value); } -public sealed partial record class HO_int_class : IValue { public int CompareTo(HO_int_class? other) => other is null ? 1 : other.Value.CompareTo(Value); } -public sealed partial record class HO_string_class : IValue { public int CompareTo(HO_string_class? other) => other is null ? 1 : string.CompareOrdinal(other.Value, Value); } - -public static class HandOrdered -{ - public sealed record Entry(string Name, Type Vo, Type Plain, object[] Values); - public static readonly Entry[] All = - [ - new("int-struct", typeof(HO_int_struct), typeof(int), [1, 2, 3]), - new("string-struct", typeof(HO_string_struct), typeof(string), ["a", "b", "c"]), - new("decimal-struct", typeof(HO_decimal_struct), typeof(decimal), [1.5m, 2.5m, 3.5m]), - new("DateTime-struct", typeof(HO_DateTime_struct), typeof(DateTime), [new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), new DateTime(2026, 1, 2, 0, 0, 0, DateTimeKind.Utc), new DateTime(2026, 1, 3, 0, 0, 0, DateTimeKind.Utc)]), - new("int-class", typeof(HO_int_class), typeof(int), [1, 2, 3]), - new("string-class", typeof(HO_string_class), typeof(string), ["a", "b", "c"]), - ]; - public static Entry Of(string name) => All.First(e => e.Name == name); - public static object Make(Entry e, object plain) => e.Vo.GetMethod("From", [e.Plain])!.Invoke(null, [plain])!; -} diff --git a/tools/probe/corpus/harness/HttpBaseline.cs b/tools/probe/corpus/harness/HttpBaseline.cs deleted file mode 100644 index 731f5d84..00000000 --- a/tools/probe/corpus/harness/HttpBaseline.cs +++ /dev/null @@ -1,800 +0,0 @@ -// The baseline every http claim shares: under the row's options, a payload with the value object behaves like the same payload with the wrapped -// type (response bytes, request reading, parameter binding, OpenAPI schema), in the hosting models the row's http.host names. -// Oracle: differential (JSON/wire/schema parity), each comparison a check (Row.Check); a difference a document makes intended ends its check -// as that citation (Cite), and one no document makes intended is compared. -using System.Net; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using System.Text.Json; -using System.Text.Json.Nodes; -using CodoMetis.TypeKit.Facts; -namespace Probes.Shapes; - -public static class HttpBaseline -{ - static string Cut(string? s) => s is null ? "(null)" : s.Length > 220 ? s[..220] + "..." : s; - - /// True only where the shape's Create refuses exactly the wrapped type's limit (the validated byte refuses 255): - /// anywhere else a refused limit is the value object's to explain. - static bool RefusesItsLimit(Entry e) - { - var refused = e.RefusedFn(); // the plain value the shape's Create refuses - if (refused is null) return false; - var t = Make.Held(e.Plain); - return new[] { "MaxValue", "MinValue" }.Any(f => t.GetField(f)?.GetValue(null) is { } limit && limit.Equals(refused)); - } - - /// - /// One wire comparison with the twin, inside its check : the wrapped type's answer (side (2)) and the - /// value object's (side (3)); side (1) refusing alike was decided before the check (). A difference a - /// document makes intended ends the check as that document's citation; BaseProbe calibrates the host on C07's rows, whose claim promises - /// nothing of the wire, so these cite the sentence itself, not a not-promised entry of C07. - /// - static string? Same(Check c, Row r, Entry e, string what, Hx.Wire p, Hx.Wire v, string input = "valid") - { - r.Log.AppendLine($"{what}: plain {(int)p.Status} {Cut(p.Body)} {p.Thrown} | vo {(int)v.Status} {Cut(v.Body)} {v.Thrown}"); - bool ok(Hx.Wire w) => (int)w.Status is >= 200 and < 300; - // both refused, and not as the twin without TypeKit refuses (Sides.Alike): evidence about refusing, and nothing to compare - if (!ok(p) && !ok(v)) return c.Inconclusive($"{what}: both refuse, not alike: wrapped {p.Side(what)}, value object {v.Side(what)}"); - // "Your own converter wins": the application's converter writes the value object instead of the generated one. - if (r.Is("json.Converters", "own-for-value-object") && ok(v)) return c.Inapplicable(Cite.Docs("src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says"), what); - if (ok(p) && !ok(v)) - { - if (ReadRefusedAsDocumented(c, r, e, input, what)) return null; - // the same: the value object's array reads a JSON array of numbers, never the base64 string the twin's byte[] wrote and reads - if (ByteArray(r, e)) return c.Inapplicable(Cite.Docs("docs/json.md#the-wrapped-types-json"), what); - // A closed set travels as its member's name; the wrapped type under the application's converter is no reference. - if (e.Kind == "closed-set" && r.Is("json.Converters", "own-for-wrapped")) return c.Inapplicable(Cite.Docs("README.md#what-you-get"), what); - // The validated shapes' Create refuses the wrapped type's limit on purpose, so the limit input is a refusal. - if (e.Kind.StartsWith("validated") && input == "at-type-limit" && RefusesItsLimit(e)) return c.Refused(Cite.Docs("docs/json.md#when-a-value-is-rejected"), what); - return (e.Wrapped == "NodaTime.Instant" && what.StartsWith("mvc bind") ? "[F1] " : "") + $"{what}: wrapped answered {(int)p.Status} {Cut(p.Body)}, value object answered {(int)v.Status} {Cut(v.Body)} {v.Thrown}"; - } - if (!ok(p) && ok(v)) - { - // docs/json.md "Null and missing": a struct value object rejects a JSON null; a sealed partial record reads it as null, - // like any class, where the struct it wraps refuses it. - if (input == "null" && Make.AdmitsNull(e.Vo) && !Make.AdmitsNull(e.Plain)) return c.Refused(Cite.Docs("docs/json.md#null-and-missing"), what); - return $"{what}: value object answered {(int)v.Status} {Cut(v.Body)} where wrapped answered {(int)p.Status} {Cut(p.Body)} {p.Thrown}"; - } - if (p.Body != v.Body) - { - // A closed set travels as its member's name, never as the string or number standing in for it. - if (e.Kind == "closed-set") return c.Inapplicable(Cite.Docs("README.md#what-you-get"), what); - // docs/json.md "The wrapped type's JSON": an array of value objects over byte is a JSON array of numbers, where the twin, a byte[], - // is one base64 string (KB-038). - if (ByteArray(r, e)) return c.Inapplicable(Cite.Docs("docs/json.md#the-wrapped-types-json"), what); - return $"{what}: bodies differ: wrapped {Cut(p.Body)} vs value object {Cut(v.Body)}"; - } - c.Note(NoteKeys.Compared, what); - return null; - } - - /// - /// One check of the wire, three-sided (Sides): side (2) and side (3) answered on the host with - /// TypeKit; side (1) is asked of the host without it (), lazily, only where they disagree or refuse. Where (1) - /// refuses alike with (3), the check is the framework's refusal against Baseline.Framework; where (1) answers and the value object's - /// write was refused under a depth limit, side (1) is a converter's value there (KB-040, in process); otherwise - /// compares, once (2) is held to (1). - /// - static async Task Wire(Row r, Entry e, string obligation, Baseline against, string what, Hx.Wire p, Hx.Wire v, Func> one, Func> body) - { - var two = p.Side(what); var three = v.Side(what); - Sides.Answer? first = null; Sides.Side? framework = null; - if (two.Refused || three.Refused || p.Body != v.Body) - { - if (await one() is { } w) first = w.Side(what); - if (three.Refused && first is { Refused: true } && Sides.Alike(first, three)) framework = Sides.Measured(first, "the twin on a host without TypeKit"); - else if (three.Refused && v.Ex is { } thrown && first is { Refused: false } && (Sides.Why = null) is null - && Sides.WrittenByAConverter(r, e, Twin(r, e), Sides.WithoutTypeKit(JsonRow.Apply(r, e).Options!), new("write", null, thrown)) is { } converter - && Sides.Alike(converter.Answer, new("write", null, thrown))) - return Sides.Check(r, obligation, against, converter, new("write", null, thrown), null, null, _ => (string?)null); - } - var failed = await Sides.Check(r, obligation, against, framework, three, first, first is null ? null : two, body); - // the failure names side (1), for the triage of the row - return failed is null || first is null ? failed : failed + $" [side (1), the twin without TypeKit: {first}{(Sides.Why is { } why ? "; " + why : "")}]"; - } - - /// The wrapped twin's payload, as the echo writes it: the row's position and containing type around the wrapped value. - static Held Twin(Row r, Entry e) => Pos.Build(r["shape.position"], r.Get("shape.containing") ?? "class", e, false, e.Valid()); - - /// An array of value objects over byte, whose twin is a byte[] (KB-038). - static bool ByteArray(Row r, Entry e) => e.Wrapped == "byte" && r["shape.position"] == "array-element"; - - /// What MVC writes for a type's default: null for no content (a reference, or a Nullable), else its JSON under MVC's options. - static string? DefaultBody(WebApplication app, Type t) => - !t.IsValueType || Nullable.GetUnderlyingType(t) is not null ? null - : JsonSerializer.Serialize(Activator.CreateInstance(t), t, app.Services.GetRequiredService>().Value.JsonSerializerOptions); - - /// - /// Whether an answer is the type's default as MVC writes it: 204 for null, else 200 with its JSON, compared as JSON, since MVC's - /// formatter escapes with its own encoder where the options name none. - /// - static bool HoldsDefault(Hx.Wire w, string? body) - { - if (body is null) return w.Status == HttpStatusCode.NoContent; - if (w.Status != HttpStatusCode.OK) return false; - try { return JsonNode.DeepEquals(JsonNode.Parse(w.Body), JsonNode.Parse(body)); } - catch (JsonException) { return false; } - } - - /// A text the wrapped type does not read, for a twin that shows what MVC does with a binding failure; none where any text binds. - static string? Unreadable(Entry e) => e.Wrapped is "string" or "Uri" or "own-record-class" ? null : "abc"; - - /// - /// The value object refused a read the wrapped type answered, as a document says it does (the JSON family's documented differences, each - /// cited by its sentence; JsonParity.ReadMayDiffer listed them uncited): side (1) answered and (3) refused, which the citation makes - /// intended (Sides). The check ends as that refusal and true is returned. - /// - static bool ReadRefusedAsDocumented(Check c, Row r, Entry e, string input, string what) - { - if (r.Is("json.ReferenceHandler", "Preserve") && e.Wrapped == "own-record-class") { c.Refused(Cite.Docs("src/CodoMetis.TypeKit.Generators/README.md#json"), what); return true; } - if (e.Kind.StartsWith("validated") && input == "refused-by-create" && r.Get("json.Converters") != "StoredJsonConverterFactory") { c.Refused(Cite.Docs("docs/json.md#when-a-value-is-rejected"), what); return true; } - if (input == "null" && !Make.AdmitsNull(e.Vo)) { c.Refused(Cite.Docs("docs/json.md#null-and-missing"), what); return true; } - if (e.Kind == "closed-set" && input != "valid") { c.Refused(Cite.Docs("README.md#what-you-get"), what); return true; } - return false; - } - - /// Where two schemas first differ, and what each side has there: the comparison's 220 characters rarely reach it. - static string FirstDifference(JsonNode? p, JsonNode? v, string path) => - Difference(p, v, path) is var d ? $"{d.Path}: wrapped {Cut(d.P?.ToJsonString() ?? "(absent)")} | value object {Cut(d.V?.ToJsonString() ?? "(absent)")}" : ""; - - /// - /// Where two schemas first differ: the path as it is shown, the keys that lead there (a property's name, an element's index), each - /// side's node there (null where it is absent) and the node holding it, so a difference is told by its structure, never by its path's text. - /// - sealed record Diff(string Path, IReadOnlyList Keys, JsonNode? P, JsonNode? V, JsonNode? ParentP, JsonNode? ParentV); - - static Diff Difference(JsonNode? p, JsonNode? v, string path) => Difference(p, v, path, [], null, null); - - static Diff Difference(JsonNode? p, JsonNode? v, string path, List keys, JsonNode? parentP, JsonNode? parentV) - { - if (p is JsonObject po && v is JsonObject vo) - { - foreach (var k in po.Select(kv => kv.Key).Union(vo.Select(kv => kv.Key)).Order(StringComparer.Ordinal)) - if (!JsonNode.DeepEquals(po[k], vo[k])) return Difference(po[k], vo[k], path + "." + k, [.. keys, k], p, v); - } - else if (p is JsonArray pa && v is JsonArray va && pa.Count == va.Count) - { - for (var i = 0; i < pa.Count; i++) - if (!JsonNode.DeepEquals(pa[i], va[i])) return Difference(pa[i], va[i], $"{path}[{i}]", [.. keys, i.ToString(System.Globalization.CultureInfo.InvariantCulture)], p, v); - } - return new(path, keys, p, v, parentP, parentV); - } - - /// The node the keys of a lead to in another schema, or null where it has none. - static JsonNode? At(JsonNode? node, IEnumerable keys) - { - foreach (var k in keys) - node = node switch { JsonObject o => o[k], JsonArray a when int.TryParse(k, out var i) && i < a.Count => a[i], _ => null }; - return node; - } - - /// - /// Side (1) of a row's wire and schema comparisons: the same host started without TypeKit (HostCfg.WithoutTypeKit), once, on the first - /// comparison that asks for it; null where it does not start, which no comparison then excuses anything by. The twin's side (1) is a - /// host with the twin's endpoints alone (plain): ASP.NET alone describing the value object's types beside them names components - /// after them and writes the twin's own schemas otherwise. A host with both (both) answers for the value object's position. - /// - sealed class SideOne(Row r, Entry e, string only) : IAsyncDisposable - { - Task? started; - public string? Down { get; private set; } - public Task Host() => started ??= Start(); - async Task Start() - { - try { return await Hx.Start(r, e, new HostCfg { FaultControllers = false, MapBlanketMapping = false, WithoutTypeKit = true, PayloadOnly = only }); } - catch (Exception x) when (x is not Sides.InputsNotEquivalent) { Down = x.GetType().Name + ": " + Cut(x.Message); r.Log.AppendLine("side (1) host did not start: " + Down); return null; } - } - public async Task Ask(Func> ask) => await Host() is { } app ? await ask(app) : null; - public async ValueTask DisposeAsync() { if (started is not null && await started is { } app) await app.DisposeAsync(); } - } - - /// - /// BaseProbe's calibration of the shared host: every wire comparison a check of its own, named (BaseProbe - /// runs on rows of C07, which promises nothing of the wire, so its checks are the implicit claim). Per side: the echo against the - /// twin; the request read with the row's input.json, against the twin, or a closed set against its own response read back; the binding of - /// the row's http.binding with a valid value against the twin, and with a value Create refuses against the documented 400. Each is - /// three-sided (Sides): side (1), the twin on the same host without TypeKit, is asked where the twin and the value object disagree or - /// refuse. With , the schema parity too (C21's check). Every check runs; the first failure is returned. - /// - public static async Task Run(Row r, Entry e, bool schema = true, string obligation = "claim") - { - WebApplication app; - try { app = await Hx.Start(r, e, new HostCfg { FaultControllers = false, MapBlanketMapping = false }); } - catch (Exception ex) when (ex is not Sides.InputsNotEquivalent) { return "baseline host did not start: " + ex.GetType().Name + ": " + Cut(ex.Message); } - await using var _ = app; - await using var one = new SideOne(r, e, "plain"); - await using var alone = new SideOne(r, e, "both"); - var jopts = JsonRow.Apply(r, e).Options!; - var position = r["shape.position"]; - var plainSample = e.Valid(); - var input = r.Get("input.json") ?? "valid"; - var ownVo = r.Is("json.Converters", "own-for-value-object"); - var bind = r["http.binding"]; - string? failed = null; - var sides = new List<(string name, Func path, string echo, string inn, string bindPre)>(); - if (Hx.HasMinimal(r)) sides.Add(("minimal", p => Hx.M(r, p), "/echo/", "/in/", "/bind/")); - if (Hx.HasMvc(r)) sides.Add(("mvc", p => Hx.C(r, p), "/mvc/echo/", "/mvc/in/", "/mvc/bind/")); - foreach (var (name, path, echo, inn, bindPre) in sides) - { - // 1. response side - var ep = await Hx.Get(app, path(echo + "plain")); var ev = await Hx.Get(app, path(echo + "vo")); - var f1 = await Wire(r, e, obligation, Baseline.Twin, name + " echo", ep, ev, () => one.Ask(h => Hx.Get(h, path(echo + "plain"))), - c => Task.FromResult(Same(c, r, e, name + " echo", ep, ev))); - failed ??= f1; - // 2. request side, with the row's input.json, where the wrapped type's echo gave a body to change - if ((int)ep.Status == 200) - { - string? f2; - if (ownVo) - // "Your own converter wins": the application's converter reads the value object, so the twin's text is none of its; - // only that it reads its own response back is held, and the check earns nothing. - f2 = await r.Check(obligation, Baseline.RoundTrip, async c => - { - var ip = await Hx.Send(app, HttpMethod.Post, path(inn + "plain"), ep.Body); var iv = await Hx.Send(app, HttpMethod.Post, path(inn + "vo"), ev.Body); - if ((int)ev.Status == 200 && (int)iv.Status != 200 && (int)ip.Status == 200) return $"{name} in: the value object cannot read its own response ({(int)iv.Status}): {Cut(iv.Body)}"; - return c.Inapplicable(Cite.Docs("src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says"), name + " in"); - }); - else if (e.Kind == "closed-set") - // A closed set travels as its member's name (README "What you get"): the string standing in for it is no twin, so the - // value object reads its own response back. - f2 = await r.Check(obligation, Baseline.RoundTrip, async c => - { - if ((int)ev.Status != 200) return c.Inconclusive($"{name} in: the value object's echo answered {(int)ev.Status}, no response of its own to read back"); - var ip = await Hx.Send(app, HttpMethod.Post, path(inn + "plain"), ep.Body); var iv = await Hx.Send(app, HttpMethod.Post, path(inn + "vo"), ev.Body); - r.Log.AppendLine($"{name} in(own response): plain {(int)ip.Status} {Cut(ip.Body)} {ip.Thrown} | vo {(int)iv.Status} {Cut(iv.Body)} {iv.Thrown}"); - // The wrapped type does not read its own response back under the row's options either: nothing to tell. - if ((int)iv.Status != 200 && (int)ip.Status != 200) return c.Inconclusive($"{name} in: neither side reads its own response back ({(int)ip.Status}, {(int)iv.Status})"); - if ((int)iv.Status != 200) return $"{name} in: the closed set cannot read its own response ({(int)iv.Status}), the wrapped type reads its own: {Cut(iv.Body)} {iv.Thrown}"; - c.Note(NoteKeys.Compared, name + " in: its own response read back"); - return null; - }); - else - { - var text = JsonParity.InputFor(input, e, jopts, ep.Body, position); - var ip = await Hx.Send(app, HttpMethod.Post, path(inn + "plain"), text); var iv = await Hx.Send(app, HttpMethod.Post, path(inn + "vo"), text); - var what = name + " in(" + input + ")"; - f2 = await Wire(r, e, obligation, Baseline.Twin, what, ip, iv, () => one.Ask(h => Hx.Send(h, HttpMethod.Post, path(inn + "plain"), text)), - c => Task.FromResult(Same(c, r, e, what, ip, iv, input))); - } - failed ??= f2; - } - // 3. binding, the row's http.binding - var unbindable = e.Wrapped == "own-record-class" && bind != "body" ? "a record class" - : e.Wrapped == "NodaTime.Instant" && name == "minimal" && bind != "body" ? "Instant in a minimal API" : null; - if (unbindable is not null) - { - // No sentence cites it: the wrapped type is not bound from text either, so neither side has an endpoint and there is no twin. - r.Check(obligation, Baseline.Twin, c => c.Inconclusive($"{name} bind {bind}: {unbindable} is not bound from text, the wrapped type neither")); - continue; - } - if (bind == "body" && ownVo) - { - // "Your own converter wins": the application's converter reads the body, not the twin's JSON. - r.Check(obligation, Baseline.Twin, c => c.Inapplicable(Cite.Docs("src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says"), $"{name} bind body")); - continue; - } - var valid = Hx.BindText(e, plainSample); - var refused = e.Refused() is { } rf ? Hx.BindText(e, rf) : null; - async Task B(WebApplication host, string which, string txt, string w) - { - var enc = Uri.EscapeDataString(txt); - return bind switch - { - "route" => await Hx.Get(host, path(bindPre + "route/" + w + "/" + enc)), - "query" => await Hx.Get(host, path(bindPre + "query/" + w + "?v=" + enc)), - "as-parameters" => await Hx.Get(host, path(bindPre + "params/" + w + "?v=" + enc)), - "header" => await Hx.Send(host, HttpMethod.Get, path(bindPre + "header/" + w), header: ("v", txt)), - "form" => await Hx.Send(host, HttpMethod.Post, path(bindPre + "form/" + w), "v=" + enc, "application/x-www-form-urlencoded"), - _ => await Hx.Send(host, HttpMethod.Post, path(bindPre + "body/" + w), - which == "valid" ? JsonSerializer.Serialize(plainSample, e.Plain, jopts) : which == "refused" ? JsonSerializer.Serialize(e.Refused(), e.Plain, jopts) : JsonSerializer.Serialize(txt)), - }; - } - // Both sides are mapped for every binding they reach here: a 404 is the harness's routing, never a binding result. Counted as - // "plain unbindable", a plain route mapped under another name went unnoticed. - static string? Routed(string name, string bind, Hx.Wire bp, Hx.Wire bv) => - bp.Status == HttpStatusCode.NotFound || bv.Status == HttpStatusCode.NotFound - ? $"harness: {name} bind {bind} has no endpoint (plain {(int)bp.Status}, value object {(int)bv.Status})" : null; - // A text a route segment cannot carry as it is ('/', ':', '?', '#'): escaped, it arrives as other text ('%2F' stays), so the value - // bound is not the one sent. A limit of the harness, which no sentence cites: the check cannot tell. - bool Unsafe(string txt) => bind == "route" && (txt.Contains('/') || txt.Contains(':') || txt.Contains('?') || txt.Contains('#')); - string? Unplaceable(Check c, string txt) => c.Inconclusive($"{name} bind route: the text {txt} cannot be carried by a route segment as it is"); - string? f3; - if (Unsafe(valid)) f3 = r.Check(obligation, Baseline.Twin, c => Unplaceable(c, valid)); - else - { - var bp = await B(app, "valid", valid, "plain"); var bv = await B(app, "valid", valid, "vo"); - r.Log.AppendLine($"{name} bind {bind} valid text={valid}: plain {(int)bp.Status} {Cut(bp.Body)} {bp.Thrown} | vo {(int)bv.Status} {Cut(bv.Body)} {bv.Thrown}"); - f3 = Routed(name, bind, bp, bv) is { } unrouted - ? r.Check(obligation, Baseline.Twin, _ => unrouted) - : await Wire(r, e, obligation, Baseline.Twin, name + " bind " + bind, bp, bv, () => one.Ask(h => B(h, "valid", valid, "plain")), - c => Task.FromResult(Same(c, r, e, name + " bind " + bind, bp, bv))); - } - failed ??= f3; - if (refused is null) continue; - // README "Binding" (AspNetCore): a value Create rejects gets a 400, the one set of rules. - var against = Baseline.Docs("src/CodoMetis.TypeKit.AspNetCore/README.md#binding"); - if (Unsafe(refused)) { failed ??= r.Check(obligation, against, c => Unplaceable(c, refused)); continue; } - var rp = await B(app, "refused", refused, "plain"); var rv = await B(app, "refused", refused, "vo"); - r.Log.AppendLine($"{name} bind {bind} refused text={refused}: plain {(int)rp.Status} {Cut(rp.Body)} {rp.Thrown} | vo {(int)rv.Status} {Cut(rv.Body)} {rv.Thrown}"); - if (Routed(name, bind, rp, rv) is { } unrouted2) { failed ??= r.Check(obligation, against, _ => unrouted2); continue; } - // Side (1) of a refused binding: the twin on the host without TypeKit, refused a binding too, as MVC refuses a text the wrapped - // type cannot read. Where the value object's refusal could not be answered (a 500, which MVC's 400 under a source-generated - // context that does not list ValidationProblemDetails is, KB-041), the twin's refused binding is side (1): read from KB-041's - // fixture where it covers the row, else asked of the host without TypeKit. - Sides.Side? framework = null; - if (rv.Ex is { } refusal && Unreadable(e) is { } unreadable) - { - var settings = Sides.Settings(r, ("endpoint", name == "mvc" ? "mvc-api-controller" : "minimal"), ("binding", "fails")); - if (Sides.Covered(r, "KB-041", settings) is { Kind: FactAnswer.Exception } answer) - framework = new(new(name + " bind " + bind + " refused", null, Status: 500, FixtureRefusal: (string?)answer.Value["type"]), Baseline.Fact("KB-041"), $"KB-041 {answer.Id}: a binding MVC refuses, under a source-generated context"); - else if (await one.Ask(h => B(h, "unreadable", unreadable, "plain")) is { } twin) - framework = Sides.Measured(twin.Side(name + " bind " + bind + " refused"), $"the twin on a host without TypeKit, sent {unreadable}"); - } - var f4 = await Sides.Check(r, obligation, against, framework, rv.Side(name + " bind " + bind + " refused"), null, null, async c => - { - if ((int)rp.Status is not (>= 200 and < 300)) return c.Inconclusive($"{name} bind {bind}: the wrapped type refuses the value Create refuses too ({(int)rp.Status})"); - if (r.Is("json.ReferenceHandler", "Preserve") && e.Wrapped == "own-record-class") return c.Refused(Cite.Docs("src/CodoMetis.TypeKit.Generators/README.md#json"), "bind"); - // README "What TypeKit guarantees": StoredJsonConverterFactory reads JSON the application stored itself, without validation. - if (bind == "body" && r.Is("json.Converters", "StoredJsonConverterFactory")) return c.Inapplicable(Cite.Docs("README.md#what-typekit-guarantees"), "bind body"); - // KB-037: under MaxValidationDepth=1 MVC runs an action whose parameter it could not bind, with the parameter at its default. The - // value object must hold its default then, never the refused value; side (1), the twin sent a text it cannot read, shows the - // framework doing the same: read from KB-037's fixture where it covers the row, else asked of the host without TypeKit. - if (name == "mvc" && r.Is("mvc.MaxValidationDepth", "1") && (int)rv.Status is >= 200 and < 300) - { - if (DefaultBody(app, e.Vo) is var voDefault && !HoldsDefault(rv, voDefault)) - return $"{name} bind {bind}: validated value object bound the refused value (status {(int)rv.Status}) {Cut(rv.Body)}, not its default ({(voDefault is null ? "204" : "200 " + Cut(voDefault))})"; - if (Unreadable(e) is not { } text) - return c.Inapplicable(Cite.Fact("KB-037"), $"{name} bind {bind}: the value object holds its default; any text binds the wrapped type, so no twin shows it"); - Hx.Wire? bt; - if (Sides.Covered(r, "KB-037", Sides.Settings(r, ("text", text))) is { Kind: FactAnswer.Status } answer) - bt = new((HttpStatusCode)(int)answer.Value["status"]!, (string?)answer.Value["body"] ?? ""); - else bt = await one.Ask(h => B(h, "unreadable", text, "plain")); - if (bt is null) return c.Inconclusive($"{name} bind {bind}: the value object holds its default (KB-037); no host without TypeKit to show the wrapped type's ({one.Down})"); - r.Log.AppendLine($"{name} bind {bind} unreadable text={text}: plain {(int)bt.Status} {Cut(bt.Body)} {bt.Thrown}"); - if (HoldsDefault(bt, DefaultBody(app, e.Plain))) - return c.Inapplicable(Cite.Fact("KB-037"), $"{name} bind {bind}: both hold their default, the wrapped type for a text it cannot read"); - if ((int)bt.Status is >= 400 and < 500) - return $"{name} bind {bind}: the value object answered {(int)rv.Status} holding its default, where the wrapped type rejects a text it cannot read ({(int)bt.Status})"; - // KB-037's class from the query: the validator throws at the twin's property, and the value object, written by a - // converter of its own, is no class it walks - return c.Inconclusive($"{name} bind {bind}: the value object holds its default (KB-037); the wrapped type answered {(int)bt.Status} {Cut(bt.Body)} {bt.Thrown}"); - } - // a refused value must not bind (the one set of rules), except where the application skips validation on purpose - if ((int)rv.Status is >= 200 and < 300) return $"{name} bind {bind}: validated value object bound the refused value (status {(int)rv.Status}) {Cut(rv.Body)}"; - if ((int)rv.Status != 400) return $"{name} bind {bind}: refused value answered {(int)rv.Status}, expected 400: {Cut(rv.Body)} {rv.Thrown}"; - c.Note(NoteKeys.Compared, $"{name} bind {bind}: the refused value answered 400"); - return null; - }); - failed ??= f4; - } - // 4. schema parity - if (schema) { var f5 = await r.Check(obligation, Baseline.Twin, async c => await SchemaParity(app, one, alone, r, e, c)); failed ??= f5; } - return failed; - } - - /// - /// The schema parity alone, on the same host as : claim C21's check (C21OpenapiSchemaParityProbe), which BaseProbe leaves - /// out of its calibration, run once for each of the row's against the twin. C21 promises the schema ASP.NET - /// Core publishes for the underlying type, so side (1), the same host without TypeKit, is started on every row (Sides.Unconditional). - /// Every check runs; the first failure is returned. - /// - public static async Task Schema(Row r, Entry e, IEnumerable obligations) - { - WebApplication? app = null; string? down = null; - try { app = await Hx.Start(r, e, new HostCfg { FaultControllers = false, MapBlanketMapping = false }); } - catch (Exception ex) when (ex is not Sides.InputsNotEquivalent) { down = "schema host did not start: " + ex.GetType().Name + ": " + Cut(ex.Message); } - await using var _ = app; - await using var one = new SideOne(r, e, "plain"); - await using var alone = new SideOne(r, e, "both"); - var list = obligations.ToList(); - if (down is null && list.Any(o => Sides.Unconditional(r, o))) await one.Host(); - string? failed = null; - foreach (var obligation in list) - { - var f = await r.Check(obligation, Baseline.Twin, c => down is not null ? Task.FromResult(down) : SchemaParity(app!, one, alone, r, e, c)); - failed ??= f; - } - return failed; - } - - /// - /// Where either side is a null-or-reference (oneOf [null, component]), both sides there in one spelling: - /// the null as x-null beside the schema, wherever the side spelled it (type [null, X], nullable, - /// x-nullable, a null in an enum, or the null-or itself). A shared component cannot hold the null of one - /// position, so a nullable value object that is one is null around its reference, where ASP.NET writes the - /// wrapped type's null inside its inline schema (decision 36: "as a container's element (`oneOf: [null, - /// component]`, ASP.NET's own form)"; AspNetCore README: "A nullable element (`List<Quantity?>`, - /// `Dictionary<string, Quantity?>`) is the component or null, as a nullable property is."). Whether a side - /// admits null is kept; everywhere else, inlined schemas included, the comparison stays byte for byte (rows 170, - /// 196 and 215 of C21). Returns both sides without the marks put on resolved references. - /// - static (string, string, bool) NullAroundReferences(string sp, string sv) - { - var around = false; - static bool IsNull(JsonNode? a) => a is JsonObject ao && (ao["type"]?.ToJsonString() == "\"null\"" || ao["enum"]?.ToJsonString() == "[null]"); - static bool NullOrReference(JsonNode? n) => - n is JsonObject o && o["oneOf"] is JsonArray alts && alts.Count == 2 && alts.Any(IsNull) && alts.Any(a => a is JsonObject ao && ao.ContainsKey(Hx.ResolvedReference)); - static JsonNode? Unmark(JsonNode? n) => n switch - { - JsonObject o => new JsonObject(o.Where(kv => kv.Key != Hx.ResolvedReference).Select(kv => KeyValuePair.Create(kv.Key, Unmark(kv.Value)))), - JsonArray a => new JsonArray(a.Select(Unmark).ToArray()), - _ => n?.DeepClone(), - }; - static JsonNode? OneSpelling(JsonNode? n) - { - switch (n) - { - case JsonObject o when o["oneOf"] is JsonArray alts && alts.Count == 2 && alts.Any(IsNull) && alts.FirstOrDefault(a => !IsNull(a)) is JsonObject keep: - { - var c = (JsonObject)OneSpelling(keep)!; c["x-null"] = true; return Sorted(c); - } - case JsonObject o: - { - var c = new JsonObject(); - foreach (var (k, v) in o) - { - if (k == Hx.ResolvedReference) continue; - if (k is "nullable" or "x-nullable") { if (v?.GetValue() == true) c["x-null"] = true; continue; } - if (k == "allOf" && v is JsonArray aa && aa.Count == 1 && aa[0] is JsonObject a0 && a0.Count == 1 && a0.ContainsKey("x-nullable")) { c["x-null"] = true; continue; } - if (k == "type" && v is JsonArray ta && ta.Any(x => x?.GetValue() == "null")) - { - var f = ta.Where(x => x?.GetValue() != "null").ToList(); - c["type"] = f.Count == 1 ? f[0]!.DeepClone() : new JsonArray(f.Select(x => x!.DeepClone()).ToArray()); c["x-null"] = true; continue; - } - if (k == "type" && v?.ToJsonString() == "\"null\"") { c["x-null"] = true; continue; } - if (k == "enum" && v is JsonArray en && en.Any(x => x is null)) { c["enum"] = new JsonArray(en.Where(x => x is not null).Select(x => x!.DeepClone()).ToArray()); c["x-null"] = true; continue; } - if (k == "anyOf" && v is JsonArray any && any.Any(IsNull)) { c["anyOf"] = new JsonArray(any.Where(x => !IsNull(x)).Select(OneSpelling).ToArray()); c["x-null"] = true; continue; } - c[k] = OneSpelling(v); - } - return Sorted(c); - } - case JsonArray a: return new JsonArray(a.Select(OneSpelling).ToArray()); - default: return n?.DeepClone(); - } - } - static JsonObject Sorted(JsonObject o) => new(o.OrderBy(kv => kv.Key, StringComparer.Ordinal).Select(kv => KeyValuePair.Create(kv.Key, kv.Value?.DeepClone()))); - (JsonNode?, JsonNode?) Walk(JsonNode? p, JsonNode? v) - { - if (NullOrReference(p) || NullOrReference(v)) { around = true; return (OneSpelling(p), OneSpelling(v)); } - if (p is JsonObject po && v is JsonObject vo) - { - var cp = new JsonObject(); var cv = new JsonObject(); - foreach (var k in po.Select(kv => kv.Key).Union(vo.Select(kv => kv.Key)).Where(k => k != Hx.ResolvedReference).Order(StringComparer.Ordinal)) - { - if (!po.ContainsKey(k)) { cv[k] = Unmark(vo[k]); continue; } - if (!vo.ContainsKey(k)) { cp[k] = Unmark(po[k]); continue; } - var (a, b) = Walk(po[k], vo[k]); cp[k] = a; cv[k] = b; - } - return (cp, cv); - } - if (p is JsonArray pa && v is JsonArray va && pa.Count == va.Count) - { - var cp = new JsonArray(); var cv = new JsonArray(); - for (var i = 0; i < pa.Count; i++) { var (a, b) = Walk(pa[i], va[i]); cp.Add(a); cv.Add(b); } - return (cp, cv); - } - return (Unmark(p), Unmark(v)); - } - var (np, nv) = Walk(JsonNode.Parse(sp), JsonNode.Parse(sv)); - return (np?.ToJsonString() ?? "null", nv?.ToJsonString() ?? "null", around); - } - - /// - /// The component two operations of one document both refer to at the same place (a parameter's or the body's schema), where one - /// substitutes the value object for the twin: two types under one name, told by the references' structure. - /// - static string? SharedComponent(JsonNode twin, JsonNode valueObject) - { - static IEnumerable<(string, string)> References(JsonNode op) - { - if (op["parameters"] is JsonArray ps) - for (var i = 0; i < ps.Count; i++) - if (ps[i]?["schema"]?["$ref"]?.GetValue() is { } p) yield return ($"parameter {i}", p); - if (op["requestBody"]?["content"] is JsonObject content) - foreach (var (media, body) in content) - if (body?["schema"]?["$ref"]?.GetValue() is { } b) yield return ($"body {media}", b); - } - var twins = References(twin).ToDictionary(x => x.Item1, x => x.Item2); - return References(valueObject).FirstOrDefault(x => twins.TryGetValue(x.Item1, out var t) && t == x.Item2).Item2; - } - - /// A schema that is the OpenAPI 2.0 null flag and nothing else: {"allOf":[{"x-nullable":true}]}. - static bool OnlyTheNullFlag(JsonObject o) => - o.Count == 1 && o["allOf"] is JsonArray { Count: 1 } all && all[0] is JsonObject { Count: 1 } flag && flag["x-nullable"]?.GetValueKind() == JsonValueKind.True; - - /// - /// The OpenAPI 2.0 null flag with the wrapped type's format beside it and nothing else, {"format":"uri","allOf":[{"x-nullable":true}]}: - /// a nullable component's property, which TypeKit gives the wrapped type's format (KB-026). - /// - static bool TheNullFlagBeside(JsonObject o) => - o.ContainsKey("format") && o.Count == 2 && OnlyTheNullFlag(new JsonObject { ["allOf"] = o["allOf"]?.DeepClone() }); - - /// The keys of a difference up to the property it lies in (…, properties, P), or null where it lies in none. - static IReadOnlyList? PropertyAt(IReadOnlyList keys) - { - for (var i = keys.Count - 2; i >= 0; i--) - if (keys[i] == "properties") return [.. keys.Take(i + 2)]; - return null; - } - - /// - /// OpenAPI 2.0's nullable property of a component (KB-026): allOf: [{x-nullable: true}], without the reference and so without a - /// type, beside what TypeKit kept of the wrapped type (a format). - /// - static bool TheNullFlagWithoutItsReference(JsonObject o) => - o["allOf"] is JsonArray { Count: 1 } all && all[0] is JsonObject { Count: 1 } flag && flag["x-nullable"]?.GetValueKind() == JsonValueKind.True - && !o.ContainsKey("$ref") && !o.ContainsKey(Hx.ResolvedReference) && !o.ContainsKey("type"); - - /// - /// A body whose schema is its format and its null alone, {"schema": {"format": "int32", "allOf": [{"x-nullable": true}]}}, or its null - /// alone where the type has no format, anywhere in an operation: told by its structure (KB-001). - /// - static bool OnlyItsNull(JsonNode? n) => n switch - { - JsonObject o => o["schema"] is JsonObject schema && schema.All(kv => kv.Key is "allOf" or "format") - && schema["allOf"] is JsonArray { Count: 1 } all && all[0] is JsonObject { Count: 1 } flag - && flag["x-nullable"]?.GetValueKind() == JsonValueKind.True - || o.Any(kv => OnlyItsNull(kv.Value)), - JsonArray a => a.Any(OnlyItsNull), - _ => false, - }; - - /// - /// The value object's schema (3) against the wrapped twin's (2) at each endpoint pair, three-sided (Sides): the twin's schema is held to - /// the one ASP.NET publishes on the same host without TypeKit (1), where that host writes a document; and where the value object's schema - /// differs from the twin's, side (1)'s document at the same path tells whether ASP.NET alone writes that difference. - /// - static async Task SchemaParity(WebApplication app, SideOne one, SideOne alone, Row r, Entry e, Check c) - { - JsonNode doc; - try { doc = (await Hx.Doc(app))!; } - catch (Exception ex) - { - // The baseline is ASP.NET alone on the same host: a document it refuses too has no schema to compare (decision 99). - return await Hx.DocRefusedByAspNetAlone(r, e, new HostCfg { FaultControllers = false, MapBlanketMapping = false }, ex); - } - // docs/json.md "Describing what a converter writes": ASP.NET drops items/additionalProperties of a converter it cannot see into, the value - // object's schema is the underlying type's; and the application's converter for the value object describes it as that converter says. - // C21 declares both on every obligation (claims.json inapplicable), so Row.Check records them before this body runs and no row of - // C21 is generated with them; these lines hold for the schema half run under another claim (ShrinkProbe's baseline). - if (r.Is("json.Converters", "own-for-wrapped")) return c.Inapplicable(Cite.NotPromised("own-converter-for-the-wrapped-type")); - if (r.Is("json.Converters", "own-for-value-object")) return c.Inapplicable(Cite.NotPromised("own-converter-for-the-value-object")); - // side (1): the document ASP.NET writes for the twin's endpoints without TypeKit, where it writes one; and, asked only where the value - // object's schema differs from the twin's, the one it writes for both sides' endpoints, for the value object's position - async Task Document(SideOne host) - { - if (await host.Host() is not { } h) return null; - try { return await Hx.Doc(h); } - catch (Exception x) { r.Log.AppendLine("side (1) document refused: " + x.GetType().Name + ": " + Cut(x.Message)); return null; } - } - var doc1 = await Document(one); - JsonNode? Op(JsonNode d, string path, string method) => d["paths"]?[path]?[method]; - // only the schemas are compared (the claim is about the schema); whether a parameter or body is required follows the .NET type's nullability - JsonNode? Schemas(JsonNode? n) - { - switch (n) - { - case JsonArray a: { var c = new JsonArray(); foreach (var x in a) c.Add(Schemas(x)); return c; } - case JsonObject o: - { - var c = new JsonObject(); - foreach (var kv in o.OrderBy(k => k.Key, StringComparer.Ordinal)) - { - if (kv.Key == "required" && kv.Value is JsonValue) continue; // parameter / body / old-style flags - c[kv.Key] = Schemas(kv.Value); - } - return c; - } - default: return n?.DeepClone(); - } - } - string Sig(JsonNode d, JsonNode op) - { - var o = new JsonObject(); - o["parameters"] = Schemas(Hx.Deep(d, op["parameters"], mark: true)); - o["requestBody"] = Schemas(Hx.Deep(d, op["requestBody"], mark: true)); - o["ok"] = Schemas(Hx.Deep(d, op["responses"]?["200"], mark: true)); - return o.ToJsonString(); - } - // The media types of an operation's request body and 200 response, each named by where it is - static HashSet MediaTypes(JsonNode op) => - [ - .. (op["requestBody"]?["content"] as JsonObject ?? []).Select(kv => "requestBody:" + kv.Key), - .. (op["responses"]?["200"]?["content"] as JsonObject ?? []).Select(kv => "ok:" + kv.Key), - ]; - // A signature with only the request body's and the 200 response's media types given - static string UnderMediaTypes(string sig, IEnumerable media) - { - var keep = media.ToHashSet(); - var o = JsonNode.Parse(sig)!.AsObject(); - foreach (var (part, name) in new[] { ("requestBody", "requestBody"), ("ok", "ok") }) - if (o[part]?["content"] is JsonObject content) - foreach (var key in content.Select(kv => kv.Key).ToList()) - if (!keep.Contains(name + ":" + key)) content.Remove(key); - return o.ToJsonString(); - } - var checks = new List<(string, string)>(); - if (Hx.HasMinimal(r)) { checks.Add((Hx.M(r, "/schema/plain"), Hx.M(r, "/schema/vo"))); } - if (Hx.HasMvc(r)) - { - var bind = r["http.binding"] switch { "route" => "route", "query" => "query", "header" => "header", "form" => "form", "as-parameters" => "params", _ => "body" }; - var sfx = bind == "route" ? "/{v}" : ""; - checks.Add((Hx.C(r, $"/mvc/bind/{bind}/plain{sfx}"), Hx.C(r, $"/mvc/bind/{bind}/vo{sfx}"))); - } - if (Hx.HasMinimal(r)) - { - var bind = r["http.binding"] switch { "route" => "route", "query" => "query", "header" => "header", "form" => "form", "as-parameters" => "params", _ => "body" }; - // route: /bind/route/{which}/{v} - checks.Add((bind == "route" ? Hx.M(r, "/bind/route/plain/{v}") : Hx.M(r, $"/bind/{bind}/plain"), bind == "route" ? Hx.M(r, "/bind/route/vo/{v}") : Hx.M(r, $"/bind/{bind}/vo"))); - } - // The inputs of the two sides, apart from the type substituted: where each value is bound, under which - // name, and whether it is required. They must agree before the schemas are compared; otherwise the - // comparison measures the harness (G1: a nullable class value object against a required int). - string Inputs(JsonNode op) - { - var parts = new List(); - if (op["parameters"] is JsonArray ps) - foreach (var prm in ps) { var q = Hx.Deep(doc, prm); parts.Add($"{q?["in"]}:{q?["name"]}:required={q?["required"]?.ToJsonString() ?? "false"}"); } - if (Hx.Deep(doc, op["requestBody"]) is JsonObject rb) parts.Add($"body:required={rb["required"]?.ToJsonString() ?? "false"}"); - return string.Join(",", parts); - } - foreach (var (pp0, vp) in checks) - { - var method = pp0.Contains("/schema/") || pp0.Contains("/body/") || pp0.Contains("/form/") ? "post" : "get"; - // Like with like from the start: the twin is the wrapped type at the value object's nullability (shapes.cs), so a class - // value object over a struct faces the struct made nullable, at every endpoint (row 384 of C07: a record class over TimeOnly, - // optional, against TimeOnly?). - var pp = pp0; - var po = Op(doc, pp, method); var vo = Op(doc, vp, method); - if (po is null) - { - // Mapped for bodies only (a record class, Instant in a minimal API): neither side is there. - if (vo is not null) return $"harness: the value object's {method} {vp} is documented, its plain twin is not (missing {method} {pp})"; - c.Note(NoteKeys.SchemaNeitherSideDocumented, $"missing {method} {pp}"); continue; - } - if (vo is null) return $"missing {method} {vp}"; - // The twin's and the value object's shapes are two .NET types; one component name for both (an id the row's CreateSchemaReferenceId - // makes alike for both, such as an array of a generic type) holds one schema for the two, so the document cannot tell them apart. - if (SharedComponent(po, vo) is { } shared) - { c.Inconclusive($"{pp} vs {vp}: the twin's and the value object's shapes are one component, {shared}, in the document: nothing to compare"); continue; } - // (2) held to (1): the twin's schema is the one ASP.NET publishes on the same host without TypeKit, but where the twin itself - // holds one of TypeKit's types (a Patch), which no host without TypeKit describes - var po1 = doc1 is null ? null : Op(doc1, pp, method); - var twinHoldsTypeKit = pp.Contains("/schema/") && Sides.HoldsTypeKit(Twin(r, e).Declared); - if (po1 is not null && !twinHoldsTypeKit && Sig(doc1!, po1) is var s1 && Sig(doc, po) is var s2 && s1 != s2) - return $"schema of the wrapped type at {pp}: TypeKit changed the wrapped type's schema, at {FirstDifference(JsonNode.Parse(s1), JsonNode.Parse(s2), "$")} (without TypeKit, then with it)"; - if (Inputs(po) is var ip && Inputs(vo) is var iv && ip != iv) - { - // No twin of the wrapped type at the value object's nullability here (a struct value object over a class, which no plain - // type mirrors), or a wrapped record class, which ASP.NET binds by its properties where the value object is one value: the - // two sides are not alike, and no sentence cites it, so the check cannot tell. - if (Make.AdmitsNull(e.Vo) != Make.AdmitsNull(e.Plain)) - c.Inconclusive($"{pp} vs {vp}: no twin of {e.Plain.Name} at the value object's nullability, the inputs differ: wrapped [{ip}] vs value object [{iv}]"); - else if (e.Wrapped == "own-record-class") - c.Inconclusive($"{pp} vs {vp}: the wrapped record class is bound by its properties, the value object as one value, the inputs differ: wrapped [{ip}] vs value object [{iv}]"); - else r.Outcome("harness-error", $"inputs not equivalent for {pp} vs {vp}: wrapped [{ip}] vs value object [{iv}]"); - continue; - } - var sp = Sig(doc, po); var sv = Sig(doc, vo); - // MVC lists a body's and a response's media types by what its formatters can read and write of the .NET type, and the twin - // and the value object are two types (row 38 of C21: XmlSerializerInputFormatter reads a record class value object, not the - // Uri it wraps). Where the lists differ and side (1), ASP.NET alone with both sides' endpoints, lists each side's the same, - // the formatters decided it, not TypeKit: the schemas are compared under the media types both list. Otherwise as they are. - if (MediaTypes(po) is var mp && MediaTypes(vo) is var mv && !mp.SetEquals(mv) - && await Document(alone) is { } both0 && Op(both0, pp, method) is { } pAlone && Op(both0, vp, method) is { } vAlone - && MediaTypes(pAlone).SetEquals(mp) && MediaTypes(vAlone).SetEquals(mv)) - { - r.Log.AppendLine($"{pp} vs {vp}: media types the formatters list differ alike without TypeKit, compared under [{string.Join(", ", mp.Intersect(mv))}]"); - sp = UnderMediaTypes(sp, mp.Intersect(mv)); sv = UnderMediaTypes(sv, mp.Intersect(mv)); - } - if (NullAroundReferences(sp, sv) is var (np, nv, around)) { sp = np; sv = nv; if (around) c.Note(NoteKeys.SchemaNullAroundReferenceNormalised); } - if (sp != sv && Make.AdmitsNull(e.Vo) && Make.AdmitsNull(e.Plain) && e.Vo.IsValueType != e.Plain.IsValueType) - { - // Both sides nullable alike, one a class and the other a struct made nullable (a class value object and its nullable twin, or - // a nullable struct value object and a wrapped class); OpenAPI 3.x spells nullability two ways (oneOf [null, X] or type [null, X]). - sp = Hx.NoNull(JsonNode.Parse(sp))!.ToJsonString(); sv = Hx.NoNull(JsonNode.Parse(sv))!.ToJsonString(); - c.Note(NoteKeys.SchemaNullableSpellingsNormalised); - } - if (sp != sv && Make.AdmitsNull(e.Vo) != Make.AdmitsNull(e.Plain)) - { - // A struct value object over a class, whose twin is nullable by being a class where the value object is not (no plain - // type mirrors it); the markers go, the rest of the schema must agree. - sp = Hx.NoNull(JsonNode.Parse(sp))!.ToJsonString(); sv = Hx.NoNull(JsonNode.Parse(sv))!.ToJsonString(); - c.Note(NoteKeys.SchemaClassVoNullabilityNormalised); - } - if (sp != sv) - { - if (e.Kind == "closed-set") { c.Inapplicable(Cite.NotPromised("a-closed-set-is-a-string-enum"), "schema"); continue; } - // A form binds the wrapped record class by its properties and the value object otherwise, as the parameters above: the two - // sides' fields are not alike, and no sentence cites it, so the check cannot tell (row 62 of C21). - if (e.Wrapped == "own-record-class" && pp.Contains("/form/") && method == "post") - { c.Inconclusive($"{pp} vs {vp}: the wrapped record class is bound from a form by its properties, the value object otherwise: the fields differ"); continue; } - var d = Difference(JsonNode.Parse(sp), JsonNode.Parse(sv), "$"); - // Decision 36: a nullable value object admits null wherever it appears, where ASP.NET may write the twin's nullable type without - // it (the items of a list of double? under AllowNamedFloatingPointLiterals, row 216 of C21; the twin is held to side (1) above). - // Only that null differs: the value object admits it where the twin does not, and the schemas are otherwise the same. - if (Make.AdmitsNull(e.Vo) && d.Keys is [.., "x-null"] && d.P is null && d.V?.GetValueKind() == JsonValueKind.True - && JsonNode.DeepEquals(Hx.NoNull(JsonNode.Parse(sp)), Hx.NoNull(JsonNode.Parse(sv)))) - { c.Inapplicable(Cite.NotPromised("a-nullable-value-object-admits-null"), d.Path); continue; } - var openApi2 = r.Is("openapi.OpenApiVersion", "OpenApi2_0"); - // Side (1) at the same path: the twin's and the value object's operations in the document ASP.NET writes without TypeKit. - var twin1 = po1 is null ? null : JsonNode.Parse(Sig(doc1!, po1)); - var both1 = openApi2 && d.P is null && d.V is not null ? await Document(alone) : null; - var vo1 = both1 is null ? null : Op(both1, vp, method) is { } o1 ? JsonNode.Parse(Sig(both1, o1)) : null; - var vo3 = JsonNode.Parse(Sig(doc, vo)); - // KB-026: OpenAPI 2.0 writes a nullable property of any component as allOf [{x-nullable: true}], without the reference, and a - // value object keeps a component of its own (not-promised openapi-2-nullable-component-loses-its-type; row 38 of C21). Side (1) - // shows it: without TypeKit, ASP.NET writes the value object's position at the same path the same, where the twin has nothing. - if (openApi2 && d.P is null && d.V is not null && At(vo1, d.Keys) is { } same1 && JsonNode.DeepEquals(same1, At(vo3, d.Keys))) - { c.Inapplicable(Cite.NotPromised("openapi-2-nullable-component-loses-its-type"), d.Path); continue; } - // The same, where the spellings of the null were normalised above, which takes the null flag out of the value object's - // schema and leaves it without what the 2.0 writer dropped with the reference: its type, its pattern (rows 386, 178, 181 - // and 243 of C21: a nullable struct value object over Uri as a required-init property, and class value objects whose - // property admits null as a type parameter's or set through the constructor). Told by its structure: the value object's - // property is the null flag, alone or beside the wrapped type's format, and the twin's, which admits null, has that format. - if (openApi2 && PropertyAt(d.Keys) is { } property - && At(vo3, property) is JsonObject flagged3 && (TheNullFlagBeside(flagged3) || OnlyTheNullFlag(flagged3)) - && At(JsonNode.Parse(Sig(doc, po)), property) is JsonObject typed2 - && (typed2["x-nullable"]?.GetValueKind() == JsonValueKind.True - // or the twin is a component itself (a wrapped record class, row 444 of C21), whose reference ASP.NET's own rule - // leaves without the null the value object admits (decision 36: a-nullable-value-object-admits-null) - || typed2.ContainsKey(Hx.ResolvedReference) && Make.AdmitsNull(e.Vo)) - && JsonNode.DeepEquals(typed2["format"], flagged3["format"])) - { c.Inapplicable(Cite.NotPromised("openapi-2-nullable-component-loses-its-type"), d.Path); continue; } - // AspNetCore README "What the document says": a nullable element is the component or null, as a nullable property is, which - // OpenAPI 2.0 writes as allOf [{x-nullable: true}] without the reference (not-promised openapi-2-nullable-element-loses-its-type; - // row 358 of C21, a dictionary's value). ASP.NET alone drops a converter-backed element, so side (1) has nothing there; told by - // its structure, the value object's element is the null flag alone where the twin's admits null. - if (openApi2 && d.Keys is [.., "items" or "additionalProperties", "allOf"] && d.P is null - && At(vo3, d.Keys.SkipLast(1)) is JsonObject element3 && OnlyTheNullFlag(element3) - && At(JsonNode.Parse(Sig(doc, po)), d.Keys.SkipLast(1)) is JsonObject element2 && element2["x-nullable"]?.GetValueKind() == JsonValueKind.True) - { c.Inapplicable(Cite.NotPromised("openapi-2-nullable-element-loses-its-type"), d.Path); continue; } - // The same loss told by the row's axes, where side (1) cannot show it: a nullable value object that is a component is a - // nullable property wherever the serializer's contract says so (a get-only or a type parameter's property among them), where - // ASP.NET alone adds the null only for a settable annotated one. Its property is then the null flag without the reference, - // where the twin's keeps its type beside x-nullable. - if (openApi2 && Make.AdmitsNull(e.Vo) && !r.Is("openapi.CreateSchemaReferenceId", "null-inlines-all") && d.V is null - && At(vo3, d.Keys.SkipLast(1)) is JsonObject lost && TheNullFlagWithoutItsReference(lost) - && At(JsonNode.Parse(Sig(doc, po)), d.Keys.SkipLast(1)) is JsonObject kept && kept["x-nullable"]?.GetValueKind() == JsonValueKind.True) - { c.Inapplicable(Cite.NotPromised("openapi-2-nullable-component-loses-its-type"), d.Path); continue; } - // KB-001: OpenAPI 2.0 writes a nullable body of the wrapped value type as its format and allOf [{x-nullable: true}] alone (the - // null alone where it has no format, an enum), where a class value object's body in a minimal API is not nullable and keeps its - // schema (not-promised openapi-2-nullable-body-keeps-only-its-null; row 257, and rows 113 and 282 over TimeOnly and char). Side (1) - // shows it: without TypeKit, the twin's schema there is that format and that null alone. - // AspNetCore README "What the document says" (KB-026), seen from the twin: OpenAPI 2.0 writes a nullable property of any component, - // the twin's nullable enum among them, as allOf [{x-nullable: true}] without the reference, where the value object keeps its type. - // Side (1) shows it: without TypeKit, the twin's property there is that null alone. - if (openApi2 && d.P is null && d.V is not null && Make.AdmitsNull(e.Plain) && At(twin1, d.Keys.SkipLast(1)) is JsonObject t1 && OnlyTheNullFlag(t1)) - { c.Inapplicable(Cite.Docs("src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says"), d.Path); continue; } - if (openApi2 && !e.Vo.IsValueType && Nullable.GetUnderlyingType(e.Plain) is not null && OnlyItsNull(twin1) && OnlyItsNull(JsonNode.Parse(Sig(doc, po)))) - { c.Inapplicable(Cite.NotPromised("openapi-2-nullable-body-keeps-only-its-null"), pp); continue; } - // KB-027: the component ASP.NET makes for an enum written by name is the schema of the first use it meets, so a wrapped E? - // parameter lists null through E's component where E was first met nullable; the value object's parameter is the enum's - // content without it wherever the enum is a component (not-promised enum-component-is-its-first-use; row 285 of C21). Side (1) - // shows it: without TypeKit, the twin's parameter lists the same null, through the same resolved component. - // The same component met first as a nullable use lists that null for a property too, the twin's non-nullable one included - // (not-promised enum-component-lists-the-null-of-its-first-use; row 196 of C21, a polymorphic derived type's property). - if (d.Keys is [.., "enum"] && d.P is JsonArray we && d.V is JsonArray ve && we.Count == ve.Count + 1 && we[^1] is null - && JsonNode.DeepEquals(new JsonArray(we.Take(ve.Count).Select(x => x?.DeepClone()).ToArray()), ve) - && At(JsonNode.Parse(Sig(doc, po)), d.Keys.SkipLast(1)) is JsonObject held && held.ContainsKey(Hx.ResolvedReference) - && At(twin1, d.Keys) is { } listed1 && JsonNode.DeepEquals(listed1, d.P)) - { - if (d.Keys is ["parameters", ..]) c.Inapplicable(Cite.NotPromised("enum-component-is-its-first-use"), vp); - else c.Inapplicable(Cite.NotPromised("enum-component-lists-the-null-of-its-first-use"), vp); - continue; - } - // AspNetCore README "A described type": ASP.NET cannot see through a converter it did not write, one a library registers for - // its own type among them, and drops a list's items and a dictionary's additionalProperties of that type, where the value - // object's element is put back (not-promised a-librarys-converter-for-the-wrapped-type; row 422 of C21, a list of Instant?). - // Side (1) shows it: without TypeKit, the twin's container has no element schema either. - if (e.Wrapped == "NodaTime.Instant" && d.Keys is [.., "items" or "additionalProperties"] && d.P is null && d.V is not null - && twin1 is not null && At(twin1, d.Keys.SkipLast(1)) is JsonObject container1 && !container1.ContainsKey(d.Keys[^1])) - { c.Inapplicable(Cite.NotPromised("a-librarys-converter-for-the-wrapped-type"), d.Path); continue; } - return $"schema differs for {pp} vs {vp} at {FirstDifference(JsonNode.Parse(sp), JsonNode.Parse(sv), "$")}:\n wrapped {Cut(sp)}\n value object {Cut(sv)}"; - } - } - return null; - } -} diff --git a/tools/probe/corpus/harness/HttpHost.cs b/tools/probe/corpus/harness/HttpHost.cs deleted file mode 100644 index d37238fc..00000000 --- a/tools/probe/corpus/harness/HttpHost.cs +++ /dev/null @@ -1,438 +0,0 @@ -// The shared HTTP host: applies a row's json.*, openapi.*, mvc.* and http.* axes and the shape axes, then runs the baseline checks -// (payload echo/in/binding parity, schema parity) that every http claim shares. Claim-specific scenarios plug in through HostCfg. -using System.Net; -using System.Net.Http.Headers; -using System.Reflection; -using System.Text; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; -using CodoMetis.TypeKit; -using CodoMetis.TypeKit.AspNetCore; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Routing; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ApplicationModels; -using Microsoft.AspNetCore.Mvc.ApplicationParts; -using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.AspNetCore.Mvc.Formatters; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; -using Microsoft.AspNetCore.Mvc.Routing; -using Microsoft.AspNetCore.Mvc.ActionConstraints; -using Microsoft.AspNetCore.OpenApi; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.OpenApi; -using HttpJsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions; -using MvcJsonOptions = Microsoft.AspNetCore.Mvc.JsonOptions; -namespace Probes.Shapes; - -public sealed record Payload(Held Vo, Held Plain); - -public sealed class NullBinderProvider : IModelBinderProvider { public IModelBinder? GetBinder(ModelBinderProviderContext context) => null; } -public sealed class PrefixConvention(string prefix) : IApplicationModelConvention -{ - public void Apply(ApplicationModel application) - { - foreach (var c in application.Controllers) - foreach (var s in c.Selectors.Concat(c.Actions.SelectMany(a => a.Selectors))) - if (s.AttributeRouteModel is { } m && !(m.Template?.StartsWith('/') ?? false)) - s.AttributeRouteModel = AttributeRouteModel.CombineAttributeRouteModel(new AttributeRouteModel(new RouteAttribute(prefix)), m); - } -} - -/// -/// http.host=controller-without-ApiController-visible: what ASP.NET documents as the switch beside [ApiController] and -/// [ApiExplorerSettings], a convention that sets ApiExplorer.IsVisible, so the document checks reach those controllers. -/// -public sealed class VisibleToApiExplorer : IApplicationModelConvention -{ - public void Apply(ApplicationModel application) - { - foreach (var c in application.Controllers) c.ApiExplorer.IsVisible = true; - } -} - -/// Closed generic controllers: one over the value object, one over the wrapped type; routes are set by BindConvention. -[ApiController] -public sealed class MvcBind : ControllerBase -{ - public sealed class Q { public T V { get; set; } = default!; } - [ProducesResponseType(200), Route("_")] - public IActionResult RouteA([FromRoute(Name = "v")] T v) => new ObjectResult(v) { DeclaredType = typeof(T) }; - [ProducesResponseType(200), Route("_")] - public IActionResult QueryA([FromQuery(Name = "v")] T v) => new ObjectResult(v) { DeclaredType = typeof(T) }; - [ProducesResponseType(200), Route("_")] - public IActionResult HeaderA([FromHeader(Name = "v")] T v) => new ObjectResult(v) { DeclaredType = typeof(T) }; - [ProducesResponseType(200), Route("_")] - public IActionResult BodyA([FromBody] T v) => new ObjectResult(v) { DeclaredType = typeof(T) }; - [ProducesResponseType(200), Route("_")] - public IActionResult FormA([FromForm(Name = "v")] T v) => new ObjectResult(v) { DeclaredType = typeof(T) }; - [ProducesResponseType(200), Route("_")] - public IActionResult ParamsA([FromQuery] Q q) => new ObjectResult(q.V) { DeclaredType = typeof(T) }; -} -public sealed class BindConvention(Type vo, Type plain, string prefix) : IControllerModelConvention -{ - public void Apply(ControllerModel c) - { - if (!c.ControllerType.IsGenericType || c.ControllerType.GetGenericTypeDefinition() != typeof(MvcBind<>)) return; - var arg = c.ControllerType.GenericTypeArguments[0]; - // By the entry's own types: the plain side is the twin, at the value object's nullability. - var which = arg == vo ? "vo" : arg == plain ? "plain" : throw new InvalidOperationException($"MvcBind<{arg.Name}> is neither side's"); - foreach (var a in c.Actions) - { - var (kind, post) = a.ActionName switch { "RouteA" => ("route", false), "QueryA" => ("query", false), "HeaderA" => ("header", false), "BodyA" => ("body", true), "FormA" => ("form", true), _ => ("params", false) }; - a.Selectors.Clear(); - a.Selectors.Add(new SelectorModel - { - AttributeRouteModel = new AttributeRouteModel(new RouteAttribute($"{prefix}mvc/bind/{kind}/{which}" + (kind == "route" ? "/{v}" : ""))), - ActionConstraints = { new Microsoft.AspNetCore.Mvc.ActionConstraints.HttpMethodActionConstraint([post ? "POST" : "GET"]) }, - EndpointMetadata = { new Microsoft.AspNetCore.Routing.HttpMethodMetadata([post ? "POST" : "GET"]) }, - }); - } - } -} -[ApiController] -public sealed class MvcPayloadController(Payload payload) : ControllerBase -{ - [HttpGet("mvc/echo/{which}")] - public IActionResult Echo(string which) { var h = which == "vo" ? payload.Vo : payload.Plain; return new ObjectResult(h.Value) { DeclaredType = h.Declared }; } - - [HttpPost("mvc/in/{which}")] - public async Task In(string which) - { - var h = which == "vo" ? payload.Vo : payload.Plain; - var f = HttpContext.RequestServices.GetRequiredService>().Value.InputFormatters.OfType().First(); - var meta = HttpContext.RequestServices.GetRequiredService().GetMetadataForType(h.Declared); - var factory = HttpContext.RequestServices.GetRequiredService(); - var ctx = new InputFormatterContext(HttpContext, "model", ModelState, meta, factory.CreateReader); - var result = await f.ReadAsync(ctx); - if (result.HasError) return StatusCode(400); - return new ObjectResult(result.Model) { DeclaredType = h.Declared }; - } -} - -public sealed class HostCfg -{ - public Action? Minimal; - public Func? ControllerBuilder; - public Action? Services; - public string Mapping = "by-kind"; - public bool Qualified; - public ListedLogs? Logs; - public Type[]? Controllers; // null: the fault controllers of the host - public bool FaultControllers = true; - public bool Payload = true; // payload echo / in / binding / schema endpoints - public string PayloadOnly = "both"; // "both", "vo" or "plain": the endpoints of one side only, minimal and MVC (side (1)'s twin alone, a document refusal isolated) - public bool MapBlanketMapping = true; - public bool TypeKitOpenApi = true; // false: the document as ASP.NET alone writes it, the baseline of a refused one - /// - /// The three-sided differential's side (1) (Sides): no AddTypeKit(), none of TypeKit's JSON converters on either set of options, and no - /// fault responses; the wrapped twin's endpoints answer as ASP.NET and System.Text.Json have them without TypeKit. - /// - public bool WithoutTypeKit; - public HostCfg Copy() => (HostCfg)MemberwiseClone(); -} - -public static class Hx -{ - public static bool HasMinimal(Row r) => r["http.host"] is "minimal" or "both" or "route-group"; - public static bool HasMvc(Row r) => r["http.host"] is "mvc" or "both" or "controller-without-ApiController" or "controller-without-ApiController-visible"; - /// The fault controllers have no [ApiController]: MVC infers no [FromBody] and answers no automatic 400. - public static bool Plain(Row r) => r["http.host"] is "controller-without-ApiController" or "controller-without-ApiController-visible"; - /// A controller without [ApiController] is in the document only when something makes it visible to the API explorer. - public static bool InDocument(Row r) => r["http.host"] != "controller-without-ApiController"; - /// Path of a minimal endpoint (route-group hosts map everything under /rg). - public static string M(Row r, string path) => (r["http.host"] == "route-group" ? "/rg" : "") + path; - /// Path of a controller endpoint (own-route-prefix convention puts /px before relative routes). - public static string C(Row r, string path) => (r.Is("mvc.Conventions", "own-route-prefix") ? "/px" : "") + path; - - public static Type[] FaultControllers(Row r) => - Plain(r) ? [typeof(PlainAController), typeof(PlainBController)] : [typeof(MvcAController), typeof(MvcBController)]; - - static void ApplyMvc(MvcOptions o, Row r) - { - foreach (var kv in r.All.Where(kv => kv.Key.StartsWith("mvc.") && kv.Value != "default")) - { - var n = kv.Key[4..]; - switch (n) - { - case "InputFormatters": o.InputFormatters.Add(new XmlSerializerInputFormatter(o)); continue; - case "OutputFormatters": o.OutputFormatters.Add(new XmlSerializerOutputFormatter()); continue; - case "ModelBinderProviders": o.ModelBinderProviders.Insert(0, new NullBinderProvider()); continue; - case "Conventions": o.Conventions.Add(new PrefixConvention("px")); continue; - } - var p = typeof(MvcOptions).GetProperty(n) ?? throw new ArgumentException("no mvc option " + n); - var t = Nullable.GetUnderlyingType(p.PropertyType) ?? p.PropertyType; - object v = t == typeof(bool) ? bool.Parse(kv.Value) : t == typeof(int) ? int.Parse(kv.Value) : t.IsEnum ? Enum.Parse(t, kv.Value) : throw new ArgumentException($"no hand value for mvc.{n}"); - p.SetValue(o, v); - } - } - - static void ApplyOpenApi(OpenApiOptions o, Row r) - { - var v = r.Get("openapi.OpenApiVersion"); - if (v is not (null or "default")) o.OpenApiVersion = Enum.Parse(v); - switch (r.Get("openapi.CreateSchemaReferenceId")) - { - case "own-prefixing": o.CreateSchemaReferenceId = ti => OpenApiOptions.CreateDefaultSchemaReferenceId(ti) is { } id ? "X" + id : null; break; - case "null-inlines-all": o.CreateSchemaReferenceId = _ => null; break; - } - switch (r.Get("openapi.transformers")) - { - case "own-schema-transformer": o.AddSchemaTransformer((s, c, ct) => Task.CompletedTask); break; - case "own-operation-transformer": o.AddOperationTransformer((op, c, ct) => Task.CompletedTask); break; - case "own-document-transformer": o.AddDocumentTransformer((d, c, ct) => Task.CompletedTask); break; - } - } - - public static async Task Start(Row r, Entry e, HostCfg c) - { - var builder = WebApplication.CreateBuilder(new WebApplicationOptions { EnvironmentName = Environments.Production }); - builder.WebHost.UseTestServer(); - builder.Logging.ClearProviders(); - if (c.Logs is not null) builder.Logging.AddProvider(c.Logs); - var plain = e.Valid(); var vo = Make.Vo(e, plain); - var (twin, held) = Sides.Twins(r["shape.position"], r.Get("shape.containing") ?? "class", e, plain, vo); - var payload = new Payload(held, twin); - builder.Services.AddSingleton(payload); - builder.Services.ConfigureHttpJsonOptions(o => { JsonRow.Apply(r, e, o.SerializerOptions); if (c.WithoutTypeKit) Sides.DropTypeKit(o.SerializerOptions); }); - builder.Services.AddOpenApi(x => { ApplyOpenApi(x, r); if (c.TypeKitOpenApi && !c.WithoutTypeKit) x.AddTypeKit(); }); - if (!c.WithoutTypeKit) builder.Services.Configure(x => x.QualifiedFaultNames = c.Qualified); - c.Services?.Invoke(builder.Services); - if (c.MapBlanketMapping && !c.WithoutTypeKit) - { - builder.Services.AddFaultResponses(f => f switch { SFault.First => 409, SFault.Second => 410, _ => throw new ArgumentOutOfRangeException(nameof(f)) }); - if (c.Mapping == "by-kind") builder.Services.AddFaultResponses(); - else if (c.Mapping == "own-IFault-mapping") builder.Services.AddFaultResponses(f => FaultResponse.For(f.Kind).WithType(new Uri($"https://p.example/{f.Kind}"))); - else if (c.Mapping == "per-set") { builder.Services.AddFaultResponses(); builder.Services.AddFaultResponses(); } - } - var mvc = HasMvc(r); - if (mvc) - { - var controllers = new List(); - if (c.FaultControllers) controllers.AddRange(c.Controllers ?? FaultControllers(r)); - if (c.Payload) - { - controllers.Add(typeof(MvcPayloadController)); - if (c.PayloadOnly != "plain") controllers.Add(typeof(MvcBind<>).MakeGenericType(e.Vo)); - if (c.PayloadOnly != "vo") controllers.Add(typeof(MvcBind<>).MakeGenericType(e.Plain)); - } - var pfx = ""; - builder.Services.AddControllers(o => - { - o.Conventions.Add(new BindConvention(e.Vo, e.Plain, pfx)); // before the prefix convention, so that it prefixes these routes too - if (r["http.host"] == "controller-without-ApiController-visible") o.Conventions.Add(new VisibleToApiExplorer()); - ApplyMvc(o, r); - }) - .AddJsonOptions(o => { JsonRow.Apply(r, e, o.JsonSerializerOptions); if (c.WithoutTypeKit) Sides.DropTypeKit(o.JsonSerializerOptions); }) - .ConfigureApplicationPartManager(p => { p.ApplicationParts.Clear(); p.FeatureProviders.Add(new OnlyControllers([.. controllers])); }); - } - var app = builder.Build(); - app.MapOpenApi(); - if (HasMinimal(r)) - { - IEndpointRouteBuilder root = r["http.host"] == "route-group" ? app.MapGroup("/rg") : app; - c.Minimal?.Invoke(root); - if (c.Payload) MapPayload(root, payload, r, e, c.PayloadOnly); - } - if (mvc) { var cb = app.MapControllers(); c.ControllerBuilder?.Invoke(cb); } - try { await app.StartAsync(); } catch { await app.DisposeAsync(); throw; } - return app; - } - - // ---- minimal payload endpoints - static void MapPayload(IEndpointRouteBuilder b, Payload p, Row r, Entry e, string only) - { - foreach (var which in new[] { "vo", "plain" }) - { - if (only != "both" && only != which) continue; - var h = which == "vo" ? p.Vo : p.Plain; - b.MapGet($"/echo/{which}", async (HttpContext ctx) => { await ctx.Response.WriteAsJsonAsync(h.Value, h.Declared, options: null, contentType: null, ctx.RequestAborted); }); - b.MapPost($"/in/{which}", async (HttpContext ctx) => - { - object? v; - try { v = await ctx.Request.ReadFromJsonAsync(h.Declared, ctx.RequestAborted); } - catch (JsonException) { ctx.Response.StatusCode = 400; return; } - catch (BadHttpRequestException) { ctx.Response.StatusCode = 400; return; } - await ctx.Response.WriteAsJsonAsync(v, h.Declared, options: null, contentType: null, ctx.RequestAborted); - }); - typeof(Hx).GetMethod(nameof(MapBind), BindingFlags.NonPublic | BindingFlags.Static)!.MakeGenericMethod(which == "vo" ? e.Vo : e.Plain) - .Invoke(null, [b, which, e.Wrapped is "own-record-class" or "NodaTime.Instant"]); - typeof(Hx).GetMethod(nameof(MapSchema), BindingFlags.NonPublic | BindingFlags.Static)!.MakeGenericMethod(h.Declared).Invoke(null, [b, which]); - } - } - - sealed record AsP([FromQuery(Name = "v")] T V); - static void MapBind(IEndpointRouteBuilder b, string which, bool bodyOnly) - { - static IResult Out(HttpContext ctx, T v) => Results.Json(v, ctx.RequestServices.GetRequiredService>().Value.SerializerOptions, null, null); - b.MapPost($"/bind/body/{which}", (HttpContext ctx, [FromBody] T v) => Out(ctx, v)); - if (bodyOnly) return; // a record class cannot be bound from a route, query, header or form (the wrapped type neither) - b.MapGet($"/bind/route/{which}/{{v}}", (HttpContext ctx, T v) => Out(ctx, v)); - b.MapGet($"/bind/query/{which}", (HttpContext ctx, [FromQuery(Name = "v")] T v) => Out(ctx, v)); - b.MapGet($"/bind/header/{which}", (HttpContext ctx, [FromHeader(Name = "v")] T v) => Out(ctx, v)); - b.MapPost($"/bind/form/{which}", (HttpContext ctx, [FromForm(Name = "v")] T v) => Out(ctx, v)).DisableAntiforgery(); - b.MapGet($"/bind/params/{which}", (HttpContext ctx, [AsParameters] AsP p) => Out(ctx, p.V)); - } - static void MapSchema(IEndpointRouteBuilder b, string which) => - b.MapPost($"/schema/{which}", ([FromBody] T body) => Results.Ok(body)); - - // ---- client side - /// An answer on the wire: its status and body, or the exception the server threw (, a 500), named in . - public sealed record Wire(HttpStatusCode Status, string Body, string? Thrown = null, Exception? Ex = null) - { - /// The answer as the three-sided differential compares it (Sides): the status, the body, and the exception's type and stage. - public Sides.Answer Side(string operation) => new(operation, Ex is null ? Body : null, Ex, (int)Status); - } - public static async Task Send(WebApplication app, HttpMethod m, string path, string? body = null, string? contentType = null, (string, string)? header = null) - { - var req = new HttpRequestMessage(m, path); - req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - if (body is not null) req.Content = new StringContent(body, Encoding.UTF8, contentType ?? "application/json"); - if (header is { } h) req.Headers.TryAddWithoutValidation(h.Item1, h.Item2); - try - { - var resp = await app.GetTestClient().SendAsync(req); - return new(resp.StatusCode, await resp.Content.ReadAsStringAsync()); - } - catch (Exception ex) { return new(HttpStatusCode.InternalServerError, "", ex.GetType().Name + ": " + ex.Message, ex); } - } - public static Task Get(WebApplication app, string path) => Send(app, HttpMethod.Get, path); - public static async Task Doc(WebApplication app) => JsonNode.Parse(await app.GetTestClient().GetStringAsync("/openapi/v1.json")); - - /// - /// The document failed with AddTypeKit(). The same host without it is side (1) of the three (Sides): where ASP.NET alone refuses the - /// document alike, by exception type and stage and never by message (), the document part has nothing to check - /// (); where it writes the document, or refuses it otherwise, the failure is returned (F2 hid behind a baseline - /// that kept AddTypeKit() on both sides). says whether the check compared an answer before the document. - /// - public static async Task DocRefusedByAspNetAlone(Row r, Entry e, HostCfg c, Exception withTypeKit, bool comparedBefore = false) - { - static string Say(Exception x) => x.GetType().Name + ": " + (x.Message.Length > 200 ? x.Message[..200] + "..." : x.Message); - var alone = c.Copy(); alone.TypeKitOpenApi = false; - try - { - await using var app = await Start(r, e, alone); - await Doc(app); - } - catch (Exception ex) when (DocAlike(ex, withTypeKit)) - { - DocRefused(r, ex, comparedBefore); - return null; - } - catch (Exception ex) { return $"document: AddTypeKit() fails ({Say(withTypeKit)}), ASP.NET alone fails otherwise ({Say(ex)})"; } - return $"document: AddTypeKit() fails ({Say(withTypeKit)}), ASP.NET alone writes it"; - } - - /// - /// ASP.NET alone (side (1)) and AddTypeKit() (side (3)) refuse the document alike: the same exception type at the same stage (a - /// JsonException's Path), as compares two refusals. No message is compared: the two documents differ in length - /// by what AddTypeKit() wrote, so one refusal is at another line and byte of each (rows 31 and 140 of C21), and a refusal's words decide - /// no row (decision 99, WS2's three-sided differential). - /// - public static bool DocAlike(Exception one, Exception three) => - Sides.Alike(new Sides.Answer("document", null, one), new Sides.Answer("document", null, three)); - - /// - /// The document refused alike by ASP.NET alone: its part has nothing to check. A check that compared nothing before it is the framework's - /// refusal, an expected refusal that earns nothing (framework-refuses); one that compared an answer on the wire first is judged by that - /// answer (doc:refused-by-aspnet-alone, a pass: decision 99's "judged by its other checks"). - /// - public static void DocRefused(Row r, Exception one, bool comparedBefore) - { - if (comparedBefore) r.Note(NoteKeys.DocRefusedByAspnetAlone, one.GetType().Name); - else r.Note(NoteKeys.FrameworkRefuses, $"document refused by ASP.NET alone too ({one.GetType().Name}), side (1) measured on the same host without AddTypeKit()"); - } - - public static string BindText(Entry e, object plain) => e.Wrapped switch - { - "DateTime" => ((DateTime)plain).ToString("O"), "DateTimeOffset" => ((DateTimeOffset)plain).ToString("O"), - "DateOnly" => ((DateOnly)plain).ToString("yyyy-MM-dd"), "TimeOnly" => ((TimeOnly)plain).ToString("HH:mm:ss"), - "TimeSpan" => ((TimeSpan)plain).ToString("c"), "bool" => (bool)plain ? "true" : "false", "enum" or "flags-enum" => plain.ToString()!, - _ => Convert.ToString(plain, System.Globalization.CultureInfo.InvariantCulture)! - }; - - // ---- schema comparison - /// Drops nullability markers (a class value object over a value type is nullable by being a class; not the value object's doing). - public static JsonNode? NoNull(JsonNode? n) - { - switch (n) - { - case JsonObject o: - static bool IsNull(JsonNode? a) => a is JsonObject ao && (ao["type"]?.ToJsonString() == "\"null\"" || ao["enum"]?.ToJsonString() == "[null]"); - if (o["oneOf"] is JsonArray alts && alts.Count == 2 && alts.Any(IsNull) && alts.FirstOrDefault(a => !IsNull(a)) is { } keep) - return NoNull(keep.DeepClone()); - var c = new JsonObject(); - foreach (var kv in o) - { - if (kv.Key is "nullable" or "x-nullable" or "x-null" or "required") continue; - if (kv.Key == "type" && kv.Value is JsonArray ta) { var f = ta.Where(x => x?.GetValue() != "null").ToList(); c["type"] = f.Count == 1 ? f[0]!.DeepClone() : new JsonArray(f.Select(x => x!.DeepClone()).ToArray()); continue; } - if (kv.Key == "allOf" && kv.Value is JsonArray aa && aa.Count == 1 && aa[0] is JsonObject a0 && a0.Count == 1 && a0.ContainsKey("x-nullable")) continue; - // OpenAPI 3.0 writes a type of several with null as anyOf [{type: integer}, {type: string}, {enum: [null]}] (row 260 of C21) - if (kv.Key == "anyOf" && kv.Value is JsonArray any && any.Any(IsNull)) { c["anyOf"] = new JsonArray(any.Where(x => !IsNull(x)).Select(NoNull).ToArray()); continue; } - if (kv.Key == "enum" && kv.Value is JsonArray en && en.Any(x => x is null)) { c["enum"] = new JsonArray(en.Where(x => x is not null).Select(x => x!.DeepClone()).ToArray()); continue; } - c[kv.Key] = NoNull(kv.Value); - } - return c; - case JsonArray a: var ca = new JsonArray(); foreach (var x in a) ca.Add(NoNull(x)); return ca; - default: return n?.DeepClone(); - } - } - - /// The key puts on a schema it resolved from a reference, when asked to mark them. - public const string ResolvedReference = "x-probe-resolved-reference"; - - public static JsonNode? Deep(JsonNode doc, JsonNode? n, HashSet? seen = null, bool mark = false) - { - seen ??= []; - switch (n) - { - case JsonObject o: - if (o["$ref"] is { } rf) - { - var key = rf.GetValue(); - if (!seen.Add(key)) return JsonValue.Create(""); - var target = doc; foreach (var seg in key.TrimStart('#', '/').Split('/')) target = target?[seg]; - var res = Deep(doc, target, seen, mark); seen.Remove(key); - if (mark && res is JsonObject resolved) resolved[ResolvedReference] = true; - return res; - } - var c = new JsonObject(); - foreach (var kv in o.OrderBy(k => k.Key, StringComparer.Ordinal)) - if (kv.Key is not ("title" or "description" or "example" or "examples")) - c[kv.Key] = kv.Key == "discriminator" ? Discriminator(o, kv.Value) : Deep(doc, kv.Value, seen, mark); - return c; - case JsonArray a: var ca = new JsonArray(); foreach (var x in a) ca.Add(Deep(doc, x, seen, mark)); return ca; - default: return n?.DeepClone(); - } - } - - /// - /// A discriminator's mapping names a component per value, and a component's name is made of the type's, which - /// differs between the two sides by construction (row 347 of C07: PB<TimeSpan> against - /// PB<V>). Each value names instead the branch of the same schema's anyOf or oneOf it maps to, whose - /// content compares; a target that is no branch stays as it is, and so still differs. - /// - static JsonNode? Discriminator(JsonObject schema, JsonNode? discriminator) - { - if (discriminator is not JsonObject d || d["mapping"] is not JsonObject mapping) return discriminator?.DeepClone(); - var branches = (schema["anyOf"] ?? schema["oneOf"]) as JsonArray; - var c = (JsonObject)d.DeepClone(); - var named = new JsonObject(); - foreach (var (value, target) in mapping.OrderBy(m => m.Key, StringComparer.Ordinal)) - { - var index = -1; - for (var i = 0; target is JsonValue && i < (branches?.Count ?? 0); i++) - if (branches![i]?["$ref"]?.GetValue() == target.GetValue()) { index = i; break; } - named[value] = index >= 0 ? $"branch {index}" : target?.DeepClone(); - } - c["mapping"] = named; - return c; - } -} diff --git a/tools/probe/corpus/harness/JsonFamily.cs b/tools/probe/corpus/harness/JsonFamily.cs deleted file mode 100644 index 8a1ef837..00000000 --- a/tools/probe/corpus/harness/JsonFamily.cs +++ /dev/null @@ -1,89 +0,0 @@ -// Shared code of the JSON family's claim checks (C01 decimal spelling, C10 Preserve guard, L06 off the wire): -// serializing a held shape, reading its tokens, rewriting one token, and reading what a document holds back. -using System.Text; -using System.Text.Json; -using Probes.Shapes; -namespace Probes.Json; - -/// One token of a JSON document: its kind, its raw text (a string's without quotes), its value as text, and its byte span. -public sealed record JTok(JsonTokenType Type, string Raw, string? Text, int Start, int End); - -public static class Jx -{ - public static (string? Json, Exception? Ex) Ser(Held h, JsonSerializerOptions o) - { try { return (JsonSerializer.Serialize(h.Value, h.Declared, o), null); } catch (Exception ex) { return (null, ex); } } - - public static (object? Value, Exception? Ex) De(string text, Type declared, JsonSerializerOptions o) - { try { return (JsonSerializer.Deserialize(text, declared, o), null); } catch (Exception ex) { return (null, ex); } } - - /// Every token of a document, with the byte span it takes (a string's and a property name's quotes included). - public static List Tokens(string json) - { - var bytes = Encoding.UTF8.GetBytes(json); - var rd = new Utf8JsonReader(bytes, new JsonReaderOptions { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip }); - var list = new List(); - while (rd.Read()) - { - var start = (int)rd.TokenStartIndex; - switch (rd.TokenType) - { - case JsonTokenType.Number: - list.Add(new(rd.TokenType, Encoding.UTF8.GetString(rd.ValueSpan), Encoding.UTF8.GetString(rd.ValueSpan), start, start + rd.ValueSpan.Length)); break; - case JsonTokenType.String or JsonTokenType.PropertyName: - list.Add(new(rd.TokenType, Encoding.UTF8.GetString(rd.ValueSpan), rd.GetString(), start, start + rd.ValueSpan.Length + 2)); break; - default: - list.Add(new(rd.TokenType, "", null, start, start)); break; - } - } - return list; - } - - /// The document with the tokens replaced by (raw JSON text). - public static string Rewrite(string json, IEnumerable at, Func with) - { - var bytes = Encoding.UTF8.GetBytes(json); - var sb = new StringBuilder(); var pos = 0; - foreach (var t in at.OrderBy(t => t.Start)) - { - sb.Append(Encoding.UTF8.GetString(bytes, pos, t.Start - pos)); sb.Append(with(t)); pos = t.End; - } - sb.Append(Encoding.UTF8.GetString(bytes, pos, bytes.Length - pos)); - return sb.ToString(); - } - - /// Options that write what a value holds as the serializer writes it by default (a decimal with its scale, every field). - public static readonly JsonSerializerOptions Plain = new() { IncludeFields = true }; - - public static string Cut(string? s) => s is null ? "(null)" : s.Length > 220 ? s[..220] + "..." : s; - public static string Name(Type t) => t.IsGenericType ? t.Name[..t.Name.IndexOf('`')] + "<" + string.Join(", ", t.GetGenericArguments().Select(Name)) + ">" : t.Name; - /// A refusal's message with the type the other side holds named instead: the serializer names the shape it refused. - public static string Swap(string message, Type from, Type to) => message.Replace(from.ToString(), to.ToString()); - /// What a containing type holds: its property P or its field F. - public static object? Member(Type declared, object? holder) => - holder is null ? null : declared.GetProperty("P") is { } p ? p.GetValue(holder) : declared.GetField("F") is { } f ? f.GetValue(holder) : null; - public static string Ex(Exception e) => e.GetType().Name + ": " + Cut(e.Message); - - /// A copy of the options that writes every member: the ignore conditions reset, so a read has the member to read. - public static JsonSerializerOptions Full(JsonSerializerOptions o) => new(o) - { - DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.Never, IgnoreReadOnlyProperties = false, IgnoreReadOnlyFields = false, - // A number written as a string reads back only with AllowReadingFromString; the document is input, so it says its other numbers plainly. - NumberHandling = o.NumberHandling & ~System.Text.Json.Serialization.JsonNumberHandling.WriteAsString, - }; -} - -/// -/// The row's json.* axes, applied by JsonRow: under json.TypeInfoResolver=source-generated-context the resolver is ProbeContext alone -/// (Shapes.g.cs), which lists every shape a row holds and its twin, so the setting runs as the row names it. A shape of the claim's own -/// the context does not list (L06's Option or Result) is the generator's gap: a harness error. -/// -public static class JCtx -{ - public static JsonSerializerOptions Apply(Row r, Entry e, params Type[] shapes) - { - var o = JsonRow.Apply(r, e).Options!; - var missing = JsonRow.Unlisted(r, shapes).Select(Jx.Name).ToList(); - if (missing.Count > 0) throw new InvalidOperationException("ProbeContext does not list (run tools/probe/generators/shapes.cs): " + string.Join(", ", missing)); - return o; - } -} diff --git a/tools/probe/corpus/harness/JsonFamilyShapes.g.cs b/tools/probe/corpus/harness/JsonFamilyShapes.g.cs deleted file mode 100644 index 5cb98433..00000000 --- a/tools/probe/corpus/harness/JsonFamilyShapes.g.cs +++ /dev/null @@ -1,493 +0,0 @@ -// by tools/probe/generators/json-family.cs stand-ins: L06's polymorphic stand-ins over Option and Result. Do not edit. -#nullable disable -#pragma warning disable CS8618, CS1591, CMTK0005, CMTK0006 -using CodoMetis.TypeKit; -using Probes.Shapes; -namespace Probes.Json; - -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_string_O), "d")] public abstract class L06PB_V_record_struct_string_O { } -public sealed class L06PD_V_record_struct_string_O : L06PB_V_record_struct_string_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_string_RTE), "d")] public abstract class L06PB_V_record_struct_string_RTE { } -public sealed class L06PD_V_record_struct_string_RTE : L06PB_V_record_struct_string_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_string_RE), "d")] public abstract class L06PB_V_record_struct_string_RE { } -public sealed class L06PD_V_record_struct_string_RE : L06PB_V_record_struct_string_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_Guid_O), "d")] public abstract class L06PB_V_record_struct_Guid_O { } -public sealed class L06PD_V_record_struct_Guid_O : L06PB_V_record_struct_Guid_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_Guid_RTE), "d")] public abstract class L06PB_V_record_struct_Guid_RTE { } -public sealed class L06PD_V_record_struct_Guid_RTE : L06PB_V_record_struct_Guid_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_Guid_RE), "d")] public abstract class L06PB_V_record_struct_Guid_RE { } -public sealed class L06PD_V_record_struct_Guid_RE : L06PB_V_record_struct_Guid_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_int_O), "d")] public abstract class L06PB_V_record_struct_int_O { } -public sealed class L06PD_V_record_struct_int_O : L06PB_V_record_struct_int_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_int_RTE), "d")] public abstract class L06PB_V_record_struct_int_RTE { } -public sealed class L06PD_V_record_struct_int_RTE : L06PB_V_record_struct_int_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_int_RE), "d")] public abstract class L06PB_V_record_struct_int_RE { } -public sealed class L06PD_V_record_struct_int_RE : L06PB_V_record_struct_int_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_long_O), "d")] public abstract class L06PB_V_record_struct_long_O { } -public sealed class L06PD_V_record_struct_long_O : L06PB_V_record_struct_long_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_long_RTE), "d")] public abstract class L06PB_V_record_struct_long_RTE { } -public sealed class L06PD_V_record_struct_long_RTE : L06PB_V_record_struct_long_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_long_RE), "d")] public abstract class L06PB_V_record_struct_long_RE { } -public sealed class L06PD_V_record_struct_long_RE : L06PB_V_record_struct_long_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_byte_O), "d")] public abstract class L06PB_V_record_struct_byte_O { } -public sealed class L06PD_V_record_struct_byte_O : L06PB_V_record_struct_byte_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_byte_RTE), "d")] public abstract class L06PB_V_record_struct_byte_RTE { } -public sealed class L06PD_V_record_struct_byte_RTE : L06PB_V_record_struct_byte_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_byte_RE), "d")] public abstract class L06PB_V_record_struct_byte_RE { } -public sealed class L06PD_V_record_struct_byte_RE : L06PB_V_record_struct_byte_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_ulong_O), "d")] public abstract class L06PB_V_record_struct_ulong_O { } -public sealed class L06PD_V_record_struct_ulong_O : L06PB_V_record_struct_ulong_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_ulong_RTE), "d")] public abstract class L06PB_V_record_struct_ulong_RTE { } -public sealed class L06PD_V_record_struct_ulong_RTE : L06PB_V_record_struct_ulong_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_ulong_RE), "d")] public abstract class L06PB_V_record_struct_ulong_RE { } -public sealed class L06PD_V_record_struct_ulong_RE : L06PB_V_record_struct_ulong_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_decimal_O), "d")] public abstract class L06PB_V_record_struct_decimal_O { } -public sealed class L06PD_V_record_struct_decimal_O : L06PB_V_record_struct_decimal_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_decimal_RTE), "d")] public abstract class L06PB_V_record_struct_decimal_RTE { } -public sealed class L06PD_V_record_struct_decimal_RTE : L06PB_V_record_struct_decimal_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_decimal_RE), "d")] public abstract class L06PB_V_record_struct_decimal_RE { } -public sealed class L06PD_V_record_struct_decimal_RE : L06PB_V_record_struct_decimal_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_double_O), "d")] public abstract class L06PB_V_record_struct_double_O { } -public sealed class L06PD_V_record_struct_double_O : L06PB_V_record_struct_double_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_double_RTE), "d")] public abstract class L06PB_V_record_struct_double_RTE { } -public sealed class L06PD_V_record_struct_double_RTE : L06PB_V_record_struct_double_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_double_RE), "d")] public abstract class L06PB_V_record_struct_double_RE { } -public sealed class L06PD_V_record_struct_double_RE : L06PB_V_record_struct_double_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_bool_O), "d")] public abstract class L06PB_V_record_struct_bool_O { } -public sealed class L06PD_V_record_struct_bool_O : L06PB_V_record_struct_bool_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_bool_RTE), "d")] public abstract class L06PB_V_record_struct_bool_RTE { } -public sealed class L06PD_V_record_struct_bool_RTE : L06PB_V_record_struct_bool_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_bool_RE), "d")] public abstract class L06PB_V_record_struct_bool_RE { } -public sealed class L06PD_V_record_struct_bool_RE : L06PB_V_record_struct_bool_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_char_O), "d")] public abstract class L06PB_V_record_struct_char_O { } -public sealed class L06PD_V_record_struct_char_O : L06PB_V_record_struct_char_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_char_RTE), "d")] public abstract class L06PB_V_record_struct_char_RTE { } -public sealed class L06PD_V_record_struct_char_RTE : L06PB_V_record_struct_char_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_char_RE), "d")] public abstract class L06PB_V_record_struct_char_RE { } -public sealed class L06PD_V_record_struct_char_RE : L06PB_V_record_struct_char_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_DateTime_O), "d")] public abstract class L06PB_V_record_struct_DateTime_O { } -public sealed class L06PD_V_record_struct_DateTime_O : L06PB_V_record_struct_DateTime_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_DateTime_RTE), "d")] public abstract class L06PB_V_record_struct_DateTime_RTE { } -public sealed class L06PD_V_record_struct_DateTime_RTE : L06PB_V_record_struct_DateTime_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_DateTime_RE), "d")] public abstract class L06PB_V_record_struct_DateTime_RE { } -public sealed class L06PD_V_record_struct_DateTime_RE : L06PB_V_record_struct_DateTime_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_DateTimeOffset_O), "d")] public abstract class L06PB_V_record_struct_DateTimeOffset_O { } -public sealed class L06PD_V_record_struct_DateTimeOffset_O : L06PB_V_record_struct_DateTimeOffset_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_DateTimeOffset_RTE), "d")] public abstract class L06PB_V_record_struct_DateTimeOffset_RTE { } -public sealed class L06PD_V_record_struct_DateTimeOffset_RTE : L06PB_V_record_struct_DateTimeOffset_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_DateTimeOffset_RE), "d")] public abstract class L06PB_V_record_struct_DateTimeOffset_RE { } -public sealed class L06PD_V_record_struct_DateTimeOffset_RE : L06PB_V_record_struct_DateTimeOffset_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_DateOnly_O), "d")] public abstract class L06PB_V_record_struct_DateOnly_O { } -public sealed class L06PD_V_record_struct_DateOnly_O : L06PB_V_record_struct_DateOnly_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_DateOnly_RTE), "d")] public abstract class L06PB_V_record_struct_DateOnly_RTE { } -public sealed class L06PD_V_record_struct_DateOnly_RTE : L06PB_V_record_struct_DateOnly_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_DateOnly_RE), "d")] public abstract class L06PB_V_record_struct_DateOnly_RE { } -public sealed class L06PD_V_record_struct_DateOnly_RE : L06PB_V_record_struct_DateOnly_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_TimeOnly_O), "d")] public abstract class L06PB_V_record_struct_TimeOnly_O { } -public sealed class L06PD_V_record_struct_TimeOnly_O : L06PB_V_record_struct_TimeOnly_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_TimeOnly_RTE), "d")] public abstract class L06PB_V_record_struct_TimeOnly_RTE { } -public sealed class L06PD_V_record_struct_TimeOnly_RTE : L06PB_V_record_struct_TimeOnly_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_TimeOnly_RE), "d")] public abstract class L06PB_V_record_struct_TimeOnly_RE { } -public sealed class L06PD_V_record_struct_TimeOnly_RE : L06PB_V_record_struct_TimeOnly_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_TimeSpan_O), "d")] public abstract class L06PB_V_record_struct_TimeSpan_O { } -public sealed class L06PD_V_record_struct_TimeSpan_O : L06PB_V_record_struct_TimeSpan_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_TimeSpan_RTE), "d")] public abstract class L06PB_V_record_struct_TimeSpan_RTE { } -public sealed class L06PD_V_record_struct_TimeSpan_RTE : L06PB_V_record_struct_TimeSpan_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_TimeSpan_RE), "d")] public abstract class L06PB_V_record_struct_TimeSpan_RE { } -public sealed class L06PD_V_record_struct_TimeSpan_RE : L06PB_V_record_struct_TimeSpan_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_enum_O), "d")] public abstract class L06PB_V_record_struct_enum_O { } -public sealed class L06PD_V_record_struct_enum_O : L06PB_V_record_struct_enum_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_enum_RTE), "d")] public abstract class L06PB_V_record_struct_enum_RTE { } -public sealed class L06PD_V_record_struct_enum_RTE : L06PB_V_record_struct_enum_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_enum_RE), "d")] public abstract class L06PB_V_record_struct_enum_RE { } -public sealed class L06PD_V_record_struct_enum_RE : L06PB_V_record_struct_enum_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_flags_enum_O), "d")] public abstract class L06PB_V_record_struct_flags_enum_O { } -public sealed class L06PD_V_record_struct_flags_enum_O : L06PB_V_record_struct_flags_enum_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_flags_enum_RTE), "d")] public abstract class L06PB_V_record_struct_flags_enum_RTE { } -public sealed class L06PD_V_record_struct_flags_enum_RTE : L06PB_V_record_struct_flags_enum_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_flags_enum_RE), "d")] public abstract class L06PB_V_record_struct_flags_enum_RE { } -public sealed class L06PD_V_record_struct_flags_enum_RE : L06PB_V_record_struct_flags_enum_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_Uri_O), "d")] public abstract class L06PB_V_record_struct_Uri_O { } -public sealed class L06PD_V_record_struct_Uri_O : L06PB_V_record_struct_Uri_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_Uri_RTE), "d")] public abstract class L06PB_V_record_struct_Uri_RTE { } -public sealed class L06PD_V_record_struct_Uri_RTE : L06PB_V_record_struct_Uri_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_Uri_RE), "d")] public abstract class L06PB_V_record_struct_Uri_RE { } -public sealed class L06PD_V_record_struct_Uri_RE : L06PB_V_record_struct_Uri_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_NodaTime_Instant_O), "d")] public abstract class L06PB_V_record_struct_NodaTime_Instant_O { } -public sealed class L06PD_V_record_struct_NodaTime_Instant_O : L06PB_V_record_struct_NodaTime_Instant_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_NodaTime_Instant_RTE), "d")] public abstract class L06PB_V_record_struct_NodaTime_Instant_RTE { } -public sealed class L06PD_V_record_struct_NodaTime_Instant_RTE : L06PB_V_record_struct_NodaTime_Instant_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_NodaTime_Instant_RE), "d")] public abstract class L06PB_V_record_struct_NodaTime_Instant_RE { } -public sealed class L06PD_V_record_struct_NodaTime_Instant_RE : L06PB_V_record_struct_NodaTime_Instant_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_own_record_class_O), "d")] public abstract class L06PB_V_record_struct_own_record_class_O { } -public sealed class L06PD_V_record_struct_own_record_class_O : L06PB_V_record_struct_own_record_class_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_own_record_class_RTE), "d")] public abstract class L06PB_V_record_struct_own_record_class_RTE { } -public sealed class L06PD_V_record_struct_own_record_class_RTE : L06PB_V_record_struct_own_record_class_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_record_struct_own_record_class_RE), "d")] public abstract class L06PB_V_record_struct_own_record_class_RE { } -public sealed class L06PD_V_record_struct_own_record_class_RE : L06PB_V_record_struct_own_record_class_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_string_O), "d")] public abstract class L06PB_V_sealed_record_class_string_O { } -public sealed class L06PD_V_sealed_record_class_string_O : L06PB_V_sealed_record_class_string_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_string_RTE), "d")] public abstract class L06PB_V_sealed_record_class_string_RTE { } -public sealed class L06PD_V_sealed_record_class_string_RTE : L06PB_V_sealed_record_class_string_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_string_RE), "d")] public abstract class L06PB_V_sealed_record_class_string_RE { } -public sealed class L06PD_V_sealed_record_class_string_RE : L06PB_V_sealed_record_class_string_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_Guid_O), "d")] public abstract class L06PB_V_sealed_record_class_Guid_O { } -public sealed class L06PD_V_sealed_record_class_Guid_O : L06PB_V_sealed_record_class_Guid_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_Guid_RTE), "d")] public abstract class L06PB_V_sealed_record_class_Guid_RTE { } -public sealed class L06PD_V_sealed_record_class_Guid_RTE : L06PB_V_sealed_record_class_Guid_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_Guid_RE), "d")] public abstract class L06PB_V_sealed_record_class_Guid_RE { } -public sealed class L06PD_V_sealed_record_class_Guid_RE : L06PB_V_sealed_record_class_Guid_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_int_O), "d")] public abstract class L06PB_V_sealed_record_class_int_O { } -public sealed class L06PD_V_sealed_record_class_int_O : L06PB_V_sealed_record_class_int_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_int_RTE), "d")] public abstract class L06PB_V_sealed_record_class_int_RTE { } -public sealed class L06PD_V_sealed_record_class_int_RTE : L06PB_V_sealed_record_class_int_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_int_RE), "d")] public abstract class L06PB_V_sealed_record_class_int_RE { } -public sealed class L06PD_V_sealed_record_class_int_RE : L06PB_V_sealed_record_class_int_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_long_O), "d")] public abstract class L06PB_V_sealed_record_class_long_O { } -public sealed class L06PD_V_sealed_record_class_long_O : L06PB_V_sealed_record_class_long_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_long_RTE), "d")] public abstract class L06PB_V_sealed_record_class_long_RTE { } -public sealed class L06PD_V_sealed_record_class_long_RTE : L06PB_V_sealed_record_class_long_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_long_RE), "d")] public abstract class L06PB_V_sealed_record_class_long_RE { } -public sealed class L06PD_V_sealed_record_class_long_RE : L06PB_V_sealed_record_class_long_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_byte_O), "d")] public abstract class L06PB_V_sealed_record_class_byte_O { } -public sealed class L06PD_V_sealed_record_class_byte_O : L06PB_V_sealed_record_class_byte_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_byte_RTE), "d")] public abstract class L06PB_V_sealed_record_class_byte_RTE { } -public sealed class L06PD_V_sealed_record_class_byte_RTE : L06PB_V_sealed_record_class_byte_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_byte_RE), "d")] public abstract class L06PB_V_sealed_record_class_byte_RE { } -public sealed class L06PD_V_sealed_record_class_byte_RE : L06PB_V_sealed_record_class_byte_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_ulong_O), "d")] public abstract class L06PB_V_sealed_record_class_ulong_O { } -public sealed class L06PD_V_sealed_record_class_ulong_O : L06PB_V_sealed_record_class_ulong_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_ulong_RTE), "d")] public abstract class L06PB_V_sealed_record_class_ulong_RTE { } -public sealed class L06PD_V_sealed_record_class_ulong_RTE : L06PB_V_sealed_record_class_ulong_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_ulong_RE), "d")] public abstract class L06PB_V_sealed_record_class_ulong_RE { } -public sealed class L06PD_V_sealed_record_class_ulong_RE : L06PB_V_sealed_record_class_ulong_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_decimal_O), "d")] public abstract class L06PB_V_sealed_record_class_decimal_O { } -public sealed class L06PD_V_sealed_record_class_decimal_O : L06PB_V_sealed_record_class_decimal_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_decimal_RTE), "d")] public abstract class L06PB_V_sealed_record_class_decimal_RTE { } -public sealed class L06PD_V_sealed_record_class_decimal_RTE : L06PB_V_sealed_record_class_decimal_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_decimal_RE), "d")] public abstract class L06PB_V_sealed_record_class_decimal_RE { } -public sealed class L06PD_V_sealed_record_class_decimal_RE : L06PB_V_sealed_record_class_decimal_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_double_O), "d")] public abstract class L06PB_V_sealed_record_class_double_O { } -public sealed class L06PD_V_sealed_record_class_double_O : L06PB_V_sealed_record_class_double_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_double_RTE), "d")] public abstract class L06PB_V_sealed_record_class_double_RTE { } -public sealed class L06PD_V_sealed_record_class_double_RTE : L06PB_V_sealed_record_class_double_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_double_RE), "d")] public abstract class L06PB_V_sealed_record_class_double_RE { } -public sealed class L06PD_V_sealed_record_class_double_RE : L06PB_V_sealed_record_class_double_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_bool_O), "d")] public abstract class L06PB_V_sealed_record_class_bool_O { } -public sealed class L06PD_V_sealed_record_class_bool_O : L06PB_V_sealed_record_class_bool_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_bool_RTE), "d")] public abstract class L06PB_V_sealed_record_class_bool_RTE { } -public sealed class L06PD_V_sealed_record_class_bool_RTE : L06PB_V_sealed_record_class_bool_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_bool_RE), "d")] public abstract class L06PB_V_sealed_record_class_bool_RE { } -public sealed class L06PD_V_sealed_record_class_bool_RE : L06PB_V_sealed_record_class_bool_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_char_O), "d")] public abstract class L06PB_V_sealed_record_class_char_O { } -public sealed class L06PD_V_sealed_record_class_char_O : L06PB_V_sealed_record_class_char_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_char_RTE), "d")] public abstract class L06PB_V_sealed_record_class_char_RTE { } -public sealed class L06PD_V_sealed_record_class_char_RTE : L06PB_V_sealed_record_class_char_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_char_RE), "d")] public abstract class L06PB_V_sealed_record_class_char_RE { } -public sealed class L06PD_V_sealed_record_class_char_RE : L06PB_V_sealed_record_class_char_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_DateTime_O), "d")] public abstract class L06PB_V_sealed_record_class_DateTime_O { } -public sealed class L06PD_V_sealed_record_class_DateTime_O : L06PB_V_sealed_record_class_DateTime_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_DateTime_RTE), "d")] public abstract class L06PB_V_sealed_record_class_DateTime_RTE { } -public sealed class L06PD_V_sealed_record_class_DateTime_RTE : L06PB_V_sealed_record_class_DateTime_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_DateTime_RE), "d")] public abstract class L06PB_V_sealed_record_class_DateTime_RE { } -public sealed class L06PD_V_sealed_record_class_DateTime_RE : L06PB_V_sealed_record_class_DateTime_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_DateTimeOffset_O), "d")] public abstract class L06PB_V_sealed_record_class_DateTimeOffset_O { } -public sealed class L06PD_V_sealed_record_class_DateTimeOffset_O : L06PB_V_sealed_record_class_DateTimeOffset_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_DateTimeOffset_RTE), "d")] public abstract class L06PB_V_sealed_record_class_DateTimeOffset_RTE { } -public sealed class L06PD_V_sealed_record_class_DateTimeOffset_RTE : L06PB_V_sealed_record_class_DateTimeOffset_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_DateTimeOffset_RE), "d")] public abstract class L06PB_V_sealed_record_class_DateTimeOffset_RE { } -public sealed class L06PD_V_sealed_record_class_DateTimeOffset_RE : L06PB_V_sealed_record_class_DateTimeOffset_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_DateOnly_O), "d")] public abstract class L06PB_V_sealed_record_class_DateOnly_O { } -public sealed class L06PD_V_sealed_record_class_DateOnly_O : L06PB_V_sealed_record_class_DateOnly_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_DateOnly_RTE), "d")] public abstract class L06PB_V_sealed_record_class_DateOnly_RTE { } -public sealed class L06PD_V_sealed_record_class_DateOnly_RTE : L06PB_V_sealed_record_class_DateOnly_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_DateOnly_RE), "d")] public abstract class L06PB_V_sealed_record_class_DateOnly_RE { } -public sealed class L06PD_V_sealed_record_class_DateOnly_RE : L06PB_V_sealed_record_class_DateOnly_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_TimeOnly_O), "d")] public abstract class L06PB_V_sealed_record_class_TimeOnly_O { } -public sealed class L06PD_V_sealed_record_class_TimeOnly_O : L06PB_V_sealed_record_class_TimeOnly_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_TimeOnly_RTE), "d")] public abstract class L06PB_V_sealed_record_class_TimeOnly_RTE { } -public sealed class L06PD_V_sealed_record_class_TimeOnly_RTE : L06PB_V_sealed_record_class_TimeOnly_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_TimeOnly_RE), "d")] public abstract class L06PB_V_sealed_record_class_TimeOnly_RE { } -public sealed class L06PD_V_sealed_record_class_TimeOnly_RE : L06PB_V_sealed_record_class_TimeOnly_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_TimeSpan_O), "d")] public abstract class L06PB_V_sealed_record_class_TimeSpan_O { } -public sealed class L06PD_V_sealed_record_class_TimeSpan_O : L06PB_V_sealed_record_class_TimeSpan_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_TimeSpan_RTE), "d")] public abstract class L06PB_V_sealed_record_class_TimeSpan_RTE { } -public sealed class L06PD_V_sealed_record_class_TimeSpan_RTE : L06PB_V_sealed_record_class_TimeSpan_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_TimeSpan_RE), "d")] public abstract class L06PB_V_sealed_record_class_TimeSpan_RE { } -public sealed class L06PD_V_sealed_record_class_TimeSpan_RE : L06PB_V_sealed_record_class_TimeSpan_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_enum_O), "d")] public abstract class L06PB_V_sealed_record_class_enum_O { } -public sealed class L06PD_V_sealed_record_class_enum_O : L06PB_V_sealed_record_class_enum_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_enum_RTE), "d")] public abstract class L06PB_V_sealed_record_class_enum_RTE { } -public sealed class L06PD_V_sealed_record_class_enum_RTE : L06PB_V_sealed_record_class_enum_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_enum_RE), "d")] public abstract class L06PB_V_sealed_record_class_enum_RE { } -public sealed class L06PD_V_sealed_record_class_enum_RE : L06PB_V_sealed_record_class_enum_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_flags_enum_O), "d")] public abstract class L06PB_V_sealed_record_class_flags_enum_O { } -public sealed class L06PD_V_sealed_record_class_flags_enum_O : L06PB_V_sealed_record_class_flags_enum_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_flags_enum_RTE), "d")] public abstract class L06PB_V_sealed_record_class_flags_enum_RTE { } -public sealed class L06PD_V_sealed_record_class_flags_enum_RTE : L06PB_V_sealed_record_class_flags_enum_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_flags_enum_RE), "d")] public abstract class L06PB_V_sealed_record_class_flags_enum_RE { } -public sealed class L06PD_V_sealed_record_class_flags_enum_RE : L06PB_V_sealed_record_class_flags_enum_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_Uri_O), "d")] public abstract class L06PB_V_sealed_record_class_Uri_O { } -public sealed class L06PD_V_sealed_record_class_Uri_O : L06PB_V_sealed_record_class_Uri_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_Uri_RTE), "d")] public abstract class L06PB_V_sealed_record_class_Uri_RTE { } -public sealed class L06PD_V_sealed_record_class_Uri_RTE : L06PB_V_sealed_record_class_Uri_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_Uri_RE), "d")] public abstract class L06PB_V_sealed_record_class_Uri_RE { } -public sealed class L06PD_V_sealed_record_class_Uri_RE : L06PB_V_sealed_record_class_Uri_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_NodaTime_Instant_O), "d")] public abstract class L06PB_V_sealed_record_class_NodaTime_Instant_O { } -public sealed class L06PD_V_sealed_record_class_NodaTime_Instant_O : L06PB_V_sealed_record_class_NodaTime_Instant_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_NodaTime_Instant_RTE), "d")] public abstract class L06PB_V_sealed_record_class_NodaTime_Instant_RTE { } -public sealed class L06PD_V_sealed_record_class_NodaTime_Instant_RTE : L06PB_V_sealed_record_class_NodaTime_Instant_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_NodaTime_Instant_RE), "d")] public abstract class L06PB_V_sealed_record_class_NodaTime_Instant_RE { } -public sealed class L06PD_V_sealed_record_class_NodaTime_Instant_RE : L06PB_V_sealed_record_class_NodaTime_Instant_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_own_record_class_O), "d")] public abstract class L06PB_V_sealed_record_class_own_record_class_O { } -public sealed class L06PD_V_sealed_record_class_own_record_class_O : L06PB_V_sealed_record_class_own_record_class_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_own_record_class_RTE), "d")] public abstract class L06PB_V_sealed_record_class_own_record_class_RTE { } -public sealed class L06PD_V_sealed_record_class_own_record_class_RTE : L06PB_V_sealed_record_class_own_record_class_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_sealed_record_class_own_record_class_RE), "d")] public abstract class L06PB_V_sealed_record_class_own_record_class_RE { } -public sealed class L06PD_V_sealed_record_class_own_record_class_RE : L06PB_V_sealed_record_class_own_record_class_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_string_O), "d")] public abstract class L06PB_V_validated_struct_string_O { } -public sealed class L06PD_V_validated_struct_string_O : L06PB_V_validated_struct_string_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_string_RTE), "d")] public abstract class L06PB_V_validated_struct_string_RTE { } -public sealed class L06PD_V_validated_struct_string_RTE : L06PB_V_validated_struct_string_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_string_RE), "d")] public abstract class L06PB_V_validated_struct_string_RE { } -public sealed class L06PD_V_validated_struct_string_RE : L06PB_V_validated_struct_string_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_Guid_O), "d")] public abstract class L06PB_V_validated_struct_Guid_O { } -public sealed class L06PD_V_validated_struct_Guid_O : L06PB_V_validated_struct_Guid_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_Guid_RTE), "d")] public abstract class L06PB_V_validated_struct_Guid_RTE { } -public sealed class L06PD_V_validated_struct_Guid_RTE : L06PB_V_validated_struct_Guid_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_Guid_RE), "d")] public abstract class L06PB_V_validated_struct_Guid_RE { } -public sealed class L06PD_V_validated_struct_Guid_RE : L06PB_V_validated_struct_Guid_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_int_O), "d")] public abstract class L06PB_V_validated_struct_int_O { } -public sealed class L06PD_V_validated_struct_int_O : L06PB_V_validated_struct_int_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_int_RTE), "d")] public abstract class L06PB_V_validated_struct_int_RTE { } -public sealed class L06PD_V_validated_struct_int_RTE : L06PB_V_validated_struct_int_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_int_RE), "d")] public abstract class L06PB_V_validated_struct_int_RE { } -public sealed class L06PD_V_validated_struct_int_RE : L06PB_V_validated_struct_int_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_long_O), "d")] public abstract class L06PB_V_validated_struct_long_O { } -public sealed class L06PD_V_validated_struct_long_O : L06PB_V_validated_struct_long_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_long_RTE), "d")] public abstract class L06PB_V_validated_struct_long_RTE { } -public sealed class L06PD_V_validated_struct_long_RTE : L06PB_V_validated_struct_long_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_long_RE), "d")] public abstract class L06PB_V_validated_struct_long_RE { } -public sealed class L06PD_V_validated_struct_long_RE : L06PB_V_validated_struct_long_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_byte_O), "d")] public abstract class L06PB_V_validated_struct_byte_O { } -public sealed class L06PD_V_validated_struct_byte_O : L06PB_V_validated_struct_byte_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_byte_RTE), "d")] public abstract class L06PB_V_validated_struct_byte_RTE { } -public sealed class L06PD_V_validated_struct_byte_RTE : L06PB_V_validated_struct_byte_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_byte_RE), "d")] public abstract class L06PB_V_validated_struct_byte_RE { } -public sealed class L06PD_V_validated_struct_byte_RE : L06PB_V_validated_struct_byte_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_ulong_O), "d")] public abstract class L06PB_V_validated_struct_ulong_O { } -public sealed class L06PD_V_validated_struct_ulong_O : L06PB_V_validated_struct_ulong_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_ulong_RTE), "d")] public abstract class L06PB_V_validated_struct_ulong_RTE { } -public sealed class L06PD_V_validated_struct_ulong_RTE : L06PB_V_validated_struct_ulong_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_ulong_RE), "d")] public abstract class L06PB_V_validated_struct_ulong_RE { } -public sealed class L06PD_V_validated_struct_ulong_RE : L06PB_V_validated_struct_ulong_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_decimal_O), "d")] public abstract class L06PB_V_validated_struct_decimal_O { } -public sealed class L06PD_V_validated_struct_decimal_O : L06PB_V_validated_struct_decimal_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_decimal_RTE), "d")] public abstract class L06PB_V_validated_struct_decimal_RTE { } -public sealed class L06PD_V_validated_struct_decimal_RTE : L06PB_V_validated_struct_decimal_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_decimal_RE), "d")] public abstract class L06PB_V_validated_struct_decimal_RE { } -public sealed class L06PD_V_validated_struct_decimal_RE : L06PB_V_validated_struct_decimal_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_double_O), "d")] public abstract class L06PB_V_validated_struct_double_O { } -public sealed class L06PD_V_validated_struct_double_O : L06PB_V_validated_struct_double_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_double_RTE), "d")] public abstract class L06PB_V_validated_struct_double_RTE { } -public sealed class L06PD_V_validated_struct_double_RTE : L06PB_V_validated_struct_double_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_double_RE), "d")] public abstract class L06PB_V_validated_struct_double_RE { } -public sealed class L06PD_V_validated_struct_double_RE : L06PB_V_validated_struct_double_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_bool_O), "d")] public abstract class L06PB_V_validated_struct_bool_O { } -public sealed class L06PD_V_validated_struct_bool_O : L06PB_V_validated_struct_bool_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_bool_RTE), "d")] public abstract class L06PB_V_validated_struct_bool_RTE { } -public sealed class L06PD_V_validated_struct_bool_RTE : L06PB_V_validated_struct_bool_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_bool_RE), "d")] public abstract class L06PB_V_validated_struct_bool_RE { } -public sealed class L06PD_V_validated_struct_bool_RE : L06PB_V_validated_struct_bool_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_char_O), "d")] public abstract class L06PB_V_validated_struct_char_O { } -public sealed class L06PD_V_validated_struct_char_O : L06PB_V_validated_struct_char_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_char_RTE), "d")] public abstract class L06PB_V_validated_struct_char_RTE { } -public sealed class L06PD_V_validated_struct_char_RTE : L06PB_V_validated_struct_char_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_char_RE), "d")] public abstract class L06PB_V_validated_struct_char_RE { } -public sealed class L06PD_V_validated_struct_char_RE : L06PB_V_validated_struct_char_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_DateTime_O), "d")] public abstract class L06PB_V_validated_struct_DateTime_O { } -public sealed class L06PD_V_validated_struct_DateTime_O : L06PB_V_validated_struct_DateTime_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_DateTime_RTE), "d")] public abstract class L06PB_V_validated_struct_DateTime_RTE { } -public sealed class L06PD_V_validated_struct_DateTime_RTE : L06PB_V_validated_struct_DateTime_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_DateTime_RE), "d")] public abstract class L06PB_V_validated_struct_DateTime_RE { } -public sealed class L06PD_V_validated_struct_DateTime_RE : L06PB_V_validated_struct_DateTime_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_DateTimeOffset_O), "d")] public abstract class L06PB_V_validated_struct_DateTimeOffset_O { } -public sealed class L06PD_V_validated_struct_DateTimeOffset_O : L06PB_V_validated_struct_DateTimeOffset_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_DateTimeOffset_RTE), "d")] public abstract class L06PB_V_validated_struct_DateTimeOffset_RTE { } -public sealed class L06PD_V_validated_struct_DateTimeOffset_RTE : L06PB_V_validated_struct_DateTimeOffset_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_DateTimeOffset_RE), "d")] public abstract class L06PB_V_validated_struct_DateTimeOffset_RE { } -public sealed class L06PD_V_validated_struct_DateTimeOffset_RE : L06PB_V_validated_struct_DateTimeOffset_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_DateOnly_O), "d")] public abstract class L06PB_V_validated_struct_DateOnly_O { } -public sealed class L06PD_V_validated_struct_DateOnly_O : L06PB_V_validated_struct_DateOnly_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_DateOnly_RTE), "d")] public abstract class L06PB_V_validated_struct_DateOnly_RTE { } -public sealed class L06PD_V_validated_struct_DateOnly_RTE : L06PB_V_validated_struct_DateOnly_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_DateOnly_RE), "d")] public abstract class L06PB_V_validated_struct_DateOnly_RE { } -public sealed class L06PD_V_validated_struct_DateOnly_RE : L06PB_V_validated_struct_DateOnly_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_TimeOnly_O), "d")] public abstract class L06PB_V_validated_struct_TimeOnly_O { } -public sealed class L06PD_V_validated_struct_TimeOnly_O : L06PB_V_validated_struct_TimeOnly_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_TimeOnly_RTE), "d")] public abstract class L06PB_V_validated_struct_TimeOnly_RTE { } -public sealed class L06PD_V_validated_struct_TimeOnly_RTE : L06PB_V_validated_struct_TimeOnly_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_TimeOnly_RE), "d")] public abstract class L06PB_V_validated_struct_TimeOnly_RE { } -public sealed class L06PD_V_validated_struct_TimeOnly_RE : L06PB_V_validated_struct_TimeOnly_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_TimeSpan_O), "d")] public abstract class L06PB_V_validated_struct_TimeSpan_O { } -public sealed class L06PD_V_validated_struct_TimeSpan_O : L06PB_V_validated_struct_TimeSpan_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_TimeSpan_RTE), "d")] public abstract class L06PB_V_validated_struct_TimeSpan_RTE { } -public sealed class L06PD_V_validated_struct_TimeSpan_RTE : L06PB_V_validated_struct_TimeSpan_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_TimeSpan_RE), "d")] public abstract class L06PB_V_validated_struct_TimeSpan_RE { } -public sealed class L06PD_V_validated_struct_TimeSpan_RE : L06PB_V_validated_struct_TimeSpan_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_enum_O), "d")] public abstract class L06PB_V_validated_struct_enum_O { } -public sealed class L06PD_V_validated_struct_enum_O : L06PB_V_validated_struct_enum_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_enum_RTE), "d")] public abstract class L06PB_V_validated_struct_enum_RTE { } -public sealed class L06PD_V_validated_struct_enum_RTE : L06PB_V_validated_struct_enum_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_enum_RE), "d")] public abstract class L06PB_V_validated_struct_enum_RE { } -public sealed class L06PD_V_validated_struct_enum_RE : L06PB_V_validated_struct_enum_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_flags_enum_O), "d")] public abstract class L06PB_V_validated_struct_flags_enum_O { } -public sealed class L06PD_V_validated_struct_flags_enum_O : L06PB_V_validated_struct_flags_enum_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_flags_enum_RTE), "d")] public abstract class L06PB_V_validated_struct_flags_enum_RTE { } -public sealed class L06PD_V_validated_struct_flags_enum_RTE : L06PB_V_validated_struct_flags_enum_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_flags_enum_RE), "d")] public abstract class L06PB_V_validated_struct_flags_enum_RE { } -public sealed class L06PD_V_validated_struct_flags_enum_RE : L06PB_V_validated_struct_flags_enum_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_Uri_O), "d")] public abstract class L06PB_V_validated_struct_Uri_O { } -public sealed class L06PD_V_validated_struct_Uri_O : L06PB_V_validated_struct_Uri_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_Uri_RTE), "d")] public abstract class L06PB_V_validated_struct_Uri_RTE { } -public sealed class L06PD_V_validated_struct_Uri_RTE : L06PB_V_validated_struct_Uri_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_Uri_RE), "d")] public abstract class L06PB_V_validated_struct_Uri_RE { } -public sealed class L06PD_V_validated_struct_Uri_RE : L06PB_V_validated_struct_Uri_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_NodaTime_Instant_O), "d")] public abstract class L06PB_V_validated_struct_NodaTime_Instant_O { } -public sealed class L06PD_V_validated_struct_NodaTime_Instant_O : L06PB_V_validated_struct_NodaTime_Instant_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_NodaTime_Instant_RTE), "d")] public abstract class L06PB_V_validated_struct_NodaTime_Instant_RTE { } -public sealed class L06PD_V_validated_struct_NodaTime_Instant_RTE : L06PB_V_validated_struct_NodaTime_Instant_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_NodaTime_Instant_RE), "d")] public abstract class L06PB_V_validated_struct_NodaTime_Instant_RE { } -public sealed class L06PD_V_validated_struct_NodaTime_Instant_RE : L06PB_V_validated_struct_NodaTime_Instant_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_own_record_class_O), "d")] public abstract class L06PB_V_validated_struct_own_record_class_O { } -public sealed class L06PD_V_validated_struct_own_record_class_O : L06PB_V_validated_struct_own_record_class_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_own_record_class_RTE), "d")] public abstract class L06PB_V_validated_struct_own_record_class_RTE { } -public sealed class L06PD_V_validated_struct_own_record_class_RTE : L06PB_V_validated_struct_own_record_class_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_struct_own_record_class_RE), "d")] public abstract class L06PB_V_validated_struct_own_record_class_RE { } -public sealed class L06PD_V_validated_struct_own_record_class_RE : L06PB_V_validated_struct_own_record_class_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_string_O), "d")] public abstract class L06PB_V_validated_class_string_O { } -public sealed class L06PD_V_validated_class_string_O : L06PB_V_validated_class_string_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_string_RTE), "d")] public abstract class L06PB_V_validated_class_string_RTE { } -public sealed class L06PD_V_validated_class_string_RTE : L06PB_V_validated_class_string_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_string_RE), "d")] public abstract class L06PB_V_validated_class_string_RE { } -public sealed class L06PD_V_validated_class_string_RE : L06PB_V_validated_class_string_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_Guid_O), "d")] public abstract class L06PB_V_validated_class_Guid_O { } -public sealed class L06PD_V_validated_class_Guid_O : L06PB_V_validated_class_Guid_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_Guid_RTE), "d")] public abstract class L06PB_V_validated_class_Guid_RTE { } -public sealed class L06PD_V_validated_class_Guid_RTE : L06PB_V_validated_class_Guid_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_Guid_RE), "d")] public abstract class L06PB_V_validated_class_Guid_RE { } -public sealed class L06PD_V_validated_class_Guid_RE : L06PB_V_validated_class_Guid_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_int_O), "d")] public abstract class L06PB_V_validated_class_int_O { } -public sealed class L06PD_V_validated_class_int_O : L06PB_V_validated_class_int_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_int_RTE), "d")] public abstract class L06PB_V_validated_class_int_RTE { } -public sealed class L06PD_V_validated_class_int_RTE : L06PB_V_validated_class_int_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_int_RE), "d")] public abstract class L06PB_V_validated_class_int_RE { } -public sealed class L06PD_V_validated_class_int_RE : L06PB_V_validated_class_int_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_long_O), "d")] public abstract class L06PB_V_validated_class_long_O { } -public sealed class L06PD_V_validated_class_long_O : L06PB_V_validated_class_long_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_long_RTE), "d")] public abstract class L06PB_V_validated_class_long_RTE { } -public sealed class L06PD_V_validated_class_long_RTE : L06PB_V_validated_class_long_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_long_RE), "d")] public abstract class L06PB_V_validated_class_long_RE { } -public sealed class L06PD_V_validated_class_long_RE : L06PB_V_validated_class_long_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_byte_O), "d")] public abstract class L06PB_V_validated_class_byte_O { } -public sealed class L06PD_V_validated_class_byte_O : L06PB_V_validated_class_byte_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_byte_RTE), "d")] public abstract class L06PB_V_validated_class_byte_RTE { } -public sealed class L06PD_V_validated_class_byte_RTE : L06PB_V_validated_class_byte_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_byte_RE), "d")] public abstract class L06PB_V_validated_class_byte_RE { } -public sealed class L06PD_V_validated_class_byte_RE : L06PB_V_validated_class_byte_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_ulong_O), "d")] public abstract class L06PB_V_validated_class_ulong_O { } -public sealed class L06PD_V_validated_class_ulong_O : L06PB_V_validated_class_ulong_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_ulong_RTE), "d")] public abstract class L06PB_V_validated_class_ulong_RTE { } -public sealed class L06PD_V_validated_class_ulong_RTE : L06PB_V_validated_class_ulong_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_ulong_RE), "d")] public abstract class L06PB_V_validated_class_ulong_RE { } -public sealed class L06PD_V_validated_class_ulong_RE : L06PB_V_validated_class_ulong_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_decimal_O), "d")] public abstract class L06PB_V_validated_class_decimal_O { } -public sealed class L06PD_V_validated_class_decimal_O : L06PB_V_validated_class_decimal_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_decimal_RTE), "d")] public abstract class L06PB_V_validated_class_decimal_RTE { } -public sealed class L06PD_V_validated_class_decimal_RTE : L06PB_V_validated_class_decimal_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_decimal_RE), "d")] public abstract class L06PB_V_validated_class_decimal_RE { } -public sealed class L06PD_V_validated_class_decimal_RE : L06PB_V_validated_class_decimal_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_double_O), "d")] public abstract class L06PB_V_validated_class_double_O { } -public sealed class L06PD_V_validated_class_double_O : L06PB_V_validated_class_double_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_double_RTE), "d")] public abstract class L06PB_V_validated_class_double_RTE { } -public sealed class L06PD_V_validated_class_double_RTE : L06PB_V_validated_class_double_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_double_RE), "d")] public abstract class L06PB_V_validated_class_double_RE { } -public sealed class L06PD_V_validated_class_double_RE : L06PB_V_validated_class_double_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_bool_O), "d")] public abstract class L06PB_V_validated_class_bool_O { } -public sealed class L06PD_V_validated_class_bool_O : L06PB_V_validated_class_bool_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_bool_RTE), "d")] public abstract class L06PB_V_validated_class_bool_RTE { } -public sealed class L06PD_V_validated_class_bool_RTE : L06PB_V_validated_class_bool_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_bool_RE), "d")] public abstract class L06PB_V_validated_class_bool_RE { } -public sealed class L06PD_V_validated_class_bool_RE : L06PB_V_validated_class_bool_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_char_O), "d")] public abstract class L06PB_V_validated_class_char_O { } -public sealed class L06PD_V_validated_class_char_O : L06PB_V_validated_class_char_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_char_RTE), "d")] public abstract class L06PB_V_validated_class_char_RTE { } -public sealed class L06PD_V_validated_class_char_RTE : L06PB_V_validated_class_char_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_char_RE), "d")] public abstract class L06PB_V_validated_class_char_RE { } -public sealed class L06PD_V_validated_class_char_RE : L06PB_V_validated_class_char_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_DateTime_O), "d")] public abstract class L06PB_V_validated_class_DateTime_O { } -public sealed class L06PD_V_validated_class_DateTime_O : L06PB_V_validated_class_DateTime_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_DateTime_RTE), "d")] public abstract class L06PB_V_validated_class_DateTime_RTE { } -public sealed class L06PD_V_validated_class_DateTime_RTE : L06PB_V_validated_class_DateTime_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_DateTime_RE), "d")] public abstract class L06PB_V_validated_class_DateTime_RE { } -public sealed class L06PD_V_validated_class_DateTime_RE : L06PB_V_validated_class_DateTime_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_DateTimeOffset_O), "d")] public abstract class L06PB_V_validated_class_DateTimeOffset_O { } -public sealed class L06PD_V_validated_class_DateTimeOffset_O : L06PB_V_validated_class_DateTimeOffset_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_DateTimeOffset_RTE), "d")] public abstract class L06PB_V_validated_class_DateTimeOffset_RTE { } -public sealed class L06PD_V_validated_class_DateTimeOffset_RTE : L06PB_V_validated_class_DateTimeOffset_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_DateTimeOffset_RE), "d")] public abstract class L06PB_V_validated_class_DateTimeOffset_RE { } -public sealed class L06PD_V_validated_class_DateTimeOffset_RE : L06PB_V_validated_class_DateTimeOffset_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_DateOnly_O), "d")] public abstract class L06PB_V_validated_class_DateOnly_O { } -public sealed class L06PD_V_validated_class_DateOnly_O : L06PB_V_validated_class_DateOnly_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_DateOnly_RTE), "d")] public abstract class L06PB_V_validated_class_DateOnly_RTE { } -public sealed class L06PD_V_validated_class_DateOnly_RTE : L06PB_V_validated_class_DateOnly_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_DateOnly_RE), "d")] public abstract class L06PB_V_validated_class_DateOnly_RE { } -public sealed class L06PD_V_validated_class_DateOnly_RE : L06PB_V_validated_class_DateOnly_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_TimeOnly_O), "d")] public abstract class L06PB_V_validated_class_TimeOnly_O { } -public sealed class L06PD_V_validated_class_TimeOnly_O : L06PB_V_validated_class_TimeOnly_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_TimeOnly_RTE), "d")] public abstract class L06PB_V_validated_class_TimeOnly_RTE { } -public sealed class L06PD_V_validated_class_TimeOnly_RTE : L06PB_V_validated_class_TimeOnly_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_TimeOnly_RE), "d")] public abstract class L06PB_V_validated_class_TimeOnly_RE { } -public sealed class L06PD_V_validated_class_TimeOnly_RE : L06PB_V_validated_class_TimeOnly_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_TimeSpan_O), "d")] public abstract class L06PB_V_validated_class_TimeSpan_O { } -public sealed class L06PD_V_validated_class_TimeSpan_O : L06PB_V_validated_class_TimeSpan_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_TimeSpan_RTE), "d")] public abstract class L06PB_V_validated_class_TimeSpan_RTE { } -public sealed class L06PD_V_validated_class_TimeSpan_RTE : L06PB_V_validated_class_TimeSpan_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_TimeSpan_RE), "d")] public abstract class L06PB_V_validated_class_TimeSpan_RE { } -public sealed class L06PD_V_validated_class_TimeSpan_RE : L06PB_V_validated_class_TimeSpan_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_enum_O), "d")] public abstract class L06PB_V_validated_class_enum_O { } -public sealed class L06PD_V_validated_class_enum_O : L06PB_V_validated_class_enum_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_enum_RTE), "d")] public abstract class L06PB_V_validated_class_enum_RTE { } -public sealed class L06PD_V_validated_class_enum_RTE : L06PB_V_validated_class_enum_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_enum_RE), "d")] public abstract class L06PB_V_validated_class_enum_RE { } -public sealed class L06PD_V_validated_class_enum_RE : L06PB_V_validated_class_enum_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_flags_enum_O), "d")] public abstract class L06PB_V_validated_class_flags_enum_O { } -public sealed class L06PD_V_validated_class_flags_enum_O : L06PB_V_validated_class_flags_enum_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_flags_enum_RTE), "d")] public abstract class L06PB_V_validated_class_flags_enum_RTE { } -public sealed class L06PD_V_validated_class_flags_enum_RTE : L06PB_V_validated_class_flags_enum_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_flags_enum_RE), "d")] public abstract class L06PB_V_validated_class_flags_enum_RE { } -public sealed class L06PD_V_validated_class_flags_enum_RE : L06PB_V_validated_class_flags_enum_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_Uri_O), "d")] public abstract class L06PB_V_validated_class_Uri_O { } -public sealed class L06PD_V_validated_class_Uri_O : L06PB_V_validated_class_Uri_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_Uri_RTE), "d")] public abstract class L06PB_V_validated_class_Uri_RTE { } -public sealed class L06PD_V_validated_class_Uri_RTE : L06PB_V_validated_class_Uri_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_Uri_RE), "d")] public abstract class L06PB_V_validated_class_Uri_RE { } -public sealed class L06PD_V_validated_class_Uri_RE : L06PB_V_validated_class_Uri_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_NodaTime_Instant_O), "d")] public abstract class L06PB_V_validated_class_NodaTime_Instant_O { } -public sealed class L06PD_V_validated_class_NodaTime_Instant_O : L06PB_V_validated_class_NodaTime_Instant_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_NodaTime_Instant_RTE), "d")] public abstract class L06PB_V_validated_class_NodaTime_Instant_RTE { } -public sealed class L06PD_V_validated_class_NodaTime_Instant_RTE : L06PB_V_validated_class_NodaTime_Instant_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_NodaTime_Instant_RE), "d")] public abstract class L06PB_V_validated_class_NodaTime_Instant_RE { } -public sealed class L06PD_V_validated_class_NodaTime_Instant_RE : L06PB_V_validated_class_NodaTime_Instant_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_own_record_class_O), "d")] public abstract class L06PB_V_validated_class_own_record_class_O { } -public sealed class L06PD_V_validated_class_own_record_class_O : L06PB_V_validated_class_own_record_class_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_own_record_class_RTE), "d")] public abstract class L06PB_V_validated_class_own_record_class_RTE { } -public sealed class L06PD_V_validated_class_own_record_class_RTE : L06PB_V_validated_class_own_record_class_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_V_validated_class_own_record_class_RE), "d")] public abstract class L06PB_V_validated_class_own_record_class_RE { } -public sealed class L06PD_V_validated_class_own_record_class_RE : L06PB_V_validated_class_own_record_class_RE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_PSet_O), "d")] public abstract class L06PB_PSet_O { } -public sealed class L06PD_PSet_O : L06PB_PSet_O { public Option Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_PSet_RTE), "d")] public abstract class L06PB_PSet_RTE { } -public sealed class L06PD_PSet_RTE : L06PB_PSet_RTE { public Result Value { get; set; } } -[System.Text.Json.Serialization.JsonPolymorphic] [System.Text.Json.Serialization.JsonDerivedType(typeof(L06PD_PSet_RE), "d")] public abstract class L06PB_PSet_RE { } -public sealed class L06PD_PSet_RE : L06PB_PSet_RE { public Result Value { get; set; } } diff --git a/tools/probe/corpus/harness/JsonRow.cs b/tools/probe/corpus/harness/JsonRow.cs deleted file mode 100644 index f39be575..00000000 --- a/tools/probe/corpus/harness/JsonRow.cs +++ /dev/null @@ -1,183 +0,0 @@ -// Applies a row's json.* axes to JsonSerializerOptions (reflection for every plain bool/enum setting, hand-listed values by name). -using System.Reflection; -using System.Text.Encodings.Web; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; -using CodoMetis.TypeKit.ValueObjects; -using NodaTime; -using NodaTime.Serialization.SystemTextJson; -namespace Probes.Shapes; - -public sealed class UpperPolicy : JsonNamingPolicy { public override string ConvertName(string name) => name.ToUpperInvariant(); } - -public static class Make -{ - /// - /// The type a nullable one holds: under http.nullability=nullable an entry's Vo and Plain are T?, the types the - /// endpoints declare, while the value object is still made by From(T) and read through its Value. - /// - public static Type Held(Type t) => Nullable.GetUnderlyingType(t) ?? t; - /// Whether a type admits null: a class, or a struct made nullable. A value object and its twin admit it alike (Equivalence). - public static bool AdmitsNull(Type t) => !t.IsValueType || Nullable.GetUnderlyingType(t) is not null; - - /// Creates the value object over a plain sample (From / FromKnownGood), with optional parameters filled. - public static object Vo(Entry e, object plain) - { - if (e.Kind == "closed-set") return PSet.All.First(x => x.Name == (string)plain); - var m = Held(e.Vo).GetMethods(BindingFlags.Public | BindingFlags.Static) - .First(x => (x.Name is "FromKnownGood" or "From") && x.GetParameters().Length > 0 && x.GetParameters()[0].ParameterType == Held(e.Plain)); - var args = m.GetParameters().Select((p, i) => i == 0 ? plain : Type.Missing).ToArray(); - return m.Invoke(null, BindingFlags.OptionalParamBinding | BindingFlags.Default, null, args, null)!; - } - public static object Unwrap(Entry e, object vo) => e.Kind == "closed-set" ? ((PSet)vo).Name : Held(e.Vo).GetProperty("Value")!.GetValue(vo)!; -} - -/// A converter an application registers for the wrapped type: writes "own:" + the default JSON. -public sealed class OwnWrappedConverter(JsonSerializerOptions inner) : JsonConverter -{ - public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var s = reader.GetString() ?? throw new JsonException("null"); - if (!s.StartsWith("own:")) throw new JsonException("not own"); - return JsonSerializer.Deserialize(s[4..], inner); - } - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => writer.WriteStringValue("own:" + JsonSerializer.Serialize(value, inner)); - public override T ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => Read(ref reader, typeToConvert, options)!; - public override void WriteAsPropertyName(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => writer.WritePropertyName("own:" + JsonSerializer.Serialize(value, inner)); -} - -/// A converter an application registers for the value object itself: writes "vo:" + the wrapped value's default JSON. -public sealed class OwnVoFactory(Entry e, JsonSerializerOptions inner) : JsonConverterFactory -{ - public override bool CanConvert(Type t) => t == Make.Held(e.Vo); // System.Text.Json asks for T, never for T? - public override JsonConverter? CreateConverter(Type t, JsonSerializerOptions options) => - (JsonConverter)Activator.CreateInstance(typeof(OwnVo<>).MakeGenericType(t), e, inner)!; -} -public sealed class OwnVo(Entry e, JsonSerializerOptions inner) : JsonConverter -{ - public override V? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var s = reader.GetString() ?? throw new JsonException("null"); - if (!s.StartsWith("vo:")) throw new JsonException("not vo"); - return (V)Make.Vo(e, JsonSerializer.Deserialize(s[3..], Make.Held(e.Plain), inner)!); - } - public override void Write(Utf8JsonWriter writer, V value, JsonSerializerOptions options) => - writer.WriteStringValue("vo:" + JsonSerializer.Serialize(Make.Unwrap(e, value!), Make.Held(e.Plain), inner)); - public override V ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => Read(ref reader, typeToConvert, options)!; - public override void WriteAsPropertyName(Utf8JsonWriter writer, V value, JsonSerializerOptions options) => - writer.WritePropertyName("vo:" + JsonSerializer.Serialize(Make.Unwrap(e, value!), Make.Held(e.Plain), inner)); -} - -public sealed record Applied(JsonSerializerOptions? Options, string? Refused); - -public static class JsonRow -{ - public static JsonSerializerOptions Inner(Entry e) - { - var o = new JsonSerializerOptions(); - if (e.Wrapped == "NodaTime.Instant") o.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); - return o; - } - - /// Applies every json.* axis of the row to (or a new instance). Refused = STJ rejected a combination with an exception of its own (a pass). - public static Applied Apply(Row r, Entry e, JsonSerializerOptions? into = null) - { - var o = into ?? new JsonSerializerOptions(); - var dropped = new List(); - if (e.Wrapped == "NodaTime.Instant") o.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); - if (r.Get("json.PreferredObjectCreationHandling") == "Populate" && r.Get("json.ReferenceHandler") is not (null or "default")) - { - dropped.Add("json.PreferredObjectCreationHandling=Populate (STJ: incompatible with reference handling)"); - r.Substituted("json.PreferredObjectCreationHandling", "default", "STJ: incompatible with reference handling"); - r = r.With("json.PreferredObjectCreationHandling", "default"); - } - // resolver before chain: a chain on top of a resolver is a combination STJ may refuse - foreach (var kv in r.All.Where(kv => kv.Key.StartsWith("json.") && kv.Value != "default").OrderBy(kv => kv.Key == "json.TypeInfoResolverChain" ? 1 : 0)) - { - try { Set(r, o, kv.Key[5..], kv.Value, e); } - catch (Exception ex) when (ex is ArgumentException or InvalidOperationException or NotSupportedException) - { - // STJ refuses this setting with an exception of its own: a pass for the pairs of that value; the rest of the row is still probed - dropped.Add($"{kv.Key}={kv.Value} ({ex.GetType().Name})"); - r.Substituted(kv.Key, "default", "STJ refused it: " + ex.GetType().Name); - } - } - r.Note(NoteKeys.Dropped, string.Join(",", dropped)); - return new(o, dropped.Count == 0 ? null : string.Join(",", dropped)); - } - - static JsonNamingPolicy? Policy(string v) => v switch - { - "CamelCase" => JsonNamingPolicy.CamelCase, "SnakeCaseLower" => JsonNamingPolicy.SnakeCaseLower, "SnakeCaseUpper" => JsonNamingPolicy.SnakeCaseUpper, - "KebabCaseLower" => JsonNamingPolicy.KebabCaseLower, "KebabCaseUpper" => JsonNamingPolicy.KebabCaseUpper, "own-upper-case" => new UpperPolicy(), - _ => throw new ArgumentException("unknown policy " + v) - }; - - /// - /// json.TypeInfoResolver=source-generated-context is strict: the options resolve through ProbeContext alone, which lists every shape a row - /// holds and its twin (shapes.cs), with no reflection behind it; json.TypeInfoResolverChain=context-then-default is the configuration with - /// the reflection fallback, the context then reflection, beside it or on its own. A row's shape the context does not list is the generator's gap, a harness error: the serializer's refusal of - /// an unlisted type would read as the framework's. - /// - static void Listed(Row r, Entry e) - { - var plain = e.Valid(); var vo = Make.Vo(e, plain); - var pos = r["shape.position"]; var con = (r.Get("shape.containing") ?? "class"); - var (hp, hv) = Sides.Twins(pos, con, e, plain, vo); - var missing = Missing(hp.Declared, hv.Declared).Select(t => t.ToString()).ToList(); - if (missing.Count > 0) { r.Outcome("harness-error", $"ProbeContext does not list {string.Join(", ", missing)}: run tools/probe/generators/shapes.cs"); return; } - r.Note(NoteKeys.CtxStrict); - } - - /// The ProbeContext does not list, under json.TypeInfoResolver=source-generated-context: the generator's gap, which the caller reports as a harness error. - public static IEnumerable Unlisted(Row r, params Type[] shapes) => - r.Is("json.TypeInfoResolver", "source-generated-context") ? Missing(shapes) : []; - - /// - /// The types ProbeContext has no metadata for, asked through the resolver's interface, which builds no contract: building one is the - /// serializer's step, where a context refuses an Option or Result made nullable (L06's not-promised entry), not the listing's. - /// - static IEnumerable Missing(params Type[] shapes) - { - // the context answers what a type's metadata asks the options for (the struct a Nullable holds) too - var probe = new JsonSerializerOptions { TypeInfoResolver = ProbeContext.Default }; - return shapes.Where(t => ((IJsonTypeInfoResolver)ProbeContext.Default).GetTypeInfo(t, probe) is null).ToList(); - } - - static void Set(Row row, JsonSerializerOptions o, string name, string v, Entry e) - { - switch (name) - { - case "PropertyNamingPolicy": o.PropertyNamingPolicy = Policy(v); return; - case "DictionaryKeyPolicy": o.DictionaryKeyPolicy = Policy(v); return; - case "ReferenceHandler": o.ReferenceHandler = v == "Preserve" ? ReferenceHandler.Preserve : ReferenceHandler.IgnoreCycles; return; - case "Encoder": o.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; return; - case "MaxDepth": o.MaxDepth = int.Parse(v); return; - case "Converters": - switch (v) - { - case "own-for-wrapped": o.Converters.Insert(0, (JsonConverter)Activator.CreateInstance(typeof(OwnWrappedConverter<>).MakeGenericType(Make.Held(e.Plain)), Inner(e))!); break; - case "own-for-value-object": o.Converters.Insert(0, new OwnVoFactory(e, Inner(e))); break; - case "JsonStringEnumConverter": o.Converters.Add(new JsonStringEnumConverter()); break; - case "StoredJsonConverterFactory": o.Converters.Add(new StoredJsonConverterFactory()); break; - default: throw new ArgumentException("unknown converter " + v); - } - return; - case "TypeInfoResolver": - if (v == "source-generated-context") { o.TypeInfoResolver = ProbeContext.Default; Listed(row, e); } - else o.TypeInfoResolver = new DefaultJsonTypeInfoResolver { Modifiers = { ti => { foreach (var p in ti.Properties) p.Name += "_x"; } } }; - return; - case "TypeInfoResolverChain": - // The context, then reflection for what it does not list: under json.TypeInfoResolver=source-generated-context the chain - // already starts with the context, which System.Text.Json refuses to insert a second time (the resolver into its own - // chain), so the reflection fallback follows it. That pair is the context with the fallback, the strict context's other side. - if (!o.TypeInfoResolverChain.Contains(ProbeContext.Default)) o.TypeInfoResolverChain.Add(ProbeContext.Default); - o.TypeInfoResolverChain.Add(new DefaultJsonTypeInfoResolver()); return; - } - var prop = typeof(JsonSerializerOptions).GetProperty(name) ?? throw new ArgumentException("no json option " + name); - object value = prop.PropertyType == typeof(bool) ? bool.Parse(v) : prop.PropertyType.IsEnum ? Enum.Parse(prop.PropertyType, v) : throw new ArgumentException($"no hand value for {name}"); - try { prop.SetValue(o, value); } - catch (TargetInvocationException ex) { throw ex.InnerException!; } - } -} diff --git a/tools/probe/corpus/harness/KeyShapes.g.cs b/tools/probe/corpus/harness/KeyShapes.g.cs deleted file mode 100644 index 0a98b904..00000000 --- a/tools/probe/corpus/harness/KeyShapes.g.cs +++ /dev/null @@ -1,128 +0,0 @@ -// by tools/probe/generators/ef.cs keys; the EF key probes' shape catalogue (shape.kind x shape.wrapped). Do not edit. -#nullable enable -#pragma warning disable CMTK0014, CS8618, CS1591, CMTK0005, CMTK1000, CMTK0003, CMTK0006, CA1000 -using CodoMetis.TypeKit; -using CodoMetis.TypeKit.ValueObjects; -using Probes.Shapes; -namespace Probes.Ef; -public readonly partial record struct K_record_struct_Guid : IValue; -public readonly partial record struct K_record_struct_byte : IValue; -public readonly partial record struct K_record_struct_char : IValue; -public readonly partial record struct K_record_struct_int : IValue; -public readonly partial record struct K_record_struct_long : IValue; -public readonly partial record struct K_record_struct_string : IValue; -public readonly partial record struct K_record_struct_ulong : IValue; -public sealed partial record class K_sealed_record_class_Guid : IValue; -public sealed partial record class K_sealed_record_class_byte : IValue; -public sealed partial record class K_sealed_record_class_char : IValue; -public sealed partial record class K_sealed_record_class_int : IValue; -public sealed partial record class K_sealed_record_class_long : IValue; -public sealed partial record class K_sealed_record_class_string : IValue; -public sealed partial record class K_sealed_record_class_ulong : IValue; -public sealed partial record class K_validated_class_Guid : IValidatedValue -{ - public static Result Create(Guid value) => - EqualityComparer.Default.Equals(value, Guid.Empty) ? Result.Error(ShapeFault.Refused) : new K_validated_class_Guid(value); -} -public sealed partial record class K_validated_class_byte : IValidatedValue -{ - public static Result Create(byte value) => - EqualityComparer.Default.Equals(value, (byte)255) ? Result.Error(ShapeFault.Refused) : new K_validated_class_byte(value); -} -public sealed partial record class K_validated_class_char : IValidatedValue -{ - public static Result Create(char value) => - EqualityComparer.Default.Equals(value, 'x') ? Result.Error(ShapeFault.Refused) : new K_validated_class_char(value); -} -public sealed partial record class K_validated_class_int : IValidatedValue -{ - public static Result Create(int value) => - EqualityComparer.Default.Equals(value, -1) ? Result.Error(ShapeFault.Refused) : new K_validated_class_int(value); -} -public sealed partial record class K_validated_class_long : IValidatedValue -{ - public static Result Create(long value) => - EqualityComparer.Default.Equals(value, -1L) ? Result.Error(ShapeFault.Refused) : new K_validated_class_long(value); -} -public sealed partial record class K_validated_class_string : IValidatedValue -{ - public static Result Create(string value) => - EqualityComparer.Default.Equals(value, "SECRET-7f3a") ? Result.Error(ShapeFault.Refused) : new K_validated_class_string(value); -} -public sealed partial record class K_validated_class_ulong : IValidatedValue -{ - public static Result Create(ulong value) => - EqualityComparer.Default.Equals(value, 0UL) ? Result.Error(ShapeFault.Refused) : new K_validated_class_ulong(value); -} -public readonly partial record struct K_validated_struct_Guid : IValidatedValue -{ - public static Result Create(Guid value) => - EqualityComparer.Default.Equals(value, Guid.Empty) ? Result.Error(ShapeFault.Refused) : new K_validated_struct_Guid(value); -} -public readonly partial record struct K_validated_struct_byte : IValidatedValue -{ - public static Result Create(byte value) => - EqualityComparer.Default.Equals(value, (byte)255) ? Result.Error(ShapeFault.Refused) : new K_validated_struct_byte(value); -} -public readonly partial record struct K_validated_struct_char : IValidatedValue -{ - public static Result Create(char value) => - EqualityComparer.Default.Equals(value, 'x') ? Result.Error(ShapeFault.Refused) : new K_validated_struct_char(value); -} -public readonly partial record struct K_validated_struct_int : IValidatedValue -{ - public static Result Create(int value) => - EqualityComparer.Default.Equals(value, -1) ? Result.Error(ShapeFault.Refused) : new K_validated_struct_int(value); -} -public readonly partial record struct K_validated_struct_long : IValidatedValue -{ - public static Result Create(long value) => - EqualityComparer.Default.Equals(value, -1L) ? Result.Error(ShapeFault.Refused) : new K_validated_struct_long(value); -} -public readonly partial record struct K_validated_struct_string : IValidatedValue -{ - public static Result Create(string value) => - EqualityComparer.Default.Equals(value, "SECRET-7f3a") ? Result.Error(ShapeFault.Refused) : new K_validated_struct_string(value); -} -public readonly partial record struct K_validated_struct_ulong : IValidatedValue -{ - public static Result Create(ulong value) => - EqualityComparer.Default.Equals(value, 0UL) ? Result.Error(ShapeFault.Refused) : new K_validated_struct_ulong(value); -} -public static partial class KeyRegistry -{ - public static readonly List All = Build(); - static List Build() { var e = new List(); Fill(e); return e; } - public static EfEntry Of(Probes.Shapes.Row r) { var k = r["shape.kind"]; var w = r["shape.wrapped"]; return All.First(x => x.Kind == k && x.Wrapped == w); } - public static void Fill(List e) - { - e.Add(new EfEntry("record-struct", "Guid", "top-level", typeof(K_record_struct_Guid), typeof(Guid), () => Guid.Parse("11111111-1111-1111-1111-111111111111"))); - e.Add(new EfEntry("record-struct", "byte", "top-level", typeof(K_record_struct_byte), typeof(byte), () => (byte)7)); - e.Add(new EfEntry("record-struct", "char", "top-level", typeof(K_record_struct_char), typeof(char), () => 'a')); - e.Add(new EfEntry("record-struct", "int", "top-level", typeof(K_record_struct_int), typeof(int), () => 42)); - e.Add(new EfEntry("record-struct", "long", "top-level", typeof(K_record_struct_long), typeof(long), () => 4200000000L)); - e.Add(new EfEntry("record-struct", "string", "top-level", typeof(K_record_struct_string), typeof(string), () => "alpha")); - e.Add(new EfEntry("record-struct", "ulong", "top-level", typeof(K_record_struct_ulong), typeof(ulong), () => 4200000000UL)); - e.Add(new EfEntry("sealed-record-class", "Guid", "top-level", typeof(K_sealed_record_class_Guid), typeof(Guid), () => Guid.Parse("11111111-1111-1111-1111-111111111111"))); - e.Add(new EfEntry("sealed-record-class", "byte", "top-level", typeof(K_sealed_record_class_byte), typeof(byte), () => (byte)7)); - e.Add(new EfEntry("sealed-record-class", "char", "top-level", typeof(K_sealed_record_class_char), typeof(char), () => 'a')); - e.Add(new EfEntry("sealed-record-class", "int", "top-level", typeof(K_sealed_record_class_int), typeof(int), () => 42)); - e.Add(new EfEntry("sealed-record-class", "long", "top-level", typeof(K_sealed_record_class_long), typeof(long), () => 4200000000L)); - e.Add(new EfEntry("sealed-record-class", "string", "top-level", typeof(K_sealed_record_class_string), typeof(string), () => "alpha")); - e.Add(new EfEntry("sealed-record-class", "ulong", "top-level", typeof(K_sealed_record_class_ulong), typeof(ulong), () => 4200000000UL)); - e.Add(new EfEntry("validated-class", "Guid", "top-level", typeof(K_validated_class_Guid), typeof(Guid), () => Guid.Parse("11111111-1111-1111-1111-111111111111"))); - e.Add(new EfEntry("validated-class", "byte", "top-level", typeof(K_validated_class_byte), typeof(byte), () => (byte)7)); - e.Add(new EfEntry("validated-class", "char", "top-level", typeof(K_validated_class_char), typeof(char), () => 'a')); - e.Add(new EfEntry("validated-class", "int", "top-level", typeof(K_validated_class_int), typeof(int), () => 42)); - e.Add(new EfEntry("validated-class", "long", "top-level", typeof(K_validated_class_long), typeof(long), () => 4200000000L)); - e.Add(new EfEntry("validated-class", "string", "top-level", typeof(K_validated_class_string), typeof(string), () => "alpha")); - e.Add(new EfEntry("validated-class", "ulong", "top-level", typeof(K_validated_class_ulong), typeof(ulong), () => 4200000000UL)); - e.Add(new EfEntry("validated-struct", "Guid", "top-level", typeof(K_validated_struct_Guid), typeof(Guid), () => Guid.Parse("11111111-1111-1111-1111-111111111111"))); - e.Add(new EfEntry("validated-struct", "byte", "top-level", typeof(K_validated_struct_byte), typeof(byte), () => (byte)7)); - e.Add(new EfEntry("validated-struct", "char", "top-level", typeof(K_validated_struct_char), typeof(char), () => 'a')); - e.Add(new EfEntry("validated-struct", "int", "top-level", typeof(K_validated_struct_int), typeof(int), () => 42)); - e.Add(new EfEntry("validated-struct", "long", "top-level", typeof(K_validated_struct_long), typeof(long), () => 4200000000L)); - e.Add(new EfEntry("validated-struct", "string", "top-level", typeof(K_validated_struct_string), typeof(string), () => "alpha")); - e.Add(new EfEntry("validated-struct", "ulong", "top-level", typeof(K_validated_struct_ulong), typeof(ulong), () => 4200000000UL)); - } -} diff --git a/tools/probe/corpus/harness/L01JsonBytesProbe.cs b/tools/probe/corpus/harness/L01JsonBytesProbe.cs deleted file mode 100644 index 4eb89c77..00000000 --- a/tools/probe/corpus/harness/L01JsonBytesProbe.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Claim L01-json-bytes (README#what-typekit-guarantees "Swapping a primitive for a value object keeps the JSON"; docs/json.md#the-wrapped-types-json): -// the value object's JSON is the wrapped type's, byte for byte, under whatever options, as value and as key, and each reads the other's JSON. -// Oracle: differential (JsonParity). Expected: no failure for any row; STJ refusing its own option combination, and the documented -// Preserve refusal, are passes. -using Probes.Shapes; -using System.Text.Json; -namespace Probes; -public sealed class L01JsonBytesProbe -{ - public static TheoryData Data() => Rows.Data("L01JsonBytesProbe"); - [Theory, MemberData(nameof(Data))] - public Task Row(string probe, int idx) => Rec.Run(probe, idx, async r => - { - var e = Registry.Of(r); - var a = JsonRow.Apply(r, e); - JsonSerializerOptions? plainOpts = null; - if (e.Kind == "closed-set" && r.Is("json.Converters", "own-for-wrapped")) plainOpts = JsonRow.Apply(r.With("json.Converters", "default"), e).Options; // a closed set travels as its name, not as a string - await Task.CompletedTask; - return JsonParity.Check(r, e, a.Options!, r["shape.position"], (r.Get("shape.containing") ?? "class"), r["input.json"], plainOpts); - }); -} diff --git a/tools/probe/corpus/harness/L02NoQuoteProbe.cs b/tools/probe/corpus/harness/L02NoQuoteProbe.cs deleted file mode 100644 index 11f88a8b..00000000 --- a/tools/probe/corpus/harness/L02NoQuoteProbe.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Claim L02-no-quote (README#what-typekit-guarantees; src/CodoMetis.TypeKit.Generators/README.md#parsing-and-formatting). L02 (exception): a rejection never repeats the input. -// Expected: no failure for any row; the documented differences (lossy default text of TimeOnly/DateTime/DateTimeOffset, a registered converter, StoredJsonConverterFactory) are passes. -using Probes.Shapes; -namespace Probes; -public sealed class L02NoQuoteProbe -{ - public static TheoryData Data() => Rows.Data("L02NoQuoteProbe"); - [Theory, MemberData(nameof(Data))] - public Task Row(string probe, int idx) => Rec.Run(probe, idx, r => Task.FromResult(TextClaims.NoQuote(r, Registry.Of(r)))); -} diff --git a/tools/probe/corpus/harness/L03OneRuleProbe.cs b/tools/probe/corpus/harness/L03OneRuleProbe.cs deleted file mode 100644 index e8e1fd24..00000000 --- a/tools/probe/corpus/harness/L03OneRuleProbe.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Claim L03-one-rule (README#what-typekit-guarantees; src/CodoMetis.TypeKit.Generators/README.md#parsing-and-formatting). L03 (claim): every entry runs Create and names its fault. -// Expected: no failure for any row; the documented differences (lossy default text of TimeOnly/DateTime/DateTimeOffset, a registered converter, StoredJsonConverterFactory) are passes. -using Probes.Shapes; -namespace Probes; -public sealed class L03OneRuleProbe -{ - public static TheoryData Data() => Rows.Data("L03OneRuleProbe"); - [Theory, MemberData(nameof(Data))] - public Task Row(string probe, int idx) => Rec.Run(probe, idx, r => Task.FromResult(TextClaims.OneRule(r, Registry.Of(r)))); -} diff --git a/tools/probe/corpus/harness/L04RoundTripProbe.cs b/tools/probe/corpus/harness/L04RoundTripProbe.cs deleted file mode 100644 index 2ae7ad0c..00000000 --- a/tools/probe/corpus/harness/L04RoundTripProbe.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Claim L04-round-trip (README#what-typekit-guarantees; src/CodoMetis.TypeKit.Generators/README.md#parsing-and-formatting). L04 round trip (metamorphic): VO text/JSON written by the project reads back as the same value. -// Expected: no failure for any row; the documented differences (lossy default text of TimeOnly/DateTime/DateTimeOffset, a registered converter, StoredJsonConverterFactory) are passes. -using Probes.Shapes; -namespace Probes; -public sealed class L04RoundTripProbe -{ - public static TheoryData Data() => Rows.Data("L04RoundTripProbe"); - [Theory, MemberData(nameof(Data))] - public Task Row(string probe, int idx) => Rec.Run(probe, idx, r => Task.FromResult(TextClaims.RoundTrip(r, Registry.Of(r)))); -} diff --git a/tools/probe/corpus/harness/L05Shapes.g.cs b/tools/probe/corpus/harness/L05Shapes.g.cs deleted file mode 100644 index f3f576ad..00000000 --- a/tools/probe/corpus/harness/L05Shapes.g.cs +++ /dev/null @@ -1,575 +0,0 @@ -// by tools/probe/generators/json-family.cs from the rows of L05-equality. Do not edit. -#nullable enable -#pragma warning disable CS1591, CMTK0005, CMTK1023 // CMTK1023: a base record with an explicit IEquatable is the shape under test -using CodoMetis.TypeKit; -using CodoMetis.TypeKit.ValueObjects; -using CodoMetis.TypeKit.ClosedSets; -using Probes.Shapes; -namespace Probes.Json; - -public interface IL05Tag { string Tag { get; } } -public abstract record L05B1; -public abstract record L05B2a; -public abstract record L05B2b : L05B2a; -public abstract record L05B3a; -public abstract record L05B3b : L05B3a; -public abstract record L05B3c : L05B3b; -// a deeper base whose own IEquatable says "never equal": the value object must not use it -public abstract record L05E0 : IEquatable { bool IEquatable.Equals(L05E0? other) => false; } -public abstract record L05E1 : L05E0; -public abstract class L05CB1; -public abstract class L05CB2a; -public abstract class L05CB2b : L05CB2a; -public abstract class L05CB3a; -public abstract class L05CB3b : L05CB3a; -public abstract class L05CB3c : L05CB3b; -public abstract class L05CE0 : IEquatable { bool IEquatable.Equals(L05CE0? other) => ReferenceEquals(this, other); } -public abstract class L05CE1 : L05CE0; - -public partial class L05Oc0 { public sealed partial class L05_0_closed_set__ : L05CB1, IClosedSet { public static readonly L05_0_closed_set__ Alpha = new(); public static readonly L05_0_closed_set__ Beta = new(); } } -public partial class L05Ot1 { public partial class Inner { public sealed partial class L05_1_closed_set__ : L05CB1, IClosedSet { public static readonly L05_1_closed_set__ Alpha = new(); public static readonly L05_1_closed_set__ Beta = new(); } } } -public partial class L05Oc2 { public sealed partial class L05_2_closed_set__ : L05CB2b, IClosedSet { public static readonly L05_2_closed_set__ Alpha = new(); public static readonly L05_2_closed_set__ Beta = new(); } } -public partial struct L05Os3 { public sealed partial class L05_3_closed_set__ : L05CB2b, IClosedSet { public static readonly L05_3_closed_set__ Alpha = new(); public static readonly L05_3_closed_set__ Beta = new(); } } -public sealed partial class L05_4_closed_set__ : L05CB2b, IClosedSet { public static readonly L05_4_closed_set__ Alpha = new(); public static readonly L05_4_closed_set__ Beta = new(); } -public partial class L05Oc5 { public sealed partial class L05_5_closed_set__ : L05CB3c, IClosedSet { public static readonly L05_5_closed_set__ Alpha = new(); public static readonly L05_5_closed_set__ Beta = new(); } } -public partial struct L05Os6 { public sealed partial class L05_6_closed_set__ : L05CB3c, IClosedSet { public static readonly L05_6_closed_set__ Alpha = new(); public static readonly L05_6_closed_set__ Beta = new(); } } -public partial class L05Ot7 { public partial class Inner { public sealed partial class L05_7_closed_set__ : L05CB3c, IClosedSet { public static readonly L05_7_closed_set__ Alpha = new(); public static readonly L05_7_closed_set__ Beta = new(); } } } -public sealed partial class L05_8_closed_set__ : L05CB3c, IClosedSet { public static readonly L05_8_closed_set__ Alpha = new(); public static readonly L05_8_closed_set__ Beta = new(); } -public partial class L05Oc9 { public sealed partial class L05_9_closed_set__ : L05CE1, IClosedSet { public static readonly L05_9_closed_set__ Alpha = new(); public static readonly L05_9_closed_set__ Beta = new(); } } -public partial struct L05Os10 { public sealed partial class L05_10_closed_set__ : L05CE1, IClosedSet { public static readonly L05_10_closed_set__ Alpha = new(); public static readonly L05_10_closed_set__ Beta = new(); } } -public partial class L05Ot11 { public partial class Inner { public sealed partial class L05_11_closed_set__ : L05CE1, IClosedSet { public static readonly L05_11_closed_set__ Alpha = new(); public static readonly L05_11_closed_set__ Beta = new(); } } } -public sealed partial class L05_12_closed_set__ : L05CE1, IClosedSet { public static readonly L05_12_closed_set__ Alpha = new(); public static readonly L05_12_closed_set__ Beta = new(); } -public partial class L05Oc13 { public sealed partial class L05_13_closed_set__ : IL05Tag, IClosedSet { public static readonly L05_13_closed_set__ Alpha = new(); public static readonly L05_13_closed_set__ Beta = new(); string IL05Tag.Tag => "t"; } } -public sealed partial class L05_14_closed_set__ : IL05Tag, IClosedSet { public static readonly L05_14_closed_set__ Alpha = new(); public static readonly L05_14_closed_set__ Beta = new(); string IL05Tag.Tag => "t"; } -public partial struct L05Os15 { public sealed partial class L05_15_closed_set__ : IClosedSet { public static readonly L05_15_closed_set__ Alpha = new(); public static readonly L05_15_closed_set__ Beta = new(); } } -public partial class L05Oc16 { public readonly partial record struct L05_16_record_struct_DateOnly : IL05Tag, IValue { string IL05Tag.Tag => "t"; } } -public partial struct L05Os17 { public readonly partial record struct L05_17_record_struct_DateOnly : IL05Tag, IValue { string IL05Tag.Tag => "t"; } } -public partial class L05Ot18 { public partial class Inner { public readonly partial record struct L05_18_record_struct_DateOnly : IL05Tag, IValue { string IL05Tag.Tag => "t"; } } } -public partial class L05Oc19 { public readonly partial record struct L05_19_record_struct_DateTime : IValue { } } -public partial struct L05Os20 { public readonly partial record struct L05_20_record_struct_DateTimeOffset : IValue { } } -public partial struct L05Os21 { public readonly partial record struct L05_21_record_struct_Guid : IL05Tag, IValue { string IL05Tag.Tag => "t"; } } -public readonly partial record struct L05_22_record_struct_Guid : IL05Tag, IValue { string IL05Tag.Tag => "t"; } -public partial class L05Oc23 { public readonly partial record struct L05_23_record_struct_NodaTime_Instant : IL05Tag, IValue { string IL05Tag.Tag => "t"; } } -public partial struct L05Os24 { public readonly partial record struct L05_24_record_struct_NodaTime_Instant : IL05Tag, IValue { string IL05Tag.Tag => "t"; } } -public readonly partial record struct L05_25_record_struct_NodaTime_Instant : IL05Tag, IValue { string IL05Tag.Tag => "t"; } -public partial class L05Ot26 { public partial class Inner { public readonly partial record struct L05_26_record_struct_TimeOnly : IL05Tag, IValue { string IL05Tag.Tag => "t"; } } } -public readonly partial record struct L05_27_record_struct_TimeOnly : IL05Tag, IValue { string IL05Tag.Tag => "t"; } -public partial struct L05Os28 { public readonly partial record struct L05_28_record_struct_TimeOnly : IValue { } } -public partial class L05Oc29 { public readonly partial record struct L05_29_record_struct_TimeSpan : IL05Tag, IValue { string IL05Tag.Tag => "t"; } } -public partial class L05Oc30 { public readonly partial record struct L05_30_record_struct_TimeSpan : IValue { } } -public readonly partial record struct L05_31_record_struct_Uri : IL05Tag, IValue { string IL05Tag.Tag => "t"; } -public readonly partial record struct L05_32_record_struct_Uri : IValue { } -public partial struct L05Os33 { public readonly partial record struct L05_33_record_struct_bool : IL05Tag, IValue { string IL05Tag.Tag => "t"; } } -public partial class L05Ot34 { public partial class Inner { public readonly partial record struct L05_34_record_struct_bool : IL05Tag, IValue { string IL05Tag.Tag => "t"; } } } -public readonly partial record struct L05_35_record_struct_bool : IL05Tag, IValue { string IL05Tag.Tag => "t"; } -public readonly partial record struct L05_36_record_struct_byte : IL05Tag, IValue { string IL05Tag.Tag => "t"; } -public partial class L05Oc37 { public readonly partial record struct L05_37_record_struct_byte : IValue { } } -public readonly partial record struct L05_38_record_struct_char : IL05Tag, IValue { string IL05Tag.Tag => "t"; } -public partial struct L05Os39 { public readonly partial record struct L05_39_record_struct_char : IValue { } } -public partial class L05Oc40 { public readonly partial record struct L05_40_record_struct_decimal : IL05Tag, IValue { string IL05Tag.Tag => "t"; } } -public readonly partial record struct L05_41_record_struct_decimal : IL05Tag, IValue { string IL05Tag.Tag => "t"; } -public partial class L05Oc42 { public readonly partial record struct L05_42_record_struct_decimal : IValue { } } -public partial class L05Oc43 { public readonly partial record struct L05_43_record_struct_double : IL05Tag, IValue { string IL05Tag.Tag => "t"; } } -public partial struct L05Os44 { public readonly partial record struct L05_44_record_struct_double : IL05Tag, IValue { string IL05Tag.Tag => "t"; } } -public partial class L05Ot45 { public partial class Inner { public readonly partial record struct L05_45_record_struct_double : IL05Tag, IValue { string IL05Tag.Tag => "t"; } } } -public partial class L05Ot46 { public partial class Inner { public readonly partial record struct L05_46_record_struct_double : IValue { } } } -public partial class L05Oc47 { public readonly partial record struct L05_47_record_struct_enum : IL05Tag, IValue { string IL05Tag.Tag => "t"; } } -public partial struct L05Os48 { public readonly partial record struct L05_48_record_struct_enum : IL05Tag, IValue { string IL05Tag.Tag => "t"; } } -public readonly partial record struct L05_49_record_struct_flags_enum : IL05Tag, IValue { string IL05Tag.Tag => "t"; } -public readonly partial record struct L05_50_record_struct_flags_enum : IValue { } -public partial struct L05Os51 { public readonly partial record struct L05_51_record_struct_int : IL05Tag, IValue { string IL05Tag.Tag => "t"; } } -public partial class L05Ot52 { public partial class Inner { public readonly partial record struct L05_52_record_struct_int : IValue { } } } -public partial class L05Ot53 { public partial class Inner { public readonly partial record struct L05_53_record_struct_long : IValue { } } } -public readonly partial record struct L05_54_record_struct_long : IValue { } -public partial class L05Ot55 { public partial class Inner { public readonly partial record struct L05_55_record_struct_own_record_class : IL05Tag, IValue { string IL05Tag.Tag => "t"; } } } -public partial struct L05Os56 { public readonly partial record struct L05_56_record_struct_string : IL05Tag, IValue { string IL05Tag.Tag => "t"; } } -public readonly partial record struct L05_57_record_struct_string : IL05Tag, IValue { string IL05Tag.Tag => "t"; } -public partial class L05Oc58 { public readonly partial record struct L05_58_record_struct_string : IValue { } } -public partial class L05Oc59 { public readonly partial record struct L05_59_record_struct_ulong : IL05Tag, IValue { string IL05Tag.Tag => "t"; } } -public partial struct L05Os60 { public readonly partial record struct L05_60_record_struct_ulong : IL05Tag, IValue { string IL05Tag.Tag => "t"; } } -public partial struct L05Os61 { public sealed partial record L05_61_sealed_record_class_DateOnly : L05B2b, IValue { } } -public sealed partial record L05_62_sealed_record_class_DateOnly : L05B3c, IValue { } -public partial struct L05Os63 { public sealed partial record L05_63_sealed_record_class_DateOnly : IValue { } } -public sealed partial record L05_64_sealed_record_class_DateOnly : IValue { } -public sealed partial record L05_65_sealed_record_class_DateTime : L05B1, IValue { } -public partial class L05Oc66 { public sealed partial record L05_66_sealed_record_class_DateTime : L05B2b, IValue { } } -public partial struct L05Os67 { public sealed partial record L05_67_sealed_record_class_DateTime : L05B3c, IValue { } } -public partial class L05Ot68 { public partial class Inner { public sealed partial record L05_68_sealed_record_class_DateTime : L05B3c, IValue { } } } -public sealed partial record L05_69_sealed_record_class_DateTime : L05B3c, IValue { } -public partial struct L05Os70 { public sealed partial record L05_70_sealed_record_class_DateTime : L05E1, IValue { } } -public partial class L05Oc71 { public sealed partial record L05_71_sealed_record_class_DateTimeOffset : L05B1, IValue { } } -public sealed partial record L05_72_sealed_record_class_DateTimeOffset : L05B2b, IValue { } -public partial class L05Oc73 { public sealed partial record L05_73_sealed_record_class_DateTimeOffset : L05B3c, IValue { } } -public partial class L05Ot74 { public partial class Inner { public sealed partial record L05_74_sealed_record_class_DateTimeOffset : L05B3c, IValue { } } } -public sealed partial record L05_75_sealed_record_class_DateTimeOffset : L05B3c, IValue { } -public partial class L05Oc76 { public sealed partial record L05_76_sealed_record_class_DateTimeOffset : L05E1, IValue { } } -public sealed partial record L05_77_sealed_record_class_DateTimeOffset : L05E1, IValue { } -public partial class L05Oc78 { public sealed partial record L05_78_sealed_record_class_Guid : L05B1, IValue { } } -public partial struct L05Os79 { public sealed partial record L05_79_sealed_record_class_Guid : L05B1, IValue { } } -public sealed partial record L05_80_sealed_record_class_Guid : L05B2b, IValue { } -public sealed partial record L05_81_sealed_record_class_Guid : L05B3c, IValue { } -public partial class L05Oc82 { public sealed partial record L05_82_sealed_record_class_Guid : L05E1, IValue { } } -public partial class L05Ot83 { public partial class Inner { public sealed partial record L05_83_sealed_record_class_Guid : L05E1, IValue { } } } -public partial class L05Ot84 { public partial class Inner { public sealed partial record L05_84_sealed_record_class_NodaTime_Instant : L05B1, IValue { } } } -public partial struct L05Os85 { public sealed partial record L05_85_sealed_record_class_NodaTime_Instant : L05E1, IValue { } } -public partial struct L05Os86 { public sealed partial record L05_86_sealed_record_class_NodaTime_Instant : IValue { } } -public partial struct L05Os87 { public sealed partial record L05_87_sealed_record_class_TimeOnly : L05B3c, IValue { } } -public partial class L05Ot88 { public partial class Inner { public sealed partial record L05_88_sealed_record_class_TimeOnly : L05B3c, IValue { } } } -public partial class L05Oc89 { public sealed partial record L05_89_sealed_record_class_TimeSpan : L05B1, IValue { } } -public partial class L05Ot90 { public partial class Inner { public sealed partial record L05_90_sealed_record_class_TimeSpan : L05B1, IValue { } } } -public sealed partial record L05_91_sealed_record_class_TimeSpan : L05B2b, IValue { } -public sealed partial record L05_92_sealed_record_class_TimeSpan : L05B3c, IValue { } -public partial struct L05Os93 { public sealed partial record L05_93_sealed_record_class_TimeSpan : IL05Tag, IValue { string IL05Tag.Tag => "t"; } } -public partial class L05Oc94 { public sealed partial record L05_94_sealed_record_class_TimeSpan : IValue { } } -public partial class L05Oc95 { public sealed partial record L05_95_sealed_record_class_Uri : L05B1, IValue { } } -public partial struct L05Os96 { public sealed partial record L05_96_sealed_record_class_Uri : L05B2b, IValue { } } -public partial struct L05Os97 { public sealed partial record L05_97_sealed_record_class_Uri : L05B3c, IValue { } } -public partial class L05Ot98 { public partial class Inner { public sealed partial record L05_98_sealed_record_class_Uri : L05E1, IValue { } } } -public partial class L05Oc99 { public sealed partial record L05_99_sealed_record_class_bool : L05B2b, IValue { } } -public partial class L05Ot100 { public partial class Inner { public sealed partial record L05_100_sealed_record_class_bool : L05B2b, IValue { } } } -public partial struct L05Os101 { public sealed partial record L05_101_sealed_record_class_bool : L05B3c, IValue { } } -public partial class L05Ot102 { public partial class Inner { public sealed partial record L05_102_sealed_record_class_bool : L05E1, IValue { } } } -public partial class L05Ot103 { public partial class Inner { public sealed partial record L05_103_sealed_record_class_bool : IL05Tag, IValue { string IL05Tag.Tag => "t"; } } } -public partial class L05Ot104 { public partial class Inner { public sealed partial record L05_104_sealed_record_class_byte : L05B1, IValue { } } } -public partial struct L05Os105 { public sealed partial record L05_105_sealed_record_class_byte : L05B2b, IValue { } } -public partial class L05Oc106 { public sealed partial record L05_106_sealed_record_class_byte : L05B3c, IValue { } } -public sealed partial record L05_107_sealed_record_class_byte : L05B3c, IValue { } -public partial class L05Ot108 { public partial class Inner { public sealed partial record L05_108_sealed_record_class_byte : IL05Tag, IValue { string IL05Tag.Tag => "t"; } } } -public sealed partial record L05_109_sealed_record_class_byte : IL05Tag, IValue { string IL05Tag.Tag => "t"; } -public sealed partial record L05_110_sealed_record_class_byte : IValue { } -public partial class L05Ot111 { public partial class Inner { public sealed partial record L05_111_sealed_record_class_char : L05B1, IValue { } } } -public sealed partial record L05_112_sealed_record_class_char : L05B2b, IValue { } -public sealed partial record L05_113_sealed_record_class_char : L05E1, IValue { } -public partial class L05Ot114 { public partial class Inner { public sealed partial record L05_114_sealed_record_class_char : IL05Tag, IValue { string IL05Tag.Tag => "t"; } } } -public partial class L05Oc115 { public sealed partial record L05_115_sealed_record_class_decimal : L05B1, IValue { } } -public partial struct L05Os116 { public sealed partial record L05_116_sealed_record_class_decimal : L05B3c, IValue { } } -public partial class L05Oc117 { public sealed partial record L05_117_sealed_record_class_decimal : L05E1, IValue { } } -public sealed partial record L05_118_sealed_record_class_decimal : L05E1, IValue { } -public partial class L05Ot119 { public partial class Inner { public sealed partial record L05_119_sealed_record_class_double : L05B1, IValue { } } } -public sealed partial record L05_120_sealed_record_class_double : L05B2b, IValue { } -public partial class L05Oc121 { public sealed partial record L05_121_sealed_record_class_double : L05B3c, IValue { } } -public partial class L05Oc122 { public sealed partial record L05_122_sealed_record_class_double : L05E1, IValue { } } -public partial struct L05Os123 { public sealed partial record L05_123_sealed_record_class_enum : L05B1, IValue { } } -public partial class L05Ot124 { public partial class Inner { public sealed partial record L05_124_sealed_record_class_enum : L05B2b, IValue { } } } -public partial class L05Ot125 { public partial class Inner { public sealed partial record L05_125_sealed_record_class_enum : L05B3c, IValue { } } } -public partial class L05Ot126 { public partial class Inner { public sealed partial record L05_126_sealed_record_class_enum : L05E1, IValue { } } } -public partial struct L05Os127 { public sealed partial record L05_127_sealed_record_class_enum : IL05Tag, IValue { string IL05Tag.Tag => "t"; } } -public partial class L05Oc128 { public sealed partial record L05_128_sealed_record_class_enum : IValue { } } -public partial class L05Oc129 { public sealed partial record L05_129_sealed_record_class_flags_enum : L05B1, IValue { } } -public partial class L05Ot130 { public partial class Inner { public sealed partial record L05_130_sealed_record_class_flags_enum : L05B1, IValue { } } } -public sealed partial record L05_131_sealed_record_class_flags_enum : L05B2b, IValue { } -public sealed partial record L05_132_sealed_record_class_flags_enum : L05B3c, IValue { } -public partial class L05Ot133 { public partial class Inner { public sealed partial record L05_133_sealed_record_class_flags_enum : L05E1, IValue { } } } -public partial struct L05Os134 { public sealed partial record L05_134_sealed_record_class_flags_enum : IL05Tag, IValue { string IL05Tag.Tag => "t"; } } -public sealed partial record L05_135_sealed_record_class_int : L05B2b, IValue { } -public partial struct L05Os136 { public sealed partial record L05_136_sealed_record_class_int : L05E1, IValue { } } -public partial class L05Ot137 { public partial class Inner { public sealed partial record L05_137_sealed_record_class_long : L05B2b, IValue { } } } -public partial class L05Ot138 { public partial class Inner { public sealed partial record L05_138_sealed_record_class_long : L05B3c, IValue { } } } -public partial struct L05Os139 { public sealed partial record L05_139_sealed_record_class_long : L05E1, IValue { } } -public partial class L05Ot140 { public partial class Inner { public sealed partial record L05_140_sealed_record_class_own_record_class : L05B2b, IValue { } } } -public partial struct L05Os141 { public sealed partial record L05_141_sealed_record_class_own_record_class : L05E1, IValue { } } -public partial class L05Ot142 { public partial class Inner { public sealed partial record L05_142_sealed_record_class_own_record_class : L05E1, IValue { } } } -public partial struct L05Os143 { public sealed partial record L05_143_sealed_record_class_string : L05B1, IValue { } } -public sealed partial record L05_144_sealed_record_class_string : L05B3c, IValue { } -public partial class L05Ot145 { public partial class Inner { public sealed partial record L05_145_sealed_record_class_string : L05E1, IValue { } } } -public partial class L05Ot146 { public partial class Inner { public sealed partial record L05_146_sealed_record_class_ulong : L05B3c, IValue { } } } -public partial class L05Ot147 { public partial class Inner { public sealed partial record L05_147_sealed_record_class_ulong : L05E1, IValue { } } } -public sealed partial record L05_148_sealed_record_class_ulong : L05E1, IValue { } -public partial struct L05Os149 { public sealed partial record L05_149_validated_class_DateOnly : L05B1, IValidatedValue { public static Result Create(DateOnly value) => new L05_149_validated_class_DateOnly(value); } } -public sealed partial record L05_150_validated_class_DateOnly : L05E1, IValidatedValue { public static Result Create(DateOnly value) => new L05_150_validated_class_DateOnly(value); } -public sealed partial record L05_151_validated_class_DateOnly : IL05Tag, IValidatedValue { public static Result Create(DateOnly value) => new L05_151_validated_class_DateOnly(value); string IL05Tag.Tag => "t"; } -public partial class L05Ot152 { public partial class Inner { public sealed partial record L05_152_validated_class_DateOnly : IValidatedValue { public static Result Create(DateOnly value) => new L05_152_validated_class_DateOnly(value); } } } -public partial struct L05Os153 { public sealed partial record L05_153_validated_class_DateTime : L05B1, IValidatedValue { public static Result Create(DateTime value) => new L05_153_validated_class_DateTime(value); } } -public partial class L05Ot154 { public partial class Inner { public sealed partial record L05_154_validated_class_DateTime : L05B1, IValidatedValue { public static Result Create(DateTime value) => new L05_154_validated_class_DateTime(value); } } } -public sealed partial record L05_155_validated_class_DateTime : L05B2b, IValidatedValue { public static Result Create(DateTime value) => new L05_155_validated_class_DateTime(value); } -public partial class L05Oc156 { public sealed partial record L05_156_validated_class_DateTime : L05B3c, IValidatedValue { public static Result Create(DateTime value) => new L05_156_validated_class_DateTime(value); } } -public partial struct L05Os157 { public sealed partial record L05_157_validated_class_DateTime : L05E1, IValidatedValue { public static Result Create(DateTime value) => new L05_157_validated_class_DateTime(value); } } -public partial class L05Oc158 { public sealed partial record L05_158_validated_class_DateTimeOffset : IL05Tag, IValidatedValue { public static Result Create(DateTimeOffset value) => new L05_158_validated_class_DateTimeOffset(value); string IL05Tag.Tag => "t"; } } -public partial class L05Oc159 { public sealed partial record L05_159_validated_class_Guid : L05B2b, IValidatedValue { public static Result Create(Guid value) => new L05_159_validated_class_Guid(value); } } -public partial class L05Ot160 { public partial class Inner { public sealed partial record L05_160_validated_class_Guid : L05B2b, IValidatedValue { public static Result Create(Guid value) => new L05_160_validated_class_Guid(value); } } } -public partial class L05Oc161 { public sealed partial record L05_161_validated_class_Guid : IL05Tag, IValidatedValue { public static Result Create(Guid value) => new L05_161_validated_class_Guid(value); string IL05Tag.Tag => "t"; } } -public partial class L05Ot162 { public partial class Inner { public sealed partial record L05_162_validated_class_NodaTime_Instant : L05B2b, IValidatedValue { public static Result Create(global::NodaTime.Instant value) => new L05_162_validated_class_NodaTime_Instant(value); } } } -public partial class L05Oc163 { public sealed partial record L05_163_validated_class_NodaTime_Instant : L05B3c, IValidatedValue { public static Result Create(global::NodaTime.Instant value) => new L05_163_validated_class_NodaTime_Instant(value); } } -public partial struct L05Os164 { public sealed partial record L05_164_validated_class_NodaTime_Instant : L05B3c, IValidatedValue { public static Result Create(global::NodaTime.Instant value) => new L05_164_validated_class_NodaTime_Instant(value); } } -public partial class L05Ot165 { public partial class Inner { public sealed partial record L05_165_validated_class_NodaTime_Instant : L05B3c, IValidatedValue { public static Result Create(global::NodaTime.Instant value) => new L05_165_validated_class_NodaTime_Instant(value); } } } -public partial class L05Oc166 { public sealed partial record L05_166_validated_class_NodaTime_Instant : L05E1, IValidatedValue { public static Result Create(global::NodaTime.Instant value) => new L05_166_validated_class_NodaTime_Instant(value); } } -public sealed partial record L05_167_validated_class_NodaTime_Instant : L05E1, IValidatedValue { public static Result Create(global::NodaTime.Instant value) => new L05_167_validated_class_NodaTime_Instant(value); } -public partial class L05Oc168 { public sealed partial record L05_168_validated_class_TimeOnly : L05B1, IValidatedValue { public static Result Create(TimeOnly value) => new L05_168_validated_class_TimeOnly(value); } } -public partial struct L05Os169 { public sealed partial record L05_169_validated_class_TimeOnly : L05B1, IValidatedValue { public static Result Create(TimeOnly value) => new L05_169_validated_class_TimeOnly(value); } } -public sealed partial record L05_170_validated_class_TimeOnly : L05B2b, IValidatedValue { public static Result Create(TimeOnly value) => new L05_170_validated_class_TimeOnly(value); } -public partial class L05Oc171 { public sealed partial record L05_171_validated_class_TimeOnly : L05E1, IValidatedValue { public static Result Create(TimeOnly value) => new L05_171_validated_class_TimeOnly(value); } } -public sealed partial record L05_172_validated_class_TimeOnly : IValidatedValue { public static Result Create(TimeOnly value) => new L05_172_validated_class_TimeOnly(value); } -public sealed partial record L05_173_validated_class_TimeSpan : L05B1, IValidatedValue { public static Result Create(TimeSpan value) => new L05_173_validated_class_TimeSpan(value); } -public partial class L05Oc174 { public sealed partial record L05_174_validated_class_TimeSpan : L05B2b, IValidatedValue { public static Result Create(TimeSpan value) => new L05_174_validated_class_TimeSpan(value); } } -public partial struct L05Os175 { public sealed partial record L05_175_validated_class_TimeSpan : L05B2b, IValidatedValue { public static Result Create(TimeSpan value) => new L05_175_validated_class_TimeSpan(value); } } -public partial class L05Ot176 { public partial class Inner { public sealed partial record L05_176_validated_class_TimeSpan : L05E1, IValidatedValue { public static Result Create(TimeSpan value) => new L05_176_validated_class_TimeSpan(value); } } } -public partial struct L05Os177 { public sealed partial record L05_177_validated_class_Uri : L05B1, IValidatedValue { public static Result Create(Uri value) => new L05_177_validated_class_Uri(value); } } -public partial class L05Ot178 { public partial class Inner { public sealed partial record L05_178_validated_class_Uri : L05B3c, IValidatedValue { public static Result Create(Uri value) => new L05_178_validated_class_Uri(value); } } } -public sealed partial record L05_179_validated_class_Uri : IL05Tag, IValidatedValue { public static Result Create(Uri value) => new L05_179_validated_class_Uri(value); string IL05Tag.Tag => "t"; } -public partial class L05Oc180 { public sealed partial record L05_180_validated_class_Uri : IValidatedValue { public static Result Create(Uri value) => new L05_180_validated_class_Uri(value); } } -public partial struct L05Os181 { public sealed partial record L05_181_validated_class_bool : L05B1, IValidatedValue { public static Result Create(bool value) => new L05_181_validated_class_bool(value); } } -public partial class L05Oc182 { public sealed partial record L05_182_validated_class_bool : L05E1, IValidatedValue { public static Result Create(bool value) => new L05_182_validated_class_bool(value); } } -public partial struct L05Os183 { public sealed partial record L05_183_validated_class_bool : IL05Tag, IValidatedValue { public static Result Create(bool value) => new L05_183_validated_class_bool(value); string IL05Tag.Tag => "t"; } } -public partial struct L05Os184 { public sealed partial record L05_184_validated_class_byte : L05B2b, IValidatedValue { public static Result Create(byte value) => new L05_184_validated_class_byte(value); } } -public partial class L05Oc185 { public sealed partial record L05_185_validated_class_byte : L05E1, IValidatedValue { public static Result Create(byte value) => new L05_185_validated_class_byte(value); } } -public partial class L05Ot186 { public partial class Inner { public sealed partial record L05_186_validated_class_byte : L05E1, IValidatedValue { public static Result Create(byte value) => new L05_186_validated_class_byte(value); } } } -public partial struct L05Os187 { public sealed partial record L05_187_validated_class_byte : IL05Tag, IValidatedValue { public static Result Create(byte value) => new L05_187_validated_class_byte(value); string IL05Tag.Tag => "t"; } } -public partial class L05Oc188 { public sealed partial record L05_188_validated_class_char : L05B2b, IValidatedValue { public static Result Create(char value) => new L05_188_validated_class_char(value); } } -public partial class L05Oc189 { public sealed partial record L05_189_validated_class_char : L05B3c, IValidatedValue { public static Result Create(char value) => new L05_189_validated_class_char(value); } } -public partial class L05Ot190 { public partial class Inner { public sealed partial record L05_190_validated_class_char : L05B3c, IValidatedValue { public static Result Create(char value) => new L05_190_validated_class_char(value); } } } -public partial class L05Ot191 { public partial class Inner { public sealed partial record L05_191_validated_class_char : L05E1, IValidatedValue { public static Result Create(char value) => new L05_191_validated_class_char(value); } } } -public sealed partial record L05_192_validated_class_char : L05E1, IValidatedValue { public static Result Create(char value) => new L05_192_validated_class_char(value); } -public sealed partial record L05_193_validated_class_decimal : L05B1, IValidatedValue { public static Result Create(decimal value) => new L05_193_validated_class_decimal(value); } -public partial class L05Ot194 { public partial class Inner { public sealed partial record L05_194_validated_class_decimal : L05B2b, IValidatedValue { public static Result Create(decimal value) => new L05_194_validated_class_decimal(value); } } } -public sealed partial record L05_195_validated_class_decimal : L05B2b, IValidatedValue { public static Result Create(decimal value) => new L05_195_validated_class_decimal(value); } -public partial struct L05Os196 { public sealed partial record L05_196_validated_class_decimal : IValidatedValue { public static Result Create(decimal value) => new L05_196_validated_class_decimal(value); } } -public sealed partial record L05_197_validated_class_decimal : IValidatedValue { public static Result Create(decimal value) => new L05_197_validated_class_decimal(value); } -public sealed partial record L05_198_validated_class_double : L05B2b, IValidatedValue { public static Result Create(double value) => new L05_198_validated_class_double(value); } -public partial struct L05Os199 { public sealed partial record L05_199_validated_class_double : L05B3c, IValidatedValue { public static Result Create(double value) => new L05_199_validated_class_double(value); } } -public partial struct L05Os200 { public sealed partial record L05_200_validated_class_double : IL05Tag, IValidatedValue { public static Result Create(double value) => new L05_200_validated_class_double(value); string IL05Tag.Tag => "t"; } } -public partial class L05Ot201 { public partial class Inner { public sealed partial record L05_201_validated_class_enum : L05B1, IValidatedValue { public static Result Create(PEnum value) => new L05_201_validated_class_enum(value); } } } -public sealed partial record L05_202_validated_class_enum : L05B2b, IValidatedValue { public static Result Create(PEnum value) => new L05_202_validated_class_enum(value); } -public partial class L05Oc203 { public sealed partial record L05_203_validated_class_flags_enum : L05E1, IValidatedValue { public static Result Create(PFlags value) => new L05_203_validated_class_flags_enum(value); } } -public partial class L05Ot204 { public partial class Inner { public sealed partial record L05_204_validated_class_flags_enum : IL05Tag, IValidatedValue { public static Result Create(PFlags value) => new L05_204_validated_class_flags_enum(value); string IL05Tag.Tag => "t"; } } } -public partial struct L05Os205 { public sealed partial record L05_205_validated_class_int : L05B1, IValidatedValue { public static Result Create(int value) => new L05_205_validated_class_int(value); } } -public partial class L05Oc206 { public sealed partial record L05_206_validated_class_int : L05B3c, IValidatedValue { public static Result Create(int value) => new L05_206_validated_class_int(value); } } -public partial class L05Oc207 { public sealed partial record L05_207_validated_class_int : IL05Tag, IValidatedValue { public static Result Create(int value) => new L05_207_validated_class_int(value); string IL05Tag.Tag => "t"; } } -public partial class L05Oc208 { public sealed partial record L05_208_validated_class_long : L05B1, IValidatedValue { public static Result Create(long value) => new L05_208_validated_class_long(value); } } -public partial class L05Ot209 { public partial class Inner { public sealed partial record L05_209_validated_class_long : L05B2b, IValidatedValue { public static Result Create(long value) => new L05_209_validated_class_long(value); } } } -public partial class L05Oc210 { public sealed partial record L05_210_validated_class_long : L05E1, IValidatedValue { public static Result Create(long value) => new L05_210_validated_class_long(value); } } -public partial struct L05Os211 { public sealed partial record L05_211_validated_class_long : L05E1, IValidatedValue { public static Result Create(long value) => new L05_211_validated_class_long(value); } } -public partial class L05Ot212 { public partial class Inner { public sealed partial record L05_212_validated_class_long : IL05Tag, IValidatedValue { public static Result Create(long value) => new L05_212_validated_class_long(value); string IL05Tag.Tag => "t"; } } } -public partial struct L05Os213 { public sealed partial record L05_213_validated_class_own_record_class : L05B1, IValidatedValue { public static Result Create(PRec value) => new L05_213_validated_class_own_record_class(value); } } -public partial class L05Ot214 { public partial class Inner { public sealed partial record L05_214_validated_class_own_record_class : L05B1, IValidatedValue { public static Result Create(PRec value) => new L05_214_validated_class_own_record_class(value); } } } -public sealed partial record L05_215_validated_class_own_record_class : L05B2b, IValidatedValue { public static Result Create(PRec value) => new L05_215_validated_class_own_record_class(value); } -public partial struct L05Os216 { public sealed partial record L05_216_validated_class_own_record_class : L05B3c, IValidatedValue { public static Result Create(PRec value) => new L05_216_validated_class_own_record_class(value); } } -public partial class L05Ot217 { public partial class Inner { public sealed partial record L05_217_validated_class_own_record_class : L05B3c, IValidatedValue { public static Result Create(PRec value) => new L05_217_validated_class_own_record_class(value); } } } -public partial struct L05Os218 { public sealed partial record L05_218_validated_class_own_record_class : L05E1, IValidatedValue { public static Result Create(PRec value) => new L05_218_validated_class_own_record_class(value); } } -public partial class L05Oc219 { public sealed partial record L05_219_validated_class_own_record_class : IValidatedValue { public static Result Create(PRec value) => new L05_219_validated_class_own_record_class(value); } } -public partial struct L05Os220 { public sealed partial record L05_220_validated_class_string : L05B1, IValidatedValue { public static Result Create(string value) => new L05_220_validated_class_string(value); } } -public partial class L05Ot221 { public partial class Inner { public sealed partial record L05_221_validated_class_string : L05B2b, IValidatedValue { public static Result Create(string value) => new L05_221_validated_class_string(value); } } } -public partial class L05Oc222 { public sealed partial record L05_222_validated_class_string : L05B3c, IValidatedValue { public static Result Create(string value) => new L05_222_validated_class_string(value); } } -public sealed partial record L05_223_validated_class_string : IL05Tag, IValidatedValue { public static Result Create(string value) => new L05_223_validated_class_string(value); string IL05Tag.Tag => "t"; } -public partial struct L05Os224 { public sealed partial record L05_224_validated_class_ulong : L05B1, IValidatedValue { public static Result Create(ulong value) => new L05_224_validated_class_ulong(value); } } -public sealed partial record L05_225_validated_class_ulong : L05B1, IValidatedValue { public static Result Create(ulong value) => new L05_225_validated_class_ulong(value); } -public sealed partial record L05_226_validated_class_ulong : L05B2b, IValidatedValue { public static Result Create(ulong value) => new L05_226_validated_class_ulong(value); } -public partial class L05Oc227 { public sealed partial record L05_227_validated_class_ulong : L05B3c, IValidatedValue { public static Result Create(ulong value) => new L05_227_validated_class_ulong(value); } } -public partial class L05Oc228 { public sealed partial record L05_228_validated_class_ulong : IL05Tag, IValidatedValue { public static Result Create(ulong value) => new L05_228_validated_class_ulong(value); string IL05Tag.Tag => "t"; } } -public partial struct L05Os229 { public sealed partial record L05_229_validated_class_ulong : IL05Tag, IValidatedValue { public static Result Create(ulong value) => new L05_229_validated_class_ulong(value); string IL05Tag.Tag => "t"; } } -public partial struct L05Os230 { public sealed partial record L05_230_validated_class_ulong : IValidatedValue { public static Result Create(ulong value) => new L05_230_validated_class_ulong(value); } } -public partial class L05Ot231 { public partial class Inner { public readonly partial record struct L05_231_validated_struct_DateOnly : IL05Tag, IValidatedValue { public static Result Create(DateOnly value) => new L05_231_validated_struct_DateOnly(value); string IL05Tag.Tag => "t"; } } } -public readonly partial record struct L05_232_validated_struct_DateOnly : IValidatedValue { public static Result Create(DateOnly value) => new L05_232_validated_struct_DateOnly(value); } -public partial class L05Ot233 { public partial class Inner { public readonly partial record struct L05_233_validated_struct_DateTime : IL05Tag, IValidatedValue { public static Result Create(DateTime value) => new L05_233_validated_struct_DateTime(value); string IL05Tag.Tag => "t"; } } } -public partial struct L05Os234 { public readonly partial record struct L05_234_validated_struct_DateTimeOffset : IValidatedValue { public static Result Create(DateTimeOffset value) => new L05_234_validated_struct_DateTimeOffset(value); } } -public readonly partial record struct L05_235_validated_struct_DateTimeOffset : IValidatedValue { public static Result Create(DateTimeOffset value) => new L05_235_validated_struct_DateTimeOffset(value); } -public partial class L05Oc236 { public readonly partial record struct L05_236_validated_struct_Guid : IValidatedValue { public static Result Create(Guid value) => new L05_236_validated_struct_Guid(value); } } -public partial struct L05Os237 { public readonly partial record struct L05_237_validated_struct_Guid : IValidatedValue { public static Result Create(Guid value) => new L05_237_validated_struct_Guid(value); } } -public readonly partial record struct L05_238_validated_struct_NodaTime_Instant : IValidatedValue { public static Result Create(global::NodaTime.Instant value) => new L05_238_validated_struct_NodaTime_Instant(value); } -public partial class L05Oc239 { public readonly partial record struct L05_239_validated_struct_TimeOnly : IL05Tag, IValidatedValue { public static Result Create(TimeOnly value) => new L05_239_validated_struct_TimeOnly(value); string IL05Tag.Tag => "t"; } } -public readonly partial record struct L05_240_validated_struct_TimeOnly : IValidatedValue { public static Result Create(TimeOnly value) => new L05_240_validated_struct_TimeOnly(value); } -public partial class L05Oc241 { public readonly partial record struct L05_241_validated_struct_TimeSpan : IValidatedValue { public static Result Create(TimeSpan value) => new L05_241_validated_struct_TimeSpan(value); } } -public partial struct L05Os242 { public readonly partial record struct L05_242_validated_struct_Uri : IL05Tag, IValidatedValue { public static Result Create(Uri value) => new L05_242_validated_struct_Uri(value); string IL05Tag.Tag => "t"; } } -public readonly partial record struct L05_243_validated_struct_Uri : IL05Tag, IValidatedValue { public static Result Create(Uri value) => new L05_243_validated_struct_Uri(value); string IL05Tag.Tag => "t"; } -public partial class L05Oc244 { public readonly partial record struct L05_244_validated_struct_Uri : IValidatedValue { public static Result Create(Uri value) => new L05_244_validated_struct_Uri(value); } } -public partial class L05Oc245 { public readonly partial record struct L05_245_validated_struct_bool : IL05Tag, IValidatedValue { public static Result Create(bool value) => new L05_245_validated_struct_bool(value); string IL05Tag.Tag => "t"; } } -public partial class L05Ot246 { public partial class Inner { public readonly partial record struct L05_246_validated_struct_bool : IValidatedValue { public static Result Create(bool value) => new L05_246_validated_struct_bool(value); } } } -public readonly partial record struct L05_247_validated_struct_byte : IL05Tag, IValidatedValue { public static Result Create(byte value) => new L05_247_validated_struct_byte(value); string IL05Tag.Tag => "t"; } -public partial class L05Oc248 { public readonly partial record struct L05_248_validated_struct_char : IL05Tag, IValidatedValue { public static Result Create(char value) => new L05_248_validated_struct_char(value); string IL05Tag.Tag => "t"; } } -public readonly partial record struct L05_249_validated_struct_decimal : IL05Tag, IValidatedValue { public static Result Create(decimal value) => new L05_249_validated_struct_decimal(value); string IL05Tag.Tag => "t"; } -public partial class L05Ot250 { public partial class Inner { public readonly partial record struct L05_250_validated_struct_double : IL05Tag, IValidatedValue { public static Result Create(double value) => new L05_250_validated_struct_double(value); string IL05Tag.Tag => "t"; } } } -public partial class L05Ot251 { public partial class Inner { public readonly partial record struct L05_251_validated_struct_double : IValidatedValue { public static Result Create(double value) => new L05_251_validated_struct_double(value); } } } -public partial class L05Oc252 { public readonly partial record struct L05_252_validated_struct_enum : IValidatedValue { public static Result Create(PEnum value) => new L05_252_validated_struct_enum(value); } } -public partial struct L05Os253 { public readonly partial record struct L05_253_validated_struct_enum : IValidatedValue { public static Result Create(PEnum value) => new L05_253_validated_struct_enum(value); } } -public partial class L05Ot254 { public partial class Inner { public readonly partial record struct L05_254_validated_struct_enum : IValidatedValue { public static Result Create(PEnum value) => new L05_254_validated_struct_enum(value); } } } -public partial class L05Ot255 { public partial class Inner { public readonly partial record struct L05_255_validated_struct_flags_enum : IValidatedValue { public static Result Create(PFlags value) => new L05_255_validated_struct_flags_enum(value); } } } -public readonly partial record struct L05_256_validated_struct_flags_enum : IValidatedValue { public static Result Create(PFlags value) => new L05_256_validated_struct_flags_enum(value); } -public partial struct L05Os257 { public readonly partial record struct L05_257_validated_struct_int : IL05Tag, IValidatedValue { public static Result Create(int value) => new L05_257_validated_struct_int(value); string IL05Tag.Tag => "t"; } } -public partial class L05Ot258 { public partial class Inner { public readonly partial record struct L05_258_validated_struct_int : IL05Tag, IValidatedValue { public static Result Create(int value) => new L05_258_validated_struct_int(value); string IL05Tag.Tag => "t"; } } } -public partial class L05Ot259 { public partial class Inner { public readonly partial record struct L05_259_validated_struct_int : IValidatedValue { public static Result Create(int value) => new L05_259_validated_struct_int(value); } } } -public readonly partial record struct L05_260_validated_struct_int : IValidatedValue { public static Result Create(int value) => new L05_260_validated_struct_int(value); } -public partial struct L05Os261 { public readonly partial record struct L05_261_validated_struct_long : IL05Tag, IValidatedValue { public static Result Create(long value) => new L05_261_validated_struct_long(value); string IL05Tag.Tag => "t"; } } -public readonly partial record struct L05_262_validated_struct_long : IL05Tag, IValidatedValue { public static Result Create(long value) => new L05_262_validated_struct_long(value); string IL05Tag.Tag => "t"; } -public readonly partial record struct L05_263_validated_struct_own_record_class : IL05Tag, IValidatedValue { public static Result Create(PRec value) => new L05_263_validated_struct_own_record_class(value); string IL05Tag.Tag => "t"; } -public partial struct L05Os264 { public readonly partial record struct L05_264_validated_struct_own_record_class : IValidatedValue { public static Result Create(PRec value) => new L05_264_validated_struct_own_record_class(value); } } -public partial class L05Ot265 { public partial class Inner { public readonly partial record struct L05_265_validated_struct_string : IL05Tag, IValidatedValue { public static Result Create(string value) => new L05_265_validated_struct_string(value); string IL05Tag.Tag => "t"; } } } -public partial class L05Oc266 { public readonly partial record struct L05_266_validated_struct_string : IValidatedValue { public static Result Create(string value) => new L05_266_validated_struct_string(value); } } -public readonly partial record struct L05_267_validated_struct_string : IValidatedValue { public static Result Create(string value) => new L05_267_validated_struct_string(value); } -public readonly partial record struct L05_268_validated_struct_ulong : IL05Tag, IValidatedValue { public static Result Create(ulong value) => new L05_268_validated_struct_ulong(value); string IL05Tag.Tag => "t"; } -public readonly partial record struct L05_269_validated_struct_ulong : IValidatedValue { public static Result Create(ulong value) => new L05_269_validated_struct_ulong(value); } - -public static partial class L05Registry -{ - static partial void Fill() - { - Add("closed-set", "-", "base-depth-1", "nested-in-class", typeof(L05Oc0.L05_0_closed_set__), () => L05Oc0.L05_0_closed_set__.Alpha, () => L05Oc0.L05_0_closed_set__.All.First(m => m.Name == "Alpha"), () => L05Oc0.L05_0_closed_set__.Beta); - Add("closed-set", "-", "base-depth-1", "nested-two-levels", typeof(L05Ot1.Inner.L05_1_closed_set__), () => L05Ot1.Inner.L05_1_closed_set__.Alpha, () => L05Ot1.Inner.L05_1_closed_set__.All.First(m => m.Name == "Alpha"), () => L05Ot1.Inner.L05_1_closed_set__.Beta); - Add("closed-set", "-", "base-depth-2", "nested-in-class", typeof(L05Oc2.L05_2_closed_set__), () => L05Oc2.L05_2_closed_set__.Alpha, () => L05Oc2.L05_2_closed_set__.All.First(m => m.Name == "Alpha"), () => L05Oc2.L05_2_closed_set__.Beta); - Add("closed-set", "-", "base-depth-2", "nested-in-struct", typeof(L05Os3.L05_3_closed_set__), () => L05Os3.L05_3_closed_set__.Alpha, () => L05Os3.L05_3_closed_set__.All.First(m => m.Name == "Alpha"), () => L05Os3.L05_3_closed_set__.Beta); - Add("closed-set", "-", "base-depth-2", "top-level", typeof(L05_4_closed_set__), () => L05_4_closed_set__.Alpha, () => L05_4_closed_set__.All.First(m => m.Name == "Alpha"), () => L05_4_closed_set__.Beta); - Add("closed-set", "-", "base-depth-3", "nested-in-class", typeof(L05Oc5.L05_5_closed_set__), () => L05Oc5.L05_5_closed_set__.Alpha, () => L05Oc5.L05_5_closed_set__.All.First(m => m.Name == "Alpha"), () => L05Oc5.L05_5_closed_set__.Beta); - Add("closed-set", "-", "base-depth-3", "nested-in-struct", typeof(L05Os6.L05_6_closed_set__), () => L05Os6.L05_6_closed_set__.Alpha, () => L05Os6.L05_6_closed_set__.All.First(m => m.Name == "Alpha"), () => L05Os6.L05_6_closed_set__.Beta); - Add("closed-set", "-", "base-depth-3", "nested-two-levels", typeof(L05Ot7.Inner.L05_7_closed_set__), () => L05Ot7.Inner.L05_7_closed_set__.Alpha, () => L05Ot7.Inner.L05_7_closed_set__.All.First(m => m.Name == "Alpha"), () => L05Ot7.Inner.L05_7_closed_set__.Beta); - Add("closed-set", "-", "base-depth-3", "top-level", typeof(L05_8_closed_set__), () => L05_8_closed_set__.Alpha, () => L05_8_closed_set__.All.First(m => m.Name == "Alpha"), () => L05_8_closed_set__.Beta); - Add("closed-set", "-", "base-explicit-IEquatable", "nested-in-class", typeof(L05Oc9.L05_9_closed_set__), () => L05Oc9.L05_9_closed_set__.Alpha, () => L05Oc9.L05_9_closed_set__.All.First(m => m.Name == "Alpha"), () => L05Oc9.L05_9_closed_set__.Beta); - Add("closed-set", "-", "base-explicit-IEquatable", "nested-in-struct", typeof(L05Os10.L05_10_closed_set__), () => L05Os10.L05_10_closed_set__.Alpha, () => L05Os10.L05_10_closed_set__.All.First(m => m.Name == "Alpha"), () => L05Os10.L05_10_closed_set__.Beta); - Add("closed-set", "-", "base-explicit-IEquatable", "nested-two-levels", typeof(L05Ot11.Inner.L05_11_closed_set__), () => L05Ot11.Inner.L05_11_closed_set__.Alpha, () => L05Ot11.Inner.L05_11_closed_set__.All.First(m => m.Name == "Alpha"), () => L05Ot11.Inner.L05_11_closed_set__.Beta); - Add("closed-set", "-", "base-explicit-IEquatable", "top-level", typeof(L05_12_closed_set__), () => L05_12_closed_set__.Alpha, () => L05_12_closed_set__.All.First(m => m.Name == "Alpha"), () => L05_12_closed_set__.Beta); - Add("closed-set", "-", "explicit-interface-member", "nested-in-class", typeof(L05Oc13.L05_13_closed_set__), () => L05Oc13.L05_13_closed_set__.Alpha, () => L05Oc13.L05_13_closed_set__.All.First(m => m.Name == "Alpha"), () => L05Oc13.L05_13_closed_set__.Beta); - Add("closed-set", "-", "explicit-interface-member", "top-level", typeof(L05_14_closed_set__), () => L05_14_closed_set__.Alpha, () => L05_14_closed_set__.All.First(m => m.Name == "Alpha"), () => L05_14_closed_set__.Beta); - Add("closed-set", "-", "none", "nested-in-struct", typeof(L05Os15.L05_15_closed_set__), () => L05Os15.L05_15_closed_set__.Alpha, () => L05Os15.L05_15_closed_set__.All.First(m => m.Name == "Alpha"), () => L05Os15.L05_15_closed_set__.Beta); - Add("record-struct", "DateOnly", "explicit-interface-member", "nested-in-class", typeof(L05Oc16.L05_16_record_struct_DateOnly), () => new DateOnly(2026, 10, 15), () => DateOnly.FromDayNumber(new DateOnly(2026, 10, 15).DayNumber), () => new DateOnly(2026, 10, 16)); - Add("record-struct", "DateOnly", "explicit-interface-member", "nested-in-struct", typeof(L05Os17.L05_17_record_struct_DateOnly), () => new DateOnly(2026, 10, 15), () => DateOnly.FromDayNumber(new DateOnly(2026, 10, 15).DayNumber), () => new DateOnly(2026, 10, 16)); - Add("record-struct", "DateOnly", "explicit-interface-member", "nested-two-levels", typeof(L05Ot18.Inner.L05_18_record_struct_DateOnly), () => new DateOnly(2026, 10, 15), () => DateOnly.FromDayNumber(new DateOnly(2026, 10, 15).DayNumber), () => new DateOnly(2026, 10, 16)); - Add("record-struct", "DateTime", "none", "nested-in-class", typeof(L05Oc19.L05_19_record_struct_DateTime), () => new DateTime(2026, 10, 15, 8, 30, 0, DateTimeKind.Utc), () => new DateTime(2026, 10, 15, 8, 30, 0, DateTimeKind.Unspecified), () => new DateTime(2026, 10, 15, 8, 31, 0, DateTimeKind.Utc)); - Add("record-struct", "DateTimeOffset", "none", "nested-in-struct", typeof(L05Os20.L05_20_record_struct_DateTimeOffset), () => new DateTimeOffset(2026, 10, 15, 8, 30, 0, TimeSpan.Zero), () => new DateTimeOffset(2026, 10, 15, 10, 30, 0, TimeSpan.FromHours(2)), () => new DateTimeOffset(2026, 10, 15, 8, 31, 0, TimeSpan.Zero)); - Add("record-struct", "Guid", "explicit-interface-member", "nested-in-struct", typeof(L05Os21.L05_21_record_struct_Guid), () => Guid.Parse("11111111-1111-1111-1111-111111111111"), () => new Guid("11111111111111111111111111111111"), () => Guid.Parse("22222222-2222-2222-2222-222222222222")); - Add("record-struct", "Guid", "explicit-interface-member", "top-level", typeof(L05_22_record_struct_Guid), () => Guid.Parse("11111111-1111-1111-1111-111111111111"), () => new Guid("11111111111111111111111111111111"), () => Guid.Parse("22222222-2222-2222-2222-222222222222")); - Add("record-struct", "NodaTime.Instant", "explicit-interface-member", "nested-in-class", typeof(L05Oc23.L05_23_record_struct_NodaTime_Instant), () => global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 30), () => global::NodaTime.Instant.FromUnixTimeTicks(global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 30).ToUnixTimeTicks()), () => global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 31)); - Add("record-struct", "NodaTime.Instant", "explicit-interface-member", "nested-in-struct", typeof(L05Os24.L05_24_record_struct_NodaTime_Instant), () => global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 30), () => global::NodaTime.Instant.FromUnixTimeTicks(global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 30).ToUnixTimeTicks()), () => global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 31)); - Add("record-struct", "NodaTime.Instant", "explicit-interface-member", "top-level", typeof(L05_25_record_struct_NodaTime_Instant), () => global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 30), () => global::NodaTime.Instant.FromUnixTimeTicks(global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 30).ToUnixTimeTicks()), () => global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 31)); - Add("record-struct", "TimeOnly", "explicit-interface-member", "nested-two-levels", typeof(L05Ot26.Inner.L05_26_record_struct_TimeOnly), () => new TimeOnly(8, 30, 15), () => new TimeOnly(new TimeOnly(8, 30, 15).Ticks), () => new TimeOnly(8, 30, 16)); - Add("record-struct", "TimeOnly", "explicit-interface-member", "top-level", typeof(L05_27_record_struct_TimeOnly), () => new TimeOnly(8, 30, 15), () => new TimeOnly(new TimeOnly(8, 30, 15).Ticks), () => new TimeOnly(8, 30, 16)); - Add("record-struct", "TimeOnly", "none", "nested-in-struct", typeof(L05Os28.L05_28_record_struct_TimeOnly), () => new TimeOnly(8, 30, 15), () => new TimeOnly(new TimeOnly(8, 30, 15).Ticks), () => new TimeOnly(8, 30, 16)); - Add("record-struct", "TimeSpan", "explicit-interface-member", "nested-in-class", typeof(L05Oc29.L05_29_record_struct_TimeSpan), () => TimeSpan.FromMinutes(90), () => TimeSpan.FromSeconds(5400), () => TimeSpan.FromMinutes(91)); - Add("record-struct", "TimeSpan", "none", "nested-in-class", typeof(L05Oc30.L05_30_record_struct_TimeSpan), () => TimeSpan.FromMinutes(90), () => TimeSpan.FromSeconds(5400), () => TimeSpan.FromMinutes(91)); - Add("record-struct", "Uri", "explicit-interface-member", "top-level", typeof(L05_31_record_struct_Uri), () => new Uri("https://example.com/a"), () => new Uri("HTTPS://EXAMPLE.COM/a"), () => new Uri("https://example.com/b")); - Add("record-struct", "Uri", "none", "top-level", typeof(L05_32_record_struct_Uri), () => new Uri("https://example.com/a"), () => new Uri("HTTPS://EXAMPLE.COM/a"), () => new Uri("https://example.com/b")); - Add("record-struct", "bool", "explicit-interface-member", "nested-in-struct", typeof(L05Os33.L05_33_record_struct_bool), () => false, () => false, () => true); - Add("record-struct", "bool", "explicit-interface-member", "nested-two-levels", typeof(L05Ot34.Inner.L05_34_record_struct_bool), () => false, () => false, () => true); - Add("record-struct", "bool", "explicit-interface-member", "top-level", typeof(L05_35_record_struct_bool), () => false, () => false, () => true); - Add("record-struct", "byte", "explicit-interface-member", "top-level", typeof(L05_36_record_struct_byte), () => (byte)7, () => (byte)7, () => (byte)8); - Add("record-struct", "byte", "none", "nested-in-class", typeof(L05Oc37.L05_37_record_struct_byte), () => (byte)7, () => (byte)7, () => (byte)8); - Add("record-struct", "char", "explicit-interface-member", "top-level", typeof(L05_38_record_struct_char), () => 'a', () => 'a', () => 'b'); - Add("record-struct", "char", "none", "nested-in-struct", typeof(L05Os39.L05_39_record_struct_char), () => 'a', () => 'a', () => 'b'); - Add("record-struct", "decimal", "explicit-interface-member", "nested-in-class", typeof(L05Oc40.L05_40_record_struct_decimal), () => 7.8m, () => 7.80m, () => 7.81m); - Add("record-struct", "decimal", "explicit-interface-member", "top-level", typeof(L05_41_record_struct_decimal), () => 7.8m, () => 7.80m, () => 7.81m); - Add("record-struct", "decimal", "none", "nested-in-class", typeof(L05Oc42.L05_42_record_struct_decimal), () => 7.8m, () => 7.80m, () => 7.81m); - Add("record-struct", "double", "explicit-interface-member", "nested-in-class", typeof(L05Oc43.L05_43_record_struct_double), () => 0.0, () => -0.0, () => 1.5); - Add("record-struct", "double", "explicit-interface-member", "nested-in-struct", typeof(L05Os44.L05_44_record_struct_double), () => 0.0, () => -0.0, () => 1.5); - Add("record-struct", "double", "explicit-interface-member", "nested-two-levels", typeof(L05Ot45.Inner.L05_45_record_struct_double), () => 0.0, () => -0.0, () => 1.5); - Add("record-struct", "double", "none", "nested-two-levels", typeof(L05Ot46.Inner.L05_46_record_struct_double), () => 0.0, () => -0.0, () => 1.5); - Add("record-struct", "enum", "explicit-interface-member", "nested-in-class", typeof(L05Oc47.L05_47_record_struct_enum), () => PEnum.B, () => (PEnum)1, () => PEnum.C); - Add("record-struct", "enum", "explicit-interface-member", "nested-in-struct", typeof(L05Os48.L05_48_record_struct_enum), () => PEnum.B, () => (PEnum)1, () => PEnum.C); - Add("record-struct", "flags-enum", "explicit-interface-member", "top-level", typeof(L05_49_record_struct_flags_enum), () => PFlags.P | PFlags.Q, () => (PFlags)3, () => PFlags.R); - Add("record-struct", "flags-enum", "none", "top-level", typeof(L05_50_record_struct_flags_enum), () => PFlags.P | PFlags.Q, () => (PFlags)3, () => PFlags.R); - Add("record-struct", "int", "explicit-interface-member", "nested-in-struct", typeof(L05Os51.L05_51_record_struct_int), () => 42, () => 42, () => 43); - Add("record-struct", "int", "none", "nested-two-levels", typeof(L05Ot52.Inner.L05_52_record_struct_int), () => 42, () => 42, () => 43); - Add("record-struct", "long", "none", "nested-two-levels", typeof(L05Ot53.Inner.L05_53_record_struct_long), () => 4200000000L, () => 4200000000L, () => 4200000001L); - Add("record-struct", "long", "none", "top-level", typeof(L05_54_record_struct_long), () => 4200000000L, () => 4200000000L, () => 4200000001L); - Add("record-struct", "own-record-class", "explicit-interface-member", "nested-two-levels", typeof(L05Ot55.Inner.L05_55_record_struct_own_record_class), () => new PRec { A = "x", B = 1 }, () => new PRec { A = new string("x".ToCharArray()), B = 1 }, () => new PRec { A = "y", B = 1 }); - Add("record-struct", "string", "explicit-interface-member", "nested-in-struct", typeof(L05Os56.L05_56_record_struct_string), () => "alpha", () => new string("alpha".ToCharArray()), () => "beta"); - Add("record-struct", "string", "explicit-interface-member", "top-level", typeof(L05_57_record_struct_string), () => "alpha", () => new string("alpha".ToCharArray()), () => "beta"); - Add("record-struct", "string", "none", "nested-in-class", typeof(L05Oc58.L05_58_record_struct_string), () => "alpha", () => new string("alpha".ToCharArray()), () => "beta"); - Add("record-struct", "ulong", "explicit-interface-member", "nested-in-class", typeof(L05Oc59.L05_59_record_struct_ulong), () => 4200000000UL, () => 4200000000UL, () => 4200000001UL); - Add("record-struct", "ulong", "explicit-interface-member", "nested-in-struct", typeof(L05Os60.L05_60_record_struct_ulong), () => 4200000000UL, () => 4200000000UL, () => 4200000001UL); - Add("sealed-record-class", "DateOnly", "base-depth-2", "nested-in-struct", typeof(L05Os61.L05_61_sealed_record_class_DateOnly), () => new DateOnly(2026, 10, 15), () => DateOnly.FromDayNumber(new DateOnly(2026, 10, 15).DayNumber), () => new DateOnly(2026, 10, 16)); - Add("sealed-record-class", "DateOnly", "base-depth-3", "top-level", typeof(L05_62_sealed_record_class_DateOnly), () => new DateOnly(2026, 10, 15), () => DateOnly.FromDayNumber(new DateOnly(2026, 10, 15).DayNumber), () => new DateOnly(2026, 10, 16)); - Add("sealed-record-class", "DateOnly", "none", "nested-in-struct", typeof(L05Os63.L05_63_sealed_record_class_DateOnly), () => new DateOnly(2026, 10, 15), () => DateOnly.FromDayNumber(new DateOnly(2026, 10, 15).DayNumber), () => new DateOnly(2026, 10, 16)); - Add("sealed-record-class", "DateOnly", "none", "top-level", typeof(L05_64_sealed_record_class_DateOnly), () => new DateOnly(2026, 10, 15), () => DateOnly.FromDayNumber(new DateOnly(2026, 10, 15).DayNumber), () => new DateOnly(2026, 10, 16)); - Add("sealed-record-class", "DateTime", "base-depth-1", "top-level", typeof(L05_65_sealed_record_class_DateTime), () => new DateTime(2026, 10, 15, 8, 30, 0, DateTimeKind.Utc), () => new DateTime(2026, 10, 15, 8, 30, 0, DateTimeKind.Unspecified), () => new DateTime(2026, 10, 15, 8, 31, 0, DateTimeKind.Utc)); - Add("sealed-record-class", "DateTime", "base-depth-2", "nested-in-class", typeof(L05Oc66.L05_66_sealed_record_class_DateTime), () => new DateTime(2026, 10, 15, 8, 30, 0, DateTimeKind.Utc), () => new DateTime(2026, 10, 15, 8, 30, 0, DateTimeKind.Unspecified), () => new DateTime(2026, 10, 15, 8, 31, 0, DateTimeKind.Utc)); - Add("sealed-record-class", "DateTime", "base-depth-3", "nested-in-struct", typeof(L05Os67.L05_67_sealed_record_class_DateTime), () => new DateTime(2026, 10, 15, 8, 30, 0, DateTimeKind.Utc), () => new DateTime(2026, 10, 15, 8, 30, 0, DateTimeKind.Unspecified), () => new DateTime(2026, 10, 15, 8, 31, 0, DateTimeKind.Utc)); - Add("sealed-record-class", "DateTime", "base-depth-3", "nested-two-levels", typeof(L05Ot68.Inner.L05_68_sealed_record_class_DateTime), () => new DateTime(2026, 10, 15, 8, 30, 0, DateTimeKind.Utc), () => new DateTime(2026, 10, 15, 8, 30, 0, DateTimeKind.Unspecified), () => new DateTime(2026, 10, 15, 8, 31, 0, DateTimeKind.Utc)); - Add("sealed-record-class", "DateTime", "base-depth-3", "top-level", typeof(L05_69_sealed_record_class_DateTime), () => new DateTime(2026, 10, 15, 8, 30, 0, DateTimeKind.Utc), () => new DateTime(2026, 10, 15, 8, 30, 0, DateTimeKind.Unspecified), () => new DateTime(2026, 10, 15, 8, 31, 0, DateTimeKind.Utc)); - Add("sealed-record-class", "DateTime", "base-explicit-IEquatable", "nested-in-struct", typeof(L05Os70.L05_70_sealed_record_class_DateTime), () => new DateTime(2026, 10, 15, 8, 30, 0, DateTimeKind.Utc), () => new DateTime(2026, 10, 15, 8, 30, 0, DateTimeKind.Unspecified), () => new DateTime(2026, 10, 15, 8, 31, 0, DateTimeKind.Utc)); - Add("sealed-record-class", "DateTimeOffset", "base-depth-1", "nested-in-class", typeof(L05Oc71.L05_71_sealed_record_class_DateTimeOffset), () => new DateTimeOffset(2026, 10, 15, 8, 30, 0, TimeSpan.Zero), () => new DateTimeOffset(2026, 10, 15, 10, 30, 0, TimeSpan.FromHours(2)), () => new DateTimeOffset(2026, 10, 15, 8, 31, 0, TimeSpan.Zero)); - Add("sealed-record-class", "DateTimeOffset", "base-depth-2", "top-level", typeof(L05_72_sealed_record_class_DateTimeOffset), () => new DateTimeOffset(2026, 10, 15, 8, 30, 0, TimeSpan.Zero), () => new DateTimeOffset(2026, 10, 15, 10, 30, 0, TimeSpan.FromHours(2)), () => new DateTimeOffset(2026, 10, 15, 8, 31, 0, TimeSpan.Zero)); - Add("sealed-record-class", "DateTimeOffset", "base-depth-3", "nested-in-class", typeof(L05Oc73.L05_73_sealed_record_class_DateTimeOffset), () => new DateTimeOffset(2026, 10, 15, 8, 30, 0, TimeSpan.Zero), () => new DateTimeOffset(2026, 10, 15, 10, 30, 0, TimeSpan.FromHours(2)), () => new DateTimeOffset(2026, 10, 15, 8, 31, 0, TimeSpan.Zero)); - Add("sealed-record-class", "DateTimeOffset", "base-depth-3", "nested-two-levels", typeof(L05Ot74.Inner.L05_74_sealed_record_class_DateTimeOffset), () => new DateTimeOffset(2026, 10, 15, 8, 30, 0, TimeSpan.Zero), () => new DateTimeOffset(2026, 10, 15, 10, 30, 0, TimeSpan.FromHours(2)), () => new DateTimeOffset(2026, 10, 15, 8, 31, 0, TimeSpan.Zero)); - Add("sealed-record-class", "DateTimeOffset", "base-depth-3", "top-level", typeof(L05_75_sealed_record_class_DateTimeOffset), () => new DateTimeOffset(2026, 10, 15, 8, 30, 0, TimeSpan.Zero), () => new DateTimeOffset(2026, 10, 15, 10, 30, 0, TimeSpan.FromHours(2)), () => new DateTimeOffset(2026, 10, 15, 8, 31, 0, TimeSpan.Zero)); - Add("sealed-record-class", "DateTimeOffset", "base-explicit-IEquatable", "nested-in-class", typeof(L05Oc76.L05_76_sealed_record_class_DateTimeOffset), () => new DateTimeOffset(2026, 10, 15, 8, 30, 0, TimeSpan.Zero), () => new DateTimeOffset(2026, 10, 15, 10, 30, 0, TimeSpan.FromHours(2)), () => new DateTimeOffset(2026, 10, 15, 8, 31, 0, TimeSpan.Zero)); - Add("sealed-record-class", "DateTimeOffset", "base-explicit-IEquatable", "top-level", typeof(L05_77_sealed_record_class_DateTimeOffset), () => new DateTimeOffset(2026, 10, 15, 8, 30, 0, TimeSpan.Zero), () => new DateTimeOffset(2026, 10, 15, 10, 30, 0, TimeSpan.FromHours(2)), () => new DateTimeOffset(2026, 10, 15, 8, 31, 0, TimeSpan.Zero)); - Add("sealed-record-class", "Guid", "base-depth-1", "nested-in-class", typeof(L05Oc78.L05_78_sealed_record_class_Guid), () => Guid.Parse("11111111-1111-1111-1111-111111111111"), () => new Guid("11111111111111111111111111111111"), () => Guid.Parse("22222222-2222-2222-2222-222222222222")); - Add("sealed-record-class", "Guid", "base-depth-1", "nested-in-struct", typeof(L05Os79.L05_79_sealed_record_class_Guid), () => Guid.Parse("11111111-1111-1111-1111-111111111111"), () => new Guid("11111111111111111111111111111111"), () => Guid.Parse("22222222-2222-2222-2222-222222222222")); - Add("sealed-record-class", "Guid", "base-depth-2", "top-level", typeof(L05_80_sealed_record_class_Guid), () => Guid.Parse("11111111-1111-1111-1111-111111111111"), () => new Guid("11111111111111111111111111111111"), () => Guid.Parse("22222222-2222-2222-2222-222222222222")); - Add("sealed-record-class", "Guid", "base-depth-3", "top-level", typeof(L05_81_sealed_record_class_Guid), () => Guid.Parse("11111111-1111-1111-1111-111111111111"), () => new Guid("11111111111111111111111111111111"), () => Guid.Parse("22222222-2222-2222-2222-222222222222")); - Add("sealed-record-class", "Guid", "base-explicit-IEquatable", "nested-in-class", typeof(L05Oc82.L05_82_sealed_record_class_Guid), () => Guid.Parse("11111111-1111-1111-1111-111111111111"), () => new Guid("11111111111111111111111111111111"), () => Guid.Parse("22222222-2222-2222-2222-222222222222")); - Add("sealed-record-class", "Guid", "base-explicit-IEquatable", "nested-two-levels", typeof(L05Ot83.Inner.L05_83_sealed_record_class_Guid), () => Guid.Parse("11111111-1111-1111-1111-111111111111"), () => new Guid("11111111111111111111111111111111"), () => Guid.Parse("22222222-2222-2222-2222-222222222222")); - Add("sealed-record-class", "NodaTime.Instant", "base-depth-1", "nested-two-levels", typeof(L05Ot84.Inner.L05_84_sealed_record_class_NodaTime_Instant), () => global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 30), () => global::NodaTime.Instant.FromUnixTimeTicks(global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 30).ToUnixTimeTicks()), () => global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 31)); - Add("sealed-record-class", "NodaTime.Instant", "base-explicit-IEquatable", "nested-in-struct", typeof(L05Os85.L05_85_sealed_record_class_NodaTime_Instant), () => global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 30), () => global::NodaTime.Instant.FromUnixTimeTicks(global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 30).ToUnixTimeTicks()), () => global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 31)); - Add("sealed-record-class", "NodaTime.Instant", "none", "nested-in-struct", typeof(L05Os86.L05_86_sealed_record_class_NodaTime_Instant), () => global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 30), () => global::NodaTime.Instant.FromUnixTimeTicks(global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 30).ToUnixTimeTicks()), () => global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 31)); - Add("sealed-record-class", "TimeOnly", "base-depth-3", "nested-in-struct", typeof(L05Os87.L05_87_sealed_record_class_TimeOnly), () => new TimeOnly(8, 30, 15), () => new TimeOnly(new TimeOnly(8, 30, 15).Ticks), () => new TimeOnly(8, 30, 16)); - Add("sealed-record-class", "TimeOnly", "base-depth-3", "nested-two-levels", typeof(L05Ot88.Inner.L05_88_sealed_record_class_TimeOnly), () => new TimeOnly(8, 30, 15), () => new TimeOnly(new TimeOnly(8, 30, 15).Ticks), () => new TimeOnly(8, 30, 16)); - Add("sealed-record-class", "TimeSpan", "base-depth-1", "nested-in-class", typeof(L05Oc89.L05_89_sealed_record_class_TimeSpan), () => TimeSpan.FromMinutes(90), () => TimeSpan.FromSeconds(5400), () => TimeSpan.FromMinutes(91)); - Add("sealed-record-class", "TimeSpan", "base-depth-1", "nested-two-levels", typeof(L05Ot90.Inner.L05_90_sealed_record_class_TimeSpan), () => TimeSpan.FromMinutes(90), () => TimeSpan.FromSeconds(5400), () => TimeSpan.FromMinutes(91)); - Add("sealed-record-class", "TimeSpan", "base-depth-2", "top-level", typeof(L05_91_sealed_record_class_TimeSpan), () => TimeSpan.FromMinutes(90), () => TimeSpan.FromSeconds(5400), () => TimeSpan.FromMinutes(91)); - Add("sealed-record-class", "TimeSpan", "base-depth-3", "top-level", typeof(L05_92_sealed_record_class_TimeSpan), () => TimeSpan.FromMinutes(90), () => TimeSpan.FromSeconds(5400), () => TimeSpan.FromMinutes(91)); - Add("sealed-record-class", "TimeSpan", "explicit-interface-member", "nested-in-struct", typeof(L05Os93.L05_93_sealed_record_class_TimeSpan), () => TimeSpan.FromMinutes(90), () => TimeSpan.FromSeconds(5400), () => TimeSpan.FromMinutes(91)); - Add("sealed-record-class", "TimeSpan", "none", "nested-in-class", typeof(L05Oc94.L05_94_sealed_record_class_TimeSpan), () => TimeSpan.FromMinutes(90), () => TimeSpan.FromSeconds(5400), () => TimeSpan.FromMinutes(91)); - Add("sealed-record-class", "Uri", "base-depth-1", "nested-in-class", typeof(L05Oc95.L05_95_sealed_record_class_Uri), () => new Uri("https://example.com/a"), () => new Uri("HTTPS://EXAMPLE.COM/a"), () => new Uri("https://example.com/b")); - Add("sealed-record-class", "Uri", "base-depth-2", "nested-in-struct", typeof(L05Os96.L05_96_sealed_record_class_Uri), () => new Uri("https://example.com/a"), () => new Uri("HTTPS://EXAMPLE.COM/a"), () => new Uri("https://example.com/b")); - Add("sealed-record-class", "Uri", "base-depth-3", "nested-in-struct", typeof(L05Os97.L05_97_sealed_record_class_Uri), () => new Uri("https://example.com/a"), () => new Uri("HTTPS://EXAMPLE.COM/a"), () => new Uri("https://example.com/b")); - Add("sealed-record-class", "Uri", "base-explicit-IEquatable", "nested-two-levels", typeof(L05Ot98.Inner.L05_98_sealed_record_class_Uri), () => new Uri("https://example.com/a"), () => new Uri("HTTPS://EXAMPLE.COM/a"), () => new Uri("https://example.com/b")); - Add("sealed-record-class", "bool", "base-depth-2", "nested-in-class", typeof(L05Oc99.L05_99_sealed_record_class_bool), () => false, () => false, () => true); - Add("sealed-record-class", "bool", "base-depth-2", "nested-two-levels", typeof(L05Ot100.Inner.L05_100_sealed_record_class_bool), () => false, () => false, () => true); - Add("sealed-record-class", "bool", "base-depth-3", "nested-in-struct", typeof(L05Os101.L05_101_sealed_record_class_bool), () => false, () => false, () => true); - Add("sealed-record-class", "bool", "base-explicit-IEquatable", "nested-two-levels", typeof(L05Ot102.Inner.L05_102_sealed_record_class_bool), () => false, () => false, () => true); - Add("sealed-record-class", "bool", "explicit-interface-member", "nested-two-levels", typeof(L05Ot103.Inner.L05_103_sealed_record_class_bool), () => false, () => false, () => true); - Add("sealed-record-class", "byte", "base-depth-1", "nested-two-levels", typeof(L05Ot104.Inner.L05_104_sealed_record_class_byte), () => (byte)7, () => (byte)7, () => (byte)8); - Add("sealed-record-class", "byte", "base-depth-2", "nested-in-struct", typeof(L05Os105.L05_105_sealed_record_class_byte), () => (byte)7, () => (byte)7, () => (byte)8); - Add("sealed-record-class", "byte", "base-depth-3", "nested-in-class", typeof(L05Oc106.L05_106_sealed_record_class_byte), () => (byte)7, () => (byte)7, () => (byte)8); - Add("sealed-record-class", "byte", "base-depth-3", "top-level", typeof(L05_107_sealed_record_class_byte), () => (byte)7, () => (byte)7, () => (byte)8); - Add("sealed-record-class", "byte", "explicit-interface-member", "nested-two-levels", typeof(L05Ot108.Inner.L05_108_sealed_record_class_byte), () => (byte)7, () => (byte)7, () => (byte)8); - Add("sealed-record-class", "byte", "explicit-interface-member", "top-level", typeof(L05_109_sealed_record_class_byte), () => (byte)7, () => (byte)7, () => (byte)8); - Add("sealed-record-class", "byte", "none", "top-level", typeof(L05_110_sealed_record_class_byte), () => (byte)7, () => (byte)7, () => (byte)8); - Add("sealed-record-class", "char", "base-depth-1", "nested-two-levels", typeof(L05Ot111.Inner.L05_111_sealed_record_class_char), () => 'a', () => 'a', () => 'b'); - Add("sealed-record-class", "char", "base-depth-2", "top-level", typeof(L05_112_sealed_record_class_char), () => 'a', () => 'a', () => 'b'); - Add("sealed-record-class", "char", "base-explicit-IEquatable", "top-level", typeof(L05_113_sealed_record_class_char), () => 'a', () => 'a', () => 'b'); - Add("sealed-record-class", "char", "explicit-interface-member", "nested-two-levels", typeof(L05Ot114.Inner.L05_114_sealed_record_class_char), () => 'a', () => 'a', () => 'b'); - Add("sealed-record-class", "decimal", "base-depth-1", "nested-in-class", typeof(L05Oc115.L05_115_sealed_record_class_decimal), () => 7.8m, () => 7.80m, () => 7.81m); - Add("sealed-record-class", "decimal", "base-depth-3", "nested-in-struct", typeof(L05Os116.L05_116_sealed_record_class_decimal), () => 7.8m, () => 7.80m, () => 7.81m); - Add("sealed-record-class", "decimal", "base-explicit-IEquatable", "nested-in-class", typeof(L05Oc117.L05_117_sealed_record_class_decimal), () => 7.8m, () => 7.80m, () => 7.81m); - Add("sealed-record-class", "decimal", "base-explicit-IEquatable", "top-level", typeof(L05_118_sealed_record_class_decimal), () => 7.8m, () => 7.80m, () => 7.81m); - Add("sealed-record-class", "double", "base-depth-1", "nested-two-levels", typeof(L05Ot119.Inner.L05_119_sealed_record_class_double), () => 0.0, () => -0.0, () => 1.5); - Add("sealed-record-class", "double", "base-depth-2", "top-level", typeof(L05_120_sealed_record_class_double), () => 0.0, () => -0.0, () => 1.5); - Add("sealed-record-class", "double", "base-depth-3", "nested-in-class", typeof(L05Oc121.L05_121_sealed_record_class_double), () => 0.0, () => -0.0, () => 1.5); - Add("sealed-record-class", "double", "base-explicit-IEquatable", "nested-in-class", typeof(L05Oc122.L05_122_sealed_record_class_double), () => 0.0, () => -0.0, () => 1.5); - Add("sealed-record-class", "enum", "base-depth-1", "nested-in-struct", typeof(L05Os123.L05_123_sealed_record_class_enum), () => PEnum.B, () => (PEnum)1, () => PEnum.C); - Add("sealed-record-class", "enum", "base-depth-2", "nested-two-levels", typeof(L05Ot124.Inner.L05_124_sealed_record_class_enum), () => PEnum.B, () => (PEnum)1, () => PEnum.C); - Add("sealed-record-class", "enum", "base-depth-3", "nested-two-levels", typeof(L05Ot125.Inner.L05_125_sealed_record_class_enum), () => PEnum.B, () => (PEnum)1, () => PEnum.C); - Add("sealed-record-class", "enum", "base-explicit-IEquatable", "nested-two-levels", typeof(L05Ot126.Inner.L05_126_sealed_record_class_enum), () => PEnum.B, () => (PEnum)1, () => PEnum.C); - Add("sealed-record-class", "enum", "explicit-interface-member", "nested-in-struct", typeof(L05Os127.L05_127_sealed_record_class_enum), () => PEnum.B, () => (PEnum)1, () => PEnum.C); - Add("sealed-record-class", "enum", "none", "nested-in-class", typeof(L05Oc128.L05_128_sealed_record_class_enum), () => PEnum.B, () => (PEnum)1, () => PEnum.C); - Add("sealed-record-class", "flags-enum", "base-depth-1", "nested-in-class", typeof(L05Oc129.L05_129_sealed_record_class_flags_enum), () => PFlags.P | PFlags.Q, () => (PFlags)3, () => PFlags.R); - Add("sealed-record-class", "flags-enum", "base-depth-1", "nested-two-levels", typeof(L05Ot130.Inner.L05_130_sealed_record_class_flags_enum), () => PFlags.P | PFlags.Q, () => (PFlags)3, () => PFlags.R); - Add("sealed-record-class", "flags-enum", "base-depth-2", "top-level", typeof(L05_131_sealed_record_class_flags_enum), () => PFlags.P | PFlags.Q, () => (PFlags)3, () => PFlags.R); - Add("sealed-record-class", "flags-enum", "base-depth-3", "top-level", typeof(L05_132_sealed_record_class_flags_enum), () => PFlags.P | PFlags.Q, () => (PFlags)3, () => PFlags.R); - Add("sealed-record-class", "flags-enum", "base-explicit-IEquatable", "nested-two-levels", typeof(L05Ot133.Inner.L05_133_sealed_record_class_flags_enum), () => PFlags.P | PFlags.Q, () => (PFlags)3, () => PFlags.R); - Add("sealed-record-class", "flags-enum", "explicit-interface-member", "nested-in-struct", typeof(L05Os134.L05_134_sealed_record_class_flags_enum), () => PFlags.P | PFlags.Q, () => (PFlags)3, () => PFlags.R); - Add("sealed-record-class", "int", "base-depth-2", "top-level", typeof(L05_135_sealed_record_class_int), () => 42, () => 42, () => 43); - Add("sealed-record-class", "int", "base-explicit-IEquatable", "nested-in-struct", typeof(L05Os136.L05_136_sealed_record_class_int), () => 42, () => 42, () => 43); - Add("sealed-record-class", "long", "base-depth-2", "nested-two-levels", typeof(L05Ot137.Inner.L05_137_sealed_record_class_long), () => 4200000000L, () => 4200000000L, () => 4200000001L); - Add("sealed-record-class", "long", "base-depth-3", "nested-two-levels", typeof(L05Ot138.Inner.L05_138_sealed_record_class_long), () => 4200000000L, () => 4200000000L, () => 4200000001L); - Add("sealed-record-class", "long", "base-explicit-IEquatable", "nested-in-struct", typeof(L05Os139.L05_139_sealed_record_class_long), () => 4200000000L, () => 4200000000L, () => 4200000001L); - Add("sealed-record-class", "own-record-class", "base-depth-2", "nested-two-levels", typeof(L05Ot140.Inner.L05_140_sealed_record_class_own_record_class), () => new PRec { A = "x", B = 1 }, () => new PRec { A = new string("x".ToCharArray()), B = 1 }, () => new PRec { A = "y", B = 1 }); - Add("sealed-record-class", "own-record-class", "base-explicit-IEquatable", "nested-in-struct", typeof(L05Os141.L05_141_sealed_record_class_own_record_class), () => new PRec { A = "x", B = 1 }, () => new PRec { A = new string("x".ToCharArray()), B = 1 }, () => new PRec { A = "y", B = 1 }); - Add("sealed-record-class", "own-record-class", "base-explicit-IEquatable", "nested-two-levels", typeof(L05Ot142.Inner.L05_142_sealed_record_class_own_record_class), () => new PRec { A = "x", B = 1 }, () => new PRec { A = new string("x".ToCharArray()), B = 1 }, () => new PRec { A = "y", B = 1 }); - Add("sealed-record-class", "string", "base-depth-1", "nested-in-struct", typeof(L05Os143.L05_143_sealed_record_class_string), () => "alpha", () => new string("alpha".ToCharArray()), () => "beta"); - Add("sealed-record-class", "string", "base-depth-3", "top-level", typeof(L05_144_sealed_record_class_string), () => "alpha", () => new string("alpha".ToCharArray()), () => "beta"); - Add("sealed-record-class", "string", "base-explicit-IEquatable", "nested-two-levels", typeof(L05Ot145.Inner.L05_145_sealed_record_class_string), () => "alpha", () => new string("alpha".ToCharArray()), () => "beta"); - Add("sealed-record-class", "ulong", "base-depth-3", "nested-two-levels", typeof(L05Ot146.Inner.L05_146_sealed_record_class_ulong), () => 4200000000UL, () => 4200000000UL, () => 4200000001UL); - Add("sealed-record-class", "ulong", "base-explicit-IEquatable", "nested-two-levels", typeof(L05Ot147.Inner.L05_147_sealed_record_class_ulong), () => 4200000000UL, () => 4200000000UL, () => 4200000001UL); - Add("sealed-record-class", "ulong", "base-explicit-IEquatable", "top-level", typeof(L05_148_sealed_record_class_ulong), () => 4200000000UL, () => 4200000000UL, () => 4200000001UL); - Add("validated-class", "DateOnly", "base-depth-1", "nested-in-struct", typeof(L05Os149.L05_149_validated_class_DateOnly), () => new DateOnly(2026, 10, 15), () => DateOnly.FromDayNumber(new DateOnly(2026, 10, 15).DayNumber), () => new DateOnly(2026, 10, 16)); - Add("validated-class", "DateOnly", "base-explicit-IEquatable", "top-level", typeof(L05_150_validated_class_DateOnly), () => new DateOnly(2026, 10, 15), () => DateOnly.FromDayNumber(new DateOnly(2026, 10, 15).DayNumber), () => new DateOnly(2026, 10, 16)); - Add("validated-class", "DateOnly", "explicit-interface-member", "top-level", typeof(L05_151_validated_class_DateOnly), () => new DateOnly(2026, 10, 15), () => DateOnly.FromDayNumber(new DateOnly(2026, 10, 15).DayNumber), () => new DateOnly(2026, 10, 16)); - Add("validated-class", "DateOnly", "none", "nested-two-levels", typeof(L05Ot152.Inner.L05_152_validated_class_DateOnly), () => new DateOnly(2026, 10, 15), () => DateOnly.FromDayNumber(new DateOnly(2026, 10, 15).DayNumber), () => new DateOnly(2026, 10, 16)); - Add("validated-class", "DateTime", "base-depth-1", "nested-in-struct", typeof(L05Os153.L05_153_validated_class_DateTime), () => new DateTime(2026, 10, 15, 8, 30, 0, DateTimeKind.Utc), () => new DateTime(2026, 10, 15, 8, 30, 0, DateTimeKind.Unspecified), () => new DateTime(2026, 10, 15, 8, 31, 0, DateTimeKind.Utc)); - Add("validated-class", "DateTime", "base-depth-1", "nested-two-levels", typeof(L05Ot154.Inner.L05_154_validated_class_DateTime), () => new DateTime(2026, 10, 15, 8, 30, 0, DateTimeKind.Utc), () => new DateTime(2026, 10, 15, 8, 30, 0, DateTimeKind.Unspecified), () => new DateTime(2026, 10, 15, 8, 31, 0, DateTimeKind.Utc)); - Add("validated-class", "DateTime", "base-depth-2", "top-level", typeof(L05_155_validated_class_DateTime), () => new DateTime(2026, 10, 15, 8, 30, 0, DateTimeKind.Utc), () => new DateTime(2026, 10, 15, 8, 30, 0, DateTimeKind.Unspecified), () => new DateTime(2026, 10, 15, 8, 31, 0, DateTimeKind.Utc)); - Add("validated-class", "DateTime", "base-depth-3", "nested-in-class", typeof(L05Oc156.L05_156_validated_class_DateTime), () => new DateTime(2026, 10, 15, 8, 30, 0, DateTimeKind.Utc), () => new DateTime(2026, 10, 15, 8, 30, 0, DateTimeKind.Unspecified), () => new DateTime(2026, 10, 15, 8, 31, 0, DateTimeKind.Utc)); - Add("validated-class", "DateTime", "base-explicit-IEquatable", "nested-in-struct", typeof(L05Os157.L05_157_validated_class_DateTime), () => new DateTime(2026, 10, 15, 8, 30, 0, DateTimeKind.Utc), () => new DateTime(2026, 10, 15, 8, 30, 0, DateTimeKind.Unspecified), () => new DateTime(2026, 10, 15, 8, 31, 0, DateTimeKind.Utc)); - Add("validated-class", "DateTimeOffset", "explicit-interface-member", "nested-in-class", typeof(L05Oc158.L05_158_validated_class_DateTimeOffset), () => new DateTimeOffset(2026, 10, 15, 8, 30, 0, TimeSpan.Zero), () => new DateTimeOffset(2026, 10, 15, 10, 30, 0, TimeSpan.FromHours(2)), () => new DateTimeOffset(2026, 10, 15, 8, 31, 0, TimeSpan.Zero)); - Add("validated-class", "Guid", "base-depth-2", "nested-in-class", typeof(L05Oc159.L05_159_validated_class_Guid), () => Guid.Parse("11111111-1111-1111-1111-111111111111"), () => new Guid("11111111111111111111111111111111"), () => Guid.Parse("22222222-2222-2222-2222-222222222222")); - Add("validated-class", "Guid", "base-depth-2", "nested-two-levels", typeof(L05Ot160.Inner.L05_160_validated_class_Guid), () => Guid.Parse("11111111-1111-1111-1111-111111111111"), () => new Guid("11111111111111111111111111111111"), () => Guid.Parse("22222222-2222-2222-2222-222222222222")); - Add("validated-class", "Guid", "explicit-interface-member", "nested-in-class", typeof(L05Oc161.L05_161_validated_class_Guid), () => Guid.Parse("11111111-1111-1111-1111-111111111111"), () => new Guid("11111111111111111111111111111111"), () => Guid.Parse("22222222-2222-2222-2222-222222222222")); - Add("validated-class", "NodaTime.Instant", "base-depth-2", "nested-two-levels", typeof(L05Ot162.Inner.L05_162_validated_class_NodaTime_Instant), () => global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 30), () => global::NodaTime.Instant.FromUnixTimeTicks(global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 30).ToUnixTimeTicks()), () => global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 31)); - Add("validated-class", "NodaTime.Instant", "base-depth-3", "nested-in-class", typeof(L05Oc163.L05_163_validated_class_NodaTime_Instant), () => global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 30), () => global::NodaTime.Instant.FromUnixTimeTicks(global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 30).ToUnixTimeTicks()), () => global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 31)); - Add("validated-class", "NodaTime.Instant", "base-depth-3", "nested-in-struct", typeof(L05Os164.L05_164_validated_class_NodaTime_Instant), () => global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 30), () => global::NodaTime.Instant.FromUnixTimeTicks(global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 30).ToUnixTimeTicks()), () => global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 31)); - Add("validated-class", "NodaTime.Instant", "base-depth-3", "nested-two-levels", typeof(L05Ot165.Inner.L05_165_validated_class_NodaTime_Instant), () => global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 30), () => global::NodaTime.Instant.FromUnixTimeTicks(global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 30).ToUnixTimeTicks()), () => global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 31)); - Add("validated-class", "NodaTime.Instant", "base-explicit-IEquatable", "nested-in-class", typeof(L05Oc166.L05_166_validated_class_NodaTime_Instant), () => global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 30), () => global::NodaTime.Instant.FromUnixTimeTicks(global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 30).ToUnixTimeTicks()), () => global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 31)); - Add("validated-class", "NodaTime.Instant", "base-explicit-IEquatable", "top-level", typeof(L05_167_validated_class_NodaTime_Instant), () => global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 30), () => global::NodaTime.Instant.FromUnixTimeTicks(global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 30).ToUnixTimeTicks()), () => global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 31)); - Add("validated-class", "TimeOnly", "base-depth-1", "nested-in-class", typeof(L05Oc168.L05_168_validated_class_TimeOnly), () => new TimeOnly(8, 30, 15), () => new TimeOnly(new TimeOnly(8, 30, 15).Ticks), () => new TimeOnly(8, 30, 16)); - Add("validated-class", "TimeOnly", "base-depth-1", "nested-in-struct", typeof(L05Os169.L05_169_validated_class_TimeOnly), () => new TimeOnly(8, 30, 15), () => new TimeOnly(new TimeOnly(8, 30, 15).Ticks), () => new TimeOnly(8, 30, 16)); - Add("validated-class", "TimeOnly", "base-depth-2", "top-level", typeof(L05_170_validated_class_TimeOnly), () => new TimeOnly(8, 30, 15), () => new TimeOnly(new TimeOnly(8, 30, 15).Ticks), () => new TimeOnly(8, 30, 16)); - Add("validated-class", "TimeOnly", "base-explicit-IEquatable", "nested-in-class", typeof(L05Oc171.L05_171_validated_class_TimeOnly), () => new TimeOnly(8, 30, 15), () => new TimeOnly(new TimeOnly(8, 30, 15).Ticks), () => new TimeOnly(8, 30, 16)); - Add("validated-class", "TimeOnly", "none", "top-level", typeof(L05_172_validated_class_TimeOnly), () => new TimeOnly(8, 30, 15), () => new TimeOnly(new TimeOnly(8, 30, 15).Ticks), () => new TimeOnly(8, 30, 16)); - Add("validated-class", "TimeSpan", "base-depth-1", "top-level", typeof(L05_173_validated_class_TimeSpan), () => TimeSpan.FromMinutes(90), () => TimeSpan.FromSeconds(5400), () => TimeSpan.FromMinutes(91)); - Add("validated-class", "TimeSpan", "base-depth-2", "nested-in-class", typeof(L05Oc174.L05_174_validated_class_TimeSpan), () => TimeSpan.FromMinutes(90), () => TimeSpan.FromSeconds(5400), () => TimeSpan.FromMinutes(91)); - Add("validated-class", "TimeSpan", "base-depth-2", "nested-in-struct", typeof(L05Os175.L05_175_validated_class_TimeSpan), () => TimeSpan.FromMinutes(90), () => TimeSpan.FromSeconds(5400), () => TimeSpan.FromMinutes(91)); - Add("validated-class", "TimeSpan", "base-explicit-IEquatable", "nested-two-levels", typeof(L05Ot176.Inner.L05_176_validated_class_TimeSpan), () => TimeSpan.FromMinutes(90), () => TimeSpan.FromSeconds(5400), () => TimeSpan.FromMinutes(91)); - Add("validated-class", "Uri", "base-depth-1", "nested-in-struct", typeof(L05Os177.L05_177_validated_class_Uri), () => new Uri("https://example.com/a"), () => new Uri("HTTPS://EXAMPLE.COM/a"), () => new Uri("https://example.com/b")); - Add("validated-class", "Uri", "base-depth-3", "nested-two-levels", typeof(L05Ot178.Inner.L05_178_validated_class_Uri), () => new Uri("https://example.com/a"), () => new Uri("HTTPS://EXAMPLE.COM/a"), () => new Uri("https://example.com/b")); - Add("validated-class", "Uri", "explicit-interface-member", "top-level", typeof(L05_179_validated_class_Uri), () => new Uri("https://example.com/a"), () => new Uri("HTTPS://EXAMPLE.COM/a"), () => new Uri("https://example.com/b")); - Add("validated-class", "Uri", "none", "nested-in-class", typeof(L05Oc180.L05_180_validated_class_Uri), () => new Uri("https://example.com/a"), () => new Uri("HTTPS://EXAMPLE.COM/a"), () => new Uri("https://example.com/b")); - Add("validated-class", "bool", "base-depth-1", "nested-in-struct", typeof(L05Os181.L05_181_validated_class_bool), () => false, () => false, () => true); - Add("validated-class", "bool", "base-explicit-IEquatable", "nested-in-class", typeof(L05Oc182.L05_182_validated_class_bool), () => false, () => false, () => true); - Add("validated-class", "bool", "explicit-interface-member", "nested-in-struct", typeof(L05Os183.L05_183_validated_class_bool), () => false, () => false, () => true); - Add("validated-class", "byte", "base-depth-2", "nested-in-struct", typeof(L05Os184.L05_184_validated_class_byte), () => (byte)7, () => (byte)7, () => (byte)8); - Add("validated-class", "byte", "base-explicit-IEquatable", "nested-in-class", typeof(L05Oc185.L05_185_validated_class_byte), () => (byte)7, () => (byte)7, () => (byte)8); - Add("validated-class", "byte", "base-explicit-IEquatable", "nested-two-levels", typeof(L05Ot186.Inner.L05_186_validated_class_byte), () => (byte)7, () => (byte)7, () => (byte)8); - Add("validated-class", "byte", "explicit-interface-member", "nested-in-struct", typeof(L05Os187.L05_187_validated_class_byte), () => (byte)7, () => (byte)7, () => (byte)8); - Add("validated-class", "char", "base-depth-2", "nested-in-class", typeof(L05Oc188.L05_188_validated_class_char), () => 'a', () => 'a', () => 'b'); - Add("validated-class", "char", "base-depth-3", "nested-in-class", typeof(L05Oc189.L05_189_validated_class_char), () => 'a', () => 'a', () => 'b'); - Add("validated-class", "char", "base-depth-3", "nested-two-levels", typeof(L05Ot190.Inner.L05_190_validated_class_char), () => 'a', () => 'a', () => 'b'); - Add("validated-class", "char", "base-explicit-IEquatable", "nested-two-levels", typeof(L05Ot191.Inner.L05_191_validated_class_char), () => 'a', () => 'a', () => 'b'); - Add("validated-class", "char", "base-explicit-IEquatable", "top-level", typeof(L05_192_validated_class_char), () => 'a', () => 'a', () => 'b'); - Add("validated-class", "decimal", "base-depth-1", "top-level", typeof(L05_193_validated_class_decimal), () => 7.8m, () => 7.80m, () => 7.81m); - Add("validated-class", "decimal", "base-depth-2", "nested-two-levels", typeof(L05Ot194.Inner.L05_194_validated_class_decimal), () => 7.8m, () => 7.80m, () => 7.81m); - Add("validated-class", "decimal", "base-depth-2", "top-level", typeof(L05_195_validated_class_decimal), () => 7.8m, () => 7.80m, () => 7.81m); - Add("validated-class", "decimal", "none", "nested-in-struct", typeof(L05Os196.L05_196_validated_class_decimal), () => 7.8m, () => 7.80m, () => 7.81m); - Add("validated-class", "decimal", "none", "top-level", typeof(L05_197_validated_class_decimal), () => 7.8m, () => 7.80m, () => 7.81m); - Add("validated-class", "double", "base-depth-2", "top-level", typeof(L05_198_validated_class_double), () => 0.0, () => -0.0, () => 1.5); - Add("validated-class", "double", "base-depth-3", "nested-in-struct", typeof(L05Os199.L05_199_validated_class_double), () => 0.0, () => -0.0, () => 1.5); - Add("validated-class", "double", "explicit-interface-member", "nested-in-struct", typeof(L05Os200.L05_200_validated_class_double), () => 0.0, () => -0.0, () => 1.5); - Add("validated-class", "enum", "base-depth-1", "nested-two-levels", typeof(L05Ot201.Inner.L05_201_validated_class_enum), () => PEnum.B, () => (PEnum)1, () => PEnum.C); - Add("validated-class", "enum", "base-depth-2", "top-level", typeof(L05_202_validated_class_enum), () => PEnum.B, () => (PEnum)1, () => PEnum.C); - Add("validated-class", "flags-enum", "base-explicit-IEquatable", "nested-in-class", typeof(L05Oc203.L05_203_validated_class_flags_enum), () => PFlags.P | PFlags.Q, () => (PFlags)3, () => PFlags.R); - Add("validated-class", "flags-enum", "explicit-interface-member", "nested-two-levels", typeof(L05Ot204.Inner.L05_204_validated_class_flags_enum), () => PFlags.P | PFlags.Q, () => (PFlags)3, () => PFlags.R); - Add("validated-class", "int", "base-depth-1", "nested-in-struct", typeof(L05Os205.L05_205_validated_class_int), () => 42, () => 42, () => 43); - Add("validated-class", "int", "base-depth-3", "nested-in-class", typeof(L05Oc206.L05_206_validated_class_int), () => 42, () => 42, () => 43); - Add("validated-class", "int", "explicit-interface-member", "nested-in-class", typeof(L05Oc207.L05_207_validated_class_int), () => 42, () => 42, () => 43); - Add("validated-class", "long", "base-depth-1", "nested-in-class", typeof(L05Oc208.L05_208_validated_class_long), () => 4200000000L, () => 4200000000L, () => 4200000001L); - Add("validated-class", "long", "base-depth-2", "nested-two-levels", typeof(L05Ot209.Inner.L05_209_validated_class_long), () => 4200000000L, () => 4200000000L, () => 4200000001L); - Add("validated-class", "long", "base-explicit-IEquatable", "nested-in-class", typeof(L05Oc210.L05_210_validated_class_long), () => 4200000000L, () => 4200000000L, () => 4200000001L); - Add("validated-class", "long", "base-explicit-IEquatable", "nested-in-struct", typeof(L05Os211.L05_211_validated_class_long), () => 4200000000L, () => 4200000000L, () => 4200000001L); - Add("validated-class", "long", "explicit-interface-member", "nested-two-levels", typeof(L05Ot212.Inner.L05_212_validated_class_long), () => 4200000000L, () => 4200000000L, () => 4200000001L); - Add("validated-class", "own-record-class", "base-depth-1", "nested-in-struct", typeof(L05Os213.L05_213_validated_class_own_record_class), () => new PRec { A = "x", B = 1 }, () => new PRec { A = new string("x".ToCharArray()), B = 1 }, () => new PRec { A = "y", B = 1 }); - Add("validated-class", "own-record-class", "base-depth-1", "nested-two-levels", typeof(L05Ot214.Inner.L05_214_validated_class_own_record_class), () => new PRec { A = "x", B = 1 }, () => new PRec { A = new string("x".ToCharArray()), B = 1 }, () => new PRec { A = "y", B = 1 }); - Add("validated-class", "own-record-class", "base-depth-2", "top-level", typeof(L05_215_validated_class_own_record_class), () => new PRec { A = "x", B = 1 }, () => new PRec { A = new string("x".ToCharArray()), B = 1 }, () => new PRec { A = "y", B = 1 }); - Add("validated-class", "own-record-class", "base-depth-3", "nested-in-struct", typeof(L05Os216.L05_216_validated_class_own_record_class), () => new PRec { A = "x", B = 1 }, () => new PRec { A = new string("x".ToCharArray()), B = 1 }, () => new PRec { A = "y", B = 1 }); - Add("validated-class", "own-record-class", "base-depth-3", "nested-two-levels", typeof(L05Ot217.Inner.L05_217_validated_class_own_record_class), () => new PRec { A = "x", B = 1 }, () => new PRec { A = new string("x".ToCharArray()), B = 1 }, () => new PRec { A = "y", B = 1 }); - Add("validated-class", "own-record-class", "base-explicit-IEquatable", "nested-in-struct", typeof(L05Os218.L05_218_validated_class_own_record_class), () => new PRec { A = "x", B = 1 }, () => new PRec { A = new string("x".ToCharArray()), B = 1 }, () => new PRec { A = "y", B = 1 }); - Add("validated-class", "own-record-class", "none", "nested-in-class", typeof(L05Oc219.L05_219_validated_class_own_record_class), () => new PRec { A = "x", B = 1 }, () => new PRec { A = new string("x".ToCharArray()), B = 1 }, () => new PRec { A = "y", B = 1 }); - Add("validated-class", "string", "base-depth-1", "nested-in-struct", typeof(L05Os220.L05_220_validated_class_string), () => "alpha", () => new string("alpha".ToCharArray()), () => "beta"); - Add("validated-class", "string", "base-depth-2", "nested-two-levels", typeof(L05Ot221.Inner.L05_221_validated_class_string), () => "alpha", () => new string("alpha".ToCharArray()), () => "beta"); - Add("validated-class", "string", "base-depth-3", "nested-in-class", typeof(L05Oc222.L05_222_validated_class_string), () => "alpha", () => new string("alpha".ToCharArray()), () => "beta"); - Add("validated-class", "string", "explicit-interface-member", "top-level", typeof(L05_223_validated_class_string), () => "alpha", () => new string("alpha".ToCharArray()), () => "beta"); - Add("validated-class", "ulong", "base-depth-1", "nested-in-struct", typeof(L05Os224.L05_224_validated_class_ulong), () => 4200000000UL, () => 4200000000UL, () => 4200000001UL); - Add("validated-class", "ulong", "base-depth-1", "top-level", typeof(L05_225_validated_class_ulong), () => 4200000000UL, () => 4200000000UL, () => 4200000001UL); - Add("validated-class", "ulong", "base-depth-2", "top-level", typeof(L05_226_validated_class_ulong), () => 4200000000UL, () => 4200000000UL, () => 4200000001UL); - Add("validated-class", "ulong", "base-depth-3", "nested-in-class", typeof(L05Oc227.L05_227_validated_class_ulong), () => 4200000000UL, () => 4200000000UL, () => 4200000001UL); - Add("validated-class", "ulong", "explicit-interface-member", "nested-in-class", typeof(L05Oc228.L05_228_validated_class_ulong), () => 4200000000UL, () => 4200000000UL, () => 4200000001UL); - Add("validated-class", "ulong", "explicit-interface-member", "nested-in-struct", typeof(L05Os229.L05_229_validated_class_ulong), () => 4200000000UL, () => 4200000000UL, () => 4200000001UL); - Add("validated-class", "ulong", "none", "nested-in-struct", typeof(L05Os230.L05_230_validated_class_ulong), () => 4200000000UL, () => 4200000000UL, () => 4200000001UL); - Add("validated-struct", "DateOnly", "explicit-interface-member", "nested-two-levels", typeof(L05Ot231.Inner.L05_231_validated_struct_DateOnly), () => new DateOnly(2026, 10, 15), () => DateOnly.FromDayNumber(new DateOnly(2026, 10, 15).DayNumber), () => new DateOnly(2026, 10, 16)); - Add("validated-struct", "DateOnly", "none", "top-level", typeof(L05_232_validated_struct_DateOnly), () => new DateOnly(2026, 10, 15), () => DateOnly.FromDayNumber(new DateOnly(2026, 10, 15).DayNumber), () => new DateOnly(2026, 10, 16)); - Add("validated-struct", "DateTime", "explicit-interface-member", "nested-two-levels", typeof(L05Ot233.Inner.L05_233_validated_struct_DateTime), () => new DateTime(2026, 10, 15, 8, 30, 0, DateTimeKind.Utc), () => new DateTime(2026, 10, 15, 8, 30, 0, DateTimeKind.Unspecified), () => new DateTime(2026, 10, 15, 8, 31, 0, DateTimeKind.Utc)); - Add("validated-struct", "DateTimeOffset", "none", "nested-in-struct", typeof(L05Os234.L05_234_validated_struct_DateTimeOffset), () => new DateTimeOffset(2026, 10, 15, 8, 30, 0, TimeSpan.Zero), () => new DateTimeOffset(2026, 10, 15, 10, 30, 0, TimeSpan.FromHours(2)), () => new DateTimeOffset(2026, 10, 15, 8, 31, 0, TimeSpan.Zero)); - Add("validated-struct", "DateTimeOffset", "none", "top-level", typeof(L05_235_validated_struct_DateTimeOffset), () => new DateTimeOffset(2026, 10, 15, 8, 30, 0, TimeSpan.Zero), () => new DateTimeOffset(2026, 10, 15, 10, 30, 0, TimeSpan.FromHours(2)), () => new DateTimeOffset(2026, 10, 15, 8, 31, 0, TimeSpan.Zero)); - Add("validated-struct", "Guid", "none", "nested-in-class", typeof(L05Oc236.L05_236_validated_struct_Guid), () => Guid.Parse("11111111-1111-1111-1111-111111111111"), () => new Guid("11111111111111111111111111111111"), () => Guid.Parse("22222222-2222-2222-2222-222222222222")); - Add("validated-struct", "Guid", "none", "nested-in-struct", typeof(L05Os237.L05_237_validated_struct_Guid), () => Guid.Parse("11111111-1111-1111-1111-111111111111"), () => new Guid("11111111111111111111111111111111"), () => Guid.Parse("22222222-2222-2222-2222-222222222222")); - Add("validated-struct", "NodaTime.Instant", "none", "top-level", typeof(L05_238_validated_struct_NodaTime_Instant), () => global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 30), () => global::NodaTime.Instant.FromUnixTimeTicks(global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 30).ToUnixTimeTicks()), () => global::NodaTime.Instant.FromUtc(2026, 10, 15, 8, 31)); - Add("validated-struct", "TimeOnly", "explicit-interface-member", "nested-in-class", typeof(L05Oc239.L05_239_validated_struct_TimeOnly), () => new TimeOnly(8, 30, 15), () => new TimeOnly(new TimeOnly(8, 30, 15).Ticks), () => new TimeOnly(8, 30, 16)); - Add("validated-struct", "TimeOnly", "none", "top-level", typeof(L05_240_validated_struct_TimeOnly), () => new TimeOnly(8, 30, 15), () => new TimeOnly(new TimeOnly(8, 30, 15).Ticks), () => new TimeOnly(8, 30, 16)); - Add("validated-struct", "TimeSpan", "none", "nested-in-class", typeof(L05Oc241.L05_241_validated_struct_TimeSpan), () => TimeSpan.FromMinutes(90), () => TimeSpan.FromSeconds(5400), () => TimeSpan.FromMinutes(91)); - Add("validated-struct", "Uri", "explicit-interface-member", "nested-in-struct", typeof(L05Os242.L05_242_validated_struct_Uri), () => new Uri("https://example.com/a"), () => new Uri("HTTPS://EXAMPLE.COM/a"), () => new Uri("https://example.com/b")); - Add("validated-struct", "Uri", "explicit-interface-member", "top-level", typeof(L05_243_validated_struct_Uri), () => new Uri("https://example.com/a"), () => new Uri("HTTPS://EXAMPLE.COM/a"), () => new Uri("https://example.com/b")); - Add("validated-struct", "Uri", "none", "nested-in-class", typeof(L05Oc244.L05_244_validated_struct_Uri), () => new Uri("https://example.com/a"), () => new Uri("HTTPS://EXAMPLE.COM/a"), () => new Uri("https://example.com/b")); - Add("validated-struct", "bool", "explicit-interface-member", "nested-in-class", typeof(L05Oc245.L05_245_validated_struct_bool), () => false, () => false, () => true); - Add("validated-struct", "bool", "none", "nested-two-levels", typeof(L05Ot246.Inner.L05_246_validated_struct_bool), () => false, () => false, () => true); - Add("validated-struct", "byte", "explicit-interface-member", "top-level", typeof(L05_247_validated_struct_byte), () => (byte)7, () => (byte)7, () => (byte)8); - Add("validated-struct", "char", "explicit-interface-member", "nested-in-class", typeof(L05Oc248.L05_248_validated_struct_char), () => 'a', () => 'a', () => 'b'); - Add("validated-struct", "decimal", "explicit-interface-member", "top-level", typeof(L05_249_validated_struct_decimal), () => 7.8m, () => 7.80m, () => 7.81m); - Add("validated-struct", "double", "explicit-interface-member", "nested-two-levels", typeof(L05Ot250.Inner.L05_250_validated_struct_double), () => 0.0, () => -0.0, () => 1.5); - Add("validated-struct", "double", "none", "nested-two-levels", typeof(L05Ot251.Inner.L05_251_validated_struct_double), () => 0.0, () => -0.0, () => 1.5); - Add("validated-struct", "enum", "none", "nested-in-class", typeof(L05Oc252.L05_252_validated_struct_enum), () => PEnum.B, () => (PEnum)1, () => PEnum.C); - Add("validated-struct", "enum", "none", "nested-in-struct", typeof(L05Os253.L05_253_validated_struct_enum), () => PEnum.B, () => (PEnum)1, () => PEnum.C); - Add("validated-struct", "enum", "none", "nested-two-levels", typeof(L05Ot254.Inner.L05_254_validated_struct_enum), () => PEnum.B, () => (PEnum)1, () => PEnum.C); - Add("validated-struct", "flags-enum", "none", "nested-two-levels", typeof(L05Ot255.Inner.L05_255_validated_struct_flags_enum), () => PFlags.P | PFlags.Q, () => (PFlags)3, () => PFlags.R); - Add("validated-struct", "flags-enum", "none", "top-level", typeof(L05_256_validated_struct_flags_enum), () => PFlags.P | PFlags.Q, () => (PFlags)3, () => PFlags.R); - Add("validated-struct", "int", "explicit-interface-member", "nested-in-struct", typeof(L05Os257.L05_257_validated_struct_int), () => 42, () => 42, () => 43); - Add("validated-struct", "int", "explicit-interface-member", "nested-two-levels", typeof(L05Ot258.Inner.L05_258_validated_struct_int), () => 42, () => 42, () => 43); - Add("validated-struct", "int", "none", "nested-two-levels", typeof(L05Ot259.Inner.L05_259_validated_struct_int), () => 42, () => 42, () => 43); - Add("validated-struct", "int", "none", "top-level", typeof(L05_260_validated_struct_int), () => 42, () => 42, () => 43); - Add("validated-struct", "long", "explicit-interface-member", "nested-in-struct", typeof(L05Os261.L05_261_validated_struct_long), () => 4200000000L, () => 4200000000L, () => 4200000001L); - Add("validated-struct", "long", "explicit-interface-member", "top-level", typeof(L05_262_validated_struct_long), () => 4200000000L, () => 4200000000L, () => 4200000001L); - Add("validated-struct", "own-record-class", "explicit-interface-member", "top-level", typeof(L05_263_validated_struct_own_record_class), () => new PRec { A = "x", B = 1 }, () => new PRec { A = new string("x".ToCharArray()), B = 1 }, () => new PRec { A = "y", B = 1 }); - Add("validated-struct", "own-record-class", "none", "nested-in-struct", typeof(L05Os264.L05_264_validated_struct_own_record_class), () => new PRec { A = "x", B = 1 }, () => new PRec { A = new string("x".ToCharArray()), B = 1 }, () => new PRec { A = "y", B = 1 }); - Add("validated-struct", "string", "explicit-interface-member", "nested-two-levels", typeof(L05Ot265.Inner.L05_265_validated_struct_string), () => "alpha", () => new string("alpha".ToCharArray()), () => "beta"); - Add("validated-struct", "string", "none", "nested-in-class", typeof(L05Oc266.L05_266_validated_struct_string), () => "alpha", () => new string("alpha".ToCharArray()), () => "beta"); - Add("validated-struct", "string", "none", "top-level", typeof(L05_267_validated_struct_string), () => "alpha", () => new string("alpha".ToCharArray()), () => "beta"); - Add("validated-struct", "ulong", "explicit-interface-member", "top-level", typeof(L05_268_validated_struct_ulong), () => 4200000000UL, () => 4200000000UL, () => 4200000001UL); - Add("validated-struct", "ulong", "none", "top-level", typeof(L05_269_validated_struct_ulong), () => 4200000000UL, () => 4200000000UL, () => 4200000001UL); - } -} diff --git a/tools/probe/corpus/harness/OffTheWire.cs b/tools/probe/corpus/harness/OffTheWire.cs deleted file mode 100644 index eae52bbf..00000000 --- a/tools/probe/corpus/harness/OffTheWire.cs +++ /dev/null @@ -1,221 +0,0 @@ -// Claim L06-off-the-wire (README.md#what-typekit-guarantees): "Option and Result stay off the wire. System.Text.Json throws -// NotSupportedException for either, in both directions, instead of writing {} that reads back as None. [...] If you want a wire format of -// your own, a converter you register on the options takes precedence." The message names the type (src/CodoMetis.TypeKit/README.md -// "Option or Result in JSON"); a source-generated context refuses an Option? or Result<...>? property with the serializer's own -// InvalidOperationException when it builds that contract (ibid., "Good to know"). -// Oracle: exception, in every position, with the row's options and input, for an Option or Result produced by the row's operation. A -// converter of the application's for the same type in the same row is the baseline: it must win in both directions, and where the options -// leave the member out (it writes and reads nothing there), no converter runs, which is intended ("A missing property reaches no converter"). -#pragma warning disable CMTK0001 // default-instance: the default Option and Result are the shapes under test -using System.Reflection; -using System.Text.Json; -using System.Text.Json.Serialization; -using CodoMetis.TypeKit; -using Probes.Shapes; -namespace Probes.Json; - -/// An application's own converter for an Option or Result: writes "own-or" and reads it back as the value it was made with, counting reads. -public sealed class OwnOrConverter(T value) : JsonConverter -{ - public int Reads; - public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - Reads++; - if (reader.TokenType is not (JsonTokenType.String or JsonTokenType.PropertyName) || reader.GetString() != "own-or") throw new JsonException("not own-or"); - return value; - } - public override void Write(Utf8JsonWriter writer, T v, JsonSerializerOptions options) => writer.WriteStringValue("own-or"); - public override T ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => Read(ref reader, typeToConvert, options); - public override void WriteAsPropertyName(Utf8JsonWriter writer, T v, JsonSerializerOptions options) => writer.WritePropertyName("own-or"); -} - -public static class OffTheWire -{ - public static async Task Check(Row r) - { - var kind = r["shape.kind"]; - var e = kind == "closed-set" ? Registry.Get(kind, "-") : Registry.Get(kind, r["shape.wrapped"]); - var position = r["shape.position"]; - var containing = position == "root" ? "class" : r["shape.containing"]; - var shape = r["or.shape"]; var op = r["or.operation"]; - _ = r["input.json"]; // the row's input, which every check of the row runs under; the read applies it - if (shape == "Result-E" && op is "query-syntax" or "FirstOrNone") - { r.Substituted("or.operation", "Match", "Result has no " + op + " (claims.json excludes the pair)"); op = "Match"; } - var voValue = Make.Vo(e, e.Valid()); - var v = Make.Held(e.Vo); - var produce = typeof(OffTheWire).GetMethod(nameof(Produce), BindingFlags.NonPublic | BindingFlags.Static)!.MakeGenericMethod(v); - var (orType, orValue) = await (Task<(Type, object)>)produce.Invoke(null, [shape, op, voValue])!; - var key = shape == "Option" ? "O" : shape == "Result-E" ? "RE" : "RTE"; - var pb = typeof(OffTheWire).Assembly.GetType($"Probes.Json.L06PB_{v.Name}_{key}")!; var pd = typeof(OffTheWire).Assembly.GetType($"Probes.Json.L06PD_{v.Name}_{key}")!; - var eo = e with { Vo = orType, Plain = orType, PolyBase = pb, PolyDerived = pd, PolyBasePlain = pb, PolyDerivedPlain = pd }; - var hv = Pos.Build(position, containing, e, true, voValue); // the value object's shape: its document is the read's input - var ho = Pos.Build(position, containing, eo, true, orValue); - var o = JCtx.Apply(r, e, hv.Declared, ho.Declared); - var own = (JsonConverter)Activator.CreateInstance(typeof(OwnOrConverter<>).MakeGenericType(orType), orValue)!; - var oOwn = new JsonSerializerOptions(o); oOwn.Converters.Insert(0, own); - int OwnReads() => (int)own.GetType().GetField("Reads")!.GetValue(own)!; - var name = Jx.Name(orType); - // not-promised a-source-generated-context-refuses-a-nullable - var nullableUnderContext = r.Is("json.TypeInfoResolver", "source-generated-context") && position == "nullable-property"; - string? Refused(Exception? x, string dir) - { - if (nullableUnderContext) - return x is InvalidOperationException ? null : $"{dir}: a source-generated context refuses an {name}? property with the serializer's InvalidOperationException; got {(x is null ? "no exception" : Jx.Ex(x))}"; - if (x is not NotSupportedException) return $"{dir}: expected NotSupportedException naming {name}, got {(x is null ? "no exception" : Jx.Ex(x))}"; - // TypeKit's refusal, told by its type and never by its words: the serializer passes a converter's NotSupportedException on as the - // inner exception of its own, so one in the chain is thrown by NotWireTypeJsonConverterFactory's converter, by nameof - if (!ThrownBy(x, typeof(NotWireTypeJsonConverterFactory))) return $"{dir}: the NotSupportedException is not {nameof(NotWireTypeJsonConverterFactory)}'s: {Jx.Ex(x)}"; - return null; - } - - // the baseline: the application's own converter for the type wins, in both directions - var (jOwn, xOwn) = Jx.Ser(ho, oOwn); - var (jv, xv) = Jx.Ser(hv, o); - var refused = Baseline.Refusal; - var fw = r.Check("never-written", refused, c => - { - if (xOwn is not null) - { - if (xv is not null && xv.GetType() == xOwn.GetType()) return c.Inconclusive("write: the options refuse the shape itself, with the application's converter alike: " + Jx.Ex(xv)); - return $"write: a converter registered on the options for {name} does not take precedence: {Jx.Ex(xOwn)}"; - } - var ownWritten = Jx.Tokens(jOwn!).Any(t => t.Text == "own-or"); - var (jo, xo) = Jx.Ser(ho, o); - if (xo is null) - { - if (ownWritten || jo != jOwn) return $"write: {name} was written: {Jx.Cut(jo)}"; - // the options leave the member out, the application's converter's document alike: no converter ran - c.Note(NoteKeys.JsonMemberLeftOutByOptions, "write: " + Jx.Cut(jo)); - return c.Inconclusive("write: the options leave the member out, so no converter runs: " + Jx.Cut(jo)); - } - if (Refused(xo, "write") is { } f) return f; - c.Note(NoteKeys.Compared, $"{name} by {op}, {position}: write refused"); - return null; - }); - // reading: the value object's document with the row's input, where the Option or Result stands; measured before the check, so that - // a document the serializer refuses before the member (side (1), Sides) decides the check's baseline - var full = Jx.Full(o); - var (jf, xf) = Jx.Ser(hv, full); - // does the application's converter get called there? Its own document, read under the same options, says so - var (jOwnFull, _) = Jx.Ser(ho, Jx.Full(oOwn)); - string input = r["input.json"], text = ""; - if (xf is null) text = JsonParity.InputFor(input, e, full, jf!, position); - else if (jOwnFull is not null) { input = "valid"; text = jOwnFull; } - var reads = xOwn is null && (xf is null || jOwnFull is not null); - var (_, xOwnRead) = reads ? Jx.De(jOwnFull ?? "null", ho.Declared, oOwn) : (null, null); - var memberRead = reads && OwnReads() > 0; - var (vr, xr) = reads ? Jx.De(text, ho.Declared, o) : (null, null); - // side (1): the same document with the value object where the Option or Result stands, read under the same options; refusing alike - // (the same exception type at the same path, no message compared), the serializer refused the document before the member - var three = new Sides.Answer("read", null, xr); - var one = new Sides.Answer("read", null, reads && xr is not null && !ThrownBy(xr, typeof(NotWireTypeJsonConverterFactory)) ? Jx.De(text, hv.Declared, o).Ex : null); - var framework = Sides.Alike(one, three) ? Sides.Measured(one, "the document with the value object where the Option or Result stands") : null; - var fr = await Sides.Check(r, "never-read", refused, framework, three, null, null, async c => - { - await Task.CompletedTask; - if (xOwn is not null) return c.Inconclusive("read: the options refuse the shape itself, so no document has the member: " + Jx.Ex(xOwn)); - _ = c["input.json"]; - if (xf is not null) - { - // the value object's own shape cannot be written here (a value object over a class under Preserve, a class as a key): the - // document with the application's converter has a token where the Option or Result stands, and is read as it is - if (jOwnFull is null) return c.Inconclusive("read: no document has the member: " + Jx.Ex(xf)); - c.Substituted("input.json", "valid", "the value object's document cannot be written: " + xf.GetType().Name); - } - if (xOwnRead is not null) return $"read: a converter registered on the options for {name} does not take precedence: {Jx.Ex(xOwnRead)}"; - if (xr is null && input == "null" && position == "in-patch" && Jx.Member(ho.Declared, vr) is { } patch && patch.GetType().GetProperty("State")!.GetValue(patch)!.ToString() == "Null") - // a JSON null is the patch's own null (docs/merge-patches.md): it holds no Option or Result, and none was read - return c.Inapplicable(Cite.Docs("docs/merge-patches.md#tell-absent-null-and-a-value-apart"), $"read null: the Patch<{name}> is null, nothing of {name} read"); - if (xr is null) - { - if (memberRead) return $"read {input}: {name} was read without an exception\n text={Jx.Cut(text)}"; - c.Note(NoteKeys.JsonMemberLeftOutByOptions, "read " + input); - return c.Inapplicable(Cite.NotPromised("a-missing-property-reaches-no-converter"), "read " + input); - } - if (Refused(xr, "read " + input) is { } f) return f + $"\n text={Jx.Cut(text)}"; - c.Note(NoteKeys.Compared, $"{name} by {op}, {position}: read {input} refused"); - return null; - }); - return fw ?? fr; - } - - /// - /// Whether a refusal, or one in its chain of inner exceptions, was thrown by a converter that declares: the - /// method that threw it (its TargetSite) belongs to the factory's type or to a type nested in it. Told by types, never by a message. - /// - static bool ThrownBy(Exception? x, Type factory) - { - for (; x is not null; x = x.InnerException) - for (var t = x.TargetSite?.DeclaringType; t is not null; t = t.DeclaringType) - if (t == factory) return true; - return false; - } - - static async Task<(Type, object)> Produce(string shape, string op, V v) where V : notnull - { - switch (shape) - { - case "Option": - { - Option x = op switch - { - "Match" => Option.Some(v).Match(s => Option.Some(s), () => Option.None()), - "Bind-Map-chain" => Option.Some(v).Bind(s => Option.Some(s)).Map(s => s), - "query-syntax" => from s in Option.Some(v) select s, - "Sequence-Transpose" => Option.Some(Result.Success(v)).Transpose().Match(s => s, _ => Option.None()), - "Result.Catch" => Result.Catch(() => v).ToOption(), - "FirstOrNone" => new[] { v }.FirstOrNone(), - "default-instance" => default, - "after-await-elsewhere" => await Task.Run(() => Option.Some(v)).ConfigureAwait(false), - _ => throw new ArgumentException(op), - }; - return (typeof(Option), x); - } - case "Result-E": - { - Result x = op switch - { - "Match" => Result.Error(v).Match(() => Result.Success(), er => Result.Error(er)), - "Bind-Map-chain" => Result.Success().Bind(() => Result.Error(v)), - "Sequence-Transpose" => new[] { Result.Error(v) }.Sequence(), - "Result.Catch" => Result.Catch(() => { }).MapError(_ => v), - "default-instance" => default, - "after-await-elsewhere" => await Task.Run(() => Result.Error(v)).ConfigureAwait(false), - _ => throw new ArgumentException(op), - }; - return (typeof(Result), x); - } - default: - { - Result x; - if (shape == "Task-of-Result") - { - var ok = Task.FromResult(Result.Success(v)); - x = op switch - { - "Bind-Map-chain" => await ok.BindAsync(s => Result.Success(s)).MapAsync(s => s), - "FirstOrNone" => await Task.FromResult(new[] { v }.FirstOrNone()).ToResultAsync(ShapeFault.Refused), - "Result.Catch" => await Result.CatchAsync(() => Task.FromResult(v)).MapErrorAsync(_ => ShapeFault.Refused), - _ => await Task.FromResult(await Sync(op, v)).ConfigureAwait(false), - }; - } - else if (shape == "ValueTask-of-Result") x = await new ValueTask>(await Sync(op, v)); - else x = await Sync(op, v); - return (typeof(Result), x); - } - } - } - - static async Task> Sync(string op, V v) where V : notnull => op switch - { - "Match" => Result.Success(v).Match(s => Result.Success(s), er => Result.Error(er)), - "Bind-Map-chain" => Result.Success(v).Bind(s => Result.Success(s)).Map(s => s), - "query-syntax" => from s in Result.Success(v) select s, - "Sequence-Transpose" => new[] { Result.Success(v) }.Sequence().Map(l => l[0]), - "Result.Catch" => Result.Catch(() => v).MapError(_ => ShapeFault.Refused), - "FirstOrNone" => new[] { v }.FirstOrNone().ToResult(ShapeFault.Refused), - "default-instance" => default, - "after-await-elsewhere" => await Task.Run(() => Result.Success(v)).ConfigureAwait(false), - _ => throw new ArgumentException(op), - }; -} diff --git a/tools/probe/corpus/harness/Positions.cs b/tools/probe/corpus/harness/Positions.cs deleted file mode 100644 index 800e60ba..00000000 --- a/tools/probe/corpus/harness/Positions.cs +++ /dev/null @@ -1,328 +0,0 @@ -// Builds the value a probe serializes for a row's shape.position x shape.containing (VO side and plain side), and the JSON parity oracle. -using System.Collections; -using System.Collections.Immutable; -using System.Globalization; -using System.Reflection; -using System.Text.Json; -using CodoMetis.TypeKit; -namespace Probes.Shapes; - -public sealed record Held(Type Declared, object Value); - -public static class Pos -{ - static Type Family(string containing, string style, Type m) - { - var n = containing.Replace('-', '_'); - var arity = containing == "generic-type" ? 2 : 1; - var open = typeof(PSet).Assembly.GetType($"Probes.Shapes.Cn_{n}_{style}`{arity}") ?? throw new InvalidOperationException($"no container Cn_{n}_{style}"); - return arity == 2 ? open.MakeGenericType(m, typeof(int)) : open.MakeGenericType(m); - } - - public static Held Build(string position, string containing, Entry e, bool vo, object sample) - { - var t = vo ? e.Vo : e.Plain; - if (position == "root") return new(t, sample); - Type m; object member; string style = "P"; - switch (position) - { - case "property": case "required-init": case "field": case "record-ctor-parameter": - m = t; member = sample; - style = position switch { "field" => "F", "record-ctor-parameter" => "C", "required-init" => "R", _ => "P" }; - break; - case "nullable-property": m = t.IsValueType && Nullable.GetUnderlyingType(t) is null ? typeof(Nullable<>).MakeGenericType(t) : t; member = sample; break; - case "list-element": case "read-only-list-element": - { - var l = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(t))!; l.Add(sample); - member = l; m = position == "list-element" ? l.GetType() : typeof(IReadOnlyList<>).MakeGenericType(t); break; - } - case "array-element": { var a = Array.CreateInstance(t, 1); a.SetValue(sample, 0); member = a; m = a.GetType(); break; } - case "immutable-array-element": - { - var l = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(t))!; l.Add(sample); - var create = typeof(ImmutableArray).GetMethods().First(x => x.Name == "CreateRange" && x.GetParameters().Length == 1).MakeGenericMethod(t); - member = create.Invoke(null, [l])!; m = member.GetType(); break; - } - // a dictionary key is never null: a nullable twin (or, under http.nullability=nullable, a nullable value object) is a key of the type it holds - case "dictionary-key": { var d = (IDictionary)Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(Make.Held(t), typeof(int)))!; d.Add(sample, 1); member = d; m = d.GetType(); break; } - case "dictionary-value": { var d = (IDictionary)Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(typeof(string), t))!; d.Add("k", sample); member = d; m = d.GetType(); break; } - // a Patch holds no null (T : notnull): a nullable twin is patched as the type it holds, as a dictionary key is - case "in-patch": - member = typeof(Patch).GetMethod("Value")!.MakeGenericMethod(Make.Held(t)).Invoke(null, [sample])!; m = member.GetType(); break; - case "polymorphic-derived-property": - { - var derived = vo ? e.PolyDerived : e.PolyDerivedPlain; - var inst = Activator.CreateInstance(derived)!; derived.GetProperty("Value")!.SetValue(inst, sample); - member = inst; m = vo ? e.PolyBase : e.PolyBasePlain; break; - } - default: throw new ArgumentException("unknown position " + position); - } - var ct = Family(containing, style, m); - object c; - if (style == "C") c = Activator.CreateInstance(ct, member)!; - else - { - c = Activator.CreateInstance(ct)!; - if (style == "F") ct.GetField("F")!.SetValue(c, member); else ct.GetProperty("P")!.SetValue(c, member); - } - return new(ct, c); - } -} - -/// -/// Input equivalence for the differential oracles: the twin and the value object must hold the same shape under the same options, the -/// twin standing where the value object stands, at its nullability: both admit null or neither (shapes.cs makes the twin of a class value -/// object over a struct the struct made nullable). The one difference no plain type can mirror is a struct value object over a class, -/// which refuses the null its twin reads (not-promised a-struct-refuses-a-json-null), and a dictionary key and what a patch holds, which -/// are never null on either side. A mismatch is a harness error, never a pass or a product violation (Sides.Twins). -/// -public static class Equivalence -{ - public static string? Shapes(Type plain, Type vo, Entry e) => Shapes(plain, vo, e, key: false); - - static string? Shapes(Type plain, Type vo, Entry e, bool key) - { - if (plain == vo) return null; - if (Make.Held(vo) == Make.Held(e.Vo)) return Member(plain, vo, e, key); - if (plain.IsArray && vo.IsArray) return Shapes(plain.GetElementType()!, vo.GetElementType()!, e, false); - if (plain.IsGenericType && vo.IsGenericType && plain.GetGenericTypeDefinition() == vo.GetGenericTypeDefinition()) - { - var pa = plain.GetGenericArguments(); var va = vo.GetGenericArguments(); - // a dictionary's key and what a patch holds are never null, on either side - var notNull = plain.GetGenericTypeDefinition() == typeof(Dictionary<,>) || plain.GetGenericTypeDefinition() == typeof(Patch<>); - for (var i = 0; i < pa.Length; i++) if (Shapes(pa[i], va[i], e, notNull && i == 0) is { } why) return why; - return null; - } - // The generated stand-ins (the polymorphic bases and derived types): one per side by design, equal in - // structure: the same properties by name, each of an equivalent type, and the same polymorphism. - if (plain.IsClass && vo.IsClass && !plain.IsGenericType && !vo.IsGenericType) - { - var pp = plain.GetProperties().ToDictionary(x => x.Name); var vp = vo.GetProperties().ToDictionary(x => x.Name); - if (!pp.Keys.Order().SequenceEqual(vp.Keys.Order())) return $"{Name(plain)} and {Name(vo)} have different properties"; - foreach (var (name, prop) in pp) if (Shapes(prop.PropertyType, vp[name].PropertyType, e, false) is { } why) return $"{name}: {why}"; - var pd = plain.GetCustomAttributes(typeof(System.Text.Json.Serialization.JsonDerivedTypeAttribute), false).Length; - var vd = vo.GetCustomAttributes(typeof(System.Text.Json.Serialization.JsonDerivedTypeAttribute), false).Length; - if (pd != vd) return $"{Name(plain)} and {Name(vo)} declare {pd} and {vd} derived types"; - if ((plain.BaseType is { } pb && pb != typeof(object)) || (vo.BaseType is { } vb && vb != typeof(object))) - return Shapes(plain.BaseType!, vo.BaseType!, e, false); - return null; - } - return $"{Name(plain)} and {Name(vo)} are different shapes"; - } - - /// Where the value object stands: its twin, at its nullability. - static string? Member(Type plain, Type vo, Entry e, bool key) - { - if (Make.Held(plain) != Make.Held(e.Plain)) return $"{Name(plain)} stands where {Name(vo)} should face {Name(e.Plain)}"; - if (key || Make.AdmitsNull(plain) == Make.AdmitsNull(vo)) return null; - // a struct value object over a class refuses the JSON null its twin reads: no plain type mirrors it, the claims cite it - if (!Make.AdmitsNull(vo) && !Make.Held(e.Plain).IsValueType) return null; - return $"{Name(plain)} and {Name(vo)} differ in nullability: the twin {(Make.AdmitsNull(plain) ? "admits" : "refuses")} null, the value object {(Make.AdmitsNull(vo) ? "admits" : "refuses")} it"; - } - - public static string? Options(JsonSerializerOptions plain, JsonSerializerOptions vo, bool allowConverters) - { - if (ReferenceEquals(plain, vo)) return null; - foreach (var p in typeof(JsonSerializerOptions).GetProperties()) - { - if (p.GetIndexParameters().Length > 0 || (allowConverters && p.Name == "Converters")) continue; - var a = Print(p.GetValue(plain)); var b = Print(p.GetValue(vo)); - if (a != b) return $"options differ in {p.Name}: {a} vs {b}"; - } - return null; - } - - static string Print(object? v) => v switch - { - null => "null", - string or bool or char or int or Enum => v.ToString()!, - System.Collections.IEnumerable list => "[" + string.Join(",", list.Cast().Select(x => x?.GetType().FullName)) + "]", - _ => v.GetType().FullName!, - }; - - static string Name(Type t) => t.IsGenericType ? t.Name[..t.Name.IndexOf('`')] + "<" + string.Join(",", t.GetGenericArguments().Select(Name)) + ">" : t.Name; -} - -public static class JsonParity -{ - static (string? Json, Exception? Ex) Ser(Held h, JsonSerializerOptions o) - { try { return (JsonSerializer.Serialize(h.Value, h.Declared, o), null); } catch (Exception ex) { return (null, ex); } } - static (string? Json, Exception? Ex) De(string text, Held h, JsonSerializerOptions o) - { - try { var v = JsonSerializer.Deserialize(text, h.Declared, o); return (v is null ? "null" : JsonSerializer.Serialize(v, h.Declared, o), null); } - catch (Exception ex) { return (null, ex); } - } - - static string Limit(Entry e) => e.Wrapped switch - { - "int" => "2147483647", "long" => "9223372036854775807", "byte" => "255", "ulong" => "18446744073709551615", - "decimal" => "79228162514264337593543950335", "double" => "1.7976931348623157E+308", "bool" => "true", - "string" => "\"" + new string('x', 70000) + "\"", "char" => "\"\\uffff\"", "enum" => "2147483647", "flags-enum" => "7", - "DateTime" => "\"9999-12-31T23:59:59.9999999Z\"", "DateTimeOffset" => "\"9999-12-31T23:59:59.9999999+00:00\"", - "DateOnly" => "\"9999-12-31\"", "TimeOnly" => "\"23:59:59.9999999\"", "TimeSpan" => "\"10675199.02:48:05.4775807\"", - "Uri" => "\"https://example.com/" + new string('p', 3000) + "\"", "NodaTime.Instant" => "\"9999-12-31T23:59:59.999999999Z\"", - "own-record-class" => "{\"A\":\"x\",\"B\":2147483647}", "Guid" => "\"ffffffff-ffff-ffff-ffff-ffffffffffff\"", - _ => "\"Beta\"" - }; - - /// Text a position's JSON takes for an input.json value, built from the plain side's JSON by replacing the sample's own text. - public static string InputFor(string input, Entry e, JsonSerializerOptions o, string jp, string position) => Input(input, e, o, jp, position); - static string Input(string input, Entry e, JsonSerializerOptions o, string jp, string position) - { - string sample; - try { sample = JsonSerializer.Serialize(e.Valid(), e.Plain, o); } catch { return jp; } - var at = jp.IndexOf(sample, StringComparison.Ordinal); - string Swap(string with) => at < 0 ? jp : jp[..at] + with + jp[(at + sample.Length)..]; - switch (input) - { - case "valid": return jp; - case "refused-by-create": - if (e.Refused() is not { } rf) return jp; - try { return Swap(JsonSerializer.Serialize(rf, e.Plain, o)); } catch { return jp; } - case "wrong-token": return Swap(sample.StartsWith('"') || sample.StartsWith('{') ? "[]" : "\"zz\""); - case "null": return Swap("null"); - case "empty-string": return Swap("\"\""); - case "at-type-limit": return Swap(Limit(e)); - case "quoted-number": return Swap(sample.StartsWith('"') ? sample : "\"" + sample + "\""); - case "escape-sequence": - return sample.StartsWith('"') && sample.Length > 3 ? Swap(sample[..1] + "\\u00" + ((int)sample[1]).ToString("x2") + sample[2..]) : jp; - case "duplicate-property": return jp.StartsWith('{') && jp.Length > 2 ? "{" + jp[1..^1] + "," + jp[1..^1] + "}" : jp; - case "metadata-property": return jp.StartsWith('{') ? (jp.Length > 2 ? "{\"$id\":\"1\"," + jp[1..] : "{\"$id\":\"1\"}") : jp; - default: throw new ArgumentException("input " + input); - } - } - - static bool Validated(Entry e) => e.Kind.StartsWith("validated"); - - /// - /// The oracle "JSON parity" (differential) as L01's checks, three-sided (Sides): the value object's write (3) against the wrapped twin in - /// the same shape under the same options (2), held to the twin under the application's options less TypeKit's converters (1), as - /// the-underlying-types-json, or as-a-dictionary-key-too where the value object is a dictionary key; and the read of the twin's document - /// with the row's input.json, read-as-the-underlying-type-reads, alike. L01 promises the underlying type's JSON, so side (1) runs on every - /// row. Under an application's converter for the value object, which writes it instead of the generated one, the read is that - /// converter's own document read back. Every check runs; returns the first failure. - /// - public static string? Check(Row r, Entry e, JsonSerializerOptions o, string position, string containing, string input, JsonSerializerOptions? oPlain = null) - { - oPlain ??= o; - var plain = e.Valid(); - var voV = Make.Vo(e, plain); - var (hp, hv) = Sides.Twins(position, containing, e, plain, voV); - // Input equivalence of the options: the two sides may differ only by the substituted type, not in options (the shapes: Sides.Twins). - if (Equivalence.Options(oPlain, o, allowConverters: e.Kind == "closed-set") is { } whyOptions) { r.Outcome("harness-error", "inputs not equivalent: " + whyOptions); return null; } - var one = Sides.WithoutTypeKit(oPlain); - var w1 = Sides.Write(hp, one); var w2 = Sides.Write(hp, oPlain); var w3 = Sides.Write(hv, o); - var write = position == "dictionary-key" ? "as-a-dictionary-key-too" : "the-underlying-types-json"; - const string read = "read-as-the-underlying-type-reads"; - string? fw, fr; - if (r.Get("json.Converters") == "own-for-value-object") - { - // "Your own converter wins" (AspNetCore README#what-the-document-says): the application's converter writes the value object - // instead of the generated one, so the twin's bytes are none of its. claims.json declares the write inapplicable there. - fw = r.Check(write, Baseline.Twin, c => c.Inapplicable(Cite.Docs("src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says"), "write")); - fr = r.Check(read, Baseline.RoundTrip, c => Sides.Interferes(w1, w2) ?? OwnVoRead(c, e, hp, hv, oPlain, o, w3.Value, w3.Thrown, w2.Thrown)); - return fw ?? fr; - } - fw = Sides.Check(r, write, Baseline.Twin, Sides.Framework(r, e, hp, one, w1, w3), w3, w1, w2, c => Write(c, e, position, w2, w3)); - if (e.Wrapped == "byte" && position == "array-element") - fr = r.Check(read, Baseline.Twin, c => c.Inapplicable(Cite.NotPromised("a-byte-array-is-base64"), "read")); - else if (e.Kind == "closed-set" && position == "dictionary-key" && r.Get("json.DictionaryKeyPolicy") is not (null or "default")) - fr = r.Check(read, Baseline.Twin, c => c.Inapplicable(Cite.NotPromised("a-closed-set-key-is-its-name"), "read")); - else if (w2.Value is not { } jp) - fr = r.Check(read, Baseline.Twin, c => c.Inconclusive("read: the wrapped type is not written under these options, so there is no document to read")); - else - { - var text = Input(input, e, oPlain, jp, position); - var r1 = Sides.Read(text, hp, one); var r2 = Sides.Read(text, hp, oPlain); var r3 = Sides.Read(text, hv, o); - fr = Sides.Check(r, read, Baseline.Twin, Sides.Framework(r, e, hp, one, r1, r3), r3, r1, r2, c => Read(c, e, position, input, text, jp, r2, r3)); - } - return fw ?? fr; - } - - /// The value object's document (3) is the wrapped twin's (2), byte for byte, in the same shape under the same options; (1) and (3) refusing alike is decided before (Sides). - static string? Write(Check c, Entry e, string position, Sides.Answer p, Sides.Answer v) - { - // System.Text.Json writes byte[] as base64 and an array of value objects over byte as a JSON array (KB-038); claims.json declares it - if (e.Wrapped == "byte" && position == "array-element" && p.Value != v.Value) return c.Inapplicable(Cite.NotPromised("a-byte-array-is-base64"), "write"); - if (p.Refused && v.Refused) - // Both refused, not alike (Sides.Alike): evidence about refusing, not about serializing, and nothing to compare. - return c.Inconclusive($"write: both refuse, not alike: the wrapped type {p}, the value object {v}"); - if (!p.Refused && v.Refused) - { - if (c.Is("json.ReferenceHandler", "Preserve") && e.Wrapped == "own-record-class" && v.Thrown is NotSupportedException) return c.Refused(Cite.NotPromised("preserved-references-over-a-walked-class"), "write"); - return $"wrapped serialized ({p.Value}) but the value object threw {v.Thrown!.GetType().Name}: {v.Thrown.Message}"; - } - if (p.Refused) return $"value object serialized ({v.Value}) where the wrapped type threw {p.Thrown!.GetType().Name}: {p.Thrown.Message}"; - // a closed set travels as its name, which no key policy renames; claims.json declares it - if (p.Value != v.Value && e.Kind == "closed-set" && position == "dictionary-key" && c.Get("json.DictionaryKeyPolicy") is not (null or "default")) return c.Inapplicable(Cite.NotPromised("a-closed-set-key-is-its-name"), "write"); - if (p.Value != v.Value) return $"JSON differs: wrapped {Cut(p.Value)} vs value object {Cut(v.Value)}"; - c.Note(NoteKeys.Compared, "write"); - return null; - } - - /// - /// The wrapped twin's document, with the row's input.json, read into both shapes (and what each read written again): each reads it as the - /// other does, or refuses as a document says; (1) and (3) refusing alike is decided before (Sides). - /// - static string? Read(Check c, Entry e, string position, string input, string text, string jp, Sides.Answer p, Sides.Answer v) - { - var stored = c.Get("json.Converters") == "StoredJsonConverterFactory"; - if (p.Refused && v.Refused) return c.Inconclusive($"read {input}: both refuse, not alike: the wrapped type {p}, the value object {v}"); - if (!p.Refused && v.Refused) - { - var xv = v.Thrown!; - if (c.Is("json.ReferenceHandler", "Preserve") && e.Wrapped == "own-record-class" && xv is NotSupportedException) return c.Refused(Cite.NotPromised("preserved-references-over-a-walked-class"), "read"); - // The documented refusals, each the JsonException the generated converter throws. - if (!(xv is JsonException) && ((Validated(e) && input == "refused-by-create" && !stored) || (input == "null" && e.Vo.IsValueType) || (e.Kind == "closed-set" && input != "valid"))) - return $"input {input}: the refusal is {xv.GetType().Name}, not JsonException: {Cut(xv.Message)}"; - if (Validated(e) && input == "refused-by-create" && !stored) return c.Refused(Cite.NotPromised("a-validated-value-object-refuses-what-create-refuses"), $"read {input}"); - if (input == "null" && e.Vo.IsValueType) return c.Refused(Cite.NotPromised("a-struct-refuses-a-json-null"), $"read {input}"); - if (e.Kind == "closed-set" && input != "valid") return c.Refused(Cite.Docs("src/CodoMetis.TypeKit.Generators/README.md#matching-parsing-and-binding"), $"read {input}"); // "Unknown names are rejected." - return $"input {input}: wrapped read it, value object threw {xv.GetType().Name}: {Cut(xv.Message)}\n text={Cut(text)}"; - } - // a reference-type value object takes JSON null as null, as any class does - if (p.Refused && input == "null" && Make.AdmitsNull(e.Vo) && !Make.AdmitsNull(e.Plain)) return c.Inapplicable(Cite.Docs("docs/json.md#null-and-missing"), "read null"); - if (p.Refused) return $"input {input}: value object read what the wrapped type refused ({p.Thrown?.GetType().Name}: {Cut(p.Thrown?.Message)})\n text={Cut(text)}"; - if (Validated(e) && input == "refused-by-create" && text != jp && !stored) - return $"validated value object accepted the refused value{(position == "dictionary-key" ? " as a dictionary key" : "")}: {Cut(text)}"; - if (p.Value != v.Value) return $"input {input}: read-back differs: wrapped {Cut(p.Value)} vs value object {Cut(v.Value)}"; - c.Note(NoteKeys.Compared, "read " + input); - return null; - } - - /// - /// Under an application's converter for the value object, the generated converter does not run: the value object's document is that - /// converter's, and it reads back as written under the row's options. Where the value object's document cannot be written, a converter - /// the application registers for the wrapped type at the same position is the baseline. - /// - static string? OwnVoRead(Check c, Entry e, Held hp, Held hv, JsonSerializerOptions oPlain, JsonSerializerOptions o, string? jv, Exception? ev, Exception? ep) - { - var oOwn = new JsonSerializerOptions(oPlain); - oOwn.Converters.Insert(0, (System.Text.Json.Serialization.JsonConverter)Activator.CreateInstance(typeof(OwnWrappedConverter<>).MakeGenericType(Make.Held(e.Plain)), JsonRow.Inner(e))!); - var (jo, eo) = Ser(hp, oOwn); - if (ev is not null) - { - // System.Text.Json refuses any custom converter there (MaxDepth counts its value one level deeper, KB-040), the wrapped - // type's under a converter of the application's alike - if (ep is not null || (eo is not null && eo.GetType() == ev.GetType())) return c.Inconclusive($"the application's converter cannot write here, for the wrapped type too: {ev.GetType().Name}"); - return $"own VO converter row: VO serialization threw {ev.GetType().Name}: {ev.Message}"; - } - c.Note(NoteKeys.OwnVo); - var (back, eb) = De(jv!, hv, o); - if (eb is not null) - { - // the same converter for the wrapped type, at the same position: its own document does not read back under these options either - var ebOwn = jo is null ? eo : De(jo, hp, oOwn).Ex; - if (ebOwn is not null && ebOwn.GetType() == eb.GetType()) return c.Inconclusive($"the application's converter's document does not read back under these options, for the wrapped type too: {eb.GetType().Name}"); - return "own VO converter: read-back threw " + eb.GetType().Name + ": " + eb.Message; - } - if (back != jv) return $"own VO converter round trip differs: {jv} vs {back}"; - return null; - } - static string Cut(string? s) => s is null ? "(null)" : s.Length > 200 ? s[..200] + "..." : s; -} - -public static class EntryExt -{ - public static object? Refused(this Entry e) => e.Kind.StartsWith("validated") ? e.RefusedFn() : null; -} diff --git a/tools/probe/corpus/harness/PreserveGuard.cs b/tools/probe/corpus/harness/PreserveGuard.cs deleted file mode 100644 index 2f046f3d..00000000 --- a/tools/probe/corpus/harness/PreserveGuard.cs +++ /dev/null @@ -1,252 +0,0 @@ -// Claim C10-preserve-guard (src/CodoMetis.TypeKit.Generators/README.md#json): "Preserved references (ReferenceHandler.Preserve) throw a -// NotSupportedException where the serializer could write $id or $ref inside the underlying value. That's a class it walks as an object [...] -// These are unaffected: a value the serializer writes with a value converter (a number, a string, a Guid, a Uri), anything a converter of -// your own writes, on the options or on one property, a struct of such values, and ReferenceHandler.IgnoreCycles." -// Oracle: claim for the guarded rows (a value object over a class the serializer walks, under Preserve, with no converter of the -// application's for it: NotSupportedException naming the type and Preserve, both directions, never a document); differential for the -// unaffected rows (the value object's document is the wrapped type's under the same options, never an $id twice, and every read agrees -// with the wrapped type's, a refusal being the section's: a JsonException naming the value object, with no inner exception). -using System.Text.Json; -using Probes.Shapes; -namespace Probes.Json; - -public static class PreserveGuard -{ - public static string? Check(Row r) - { - var handler = r["handler"]; - var want = handler == "none" ? "default" : handler; - var row = r; - if (r.Get("json.ReferenceHandler") is { } jr && jr != want) - { - // one setting under two names: the claim's handler decides (claims.json excludes the differing pairs) - r.Substituted("json.ReferenceHandler", want, "handler is the same setting"); - row = r.With("json.ReferenceHandler", want); - } - var kind = r["shape.kind"]; - var e = kind == "closed-set" ? Registry.Get(kind, "-") : Registry.Get(kind, r["shape.wrapped"]); - var position = r["shape.position"]; - var containing = position == "root" ? "class" : r["shape.containing"]; - _ = r["input.json"]; // the row's input, which every check of the row runs under; the reads apply it - var plain = e.Valid(); var voValue = Make.Vo(e, plain); - var (hp, hv) = Sides.Twins(position, containing, e, plain, voValue); - var o = JCtx.Apply(row, e, hp.Declared, hv.Declared); - // A closed set travels as its name, which no naming policy renames and no converter for strings writes (docs/closed-sets.md). - var op = kind == "closed-set" && (row.Get("json.DictionaryKeyPolicy") is not (null or "default") || row.Is("json.Converters", "own-for-wrapped")) - ? JCtx.Apply(row.With("json.DictionaryKeyPolicy", "default").With("json.Converters", row.Is("json.Converters", "own-for-wrapped") ? "default" : row["json.Converters"]), e, hp.Declared, hv.Declared) - : o; - var ownWrapped = row.Is("json.Converters", "own-for-wrapped"); - var ownVo = row.Is("json.Converters", "own-for-value-object"); - var guarded = want == "Preserve" && e.Wrapped == "own-record-class" && !ownWrapped && !ownVo; - // The obligation the row's handler and converters select: a converter of the application's, the guarded class under Preserve, - // IgnoreCycles, and otherwise a value the serializer writes with a value converter, under Preserve or with no handler at all. - var obligation = ownWrapped || ownVo ? "own-converters-unaffected" : guarded ? "refused-where-references-could-be-written" - : want == "IgnoreCycles" ? "ignore-cycles-unaffected" : "value-converters-unaffected"; - return guarded ? Guarded(r, obligation, e, hp, hv, o) : Unaffected(r, obligation, e, hp, hv, op, o, ownVo); - } - - /// - /// The guarded rows: NotSupportedException naming the wrapped type and Preserve, in both directions, never a document. The read of the - /// wrapped type's document is three-sided (Sides): where the twin without TypeKit refuses it alike, the serializer refused the document - /// before any converter ran. - /// - static string? Guarded(Row r, string obligation, Entry e, Held hp, Held hv, JsonSerializerOptions o) - { - var wrapped = Make.Held(e.Plain); - string? Refusal(Exception? x, string what) - { - if (x is not NotSupportedException) return $"{what}: expected NotSupportedException naming {wrapped.Name} under ReferenceHandler.Preserve, got {(x is null ? "no exception" : Jx.Ex(x))}"; - if (!x.Message.Contains(wrapped.FullName!) || !x.Message.Contains("ReferenceHandler.Preserve")) return $"{what}: the NotSupportedException does not name {wrapped.FullName} and ReferenceHandler.Preserve: {x.Message}"; - return null; - } - var (jv, xv) = Jx.Ser(hv, o); - var (jp, xp) = Jx.Ser(hp, o); - var fw = r.Check(obligation, Baseline.Refusal, c => - { - if (jv is not null) - { - // the options may leave the member out of the document, for the wrapped type too: then no converter ran, and nothing was refused - if (jp is not null && !Jx.Tokens(jp).Any(t => t.Text == "x") && !Jx.Tokens(jv).Any(t => t.Text == "x")) - { c.Note(NoteKeys.JsonMemberLeftOutByOptions, "write: " + Jx.Cut(jv)); return c.Inconclusive("write: the options leave the member out, for the wrapped type too: " + Jx.Cut(jv)); } - return Refusal(null, "write") + $"; it wrote {Jx.Cut(jv)}" + (DuplicateId(jv) is { } dup ? $" with {dup} twice" : ""); - } - if (Refusal(xv, "write") is { } f) return f; - c.Note(NoteKeys.Compared, "write refused"); - return null; - }); - if (xp is not null) - return fw ?? r.Check(obligation, Baseline.Refusal, c => c.Inconclusive("read: the wrapped type's document is refused under these options, so there is none to read: " + Jx.Ex(xp))); - // reading: the wrapped type's own document, with the row's input applied - var input = r["input.json"]; - var text = JsonParity.InputFor(input, e, Jx.Full(o), Jx.Ser(hp, Jx.Full(o)).Json ?? jp!, r["shape.position"]); - var (vp, ep) = Jx.De(text, hp.Declared, o); - var (vv, ev) = Jx.De(text, hv.Declared, o); - var one = Sides.WithoutTypeKit(o); - var r1 = new Sides.Answer("read", null, Jx.De(text, hp.Declared, one).Ex); - var r3 = new Sides.Answer("read", null, ev); - var fr = Sides.Check(r, obligation, Baseline.Refusal, r1.Refused && Sides.Alike(r1, r3) ? Sides.Measured(r1, "the twin's read under the application's options, less TypeKit's converters") : null, r3, null, null, c => - { - if (ev is null) - { - // no converter ran: the read holds none of the wrapped value (a JSON null for a class, a member the options do not read) - if (vv is not null && Jx.Tokens(JsonSerializer.Serialize(vv, hv.Declared, Jx.Plain)).Any(t => t.Text == "x")) - return $"read {input}: the value object over {Make.Held(e.Plain).Name} read a document under Preserve: {Jx.Cut(text)}"; - c.Note(NoteKeys.JsonMemberLeftOutByOptions, "read " + input); - return c.Inconclusive($"read {input}: no converter ran, the read holds none of the wrapped value"); - } - if (ev is NotSupportedException) { if (Refusal(ev, "read " + input) is { } f) return f; c.Note(NoteKeys.Compared, "read " + input + " refused"); return null; } - // refused before the guard: a JsonException naming the value object, with no inner exception (Generators README#json) - if (ev is not JsonException || ev.InnerException is not null) return Refusal(ev, "read " + input) + $"\n text={Jx.Cut(text)}"; - if (!ev.Message.Contains(Make.Held(e.Vo).Name)) return Refusal(ev, "read " + input) + $"\n text={Jx.Cut(text)}"; - c.Note(NoteKeys.Compared, "read " + input + ": refused naming the value object before the guard"); - return null; - }); - return fw ?? fr; - } - - /// - /// The unaffected rows, three-sided (Sides; C10 promises these are unaffected, so side (1) runs on every row): the value object's - /// document (3) is the wrapped type's under the same options (2), which is the twin's without TypeKit's converters (1), never an $id - /// twice, and its read agrees with the wrapped type's (ReadAlike); under a converter of the application's for the value object, that - /// converter's document reads back and reads the row's input alike with and without the reference handler. - /// - static string? Unaffected(Row r, string obligation, Entry e, Held hp, Held hv, JsonSerializerOptions op, JsonSerializerOptions o, bool ownVo) - { - var one = Sides.WithoutTypeKit(op); - var w1 = Sides.Write(hp, one); var w2 = Sides.Write(hp, op); var w3 = Sides.Write(hv, o); - var (jp, xp) = (w2.Value, w2.Thrown); var (jv, xv) = (w3.Value, w3.Thrown); - // byte[] is base64 by the serializer's own rule (KB-038), an array of value objects a JSON array: unaffected means the value - // object's document reads back as written - var byteArray = e.Wrapped == "byte" && r.Is("shape.position", "array-element") && jv is not null && jp is not null && jv != jp; - var written = Sides.Framework(r, e, hp, one, w1, w3); - var fw = Sides.Check(r, obligation, ownVo || byteArray ? Baseline.RoundTrip : Baseline.Twin, written, w3, w1, w2, c => - { - if (xp is not null && xv is not null) return $"write: both threw, not alike: the wrapped type {w2}, the value object {w3}"; - if (xv is not null) return $"write: the value object threw {Jx.Ex(xv)} where the wrapped type wrote {Jx.Cut(jp)}"; - // the application's converter for the value object writes its own document: its baseline is that document read back, whatever - // the twin's writes ("anything a converter of your own writes" is unaffected) - if (xp is not null && !ownVo) return $"write: the value object wrote {Jx.Cut(jv)} where the wrapped type threw {Jx.Ex(xp)}"; - if (DuplicateId(jv!) is { } dup) return $"write: $id {dup} twice in {Jx.Cut(jv)}"; - if (ownVo) - { - c.Note(NoteKeys.OwnVo); // the application's converter for the value object writes its own document; it reads it back, unaffected - var (jf, _) = Jx.Ser(hv, Jx.Full(o)); - var (_, xr) = Jx.De(jf ?? jv!, hv.Declared, o); - if (xr is not null) return "own converter for the value object: its document does not read back: " + Jx.Ex(xr); - return null; - } - if (byteArray) - { - var (back, xb) = Jx.De(jv!, hv.Declared, o); - if (xb is not null) return "write: the array of byte value objects does not read back: " + Jx.Ex(xb); - var again = JsonSerializer.Serialize(back, hv.Declared, o); - if (again != jv) return $"round trip: {Jx.Cut(jv)} read back as {Jx.Cut(again)}"; - c.Note(NoteKeys.Compared, "an array of byte value objects round-trips as a JSON array"); - return null; - } - if (jv != jp) return $"write: the value object wrote {Jx.Cut(jv)}, the wrapped type {Jx.Cut(jp)}"; - c.Note(NoteKeys.Compared, "write"); - return null; - }); - if (ownVo) return fw ?? OwnReadsInput(r, obligation, e, hp, hv, o); - // the wrapped type's base64 document is no array of value objects (KB-038): the round trip above holds the value object to its own - if (byteArray) return fw ?? r.Check(obligation, Baseline.Twin, c => c.Inapplicable(Cite.Fact("KB-038"), "read: the wrapped type's document is a base64 string")); - // the value object's document is refused as the framework refuses it, so is its read: the write's refusal, as it is - if (written is not null && Sides.Alike(written.Answer, w3)) return fw ?? Sides.Check(r, obligation, Baseline.Twin, written, w3, null, null, _ => (string?)null); - var full = Jx.Ser(hp, Jx.Full(op)).Json ?? jp; - if (full is null) return fw ?? r.Check(obligation, Baseline.Twin, c => c.Inconclusive("read: the wrapped type is not written under these options, so there is no document to read")); - var input = r["input.json"]; - var text = JsonParity.InputFor(input, e, Jx.Full(op), full, r["shape.position"]); - var r1 = Sides.Read(text, hp, one); var r2 = Sides.Read(text, hp, op); var r3 = Sides.Read(text, hv, o); - var fr = Sides.Check(r, obligation, Baseline.Twin, Sides.Framework(r, e, hp, one, r1, r3), r3, r1, r2, c => ReadAlike.Check(c, e, input, text, r2, r3, hp, hv, op, o)); - return fw ?? fr; - } - - /// - /// The application's converter for the value object reads the row's input inside its own spelling ("vo:" and the wrapped - /// type's JSON) alike under the row's reference handler and under none: "anything a converter of your own writes" is unaffected - /// (Generators README#json). Each side writes its own document, so Preserve's metadata is its own, and only the value is rewritten. - /// Where the handler's document is refused as the framework refuses a converter's value (KB-040), the check is that refusal. - /// - static string? OwnReadsInput(Row r, string obligation, Entry e, Held hp, Held hv, JsonSerializerOptions o) - { - var input = r["input.json"]; - var inner = JsonRow.Inner(e); - var wrapped = JsonSerializer.Serialize(Make.Unwrap(e, Make.Vo(e, e.Valid())), Make.Held(e.Plain), inner); - var value = "vo:" + JsonParity.InputFor(input, e, inner, wrapped, "root"); - var bare = new JsonSerializerOptions(o) { ReferenceHandler = null }; - (string? Read, string? Refused, Sides.Answer? Write) Through(JsonSerializerOptions x) - { - var w = Sides.Write(hv, Jx.Full(x)); - if (w.Refused) return (null, "write: " + w.Thrown!.GetType().Name, w); - var own = Jx.Tokens(w.Value!).Where(t => t.Type is JsonTokenType.String or JsonTokenType.PropertyName && t.Text is { } s && s.StartsWith("vo:", StringComparison.Ordinal)).ToList(); - if (own.Count == 0) return (null, null, null); - var (v, rx) = Jx.De(Jx.Rewrite(w.Value!, own, _ => JsonSerializer.Serialize(value)), hv.Declared, x); - if (rx is not null) return (null, "read: " + rx.GetType().Name, null); - return (JsonSerializer.Serialize(v, hv.Declared, bare), null, null); - } - var handled = Through(o); var unhandled = Through(bare); - // the handler's document, written by the application's converter, refused as the framework refuses any converter's value there - var framework = (handled.Write ?? unhandled.Write) is { } refused ? Sides.WrittenByAConverter(r, e, hp, Sides.WithoutTypeKit(Jx.Full(handled.Write is null ? bare : o)), refused) : null; - return Sides.Check(r, obligation, Baseline.RoundTrip, framework, (handled.Write ?? unhandled.Write) ?? new Sides.Answer("write", null), null, null, c => - { - if (handled.Read is null && handled.Refused is null && unhandled.Read is null && unhandled.Refused is null) - { c.Note(NoteKeys.JsonMemberLeftOutByOptions, "own converter: the options write no member"); return c.Inconclusive("own converter: the options write no member to read"); } - if (handled.Read != unhandled.Read || handled.Refused != unhandled.Refused) - return $"read {input} through the application's converter: {handled.Read ?? handled.Refused ?? "no member"} under ReferenceHandler {c["handler"]}, {unhandled.Read ?? unhandled.Refused ?? "no member"} without one"; - c.Note(NoteKeys.Compared, $"read {input} through the application's converter, alike with and without the reference handler: {Jx.Cut(handled.Read ?? handled.Refused)}"); - return null; - }); - } - - /// The first $id value a document holds twice, or null. - public static string? DuplicateId(string json) - { - var t = Jx.Tokens(json); var seen = new HashSet(); - for (var i = 0; i + 1 < t.Count; i++) - if (t[i].Type == JsonTokenType.PropertyName && t[i].Text == "$id" && !seen.Add(t[i + 1].Text ?? t[i + 1].Raw)) return t[i + 1].Text; - return null; - } -} - -/// -/// One read of the same text into the wrapped type's shape (2) and the value object's (3), each written again to be compared: they agree, or -/// differ only as the documents say (a struct value object refuses a JSON null, a sealed record reads it as null, a validated one refuses what -/// Create refuses, a closed set has only its members); a refusal of the value object's is a JsonException naming it, with no inner exception -/// (Generators README#json). Side (1) refusing alike with (3) is decided before (Sides). -/// -public static class ReadAlike -{ - public static string? Check(Check c, Entry e, string input, string text, Sides.Answer p, Sides.Answer v, Held hp, Held hv, JsonSerializerOptions op, JsonSerializerOptions ov) - { - var voName = Make.Held(e.Vo).Name; - string? Named(Exception x) => x is JsonException && x.Message.Contains(voName) && x.InnerException is null ? null - : $"read {input}: the value object's refusal is {Jx.Ex(x)}{(x.InnerException is null ? "" : " with an inner exception")}, not a JsonException naming {voName}\n text={Jx.Cut(text)}"; - var stored = c.Is("json.Converters", "StoredJsonConverterFactory"); - if (!p.Refused && !v.Refused) - { - if (e.Kind.StartsWith("validated") && input == "refused-by-create" && !stored && e.Refused() is not null && text != Jx.Ser(hp, op).Json && !c.Is("shape.position", "dictionary-key")) - return $"read {input}: the validated value object read what Create refuses\n text={Jx.Cut(text)}"; - if (p.Value != v.Value) return $"read {input}: read back differs: wrapped {Jx.Cut(p.Value)} vs value object {Jx.Cut(v.Value)}\n text={Jx.Cut(text)}"; - c.Note(NoteKeys.Compared, "read " + input); return null; - } - if (p.Refused && v.Refused) - { - if (v.Operation != "read") return $"read {input}: both refuse, not alike: the wrapped type {p}, the value object {v}"; - if (Named(v.Thrown!) is { } f) return f + $"; the wrapped type: {p}"; - c.Note(NoteKeys.Compared, "read " + input + ": both refuse, the value object naming itself"); return null; - } - if (v.Refused) - { - var intended = v.Operation == "read" && ((input == "null" && e.Vo.IsValueType) || (e.Kind.StartsWith("validated") && input == "refused-by-create" && !stored) || (e.Kind == "closed-set" && input != "valid")); - if (!intended) return $"read {input}: the wrapped type read it, the value object refused: {v} {Jx.Ex(v.Thrown!)}\n text={Jx.Cut(text)}"; - if (Named(v.Thrown!) is { } f) return f; - c.Note(NoteKeys.Compared, "read " + input + ": refused as documented"); return null; - } - // the wrapped type refused and the value object read: only a JSON null for a sealed record, which reads it as any class does - if (input == "null" && Make.AdmitsNull(e.Vo) && !Make.AdmitsNull(e.Plain) && Jx.De(text, hv.Declared, ov).Value is { } read - && Jx.Tokens(JsonSerializer.Serialize(read, hv.Declared, Jx.Plain)).Any(t => t.Type == JsonTokenType.Null)) - { c.Note(NoteKeys.Compared, "read null: the sealed record reads null, as any class does"); return null; } - return $"read {input}: the value object read what the wrapped type refused ({p})\n text={Jx.Cut(text)}"; - } -} diff --git a/tools/probe/corpus/harness/Registry.cs b/tools/probe/corpus/harness/Registry.cs deleted file mode 100644 index abe2a1d8..00000000 --- a/tools/probe/corpus/harness/Registry.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Hand half of the shape registry (see gen_shapes.py). Looks an entry up by the row's shape.kind and shape.wrapped. -namespace Probes.Shapes; -public static partial class Registry -{ - public static readonly List All = Build(); - static List Build() { var e = new List(); Fill(e); return e; } - static partial void Fill(List e); - /// The entry a row names. record-class (CMTK1006) and over-value-object (CMTK1005) are refused at build, verified; the row's other axes are - /// still probed on the nearest buildable kind. - public static Entry Of(Row r) - { - var k = r["shape.kind"]; - if (k is "record-class" or "over-value-object") { k = k == "record-class" ? "sealed-record-class" : "record-struct"; r.Substituted("shape.kind", k, "the generators refuse the requested kind"); } - var e = Get(k, r["shape.wrapped"]); - if (r.Is("http.nullability", "nullable")) - { - // The parameter or body declared nullable on both sides (T? against wrapped?): the dimension G1 exposed. - static Type N(Type t) => t.IsValueType && Nullable.GetUnderlyingType(t) is null ? typeof(Nullable<>).MakeGenericType(t) : t; - r.Note(NoteKeys.BindingNullable); - e = e with { Vo = N(e.Vo), Plain = N(e.Plain) }; - } - return e; - } - public static Entry Get(string kind, string wrapped) => - kind == "closed-set" ? All.First(x => x.Kind == "closed-set") - : All.First(x => x.Kind == kind && x.Wrapped == wrapped); -} diff --git a/tools/probe/corpus/harness/ReplayProbe.cs b/tools/probe/corpus/harness/ReplayProbe.cs deleted file mode 100644 index 07249dad..00000000 --- a/tools/probe/corpus/harness/ReplayProbe.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Debug aid (not a claim): PROBE_REPLAY="rowsfile:idx" re-runs the baseline for one row and writes its exchange log to /tmp/replay.txt. -using Probes.Shapes; -namespace Probes; -public sealed class ReplayProbe -{ - [Fact] - public async Task Replay() - { - var spec = Environment.GetEnvironmentVariable("PROBE_REPLAY"); if (spec is null) return; - var parts = spec.Split(':'); var r = Rows.Get(parts[0], int.Parse(parts[1])); - var f = await HttpBaseline.Run(r, Registry.Of(r)); - File.WriteAllText("/tmp/replay.txt", r.ToString() + "\nRESULT: " + (f ?? "pass") + "\nNOTES: " + r.Notes + "\n" + r.Log); - } -} diff --git a/tools/probe/corpus/harness/Row.cs b/tools/probe/corpus/harness/Row.cs deleted file mode 100644 index 79703c65..00000000 --- a/tools/probe/corpus/harness/Row.cs +++ /dev/null @@ -1,592 +0,0 @@ -// Row plumbing shared by every probe: each probe's rows are its own file, written by `prober rows --probe P` (the corpus's -// rows/P.json.gz, unpacked by the replay to Probes/rows/P.json); each probe records one line per row to $PROBE_OUT/