From 1203a8d5f2052764a5a52616bc8ac9ecd414f300 Mon Sep 17 00:00:00 2001 From: Dmitry Kurochkin Date: Wed, 14 Oct 2026 14:54:18 +0000 Subject: [PATCH 1/2] feat: support PureQL specification 0.1.0-preview.2.0.1 --- .github/workflows/build-and-test.yml | 2 + .gitmodules | 3 + README.md | 9 +- .../PureQLQueryDocumentTransformer.cs | 236 +++++++++++++++--- src/Tests/PureQL-Specification | 1 + .../FlatDefinitionsTests.cs | 221 ++++++++++++++++ ...L.CSharp.Model.OpenAPI.Schema.Tests.csproj | 9 + .../SpecificationTests.cs | 103 ++++++++ 8 files changed, 554 insertions(+), 30 deletions(-) create mode 100644 .gitmodules create mode 160000 src/Tests/PureQL-Specification create mode 100644 src/Tests/PureQL.CSharp.Model.OpenAPI.Schema.Tests/FlatDefinitionsTests.cs create mode 100644 src/Tests/PureQL.CSharp.Model.OpenAPI.Schema.Tests/SpecificationTests.cs diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 5c6f578..e6539c5 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -18,6 +18,8 @@ jobs: working-directory: ./src steps: - uses: actions/checkout@v7 + with: + submodules: true - name: Setup .NET uses: actions/setup-dotnet@v6 with: diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..4ca7dd0 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/Tests/PureQL-Specification"] + path = src/Tests/PureQL-Specification + url = https://github.com/kudima03/PureQL-Specification.git diff --git a/README.md b/README.md index a383268..e52bda2 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,14 @@ OpenAPI document transformer for **PureQL** — injects the full PureQL JSON Sch When ASP.NET Core generates an OpenAPI document for endpoints that accept PureQL `Query` objects, the generated schema reflects .NET's type-system view of the C# model — not the actual JSON structure that PureQL consumers need to send. `PureQL.CSharp.Model.OpenAPI.Schema` provides `PureQLQueryDocumentTransformer`, an `IOpenApiDocumentTransformer` that replaces the auto-generated schema with the canonical PureQL JSON Schema. -The transformer parses the PureQL JSON Schema, flattens its nested `definitions` tree into a set of named components, rewrites all `$ref` pointers from `#/definitions/...` to `#/components/schemas/...`, and injects the resulting schemas into the document's `components.schemas` section. +The transformer parses the PureQL JSON Schema, turns each of its definitions into a named component, rewrites all `$ref` pointers to `#/components/schemas/...`, and injects the resulting schemas into the document's `components.schemas` section. It reads the flat `$defs` of current specifications (0.1.0-preview.1.0.0 and later, tested against 0.1.0-preview.2.0.1) as well as the nested `definitions` tree of earlier ones: + +| Specification | Definition | Component | +|---------------|------------|-----------| +| `$defs` (current) | `#/$defs/add.decimal@row` | `#/components/schemas/add.decimal_row` | +| `definitions` (earlier) | `#/definitions/aggregates/date/average_date` | `#/components/schemas/aggregates_date_average_date` | + +Characters that OpenAPI does not allow in component names (`@`, `/`) become `_`. Constructs that `Microsoft.OpenApi` cannot read back are rewritten into equivalent ones: boolean subschemas become `{}` (`true`) and `{ "not": {} }` (`false`), and a `const` that is not a string becomes a one-value `enum`. The specification's `$id`, `$schema` and `version` are dropped from the `Query` schema. ## API diff --git a/src/PureQL.CSharp.Model.OpenAPI.Schema/PureQLQueryDocumentTransformer.cs b/src/PureQL.CSharp.Model.OpenAPI.Schema/PureQLQueryDocumentTransformer.cs index 9c2d5bc..e36d76e 100644 --- a/src/PureQL.CSharp.Model.OpenAPI.Schema/PureQLQueryDocumentTransformer.cs +++ b/src/PureQL.CSharp.Model.OpenAPI.Schema/PureQLQueryDocumentTransformer.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using System.Text.Json.Nodes; using Microsoft.AspNetCore.OpenApi; using Microsoft.OpenApi; @@ -15,9 +16,58 @@ public PureQLQueryDocumentTransformer(string schema) } private const string QuerySchemaKey = "Query"; + private const string DefsKey = "$defs"; + private const string DefinitionsKey = "definitions"; + private const string DefsPointerPrefix = "#/$defs/"; private const string DefinitionsPointerPrefix = "#/definitions/"; private const string ComponentsSchemasPointerPrefix = "#/components/schemas/"; + // Root keywords that describe the specification document rather than the query. + // "$id" in particular would rebase every "#/components/schemas/..." pointer onto the + // specification's release URL, where no such component exists. + private static readonly string[] DocumentKeywords = + [ + DefsKey, + DefinitionsKey, + "$schema", + "$id", + "version", + ]; + + // Keywords whose value is a subschema, a map of subschemas or an array of subschemas. + // "additionalProperties" is left out: Microsoft.OpenApi reads it as a boolean too. + private static readonly HashSet SubschemaKeywords = new( + StringComparer.Ordinal + ) + { + "items", + "contains", + "not", + "if", + "then", + "else", + "propertyNames", + }; + + private static readonly HashSet SubschemaMapKeywords = new( + StringComparer.Ordinal + ) + { + "properties", + "patternProperties", + "dependentSchemas", + }; + + private static readonly HashSet SubschemaArrayKeywords = new( + StringComparer.Ordinal + ) + { + "allOf", + "anyOf", + "oneOf", + "prefixItems", + }; + private static readonly HashSet SchemaKeywords = new(StringComparer.Ordinal) { "type", @@ -46,37 +96,56 @@ CancellationToken cancellationToken { JsonObject specNode = JsonNode.Parse(_schema)!.AsObject(); - // Every "#/definitions/..." pointer the specification actually uses. A node that - // owns one of these as a child is a grouping node, never a schema of its own. - HashSet pointedAtPaths = new HashSet(StringComparer.Ordinal); - CollectDefinitionPointers(specNode, pointedAtPaths); - - // Collect leaf schemas from the nested definitions tree. - // Nested path "aggregates/date/average_date" → flat name "aggregates_date_average_date". + // Definitions keyed by their path below the definitions root. + // "$defs" (draft 2020-12) is flat: "add.decimal@row" stays "add.decimal@row". + // "definitions" (older specifications) is nested: "aggregates/date/average_date". Dictionary defs = new Dictionary( StringComparer.Ordinal ); - CollectDefinitions( - specNode["definitions"]?.AsObject() ?? [], - string.Empty, - pointedAtPaths, - defs - ); - // Build root query schema node (strip "definitions" and "$schema" metadata). + if (specNode[DefsKey] is JsonObject flatDefs) + { + foreach ((string name, JsonNode? defNode) in flatDefs) + { + if (defNode is not null) + { + defs[name] = defNode; + } + } + } + + if (specNode[DefinitionsKey] is JsonObject nestedDefs) + { + // Every "#/definitions/..." pointer the specification actually uses. A node that + // owns one of these as a child is a grouping node, never a schema of its own. + HashSet pointedAtPaths = new HashSet(StringComparer.Ordinal); + CollectDefinitionPointers(specNode, pointedAtPaths); + + CollectDefinitions(nestedDefs, string.Empty, pointedAtPaths, defs); + } + + // Build root query schema node (strip the specification document metadata). JsonObject rootNode = specNode.DeepClone().AsObject(); - _ = rootNode.Remove("definitions"); - _ = rootNode.Remove("$schema"); + foreach (string keyword in DocumentKeywords) + { + _ = rootNode.Remove(keyword); + } - // Assemble schemas: root query + all flat definitions. + // Assemble schemas: root query + all definitions. JsonObject schemasNode = new JsonObject { [QuerySchemaKey] = rootNode }; foreach ((string path, JsonNode defNode) in defs) { - schemasNode[ToFlatName(path)] = defNode.DeepClone(); + schemasNode[ToComponentName(path)] = defNode.DeepClone(); + } + + // Spell the constructs Microsoft.OpenApi cannot read back in an equivalent form. + foreach ((string _, JsonNode? schemaNode) in schemasNode) + { + NormalizeSchema(schemaNode); } - // Rewrite all "#/definitions/X/Y" → "#/components/schemas/X_Y" so that - // every $ref is a valid OpenAPI component pointer and resolvable by Scalar. + // Rewrite all "#/$defs/X" and "#/definitions/X/Y" → "#/components/schemas/..." so + // that every $ref is a valid OpenAPI component pointer and resolvable by Scalar. RewriteRefs(schemasNode); // Wrap in a minimal OpenAPI 3.1 document so Microsoft.OpenApi can @@ -187,8 +256,8 @@ private static void CollectDefinitionPointers(JsonNode? node, HashSet re } } - // Recursively rewrites "$ref" values from "#/definitions/X/Y/Z" - // to "#/components/schemas/X_Y_Z" in place. + // Recursively rewrites "$ref" values from "#/$defs/X" and "#/definitions/X/Y/Z" + // to "#/components/schemas/X" and "#/components/schemas/X_Y_Z" in place. private static void RewriteRefs(JsonNode? node) { switch (node) @@ -198,14 +267,11 @@ private static void RewriteRefs(JsonNode? node) obj.TryGetPropertyValue("$ref", out JsonNode? refNode) && refNode is JsonValue refVal && refVal.TryGetValue(out string? refStr) - && refStr.StartsWith( - DefinitionsPointerPrefix, - StringComparison.Ordinal - ) + && TryGetDefinitionPath(refStr, out string? path) ) { obj["$ref"] = - $"{ComponentsSchemasPointerPrefix}{ToFlatName(refStr[DefinitionsPointerPrefix.Length..])}"; + $"{ComponentsSchemasPointerPrefix}{ToComponentName(path)}"; } foreach (string? key in obj.Select(p => p.Key).ToList()) { @@ -226,8 +292,120 @@ private static void RewriteRefs(JsonNode? node) } } - private static string ToFlatName(string path) + // Recursively rewrites the schema in place into an equivalent one that survives + // Microsoft.OpenApi: + // - a boolean subschema is read as null and then thrown on, so true → {} (accepts + // anything) and false → { "not": {} } (accepts nothing). Only schema positions are + // touched, never a value such as "const": true; + // - "const" is read as a string, turning "const": true into "const": "True", so a + // const that is not a string becomes an "enum" with that one value, whose items + // keep their JSON type. + private static void NormalizeSchema(JsonNode? node) + { + if (node is not JsonObject obj) + { + return; + } + + foreach ((string key, JsonNode? value) in obj.ToList()) + { + if (key == "const" && value?.GetValueKind() is not JsonValueKind.String) + { + ReplaceConstWithEnum(obj, value); + } + else if (SubschemaKeywords.Contains(key)) + { + NormalizeSubschema(value, replacement => obj[key] = replacement); + } + else if (SubschemaMapKeywords.Contains(key) && value is JsonObject map) + { + foreach ((string name, JsonNode? subschema) in map.ToList()) + { + NormalizeSubschema(subschema, replacement => map[name] = replacement); + } + } + else if (SubschemaArrayKeywords.Contains(key) && value is JsonArray arr) + { + for (int i = 0; i < arr.Count; i++) + { + int index = i; + NormalizeSubschema(arr[i], replacement => arr[index] = replacement); + } + } + else if (key == "additionalProperties") + { + NormalizeSchema(value); + } + } + } + + // An existing "enum" is kept and the const joins it through "allOf" instead: their + // intersection may be empty, and Microsoft.OpenApi drops an empty "enum". + private static void ReplaceConstWithEnum(JsonObject schema, JsonNode? constValue) + { + _ = schema.Remove("const"); + JsonArray values = new JsonArray(constValue?.DeepClone()); + + if (!schema.ContainsKey("enum")) + { + schema["enum"] = values; + return; + } + + if (schema["allOf"] is not JsonArray allOf) + { + allOf = []; + schema["allOf"] = allOf; + } + + JsonNode constSchema = new JsonObject { ["enum"] = values }; + allOf.Add(constSchema); + } + + private static void NormalizeSubschema(JsonNode? node, Action replace) + { + if (node is JsonValue value && value.TryGetValue(out bool accepts)) + { + replace(accepts ? [] : new JsonObject { ["not"] = new JsonObject() }); + } + else + { + NormalizeSchema(node); + } + } + + private static bool TryGetDefinitionPath(string pointer, out string path) + { + foreach (string prefix in (string[])[DefsPointerPrefix, DefinitionsPointerPrefix]) + { + if (pointer.StartsWith(prefix, StringComparison.Ordinal)) + { + path = pointer[prefix.Length..]; + return true; + } + } + + path = string.Empty; + return false; + } + + // OpenAPI component names must match ^[a-zA-Z0-9.\-_]+$, so every other character is + // replaced: the "/" of a nested path and the "@" of a PureQL context + // ("add.decimal@row" → "add.decimal_row"). + private static string ToComponentName(string path) { - return path.Replace('/', '_'); + return string.Create( + path.Length, + path, + static (span, source) => + { + for (int i = 0; i < source.Length; i++) + { + char c = source[i]; + span[i] = + char.IsAsciiLetterOrDigit(c) || c is '.' or '-' or '_' ? c : '_'; + } + } + ); } } diff --git a/src/Tests/PureQL-Specification b/src/Tests/PureQL-Specification new file mode 160000 index 0000000..b58f906 --- /dev/null +++ b/src/Tests/PureQL-Specification @@ -0,0 +1 @@ +Subproject commit b58f9060908e621839ef33fb8630a61dd211454c diff --git a/src/Tests/PureQL.CSharp.Model.OpenAPI.Schema.Tests/FlatDefinitionsTests.cs b/src/Tests/PureQL.CSharp.Model.OpenAPI.Schema.Tests/FlatDefinitionsTests.cs new file mode 100644 index 0000000..b88c799 --- /dev/null +++ b/src/Tests/PureQL.CSharp.Model.OpenAPI.Schema.Tests/FlatDefinitionsTests.cs @@ -0,0 +1,221 @@ +using System.Text.Json.Nodes; +using Microsoft.OpenApi; + +namespace PureQL.CSharp.Model.OpenAPI.Schema.Tests; + +public sealed record FlatDefinitionsTests +{ + // Shaped like PureQL 0.1.0-preview.2.0.x: flat "$defs" named ".@", + // "$id" and "version" on the root, boolean subschemas and boolean consts. + // lang=json,strict + private const string Specification = """ + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/PureQL-Specification.json", + "version": "1.2.3", + "title": "PureQL specification", + "$ref": "#/$defs/expr@row", + "$defs": { + "expr@row": { + "type": "object", + "required": ["operator"], + "properties": { + "operator": { "const": "add" }, + "left": true, + "right": false, + "type": { "$ref": "#/$defs/type.nullable" } + }, + "allOf": [true, { "$ref": "#/$defs/add.integer@row" }], + "if": { "required": ["left"] }, + "then": true, + "else": false, + "additionalProperties": { "properties": { "extra": true } } + }, + "add.integer@row": { + "properties": { "values": { "items": true } } + }, + "type.nullable": { + "properties": { + "nullable": { "const": true }, + "scale": { "const": 2, "enum": [1, 2] }, + "precision": { "const": 3, "enum": [1, 2], "allOf": [{ "minimum": 0 }] } + } + }, + "unused": null + } + } + """; + + private static async Task TransformedDocument() + { + OpenApiDocument document = new OpenApiDocument(); + + await new PureQLQueryDocumentTransformer(Specification).TransformAsync( + document, + null!, + CancellationToken.None + ); + + return document; + } + + private static async Task Schemas() + { + OpenApiDocument document = await TransformedDocument(); + + string serialized = await document.SerializeAsJsonAsync( + OpenApiSpecVersion.OpenApi3_1 + ); + + return JsonNode.Parse(serialized)!["components"]!["schemas"]!.AsObject(); + } + + [Fact] + public async Task EmitsEveryDefinitionUnderAValidComponentName() + { + OpenApiDocument document = await TransformedDocument(); + + Assert.Equal( + ["Query", "add.integer_row", "expr_row", "type.nullable"], + document.Components!.Schemas!.Keys.Order(StringComparer.Ordinal) + ); + } + + [Fact] + public async Task RewritesDefsReferencesToComponents() + { + JsonObject schemas = await Schemas(); + + Assert.Equal("#/components/schemas/expr_row", (string?)schemas["Query"]!["$ref"]); + Assert.Equal( + "#/components/schemas/add.integer_row", + (string?)schemas["expr_row"]!["allOf"]![1]!["$ref"] + ); + } + + [Theory] + [InlineData("$defs")] + [InlineData("$id")] + [InlineData("$schema")] + [InlineData("version")] + public async Task StripsDocumentKeywordsFromQuery(string keyword) + { + JsonObject schemas = await Schemas(); + + Assert.False(schemas["Query"]!.AsObject().ContainsKey(keyword)); + } + + [Fact] + public async Task KeepsQueryTitle() + { + JsonObject schemas = await Schemas(); + + Assert.Equal("PureQL specification", (string?)schemas["Query"]!["title"]); + } + + [Theory] + [InlineData( /*lang=json,strict*/ + "{}", + "properties", + "left" + )] + [InlineData( /*lang=json,strict*/ + """{"not":{}}""", + "properties", + "right" + )] + [InlineData( /*lang=json,strict*/ + "{}", + "then" + )] + [InlineData( /*lang=json,strict*/ + """{"not":{}}""", + "else" + )] + [InlineData( /*lang=json,strict*/ + "{}", + "additionalProperties", + "properties", + "extra" + )] + public async Task SpellsBooleanSubschemasAsObjects( + string expected, + params string[] path + ) + { + JsonObject schemas = await Schemas(); + JsonNode actual = path.Aggregate(schemas["expr_row"]!, (x, key) => x[key]!); + + Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), actual)); + } + + [Fact] + public async Task SpellsBooleanSubschemasInArraysAsObjects() + { + JsonObject schemas = await Schemas(); + + Assert.True( + JsonNode.DeepEquals(new JsonObject(), schemas["expr_row"]!["allOf"]![0]) + ); + } + + [Fact] + public async Task SpellsBooleanSubschemasInNestedSchemasAsObjects() + { + JsonObject schemas = await Schemas(); + + Assert.True( + JsonNode.DeepEquals( + new JsonObject(), + schemas["add.integer_row"]!["properties"]!["values"]!["items"] + ) + ); + } + + [Fact] + public async Task KeepsStringConst() + { + JsonObject schemas = await Schemas(); + + Assert.Equal( + "add", + (string?)schemas["expr_row"]!["properties"]!["operator"]!["const"] + ); + } + + [Fact] + public async Task SpellsNonStringConstAsEnum() + { + JsonObject schemas = await Schemas(); + JsonNode properties = schemas["type.nullable"]!["properties"]!; + JsonObject schema = properties["nullable"]!.AsObject(); + + Assert.False(schema.ContainsKey("const")); + Assert.True(JsonNode.DeepEquals(JsonNode.Parse("[true]"), schema["enum"])); + } + + // The intersection of "const": 3 and "enum": [1, 2] is empty, which an empty "enum" would + // not keep: Microsoft.OpenApi drops it, and the schema would accept anything. + [Theory] + [InlineData( + "scale", /*lang=json,strict*/ + """[{"enum":[2]}]""" + )] + [InlineData( + "precision", /*lang=json,strict*/ + """[{"minimum":0},{"enum":[3]}]""" + )] + public async Task JoinsNonStringConstToExistingEnumThroughAllOf( + string property, + string expectedAllOf + ) + { + JsonObject schemas = await Schemas(); + JsonNode properties = schemas["type.nullable"]!["properties"]!; + JsonObject schema = properties[property]!.AsObject(); + + Assert.False(schema.ContainsKey("const")); + Assert.True(JsonNode.DeepEquals(JsonNode.Parse("[1,2]"), schema["enum"])); + Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expectedAllOf), schema["allOf"])); + } +} diff --git a/src/Tests/PureQL.CSharp.Model.OpenAPI.Schema.Tests/PureQL.CSharp.Model.OpenAPI.Schema.Tests.csproj b/src/Tests/PureQL.CSharp.Model.OpenAPI.Schema.Tests/PureQL.CSharp.Model.OpenAPI.Schema.Tests.csproj index a6aa1b0..228d8cd 100644 --- a/src/Tests/PureQL.CSharp.Model.OpenAPI.Schema.Tests/PureQL.CSharp.Model.OpenAPI.Schema.Tests.csproj +++ b/src/Tests/PureQL.CSharp.Model.OpenAPI.Schema.Tests/PureQL.CSharp.Model.OpenAPI.Schema.Tests.csproj @@ -17,6 +17,15 @@ + + + + + diff --git a/src/Tests/PureQL.CSharp.Model.OpenAPI.Schema.Tests/SpecificationTests.cs b/src/Tests/PureQL.CSharp.Model.OpenAPI.Schema.Tests/SpecificationTests.cs new file mode 100644 index 0000000..812e173 --- /dev/null +++ b/src/Tests/PureQL.CSharp.Model.OpenAPI.Schema.Tests/SpecificationTests.cs @@ -0,0 +1,103 @@ +using System.Text.Json.Nodes; +using System.Text.RegularExpressions; +using Microsoft.OpenApi; + +namespace PureQL.CSharp.Model.OpenAPI.Schema.Tests; + +/// +/// Transforms PureQL specification 0.1.0-preview.2.0.1, from the +/// src/Tests/PureQL-Specification submodule. +/// +public sealed partial record SpecificationTests +{ + private static readonly string SpecificationPath = Path.Combine( + AppContext.BaseDirectory, + "Specification", + "PureQL-Specification.json" + ); + + private static async Task TransformedDocument() + { + OpenApiDocument document = new OpenApiDocument(); + + await new PureQLQueryDocumentTransformer( + File.ReadAllText(SpecificationPath) + ).TransformAsync(document, null!, CancellationToken.None); + + return document; + } + + [Fact] + public async Task EmitsQueryAndEveryDefinition() + { + JsonNode specification = JsonNode.Parse(File.ReadAllText(SpecificationPath))!; + JsonObject defs = specification["$defs"]!.AsObject(); + + OpenApiDocument document = await TransformedDocument(); + + Assert.Equal(defs.Count + 1, document.Components!.Schemas!.Count); + } + + [Fact] + public async Task EmitsOnlyValidComponentNames() + { + OpenApiDocument document = await TransformedDocument(); + + Assert.All( + document.Components!.Schemas!.Keys, + name => Assert.Matches(ComponentName(), name) + ); + } + + [Fact] + public async Task LeavesNoDanglingComponentReferences() + { + OpenApiDocument document = await TransformedDocument(); + + string serialized = await document.SerializeAsJsonAsync( + OpenApiSpecVersion.OpenApi3_1 + ); + + IEnumerable referenced = ComponentReference() + .Matches(serialized) + .Select(x => x.Groups["name"].Value); + + Assert.Empty(referenced.Except(document.Components!.Schemas!.Keys)); + } + + [Fact] + public async Task LeavesNoReferenceOutsideComponents() + { + OpenApiDocument document = await TransformedDocument(); + + string serialized = await document.SerializeAsJsonAsync( + OpenApiSpecVersion.OpenApi3_1 + ); + + Assert.DoesNotMatch(ForeignReference(), serialized); + } + + [Fact] + public async Task KeepsBooleanConstsBoolean() + { + OpenApiDocument document = await TransformedDocument(); + + string serialized = await document.SerializeAsJsonAsync( + OpenApiSpecVersion.OpenApi3_1 + ); + + Assert.DoesNotMatch(StringifiedBoolean(), serialized); + } + + [GeneratedRegex("^[a-zA-Z0-9.\\-_]+$")] + private static partial Regex ComponentName(); + + [GeneratedRegex(""""\$ref":\s*"#/components/schemas/(?[^"]+)"""")] + private static partial Regex ComponentReference(); + + [GeneratedRegex(""""\$ref":\s*"(?!#/components/schemas/)"""")] + private static partial Regex ForeignReference(); + + [GeneratedRegex(@"""const"":\s*""(True|False)""")] + private static partial Regex StringifiedBoolean(); +} From fc8f39f1504c97f10ab6386d86c5a3626027e2b1 Mon Sep 17 00:00:00 2001 From: Dmitry Kurochkin Date: Wed, 14 Oct 2026 14:58:35 +0000 Subject: [PATCH 2/2] ci: set coverage and mutation thresholds to current levels --- .github/workflows/build-and-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index e6539c5..594ea7e 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -6,8 +6,8 @@ on: env: COVERAGE_WARNING_THRESHOLD: 99 - COVERAGE_THRESHOLD: 0 - MUTATION_THRESHOLD: 0 + COVERAGE_THRESHOLD: 40 + MUTATION_THRESHOLD: 64 jobs: build: