From 5437c7b719b056626939c2b8c8d4944f7c657a0d Mon Sep 17 00:00:00 2001 From: Jason Doyle Date: Thu, 3 Sep 2026 17:00:13 -0700 Subject: [PATCH] Add conformant .NET implementation --- .github/dependabot.yml | 16 + .github/workflows/ci.yml | 59 + .github/workflows/codeql.yml | 27 +- README.md | 1 + ports/dotnet/.editorconfig | 121 + ports/dotnet/.gitignore | 8 + ports/dotnet/Directory.Build.props | 52 + ports/dotnet/Directory.Packages.props | 18 + ports/dotnet/NuGet.config | 17 + ports/dotnet/README.md | 206 + ports/dotnet/THIRD-PARTY-NOTICES.md | 128 + ports/dotnet/WorldCut.sln | 71 + ports/dotnet/global.json | 10 + ports/dotnet/scripts/package-smoke.ps1 | 393 + .../dotnet/scripts/verify-vendored-source.ps1 | 87 + ports/dotnet/src/WorldCut.Tool/PACKAGE.md | 55 + ports/dotnet/src/WorldCut.Tool/Program.cs | 171 + .../src/WorldCut.Tool/WorldCut.Tool.csproj | 39 + .../src/WorldCut.Tool/packages.lock.json | 15 + .../src/WorldCut/Engine/AcquisitionPlanner.cs | 275 + ports/dotnet/src/WorldCut/Engine/JsonPath.cs | 81 + .../WorldCut/Engine/RequirementEvaluator.cs | 613 ++ .../dotnet/src/WorldCut/Json/CanonicalJson.cs | 125 + ports/dotnet/src/WorldCut/Json/JsonKind.cs | 23 + ports/dotnet/src/WorldCut/Json/JsonText.cs | 144 + ports/dotnet/src/WorldCut/Json/JsonValue.cs | 392 + .../src/WorldCut/Json/JsonValueReader.cs | 298 + ports/dotnet/src/WorldCut/Json/Utf16.cs | 67 + .../src/WorldCut/Model/ContractRequirement.cs | 150 + .../src/WorldCut/Model/DecisionContract.cs | 39 + .../src/WorldCut/Model/NormalizedTimestamp.cs | 194 + .../dotnet/src/WorldCut/Model/Observation.cs | 54 + .../src/WorldCut/Model/ResourceIdentity.cs | 63 + ports/dotnet/src/WorldCut/Model/Witness.cs | 108 + ports/dotnet/src/WorldCut/PACKAGE.md | 93 + .../src/WorldCut/ParsedVerificationInput.cs | 76 + .../WorldCut/Vendored/JcsNet/.editorconfig | 22 + .../JcsNet/CanonicalJsonSerializer.cs | 178 + .../JcsNet/EcmaScriptNumberFormatter.cs | 111 + .../WorldCut/Vendored/JcsNet/JcsException.cs | 36 + .../Vendored/JcsNet/JsonCanonicalizer.cs | 96 + .../Vendored/JcsNet/JsonStringSerializer.cs | 96 + .../JcsNet/JsonTextSurrogateValidator.cs | 191 + .../src/WorldCut/Vendored/JcsNet/LICENSE | 21 + .../WorldCut/VerificationInputValidator.cs | 509 ++ .../dotnet/src/WorldCut/VerificationResult.cs | 397 + ports/dotnet/src/WorldCut/WorldCut.csproj | 27 + .../dotnet/src/WorldCut/WorldCutErrorCode.cs | 62 + .../dotnet/src/WorldCut/WorldCutException.cs | 63 + ports/dotnet/src/WorldCut/WorldCutProtocol.cs | 58 + ports/dotnet/src/WorldCut/WorldCutVerifier.cs | 156 + ports/dotnet/src/WorldCut/packages.lock.json | 7 + .../WorldCut.Tests/AcquisitionPlannerTests.cs | 250 + .../WorldCut.Tests/CanonicalJsonTests.cs | 188 + ports/dotnet/tests/WorldCut.Tests/CliTests.cs | 284 + .../tests/WorldCut.Tests/ConformanceCorpus.cs | 65 + .../tests/WorldCut.Tests/ConformanceTests.cs | 150 + ports/dotnet/tests/WorldCut.Tests/Digest.cs | 17 + ports/dotnet/tests/WorldCut.Tests/Fixtures.cs | 36 + .../tests/WorldCut.Tests/GlobalUsings.cs | 1 + .../tests/WorldCut.Tests/JsonReorder.cs | 63 + .../NormalizedTimestampTests.cs | 104 + .../tests/WorldCut.Tests/PropertyTests.cs | 352 + .../tests/WorldCut.Tests/PublicApiTests.cs | 284 + .../tests/WorldCut.Tests/UnicodeTests.cs | 175 + .../tests/WorldCut.Tests/ValidationTests.cs | 273 + .../tests/WorldCut.Tests/ValueEqualsInput.cs | 70 + .../tests/WorldCut.Tests/ValuePathTests.cs | 96 + .../WorldCut.Tests/WorldCut.Tests.csproj | 33 + .../0.1/canonicalization-vectors.json | 60 + .../data/conformance/0.1/invalid-vectors.json | 1833 +++++ .../data/conformance/0.1/manifest.json | 27 + .../data/conformance/0.1/raw-vectors.json | 14 + .../0.1/raw/unpaired-high-surrogate.json | 145 + .../conformance/0.1/verification-vectors.json | 6422 +++++++++++++++++ .../tests/WorldCut.Tests/packages.lock.json | 281 + scripts/generate-conformance.mjs | 10 + 77 files changed, 17551 insertions(+), 1 deletion(-) create mode 100644 ports/dotnet/.editorconfig create mode 100644 ports/dotnet/.gitignore create mode 100644 ports/dotnet/Directory.Build.props create mode 100644 ports/dotnet/Directory.Packages.props create mode 100644 ports/dotnet/NuGet.config create mode 100644 ports/dotnet/README.md create mode 100644 ports/dotnet/THIRD-PARTY-NOTICES.md create mode 100644 ports/dotnet/WorldCut.sln create mode 100644 ports/dotnet/global.json create mode 100644 ports/dotnet/scripts/package-smoke.ps1 create mode 100644 ports/dotnet/scripts/verify-vendored-source.ps1 create mode 100644 ports/dotnet/src/WorldCut.Tool/PACKAGE.md create mode 100644 ports/dotnet/src/WorldCut.Tool/Program.cs create mode 100644 ports/dotnet/src/WorldCut.Tool/WorldCut.Tool.csproj create mode 100644 ports/dotnet/src/WorldCut.Tool/packages.lock.json create mode 100644 ports/dotnet/src/WorldCut/Engine/AcquisitionPlanner.cs create mode 100644 ports/dotnet/src/WorldCut/Engine/JsonPath.cs create mode 100644 ports/dotnet/src/WorldCut/Engine/RequirementEvaluator.cs create mode 100644 ports/dotnet/src/WorldCut/Json/CanonicalJson.cs create mode 100644 ports/dotnet/src/WorldCut/Json/JsonKind.cs create mode 100644 ports/dotnet/src/WorldCut/Json/JsonText.cs create mode 100644 ports/dotnet/src/WorldCut/Json/JsonValue.cs create mode 100644 ports/dotnet/src/WorldCut/Json/JsonValueReader.cs create mode 100644 ports/dotnet/src/WorldCut/Json/Utf16.cs create mode 100644 ports/dotnet/src/WorldCut/Model/ContractRequirement.cs create mode 100644 ports/dotnet/src/WorldCut/Model/DecisionContract.cs create mode 100644 ports/dotnet/src/WorldCut/Model/NormalizedTimestamp.cs create mode 100644 ports/dotnet/src/WorldCut/Model/Observation.cs create mode 100644 ports/dotnet/src/WorldCut/Model/ResourceIdentity.cs create mode 100644 ports/dotnet/src/WorldCut/Model/Witness.cs create mode 100644 ports/dotnet/src/WorldCut/PACKAGE.md create mode 100644 ports/dotnet/src/WorldCut/ParsedVerificationInput.cs create mode 100644 ports/dotnet/src/WorldCut/Vendored/JcsNet/.editorconfig create mode 100644 ports/dotnet/src/WorldCut/Vendored/JcsNet/CanonicalJsonSerializer.cs create mode 100644 ports/dotnet/src/WorldCut/Vendored/JcsNet/EcmaScriptNumberFormatter.cs create mode 100644 ports/dotnet/src/WorldCut/Vendored/JcsNet/JcsException.cs create mode 100644 ports/dotnet/src/WorldCut/Vendored/JcsNet/JsonCanonicalizer.cs create mode 100644 ports/dotnet/src/WorldCut/Vendored/JcsNet/JsonStringSerializer.cs create mode 100644 ports/dotnet/src/WorldCut/Vendored/JcsNet/JsonTextSurrogateValidator.cs create mode 100644 ports/dotnet/src/WorldCut/Vendored/JcsNet/LICENSE create mode 100644 ports/dotnet/src/WorldCut/VerificationInputValidator.cs create mode 100644 ports/dotnet/src/WorldCut/VerificationResult.cs create mode 100644 ports/dotnet/src/WorldCut/WorldCut.csproj create mode 100644 ports/dotnet/src/WorldCut/WorldCutErrorCode.cs create mode 100644 ports/dotnet/src/WorldCut/WorldCutException.cs create mode 100644 ports/dotnet/src/WorldCut/WorldCutProtocol.cs create mode 100644 ports/dotnet/src/WorldCut/WorldCutVerifier.cs create mode 100644 ports/dotnet/src/WorldCut/packages.lock.json create mode 100644 ports/dotnet/tests/WorldCut.Tests/AcquisitionPlannerTests.cs create mode 100644 ports/dotnet/tests/WorldCut.Tests/CanonicalJsonTests.cs create mode 100644 ports/dotnet/tests/WorldCut.Tests/CliTests.cs create mode 100644 ports/dotnet/tests/WorldCut.Tests/ConformanceCorpus.cs create mode 100644 ports/dotnet/tests/WorldCut.Tests/ConformanceTests.cs create mode 100644 ports/dotnet/tests/WorldCut.Tests/Digest.cs create mode 100644 ports/dotnet/tests/WorldCut.Tests/Fixtures.cs create mode 100644 ports/dotnet/tests/WorldCut.Tests/GlobalUsings.cs create mode 100644 ports/dotnet/tests/WorldCut.Tests/JsonReorder.cs create mode 100644 ports/dotnet/tests/WorldCut.Tests/NormalizedTimestampTests.cs create mode 100644 ports/dotnet/tests/WorldCut.Tests/PropertyTests.cs create mode 100644 ports/dotnet/tests/WorldCut.Tests/PublicApiTests.cs create mode 100644 ports/dotnet/tests/WorldCut.Tests/UnicodeTests.cs create mode 100644 ports/dotnet/tests/WorldCut.Tests/ValidationTests.cs create mode 100644 ports/dotnet/tests/WorldCut.Tests/ValueEqualsInput.cs create mode 100644 ports/dotnet/tests/WorldCut.Tests/ValuePathTests.cs create mode 100644 ports/dotnet/tests/WorldCut.Tests/WorldCut.Tests.csproj create mode 100644 ports/dotnet/tests/WorldCut.Tests/data/conformance/0.1/canonicalization-vectors.json create mode 100644 ports/dotnet/tests/WorldCut.Tests/data/conformance/0.1/invalid-vectors.json create mode 100644 ports/dotnet/tests/WorldCut.Tests/data/conformance/0.1/manifest.json create mode 100644 ports/dotnet/tests/WorldCut.Tests/data/conformance/0.1/raw-vectors.json create mode 100644 ports/dotnet/tests/WorldCut.Tests/data/conformance/0.1/raw/unpaired-high-surrogate.json create mode 100644 ports/dotnet/tests/WorldCut.Tests/data/conformance/0.1/verification-vectors.json create mode 100644 ports/dotnet/tests/WorldCut.Tests/packages.lock.json diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5f53acb..83d7bec 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -37,3 +37,19 @@ updates: - dependency-name: "*" update-types: - version-update:semver-major + + - package-ecosystem: nuget + directory: /ports/dotnet + schedule: + interval: weekly + groups: + dotnet: + patterns: + - "*" + update-types: + - minor + - patch + ignore: + - dependency-name: "*" + update-types: + - version-update:semver-major diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e56c6a..b6ae464 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -143,6 +143,59 @@ jobs: cd .sdist-test ../.sdist-venv/bin/pytest + dotnet: + name: .NET on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - windows-latest + defaults: + run: + working-directory: ports/dotnet + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5 + with: + dotnet-version: | + 8.0.x + 10.0.x + - name: Restore with locked dependencies + run: dotnet restore --locked-mode + - name: Check formatting and code style + run: dotnet format --verify-no-changes --no-restore + - name: Build with warnings as errors + run: dotnet build --configuration Release --no-restore + - name: Test net8.0 and net10.0 + run: dotnet test --configuration Release --no-build + + dotnet-package: + name: .NET package + runs-on: ubuntu-latest + timeout-minutes: 20 + defaults: + run: + working-directory: ports/dotnet + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5 + with: + dotnet-version: | + 8.0.x + 10.0.x + - name: Restore with locked dependencies + run: dotnet restore --locked-mode + - name: Audit dependencies for known vulnerabilities + # NuGetAudit is enabled in ports/dotnet/Directory.Build.props; the + # explicit escalation keeps a new advisory from passing as a warning. + run: dotnet restore --locked-mode --no-cache "-warnaserror:NU1901;NU1902;NU1903;NU1904" + - name: Pack, inspect, and consume both packages in isolation + shell: pwsh + run: ./scripts/package-smoke.ps1 + required: name: Required checks if: always() @@ -152,6 +205,8 @@ jobs: - go - python - python-package + - dotnet + - dotnet-package runs-on: ubuntu-latest steps: - name: Require successful CI @@ -161,9 +216,13 @@ jobs: GO_RESULT: ${{ needs.go.result }} PYTHON_RESULT: ${{ needs.python.result }} PYTHON_PACKAGE_RESULT: ${{ needs.python-package.result }} + DOTNET_RESULT: ${{ needs.dotnet.result }} + DOTNET_PACKAGE_RESULT: ${{ needs.dotnet-package.result }} run: | test "$NODE_RESULT" = "success" test "$BENCHMARK_RESULT" = "success" test "$GO_RESULT" = "success" test "$PYTHON_RESULT" = "success" test "$PYTHON_PACKAGE_RESULT" = "success" + test "$DOTNET_RESULT" = "success" + test "$DOTNET_PACKAGE_RESULT" = "success" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e1e071f..39fa415 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -16,11 +16,36 @@ permissions: jobs: analyze: + name: Analyze ${{ matrix.language }} runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - language: javascript-typescript + build-mode: none + - language: python + build-mode: none + - language: csharp + build-mode: manual steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - if: matrix.language == 'csharp' + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5 + with: + dotnet-version: | + 8.0.x + 10.0.x - uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4 with: - languages: javascript-typescript,python + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + - name: Build the .NET port + if: matrix.language == 'csharp' + working-directory: ports/dotnet + run: dotnet build --configuration Release - name: Analyze uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4 + with: + category: /language:${{ matrix.language }} diff --git a/README.md b/README.md index 5c09997..59aa17f 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ Language-neutral protocol semantics and golden vectors are under | TypeScript | 0.1 / 0.1.2 | Reference package with documented integrations | | [Go](ports/go) | 0.1 / 0.1.2 | Independent conformant verifier and CLI; integrations not yet included | | [Python](ports/python) | 0.1 / 0.1.2 | Independent conformant verifier and CLI; integrations not yet included | +| [.NET](ports/dotnet) | 0.1 / 0.1.2 | Independent conformant verifier and CLI for .NET 8 and .NET 10; integrations not yet included | ## Run the examples diff --git a/ports/dotnet/.editorconfig b/ports/dotnet/.editorconfig new file mode 100644 index 0000000..7f83d13 --- /dev/null +++ b/ports/dotnet/.editorconfig @@ -0,0 +1,121 @@ +# Directory-scoped settings for the independent WorldCut .NET port. +# The repository root .editorconfig still applies to whitespace and encoding. + +root = false + +[*.{csproj,props,targets,config}] +indent_size = 2 + +[*.cs] +indent_size = 4 + +# --- Language and formatting conventions ------------------------------------- + +csharp_using_directive_placement = outside_namespace:error +csharp_style_namespace_declarations = file_scoped:error +csharp_prefer_braces = true:error +csharp_style_prefer_method_group_conversion = true:suggestion +csharp_style_var_for_built_in_types = false:silent +csharp_style_var_when_type_is_apparent = true:silent +csharp_style_var_elsewhere = false:silent +csharp_style_expression_bodied_methods = when_on_single_line:silent +csharp_style_expression_bodied_properties = when_on_single_line:silent +csharp_style_prefer_primary_constructors = false:silent + +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false +dotnet_style_qualification_for_field = false:error +dotnet_style_qualification_for_property = false:error +dotnet_style_qualification_for_method = false:error +dotnet_style_qualification_for_event = false:error +dotnet_style_require_accessibility_modifiers = for_non_interface_members:error +dotnet_style_readonly_field = true:error +dotnet_style_predefined_type_for_locals_parameters_members = true:error +dotnet_style_predefined_type_for_member_access = true:error + +dotnet_diagnostic.IDE0005.severity = error +dotnet_diagnostic.IDE0055.severity = error +dotnet_diagnostic.IDE0161.severity = error + +# --- Naming ------------------------------------------------------------------ +# Roslyn applies the first matching rule, so the const and static-readonly rules +# must precede the private instance-field rule. + +dotnet_naming_rule.constant_fields_are_pascal_case.severity = error +dotnet_naming_rule.constant_fields_are_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_are_pascal_case.style = pascal_case + +dotnet_naming_rule.static_readonly_fields_are_pascal_case.severity = error +dotnet_naming_rule.static_readonly_fields_are_pascal_case.symbols = static_readonly_fields +dotnet_naming_rule.static_readonly_fields_are_pascal_case.style = pascal_case + +dotnet_naming_rule.private_fields_are_underscore_camel_case.severity = error +dotnet_naming_rule.private_fields_are_underscore_camel_case.symbols = private_fields +dotnet_naming_rule.private_fields_are_underscore_camel_case.style = underscore_camel_case + +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.applicable_accessibilities = * +dotnet_naming_symbols.constant_fields.required_modifiers = const + +dotnet_naming_symbols.static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.static_readonly_fields.applicable_accessibilities = * +dotnet_naming_symbols.static_readonly_fields.required_modifiers = static,readonly + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private +dotnet_naming_symbols.private_fields.required_modifiers = + +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.underscore_camel_case.capitalization = camel_case +dotnet_naming_style.underscore_camel_case.required_prefix = _ + +# --- Analyzer policy --------------------------------------------------------- +# `AnalysisLevel=latest-all` turns on every built-in analyzer. Each rule that is +# switched off below records why it does not apply to this port. + +# The port is a small synchronous library with no logging abstraction, no async +# code, and no localization surface. +dotnet_diagnostic.CA1848.severity = none +dotnet_diagnostic.CA2007.severity = none +dotnet_diagnostic.CA1303.severity = none + +# Protocol identifiers such as WORLDCUT_INVALID_INPUT and the JSON member names +# are fixed by spec/0.1 and must not be renamed for .NET naming heuristics. +dotnet_diagnostic.CA1707.severity = none + +# The public surface exposes IReadOnlyList rather than arrays; CA1002 targets +# mutable generic collections that this port never returns. +dotnet_diagnostic.CA1002.severity = none + +# CA1812 flags internal types that the analyzer cannot see being constructed +# through reflection-free factory helpers used by this port. +dotnet_diagnostic.CA1812.severity = none + +# JsonKind.String and JsonKind.Object name the JSON value shapes defined by +# RFC 8259 and used by every WorldCut port; CA1720's "contains type name" +# heuristic does not apply to a JSON kind enumeration. +dotnet_diagnostic.CA1720.severity = none + +[Vendored/**/*.cs] +# Third-party source vendored verbatim from Jcs.NET 0.1.1 (MIT). A dedicated +# .editorconfig also lives next to that source; see +# ports/dotnet/THIRD-PARTY-NOTICES.md. +dotnet_analyzer_diagnostic.severity = none + +[tests/**/*.cs] +# Test code is never a public library surface, so the API-hygiene rules below +# do not apply. Correctness rules stay on. +# +# CA1062: xUnit supplies theory arguments; null guards would be dead code. +# CA1508: several tests deliberately assert on comparisons the analyzer can +# prove, because proving them is the point of the test. +# CA2000: StringWriter holds no unmanaged resource and lives for one assertion. +# CA1861: constant argument arrays in assertions are intentional. +# CA5394: tests use a seeded System.Random for reproducible fuzzing, never for +# anything security relevant. +dotnet_diagnostic.CA1062.severity = none +dotnet_diagnostic.CA1508.severity = none +dotnet_diagnostic.CA2000.severity = none +dotnet_diagnostic.CA1861.severity = none +dotnet_diagnostic.CA5394.severity = none diff --git a/ports/dotnet/.gitignore b/ports/dotnet/.gitignore new file mode 100644 index 0000000..a0daf0c --- /dev/null +++ b/ports/dotnet/.gitignore @@ -0,0 +1,8 @@ +bin/ +obj/ +artifacts/ +.local-feed/ +.package-smoke/ +.vs/ +TestResults/ +*.user diff --git a/ports/dotnet/Directory.Build.props b/ports/dotnet/Directory.Build.props new file mode 100644 index 0000000..8841f25 --- /dev/null +++ b/ports/dotnet/Directory.Build.props @@ -0,0 +1,52 @@ + + + + 12.0 + enable + enable + true + true + true + latest-all + true + true + true + + + + + + true + portable + true + true + true + + + + + true + true + all + low + + + + + 0.1.0 + Jason Doyle + Jason Doyle + Copyright (c) 2026 Jason Doyle + Apache-2.0 + https://github.com/Jason-Doyle/WorldCut + https://github.com/Jason-Doyle/WorldCut + git + false + README.md + true + snupkg + $(MSBuildThisFileDirectory)artifacts\package\ + false + + + diff --git a/ports/dotnet/Directory.Packages.props b/ports/dotnet/Directory.Packages.props new file mode 100644 index 0000000..e502ea8 --- /dev/null +++ b/ports/dotnet/Directory.Packages.props @@ -0,0 +1,18 @@ + + + + true + true + + + + + + + + diff --git a/ports/dotnet/NuGet.config b/ports/dotnet/NuGet.config new file mode 100644 index 0000000..774a159 --- /dev/null +++ b/ports/dotnet/NuGet.config @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/ports/dotnet/README.md b/ports/dotnet/README.md new file mode 100644 index 0000000..b61b4f3 --- /dev/null +++ b/ports/dotnet/README.md @@ -0,0 +1,206 @@ +# WorldCut .NET + +Independent .NET implementation of WorldCut protocol **0.1**, engine ruleset +**0.1.2**, and canonicalization **worldcut-json-v1**. + +It implements the complete verifier, exact acquisition planning, RFC 8785 +canonicalization, and verification-record digests without invoking Node.js, +Python, or Go, and without consuming generated TypeScript output. + +| Package | Purpose | +| --- | --- | +| [`WorldCut`](https://www.nuget.org/packages/WorldCut) | Class library. No third-party package dependencies. | +| [`WorldCut.Tool`](https://www.nuget.org/packages/WorldCut.Tool) | The `worldcut-dotnet` .NET global tool. | + +## Framework support + +Both packages multi-target **`net8.0`** and **`net10.0`**, and both target +frameworks are built, unit tested, conformance tested, packed, and exercised +from an installed package in CI. No other framework is claimed. + +Building this port requires the **.NET 10 SDK** (see [`global.json`](global.json)); +the .NET 8 runtime is additionally required to run the `net8.0` test pass. + +## Install + +```sh +dotnet add package WorldCut +dotnet tool install --global WorldCut.Tool +``` + +## Library + +Verify a document in one call: + +```csharp +using WorldCut; + +VerificationResult result = WorldCutVerifier.VerifyJsonUtf8( + File.ReadAllBytes("verification.json")); + +Console.WriteLine(result.Verdict); // ContractSatisfied +Console.WriteLine(result.Verdict.ToWireName()); // CONTRACT_SATISFIED +Console.WriteLine(result.Coverage.Required); // 3 +Console.WriteLine(result.VerificationRecordDigest); // 64 lowercase hex characters +``` + +Parse once and verify repeatedly: + +```csharp +using WorldCut; + +ParsedVerificationInput input = ParsedVerificationInput.Parse(json); + +foreach (RequirementResult requirement in WorldCutVerifier.Verify(input).RequirementResults) +{ + Console.WriteLine($"{requirement.RequirementId}: {requirement.Status.ToWireName()}"); + Console.WriteLine(requirement.Summary); +} +``` + +`ParsedVerificationInput` has no public constructor, so it cannot be built into +an invalid state, and every value reachable from a parsed input or a result is +immutable. Verifying the same parsed input twice always produces two +independent, identical results. + +Canonicalize and digest arbitrary JSON data: + +```csharp +using WorldCut.Json; + +JsonValue value = JsonValue.Parse("""{"z":1,"a":2}"""); + +CanonicalJson.Serialize(value); // {"a":2,"z":1} +CanonicalJson.ComputeSha256Hex(value); // 64 lowercase hex characters +JsonText.Indent(value); // presentation JSON, member order preserved +``` + +Handle failures: + +```csharp +using WorldCut; + +try +{ + WorldCutVerifier.VerifyJson(json); +} +catch (WorldCutException error) +{ + Console.Error.WriteLine($"{error.WireCode}: {error.Message}"); +} +``` + +`WorldCutException` is the only exception type the API raises for protocol +failures. `WireCode` is one of `WORLDCUT_INVALID_INPUT`, +`WORLDCUT_INVALID_ARGUMENT`, `WORLDCUT_FILE_READ_FAILED`, or +`WORLDCUT_RUNTIME_ERROR`. JSON syntax failures are reported as +`WORLDCUT_INVALID_INPUT`, matching the Go and Python ports. + +## CLI + +```text +Usage: worldcut-dotnet [--require-satisfied] + +Options: + --require-satisfied Exit with code 2 unless the contract is satisfied + --help Show this help +``` + +```sh +worldcut-dotnet ../../examples/coherent-deployment.json +worldcut-dotnet --require-satisfied ../../examples/git-ci-mismatch.json +``` + +The CLI reads exactly one verification input file and prints the complete +verification result as JSON on standard output, as UTF-8, with non-ASCII +characters written literally. + +| Exit code | Meaning | +| ---: | --- | +| `0` | The input was verified; or `--help` was requested; or `--require-satisfied` received a satisfied contract | +| `1` | Argument, file, input, or runtime failure | +| `2` | `--require-satisfied` received a non-satisfied verdict | + +Failures are written to standard error as a stable JSON envelope: + +```json +{"error":{"code":"WORLDCUT_INVALID_INPUT","message":"..."}} +``` + +## Canonicalization + +`worldcut-json-v1` is RFC 8785 applied to the accepted WorldCut JSON data +domain. The RFC 8785 serializer is **vendored source** from Jcs.NET 0.1.1 +(MIT), not a package dependency; see +[`THIRD-PARTY-NOTICES.md`](THIRD-PARTY-NOTICES.md) for the attribution, the +exact modifications, and why the source is vendored. + +WorldCut owns the behaviour layered on top: + +* **Unpaired surrogates are rejected before anything else happens.** + `System.Text.Json` substitutes `U+FFFD` for unpaired UTF-16 surrogates when + writing, which would silently repair malformed data into a different digest. + Every unpaired surrogate code unit — raw or `\uXXXX` escaped — is rejected + during parsing and during `JsonValue` construction. Raw input must also be + well-formed UTF-8. +* **Duplicate JSON members follow last-value-wins**, matching `JSON.parse` in + the reference implementation and `json.loads` in the Python port, rather than + RFC 8785's duplicate-name rejection. +* **Nesting limits are an explicit, stable port policy.** Parsing accepts at + most `WorldCutProtocol.MaxJsonDepth` (48) levels and canonicalization accepts + at most `WorldCutProtocol.MaxCanonicalizationDepth` (64). The parse limit is + lower on purpose: a verification record wraps input values in up to eight + further levels, so any input this port accepts can always be canonicalized. + Deeper input is a structured `WORLDCUT_INVALID_INPUT` failure, never a stack + overflow. +* **Timestamps do not use `DateTime`.** The protocol accepts the full + ECMAScript date domain, including year `0000`, which `DateTime.MinValue` + cannot represent. `NormalizedTimestamp` parses the literal + `YYYY-MM-DDTHH:MM:SS.mmmZ` grammar and orders instants with a proleptic + Gregorian millisecond ordinal. + +## Boundaries + +This port contains the verifier, canonicalization, and CLI only. It +deliberately does **not** include the cloud metadata adapters, the GitHub +Actions integration, or the Agentic Data Kernel adapter that ship with the +TypeScript reference package. + +WorldCut evaluates a declared contract deterministically. It does not decide +what the contract should be, infer missing relationships, fetch evidence, or +establish that a provider is truthful. The verification-record digest detects +record changes; it is not a digital signature. + +## Validate + +Run from `ports/dotnet` with the .NET 10 SDK and the .NET 8 runtime available: + +```sh +dotnet restore --locked-mode +dotnet format --verify-no-changes --no-restore +dotnet build --configuration Release --no-restore +dotnet test --configuration Release --no-build +dotnet pack --configuration Release --no-build +pwsh scripts/package-smoke.ps1 +``` + +`dotnet test` runs every test against both `net8.0` and `net10.0`. The suite +covers: + +* the complete shared corpus under [`conformance/0.1`](../../conformance/0.1) — + full golden verification results, stable error codes, exact canonical bytes + and digests, raw-byte rejection, and the file manifest; +* input order independence, immutability of parsed inputs and results, planner + boundaries, Unicode and year-zero handling, CLI exit codes and error + envelopes; +* deterministic seeded randomized invariant checks for canonicalization, + number round-tripping, ordering, and structured-error containment. + +The corpus is mirrored into `tests/WorldCut.Tests/data` by +`scripts/generate-conformance.mjs`, so the tests never read a file from the +parent repository. + +`scripts/package-smoke.ps1` packs both packages into a local feed, asserts the +package contents against a strict allowlist, consumes the library package from +an isolated project on both target frameworks, installs the dotnet tool from +the feed, and checks every documented CLI exit code. diff --git a/ports/dotnet/THIRD-PARTY-NOTICES.md b/ports/dotnet/THIRD-PARTY-NOTICES.md new file mode 100644 index 0000000..8478612 --- /dev/null +++ b/ports/dotnet/THIRD-PARTY-NOTICES.md @@ -0,0 +1,128 @@ +# Third-party notices — WorldCut .NET port + +The `WorldCut` NuGet package has **no third-party package dependencies**. It +does, however, contain vendored third-party source code. That source is listed +here in full, with its licence and the exact modifications WorldCut applied. + +--- + +## Jcs.NET 0.1.1 (MIT) + +| Field | Value | +| --- | --- | +| Project | Jcs.NET | +| Upstream | | +| Version | 0.1.1 | +| Commit | `8aff61685300d5d94b81f05246f95d4681e7178a` | +| Licence | MIT | +| Copyright | Copyright (c) 2026 Israel Iyonsi | +| Vendored to | `src/WorldCut/Vendored/JcsNet/` | + +Jcs.NET implements RFC 8785 (JSON Canonicalization Scheme). WorldCut's +`worldcut-json-v1` canonicalization is defined in terms of the same RFC 8785 +rules, so WorldCut uses this implementation as its canonical serializer rather +than writing a fourth independent ECMAScript number formatter. + +### Why the source is vendored instead of referenced + +* The `WorldCut` package keeps a zero-dependency install graph, which matters + for a verification library used inside deployment gates. +* The exact canonicalization bytes are protocol-normative. Pinning the source + by commit removes any possibility of a transitive package upgrade silently + changing a verification-record digest. +* Vendoring keeps the audited implementation reviewable inside this repository. + +### Files vendored + +The following files were copied from `src/Jcs.Net/` at the commit above. The +SHA-256 values identify the raw **upstream** files; they are not hashes of the +vendored copies, which contain the documented attribution and visibility +changes. `scripts/verify-vendored-source.ps1` reverses those exact changes and +checks the reconstructed bytes against these values. + +| File | Upstream SHA-256 | +| --- | --- | +| `CanonicalJsonSerializer.cs` | `3e9083f0273c29cebbc2e6e12eee9ce083580862a310e5b198ff9f8f0c624375` | +| `EcmaScriptNumberFormatter.cs` | `6f4f63a52d7e5f39a27efbbd14d8f095d7480ddbe4c455653778b0aedb1eb2fb` | +| `JcsException.cs` | `78156a3dc02aa98065446f4eb81fb34f2c24ad9687d99ff7dc0a395746a3ea4c` | +| `JsonCanonicalizer.cs` | `dd8c55948053399755cc99a7d03c3dcf9b49cde534ee86fe88cd7a412039edd0` | +| `JsonStringSerializer.cs` | `e910166a61aeba751fac47562d154a381e155384d884474eac9bd43b86b4cf4e` | +| `JsonTextSurrogateValidator.cs` | `61d919b8cc2ecec7144a567f51339fb47dd2a6df521e25c1790f9f1b4d6a1a43` | + +`src/WorldCut/Vendored/JcsNet/LICENSE` is the upstream MIT licence text, +unmodified. + +### Modifications applied by WorldCut + +1. A twelve-line attribution header was prepended to each `.cs` file. It names + the upstream project, version, commit, copyright holder, and licence, and + states that the file is not original WorldCut code. +2. `JsonCanonicalizer` was changed from `public static class` to + `internal static class`. +3. `JcsException` was changed from `public sealed class` to + `internal sealed class`. + +Changes 2 and 3 exist so that the `WorldCut` package does not re-export another +project's public API surface under the `Jcs.Net` namespace, which would collide +for consumers that also reference the real `Jcs.Net` package. No behaviour, +algorithm, message, or limit was altered. Line endings were normalised to LF to +match this repository. + +### WorldCut policy around the vendored code + +WorldCut does not call the vendored code directly from its public API. It sits +behind `WorldCut.Json.CanonicalJson`, which adds the behaviour WorldCut's +protocol requires and the vendored library deliberately does not provide: + +* **Unpaired-surrogate pre-validation owned by WorldCut.** `System.Text.Json` + substitutes `U+FFFD` for unpaired UTF-16 surrogates when *writing*. WorldCut + therefore rejects every unpaired surrogate code unit — raw or `\uXXXX` + escaped — before any value reaches canonicalization or verification, so a + malformed string can never be silently repaired into a different digest. +* **Duplicate JSON member handling.** RFC 8785 rejects duplicate object member + names. WorldCut's protocol follows `JSON.parse`/`json.loads` last-value-wins + semantics, so WorldCut deduplicates while parsing and the vendored duplicate + check is never reached. +* **A documented nesting limit.** `CanonicalJsonSerializer` caps nesting at 64 + levels and throws `JcsException`. WorldCut adopts that cap as an explicit, + stable port policy: canonicalization accepts at most + `WorldCutProtocol.MaxCanonicalizationDepth` (64) levels of nesting, and + parsing accepts at most `WorldCutProtocol.MaxJsonDepth` (48). The parsing + limit is lower on purpose, because the verification record wraps input values + in up to eight further levels; the difference guarantees that any input the + port accepts can always be canonicalized. +* **Structured errors.** Every `JsonException` (including `JcsException`) that + can escape the vendored code is translated into a `WorldCutException` with a + stable WorldCut error code. No `Jcs.Net` type is observable from the WorldCut + public API. + +### Upstream licence + +```text +MIT License + +Copyright (c) 2026 Israel Iyonsi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +--- + +WorldCut itself is licensed under the Apache License 2.0; see the repository +[`LICENSE`](../../LICENSE). diff --git a/ports/dotnet/WorldCut.sln b/ports/dotnet/WorldCut.sln new file mode 100644 index 0000000..8d2dcd1 --- /dev/null +++ b/ports/dotnet/WorldCut.sln @@ -0,0 +1,71 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorldCut", "src\WorldCut\WorldCut.csproj", "{3067FC35-0036-4F24-8EFC-F3673580A8A7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorldCut.Tool", "src\WorldCut.Tool\WorldCut.Tool.csproj", "{325DC93A-5BFF-4644-A354-56869DF4BC4F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorldCut.Tests", "tests\WorldCut.Tests\WorldCut.Tests.csproj", "{1C9E6B67-8C18-4274-A249-57CC9A5E693D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3067FC35-0036-4F24-8EFC-F3673580A8A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3067FC35-0036-4F24-8EFC-F3673580A8A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3067FC35-0036-4F24-8EFC-F3673580A8A7}.Debug|x64.ActiveCfg = Debug|Any CPU + {3067FC35-0036-4F24-8EFC-F3673580A8A7}.Debug|x64.Build.0 = Debug|Any CPU + {3067FC35-0036-4F24-8EFC-F3673580A8A7}.Debug|x86.ActiveCfg = Debug|Any CPU + {3067FC35-0036-4F24-8EFC-F3673580A8A7}.Debug|x86.Build.0 = Debug|Any CPU + {3067FC35-0036-4F24-8EFC-F3673580A8A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3067FC35-0036-4F24-8EFC-F3673580A8A7}.Release|Any CPU.Build.0 = Release|Any CPU + {3067FC35-0036-4F24-8EFC-F3673580A8A7}.Release|x64.ActiveCfg = Release|Any CPU + {3067FC35-0036-4F24-8EFC-F3673580A8A7}.Release|x64.Build.0 = Release|Any CPU + {3067FC35-0036-4F24-8EFC-F3673580A8A7}.Release|x86.ActiveCfg = Release|Any CPU + {3067FC35-0036-4F24-8EFC-F3673580A8A7}.Release|x86.Build.0 = Release|Any CPU + {325DC93A-5BFF-4644-A354-56869DF4BC4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {325DC93A-5BFF-4644-A354-56869DF4BC4F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {325DC93A-5BFF-4644-A354-56869DF4BC4F}.Debug|x64.ActiveCfg = Debug|Any CPU + {325DC93A-5BFF-4644-A354-56869DF4BC4F}.Debug|x64.Build.0 = Debug|Any CPU + {325DC93A-5BFF-4644-A354-56869DF4BC4F}.Debug|x86.ActiveCfg = Debug|Any CPU + {325DC93A-5BFF-4644-A354-56869DF4BC4F}.Debug|x86.Build.0 = Debug|Any CPU + {325DC93A-5BFF-4644-A354-56869DF4BC4F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {325DC93A-5BFF-4644-A354-56869DF4BC4F}.Release|Any CPU.Build.0 = Release|Any CPU + {325DC93A-5BFF-4644-A354-56869DF4BC4F}.Release|x64.ActiveCfg = Release|Any CPU + {325DC93A-5BFF-4644-A354-56869DF4BC4F}.Release|x64.Build.0 = Release|Any CPU + {325DC93A-5BFF-4644-A354-56869DF4BC4F}.Release|x86.ActiveCfg = Release|Any CPU + {325DC93A-5BFF-4644-A354-56869DF4BC4F}.Release|x86.Build.0 = Release|Any CPU + {1C9E6B67-8C18-4274-A249-57CC9A5E693D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1C9E6B67-8C18-4274-A249-57CC9A5E693D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1C9E6B67-8C18-4274-A249-57CC9A5E693D}.Debug|x64.ActiveCfg = Debug|Any CPU + {1C9E6B67-8C18-4274-A249-57CC9A5E693D}.Debug|x64.Build.0 = Debug|Any CPU + {1C9E6B67-8C18-4274-A249-57CC9A5E693D}.Debug|x86.ActiveCfg = Debug|Any CPU + {1C9E6B67-8C18-4274-A249-57CC9A5E693D}.Debug|x86.Build.0 = Debug|Any CPU + {1C9E6B67-8C18-4274-A249-57CC9A5E693D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1C9E6B67-8C18-4274-A249-57CC9A5E693D}.Release|Any CPU.Build.0 = Release|Any CPU + {1C9E6B67-8C18-4274-A249-57CC9A5E693D}.Release|x64.ActiveCfg = Release|Any CPU + {1C9E6B67-8C18-4274-A249-57CC9A5E693D}.Release|x64.Build.0 = Release|Any CPU + {1C9E6B67-8C18-4274-A249-57CC9A5E693D}.Release|x86.ActiveCfg = Release|Any CPU + {1C9E6B67-8C18-4274-A249-57CC9A5E693D}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {3067FC35-0036-4F24-8EFC-F3673580A8A7} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {325DC93A-5BFF-4644-A354-56869DF4BC4F} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {1C9E6B67-8C18-4274-A249-57CC9A5E693D} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + EndGlobalSection +EndGlobal diff --git a/ports/dotnet/global.json b/ports/dotnet/global.json new file mode 100644 index 0000000..df32a2e --- /dev/null +++ b/ports/dotnet/global.json @@ -0,0 +1,10 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature", + "allowPrerelease": false + }, + "test": { + "runner": "Microsoft.Testing.Platform" + } +} diff --git a/ports/dotnet/scripts/package-smoke.ps1 b/ports/dotnet/scripts/package-smoke.ps1 new file mode 100644 index 0000000..c4e7386 --- /dev/null +++ b/ports/dotnet/scripts/package-smoke.ps1 @@ -0,0 +1,393 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Packs the WorldCut .NET port and proves both packages work from a clean, + isolated NuGet feed. + +.DESCRIPTION + This script is the packaging gate for ports/dotnet. It: + + 1. packs WorldCut and WorldCut.Tool into a local feed; + 2. asserts each package contains exactly the allowed files; + 3. builds and runs an isolated library consumer on .NET 8 and .NET 10 and + compares the verification-record digest with the committed conformance + vector; + 4. installs the dotnet tool from the local feed and checks the CLI + fixture output and every documented exit code; + 5. runs the packaged net8.0 tool asset directly so both shipped target + frameworks are exercised. + + The isolated workspace deliberately shadows Directory.Build.props, + Directory.Packages.props, and NuGet.config so the consumer sees only what a + real customer would see. + +.PARAMETER Dotnet + The dotnet host to use. Defaults to `dotnet` from PATH. + +.PARAMETER Configuration + The build configuration to pack. Defaults to Release. +#> + +[CmdletBinding()] +param( + [string] $Dotnet = 'dotnet', + [string] $Configuration = 'Release' +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$portRoot = Split-Path -Parent $PSScriptRoot +$repoRoot = Split-Path -Parent (Split-Path -Parent $portRoot) +$workspace = Join-Path $portRoot '.package-smoke' +$feed = Join-Path $workspace 'feed' +$packageVersion = '0.1.0' + +# An installed tool's apphost resolves its runtime through DOTNET_ROOT or the +# machine-wide install. When an explicit dotnet host is supplied from a private +# location, point both at it so the smoke test exercises the same runtime that +# produced the packages. A `shared` directory identifies a real dotnet root; a +# symlink such as /usr/bin/dotnet is left alone. +$resolvedDotnet = Get-Command $Dotnet -ErrorAction SilentlyContinue +if ($null -ne $resolvedDotnet -and [System.IO.Path]::IsPathRooted($resolvedDotnet.Source)) { + $dotnetRoot = Split-Path -Parent $resolvedDotnet.Source + if (Test-Path -LiteralPath (Join-Path $dotnetRoot 'shared')) { + $env:DOTNET_ROOT = $dotnetRoot + $env:PATH = "$dotnetRoot$([System.IO.Path]::PathSeparator)$env:PATH" + } +} + +$libraryAllowlist = @( + '_rels/.rels', + '[Content_Types].xml', + 'WorldCut.nuspec', + 'README.md', + 'THIRD-PARTY-NOTICES.md', + 'lib/net8.0/WorldCut.dll', + 'lib/net8.0/WorldCut.xml', + 'lib/net10.0/WorldCut.dll', + 'lib/net10.0/WorldCut.xml' +) + +$toolAllowlist = @( + '_rels/.rels', + '[Content_Types].xml', + 'WorldCut.Tool.nuspec', + 'README.md', + 'THIRD-PARTY-NOTICES.md' +) +foreach ($framework in @('net8.0', 'net10.0')) { + $toolAllowlist += @( + "tools/$framework/any/DotnetToolSettings.xml", + "tools/$framework/any/WorldCut.Tool.deps.json", + "tools/$framework/any/WorldCut.Tool.dll", + "tools/$framework/any/WorldCut.Tool.pdb", + "tools/$framework/any/WorldCut.Tool.runtimeconfig.json", + "tools/$framework/any/WorldCut.Tool.xml", + "tools/$framework/any/WorldCut.dll", + "tools/$framework/any/WorldCut.pdb", + "tools/$framework/any/WorldCut.xml" + ) +} + +function Invoke-Step { + param( + [Parameter(Mandatory)] [string] $Name, + [Parameter(Mandatory)] [scriptblock] $Body + ) + + Write-Host "==> $Name" -ForegroundColor Cyan + $global:LASTEXITCODE = 0 + & $Body + if ($LASTEXITCODE -ne 0) { + throw "$Name failed with exit code $LASTEXITCODE" + } +} + +function Assert-True { + param( + [Parameter(Mandatory)] [bool] $Condition, + [Parameter(Mandatory)] [string] $Message + ) + + if (-not $Condition) { + throw $Message + } +} + +function Get-PackageEntry { + param([Parameter(Mandatory)] [string] $Path) + + Add-Type -AssemblyName System.IO.Compression.FileSystem | Out-Null + $archive = [System.IO.Compression.ZipFile]::OpenRead($Path) + try { + return @($archive.Entries | ForEach-Object { $_.FullName }) + } + finally { + $archive.Dispose() + } +} + +function Assert-PackageContent { + param( + [Parameter(Mandatory)] [string] $Path, + [Parameter(Mandatory)] [string[]] $Allowed + ) + + $entries = Get-PackageEntry -Path $Path + $ignored = 'package/services/metadata/core-properties/' + + $actual = @($entries | Where-Object { -not $_.StartsWith($ignored, 'Ordinal') }) + $unexpected = @($actual | Where-Object { $Allowed -notcontains $_ }) + $missing = @($Allowed | Where-Object { $actual -notcontains $_ }) + + if ($unexpected.Count -gt 0) { + throw "$Path contains unexpected files: $($unexpected -join ', ')" + } + if ($missing.Count -gt 0) { + throw "$Path is missing expected files: $($missing -join ', ')" + } + + foreach ($entry in $actual) { + foreach ($forbidden in @('.cs', '.csproj', '.sln', 'packages.lock.json', '.user')) { + if ($entry.EndsWith($forbidden, 'Ordinal')) { + throw "$Path leaks build or source content: $entry" + } + } + } + + Write-Host " $([System.IO.Path]::GetFileName($Path)): $($actual.Count) files, allowlist satisfied" +} + +function Get-ExpectedDigest { + $vectorPath = Join-Path $repoRoot 'conformance/0.1/verification-vectors.json' + $vectors = Get-Content -Raw -LiteralPath $vectorPath | ConvertFrom-Json + $coherent = $vectors.cases | Where-Object { $_.name -eq 'coherent' } + Assert-True ($null -ne $coherent) 'the coherent conformance vector is missing' + return $coherent.expected.verificationRecordDigest +} + +function New-IsolatedWorkspace { + if (Test-Path -LiteralPath $workspace) { + Remove-Item -Recurse -Force -LiteralPath $workspace + } + New-Item -ItemType Directory -Path $workspace | Out-Null + New-Item -ItemType Directory -Path $feed | Out-Null + + # Shadow every inherited MSBuild and NuGet setting so the consumer sees the + # package exactly as a customer would. + Set-Content -LiteralPath (Join-Path $workspace 'Directory.Build.props') -Value '' -NoNewline + Set-Content -LiteralPath (Join-Path $workspace 'Directory.Build.targets') -Value '' -NoNewline + Set-Content -LiteralPath (Join-Path $workspace 'Directory.Packages.props') -Value '' -NoNewline + Set-Content -LiteralPath (Join-Path $workspace 'global.json') -Value '{}' -NoNewline + + $feedPath = (Resolve-Path -LiteralPath $feed).Path + $nugetConfig = @" + + + + + + + + + + + + + + + + + +"@ + Set-Content -LiteralPath (Join-Path $workspace 'NuGet.config') -Value $nugetConfig +} + +function New-LibraryConsumer { + $consumer = Join-Path $workspace 'library-consumer' + New-Item -ItemType Directory -Path $consumer | Out-Null + + $project = @" + + + Exe + net8.0;net10.0 + enable + enable + LibraryConsumer + + + + + +"@ + Set-Content -LiteralPath (Join-Path $consumer 'LibraryConsumer.csproj') -Value $project + + $program = @' +using WorldCut; +using WorldCut.Json; + +byte[] source = File.ReadAllBytes(args[0]); + +ParsedVerificationInput input = ParsedVerificationInput.ParseUtf8(source); +VerificationResult first = WorldCutVerifier.Verify(input); +VerificationResult second = WorldCutVerifier.Verify(input); + +if (first.VerificationRecordDigest != second.VerificationRecordDigest) +{ + Console.Error.WriteLine("repeated verification changed the digest"); + return 1; +} + +if (CanonicalJson.Serialize(JsonValue.Parse("{\"z\":1,\"a\":2}")) != "{\"a\":2,\"z\":1}") +{ + Console.Error.WriteLine("canonicalization is wrong"); + return 1; +} + +try +{ + WorldCutVerifier.VerifyJson("not json"); + Console.Error.WriteLine("invalid input was accepted"); + return 1; +} +catch (WorldCutException error) when (error.WireCode == "WORLDCUT_INVALID_INPUT") +{ +} + +Console.WriteLine(WorldCutProtocol.ProtocolVersion); +Console.WriteLine(WorldCutProtocol.EngineVersion); +Console.WriteLine(WorldCutProtocol.Canonicalization); +Console.WriteLine(first.Verdict.ToWireName()); +Console.WriteLine(first.VerificationRecordDigest); +return 0; +'@ + Set-Content -LiteralPath (Join-Path $consumer 'Program.cs') -Value $program + + return $consumer +} + +$expectedDigest = Get-ExpectedDigest +$coherentFixture = (Resolve-Path -LiteralPath (Join-Path $repoRoot 'examples/coherent-deployment.json')).Path +$mismatchFixture = (Resolve-Path -LiteralPath (Join-Path $repoRoot 'examples/git-ci-mismatch.json')).Path + +Invoke-Step 'Verify vendored Jcs.Net provenance' { + & (Join-Path $PSScriptRoot 'verify-vendored-source.ps1') +} + +Invoke-Step 'Create the isolated workspace' { New-IsolatedWorkspace } + +Invoke-Step 'Pack WorldCut and WorldCut.Tool into the local feed' { + & $Dotnet pack (Join-Path $portRoot 'WorldCut.sln') ` + --configuration $Configuration ` + --output $feed ` + -p:ContinuousIntegrationBuild=true +} + +Invoke-Step 'Validate package contents' { + Assert-PackageContent -Path (Join-Path $feed "WorldCut.$packageVersion.nupkg") -Allowed $libraryAllowlist + Assert-PackageContent -Path (Join-Path $feed "WorldCut.Tool.$packageVersion.nupkg") -Allowed $toolAllowlist + Assert-True (Test-Path -LiteralPath (Join-Path $feed "WorldCut.$packageVersion.snupkg")) 'the library symbol package is missing' + $global:LASTEXITCODE = 0 +} + +$consumer = New-LibraryConsumer +$consumerProject = Join-Path $consumer 'LibraryConsumer.csproj' + +Invoke-Step 'Restore the isolated library consumer' { + & $Dotnet restore $consumerProject +} + +foreach ($framework in @('net8.0', 'net10.0')) { + Invoke-Step "Run the isolated library consumer on $framework" { + $output = & $Dotnet run --project $consumerProject --framework $framework --no-restore -- $coherentFixture + if ($LASTEXITCODE -ne 0) { + throw "the library consumer failed on $framework" + } + + $lines = @($output) + Assert-True ($lines.Count -ge 5) "the library consumer printed $($lines.Count) lines on $framework" + Assert-True ($lines[-5] -eq '0.1') 'the consumer reported an unexpected protocol version' + Assert-True ($lines[-4] -eq '0.1.2') 'the consumer reported an unexpected engine version' + Assert-True ($lines[-3] -eq 'worldcut-json-v1') 'the consumer reported an unexpected canonicalization' + Assert-True ($lines[-2] -eq 'CONTRACT_SATISFIED') 'the consumer reported an unexpected verdict' + Assert-True ($lines[-1] -eq $expectedDigest) "the consumer digest does not match the conformance vector on $framework" + $global:LASTEXITCODE = 0 + } +} + +$toolPath = Join-Path $workspace 'tools' + +Invoke-Step 'Install the dotnet tool from the local feed' { + # --add-source cannot be combined with package source mapping, so the + # isolated NuGet.config supplies the local feed instead. + & $Dotnet tool install WorldCut.Tool ` + --version $packageVersion ` + --tool-path $toolPath ` + --configfile (Join-Path $workspace 'NuGet.config') +} + +$toolExecutable = Join-Path $toolPath 'worldcut-dotnet' +if ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform( + [System.Runtime.InteropServices.OSPlatform]::Windows)) { + $toolExecutable = "$toolExecutable.exe" +} + +Invoke-Step 'Check the installed CLI' { + Assert-True (Test-Path -LiteralPath $toolExecutable) "the tool was not installed at $toolExecutable" + + $help = & $toolExecutable --help + Assert-True ($LASTEXITCODE -eq 0) '--help must exit with 0' + Assert-True (($help -join "`n") -match 'worldcut-dotnet') '--help must describe the command' + + $satisfied = & $toolExecutable $coherentFixture + Assert-True ($LASTEXITCODE -eq 0) 'a satisfied contract must exit with 0' + $result = ($satisfied -join "`n") | ConvertFrom-Json + Assert-True ($result.verdict -eq 'CONTRACT_SATISFIED') 'the CLI reported an unexpected verdict' + Assert-True ($result.engineVersion -eq '0.1.2') 'the CLI reported an unexpected engine version' + Assert-True ($result.verificationRecordDigest -eq $expectedDigest) 'the CLI digest does not match the conformance vector' + + & $toolExecutable --require-satisfied $coherentFixture | Out-Null + Assert-True ($LASTEXITCODE -eq 0) '--require-satisfied must exit with 0 for a satisfied contract' + + & $toolExecutable --require-satisfied $mismatchFixture | Out-Null + Assert-True ($LASTEXITCODE -eq 2) '--require-satisfied must exit with 2 for a violated contract' + + $errorOutput = & $toolExecutable --not-a-flag 2>&1 + Assert-True ($LASTEXITCODE -eq 1) 'an unknown option must exit with 1' + $envelope = ($errorOutput -join "`n") | ConvertFrom-Json + Assert-True ($envelope.error.code -eq 'WORLDCUT_INVALID_ARGUMENT') 'an unknown option must report WORLDCUT_INVALID_ARGUMENT' + + $missingOutput = & $toolExecutable (Join-Path $workspace 'missing.json') 2>&1 + Assert-True ($LASTEXITCODE -eq 1) 'a missing file must exit with 1' + $missingEnvelope = ($missingOutput -join "`n") | ConvertFrom-Json + Assert-True ($missingEnvelope.error.code -eq 'WORLDCUT_FILE_READ_FAILED') 'a missing file must report WORLDCUT_FILE_READ_FAILED' + + $global:LASTEXITCODE = 0 +} + +Invoke-Step 'Run the packaged net8.0 tool asset directly' { + $extracted = Join-Path $workspace 'tool-net8' + if (Test-Path -LiteralPath $extracted) { + Remove-Item -Recurse -Force -LiteralPath $extracted + } + + Add-Type -AssemblyName System.IO.Compression.FileSystem | Out-Null + [System.IO.Compression.ZipFile]::ExtractToDirectory( + (Join-Path $feed "WorldCut.Tool.$packageVersion.nupkg"), + $extracted) + + $assembly = Join-Path $extracted 'tools/net8.0/any/WorldCut.Tool.dll' + Assert-True (Test-Path -LiteralPath $assembly) 'the net8.0 tool asset is missing' + + $output = & $Dotnet $assembly $coherentFixture + Assert-True ($LASTEXITCODE -eq 0) 'the net8.0 tool asset must exit with 0' + $result = ($output -join "`n") | ConvertFrom-Json + Assert-True ($result.verificationRecordDigest -eq $expectedDigest) 'the net8.0 tool asset produced a different digest' + + $global:LASTEXITCODE = 0 +} + +Write-Host '' +Write-Host 'Package smoke test passed.' -ForegroundColor Green diff --git a/ports/dotnet/scripts/verify-vendored-source.ps1 b/ports/dotnet/scripts/verify-vendored-source.ps1 new file mode 100644 index 0000000..263a5e7 --- /dev/null +++ b/ports/dotnet/scripts/verify-vendored-source.ps1 @@ -0,0 +1,87 @@ +#!/usr/bin/env pwsh + +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$portRoot = Split-Path -Parent $PSScriptRoot +$sourceRoot = Join-Path $portRoot 'src/WorldCut/Vendored/JcsNet' +$headerBoundary = "// -----------------------------------------------------------------------------`n" +$utf8 = [System.Text.UTF8Encoding]::new($false) + +$expectedHashes = [ordered]@{ + 'CanonicalJsonSerializer.cs' = '3e9083f0273c29cebbc2e6e12eee9ce083580862a310e5b198ff9f8f0c624375' + 'EcmaScriptNumberFormatter.cs' = '6f4f63a52d7e5f39a27efbbd14d8f095d7480ddbe4c455653778b0aedb1eb2fb' + 'JcsException.cs' = '78156a3dc02aa98065446f4eb81fb34f2c24ad9687d99ff7dc0a395746a3ea4c' + 'JsonCanonicalizer.cs' = 'dd8c55948053399755cc99a7d03c3dcf9b49cde534ee86fe88cd7a412039edd0' + 'JsonStringSerializer.cs' = 'e910166a61aeba751fac47562d154a381e155384d884474eac9bd43b86b4cf4e' + 'JsonTextSurrogateValidator.cs' = '61d919b8cc2ecec7144a567f51339fb47dd2a6df521e25c1790f9f1b4d6a1a43' + 'LICENSE' = '8e027d0ebfb96b3d3f425b5398a723f04ed21a3d33ab8d346f6f10ff9142bfaa' +} + +$visibilityChanges = @{ + 'JcsException.cs' = @( + 'internal sealed class JcsException', + 'public sealed class JcsException' + ) + 'JsonCanonicalizer.cs' = @( + 'internal static class JsonCanonicalizer', + 'public static class JsonCanonicalizer' + ) +} + +function ConvertTo-Lf { + param([Parameter(Mandatory)] [string] $Text) + + return $Text.Replace("`r`n", "`n").Replace("`r", "`n") +} + +function Get-Sha256 { + param([Parameter(Mandatory)] [string] $Text) + + $bytes = $utf8.GetBytes($Text) + return [Convert]::ToHexString( + [System.Security.Cryptography.SHA256]::HashData($bytes) + ).ToLowerInvariant() +} + +foreach ($entry in $expectedHashes.GetEnumerator()) { + $path = Join-Path $sourceRoot $entry.Key + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { + throw "Vendored Jcs.Net file is missing: $($entry.Key)" + } + + $text = ConvertTo-Lf ([System.IO.File]::ReadAllText($path)) + if ($entry.Key -ne 'LICENSE') { + if (-not $text.StartsWith($headerBoundary, 'Ordinal')) { + throw "Vendored attribution header is missing from $($entry.Key)" + } + + $secondBoundary = $text.IndexOf( + $headerBoundary, + $headerBoundary.Length, + [System.StringComparison]::Ordinal) + if ($secondBoundary -lt 0) { + throw "Vendored attribution header is malformed in $($entry.Key)" + } + + $text = $text.Substring($secondBoundary + $headerBoundary.Length) + + if ($visibilityChanges.ContainsKey($entry.Key)) { + $change = $visibilityChanges[$entry.Key] + if (-not $text.Contains($change[0], 'Ordinal')) { + throw "Documented visibility change is missing from $($entry.Key)" + } + $text = $text.Replace($change[0], $change[1], 'Ordinal') + } + } + + $actual = Get-Sha256 $text + if ($actual -ne $entry.Value) { + throw "Vendored Jcs.Net provenance mismatch for $($entry.Key): expected $($entry.Value), got $actual" + } +} + +Write-Host 'Vendored Jcs.Net provenance verified.' diff --git a/ports/dotnet/src/WorldCut.Tool/PACKAGE.md b/ports/dotnet/src/WorldCut.Tool/PACKAGE.md new file mode 100644 index 0000000..fe346f5 --- /dev/null +++ b/ports/dotnet/src/WorldCut.Tool/PACKAGE.md @@ -0,0 +1,55 @@ +# WorldCut.Tool + +The `worldcut-dotnet` command-line verifier for the **WorldCut** +decision-coherence protocol `0.1` and engine ruleset `0.1.2`. + +It reads one verification input file and prints the complete verification +result as JSON, with stable exit codes suitable for deployment gates. + +Targets **.NET 8** and **.NET 10**. + +## Install + +```sh +dotnet tool install --global WorldCut.Tool +``` + +## Use + +```sh +worldcut-dotnet verification.json +worldcut-dotnet --require-satisfied verification.json +worldcut-dotnet --help +``` + +## Exit codes + +| Code | Meaning | +| ---: | --- | +| `0` | The input was verified; or `--help` was requested; or `--require-satisfied` received a satisfied contract | +| `1` | Argument, file, input, or runtime failure | +| `2` | `--require-satisfied` received a non-satisfied verdict | + +Failures are written to standard error as a stable JSON envelope: + +```json +{"error":{"code":"WORLDCUT_INVALID_INPUT","message":"..."}} +``` + +The error codes are `WORLDCUT_INVALID_ARGUMENT`, `WORLDCUT_FILE_READ_FAILED`, +`WORLDCUT_INVALID_INPUT`, and `WORLDCUT_RUNTIME_ERROR`. + +## Library + +The verifier itself is published as the dependency-free +[`WorldCut`](https://www.nuget.org/packages/WorldCut) package. + +## More + +* Protocol specifications: + +* Port documentation: + +* Third-party notices: `THIRD-PARTY-NOTICES.md` in this package. + +Licensed under the Apache License 2.0. diff --git a/ports/dotnet/src/WorldCut.Tool/Program.cs b/ports/dotnet/src/WorldCut.Tool/Program.cs new file mode 100644 index 0000000..afa9a73 --- /dev/null +++ b/ports/dotnet/src/WorldCut.Tool/Program.cs @@ -0,0 +1,171 @@ +using System.Text; +using WorldCut.Json; + +namespace WorldCut.Tool; + +/// +/// The worldcut-dotnet command-line entry point. +/// +/// +/// +/// The CLI reads exactly one verification input file and prints the complete +/// verification result as JSON on standard output. Failures are reported as a +/// stable JSON envelope on standard error. +/// +/// +/// Exit codes match the other WorldCut ports: +/// 0 success or --help; 1 argument, file, input, or +/// runtime failure; 2 a non-satisfied verdict under +/// --require-satisfied. +/// +/// +internal static class Program +{ + private const int ExitSuccess = 0; + private const int ExitFailure = 1; + private const int ExitNotSatisfied = 2; + + internal static int Main(string[] args) + { + using var output = CreateWriter(Console.OpenStandardOutput()); + using var error = CreateWriter(Console.OpenStandardError()); + + try + { + return Run(args, output, error); + } +#pragma warning disable CA1031 // The CLI contract requires a stable envelope for every failure. + catch (Exception failure) +#pragma warning restore CA1031 + { + return WriteError(error, WorldCutErrorCode.RuntimeError, failure.Message); + } + } + + internal static int Run(IReadOnlyList args, TextWriter output, TextWriter error) + { + bool requireSatisfied = false; + string? inputPath = null; + int positionalCount = 0; + + foreach (string argument in args) + { + if (string.Equals(argument, "--help", StringComparison.Ordinal)) + { + output.WriteLine(Usage); + return ExitSuccess; + } + + if (string.Equals(argument, "--require-satisfied", StringComparison.Ordinal)) + { + requireSatisfied = true; + continue; + } + + if (argument.StartsWith('-')) + { + return WriteError(error, WorldCutErrorCode.InvalidArgument, $"Unknown option: {argument}"); + } + + positionalCount++; + inputPath ??= argument; + } + + if (positionalCount != 1 || inputPath is null || inputPath.Length == 0) + { + return WriteError( + error, + WorldCutErrorCode.InvalidArgument, + "Exactly one verification JSON file is required"); + } + + byte[] source; + string resolvedPath; + try + { + resolvedPath = Path.GetFullPath(inputPath); + source = File.ReadAllBytes(resolvedPath); + } + catch (Exception failure) when (failure is IOException + or UnauthorizedAccessException + or NotSupportedException + or ArgumentException + or System.Security.SecurityException) + { + return WriteError( + error, + WorldCutErrorCode.FileReadFailed, + $"Unable to read {inputPath}"); + } + + VerificationResult result; + try + { + result = WorldCutVerifier.VerifyJsonUtf8(source); + } + catch (WorldCutException failure) + { + return WriteError(error, failure.Code, failure.Message); + } + + output.WriteLine(JsonText.Indent(result.ToJson())); + + return requireSatisfied && result.Verdict != ContractVerdict.ContractSatisfied + ? ExitNotSatisfied + : ExitSuccess; + } + + private static string Usage => string.Join( + '\n', + "Usage: worldcut-dotnet [--require-satisfied] ", + string.Empty, + "Options:", + " --require-satisfied Exit with code 2 unless the contract is satisfied", + " --help Show this help"); + + private static int WriteError(TextWriter error, WorldCutErrorCode code, string message) + { + error.WriteLine(JsonText.Compact(JsonValue.CreateObject( + [ + new("error", JsonValue.CreateObject( + [ + new("code", JsonValue.Create(code.ToWireCode())), + new("message", JsonValue.Create(Sanitize(message))), + ])), + ]))); + return ExitFailure; + } + + /// + /// Replaces unpaired surrogates so that a hostile path or message can never + /// stop the CLI from emitting its stable error envelope. + /// + private static string Sanitize(string message) + { + if (Utf16.IndexOfUnpairedSurrogate(message) < 0) + { + return message; + } + + var builder = new StringBuilder(message.Length); + for (int index = 0; index < message.Length; index++) + { + char current = message[index]; + if (char.IsHighSurrogate(current) + && index + 1 < message.Length + && char.IsLowSurrogate(message[index + 1])) + { + builder.Append(current).Append(message[index + 1]); + index++; + continue; + } + + builder.Append(char.IsSurrogate(current) ? '\uFFFD' : current); + } + + return builder.ToString(); + } + + private static StreamWriter CreateWriter(Stream stream) => + new(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)) { AutoFlush = true }; +} diff --git a/ports/dotnet/src/WorldCut.Tool/WorldCut.Tool.csproj b/ports/dotnet/src/WorldCut.Tool/WorldCut.Tool.csproj new file mode 100644 index 0000000..06a166c --- /dev/null +++ b/ports/dotnet/src/WorldCut.Tool/WorldCut.Tool.csproj @@ -0,0 +1,39 @@ + + + + Exe + net8.0;net10.0 + WorldCut.Tool + WorldCut.Tool + true + true + worldcut-dotnet + + false + true + en + + + + WorldCut.Tool + WorldCut CLI + The worldcut-dotnet .NET global tool. Verifies a WorldCut protocol 0.1 decision-coherence input file and prints the complete verification result as JSON, with stable exit codes for deployment gates. + verification;cli;dotnet-tool;consistency;distributed-systems;provenance + First release of the independent .NET WorldCut CLI for protocol 0.1 and engine ruleset 0.1.2. + + + + + + + + + + + + + + + + diff --git a/ports/dotnet/src/WorldCut.Tool/packages.lock.json b/ports/dotnet/src/WorldCut.Tool/packages.lock.json new file mode 100644 index 0000000..9adddd4 --- /dev/null +++ b/ports/dotnet/src/WorldCut.Tool/packages.lock.json @@ -0,0 +1,15 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "worldcut": { + "type": "Project" + } + }, + "net8.0": { + "worldcut": { + "type": "Project" + } + } + } +} \ No newline at end of file diff --git a/ports/dotnet/src/WorldCut/Engine/AcquisitionPlanner.cs b/ports/dotnet/src/WorldCut/Engine/AcquisitionPlanner.cs new file mode 100644 index 0000000..85c81d4 --- /dev/null +++ b/ports/dotnet/src/WorldCut/Engine/AcquisitionPlanner.cs @@ -0,0 +1,275 @@ +using WorldCut.Json; + +namespace WorldCut.Engine; + +/// +/// Selects the minimum declared-cost union of acquisition actions inside the +/// bounded search defined by spec/0.1/PROTOCOL.md. +/// +/// +/// Tie-breaking is, in order: lower total cost, fewer distinct actions, then +/// the lexicographically smaller sorted option-identifier sequence compared by +/// UTF-16 code units. When exact optimality cannot be established inside the +/// documented limits the plan is INCOMPLETE and empty. +/// +internal static class AcquisitionPlanner +{ + internal static AcquisitionPlan SelectPlan(IReadOnlyList requirementResults) + { + var unresolved = new List(); + foreach (RequirementResult result in requirementResults) + { + if (result.Required && result.Status != RequirementStatus.Satisfied) + { + unresolved.Add(result); + } + } + + unresolved.Sort(static (left, right) => Utf16.Compare(left.RequirementId, right.RequirementId)); + + if (unresolved.Count == 0) + { + return new AcquisitionPlan( + AcquisitionPlanStatus.NotNeeded, + null, + Array.Empty(), + Array.Empty(), + 0, + Array.Empty(), + Array.Empty()); + } + + if (unresolved.Count > WorldCutProtocol.MaxUnresolvedRequirements) + { + return IncompletePlan( + $"Acquisition planning supports at most {WorldCutProtocol.MaxUnresolvedRequirements} unresolved requirements.", + unresolved); + } + + var coverable = new List(); + var impossible = new List(); + int combinations = 1; + + foreach (RequirementResult result in unresolved) + { + int optionCount = result.AcquisitionOptions.Count; + if (optionCount == 0) + { + impossible.Add(result.RequirementId); + continue; + } + + if (combinations > WorldCutProtocol.MaxOptionCombinations / optionCount) + { + return IncompletePlan( + $"Acquisition search exceeds the {WorldCutProtocol.MaxOptionCombinations} combination limit.", + unresolved); + } + + combinations *= optionCount; + coverable.Add(result); + } + + var sortedOptions = new List(coverable.Count); + foreach (RequirementResult result in coverable) + { + var options = result.AcquisitionOptions.ToArray(); + Array.Sort(options, static (left, right) => Utf16.Compare(left.Id, right.Id)); + sortedOptions.Add(options); + } + + Candidate? best = null; + int visitedStates = 0; + var stack = new Stack(); + stack.Push(SearchState.Initial); + + while (stack.Count > 0) + { + visitedStates++; + if (visitedStates > WorldCutProtocol.MaxSearchStates) + { + return IncompletePlan( + $"Acquisition search exceeds the {WorldCutProtocol.MaxSearchStates} state limit.", + unresolved); + } + + SearchState state = stack.Pop(); + if (best is not null && state.Cost > best.Cost) + { + continue; + } + + if (state.Index >= coverable.Count) + { + Candidate candidate = state.ToCandidate(); + if (best is null || Compare(candidate, best) < 0) + { + best = candidate; + } + + continue; + } + + AcquisitionOption[] options = sortedOptions[state.Index]; + for (int index = options.Length - 1; index >= 0; index--) + { + SearchState next = state.WithOption(options[index]); + if (best is null || next.Cost <= best.Cost) + { + stack.Push(next); + } + } + } + + if (best is null) + { + return IncompletePlan("Acquisition search completed without a valid option set.", unresolved); + } + + var covered = new string[coverable.Count]; + for (int index = 0; index < coverable.Count; index++) + { + covered[index] = coverable[index].RequirementId; + } + + impossible.Sort(Utf16.Compare); + + return new AcquisitionPlan( + impossible.Count == 0 ? AcquisitionPlanStatus.Available : AcquisitionPlanStatus.Incomplete, + impossible.Count == 0 + ? null + : $"No acquisition option is available for: {string.Join(", ", impossible)}.", + best.Actions, + best.OptionIds, + best.Cost, + covered, + impossible.ToArray()); + } + + private static AcquisitionPlan IncompletePlan(string reason, List unresolved) + { + var identifiers = new string[unresolved.Count]; + for (int index = 0; index < unresolved.Count; index++) + { + identifiers[index] = unresolved[index].RequirementId; + } + + return new AcquisitionPlan( + AcquisitionPlanStatus.Incomplete, + reason, + Array.Empty(), + Array.Empty(), + 0, + Array.Empty(), + identifiers); + } + + private static int Compare(Candidate left, Candidate right) + { + if (left.Cost != right.Cost) + { + return left.Cost < right.Cost ? -1 : 1; + } + + if (left.Actions.Length != right.Actions.Length) + { + return left.Actions.Length < right.Actions.Length ? -1 : 1; + } + + return Utf16.Compare( + string.Join('\0', left.OptionIds), + string.Join('\0', right.OptionIds)); + } + + private sealed class Candidate + { + internal Candidate(string[] optionIds, AcquisitionAction[] actions, long cost) + { + OptionIds = optionIds; + Actions = actions; + Cost = cost; + } + + internal string[] OptionIds { get; } + + internal AcquisitionAction[] Actions { get; } + + internal long Cost { get; } + } + + private sealed class SearchState + { + private static readonly Dictionary NoActions = + new(StringComparer.Ordinal); + + private readonly Dictionary _actions; + private readonly List _optionIds; + + private SearchState(int index, List optionIds, Dictionary actions, long cost) + { + Index = index; + _optionIds = optionIds; + _actions = actions; + Cost = cost; + } + + internal static SearchState Initial { get; } = new(0, [], NoActions, 0); + + internal int Index { get; } + + internal long Cost { get; } + + internal SearchState WithOption(AcquisitionOption option) + { + var actions = new Dictionary(_actions, StringComparer.Ordinal); + long cost = Cost; + + foreach (AcquisitionAction action in option.Actions) + { + if (action.Cost < 0 || action.Cost > WorldCutProtocol.MaxAcquisitionCost) + { + throw WorldCutException.InvalidInput( + $"Acquisition action {action.Id} cost must be between 0 and {WorldCutProtocol.MaxAcquisitionCost}"); + } + + if (actions.TryGetValue(action.Id, out AcquisitionAction? existing)) + { + if (existing.Cost != action.Cost) + { + throw new WorldCutException( + WorldCutErrorCode.RuntimeError, + $"Acquisition action {action.Id} has conflicting declared costs"); + } + + continue; + } + + if (cost > WorldCutProtocol.MaxPlanTotalCost - action.Cost) + { + throw WorldCutException.InvalidInput( + $"Acquisition plan cost exceeds {WorldCutProtocol.MaxPlanTotalCost}"); + } + + actions.Add(action.Id, action); + cost += action.Cost; + } + + var optionIds = new List(_optionIds.Count + 1); + optionIds.AddRange(_optionIds); + optionIds.Add(option.Id); + + return new SearchState(Index + 1, optionIds, actions, cost); + } + + internal Candidate ToCandidate() + { + var optionIds = _optionIds.ToArray(); + Array.Sort(optionIds, Utf16.Compare); + + var actions = _actions.Values.ToArray(); + Array.Sort(actions, static (left, right) => Utf16.Compare(left.Id, right.Id)); + + return new Candidate(optionIds, actions, Cost); + } + } +} diff --git a/ports/dotnet/src/WorldCut/Engine/JsonPath.cs b/ports/dotnet/src/WorldCut/Engine/JsonPath.cs new file mode 100644 index 0000000..a8e017b --- /dev/null +++ b/ports/dotnet/src/WorldCut/Engine/JsonPath.cs @@ -0,0 +1,81 @@ +using System.Diagnostics.CodeAnalysis; +using WorldCut.Json; + +namespace WorldCut.Engine; + +/// +/// Resolves a deterministic value_equals path through observed JSON. +/// +/// +/// An array index is 0 or a non-zero decimal digit followed only by +/// decimal digits. Leading zeroes, signs, fractions, and properties such as +/// length never address an array element. +/// +internal static class JsonPath +{ + internal static bool TryResolve( + JsonValue value, + IReadOnlyList path, + [NotNullWhen(true)] out JsonValue? resolved) + { + JsonValue current = value; + foreach (string segment in path) + { + switch (current.Kind) + { + case JsonKind.Array: + if (!TryParseArrayIndex(segment, out long index) || index >= current.Items.Count) + { + resolved = null; + return false; + } + + current = current.Items[(int)index]; + continue; + + case JsonKind.Object: + if (!current.TryGetProperty(segment, out JsonValue? member)) + { + resolved = null; + return false; + } + + current = member; + continue; + + default: + resolved = null; + return false; + } + } + + resolved = current; + return true; + } + + private static bool TryParseArrayIndex(string segment, out long index) + { + index = 0; + if (segment.Length == 0 || segment.Length > 18) + { + return false; + } + + if (segment[0] == '0') + { + return segment.Length == 1; + } + + foreach (char digit in segment) + { + if (digit is < '0' or > '9') + { + return false; + } + + index = (index * 10) + (digit - '0'); + } + + return true; + } +} diff --git a/ports/dotnet/src/WorldCut/Engine/RequirementEvaluator.cs b/ports/dotnet/src/WorldCut/Engine/RequirementEvaluator.cs new file mode 100644 index 0000000..6433af8 --- /dev/null +++ b/ports/dotnet/src/WorldCut/Engine/RequirementEvaluator.cs @@ -0,0 +1,613 @@ +using WorldCut.Json; +using WorldCut.Model; + +namespace WorldCut.Engine; + +/// +/// Builds the exact requirement results defined by spec/0.1/RESULTS.md. +/// +/// +/// Summaries, detail members, acquisition action identifiers, and option +/// ordering are all part of the protocol 0.1 record contract. They are locked +/// down by the committed verification vectors and cannot be reworded. +/// +internal static class RequirementEvaluator +{ + internal static RequirementResult Evaluate( + ContractRequirement requirement, + IReadOnlyDictionary observationsByRole) => requirement switch + { + DependencyRequirement dependency => EvaluateDependency(dependency, observationsByRole), + CommonValidTimeRequirement temporal => EvaluateCommonValidTime(temporal, observationsByRole), + ValueEqualsRequirement value => EvaluateValueEquals(value, observationsByRole), + _ => throw new WorldCutException( + WorldCutErrorCode.RuntimeError, + $"Unsupported requirement type: {requirement.TypeName}"), + }; + + private static RequirementResult EvaluateDependency( + DependencyRequirement requirement, + IReadOnlyDictionary observationsByRole) + { + var missingRoles = new List(2); + foreach (string role in new[] { requirement.DependentRole, requirement.TargetRole }) + { + if (!observationsByRole.ContainsKey(role)) + { + missingRoles.Add(role); + } + } + + if (missingRoles.Count > 0) + { + return MissingRolesResult(requirement, missingRoles); + } + + Observation dependent = observationsByRole[requirement.DependentRole]; + Observation target = observationsByRole[requirement.TargetRole]; + DependencyWitness? dependency = dependent.Witness.FindDependency(requirement.DependencyName); + + if (dependency is null) + { + var actions = new List(2) + { + Action( + AcquisitionActionType.FetchRequiredMetadata, + dependent, + dependent.Role, + $"Fetch dependency metadata for {dependent.Role}.", + JsonValue.CreateObject( + [ + new("dependencyName", JsonValue.Create(requirement.DependencyName)), + new("targetResource", ResourceJson(target.Resource)), + ])), + }; + + if (target.Witness.Version is null) + { + actions.Add(Action( + AcquisitionActionType.FetchRequiredMetadata, + target, + target.Role, + $"Fetch the resource version for {target.Role}.", + null)); + } + + return new RequirementResult( + requirement.Id, + requirement.TypeName, + requirement.Required, + RequirementStatus.Unknown, + $"{dependent.Role} does not expose dependency {requirement.DependencyName}.", + JsonValue.CreateObject( + [ + new("dependentRole", JsonValue.Create(dependent.Role)), + new("targetRole", JsonValue.Create(target.Role)), + new("missingDependency", JsonValue.Create(requirement.DependencyName)), + ]), + [ + Option( + requirement.Id, + "fetch-dependency-metadata", + "Fetch all metadata required to compare the dependency.", + actions), + ]); + } + + if (dependency.Resource != target.Resource) + { + return new RequirementResult( + requirement.Id, + requirement.TypeName, + requirement.Required, + RequirementStatus.Violated, + $"{dependent.Role} is bound to a different resource than {target.Role}.", + JsonValue.CreateObject( + [ + new("dependentResource", ResourceJson(dependency.Resource)), + new("targetResource", ResourceJson(target.Resource)), + ]), + [ + Option( + requirement.Id, + "acquire-compatible-resource", + "Acquire dependent evidence bound to the selected target resource.", + [ + Action( + AcquisitionActionType.AcquireCompatibleEvidence, + dependent, + dependent.Role, + $"Acquire {dependent.Role} evidence for the selected {target.Role} resource.", + JsonValue.CreateObject( + [ + new("targetResource", ResourceJson(target.Resource)), + ])), + ]), + ]); + } + + if (dependency.Version is null || target.Witness.Version is null) + { + var actions = new List(2); + if (dependency.Version is null) + { + actions.Add(Action( + AcquisitionActionType.FetchRequiredMetadata, + dependent, + dependent.Role, + $"Fetch the dependency version for {dependent.Role}.", + JsonValue.CreateObject( + [ + new("dependencyName", JsonValue.Create(requirement.DependencyName)), + ]))); + } + + if (target.Witness.Version is null) + { + actions.Add(Action( + AcquisitionActionType.FetchRequiredMetadata, + target, + target.Role, + $"Fetch the resource version for {target.Role}.", + null)); + } + + return new RequirementResult( + requirement.Id, + requirement.TypeName, + requirement.Required, + RequirementStatus.Unknown, + $"Version evidence is incomplete for {requirement.Description}.", + JsonValue.CreateObject( + [ + new("dependencyVersion", OptionalString(dependency.Version)), + new("targetVersion", OptionalString(target.Witness.Version)), + ]), + [ + Option( + requirement.Id, + "fetch-all-version-metadata", + "Fetch every missing version needed for this comparison.", + actions), + ]); + } + + if (!string.Equals(dependency.Version, target.Witness.Version, StringComparison.Ordinal)) + { + return new RequirementResult( + requirement.Id, + requirement.TypeName, + requirement.Required, + RequirementStatus.Violated, + $"{requirement.Description}: {dependency.Version} does not equal {target.Witness.Version}.", + JsonValue.CreateObject( + [ + new("dependentRole", JsonValue.Create(dependent.Role)), + new("dependencyVersion", JsonValue.Create(dependency.Version)), + new("targetRole", JsonValue.Create(target.Role)), + new("targetVersion", JsonValue.Create(target.Witness.Version)), + new("relation", JsonValue.Create(DependencyWitness.Relation)), + ]), + [ + Option( + requirement.Id, + "acquire-compatible-dependent", + "Acquire dependent evidence bound to the selected target version.", + [ + Action( + AcquisitionActionType.AcquireCompatibleEvidence, + dependent, + dependent.Role, + $"Acquire {dependent.Role} evidence bound to {target.Witness.Version}.", + JsonValue.CreateObject( + [ + new("targetRole", JsonValue.Create(target.Role)), + new("targetVersion", JsonValue.Create(target.Witness.Version)), + ])), + ]), + Option( + requirement.Id, + "refresh-target", + "Refresh the target before selecting compatible evidence.", + [ + Action( + AcquisitionActionType.RefreshObservation, + target, + target.Role, + $"Refresh {target.Role} before selecting compatible evidence.", + JsonValue.CreateObject( + [ + new("dependentRole", JsonValue.Create(dependent.Role)), + new("dependentVersion", JsonValue.Create(dependency.Version)), + ])), + ]), + ]); + } + + return new RequirementResult( + requirement.Id, + requirement.TypeName, + requirement.Required, + RequirementStatus.Satisfied, + $"{requirement.Description}: both roles are bound to {dependency.Version}.", + JsonValue.CreateObject( + [ + new("dependentRole", JsonValue.Create(dependent.Role)), + new("targetRole", JsonValue.Create(target.Role)), + new("version", JsonValue.Create(dependency.Version)), + ]), + Array.Empty()); + } + + private static RequirementResult EvaluateCommonValidTime( + CommonValidTimeRequirement requirement, + IReadOnlyDictionary observationsByRole) + { + var missingRoles = new List(); + var present = new List(); + var missingValidity = new List(); + + foreach (string role in requirement.Roles) + { + if (!observationsByRole.TryGetValue(role, out Observation? observation)) + { + missingRoles.Add(role); + continue; + } + + present.Add(observation); + if (observation.Witness.Validity is null) + { + missingValidity.Add(observation); + } + } + + JsonValue withinJson = IntervalJson(requirement.Within); + var prerequisites = new List(missingRoles.Count + missingValidity.Count); + foreach (string role in missingRoles) + { + prerequisites.Add(Action( + AcquisitionActionType.RefreshObservation, + null, + role, + $"Acquire an observation for role {role}.", + null)); + } + + foreach (Observation observation in missingValidity) + { + prerequisites.Add(Action( + AcquisitionActionType.FetchRequiredMetadata, + observation, + observation.Role, + $"Fetch validity metadata for {observation.Role}.", + JsonValue.CreateObject([new("within", withinJson)]))); + } + + NormalizedTimestamp latestStart = requirement.Within.From; + NormalizedTimestamp? earliestEnd = requirement.Within.Until; + foreach (Observation observation in present) + { + ValidityInterval? validity = observation.Witness.Validity; + if (validity is null) + { + continue; + } + + latestStart = NormalizedTimestamp.Max(latestStart, validity.From); + if (validity.Until is NormalizedTimestamp end) + { + earliestEnd = earliestEnd is NormalizedTimestamp current + ? NormalizedTimestamp.Min(current, end) + : end; + } + } + + JsonValue rolesJson = StringArray(requirement.Roles); + JsonValue missingRolesJson = StringArray(missingRoles); + JsonValue missingValidityRolesJson = StringArray(missingValidity.Select(item => item.Role).ToArray()); + + if (earliestEnd is NormalizedTimestamp bound && latestStart >= bound) + { + var options = new List(present.Count); + foreach (Observation observation in present) + { + var actions = new List(1 + prerequisites.Count) + { + Action( + AcquisitionActionType.RefreshObservation, + observation, + observation.Role, + $"Refresh {observation.Role} to seek a compatible validity window.", + JsonValue.CreateObject([new("within", withinJson)])), + }; + actions.AddRange(prerequisites); + + options.Add(Option( + requirement.Id, + $"refresh-{observation.Role}", + $"Refresh {observation.Role} and acquire every other missing prerequisite.", + actions)); + } + + return new RequirementResult( + requirement.Id, + requirement.TypeName, + requirement.Required, + RequirementStatus.Violated, + $"{requirement.Description}: the known validity intervals do not overlap.", + JsonValue.CreateObject( + [ + new("roles", rolesJson), + new("latestStart", JsonValue.Create(latestStart.Text)), + new("earliestEnd", JsonValue.Create(bound.Text)), + new("missingRoles", missingRolesJson), + new("missingValidityRoles", missingValidityRolesJson), + ]), + options.ToArray()); + } + + JsonValue untilJson = earliestEnd is NormalizedTimestamp finish + ? JsonValue.Create(finish.Text) + : JsonValue.Null; + + if (missingRoles.Count > 0 || missingValidity.Count > 0) + { + return new RequirementResult( + requirement.Id, + requirement.TypeName, + requirement.Required, + RequirementStatus.Unknown, + $"{requirement.Description}: validity evidence is incomplete.", + JsonValue.CreateObject( + [ + new("roles", rolesJson), + new("missingRoles", missingRolesJson), + new("missingValidityRoles", missingValidityRolesJson), + new("possibleKnownWindow", JsonValue.CreateObject( + [ + new("from", JsonValue.Create(latestStart.Text)), + new("until", untilJson), + ])), + ]), + [ + Option( + requirement.Id, + "acquire-all-validity-prerequisites", + "Acquire every missing observation and validity witness.", + prerequisites), + ]); + } + + return new RequirementResult( + requirement.Id, + requirement.TypeName, + requirement.Required, + RequirementStatus.Satisfied, + $"{requirement.Description}: a common valid time exists.", + JsonValue.CreateObject( + [ + new("roles", rolesJson), + new("commonWindow", JsonValue.CreateObject( + [ + new("from", JsonValue.Create(latestStart.Text)), + new("until", untilJson), + ])), + ]), + Array.Empty()); + } + + private static RequirementResult EvaluateValueEquals( + ValueEqualsRequirement requirement, + IReadOnlyDictionary observationsByRole) + { + if (!observationsByRole.TryGetValue(requirement.Role, out Observation? observation)) + { + return MissingRolesResult(requirement, [requirement.Role]); + } + + string displayPath = string.Join(".", requirement.Path); + JsonValue pathJson = StringArray(requirement.Path); + + if (!JsonPath.TryResolve(observation.Value, requirement.Path, out JsonValue? actual)) + { + return new RequirementResult( + requirement.Id, + requirement.TypeName, + requirement.Required, + RequirementStatus.Unknown, + $"{requirement.Description}: value path {displayPath} is missing.", + JsonValue.CreateObject( + [ + new("role", JsonValue.Create(requirement.Role)), + new("path", pathJson), + new("expected", requirement.Expected), + ]), + [ + Option( + requirement.Id, + "acquire-value", + "Acquire evidence containing the required value path.", + [ + Action( + AcquisitionActionType.AcquireCompatibleEvidence, + observation, + observation.Role, + $"Acquire {observation.Role} evidence containing {displayPath}.", + JsonValue.CreateObject( + [ + new("path", pathJson), + new("expected", requirement.Expected), + ])), + ]), + ]); + } + + if (!string.Equals( + CanonicalJson.Serialize(actual), + CanonicalJson.Serialize(requirement.Expected), + StringComparison.Ordinal)) + { + return new RequirementResult( + requirement.Id, + requirement.TypeName, + requirement.Required, + RequirementStatus.Violated, + $"{requirement.Description}: observed value does not equal the required value.", + JsonValue.CreateObject( + [ + new("role", JsonValue.Create(requirement.Role)), + new("path", pathJson), + new("expected", requirement.Expected), + new("actual", actual), + ]), + [ + Option( + requirement.Id, + "refresh-value", + "Refresh the observation before evaluating the value again.", + [ + Action( + AcquisitionActionType.RefreshObservation, + observation, + observation.Role, + $"Refresh {observation.Role} before evaluating {displayPath}.", + JsonValue.CreateObject( + [ + new("path", pathJson), + new("expected", requirement.Expected), + ])), + ]), + ]); + } + + return new RequirementResult( + requirement.Id, + requirement.TypeName, + requirement.Required, + RequirementStatus.Satisfied, + $"{requirement.Description}: observed value matches the requirement.", + JsonValue.CreateObject( + [ + new("role", JsonValue.Create(requirement.Role)), + new("path", pathJson), + new("expected", requirement.Expected), + ]), + Array.Empty()); + } + + private static RequirementResult MissingRolesResult( + ContractRequirement requirement, + List roles) + { + var actions = new List(roles.Count); + foreach (string role in roles) + { + actions.Add(Action( + AcquisitionActionType.RefreshObservation, + null, + role, + $"Acquire an observation for role {role}.", + null)); + } + + return new RequirementResult( + requirement.Id, + requirement.TypeName, + requirement.Required, + RequirementStatus.Unknown, + $"No observations are bound to required role(s): {string.Join(", ", roles)}.", + JsonValue.CreateObject([new("missingRoles", StringArray(roles))]), + [ + Option( + requirement.Id, + "acquire-missing-roles", + "Acquire every missing role required to evaluate this requirement.", + actions), + ]); + } + + private static AcquisitionAction Action( + AcquisitionActionType type, + Observation? observation, + string role, + string description, + JsonValue? expected) + { + long cost; + if (observation is null) + { + cost = 1; + } + else if (type == AcquisitionActionType.FetchRequiredMetadata) + { + cost = Math.Max(1, (observation.AcquisitionCost + 3) / 4); + } + else + { + cost = observation.AcquisitionCost; + } + + string expectedDigest = expected is null + ? "none" + : CanonicalJson.ComputeSha256Hex(expected)[..12]; + + return new AcquisitionAction( + $"{LowercaseTypeName(type)}:{role}:{expectedDigest}", + type, + role, + cost, + description, + expected); + } + + private static AcquisitionOption Option( + string requirementId, + string suffix, + string description, + IReadOnlyList actions) => + new($"{requirementId}:{suffix}", description, actions.ToArray()); + + private static string LowercaseTypeName(AcquisitionActionType type) => type switch + { + AcquisitionActionType.RefreshObservation => "refresh_observation", + AcquisitionActionType.FetchRequiredMetadata => "fetch_required_metadata", + AcquisitionActionType.AcquireCompatibleEvidence => "acquire_compatible_evidence", + _ => throw new ArgumentOutOfRangeException(nameof(type)), + }; + + private static JsonValue ResourceJson(ResourceIdentity resource) => JsonValue.CreateObject( + [ + new("provider", JsonValue.Create(resource.Provider)), + new("account", JsonValue.Create(resource.Account)), + new("kind", JsonValue.Create(resource.Kind)), + new("key", JsonValue.Create(resource.Key)), + ]); + + private static JsonValue IntervalJson(ValidityInterval interval) => JsonValue.CreateObject( + [ + new("from", JsonValue.Create(interval.From.Text)), + new("until", interval.Until is NormalizedTimestamp until + ? JsonValue.Create(until.Text) + : JsonValue.Null), + ]); + + private static JsonValue OptionalString(string? value) => + value is null ? JsonValue.Null : JsonValue.Create(value); + + private static JsonValue StringArray(IReadOnlyList values) + { + if (values.Count == 0) + { + return JsonValue.EmptyArray; + } + + var items = new JsonValue[values.Count]; + for (int index = 0; index < values.Count; index++) + { + items[index] = JsonValue.Create(values[index]); + } + + return JsonValue.CreateArrayOwned(items); + } +} diff --git a/ports/dotnet/src/WorldCut/Json/CanonicalJson.cs b/ports/dotnet/src/WorldCut/Json/CanonicalJson.cs new file mode 100644 index 0000000..d301fa8 --- /dev/null +++ b/ports/dotnet/src/WorldCut/Json/CanonicalJson.cs @@ -0,0 +1,125 @@ +using System.Buffers; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using Jcs.Net; + +namespace WorldCut.Json; + +/// +/// The worldcut-json-v1 canonicalization scheme and its SHA-256 digest. +/// +/// +/// +/// worldcut-json-v1 is RFC 8785 (JSON Canonicalization Scheme) applied to +/// the accepted WorldCut JSON data domain: object member names are ordered by +/// raw UTF-16 code units, arrays keep their order, numbers use ECMAScript's +/// shortest round-trippable form, negative zero serializes as 0, and +/// non-finite numbers and unpaired UTF-16 surrogates are rejected. +/// +/// +/// The RFC 8785 serializer is vendored from Jcs.NET 0.1.1 (MIT). See +/// ports/dotnet/THIRD-PARTY-NOTICES.md for the attribution, the exact +/// modifications, and the WorldCut behaviour layered on top of it. +/// +/// +public static class CanonicalJson +{ + private static readonly JsonDocumentOptions DocumentOptions = new() + { + AllowTrailingCommas = false, + CommentHandling = JsonCommentHandling.Disallow, + MaxDepth = WorldCutProtocol.MaxCanonicalizationDepth, + }; + + /// Returns the canonical JSON text for . + /// The value to canonicalize. + /// The canonical JSON text. + /// is . + /// The value cannot be canonicalized. + public static string Serialize(JsonValue value) + { + ArgumentNullException.ThrowIfNull(value); + + var buffer = new ArrayBufferWriter(); + using (var writer = new Utf8JsonWriter(buffer, new JsonWriterOptions { Indented = false })) + { + Write(writer, value); + } + + try + { + using JsonDocument document = JsonDocument.Parse(buffer.WrittenMemory, DocumentOptions); + return JsonCanonicalizer.Canonicalize(document.RootElement); + } + catch (JsonException error) + { + throw WorldCutException.InvalidInput( + $"value cannot be canonicalized: {error.Message}", + error); + } + } + + /// Returns the UTF-8 encoded canonical JSON for . + /// The value to canonicalize. + /// The canonical JSON bytes that are hashed to form WorldCut digests. + /// is . + /// The value cannot be canonicalized. + public static byte[] SerializeToUtf8(JsonValue value) => + Encoding.UTF8.GetBytes(Serialize(value)); + + /// + /// Returns the lowercase hexadecimal SHA-256 digest of the canonical form of + /// . + /// + /// The value to digest. + /// A 64-character lowercase hexadecimal digest. + /// is . + /// The value cannot be canonicalized. + public static string ComputeSha256Hex(JsonValue value) + { +#pragma warning disable CA1308 // WorldCut digests are specified as lowercase hexadecimal. + return Convert.ToHexString(SHA256.HashData(SerializeToUtf8(value))).ToLowerInvariant(); +#pragma warning restore CA1308 + } + + private static void Write(Utf8JsonWriter writer, JsonValue value) + { + switch (value.Kind) + { + case JsonKind.Null: + writer.WriteNullValue(); + break; + case JsonKind.Boolean: + writer.WriteBooleanValue(value.GetBoolean()); + break; + case JsonKind.Number: + writer.WriteNumberValue(value.GetNumber()); + break; + case JsonKind.String: + writer.WriteStringValue(value.GetString()); + break; + case JsonKind.Array: + writer.WriteStartArray(); + foreach (JsonValue item in value.Items) + { + Write(writer, item); + } + + writer.WriteEndArray(); + break; + case JsonKind.Object: + writer.WriteStartObject(); + foreach (KeyValuePair member in value.Members) + { + writer.WritePropertyName(member.Key); + Write(writer, member.Value); + } + + writer.WriteEndObject(); + break; + default: + throw WorldCutException.InvalidInput($"unsupported JSON kind {value.Kind}"); + } + } +} diff --git a/ports/dotnet/src/WorldCut/Json/JsonKind.cs b/ports/dotnet/src/WorldCut/Json/JsonKind.cs new file mode 100644 index 0000000..410ddb1 --- /dev/null +++ b/ports/dotnet/src/WorldCut/Json/JsonKind.cs @@ -0,0 +1,23 @@ +namespace WorldCut.Json; + +/// The six JSON value shapes WorldCut accepts. +public enum JsonKind +{ + /// The JSON null literal. + Null = 0, + + /// A JSON true or false literal. + Boolean = 1, + + /// A finite JSON number, held as an IEEE 754 binary64 value. + Number = 2, + + /// A JSON string. + String = 3, + + /// A JSON array. + Array = 4, + + /// A JSON object. + Object = 5, +} diff --git a/ports/dotnet/src/WorldCut/Json/JsonText.cs b/ports/dotnet/src/WorldCut/Json/JsonText.cs new file mode 100644 index 0000000..251d03f --- /dev/null +++ b/ports/dotnet/src/WorldCut/Json/JsonText.cs @@ -0,0 +1,144 @@ +using System.Text; +using Jcs.Net; + +namespace WorldCut.Json; + +/// +/// Renders a as ordinary JSON text. +/// +/// +/// +/// This is presentation only. Digests always use , +/// never this output. Member order follows the order in which the value was +/// built, rather than the canonical UTF-16 ordinal order. +/// +/// +/// String escaping and number formatting reuse the same RFC 8785 primitives as +/// canonicalization, so non-ASCII characters — including supplementary-plane +/// characters such as emoji — are written literally rather than as +/// \uXXXX escapes. +/// +/// +public static class JsonText +{ + private const string Indentation = " "; + + /// Renders with two-space indentation. + /// The value to render. + /// Indented JSON text. + /// is . + public static string Indent(JsonValue value) => Render(value, indented: true); + + /// Renders without insignificant whitespace. + /// The value to render. + /// Compact JSON text. + /// is . + public static string Compact(JsonValue value) => Render(value, indented: false); + + private static string Render(JsonValue value, bool indented) + { + ArgumentNullException.ThrowIfNull(value); + + var builder = new StringBuilder(256); + Write(builder, value, indented, depth: 0); + return builder.ToString(); + } + + private static void Write(StringBuilder builder, JsonValue value, bool indented, int depth) + { + switch (value.Kind) + { + case JsonKind.Null: + builder.Append("null"); + break; + case JsonKind.Boolean: + builder.Append(value.GetBoolean() ? "true" : "false"); + break; + case JsonKind.Number: + builder.Append(EcmaScriptNumberFormatter.Format(value.GetNumber())); + break; + case JsonKind.String: + JsonStringSerializer.Serialize(builder, value.GetString()); + break; + case JsonKind.Array: + WriteArray(builder, value, indented, depth); + break; + case JsonKind.Object: + WriteObject(builder, value, indented, depth); + break; + default: + throw WorldCutException.InvalidInput($"unsupported JSON kind {value.Kind}"); + } + } + + private static void WriteArray(StringBuilder builder, JsonValue value, bool indented, int depth) + { + IReadOnlyList items = value.Items; + if (items.Count == 0) + { + builder.Append("[]"); + return; + } + + builder.Append('['); + for (int index = 0; index < items.Count; index++) + { + if (index > 0) + { + builder.Append(','); + } + + AppendLineBreak(builder, indented, depth + 1); + Write(builder, items[index], indented, depth + 1); + } + + AppendLineBreak(builder, indented, depth); + builder.Append(']'); + } + + private static void WriteObject(StringBuilder builder, JsonValue value, bool indented, int depth) + { + IReadOnlyList> members = value.Members; + if (members.Count == 0) + { + builder.Append("{}"); + return; + } + + builder.Append('{'); + for (int index = 0; index < members.Count; index++) + { + if (index > 0) + { + builder.Append(','); + } + + AppendLineBreak(builder, indented, depth + 1); + JsonStringSerializer.Serialize(builder, members[index].Key); + builder.Append(':'); + if (indented) + { + builder.Append(' '); + } + + Write(builder, members[index].Value, indented, depth + 1); + } + + AppendLineBreak(builder, indented, depth); + builder.Append('}'); + } + + private static void AppendLineBreak(StringBuilder builder, bool indented, int depth) + { + if (!indented) + { + return; + } + + builder.Append('\n'); + for (int level = 0; level < depth; level++) + { + builder.Append(Indentation); + } + } +} diff --git a/ports/dotnet/src/WorldCut/Json/JsonValue.cs b/ports/dotnet/src/WorldCut/Json/JsonValue.cs new file mode 100644 index 0000000..7446d22 --- /dev/null +++ b/ports/dotnet/src/WorldCut/Json/JsonValue.cs @@ -0,0 +1,392 @@ +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; + +namespace WorldCut.Json; + +/// +/// An immutable, validated JSON value in the WorldCut data domain. +/// +/// +/// +/// A can only be created through the factory members on +/// this type, so it can never hold data that WorldCut would refuse to +/// canonicalize: numbers are finite, strings contain no unpaired UTF-16 +/// surrogate, object member names are unique, and nesting never exceeds +/// . +/// +/// +/// Numbers use IEEE 754 binary64, matching JSON.parse in the reference +/// implementation. Negative zero is normalised to positive zero on creation. +/// +/// +public sealed class JsonValue +{ + /// + /// The largest integer magnitude an IEEE 754 binary64 value represents exactly. + /// + public const long MaxSafeInteger = 9_007_199_254_740_991L; + + private static readonly ReadOnlyCollection EmptyItems = + new(Array.Empty()); + + private static readonly ReadOnlyCollection> EmptyMembers = + new(Array.Empty>()); + + private static readonly JsonValue NullValue = new(); + private static readonly JsonValue TrueValue = new(true); + private static readonly JsonValue FalseValue = new(false); + + private readonly bool _boolean; + private readonly double _number; + private readonly string? _text; + private readonly ReadOnlyCollection? _items; + private readonly ReadOnlyCollection>? _members; + private readonly Dictionary? _memberIndex; + + private JsonValue() + { + Kind = JsonKind.Null; + } + + private JsonValue(bool value) + { + Kind = JsonKind.Boolean; + _boolean = value; + } + + private JsonValue(double value) + { + Kind = JsonKind.Number; + _number = value; + } + + private JsonValue(string value) + { + Kind = JsonKind.String; + _text = value; + } + + private JsonValue(ReadOnlyCollection items, int depth) + { + Kind = JsonKind.Array; + _items = items; + Depth = depth; + } + + private JsonValue( + ReadOnlyCollection> members, + Dictionary memberIndex, + int depth) + { + Kind = JsonKind.Object; + _members = members; + _memberIndex = memberIndex; + Depth = depth; + } + + /// The shape of this value. + public JsonKind Kind { get; } + + /// + /// The container nesting depth of this value: 0 for scalars, and one + /// more than the deepest child for arrays and objects. + /// + public int Depth { get; } + + /// The shared JSON null value. + public static JsonValue Null => NullValue; + + /// The shared JSON true value. + public static JsonValue True => TrueValue; + + /// The shared JSON false value. + public static JsonValue False => FalseValue; + + /// The shared empty JSON array. + public static JsonValue EmptyArray { get; } = new(EmptyItems, 1); + + /// The shared empty JSON object. + public static JsonValue EmptyObject { get; } = + new(EmptyMembers, new Dictionary(StringComparer.Ordinal), 1); + + /// The array elements, in document order. + /// This value is not an array. + public IReadOnlyList Items => + _items ?? throw NotOfKind(JsonKind.Array); + + /// The object members, in the order they were supplied. + /// This value is not an object. + public IReadOnlyList> Members => + _members ?? throw NotOfKind(JsonKind.Object); + + /// Creates a JSON boolean. + /// The boolean value. + /// The shared or instance. + public static JsonValue Create(bool value) => value ? TrueValue : FalseValue; + + /// Creates a finite JSON number. + /// The numeric value. + /// The created value, with negative zero normalised to zero. + /// is not finite. + public static JsonValue Create(double value) + { + if (double.IsNaN(value) || double.IsInfinity(value)) + { + throw WorldCutException.InvalidInput( + FormattableString.Invariant($"value {value} is not a finite JSON number")); + } + + return new JsonValue(value == 0d ? 0d : value); + } + + /// Creates a JSON number from a safe integer. + /// The integer value. + /// The created value. + /// + /// is outside the IEEE 754 safe-integer domain. + /// + public static JsonValue Create(long value) + { + if (Math.Abs(value) > MaxSafeInteger) + { + throw WorldCutException.InvalidInput( + FormattableString.Invariant( + $"value {value} is outside the safe JSON integer domain")); + } + + return new JsonValue((double)value); + } + + /// Creates a JSON number from an integer. + /// The integer value. + /// The created value. + public static JsonValue Create(int value) => new(value); + + /// Creates a JSON string. + /// The string value. + /// The created value. + /// is . + /// contains an unpaired surrogate. + public static JsonValue Create(string value) + { + ArgumentNullException.ThrowIfNull(value); + Utf16.RejectUnpairedSurrogates(value, "string value"); + return new JsonValue(value); + } + + /// Creates a JSON array. + /// The elements, in order. + /// The created value. + /// or an element is . + /// The resulting nesting is too deep. + public static JsonValue CreateArray(IEnumerable items) + { + ArgumentNullException.ThrowIfNull(items); + return CreateArray(items as IReadOnlyList ?? items.ToArray()); + } + + /// Creates a JSON array. + /// The elements, in order. + /// The created value. + /// or an element is . + /// The resulting nesting is too deep. + public static JsonValue CreateArray(params JsonValue[] items) + { + ArgumentNullException.ThrowIfNull(items); + return CreateArray((IReadOnlyList)items); + } + + /// Creates a JSON object. + /// The members, in order. Names must be unique. + /// The created value. + /// or a member value is . + /// + /// A member name repeats, a name contains an unpaired surrogate, or the + /// resulting nesting is too deep. + /// + public static JsonValue CreateObject(IEnumerable> members) + { + ArgumentNullException.ThrowIfNull(members); + return BuildObject(members, allowDuplicateNames: false); + } + + /// Parses one JSON document from UTF-16 text. + /// The JSON text. + /// The parsed value. + /// is . + /// The text is not a single acceptable JSON document. + public static JsonValue Parse(string json) + { + ArgumentNullException.ThrowIfNull(json); + return JsonValueReader.Parse(json); + } + + /// Parses one JSON document from UTF-8 bytes. + /// The UTF-8 encoded JSON document. + /// The parsed value. + /// The bytes are not a single acceptable JSON document. + public static JsonValue ParseUtf8(ReadOnlySpan utf8Json) => JsonValueReader.Parse(utf8Json); + + /// Reads a JSON boolean. + /// The boolean value. + /// This value is not a boolean. + public bool GetBoolean() => Kind == JsonKind.Boolean + ? _boolean + : throw NotOfKind(JsonKind.Boolean); + + /// Reads a JSON number. + /// The finite numeric value. + /// This value is not a number. + public double GetNumber() => Kind == JsonKind.Number + ? _number + : throw NotOfKind(JsonKind.Number); + + /// Reads a JSON string. + /// The string value. + /// This value is not a string. + public string GetString() => Kind == JsonKind.String + ? _text! + : throw NotOfKind(JsonKind.String); + + /// Looks up an object member by exact name. + /// The member name. + /// The member value when present. + /// when the member exists. + /// is . + public bool TryGetProperty(string name, [NotNullWhen(true)] out JsonValue? value) + { + ArgumentNullException.ThrowIfNull(name); + + if (_memberIndex is not null && _memberIndex.TryGetValue(name, out int index)) + { + value = _members![index].Value; + return true; + } + + value = null; + return false; + } + + /// Looks up an object member by exact name. + /// The member name. + /// The member value. + /// is . + /// The member does not exist. + public JsonValue GetProperty(string name) => + TryGetProperty(name, out JsonValue? value) + ? value + : throw new KeyNotFoundException( + string.Create(CultureInfo.InvariantCulture, $"The JSON value has no member {name}.")); + + /// Reports whether this value is the JSON null literal. + public bool IsNull => Kind == JsonKind.Null; + + /// Returns the worldcut-json-v1 canonical form of this value. + /// The canonical JSON text. + public override string ToString() => CanonicalJson.Serialize(this); + + internal static JsonValue CreateObjectLastWins( + IEnumerable> members) => + BuildObject(members, allowDuplicateNames: true); + + internal static JsonValue CreateArrayOwned(JsonValue[] items) + { + if (items.Length == 0) + { + return EmptyArray; + } + + int depth = 0; + foreach (JsonValue item in items) + { + depth = Math.Max(depth, item.Depth); + } + + return new JsonValue(new ReadOnlyCollection(items), RequireDepth(depth + 1)); + } + + private static JsonValue CreateArray(IReadOnlyList items) + { + if (items.Count == 0) + { + return EmptyArray; + } + + var copy = new JsonValue[items.Count]; + for (int index = 0; index < items.Count; index++) + { + copy[index] = items[index] ?? throw new ArgumentNullException( + nameof(items), + FormattableString.Invariant($"array element {index} is null")); + } + + return CreateArrayOwned(copy); + } + + private static JsonValue BuildObject( + IEnumerable> members, + bool allowDuplicateNames) + { + var ordered = new List>(); + var index = new Dictionary(StringComparer.Ordinal); + int depth = 0; + + foreach (KeyValuePair member in members) + { + string name = member.Key ?? throw new ArgumentNullException( + nameof(members), + "object member name is null"); + JsonValue value = member.Value ?? throw new ArgumentNullException( + nameof(members), + FormattableString.Invariant($"object member {name} has a null value")); + + Utf16.RejectUnpairedSurrogates(name, "object member name"); + + if (index.TryGetValue(name, out int existing)) + { + if (!allowDuplicateNames) + { + throw WorldCutException.InvalidInput( + $"object member {name} is declared more than once"); + } + + ordered[existing] = new KeyValuePair(name, value); + } + else + { + index.Add(name, ordered.Count); + ordered.Add(new KeyValuePair(name, value)); + } + + depth = Math.Max(depth, value.Depth); + } + + if (ordered.Count == 0) + { + return EmptyObject; + } + + return new JsonValue( + new ReadOnlyCollection>(ordered.ToArray()), + index, + RequireDepth(depth + 1)); + } + + private static int RequireDepth(int depth) + { + if (depth > WorldCutProtocol.MaxCanonicalizationDepth) + { + throw WorldCutException.InvalidInput( + FormattableString.Invariant( + $"JSON nesting exceeds the supported depth of {WorldCutProtocol.MaxCanonicalizationDepth} levels")); + } + + return depth; + } + + private InvalidOperationException NotOfKind(JsonKind expected) => + new(string.Create( + CultureInfo.InvariantCulture, + $"The JSON value is {Kind}, not {expected}.")); +} diff --git a/ports/dotnet/src/WorldCut/Json/JsonValueReader.cs b/ports/dotnet/src/WorldCut/Json/JsonValueReader.cs new file mode 100644 index 0000000..090fda6 --- /dev/null +++ b/ports/dotnet/src/WorldCut/Json/JsonValueReader.cs @@ -0,0 +1,298 @@ +using System.Buffers; +using System.Text; +using System.Text.Json; + +namespace WorldCut.Json; + +/// +/// Reads transport JSON into the immutable domain. +/// +/// +/// +/// WorldCut owns the pre-validation performed here rather than relying on +/// defaults, because those defaults substitute +/// U+FFFD for unpaired UTF-16 surrogates when writing. Repairing a +/// malformed string would silently change a verification-record digest, so +/// every unpaired surrogate — raw or \uXXXX escaped — is rejected before +/// any value is constructed. +/// +/// +/// Object members follow last-value-wins semantics, matching +/// JSON.parse in the TypeScript reference and the Go and Python ports. +/// +/// +internal static class JsonValueReader +{ + private static readonly UTF8Encoding StrictUtf8 = new( + encoderShouldEmitUTF8Identifier: false, + throwOnInvalidBytes: true); + + private static readonly JsonReaderOptions ReaderOptions = new() + { + AllowTrailingCommas = false, + CommentHandling = JsonCommentHandling.Disallow, + MaxDepth = WorldCutProtocol.MaxJsonDepth, + }; + + internal static JsonValue Parse(string json) + { + RejectUnpairedSurrogates(json); + return ParseValidatedUtf8(StrictUtf8.GetBytes(json)); + } + + internal static JsonValue Parse(ReadOnlySpan utf8Json) + { + string text; + try + { + text = StrictUtf8.GetString(utf8Json); + } + catch (DecoderFallbackException error) + { + throw WorldCutException.InvalidInput("input is not valid UTF-8", error); + } + + RejectUnpairedSurrogates(text); + return ParseValidatedUtf8(utf8Json); + } + + private static JsonValue ParseValidatedUtf8(ReadOnlySpan utf8Json) + { + try + { + var reader = new Utf8JsonReader(utf8Json, ReaderOptions); + if (!reader.Read()) + { + throw WorldCutException.InvalidInput("input contains no JSON value"); + } + + JsonValue value = ReadValue(ref reader); + if (reader.Read()) + { + throw WorldCutException.InvalidInput("input contains more than one JSON value"); + } + + return value; + } + catch (JsonException error) + { + throw WorldCutException.InvalidInput($"input is not valid JSON: {error.Message}", error); + } + catch (InvalidOperationException error) + { + throw WorldCutException.InvalidInput($"input is not valid JSON: {error.Message}", error); + } + } + + private static JsonValue ReadValue(ref Utf8JsonReader reader) + { + switch (reader.TokenType) + { + case JsonTokenType.StartObject: + return ReadObject(ref reader); + case JsonTokenType.StartArray: + return ReadArray(ref reader); + case JsonTokenType.String: + return JsonValue.Create(reader.GetString()!); + case JsonTokenType.Number: + return ReadNumber(ref reader); + case JsonTokenType.True: + return JsonValue.True; + case JsonTokenType.False: + return JsonValue.False; + case JsonTokenType.Null: + return JsonValue.Null; + default: + throw WorldCutException.InvalidInput( + $"input contains an unexpected JSON token: {reader.TokenType}"); + } + } + + private static JsonValue ReadObject(ref Utf8JsonReader reader) + { + var members = new List>(); + while (true) + { + if (!reader.Read()) + { + throw WorldCutException.InvalidInput("input ends inside a JSON object"); + } + + if (reader.TokenType == JsonTokenType.EndObject) + { + return JsonValue.CreateObjectLastWins(members); + } + + string name = reader.GetString()!; + if (!reader.Read()) + { + throw WorldCutException.InvalidInput("input ends after a JSON member name"); + } + + members.Add(new KeyValuePair(name, ReadValue(ref reader))); + } + } + + private static JsonValue ReadArray(ref Utf8JsonReader reader) + { + var items = new List(); + while (true) + { + if (!reader.Read()) + { + throw WorldCutException.InvalidInput("input ends inside a JSON array"); + } + + if (reader.TokenType == JsonTokenType.EndArray) + { + return items.Count == 0 + ? JsonValue.EmptyArray + : JsonValue.CreateArrayOwned(items.ToArray()); + } + + items.Add(ReadValue(ref reader)); + } + } + + private static JsonValue ReadNumber(ref Utf8JsonReader reader) + { + if (!reader.TryGetDouble(out double value) || !double.IsFinite(value)) + { + throw WorldCutException.InvalidInput( + $"input contains a JSON number outside the finite binary64 domain: {ReadRawNumber(ref reader)}"); + } + + return JsonValue.Create(value); + } + + private static string ReadRawNumber(ref Utf8JsonReader reader) + { + ReadOnlySpan raw = reader.HasValueSequence + ? reader.ValueSequence.ToArray() + : reader.ValueSpan; + return Encoding.UTF8.GetString(raw); + } + + private static void RejectUnpairedSurrogates(string text) + { + int index = 0; + bool inString = false; + + while (index < text.Length) + { + char current = text[index]; + + if (!inString) + { + if (current == '"') + { + inString = true; + index++; + continue; + } + + index += SkipLiteral(text, index); + continue; + } + + if (current == '"') + { + inString = false; + index++; + continue; + } + + if (current != '\\') + { + index += SkipLiteral(text, index); + continue; + } + + index++; + if (index >= text.Length) + { + throw WorldCutException.InvalidInput("input ends inside a JSON escape sequence"); + } + + if (text[index] != 'u') + { + index++; + continue; + } + + int hexStart = index + 1; + if (hexStart + 4 > text.Length || !TryReadHex4(text, hexStart, out int codeUnit)) + { + throw WorldCutException.InvalidInput("input contains an incomplete Unicode escape"); + } + + index = hexStart + 4; + + if (codeUnit is >= 0xD800 and <= 0xDBFF) + { + if (index + 6 > text.Length + || text[index] != '\\' + || text[index + 1] != 'u' + || !TryReadHex4(text, index + 2, out int lowUnit) + || lowUnit is < 0xDC00 or > 0xDFFF) + { + throw WorldCutException.InvalidInput( + "input contains an escaped unpaired high surrogate"); + } + + index += 6; + } + else if (codeUnit is >= 0xDC00 and <= 0xDFFF) + { + throw WorldCutException.InvalidInput( + "input contains an escaped unpaired low surrogate"); + } + } + } + + private static int SkipLiteral(string text, int index) + { + char current = text[index]; + if (char.IsHighSurrogate(current)) + { + if (index + 1 >= text.Length || !char.IsLowSurrogate(text[index + 1])) + { + throw WorldCutException.InvalidInput("input contains an unpaired high surrogate"); + } + + return 2; + } + + if (char.IsLowSurrogate(current)) + { + throw WorldCutException.InvalidInput("input contains an unpaired low surrogate"); + } + + return 1; + } + + private static bool TryReadHex4(string text, int start, out int value) + { + value = 0; + for (int offset = 0; offset < 4; offset++) + { + int digit = HexDigit(text[start + offset]); + if (digit < 0) + { + return false; + } + + value = (value * 16) + digit; + } + + return true; + } + + private static int HexDigit(char character) => character switch + { + >= '0' and <= '9' => character - '0', + >= 'a' and <= 'f' => character - 'a' + 10, + >= 'A' and <= 'F' => character - 'A' + 10, + _ => -1, + }; +} diff --git a/ports/dotnet/src/WorldCut/Json/Utf16.cs b/ports/dotnet/src/WorldCut/Json/Utf16.cs new file mode 100644 index 0000000..97c97d6 --- /dev/null +++ b/ports/dotnet/src/WorldCut/Json/Utf16.cs @@ -0,0 +1,67 @@ +namespace WorldCut.Json; + +/// +/// UTF-16 helpers shared by parsing, canonicalization, and protocol ordering. +/// +/// +/// WorldCut orders object members and protocol identifiers by raw UTF-16 code +/// units, which is exactly . +/// +public static class Utf16 +{ + /// + /// Compares two strings by raw UTF-16 code units, as the protocol requires. + /// + /// The left operand. + /// The right operand. + /// A negative, zero, or positive ordering value. + public static int Compare(string? left, string? right) => string.CompareOrdinal(left, right); + + /// + /// Returns an ordinal comparer over raw UTF-16 code units. + /// + public static StringComparer Comparer => StringComparer.Ordinal; + + /// + /// Returns the index of the first unpaired UTF-16 surrogate code unit. + /// + /// The text to inspect. + /// The index of the offending code unit, or -1 when the text is well formed. + /// is . + public static int IndexOfUnpairedSurrogate(string value) + { + ArgumentNullException.ThrowIfNull(value); + + for (int index = 0; index < value.Length; index++) + { + char current = value[index]; + if (char.IsHighSurrogate(current)) + { + if (index + 1 >= value.Length || !char.IsLowSurrogate(value[index + 1])) + { + return index; + } + + index++; + continue; + } + + if (char.IsLowSurrogate(current)) + { + return index; + } + } + + return -1; + } + + internal static void RejectUnpairedSurrogates(string value, string field) + { + int index = IndexOfUnpairedSurrogate(value); + if (index >= 0) + { + throw WorldCutException.InvalidInput( + $"{field} contains an unpaired UTF-16 surrogate U+{(int)value[index]:X4} at index {index}"); + } + } +} diff --git a/ports/dotnet/src/WorldCut/Model/ContractRequirement.cs b/ports/dotnet/src/WorldCut/Model/ContractRequirement.cs new file mode 100644 index 0000000..c2cbf22 --- /dev/null +++ b/ports/dotnet/src/WorldCut/Model/ContractRequirement.cs @@ -0,0 +1,150 @@ +using WorldCut.Json; + +namespace WorldCut.Model; + +/// The requirement kinds defined by protocol 0.1. +public enum RequirementType +{ + /// An exact dependency between two roles. + Dependency = 0, + + /// A shared non-empty validity interval across roles. + CommonValidTime = 1, + + /// An exact JSON value at a deterministic path. + ValueEquals = 2, +} + +/// The fields shared by every contract requirement. +public abstract class ContractRequirement +{ + private protected ContractRequirement( + string id, + string description, + bool required, + JsonValue raw) + { + Id = id; + Description = description; + Required = required; + Raw = raw; + } + + /// The requirement identifier, unique within one contract. + public string Id { get; } + + /// The human-readable requirement description used in summaries. + public string Description { get; } + + /// Whether the requirement affects the aggregate verdict. + public bool Required { get; } + + /// The requirement kind. + public abstract RequirementType Type { get; } + + /// The wire spelling of . + public abstract string TypeName { get; } + + /// + /// The complete accepted requirement JSON, used verbatim in the digest + /// preimage. + /// + public JsonValue Raw { get; } +} + +/// Requires one role to depend on the exact version of another. +public sealed class DependencyRequirement : ContractRequirement +{ + internal DependencyRequirement( + string id, + string description, + bool required, + JsonValue raw, + string dependentRole, + string targetRole, + string dependencyName) + : base(id, description, required, raw) + { + DependentRole = dependentRole; + TargetRole = targetRole; + DependencyName = dependencyName; + } + + /// + public override RequirementType Type => RequirementType.Dependency; + + /// + public override string TypeName => "dependency"; + + /// The role that declares the dependency. + public string DependentRole { get; } + + /// The role that owns the depended-upon resource. + public string TargetRole { get; } + + /// The dependency name to compare. + public string DependencyName { get; } +} + +/// Requires several roles to share a non-empty validity interval. +public sealed class CommonValidTimeRequirement : ContractRequirement +{ + internal CommonValidTimeRequirement( + string id, + string description, + bool required, + JsonValue raw, + string[] roles, + ValidityInterval within) + : base(id, description, required, raw) + { + Roles = Array.AsReadOnly(roles); + Within = within; + } + + /// + public override RequirementType Type => RequirementType.CommonValidTime; + + /// + public override string TypeName => "common_valid_time"; + + /// The distinct roles that must share a valid time, in declaration order. + public IReadOnlyList Roles { get; } + + /// The contract window the shared interval must fall inside. + public ValidityInterval Within { get; } +} + +/// Requires a deterministic JSON path to equal an exact value. +public sealed class ValueEqualsRequirement : ContractRequirement +{ + internal ValueEqualsRequirement( + string id, + string description, + bool required, + JsonValue raw, + string role, + string[] path, + JsonValue expected) + : base(id, description, required, raw) + { + Role = role; + Path = Array.AsReadOnly(path); + Expected = expected; + } + + /// + public override RequirementType Type => RequirementType.ValueEquals; + + /// + public override string TypeName => "value_equals"; + + /// The role whose observed value is inspected. + public string Role { get; } + + /// The path segments to follow through the observed value. + public IReadOnlyList Path { get; } + + /// The required value at . + public JsonValue Expected { get; } +} diff --git a/ports/dotnet/src/WorldCut/Model/DecisionContract.cs b/ports/dotnet/src/WorldCut/Model/DecisionContract.cs new file mode 100644 index 0000000..572990f --- /dev/null +++ b/ports/dotnet/src/WorldCut/Model/DecisionContract.cs @@ -0,0 +1,39 @@ +using WorldCut.Json; + +namespace WorldCut.Model; + +/// The decision contract carried by a verification input. +public sealed class DecisionContract +{ + internal DecisionContract( + string id, + string version, + NormalizedTimestamp decisionTime, + ContractRequirement[] requirements, + JsonValue raw) + { + Id = id; + Version = version; + DecisionTime = decisionTime; + Requirements = Array.AsReadOnly(requirements); + Raw = raw; + } + + /// The contract identifier. + public string Id { get; } + + /// The contract version. + public string Version { get; } + + /// The instant the decision is made. + public NormalizedTimestamp DecisionTime { get; } + + /// The requirements, in declaration order. + public IReadOnlyList Requirements { get; } + + /// + /// The complete accepted contract JSON, used verbatim in the digest + /// preimage after its requirements are sorted by identifier. + /// + public JsonValue Raw { get; } +} diff --git a/ports/dotnet/src/WorldCut/Model/NormalizedTimestamp.cs b/ports/dotnet/src/WorldCut/Model/NormalizedTimestamp.cs new file mode 100644 index 0000000..74e03f5 --- /dev/null +++ b/ports/dotnet/src/WorldCut/Model/NormalizedTimestamp.cs @@ -0,0 +1,194 @@ +namespace WorldCut.Model; + +/// +/// A normalized ISO-8601 UTC instant of the exact form +/// YYYY-MM-DDTHH:MM:SS.mmmZ. +/// +/// +/// +/// WorldCut deliberately does not use or +/// here. The reference implementation accepts the +/// full ECMAScript date domain, which includes year 0000, while +/// starts at year 0001. This type parses +/// the literal grammar and keeps both the original text and a proleptic +/// Gregorian millisecond ordinal for comparison. +/// +/// +/// The ordinal is only used for ordering. Emitted timestamps always reuse the +/// original accepted text, so no reformatting can change a digest. +/// +/// +public readonly struct NormalizedTimestamp : IEquatable, IComparable +{ + private const int TimestampLength = 24; + + private NormalizedTimestamp(string text, long epochMilliseconds) + { + Text = text; + EpochMilliseconds = epochMilliseconds; + } + + /// The exact accepted timestamp text. + public string Text { get; } + + /// Milliseconds relative to 1970-01-01T00:00:00.000Z, proleptic Gregorian. + public long EpochMilliseconds { get; } + + /// Compares two instants for equality. + /// The left operand. + /// The right operand. + /// when both describe the same instant. + public static bool operator ==(NormalizedTimestamp left, NormalizedTimestamp right) => + left.Equals(right); + + /// Compares two instants for inequality. + /// The left operand. + /// The right operand. + /// when the instants differ. + public static bool operator !=(NormalizedTimestamp left, NormalizedTimestamp right) => + !left.Equals(right); + + /// Reports whether precedes . + /// The left operand. + /// The right operand. + /// when is earlier. + public static bool operator <(NormalizedTimestamp left, NormalizedTimestamp right) => + left.EpochMilliseconds < right.EpochMilliseconds; + + /// Reports whether follows . + /// The left operand. + /// The right operand. + /// when is later. + public static bool operator >(NormalizedTimestamp left, NormalizedTimestamp right) => + left.EpochMilliseconds > right.EpochMilliseconds; + + /// Reports whether is not later than . + /// The left operand. + /// The right operand. + /// when is earlier or equal. + public static bool operator <=(NormalizedTimestamp left, NormalizedTimestamp right) => + left.EpochMilliseconds <= right.EpochMilliseconds; + + /// Reports whether is not earlier than . + /// The left operand. + /// The right operand. + /// when is later or equal. + public static bool operator >=(NormalizedTimestamp left, NormalizedTimestamp right) => + left.EpochMilliseconds >= right.EpochMilliseconds; + + /// Parses a normalized ISO-8601 UTC timestamp. + /// The candidate timestamp text. + /// The parsed instant when the text is accepted. + /// when the text is a normalized timestamp. + public static bool TryParse(string? text, out NormalizedTimestamp timestamp) + { + timestamp = default; + if (text is null || text.Length != TimestampLength) + { + return false; + } + + if (text[4] != '-' || text[7] != '-' || text[10] != 'T' + || text[13] != ':' || text[16] != ':' || text[19] != '.' || text[23] != 'Z') + { + return false; + } + + if (!TryReadDigits(text, 0, 4, out int year) + || !TryReadDigits(text, 5, 2, out int month) + || !TryReadDigits(text, 8, 2, out int day) + || !TryReadDigits(text, 11, 2, out int hour) + || !TryReadDigits(text, 14, 2, out int minute) + || !TryReadDigits(text, 17, 2, out int second) + || !TryReadDigits(text, 20, 3, out int millisecond)) + { + return false; + } + + if (month is < 1 or > 12 || day < 1 || day > DaysInMonth(year, month) + || hour > 23 || minute > 59 || second > 59) + { + return false; + } + + long days = DaysFromCivil(year, month, day); + long milliseconds = ((((days * 24) + hour) * 60 + minute) * 60 + second) * 1000 + millisecond; + timestamp = new NormalizedTimestamp(text, milliseconds); + return true; + } + + /// Returns the later of two instants. + /// The left operand. + /// The right operand. + /// The later instant, preferring when equal. + public static NormalizedTimestamp Max(NormalizedTimestamp left, NormalizedTimestamp right) => + right.EpochMilliseconds > left.EpochMilliseconds ? right : left; + + /// Returns the earlier of two instants. + /// The left operand. + /// The right operand. + /// The earlier instant, preferring when equal. + public static NormalizedTimestamp Min(NormalizedTimestamp left, NormalizedTimestamp right) => + right.EpochMilliseconds < left.EpochMilliseconds ? right : left; + + /// + public bool Equals(NormalizedTimestamp other) => EpochMilliseconds == other.EpochMilliseconds; + + /// + public override bool Equals(object? obj) => obj is NormalizedTimestamp other && Equals(other); + + /// + public override int GetHashCode() => EpochMilliseconds.GetHashCode(); + + /// + public int CompareTo(NormalizedTimestamp other) => + EpochMilliseconds.CompareTo(other.EpochMilliseconds); + + /// + public override string ToString() => Text ?? string.Empty; + + private static bool TryReadDigits(string text, int start, int length, out int value) + { + value = 0; + for (int offset = 0; offset < length; offset++) + { + char digit = text[start + offset]; + if (digit is < '0' or > '9') + { + return false; + } + + value = (value * 10) + (digit - '0'); + } + + return true; + } + + private static bool IsLeapYear(int year) => + year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); + + private static int DaysInMonth(int year, int month) => month switch + { + 2 => IsLeapYear(year) ? 29 : 28, + 4 or 6 or 9 or 11 => 30, + _ => 31, + }; + + /// + /// Converts a proleptic Gregorian civil date to days relative to + /// 1970-01-01, using Howard Hinnant's era-based algorithm. + /// + /// + /// The era form is used because it stays exact for year 0000, which + /// the ECMAScript date domain accepts and does not. + /// + private static long DaysFromCivil(int year, int month, int day) + { + long shiftedYear = month <= 2 ? year - 1L : year; + long era = (shiftedYear >= 0 ? shiftedYear : shiftedYear - 399) / 400; + long yearOfEra = shiftedYear - (era * 400); + long dayOfYear = ((153 * (month + (month > 2 ? -3 : 9))) + 2) / 5 + day - 1; + long dayOfEra = (yearOfEra * 365) + (yearOfEra / 4) - (yearOfEra / 100) + dayOfYear; + return (era * 146097) + dayOfEra - 719468; + } +} diff --git a/ports/dotnet/src/WorldCut/Model/Observation.cs b/ports/dotnet/src/WorldCut/Model/Observation.cs new file mode 100644 index 0000000..bdc5e42 --- /dev/null +++ b/ports/dotnet/src/WorldCut/Model/Observation.cs @@ -0,0 +1,54 @@ +using WorldCut.Json; + +namespace WorldCut.Model; + +/// One observation bound to exactly one contract role. +public sealed class Observation +{ + internal Observation( + string id, + string role, + ResourceIdentity resource, + JsonValue value, + NormalizedTimestamp observedAt, + long acquisitionCost, + ObservationWitness witness, + JsonValue raw) + { + Id = id; + Role = role; + Resource = resource; + Value = value; + ObservedAt = observedAt; + AcquisitionCost = acquisitionCost; + Witness = witness; + Raw = raw; + } + + /// The observation identifier, unique within one input. + public string Id { get; } + + /// The contract role this observation is bound to. + public string Role { get; } + + /// The identity of the observed resource. + public ResourceIdentity Resource { get; } + + /// The observed JSON value. + public JsonValue Value { get; } + + /// When the observation was taken. + public NormalizedTimestamp ObservedAt { get; } + + /// The declared cost of reacquiring this observation. + public long AcquisitionCost { get; } + + /// The provider metadata accompanying this observation. + public ObservationWitness Witness { get; } + + /// + /// The complete accepted observation JSON, used verbatim in the digest + /// preimage. + /// + public JsonValue Raw { get; } +} diff --git a/ports/dotnet/src/WorldCut/Model/ResourceIdentity.cs b/ports/dotnet/src/WorldCut/Model/ResourceIdentity.cs new file mode 100644 index 0000000..3116bd4 --- /dev/null +++ b/ports/dotnet/src/WorldCut/Model/ResourceIdentity.cs @@ -0,0 +1,63 @@ +namespace WorldCut.Model; + +/// +/// The four-component resource identity compared by WorldCut. +/// +/// +/// Versions are comparable only when all four components are equal. +/// +public sealed class ResourceIdentity : IEquatable +{ + internal ResourceIdentity(string provider, string account, string kind, string key) + { + Provider = provider; + Account = account; + Kind = kind; + Key = key; + } + + /// The system that produced the resource. + public string Provider { get; } + + /// The tenant or account that owns the resource. + public string Account { get; } + + /// The resource type. + public string Kind { get; } + + /// The provider-scoped resource key. + public string Key { get; } + + /// Compares two identities component by component. + /// The left operand. + /// The right operand. + /// when every component is equal. + public static bool operator ==(ResourceIdentity? left, ResourceIdentity? right) => + left is null ? right is null : left.Equals(right); + + /// Compares two identities component by component. + /// The left operand. + /// The right operand. + /// when any component differs. + public static bool operator !=(ResourceIdentity? left, ResourceIdentity? right) => + !(left == right); + + /// + public bool Equals(ResourceIdentity? other) => + other is not null + && string.Equals(Provider, other.Provider, StringComparison.Ordinal) + && string.Equals(Account, other.Account, StringComparison.Ordinal) + && string.Equals(Kind, other.Kind, StringComparison.Ordinal) + && string.Equals(Key, other.Key, StringComparison.Ordinal); + + /// + public override bool Equals(object? obj) => Equals(obj as ResourceIdentity); + + /// + public override int GetHashCode() => + HashCode.Combine( + StringComparer.Ordinal.GetHashCode(Provider), + StringComparer.Ordinal.GetHashCode(Account), + StringComparer.Ordinal.GetHashCode(Kind), + StringComparer.Ordinal.GetHashCode(Key)); +} diff --git a/ports/dotnet/src/WorldCut/Model/Witness.cs b/ports/dotnet/src/WorldCut/Model/Witness.cs new file mode 100644 index 0000000..b0d8de2 --- /dev/null +++ b/ports/dotnet/src/WorldCut/Model/Witness.cs @@ -0,0 +1,108 @@ +namespace WorldCut.Model; + +/// +/// A half-open validity interval [From, Until). A null upper bound is +/// positive infinity. +/// +public sealed class ValidityInterval +{ + internal ValidityInterval(NormalizedTimestamp from, NormalizedTimestamp? until) + { + From = from; + Until = until; + } + + /// The inclusive lower bound. + public NormalizedTimestamp From { get; } + + /// The exclusive upper bound, or for positive infinity. + public NormalizedTimestamp? Until { get; } +} + +/// The provenance categories accepted in protocol 0.1. +public enum WitnessProvenance +{ + /// The provider asserted the metadata. + ProviderAsserted = 0, + + /// The client observed the metadata directly. + ClientObserved = 1, + + /// The metadata was derived from other evidence. + Derived = 2, + + /// An operator supplied the metadata. + OperatorSupplied = 3, +} + +/// A declared dependency of one observation on another resource. +public sealed class DependencyWitness +{ + internal DependencyWitness( + string name, + ResourceIdentity resource, + string? version, + WitnessProvenance provenance) + { + Name = name; + Resource = resource; + Version = version; + Provenance = provenance; + } + + /// The dependency name, unique within one observation. + public string Name { get; } + + /// The resource the dependency refers to. + public ResourceIdentity Resource { get; } + + /// The only relation defined by protocol 0.1. + public static string Relation => "exact"; + + /// The declared dependency version, when the provider exposes one. + public string? Version { get; } + + /// How the dependency metadata was obtained. + public WitnessProvenance Provenance { get; } +} + +/// The metadata a provider exposes alongside an observation. +public sealed class ObservationWitness +{ + internal ObservationWitness( + WitnessProvenance provenance, + string? version, + ValidityInterval? validity, + DependencyWitness[] dependencies) + { + Provenance = provenance; + Version = version; + Validity = validity; + Dependencies = Array.AsReadOnly(dependencies); + } + + /// How the observation was obtained. + public WitnessProvenance Provenance { get; } + + /// The exact resource version, when the provider exposes one. + public string? Version { get; } + + /// The declared validity interval, when the provider exposes one. + public ValidityInterval? Validity { get; } + + /// The declared dependencies, in declaration order. + public IReadOnlyList Dependencies { get; } + + internal DependencyWitness? FindDependency(string name) + { + foreach (DependencyWitness dependency in Dependencies) + { + if (string.Equals(dependency.Name, name, StringComparison.Ordinal)) + { + return dependency; + } + } + + return null; + } +} diff --git a/ports/dotnet/src/WorldCut/PACKAGE.md b/ports/dotnet/src/WorldCut/PACKAGE.md new file mode 100644 index 0000000..b284799 --- /dev/null +++ b/ports/dotnet/src/WorldCut/PACKAGE.md @@ -0,0 +1,93 @@ +# WorldCut + +Independent .NET implementation of the **WorldCut** decision-coherence +protocol `0.1`, engine ruleset `0.1.2`, and canonicalization +`worldcut-json-v1`. + +WorldCut answers one question: *do observations gathered from independent +systems actually satisfy the version and time relationships a decision +requires?* It returns `CONTRACT_SATISFIED`, `CONTRACT_VIOLATED`, or +`INSUFFICIENT_EVIDENCE`, plus a deterministic digest of the verification +record. + +The package has **no third-party package dependencies** and targets +**.NET 8** and **.NET 10**. + +## Install + +```sh +dotnet add package WorldCut +``` + +The matching CLI is published separately as a .NET tool: + +```sh +dotnet tool install --global WorldCut.Tool +worldcut-dotnet verification.json +``` + +## Verify a document + +```csharp +using WorldCut; + +VerificationResult result = WorldCutVerifier.VerifyJsonUtf8(File.ReadAllBytes("verification.json")); + +Console.WriteLine(result.Verdict); // ContractSatisfied +Console.WriteLine(result.Verdict.ToWireName()); // CONTRACT_SATISFIED +Console.WriteLine(result.VerificationRecordDigest); // 64 lowercase hex characters +``` + +## Parse once, verify repeatedly + +```csharp +using WorldCut; + +ParsedVerificationInput input = ParsedVerificationInput.Parse(json); + +VerificationResult first = WorldCutVerifier.Verify(input); +VerificationResult second = WorldCutVerifier.Verify(input); +// first and second are independent, deeply immutable values. +``` + +`ParsedVerificationInput` has no public constructor, so it cannot be built into +an invalid state, and every value reachable from a result is immutable. + +## Canonical JSON and digests + +```csharp +using WorldCut.Json; + +JsonValue value = JsonValue.Parse("""{"z":1,"a":2}"""); + +CanonicalJson.Serialize(value); // {"a":2,"z":1} +CanonicalJson.ComputeSha256Hex(value); +``` + +## Errors + +Every failure is a `WorldCutException` carrying a stable +`Code`/`WireCode`: `WORLDCUT_INVALID_INPUT`, `WORLDCUT_INVALID_ARGUMENT`, +`WORLDCUT_FILE_READ_FAILED`, or `WORLDCUT_RUNTIME_ERROR`. JSON syntax failures +are reported as `WORLDCUT_INVALID_INPUT`, matching the Go and Python ports. + +## Boundaries + +WorldCut evaluates a declared contract deterministically. It does not decide +what the contract should be, infer missing relationships, fetch evidence, or +establish that a provider is truthful. The record digest detects record +changes; it is not a digital signature. + +This port contains the verifier, canonicalization, and CLI only. Cloud +adapters, the GitHub Actions integration, and the Agentic Data Kernel adapter +are not included. + +## More + +* Protocol, results, canonicalization, and conformance specifications: + +* Port documentation: + +* Third-party notices: `THIRD-PARTY-NOTICES.md` in this package. + +Licensed under the Apache License 2.0. diff --git a/ports/dotnet/src/WorldCut/ParsedVerificationInput.cs b/ports/dotnet/src/WorldCut/ParsedVerificationInput.cs new file mode 100644 index 0000000..893e6c5 --- /dev/null +++ b/ports/dotnet/src/WorldCut/ParsedVerificationInput.cs @@ -0,0 +1,76 @@ +using WorldCut.Json; +using WorldCut.Model; + +namespace WorldCut; + +/// +/// An immutable, fully validated WorldCut verification input. +/// +/// +/// +/// A can only be produced by +/// or , +/// so it cannot be constructed into an invalid state. Every value it exposes is +/// immutable, which means a caller can verify the same parsed input repeatedly +/// and cannot alter it between verifications. +/// +/// +public sealed class ParsedVerificationInput +{ + internal ParsedVerificationInput( + string protocolVersion, + DecisionContract contract, + Observation[] observations) + { + ProtocolVersion = protocolVersion; + Contract = contract; + Observations = Array.AsReadOnly(observations); + } + + /// The accepted protocol version, always 0.1. + public string ProtocolVersion { get; } + + /// The decision contract. + public DecisionContract Contract { get; } + + /// The observations, in input order. + public IReadOnlyList Observations { get; } + + /// Parses and validates one verification input from UTF-16 text. + /// The verification input JSON. + /// The validated immutable input. + /// is . + /// + /// The document is not a valid WorldCut 0.1 verification input. The error + /// code is always . + /// + public static ParsedVerificationInput Parse(string json) + { + ArgumentNullException.ThrowIfNull(json); + return VerificationInputValidator.Validate(JsonValue.Parse(json)); + } + + /// Parses and validates one verification input from UTF-8 bytes. + /// The UTF-8 encoded verification input JSON. + /// The validated immutable input. + /// + /// The document is not a valid WorldCut 0.1 verification input. The error + /// code is always . + /// + public static ParsedVerificationInput ParseUtf8(ReadOnlySpan utf8Json) => + VerificationInputValidator.Validate(JsonValue.ParseUtf8(utf8Json)); + + internal IReadOnlyList RequirementsById() + { + var sorted = Contract.Requirements.ToArray(); + Array.Sort(sorted, static (left, right) => Utf16.Compare(left.Id, right.Id)); + return sorted; + } + + internal IReadOnlyList ObservationsByRole() + { + var sorted = Observations.ToArray(); + Array.Sort(sorted, static (left, right) => Utf16.Compare(left.Role, right.Role)); + return sorted; + } +} diff --git a/ports/dotnet/src/WorldCut/Vendored/JcsNet/.editorconfig b/ports/dotnet/src/WorldCut/Vendored/JcsNet/.editorconfig new file mode 100644 index 0000000..1be0fc8 --- /dev/null +++ b/ports/dotnet/src/WorldCut/Vendored/JcsNet/.editorconfig @@ -0,0 +1,22 @@ +# Third-party source vendored verbatim from Jcs.NET 0.1.1 (MIT). +# +# Upstream fidelity is deliberate: this directory is kept as close to +# https://github.com/IsraelIyonsi/Jcs.NET @ 8aff616 as possible so that the +# audited RFC 8785 implementation stays reviewable and diffable. Repository +# style preferences and .NET analyzers are therefore not enforced here. +# +# See ports/dotnet/THIRD-PARTY-NOTICES.md for attribution, the exact list of +# WorldCut modifications, and the WorldCut behaviour layered on top. + +root = false + +[*.cs] +dotnet_analyzer_diagnostic.severity = none +dotnet_diagnostic.CA1032.severity = none +dotnet_diagnostic.CA1307.severity = none +dotnet_diagnostic.CA1310.severity = none +dotnet_diagnostic.CA1866.severity = none +dotnet_diagnostic.CA2251.severity = none +dotnet_diagnostic.IDE0005.severity = none +dotnet_diagnostic.IDE0055.severity = none +dotnet_diagnostic.IDE0161.severity = none diff --git a/ports/dotnet/src/WorldCut/Vendored/JcsNet/CanonicalJsonSerializer.cs b/ports/dotnet/src/WorldCut/Vendored/JcsNet/CanonicalJsonSerializer.cs new file mode 100644 index 0000000..9bb8bc4 --- /dev/null +++ b/ports/dotnet/src/WorldCut/Vendored/JcsNet/CanonicalJsonSerializer.cs @@ -0,0 +1,178 @@ +// ----------------------------------------------------------------------------- +// Vendored third-party source. This file is NOT original WorldCut code. +// +// Source: Jcs.NET 0.1.1 - https://github.com/IsraelIyonsi/Jcs.NET +// Commit: 8aff61685300d5d94b81f05246f95d4681e7178a +// Copyright: Copyright (c) 2026 Israel Iyonsi +// License: MIT (see the LICENSE file next to this source) +// +// WorldCut modifications are limited to this header and to the changes listed +// in ports/dotnet/THIRD-PARTY-NOTICES.md. Do not edit for style; upstream +// fidelity is deliberate. +// ----------------------------------------------------------------------------- +using System.Text; +using System.Text.Json; + +namespace Jcs.Net; + +internal static class CanonicalJsonSerializer +{ + private const char BeginObject = '{'; + private const char EndObject = '}'; + private const char BeginArray = '['; + private const char EndArray = ']'; + private const char NameSeparator = ':'; + private const char ValueSeparator = ','; + private const string TrueLiteral = "true"; + private const string FalseLiteral = "false"; + private const string NullLiteral = "null"; + + // Matches the effective nesting cap the string entry point already enforces: + // System.Text.Json's JsonDocument default MaxDepth is 64. Guarding the + // JsonElement path at the same limit keeps both entry points uniform and + // turns unbounded recursion on hostile input into a catchable JcsException + // instead of an uncatchable StackOverflowException. + private const int MaxNestingDepth = 64; + + internal static void Serialize(StringBuilder builder, JsonElement element) + { + Serialize(builder, element, depth: 1); + } + + private static void Serialize(StringBuilder builder, JsonElement element, int depth) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + SerializeObject(builder, element, depth); + break; + case JsonValueKind.Array: + SerializeArray(builder, element, depth); + break; + case JsonValueKind.String: + JsonStringSerializer.Serialize(builder, ReadStringValue(element)); + break; + case JsonValueKind.Number: + builder.Append(EcmaScriptNumberFormatter.Format(ToFiniteDouble(element))); + break; + case JsonValueKind.True: + builder.Append(TrueLiteral); + break; + case JsonValueKind.False: + builder.Append(FalseLiteral); + break; + case JsonValueKind.Null: + builder.Append(NullLiteral); + break; + default: + throw new JcsException("An undefined JsonElement cannot be canonicalized."); + } + } + + private static void SerializeObject(StringBuilder builder, JsonElement element, int depth) + { + RejectExcessiveDepth(depth); + + var properties = new List<(string Name, JsonElement Value)>(); + foreach (var property in element.EnumerateObject()) + { + properties.Add((ReadPropertyName(property), property.Value)); + } + + properties.Sort(static (left, right) => string.CompareOrdinal(left.Name, right.Name)); + RejectDuplicateNames(properties); + + builder.Append(BeginObject); + for (var index = 0; index < properties.Count; index++) + { + if (index > 0) + { + builder.Append(ValueSeparator); + } + + JsonStringSerializer.Serialize(builder, properties[index].Name); + builder.Append(NameSeparator); + Serialize(builder, properties[index].Value, depth + 1); + } + + builder.Append(EndObject); + } + + private static void SerializeArray(StringBuilder builder, JsonElement element, int depth) + { + RejectExcessiveDepth(depth); + + builder.Append(BeginArray); + var first = true; + foreach (var item in element.EnumerateArray()) + { + if (!first) + { + builder.Append(ValueSeparator); + } + + first = false; + Serialize(builder, item, depth + 1); + } + + builder.Append(EndArray); + } + + private static void RejectExcessiveDepth(int depth) + { + if (depth > MaxNestingDepth) + { + throw new JcsException( + $"JSON nesting exceeds the maximum supported depth of {MaxNestingDepth} levels."); + } + } + + private static void RejectDuplicateNames(List<(string Name, JsonElement Value)> sortedProperties) + { + for (var index = 1; index < sortedProperties.Count; index++) + { + if (string.CompareOrdinal(sortedProperties[index - 1].Name, sortedProperties[index].Name) == 0) + { + throw new JcsException( + $"Duplicate object member name \"{sortedProperties[index].Name}\" violates I-JSON (RFC 8785 section 3.1)."); + } + } + } + + private static string ReadStringValue(JsonElement element) + { + try + { + return element.GetString()!; + } + catch (InvalidOperationException exception) + { + throw new JcsException( + "String value contains an unpaired UTF-16 surrogate (RFC 8785 section 3.2.2.2).", exception); + } + } + + private static string ReadPropertyName(JsonProperty property) + { + try + { + return property.Name; + } + catch (InvalidOperationException exception) + { + throw new JcsException( + "Property name contains an unpaired UTF-16 surrogate (RFC 8785 section 3.2.2.2).", exception); + } + } + + private static double ToFiniteDouble(JsonElement element) + { + if (!element.TryGetDouble(out var value) || double.IsNaN(value) || double.IsInfinity(value)) + { + throw new JcsException( + $"Number {element.GetRawText()} cannot be represented as an IEEE 754 double (RFC 8785 section 3.1)."); + } + + return value; + } +} diff --git a/ports/dotnet/src/WorldCut/Vendored/JcsNet/EcmaScriptNumberFormatter.cs b/ports/dotnet/src/WorldCut/Vendored/JcsNet/EcmaScriptNumberFormatter.cs new file mode 100644 index 0000000..da4e0df --- /dev/null +++ b/ports/dotnet/src/WorldCut/Vendored/JcsNet/EcmaScriptNumberFormatter.cs @@ -0,0 +1,111 @@ +// ----------------------------------------------------------------------------- +// Vendored third-party source. This file is NOT original WorldCut code. +// +// Source: Jcs.NET 0.1.1 - https://github.com/IsraelIyonsi/Jcs.NET +// Commit: 8aff61685300d5d94b81f05246f95d4681e7178a +// Copyright: Copyright (c) 2026 Israel Iyonsi +// License: MIT (see the LICENSE file next to this source) +// +// WorldCut modifications are limited to this header and to the changes listed +// in ports/dotnet/THIRD-PARTY-NOTICES.md. Do not edit for style; upstream +// fidelity is deliberate. +// ----------------------------------------------------------------------------- +using System.Globalization; +using System.Text; + +namespace Jcs.Net; + +internal static class EcmaScriptNumberFormatter +{ + private const string Zero = "0"; + private const string ShortestRoundTripFormat = "R"; + private const char DotNetExponentMarker = 'E'; + private const char DecimalPoint = '.'; + private const char ZeroDigit = '0'; + private const char MinusSign = '-'; + private const char PlusSign = '+'; + private const char EcmaScriptExponentMarker = 'e'; + private const int MaxPlainNotationExponent = 21; + private const int MinPlainNotationExponent = -6; + + internal static string Format(double value) + { + if (double.IsNaN(value) || double.IsInfinity(value)) + { + throw new JcsException( + "NaN and Infinity cannot be represented in JSON (RFC 8785 section 3.2.2.3)."); + } + + if (value == 0d) + { + return Zero; + } + + var magnitude = Math.Abs(value); + var (digits, pointPosition) = DecomposeShortestRoundTrip(magnitude); + var formatted = ComposeEcmaScriptNotation(digits, pointPosition); + return value < 0d ? MinusSign + formatted : formatted; + } + + private static (string Digits, int PointPosition) DecomposeShortestRoundTrip(double magnitude) + { + // Depends on the .NET Core 3.0+ runtime contract that "R" yields the + // shortest round-trippable representation. The ECMAScript re-composition + // below is only correct for shortest digits; do not change this to "G17". + var text = magnitude.ToString(ShortestRoundTripFormat, CultureInfo.InvariantCulture); + + var exponent = 0; + var exponentMarkerIndex = text.IndexOf(DotNetExponentMarker); + if (exponentMarkerIndex >= 0) + { + exponent = int.Parse(text[(exponentMarkerIndex + 1)..], CultureInfo.InvariantCulture); + text = text[..exponentMarkerIndex]; + } + + var pointIndex = text.IndexOf(DecimalPoint); + var digits = pointIndex >= 0 ? text.Remove(pointIndex, 1) : text; + var integerDigitCount = pointIndex >= 0 ? pointIndex : text.Length; + + var leadingZeroCount = digits.Length - digits.TrimStart(ZeroDigit).Length; + digits = digits.Trim(ZeroDigit); + + var pointPosition = integerDigitCount + exponent - leadingZeroCount; + return (digits, pointPosition); + } + + private static string ComposeEcmaScriptNotation(string digits, int pointPosition) + { + if (digits.Length <= pointPosition && pointPosition <= MaxPlainNotationExponent) + { + return digits + new string(ZeroDigit, pointPosition - digits.Length); + } + + if (0 < pointPosition && pointPosition <= MaxPlainNotationExponent) + { + return digits[..pointPosition] + DecimalPoint + digits[pointPosition..]; + } + + if (MinPlainNotationExponent < pointPosition && pointPosition <= 0) + { + return Zero + DecimalPoint + new string(ZeroDigit, -pointPosition) + digits; + } + + return ComposeExponentialNotation(digits, pointPosition); + } + + private static string ComposeExponentialNotation(string digits, int pointPosition) + { + var builder = new StringBuilder(digits.Length + 8); + builder.Append(digits[0]); + if (digits.Length > 1) + { + builder.Append(DecimalPoint).Append(digits, 1, digits.Length - 1); + } + + var exponent = pointPosition - 1; + builder.Append(EcmaScriptExponentMarker) + .Append(exponent >= 0 ? PlusSign : MinusSign) + .Append(Math.Abs(exponent)); + return builder.ToString(); + } +} diff --git a/ports/dotnet/src/WorldCut/Vendored/JcsNet/JcsException.cs b/ports/dotnet/src/WorldCut/Vendored/JcsNet/JcsException.cs new file mode 100644 index 0000000..6b212e7 --- /dev/null +++ b/ports/dotnet/src/WorldCut/Vendored/JcsNet/JcsException.cs @@ -0,0 +1,36 @@ +// ----------------------------------------------------------------------------- +// Vendored third-party source. This file is NOT original WorldCut code. +// +// Source: Jcs.NET 0.1.1 - https://github.com/IsraelIyonsi/Jcs.NET +// Commit: 8aff61685300d5d94b81f05246f95d4681e7178a +// Copyright: Copyright (c) 2026 Israel Iyonsi +// License: MIT (see the LICENSE file next to this source) +// +// WorldCut modifications are limited to this header and to the changes listed +// in ports/dotnet/THIRD-PARTY-NOTICES.md. Do not edit for style; upstream +// fidelity is deliberate. +// ----------------------------------------------------------------------------- +using System.Text.Json; + +namespace Jcs.Net; + +/// +/// Thrown when input cannot be canonicalized under RFC 8785: invalid JSON, +/// numbers outside the IEEE 754 double range (NaN, Infinity), duplicate +/// object member names, or unpaired UTF-16 surrogates in string data. +/// +internal sealed class JcsException : JsonException +{ + /// Initializes the exception with a message describing the violation. + /// Description of the RFC 8785 constraint that was violated. + public JcsException(string message) : base(message) + { + } + + /// Initializes the exception with a message and the underlying failure. + /// Description of the RFC 8785 constraint that was violated. + /// The original exception raised by the JSON reader. + public JcsException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/ports/dotnet/src/WorldCut/Vendored/JcsNet/JsonCanonicalizer.cs b/ports/dotnet/src/WorldCut/Vendored/JcsNet/JsonCanonicalizer.cs new file mode 100644 index 0000000..8e3d5ef --- /dev/null +++ b/ports/dotnet/src/WorldCut/Vendored/JcsNet/JsonCanonicalizer.cs @@ -0,0 +1,96 @@ +// ----------------------------------------------------------------------------- +// Vendored third-party source. This file is NOT original WorldCut code. +// +// Source: Jcs.NET 0.1.1 - https://github.com/IsraelIyonsi/Jcs.NET +// Commit: 8aff61685300d5d94b81f05246f95d4681e7178a +// Copyright: Copyright (c) 2026 Israel Iyonsi +// License: MIT (see the LICENSE file next to this source) +// +// WorldCut modifications are limited to this header and to the changes listed +// in ports/dotnet/THIRD-PARTY-NOTICES.md. Do not edit for style; upstream +// fidelity is deliberate. +// ----------------------------------------------------------------------------- +using System.Text; +using System.Text.Json; + +namespace Jcs.Net; + +/// +/// Canonicalizes JSON per RFC 8785 (JSON Canonicalization Scheme) so that +/// equivalent documents always serialize to the same byte sequence. +/// +internal static class JsonCanonicalizer +{ + private const int DefaultBuilderCapacity = 256; + + /// Canonicalizes a JSON text. + /// The JSON text to canonicalize. + /// The canonical form as a string. + /// When is null. + /// When the input is not valid RFC 8785 input. + public static string Canonicalize(string json) + { + ArgumentNullException.ThrowIfNull(json); + JsonTextSurrogateValidator.Validate(json); + using var document = JsonDocument.Parse(json); + return Canonicalize(document.RootElement); + } + + /// Canonicalizes a parsed . + /// The element to canonicalize. + /// The canonical form as a string. + /// When the element is not valid RFC 8785 input. + public static string Canonicalize(JsonElement element) + { + var builder = new StringBuilder(DefaultBuilderCapacity); + CanonicalJsonSerializer.Serialize(builder, element); + return builder.ToString(); + } + + /// Canonicalizes a JSON text and encodes the result as UTF-8. + /// The JSON text to canonicalize. + /// The canonical form as UTF-8 bytes, suitable for hashing or signing. + /// When is null. + /// When the input is not valid RFC 8785 input. + public static byte[] CanonicalizeToUtf8(string json) + { + return Encoding.UTF8.GetBytes(Canonicalize(json)); + } + + /// Canonicalizes a parsed and encodes the result as UTF-8. + /// The element to canonicalize. + /// The canonical form as UTF-8 bytes, suitable for hashing or signing. + /// When the element is not valid RFC 8785 input. + public static byte[] CanonicalizeToUtf8(JsonElement element) + { + return Encoding.UTF8.GetBytes(Canonicalize(element)); + } + + /// Attempts to canonicalize a JSON text without throwing. + /// The JSON text to canonicalize. + /// The canonical form, or null when the input is invalid. + /// True when canonicalization succeeded; false for any invalid input, including null. + public static bool TryCanonicalize(string? json, out string? canonical) + { + canonical = null; + if (json is null) + { + return false; + } + + try + { + canonical = Canonicalize(json); + return true; + } + catch (JsonException) + { + // Invariant: every failure in the pipeline is JsonException-derived. + // The validator and serializer throw JcsException (including translations + // of the reader's InvalidOperationException) and the parser throws + // JsonException. Preserve that invariant or this method loses its + // no-throw guarantee. + return false; + } + } +} diff --git a/ports/dotnet/src/WorldCut/Vendored/JcsNet/JsonStringSerializer.cs b/ports/dotnet/src/WorldCut/Vendored/JcsNet/JsonStringSerializer.cs new file mode 100644 index 0000000..f790ac6 --- /dev/null +++ b/ports/dotnet/src/WorldCut/Vendored/JcsNet/JsonStringSerializer.cs @@ -0,0 +1,96 @@ +// ----------------------------------------------------------------------------- +// Vendored third-party source. This file is NOT original WorldCut code. +// +// Source: Jcs.NET 0.1.1 - https://github.com/IsraelIyonsi/Jcs.NET +// Commit: 8aff61685300d5d94b81f05246f95d4681e7178a +// Copyright: Copyright (c) 2026 Israel Iyonsi +// License: MIT (see the LICENSE file next to this source) +// +// WorldCut modifications are limited to this header and to the changes listed +// in ports/dotnet/THIRD-PARTY-NOTICES.md. Do not edit for style; upstream +// fidelity is deliberate. +// ----------------------------------------------------------------------------- +using System.Text; + +namespace Jcs.Net; + +internal static class JsonStringSerializer +{ + private const char QuotationMark = '"'; + private const char ReverseSolidus = '\\'; + private const char Backspace = '\b'; + private const char HorizontalTab = '\t'; + private const char LineFeed = '\n'; + private const char FormFeed = '\f'; + private const char CarriageReturn = '\r'; + private const char ControlRangeEnd = '\u001f'; + private const string LowercaseHexEscapePrefix = "\\u"; + private const string LowercaseFourDigitHexFormat = "x4"; + + internal static void Serialize(StringBuilder builder, string value) + { + builder.Append(QuotationMark); + for (var index = 0; index < value.Length; index++) + { + var current = value[index]; + if (char.IsHighSurrogate(current)) + { + var hasLowSurrogate = index + 1 < value.Length && char.IsLowSurrogate(value[index + 1]); + if (!hasLowSurrogate) + { + throw UnpairedSurrogate(current); + } + + builder.Append(current).Append(value[index + 1]); + index++; + continue; + } + + if (char.IsLowSurrogate(current)) + { + throw UnpairedSurrogate(current); + } + + AppendBasicPlaneCharacter(builder, current); + } + + builder.Append(QuotationMark); + } + + private static void AppendBasicPlaneCharacter(StringBuilder builder, char character) + { + switch (character) + { + case QuotationMark: + case ReverseSolidus: + builder.Append(ReverseSolidus).Append(character); + return; + case Backspace: + builder.Append(ReverseSolidus).Append('b'); + return; + case HorizontalTab: + builder.Append(ReverseSolidus).Append('t'); + return; + case LineFeed: + builder.Append(ReverseSolidus).Append('n'); + return; + case FormFeed: + builder.Append(ReverseSolidus).Append('f'); + return; + case CarriageReturn: + builder.Append(ReverseSolidus).Append('r'); + return; + case <= ControlRangeEnd: + builder.Append(LowercaseHexEscapePrefix) + .Append(((int)character).ToString( + LowercaseFourDigitHexFormat, System.Globalization.CultureInfo.InvariantCulture)); + return; + default: + builder.Append(character); + return; + } + } + + private static JcsException UnpairedSurrogate(char surrogate) => + new($"Unpaired UTF-16 surrogate U+{(int)surrogate:X4} in string data (RFC 8785 section 3.2.2.2)."); +} diff --git a/ports/dotnet/src/WorldCut/Vendored/JcsNet/JsonTextSurrogateValidator.cs b/ports/dotnet/src/WorldCut/Vendored/JcsNet/JsonTextSurrogateValidator.cs new file mode 100644 index 0000000..ebdcf48 --- /dev/null +++ b/ports/dotnet/src/WorldCut/Vendored/JcsNet/JsonTextSurrogateValidator.cs @@ -0,0 +1,191 @@ +// ----------------------------------------------------------------------------- +// Vendored third-party source. This file is NOT original WorldCut code. +// +// Source: Jcs.NET 0.1.1 - https://github.com/IsraelIyonsi/Jcs.NET +// Commit: 8aff61685300d5d94b81f05246f95d4681e7178a +// Copyright: Copyright (c) 2026 Israel Iyonsi +// License: MIT (see the LICENSE file next to this source) +// +// WorldCut modifications are limited to this header and to the changes listed +// in ports/dotnet/THIRD-PARTY-NOTICES.md. Do not edit for style; upstream +// fidelity is deliberate. +// ----------------------------------------------------------------------------- +namespace Jcs.Net; + +internal static class JsonTextSurrogateValidator +{ + private const char QuotationMark = '"'; + private const char ReverseSolidus = '\\'; + private const char UnicodeEscapeMarker = 'u'; + private const int UnicodeEscapeHexDigits = 4; + private const int HexRadix = 16; + + private enum PendingHighSurrogate + { + None, + Literal, + Escaped + } + + internal static void Validate(string json) + { + var index = 0; + while (index < json.Length) + { + var current = json[index]; + if (current == QuotationMark) + { + index = ScanString(json, index + 1); + } + else if (char.IsHighSurrogate(current) + && index + 1 < json.Length + && char.IsLowSurrogate(json[index + 1])) + { + index += 2; + } + else if (char.IsSurrogate(current)) + { + throw MalformedJson(); + } + else + { + index++; + } + } + } + + private static int ScanString(string json, int start) + { + var index = start; + var pending = PendingHighSurrogate.None; + while (index < json.Length) + { + var current = json[index]; + if (current == ReverseSolidus) + { + if (!TryReadEscape(json, ref index, out var decoded)) + { + ValidateRemainderIsWellFormedUtf16(json, index); + return json.Length; + } + + CheckPairing(ref pending, decoded, PendingHighSurrogate.Escaped); + } + else if (current == QuotationMark) + { + if (pending != PendingHighSurrogate.None) + { + throw UnpairedSurrogate(); + } + + return index + 1; + } + else + { + CheckPairing(ref pending, current, PendingHighSurrogate.Literal); + index++; + } + } + + return json.Length; + } + + private static bool TryReadEscape(string json, ref int index, out char decoded) + { + decoded = default; + if (index + 1 >= json.Length) + { + return false; + } + + if (json[index + 1] != UnicodeEscapeMarker) + { + decoded = json[index + 1]; + index += 2; + return true; + } + + var hexStart = index + 2; + if (hexStart + UnicodeEscapeHexDigits > json.Length) + { + return false; + } + + var codeUnit = 0; + for (var offset = 0; offset < UnicodeEscapeHexDigits; offset++) + { + var digit = HexDigitValue(json[hexStart + offset]); + if (digit < 0) + { + return false; + } + + codeUnit = codeUnit * HexRadix + digit; + } + + decoded = (char)codeUnit; + index = hexStart + UnicodeEscapeHexDigits; + return true; + } + + private static void CheckPairing( + ref PendingHighSurrogate pending, char codeUnit, PendingHighSurrogate representation) + { + if (pending != PendingHighSurrogate.None) + { + if (!char.IsLowSurrogate(codeUnit) || pending != representation) + { + throw UnpairedSurrogate(); + } + + pending = PendingHighSurrogate.None; + return; + } + + if (char.IsHighSurrogate(codeUnit)) + { + pending = representation; + } + else if (char.IsLowSurrogate(codeUnit)) + { + throw UnpairedSurrogate(); + } + } + + private static void ValidateRemainderIsWellFormedUtf16(string json, int start) + { + var index = start; + while (index < json.Length) + { + var current = json[index]; + if (char.IsHighSurrogate(current) + && index + 1 < json.Length + && char.IsLowSurrogate(json[index + 1])) + { + index += 2; + } + else if (char.IsSurrogate(current)) + { + throw MalformedJson(); + } + else + { + index++; + } + } + } + + private static int HexDigitValue(char character) => character switch + { + >= '0' and <= '9' => character - '0', + >= 'a' and <= 'f' => character - 'a' + 10, + >= 'A' and <= 'F' => character - 'A' + 10, + _ => -1 + }; + + private static JcsException UnpairedSurrogate() => + new("Unpaired UTF-16 surrogate in string data (RFC 8785 section 3.2.2.2)."); + + private static JcsException MalformedJson() => + new("Malformed JSON: the text is not well formed UTF-16."); +} diff --git a/ports/dotnet/src/WorldCut/Vendored/JcsNet/LICENSE b/ports/dotnet/src/WorldCut/Vendored/JcsNet/LICENSE new file mode 100644 index 0000000..faf1306 --- /dev/null +++ b/ports/dotnet/src/WorldCut/Vendored/JcsNet/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Israel Iyonsi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ports/dotnet/src/WorldCut/VerificationInputValidator.cs b/ports/dotnet/src/WorldCut/VerificationInputValidator.cs new file mode 100644 index 0000000..48fb98c --- /dev/null +++ b/ports/dotnet/src/WorldCut/VerificationInputValidator.cs @@ -0,0 +1,509 @@ +using WorldCut.Json; +using WorldCut.Model; + +namespace WorldCut; + +/// +/// Enforces every WorldCut 0.1 runtime invariant on a parsed JSON document. +/// +/// +/// Transport shape is also described by the published JSON Schemas, but the +/// invariants enforced here — unique roles, interval ordering, observation +/// timing, cost bounds, and the closed field sets — are what the invalid +/// conformance vectors lock down. +/// +internal static class VerificationInputValidator +{ + private static readonly string[] InputKeys = ["protocolVersion", "contract", "observations"]; + + private static readonly string[] ContractKeys = + ["id", "version", "decisionTime", "assumptions", "requirements"]; + + private static readonly string[] AssumptionKeys = ["clockModel", "intervalModel", "metadataModel"]; + + private static readonly string[] ObservationKeys = + ["id", "role", "resource", "value", "observedAt", "acquisitionCost", "witness"]; + + private static readonly string[] WitnessKeys = ["provenance", "version", "validity", "dependencies"]; + + private static readonly string[] DependencyKeys = + ["name", "resource", "relation", "version", "provenance"]; + + private static readonly string[] DependencyRequiredKeys = ["name", "resource", "relation", "provenance"]; + + private static readonly string[] ResourceKeys = ["provider", "account", "kind", "key"]; + + private static readonly string[] IntervalKeys = ["from", "until"]; + + private static readonly string[] RequirementBaseKeys = ["id", "description", "type"]; + + private static readonly string[] DependencyRequirementKeys = + ["id", "description", "required", "type", "dependentRole", "targetRole", "dependencyName"]; + + private static readonly string[] CommonValidTimeRequirementKeys = + ["id", "description", "required", "type", "roles", "within"]; + + private static readonly string[] ValueEqualsRequirementKeys = + ["id", "description", "required", "type", "role", "path", "expected"]; + + internal static ParsedVerificationInput Validate(JsonValue root) + { + JsonValue input = RequireObject(root, "input"); + RequireExactKeys(input, InputKeys, "input"); + RequireKeys(input, InputKeys, "input"); + + JsonValue protocolVersion = Property(input, "protocolVersion"); + if (protocolVersion.Kind != JsonKind.String + || !string.Equals(protocolVersion.GetString(), WorldCutProtocol.ProtocolVersion, StringComparison.Ordinal)) + { + throw WorldCutException.InvalidInput("input.protocolVersion must equal 0.1"); + } + + DecisionContract contract = ReadContract(Property(input, "contract")); + JsonValue observationValues = RequireArray(Property(input, "observations"), "observations"); + + var observations = new List(observationValues.Items.Count); + var identifiers = new HashSet(StringComparer.Ordinal); + var roles = new HashSet(StringComparer.Ordinal); + + foreach (JsonValue observationValue in observationValues.Items) + { + Observation observation = ReadObservation(observationValue); + if (!identifiers.Add(observation.Id)) + { + throw WorldCutException.InvalidInput($"Duplicate observation id: {observation.Id}"); + } + + if (!roles.Add(observation.Role)) + { + throw WorldCutException.InvalidInput($"Duplicate observation role: {observation.Role}"); + } + + if (observation.ObservedAt > contract.DecisionTime) + { + throw WorldCutException.InvalidInput( + $"{observation.Role}.observedAt must not be after contract.decisionTime"); + } + + observations.Add(observation); + } + + return new ParsedVerificationInput( + WorldCutProtocol.ProtocolVersion, + contract, + observations.ToArray()); + } + + private static DecisionContract ReadContract(JsonValue value) + { + JsonValue contract = RequireObject(value, "contract"); + RequireExactKeys(contract, ContractKeys, "contract"); + RequireKeys(contract, ContractKeys, "contract"); + + string id = NonEmptyString(Property(contract, "id"), "contract.id"); + string version = NonEmptyString(Property(contract, "version"), "contract.version"); + NormalizedTimestamp decisionTime = Timestamp(Property(contract, "decisionTime"), "contract.decisionTime"); + + JsonValue assumptions = RequireObject(Property(contract, "assumptions"), "contract.assumptions"); + RequireExactKeys(assumptions, AssumptionKeys, "contract.assumptions"); + RequireKeys(assumptions, AssumptionKeys, "contract.assumptions"); + if (!IsStringEqual(Property(assumptions, "clockModel"), "trusted_normalized") + || !IsStringEqual(Property(assumptions, "intervalModel"), "half_open") + || !IsStringEqual(Property(assumptions, "metadataModel"), "honest_but_possibly_incomplete")) + { + throw WorldCutException.InvalidInput("contract assumptions are not supported by this engine"); + } + + JsonValue requirementValues = RequireArray(Property(contract, "requirements"), "contract.requirements"); + var requirements = new List(requirementValues.Items.Count); + var identifiers = new HashSet(StringComparer.Ordinal); + int requiredCount = 0; + + foreach (JsonValue requirementValue in requirementValues.Items) + { + ContractRequirement requirement = ReadRequirement(requirementValue); + if (!identifiers.Add(requirement.Id)) + { + throw WorldCutException.InvalidInput($"Duplicate requirement id: {requirement.Id}"); + } + + if (requirement.Required) + { + requiredCount++; + } + + requirements.Add(requirement); + } + + if (requiredCount == 0) + { + throw WorldCutException.InvalidInput( + "A decision contract must contain at least one required requirement"); + } + + return new DecisionContract(id, version, decisionTime, requirements.ToArray(), contract); + } + + private static ContractRequirement ReadRequirement(JsonValue value) + { + JsonValue requirement = RequireObject(value, "requirement"); + RequireKeys(requirement, RequirementBaseKeys, "requirement"); + + string id = NonEmptyString(Property(requirement, "id"), "requirement.id"); + string description = NonEmptyString(Property(requirement, "description"), $"{id}.description"); + bool required = ReadRequiredFlag(requirement, id); + + JsonValue typeValue = Property(requirement, "type"); + string type = typeValue.Kind == JsonKind.String ? typeValue.GetString() : string.Empty; + + switch (type) + { + case "dependency": + RequireExactKeys(requirement, DependencyRequirementKeys, id); + RequireKeys(requirement, ["dependentRole", "targetRole", "dependencyName"], id); + return new DependencyRequirement( + id, + description, + required, + requirement, + NonEmptyString(Property(requirement, "dependentRole"), $"{id}.dependentRole"), + NonEmptyString(Property(requirement, "targetRole"), $"{id}.targetRole"), + NonEmptyString(Property(requirement, "dependencyName"), $"{id}.dependencyName")); + + case "common_valid_time": + RequireExactKeys(requirement, CommonValidTimeRequirementKeys, id); + RequireKeys(requirement, ["roles", "within"], id); + return new CommonValidTimeRequirement( + id, + description, + required, + requirement, + ReadRoles(Property(requirement, "roles"), id), + Interval(Property(requirement, "within"), $"{id}.within")); + + case "value_equals": + RequireExactKeys(requirement, ValueEqualsRequirementKeys, id); + RequireKeys(requirement, ["role", "path", "expected"], id); + return new ValueEqualsRequirement( + id, + description, + required, + requirement, + NonEmptyString(Property(requirement, "role"), $"{id}.role"), + ReadPath(Property(requirement, "path"), id), + Property(requirement, "expected")); + + default: + throw WorldCutException.InvalidInput($"Unsupported requirement type: {Describe(typeValue)}"); + } + } + + private static string[] ReadRoles(JsonValue value, string requirementId) + { + JsonValue roleValues = RequireArray(value, $"{requirementId}.roles"); + if (roleValues.Items.Count < 2) + { + throw WorldCutException.InvalidInput($"{requirementId} must reference at least two roles"); + } + + var roles = new List(roleValues.Items.Count); + var seen = new HashSet(StringComparer.Ordinal); + foreach (JsonValue roleValue in roleValues.Items) + { + string role = NonEmptyString(roleValue, $"{requirementId}.role"); + if (!seen.Add(role)) + { + throw WorldCutException.InvalidInput($"{requirementId} contains duplicate role {role}"); + } + + roles.Add(role); + } + + return roles.ToArray(); + } + + private static string[] ReadPath(JsonValue value, string requirementId) + { + JsonValue pathValues = RequireArray(value, $"{requirementId}.path"); + if (pathValues.Items.Count == 0) + { + throw WorldCutException.InvalidInput($"{requirementId}.path must contain at least one segment"); + } + + var path = new List(pathValues.Items.Count); + foreach (JsonValue segment in pathValues.Items) + { + path.Add(NonEmptyString(segment, $"{requirementId}.path segment")); + } + + return path.ToArray(); + } + + private static Observation ReadObservation(JsonValue value) + { + JsonValue observation = RequireObject(value, "observation"); + RequireExactKeys(observation, ObservationKeys, "observation"); + RequireKeys(observation, ObservationKeys, "observation"); + + string id = NonEmptyString(Property(observation, "id"), "observation.id"); + string role = NonEmptyString(Property(observation, "role"), "observation.role"); + ResourceIdentity resource = Resource(Property(observation, "resource"), $"{role}.resource"); + NormalizedTimestamp observedAt = Timestamp(Property(observation, "observedAt"), $"{role}.observedAt"); + long acquisitionCost = AcquisitionCost(Property(observation, "acquisitionCost"), role); + ObservationWitness witness = ReadWitness(Property(observation, "witness"), role); + + return new Observation( + id, + role, + resource, + Property(observation, "value"), + observedAt, + acquisitionCost, + witness, + observation); + } + + private static ObservationWitness ReadWitness(JsonValue value, string role) + { + string field = $"{role}.witness"; + JsonValue witness = RequireObject(value, field); + RequireExactKeys(witness, WitnessKeys, field); + RequireKeys(witness, ["provenance"], field); + + WitnessProvenance provenance = Provenance(Property(witness, "provenance"), $"{field}.provenance"); + + string? version = witness.TryGetProperty("version", out JsonValue? versionValue) + ? NonEmptyString(versionValue, $"{field}.version") + : null; + + ValidityInterval? validity = witness.TryGetProperty("validity", out JsonValue? validityValue) + ? Interval(validityValue, $"{field}.validity") + : null; + + DependencyWitness[] dependencies = Array.Empty(); + if (witness.TryGetProperty("dependencies", out JsonValue? dependencyValues)) + { + JsonValue array = RequireArray(dependencyValues, $"{field}.dependencies"); + var declared = new List(array.Items.Count); + var names = new HashSet(StringComparer.Ordinal); + foreach (JsonValue dependencyValue in array.Items) + { + DependencyWitness dependency = ReadDependency(dependencyValue, role); + if (!names.Add(dependency.Name)) + { + throw WorldCutException.InvalidInput( + $"Duplicate dependency {dependency.Name} on role {role}"); + } + + declared.Add(dependency); + } + + dependencies = declared.ToArray(); + } + + return new ObservationWitness(provenance, version, validity, dependencies); + } + + private static DependencyWitness ReadDependency(JsonValue value, string role) + { + string field = $"{role}.dependency"; + JsonValue dependency = RequireObject(value, field); + RequireExactKeys(dependency, DependencyKeys, field); + RequireKeys(dependency, DependencyRequiredKeys, field); + + string name = NonEmptyString(Property(dependency, "name"), "dependency.name"); + ResourceIdentity resource = Resource( + Property(dependency, "resource"), + $"{role}.dependency.{name}.resource"); + + if (!IsStringEqual(Property(dependency, "relation"), "exact")) + { + throw WorldCutException.InvalidInput($"{role}.dependency.{name}.relation is unsupported"); + } + + string? version = dependency.TryGetProperty("version", out JsonValue? versionValue) + ? NonEmptyString(versionValue, $"{role}.dependency.{name}.version") + : null; + + WitnessProvenance provenance = Provenance( + Property(dependency, "provenance"), + $"{role}.dependency.{name}.provenance"); + + return new DependencyWitness(name, resource, version, provenance); + } + + private static bool ReadRequiredFlag(JsonValue requirement, string id) + { + if (!requirement.TryGetProperty("required", out JsonValue? value)) + { + return true; + } + + if (value.Kind != JsonKind.Boolean) + { + throw WorldCutException.InvalidInput($"{id}.required must be boolean"); + } + + return value.GetBoolean(); + } + + private static ResourceIdentity Resource(JsonValue value, string field) + { + JsonValue resource = RequireObject(value, field); + RequireExactKeys(resource, ResourceKeys, field); + RequireKeys(resource, ResourceKeys, field); + return new ResourceIdentity( + NonEmptyString(Property(resource, "provider"), $"{field}.provider"), + NonEmptyString(Property(resource, "account"), $"{field}.account"), + NonEmptyString(Property(resource, "kind"), $"{field}.kind"), + NonEmptyString(Property(resource, "key"), $"{field}.key")); + } + + private static ValidityInterval Interval(JsonValue value, string field) + { + JsonValue interval = RequireObject(value, field); + RequireExactKeys(interval, IntervalKeys, field); + RequireKeys(interval, IntervalKeys, field); + + NormalizedTimestamp from = Timestamp(Property(interval, "from"), $"{field}.from"); + JsonValue untilValue = Property(interval, "until"); + if (untilValue.IsNull) + { + return new ValidityInterval(from, null); + } + + NormalizedTimestamp until = Timestamp(untilValue, $"{field}.until"); + if (until <= from) + { + throw WorldCutException.InvalidInput($"{field} must be a non-empty half-open interval"); + } + + return new ValidityInterval(from, until); + } + + private static WitnessProvenance Provenance(JsonValue value, string field) => + NonEmptyString(value, field) switch + { + "provider_asserted" => WitnessProvenance.ProviderAsserted, + "client_observed" => WitnessProvenance.ClientObserved, + "derived" => WitnessProvenance.Derived, + "operator_supplied" => WitnessProvenance.OperatorSupplied, + _ => throw WorldCutException.InvalidInput($"{field} is not a supported provenance category"), + }; + + private static long AcquisitionCost(JsonValue value, string role) + { + string message = + $"{role}.acquisitionCost must be an integer between 0 and {WorldCutProtocol.MaxAcquisitionCost}"; + + if (value.Kind != JsonKind.Number) + { + throw WorldCutException.InvalidInput(message); + } + + double cost = value.GetNumber(); + if (cost != Math.Floor(cost) + || cost < 0 + || cost > WorldCutProtocol.MaxAcquisitionCost + || Math.Abs(cost) > JsonValue.MaxSafeInteger) + { + throw WorldCutException.InvalidInput(message); + } + + return (long)cost; + } + + private static NormalizedTimestamp Timestamp(JsonValue value, string field) + { + string text = NonEmptyString(value, field); + if (!NormalizedTimestamp.TryParse(text, out NormalizedTimestamp timestamp)) + { + throw WorldCutException.InvalidInput( + $"{field} must use normalized ISO-8601 UTC form with milliseconds"); + } + + return timestamp; + } + + private static string NonEmptyString(JsonValue value, string field) + { + if (value.Kind != JsonKind.String || value.GetString().Length == 0) + { + throw WorldCutException.InvalidInput($"{field} must be a non-empty string"); + } + + return value.GetString(); + } + + private static bool IsStringEqual(JsonValue value, string expected) => + value.Kind == JsonKind.String && string.Equals(value.GetString(), expected, StringComparison.Ordinal); + + private static JsonValue RequireObject(JsonValue value, string field) => + value.Kind == JsonKind.Object + ? value + : throw WorldCutException.InvalidInput($"{field} must be a plain object"); + + private static JsonValue RequireArray(JsonValue value, string field) => + value.Kind == JsonKind.Array + ? value + : throw WorldCutException.InvalidInput($"{field} must be an array"); + + private static JsonValue Property(JsonValue container, string name) => + container.TryGetProperty(name, out JsonValue? value) ? value : JsonValue.Null; + + private static void RequireExactKeys(JsonValue container, IReadOnlyList allowed, string field) + { + List? unsupported = null; + foreach (KeyValuePair member in container.Members) + { + if (!Contains(allowed, member.Key)) + { + (unsupported ??= []).Add(member.Key); + } + } + + if (unsupported is not null) + { + throw WorldCutException.InvalidInput( + $"{field} contains unsupported field(s): {string.Join(", ", unsupported)}"); + } + } + + private static void RequireKeys(JsonValue container, IReadOnlyList required, string field) + { + List? missing = null; + foreach (string key in required) + { + if (!container.TryGetProperty(key, out _)) + { + (missing ??= []).Add(key); + } + } + + if (missing is not null) + { + throw WorldCutException.InvalidInput( + $"{field} is missing required field(s): {string.Join(", ", missing)}"); + } + } + + private static bool Contains(IReadOnlyList values, string candidate) + { + foreach (string value in values) + { + if (string.Equals(value, candidate, StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + private static string Describe(JsonValue value) => value.Kind switch + { + JsonKind.String => value.GetString(), + JsonKind.Null => "null", + _ => value.Kind.ToString(), + }; +} diff --git a/ports/dotnet/src/WorldCut/VerificationResult.cs b/ports/dotnet/src/WorldCut/VerificationResult.cs new file mode 100644 index 0000000..b06d456 --- /dev/null +++ b/ports/dotnet/src/WorldCut/VerificationResult.cs @@ -0,0 +1,397 @@ +using WorldCut.Json; + +namespace WorldCut; + +/// The status of one evaluated requirement. +public enum RequirementStatus +{ + /// Available metadata establishes the requirement. + Satisfied = 0, + + /// Available metadata establishes that the requirement is false. + Violated = 1, + + /// A required observation or witness is missing. + Unknown = 2, +} + +/// The aggregate verdict for one verification. +public enum ContractVerdict +{ + /// Every required requirement is satisfied. + ContractSatisfied = 0, + + /// At least one required requirement is violated. + ContractViolated = 1, + + /// No required requirement is violated, but evidence is missing. + InsufficientEvidence = 2, +} + +/// The kinds of acquisition action a plan can contain. +public enum AcquisitionActionType +{ + /// Acquire or refresh an observation for a role. + RefreshObservation = 0, + + /// Fetch missing witness metadata for an existing observation. + FetchRequiredMetadata = 1, + + /// Acquire evidence compatible with a selected resource or version. + AcquireCompatibleEvidence = 2, +} + +/// The state of an acquisition plan. +public enum AcquisitionPlanStatus +{ + /// No required requirement needs additional evidence. + NotNeeded = 0, + + /// An exact minimum-cost plan covers every unresolved requirement. + Available = 1, + + /// Exact optimality could not be established for every requirement. + Incomplete = 2, +} + +/// Wire spellings for the result enumerations. +public static class ResultNames +{ + /// Returns the wire spelling of a requirement status. + /// The status. + /// The wire spelling. + /// is not defined. + public static string ToWireName(this RequirementStatus status) => status switch + { + RequirementStatus.Satisfied => "SATISFIED", + RequirementStatus.Violated => "VIOLATED", + RequirementStatus.Unknown => "UNKNOWN", + _ => throw new ArgumentOutOfRangeException(nameof(status)), + }; + + /// Returns the wire spelling of an aggregate verdict. + /// The verdict. + /// The wire spelling. + /// is not defined. + public static string ToWireName(this ContractVerdict verdict) => verdict switch + { + ContractVerdict.ContractSatisfied => "CONTRACT_SATISFIED", + ContractVerdict.ContractViolated => "CONTRACT_VIOLATED", + ContractVerdict.InsufficientEvidence => "INSUFFICIENT_EVIDENCE", + _ => throw new ArgumentOutOfRangeException(nameof(verdict)), + }; + + /// Returns the wire spelling of an acquisition action type. + /// The action type. + /// The wire spelling. + /// is not defined. + public static string ToWireName(this AcquisitionActionType type) => type switch + { + AcquisitionActionType.RefreshObservation => "REFRESH_OBSERVATION", + AcquisitionActionType.FetchRequiredMetadata => "FETCH_REQUIRED_METADATA", + AcquisitionActionType.AcquireCompatibleEvidence => "ACQUIRE_COMPATIBLE_EVIDENCE", + _ => throw new ArgumentOutOfRangeException(nameof(type)), + }; + + /// Returns the wire spelling of an acquisition plan status. + /// The plan status. + /// The wire spelling. + /// is not defined. + public static string ToWireName(this AcquisitionPlanStatus status) => status switch + { + AcquisitionPlanStatus.NotNeeded => "NOT_NEEDED", + AcquisitionPlanStatus.Available => "AVAILABLE", + AcquisitionPlanStatus.Incomplete => "INCOMPLETE", + _ => throw new ArgumentOutOfRangeException(nameof(status)), + }; +} + +/// One concrete evidence-acquisition step. +public sealed class AcquisitionAction +{ + internal AcquisitionAction( + string id, + AcquisitionActionType type, + string role, + long cost, + string description, + JsonValue? expected) + { + Id = id; + Type = type; + Role = role; + Cost = cost; + Description = description; + Expected = expected; + } + + /// The action identifier used to deduplicate work across options. + public string Id { get; } + + /// The action kind. + public AcquisitionActionType Type { get; } + + /// The role the action applies to. + public string Role { get; } + + /// The declared integer cost of performing the action. + public long Cost { get; } + + /// The human-readable action description. + public string Description { get; } + + /// The expected evidence shape, or . + public JsonValue? Expected { get; } + + internal JsonValue ToJson() => JsonValue.CreateObject( + [ + new("id", JsonValue.Create(Id)), + new("type", JsonValue.Create(Type.ToWireName())), + new("role", JsonValue.Create(Role)), + new("cost", JsonValue.Create(Cost)), + new("description", JsonValue.Create(Description)), + new("expected", Expected ?? JsonValue.Null), + ]); +} + +/// One alternative set of conjunctive acquisition actions. +public sealed class AcquisitionOption +{ + internal AcquisitionOption(string id, string description, AcquisitionAction[] actions) + { + Id = id; + Description = description; + Actions = Array.AsReadOnly(actions); + } + + /// The option identifier. + public string Id { get; } + + /// The human-readable option description. + public string Description { get; } + + /// All actions in the option; they are conjunctive. + public IReadOnlyList Actions { get; } + + internal JsonValue ToJson() => JsonValue.CreateObject( + [ + new("id", JsonValue.Create(Id)), + new("description", JsonValue.Create(Description)), + new("actions", JsonValue.CreateArray(Actions.Select(action => action.ToJson()))), + ]); +} + +/// The result of evaluating one contract requirement. +public sealed class RequirementResult +{ + internal RequirementResult( + string requirementId, + string requirementType, + bool required, + RequirementStatus status, + string summary, + JsonValue details, + AcquisitionOption[] acquisitionOptions) + { + RequirementId = requirementId; + RequirementType = requirementType; + Required = required; + Status = status; + Summary = summary; + Details = details; + AcquisitionOptions = Array.AsReadOnly(acquisitionOptions); + } + + /// The evaluated requirement identifier. + public string RequirementId { get; } + + /// The wire spelling of the requirement kind. + public string RequirementType { get; } + + /// Whether the requirement affects the aggregate verdict. + public bool Required { get; } + + /// The requirement status. + public RequirementStatus Status { get; } + + /// The human-readable explanation, which is part of the record contract. + public string Summary { get; } + + /// Structured detail explaining the status. + public JsonValue Details { get; } + + /// The alternative acquisition options for this requirement. + public IReadOnlyList AcquisitionOptions { get; } + + internal JsonValue ToJson() => JsonValue.CreateObject( + [ + new("requirementId", JsonValue.Create(RequirementId)), + new("requirementType", JsonValue.Create(RequirementType)), + new("required", JsonValue.Create(Required)), + new("status", JsonValue.Create(Status.ToWireName())), + new("summary", JsonValue.Create(Summary)), + new("details", Details), + new("acquisitionOptions", JsonValue.CreateArray(AcquisitionOptions.Select(option => option.ToJson()))), + ]); +} + +/// The bounded evidence-acquisition plan for one verification. +public sealed class AcquisitionPlan +{ + internal AcquisitionPlan( + AcquisitionPlanStatus status, + string? reason, + AcquisitionAction[] actions, + string[] selectedOptionIds, + long totalCost, + string[] coveredRequirementIds, + string[] unresolvedRequirementIds) + { + Status = status; + Reason = reason; + Actions = Array.AsReadOnly(actions); + SelectedOptionIds = Array.AsReadOnly(selectedOptionIds); + TotalCost = totalCost; + CoveredRequirementIds = Array.AsReadOnly(coveredRequirementIds); + UnresolvedRequirementIds = Array.AsReadOnly(unresolvedRequirementIds); + } + + /// The plan status. + public AcquisitionPlanStatus Status { get; } + + /// Why the plan is incomplete, or . + public string? Reason { get; } + + /// The distinct selected actions, ordered by action identifier. + public IReadOnlyList Actions { get; } + + /// The selected option identifiers, ordered by UTF-16 code units. + public IReadOnlyList SelectedOptionIds { get; } + + /// The sum of the distinct action costs. + public long TotalCost { get; } + + /// Requirements that have at least one acquisition option. + public IReadOnlyList CoveredRequirementIds { get; } + + /// Requirements that could not be covered. + public IReadOnlyList UnresolvedRequirementIds { get; } + + internal JsonValue ToJson() => JsonValue.CreateObject( + [ + new("status", JsonValue.Create(Status.ToWireName())), + new("reason", Reason is null ? JsonValue.Null : JsonValue.Create(Reason)), + new("actions", JsonValue.CreateArray(Actions.Select(action => action.ToJson()))), + new("selectedOptionIds", JsonValue.CreateArray(SelectedOptionIds.Select(JsonValue.Create))), + new("totalCost", JsonValue.Create(TotalCost)), + new("coveredRequirementIds", JsonValue.CreateArray(CoveredRequirementIds.Select(JsonValue.Create))), + new("unresolvedRequirementIds", JsonValue.CreateArray(UnresolvedRequirementIds.Select(JsonValue.Create))), + ]); +} + +/// Requirement counts for one verification. +public sealed class VerificationCoverage +{ + internal VerificationCoverage(int required, int satisfied, int violated, int unknown, int advisory) + { + Required = required; + Satisfied = satisfied; + Violated = violated; + Unknown = unknown; + Advisory = advisory; + } + + /// The number of required requirement results. + public int Required { get; } + + /// Required results with status SATISFIED. + public int Satisfied { get; } + + /// Required results with status VIOLATED. + public int Violated { get; } + + /// Required results with status UNKNOWN. + public int Unknown { get; } + + /// The number of advisory requirement results. + public int Advisory { get; } + + internal JsonValue ToJson() => JsonValue.CreateObject( + [ + new("required", JsonValue.Create(Required)), + new("satisfied", JsonValue.Create(Satisfied)), + new("violated", JsonValue.Create(Violated)), + new("unknown", JsonValue.Create(Unknown)), + new("advisory", JsonValue.Create(Advisory)), + ]); +} + +/// The complete, immutable outcome of one verification. +public sealed class VerificationResult +{ + internal VerificationResult( + string protocolVersion, + string contractId, + string contractVersion, + ContractVerdict verdict, + VerificationCoverage coverage, + RequirementResult[] requirementResults, + AcquisitionPlan acquisitionPlan, + string verificationRecordDigest) + { + ProtocolVersion = protocolVersion; + ContractId = contractId; + ContractVersion = contractVersion; + Verdict = verdict; + Coverage = coverage; + RequirementResults = Array.AsReadOnly(requirementResults); + AcquisitionPlan = acquisitionPlan; + VerificationRecordDigest = verificationRecordDigest; + } + + /// The protocol version echoed from the input. + public string ProtocolVersion { get; } + + /// The engine ruleset that produced this result. + public string EngineVersion { get; } = WorldCutProtocol.EngineVersion; + + /// The canonicalization scheme used for the digest. + public string Canonicalization { get; } = WorldCutProtocol.Canonicalization; + + /// The verified contract identifier. + public string ContractId { get; } + + /// The verified contract version. + public string ContractVersion { get; } + + /// The aggregate verdict. + public ContractVerdict Verdict { get; } + + /// Requirement counts. + public VerificationCoverage Coverage { get; } + + /// Requirement results, ordered by requirement identifier. + public IReadOnlyList RequirementResults { get; } + + /// The bounded acquisition plan. + public AcquisitionPlan AcquisitionPlan { get; } + + /// The SHA-256 digest of the canonical verification record. + public string VerificationRecordDigest { get; } + + /// Returns the complete result as protocol JSON. + /// An immutable JSON object matching the published result schema. + public JsonValue ToJson() => JsonValue.CreateObject( + [ + new("protocolVersion", JsonValue.Create(ProtocolVersion)), + new("engineVersion", JsonValue.Create(EngineVersion)), + new("canonicalization", JsonValue.Create(Canonicalization)), + new("contractId", JsonValue.Create(ContractId)), + new("contractVersion", JsonValue.Create(ContractVersion)), + new("verdict", JsonValue.Create(Verdict.ToWireName())), + new("coverage", Coverage.ToJson()), + new("requirementResults", JsonValue.CreateArray(RequirementResults.Select(result => result.ToJson()))), + new("acquisitionPlan", AcquisitionPlan.ToJson()), + new("verificationRecordDigest", JsonValue.Create(VerificationRecordDigest)), + ]); +} diff --git a/ports/dotnet/src/WorldCut/WorldCut.csproj b/ports/dotnet/src/WorldCut/WorldCut.csproj new file mode 100644 index 0000000..a1baebb --- /dev/null +++ b/ports/dotnet/src/WorldCut/WorldCut.csproj @@ -0,0 +1,27 @@ + + + + net8.0;net10.0 + WorldCut + WorldCut + true + + + + WorldCut + WorldCut + Independent .NET implementation of the WorldCut decision-coherence protocol 0.1: verify that observations from independent systems satisfy explicit version and temporal requirements, with deterministic RFC 8785 canonicalization and verification-record digests. No third-party package dependencies. + verification;consistency;distributed-systems;provenance;canonicalization;rfc8785;jcs;determinism + First release of the independent .NET port of WorldCut protocol 0.1 and engine ruleset 0.1.2. + + + + + + + + + + + + diff --git a/ports/dotnet/src/WorldCut/WorldCutErrorCode.cs b/ports/dotnet/src/WorldCut/WorldCutErrorCode.cs new file mode 100644 index 0000000..308438a --- /dev/null +++ b/ports/dotnet/src/WorldCut/WorldCutErrorCode.cs @@ -0,0 +1,62 @@ +namespace WorldCut; + +/// +/// Stable failure categories reported by the WorldCut .NET port. +/// +/// +/// The wire spelling of each member is defined by +/// and is part of this port's public contract. +/// +public enum WorldCutErrorCode +{ + /// + /// Transport bytes, JSON syntax, or protocol invariants were rejected. + /// + /// + /// Like the Go and Python ports, JSON syntax failures are reported with + /// this code rather than a separate parse code. + /// + InvalidInput = 0, + + /// Command-line arguments were rejected. + InvalidArgument = 1, + + /// A verification input file could not be read. + FileReadFailed = 2, + + /// An unexpected internal failure occurred. + RuntimeError = 3, +} + +/// +/// The stable wire spellings of . +/// +public static class WorldCutErrorCodes +{ + /// Wire code for . + public const string InvalidInput = "WORLDCUT_INVALID_INPUT"; + + /// Wire code for . + public const string InvalidArgument = "WORLDCUT_INVALID_ARGUMENT"; + + /// Wire code for . + public const string FileReadFailed = "WORLDCUT_FILE_READ_FAILED"; + + /// Wire code for . + public const string RuntimeError = "WORLDCUT_RUNTIME_ERROR"; + + /// Returns the stable wire spelling of . + /// The error category. + /// The wire code string. + /// + /// is not a defined member. + /// + public static string ToWireCode(this WorldCutErrorCode code) => code switch + { + WorldCutErrorCode.InvalidInput => InvalidInput, + WorldCutErrorCode.InvalidArgument => InvalidArgument, + WorldCutErrorCode.FileReadFailed => FileReadFailed, + WorldCutErrorCode.RuntimeError => RuntimeError, + _ => throw new ArgumentOutOfRangeException(nameof(code)), + }; +} diff --git a/ports/dotnet/src/WorldCut/WorldCutException.cs b/ports/dotnet/src/WorldCut/WorldCutException.cs new file mode 100644 index 0000000..b79d5fc --- /dev/null +++ b/ports/dotnet/src/WorldCut/WorldCutException.cs @@ -0,0 +1,63 @@ +namespace WorldCut; + +/// +/// The single exception type raised by the WorldCut public API. +/// +/// +/// Every failure carries a stable . Callers that surface +/// failures to another process should emit . +/// +public sealed class WorldCutException : Exception +{ + /// Creates a runtime-category exception with a default message. + public WorldCutException() + : this(WorldCutErrorCode.RuntimeError, "WorldCut failed.") + { + } + + /// Creates a runtime-category exception. + /// The failure description. + public WorldCutException(string message) + : this(WorldCutErrorCode.RuntimeError, message) + { + } + + /// Creates a runtime-category exception with an inner cause. + /// The failure description. + /// The underlying failure. + public WorldCutException(string message, Exception? innerException) + : this(WorldCutErrorCode.RuntimeError, message, innerException) + { + } + + /// Creates an exception in an explicit failure category. + /// The stable failure category. + /// The failure description. + public WorldCutException(WorldCutErrorCode code, string message) + : base(message) + { + Code = code; + } + + /// Creates an exception in an explicit failure category. + /// The stable failure category. + /// The failure description. + /// The underlying failure. + public WorldCutException(WorldCutErrorCode code, string message, Exception? innerException) + : base(message, innerException) + { + Code = code; + } + + /// The stable failure category. + public WorldCutErrorCode Code { get; } + + /// The stable wire spelling of . + public string WireCode => Code.ToWireCode(); + + internal static WorldCutException InvalidInput(string message) => + new(WorldCutErrorCode.InvalidInput, message); + + internal static WorldCutException InvalidInput(string message, Exception? innerException) => + new(WorldCutErrorCode.InvalidInput, message, innerException); +} diff --git a/ports/dotnet/src/WorldCut/WorldCutProtocol.cs b/ports/dotnet/src/WorldCut/WorldCutProtocol.cs new file mode 100644 index 0000000..8fe7d27 --- /dev/null +++ b/ports/dotnet/src/WorldCut/WorldCutProtocol.cs @@ -0,0 +1,58 @@ +namespace WorldCut; + +/// +/// Stable protocol identifiers and bounded limits implemented by this port. +/// +/// +/// Package versions and protocol versions are independent. A package patch may +/// keep protocol 0.1 and engine 0.1.2 when verification semantics +/// are unchanged. +/// +public static class WorldCutProtocol +{ + /// The only wire protocol version this port accepts. + public const string ProtocolVersion = "0.1"; + + /// The engine ruleset that produces verification results. + public const string EngineVersion = "0.1.2"; + + /// The canonicalization scheme used for every digest. + public const string Canonicalization = "worldcut-json-v1"; + + /// The highest acquisition cost a single observation may declare. + public const long MaxAcquisitionCost = 1_000_000_000L; + + /// The highest total cost an acquisition plan may reach. + public const long MaxPlanTotalCost = 64_000_000_000L; + + /// The highest number of unresolved requirements the planner will optimise. + public const int MaxUnresolvedRequirements = 64; + + /// The highest number of option combinations the planner will enumerate. + public const int MaxOptionCombinations = 65_536; + + /// The defensive search-state limit for acquisition planning. + public const int MaxSearchStates = (MaxUnresolvedRequirements + 1) * MaxOptionCombinations; + + /// + /// The deepest JSON nesting this port will parse from transport input. + /// + /// + /// This is an explicit, stable port policy rather than a protocol rule. A + /// verification record wraps input values in up to eight further levels, so + /// keeping the parse limit below + /// guarantees that every accepted input can also be canonicalized. Deeper + /// input is rejected with + /// instead of exhausting the stack. + /// + public const int MaxJsonDepth = 48; + + /// + /// The deepest JSON nesting will serialize. + /// + /// + /// This matches the nesting cap of the vendored RFC 8785 canonicalizer. See + /// ports/dotnet/THIRD-PARTY-NOTICES.md. + /// + public const int MaxCanonicalizationDepth = 64; +} diff --git a/ports/dotnet/src/WorldCut/WorldCutVerifier.cs b/ports/dotnet/src/WorldCut/WorldCutVerifier.cs new file mode 100644 index 0000000..9e1036f --- /dev/null +++ b/ports/dotnet/src/WorldCut/WorldCutVerifier.cs @@ -0,0 +1,156 @@ +using WorldCut.Engine; +using WorldCut.Json; +using WorldCut.Model; + +namespace WorldCut; + +/// +/// The WorldCut 0.1 decision-coherence verifier. +/// +/// +/// Verification is deterministic and side-effect free. It never fetches +/// evidence, infers missing relationships, or judges whether a provider is +/// truthful; it evaluates the supplied contract against the supplied +/// observations and fails closed when required evidence is absent. +/// +public static class WorldCutVerifier +{ + /// Verifies a validated input. + /// The parsed verification input. + /// A fully immutable verification result. + /// is . + /// The input cannot be verified. + public static VerificationResult Verify(ParsedVerificationInput input) + { + ArgumentNullException.ThrowIfNull(input); + + var observationsByRole = new Dictionary( + input.Observations.Count, + StringComparer.Ordinal); + foreach (Observation observation in input.Observations) + { + observationsByRole.Add(observation.Role, observation); + } + + IReadOnlyList requirements = input.RequirementsById(); + var results = new RequirementResult[requirements.Count]; + for (int index = 0; index < requirements.Count; index++) + { + results[index] = RequirementEvaluator.Evaluate(requirements[index], observationsByRole); + } + + int satisfied = 0; + int violated = 0; + int unknown = 0; + int advisory = 0; + foreach (RequirementResult result in results) + { + if (!result.Required) + { + advisory++; + continue; + } + + switch (result.Status) + { + case RequirementStatus.Satisfied: + satisfied++; + break; + case RequirementStatus.Violated: + violated++; + break; + default: + unknown++; + break; + } + } + + ContractVerdict verdict = violated > 0 + ? ContractVerdict.ContractViolated + : unknown > 0 + ? ContractVerdict.InsufficientEvidence + : ContractVerdict.ContractSatisfied; + + AcquisitionPlan plan = AcquisitionPlanner.SelectPlan(results); + var coverage = new VerificationCoverage( + satisfied + violated + unknown, + satisfied, + violated, + unknown, + advisory); + + JsonValue record = BuildRecord(input, requirements, verdict, results, plan); + + return new VerificationResult( + input.ProtocolVersion, + input.Contract.Id, + input.Contract.Version, + verdict, + coverage, + results, + plan, + CanonicalJson.ComputeSha256Hex(record)); + } + + /// Parses, validates, and verifies one verification input. + /// The verification input JSON. + /// A fully immutable verification result. + /// is . + /// The document is not a verifiable input. + public static VerificationResult VerifyJson(string json) => + Verify(ParsedVerificationInput.Parse(json)); + + /// Parses, validates, and verifies one verification input. + /// The UTF-8 encoded verification input JSON. + /// A fully immutable verification result. + /// The bytes are not a verifiable input. + public static VerificationResult VerifyJsonUtf8(ReadOnlySpan utf8Json) => + Verify(ParsedVerificationInput.ParseUtf8(utf8Json)); + + private static JsonValue BuildRecord( + ParsedVerificationInput input, + IReadOnlyList sortedRequirements, + ContractVerdict verdict, + RequirementResult[] results, + AcquisitionPlan plan) + { + var sortedRequirementJson = new JsonValue[sortedRequirements.Count]; + for (int index = 0; index < sortedRequirements.Count; index++) + { + sortedRequirementJson[index] = sortedRequirements[index].Raw; + } + + var contractMembers = new List>(input.Contract.Raw.Members.Count); + foreach (KeyValuePair member in input.Contract.Raw.Members) + { + contractMembers.Add(string.Equals(member.Key, "requirements", StringComparison.Ordinal) + ? new KeyValuePair(member.Key, JsonValue.CreateArrayOwned(sortedRequirementJson)) + : member); + } + + IReadOnlyList sortedObservations = input.ObservationsByRole(); + var observationJson = new JsonValue[sortedObservations.Count]; + for (int index = 0; index < sortedObservations.Count; index++) + { + observationJson[index] = sortedObservations[index].Raw; + } + + var resultJson = new JsonValue[results.Length]; + for (int index = 0; index < results.Length; index++) + { + resultJson[index] = results[index].ToJson(); + } + + return JsonValue.CreateObject( + [ + new("protocolVersion", JsonValue.Create(input.ProtocolVersion)), + new("engineVersion", JsonValue.Create(WorldCutProtocol.EngineVersion)), + new("canonicalization", JsonValue.Create(WorldCutProtocol.Canonicalization)), + new("contract", JsonValue.CreateObject(contractMembers)), + new("observations", JsonValue.CreateArrayOwned(observationJson)), + new("verdict", JsonValue.Create(verdict.ToWireName())), + new("requirementResults", JsonValue.CreateArrayOwned(resultJson)), + new("acquisitionPlan", plan.ToJson()), + ]); + } +} diff --git a/ports/dotnet/src/WorldCut/packages.lock.json b/ports/dotnet/src/WorldCut/packages.lock.json new file mode 100644 index 0000000..83c6fb5 --- /dev/null +++ b/ports/dotnet/src/WorldCut/packages.lock.json @@ -0,0 +1,7 @@ +{ + "version": 2, + "dependencies": { + "net10.0": {}, + "net8.0": {} + } +} \ No newline at end of file diff --git a/ports/dotnet/tests/WorldCut.Tests/AcquisitionPlannerTests.cs b/ports/dotnet/tests/WorldCut.Tests/AcquisitionPlannerTests.cs new file mode 100644 index 0000000..c7565ff --- /dev/null +++ b/ports/dotnet/tests/WorldCut.Tests/AcquisitionPlannerTests.cs @@ -0,0 +1,250 @@ +using WorldCut.Engine; +using WorldCut.Json; + +namespace WorldCut.Tests; + +/// +/// Boundary behaviour of the bounded acquisition planner. +/// +public sealed class AcquisitionPlannerTests +{ + [Fact] + public void Shared_actions_are_deduplicated_and_counted_once() + { + AcquisitionAction shared = Action("shared", 3); + + AcquisitionPlan plan = AcquisitionPlanner.SelectPlan( + [ + Unresolved("r1", [Option("r1-shared", shared), Option("r1-only", Action("r1", 2))]), + Unresolved("r2", [Option("r2-shared", shared), Option("r2-only", Action("r2", 2))]), + ]); + + Assert.Equal(AcquisitionPlanStatus.Available, plan.Status); + Assert.Equal(3, plan.TotalCost); + Assert.Equal(["shared"], plan.Actions.Select(action => action.Id)); + Assert.Equal(["r1-shared", "r2-shared"], plan.SelectedOptionIds); + Assert.Equal(["r1", "r2"], plan.CoveredRequirementIds); + Assert.Empty(plan.UnresolvedRequirementIds); + } + + [Fact] + public void Ties_break_by_action_count_before_option_identifier() + { + AcquisitionPlan plan = AcquisitionPlanner.SelectPlan( + [ + Unresolved("r", + [ + Option("r:b", Action("one", 2)), + Option("r:a", Action("left", 1), Action("right", 1)), + ]), + ]); + + Assert.Equal(["r:b"], plan.SelectedOptionIds); + Assert.Equal(2, plan.TotalCost); + } + + [Fact] + public void Ties_break_by_sorted_option_identifier_last() + { + AcquisitionPlan plan = AcquisitionPlanner.SelectPlan( + [ + Unresolved("r", + [ + Option("r:b", Action("second", 2)), + Option("r:a", Action("first", 2)), + ]), + ]); + + Assert.Equal(["r:a"], plan.SelectedOptionIds); + } + + [Fact] + public void A_satisfied_contract_needs_no_plan() + { + AcquisitionPlan plan = AcquisitionPlanner.SelectPlan( + [ + new RequirementResult( + "r", + "dependency", + true, + RequirementStatus.Satisfied, + "r", + JsonValue.Null, + Array.Empty()), + ]); + + Assert.Equal(AcquisitionPlanStatus.NotNeeded, plan.Status); + Assert.Null(plan.Reason); + Assert.Equal(0, plan.TotalCost); + Assert.Empty(plan.Actions); + Assert.Empty(plan.CoveredRequirementIds); + Assert.Empty(plan.UnresolvedRequirementIds); + } + + [Fact] + public void Advisory_requirements_never_participate() + { + AcquisitionPlan plan = AcquisitionPlanner.SelectPlan( + [ + new RequirementResult( + "advisory", + "value_equals", + false, + RequirementStatus.Unknown, + "advisory", + JsonValue.Null, + [Option("advisory:only", Action("advisory", 9))]), + ]); + + Assert.Equal(AcquisitionPlanStatus.NotNeeded, plan.Status); + } + + [Fact] + public void Requirements_without_options_are_reported_as_unresolved() + { + AcquisitionPlan plan = AcquisitionPlanner.SelectPlan( + [ + Unresolved("covered", [Option("covered:only", Action("a", 4))]), + Unresolved("bare", []), + ]); + + Assert.Equal(AcquisitionPlanStatus.Incomplete, plan.Status); + Assert.Equal("No acquisition option is available for: bare.", plan.Reason); + Assert.Equal(["covered"], plan.CoveredRequirementIds); + Assert.Equal(["bare"], plan.UnresolvedRequirementIds); + Assert.Equal(4, plan.TotalCost); + Assert.Equal(["a"], plan.Actions.Select(action => action.Id)); + } + + [Fact] + public void The_unresolved_requirement_limit_is_exactly_sixty_four() + { + RequirementResult[] atLimit = Enumerate(WorldCutProtocol.MaxUnresolvedRequirements); + RequirementResult[] beyondLimit = Enumerate(WorldCutProtocol.MaxUnresolvedRequirements + 1); + + Assert.Equal(AcquisitionPlanStatus.Available, AcquisitionPlanner.SelectPlan(atLimit).Status); + + AcquisitionPlan plan = AcquisitionPlanner.SelectPlan(beyondLimit); + Assert.Equal(AcquisitionPlanStatus.Incomplete, plan.Status); + Assert.Equal( + "Acquisition planning supports at most 64 unresolved requirements.", + plan.Reason); + Assert.Empty(plan.Actions); + Assert.Empty(plan.SelectedOptionIds); + Assert.Equal(0, plan.TotalCost); + Assert.Equal( + WorldCutProtocol.MaxUnresolvedRequirements + 1, + plan.UnresolvedRequirementIds.Count); + } + + [Fact] + public void The_combination_limit_is_exactly_sixty_five_thousand_five_hundred_and_thirty_six() + { + // 2^16 == 65536 combinations is accepted; 2^17 is not. + Assert.Equal(AcquisitionPlanStatus.Available, AcquisitionPlanner.SelectPlan(Binary(16)).Status); + + AcquisitionPlan plan = AcquisitionPlanner.SelectPlan(Binary(17)); + + Assert.Equal(AcquisitionPlanStatus.Incomplete, plan.Status); + Assert.Equal("Acquisition search exceeds the 65536 combination limit.", plan.Reason); + Assert.Empty(plan.Actions); + Assert.Equal(17, plan.UnresolvedRequirementIds.Count); + } + + [Fact] + public void The_defensive_state_limit_matches_the_specification() => + Assert.Equal(4_259_840, WorldCutProtocol.MaxSearchStates); + + [Fact] + public void An_action_cost_above_the_protocol_bound_is_rejected() + { + WorldCutException error = Assert.Throws(() => AcquisitionPlanner.SelectPlan( + [ + Unresolved("r", [Option("r:only", Action("a", WorldCutProtocol.MaxAcquisitionCost + 1))]), + ])); + + Assert.Equal(WorldCutErrorCode.InvalidInput, error.Code); + } + + [Fact] + public void A_total_plan_cost_above_the_protocol_bound_is_rejected() + { + var results = new List(); + for (int index = 0; index < 65; index++) + { + results.Add(Unresolved( + $"r{index:D2}", + [Option($"r{index:D2}:only", Action($"a{index:D2}", WorldCutProtocol.MaxAcquisitionCost))])); + } + + // 65 requirements exceeds the planning limit, so trim to 64 maximum-cost + // actions and add one more option to the last requirement. + results.RemoveAt(64); + results[63] = Unresolved( + "r63", + [ + Option("r63:only", Action("a63", WorldCutProtocol.MaxAcquisitionCost)), + Option( + "r63:pair", + Action("a63", WorldCutProtocol.MaxAcquisitionCost), + Action("a64", WorldCutProtocol.MaxAcquisitionCost)), + ]); + + WorldCutException error = Assert.Throws( + () => AcquisitionPlanner.SelectPlan(results)); + + Assert.Equal(WorldCutErrorCode.InvalidInput, error.Code); + Assert.Contains("64000000000", error.Message, StringComparison.Ordinal); + } + + [Fact] + public void Zero_cost_actions_are_accepted() + { + AcquisitionPlan plan = AcquisitionPlanner.SelectPlan( + [ + Unresolved("r", [Option("r:free", Action("free", 0))]), + ]); + + Assert.Equal(AcquisitionPlanStatus.Available, plan.Status); + Assert.Equal(0, plan.TotalCost); + Assert.Equal(["free"], plan.Actions.Select(action => action.Id)); + } + + private static RequirementResult[] Enumerate(int count) + { + var results = new RequirementResult[count]; + for (int index = 0; index < count; index++) + { + string id = $"r{index:D3}"; + results[index] = Unresolved(id, [Option($"{id}:only", Action(id, 1))]); + } + + return results; + } + + private static RequirementResult[] Binary(int count) + { + var results = new RequirementResult[count]; + for (int index = 0; index < count; index++) + { + string id = $"r{index:D3}"; + results[index] = Unresolved( + id, + [ + Option($"{id}:a", Action($"{id}-a", 1)), + Option($"{id}:b", Action($"{id}-b", 1)), + ]); + } + + return results; + } + + private static AcquisitionAction Action(string id, long cost) => + new(id, AcquisitionActionType.RefreshObservation, id, cost, id, null); + + private static AcquisitionOption Option(string id, params AcquisitionAction[] actions) => + new(id, id, actions); + + private static RequirementResult Unresolved(string id, AcquisitionOption[] options) => + new(id, "dependency", true, RequirementStatus.Unknown, id, JsonValue.Null, options); +} diff --git a/ports/dotnet/tests/WorldCut.Tests/CanonicalJsonTests.cs b/ports/dotnet/tests/WorldCut.Tests/CanonicalJsonTests.cs new file mode 100644 index 0000000..ecf3e4b --- /dev/null +++ b/ports/dotnet/tests/WorldCut.Tests/CanonicalJsonTests.cs @@ -0,0 +1,188 @@ +using WorldCut.Json; + +namespace WorldCut.Tests; + +/// +/// Focused checks on the worldcut-json-v1 canonicalization rules that +/// the shared vectors only cover by example. +/// +public sealed class CanonicalJsonTests +{ + [Fact] + public void Negative_zero_serializes_as_zero() + { + Assert.Equal("0", CanonicalJson.Serialize(JsonValue.Create(-0.0))); + Assert.Equal("0", CanonicalJson.Serialize(JsonValue.Create(0.0))); + Assert.Equal( + CanonicalJson.ComputeSha256Hex(JsonValue.Create(0.0)), + CanonicalJson.ComputeSha256Hex(JsonValue.Create(-0.0))); + } + + [Fact] + public void Negative_zero_is_normalised_on_creation() + { + double value = JsonValue.Create(-0.0).GetNumber(); + + Assert.False(double.IsNegative(value)); + } + + [Theory] + [InlineData(0d, "0")] + [InlineData(1d, "1")] + [InlineData(-1d, "-1")] + [InlineData(4.5d, "4.5")] + [InlineData(0.002d, "0.002")] + [InlineData(1e30d, "1e+30")] + [InlineData(1e-27d, "1e-27")] + [InlineData(1e21d, "1e+21")] + [InlineData(1e20d, "100000000000000000000")] + [InlineData(1e-6d, "0.000001")] + [InlineData(1e-7d, "1e-7")] + [InlineData(333333333.33333329d, "333333333.3333333")] + [InlineData(9007199254740991d, "9007199254740991")] + [InlineData(5e-324d, "5e-324")] + [InlineData(1.7976931348623157e308d, "1.7976931348623157e+308")] + public void Numbers_use_the_ecmascript_shortest_round_trip_form(double value, string expected) => + Assert.Equal(expected, CanonicalJson.Serialize(JsonValue.Create(value))); + + [Fact] + public void Object_member_names_sort_by_raw_utf16_code_units() + { + JsonValue value = JsonValue.Parse( + "{\"\\ud83d\\ude00\":1,\"\\u20ac\":2,\"\\u00f6\":3,\"1\":4,\"\\r\":5}"); + + Assert.Equal( + "{\"\\r\":5,\"1\":4,\"\u00f6\":3,\"\u20ac\":2,\"\ud83d\ude00\":1}", + CanonicalJson.Serialize(value)); + } + + [Fact] + public void Supplementary_characters_sort_after_the_basic_plane_by_code_unit() + { + // U+FFFD is a single code unit above the surrogate range, so it sorts + // after a supplementary character whose leading code unit is U+D83D. + JsonValue value = JsonValue.Parse("{\"\\ufffd\":1,\"\\ud83d\\ude00\":2}"); + + Assert.Equal("{\"\ud83d\ude00\":2,\"\ufffd\":1}", CanonicalJson.Serialize(value)); + } + + [Fact] + public void Arrays_keep_their_original_order() + { + JsonValue value = JsonValue.Parse("[3,1,2]"); + + Assert.Equal("[3,1,2]", CanonicalJson.Serialize(value)); + } + + [Theory] + [InlineData("\"text\"", "\"text\"")] + [InlineData("42", "42")] + [InlineData("true", "true")] + [InlineData("false", "false")] + [InlineData("null", "null")] + public void Top_level_scalars_canonicalize(string json, string expected) => + Assert.Equal(expected, CanonicalJson.Serialize(JsonValue.Parse(json))); + + [Fact] + public void Control_characters_use_lowercase_short_escapes() + { + JsonValue value = JsonValue.Create("\u0000\u001f\b\t\n\f\r\"\\/"); + + Assert.Equal( + "\"\\u0000\\u001f\\b\\t\\n\\f\\r\\\"\\\\/\"", + CanonicalJson.Serialize(value)); + } + + [Fact] + public void Non_finite_numbers_are_rejected() + { + Assert.Equal( + WorldCutErrorCode.InvalidInput, + Assert.Throws(() => JsonValue.Create(double.NaN)).Code); + Assert.Throws(() => JsonValue.Create(double.PositiveInfinity)); + Assert.Throws(() => JsonValue.Create(double.NegativeInfinity)); + } + + [Theory] + [InlineData("1e400")] + [InlineData("-1e400")] + [InlineData("[1e999]")] + public void Json_numbers_outside_the_binary64_domain_are_rejected(string json) => + Assert.Equal( + WorldCutErrorCode.InvalidInput, + Assert.Throws(() => JsonValue.Parse(json)).Code); + + [Fact] + public void Underflowing_numbers_parse_as_zero_like_the_reference() + { + Assert.Equal("0", CanonicalJson.Serialize(JsonValue.Parse("1e-400"))); + } + + [Fact] + public void Digest_is_the_sha256_of_the_canonical_utf8_bytes() + { + JsonValue value = JsonValue.Parse("{\"b\":1,\"a\":\"\u20ac\"}"); + + Assert.Equal( + Digest.Sha256Hex(CanonicalJson.SerializeToUtf8(value)), + CanonicalJson.ComputeSha256Hex(value)); + Assert.Equal(64, CanonicalJson.ComputeSha256Hex(value).Length); + } + + [Fact] + public void Duplicate_object_members_follow_last_value_wins() + { + JsonValue value = JsonValue.Parse("{\"a\":1,\"b\":2,\"a\":3}"); + + Assert.Equal("{\"a\":3,\"b\":2}", CanonicalJson.Serialize(value)); + } + + [Fact] + public void Explicitly_constructed_objects_reject_duplicate_member_names() + { + WorldCutException error = Assert.Throws(() => JsonValue.CreateObject( + [ + new("a", JsonValue.Create(1)), + new("a", JsonValue.Create(2)), + ])); + + Assert.Equal(WorldCutErrorCode.InvalidInput, error.Code); + } + + [Fact] + public void Canonicalization_accepts_the_documented_maximum_depth() + { + string json = new string('[', WorldCutProtocol.MaxCanonicalizationDepth) + + "0" + + new string(']', WorldCutProtocol.MaxCanonicalizationDepth); + + JsonValue value = BuildNestedArray(WorldCutProtocol.MaxCanonicalizationDepth); + + Assert.Equal(WorldCutProtocol.MaxCanonicalizationDepth, value.Depth); + Assert.Equal(json, CanonicalJson.Serialize(value)); + } + + [Fact] + public void Canonicalization_rejects_one_level_beyond_the_documented_maximum() + { + WorldCutException error = Assert.Throws( + () => BuildNestedArray(WorldCutProtocol.MaxCanonicalizationDepth + 1)); + + Assert.Equal(WorldCutErrorCode.InvalidInput, error.Code); + } + + [Fact] + public void Serialize_rejects_a_null_argument() => + Assert.Throws(() => CanonicalJson.Serialize(null!)); + + private static JsonValue BuildNestedArray(int depth) + { + JsonValue value = JsonValue.Create(0); + for (int level = 0; level < depth; level++) + { + value = JsonValue.CreateArray(value); + } + + return value; + } +} diff --git a/ports/dotnet/tests/WorldCut.Tests/CliTests.cs b/ports/dotnet/tests/WorldCut.Tests/CliTests.cs new file mode 100644 index 0000000..5f4edc1 --- /dev/null +++ b/ports/dotnet/tests/WorldCut.Tests/CliTests.cs @@ -0,0 +1,284 @@ +using System.Diagnostics; +using System.Globalization; +using System.Text; +using WorldCut.Json; +using WorldCut.Tool; + +namespace WorldCut.Tests; + +/// +/// Command-line behaviour: output shape, exit codes, and the stable error +/// envelope written to standard error. +/// +public sealed class CliTests : IDisposable +{ + private readonly string _directory = System.IO.Directory.CreateTempSubdirectory("worldcut-cli").FullName; + + public void Dispose() => System.IO.Directory.Delete(_directory, recursive: true); + + [Fact] + public void Prints_the_complete_verification_result() + { + string path = Write("satisfied.json", Fixtures.InputWithVerdict("CONTRACT_SATISFIED")); + var output = new StringWriter(CultureInfo.InvariantCulture); + var error = new StringWriter(CultureInfo.InvariantCulture); + + Assert.Equal(0, Program.Run([path], output, error)); + Assert.Equal(string.Empty, error.ToString()); + + JsonValue printed = JsonValue.Parse(output.ToString()); + Assert.Equal( + CanonicalJson.Serialize(Fixtures.Expected("coherent")), + CanonicalJson.Serialize(printed)); + } + + [Fact] + public void Require_satisfied_exits_with_two_for_a_violated_contract() + { + string path = Write("violated.json", Fixtures.InputWithVerdict("CONTRACT_VIOLATED")); + var output = new StringWriter(CultureInfo.InvariantCulture); + var error = new StringWriter(CultureInfo.InvariantCulture); + + Assert.Equal(2, Program.Run(["--require-satisfied", path], output, error)); + + JsonValue printed = JsonValue.Parse(output.ToString()); + Assert.Equal("CONTRACT_VIOLATED", printed.GetProperty("verdict").GetString()); + Assert.Equal(string.Empty, error.ToString()); + } + + [Fact] + public void Require_satisfied_exits_with_two_for_insufficient_evidence() + { + string path = Write("unknown.json", Fixtures.InputWithVerdict("INSUFFICIENT_EVIDENCE")); + + Assert.Equal(2, Run(["--require-satisfied", path], out _, out _)); + } + + [Fact] + public void Require_satisfied_exits_with_zero_for_a_satisfied_contract() + { + string path = Write("satisfied.json", Fixtures.InputWithVerdict("CONTRACT_SATISFIED")); + + Assert.Equal(0, Run([path, "--require-satisfied"], out _, out _)); + } + + [Theory] + [InlineData("--help")] + public void Help_exits_with_zero(string flag) + { + Assert.Equal(0, Run([flag], out string output, out string error)); + + Assert.Contains("worldcut-dotnet", output, StringComparison.Ordinal); + Assert.Contains("--require-satisfied", output, StringComparison.Ordinal); + Assert.Equal(string.Empty, error); + } + + [Fact] + public void An_unsupported_short_help_flag_is_an_argument_error() + { + Assert.Equal(1, Run(["-h"], out _, out string error)); + + AssertErrorCode("WORLDCUT_INVALID_ARGUMENT", error); + } + + [Fact] + public void An_argument_containing_an_unpaired_surrogate_still_produces_the_envelope() + { + Assert.Equal(1, Run(["--bad\ud800"], out _, out string error)); + + AssertErrorCode("WORLDCUT_INVALID_ARGUMENT", error); + Assert.Contains("\ufffd", error, StringComparison.Ordinal); + } + + [Fact] + public void Help_wins_over_other_arguments() + { + string path = Write("satisfied.json", Fixtures.InputWithVerdict("CONTRACT_SATISFIED")); + + Assert.Equal(0, Run(["--require-satisfied", path, "--help"], out string output, out _)); + Assert.Contains("Usage:", output, StringComparison.Ordinal); + } + + [Theory] + [InlineData("")] + [InlineData("--unknown")] + [InlineData("-x")] + public void Invalid_arguments_produce_a_stable_error_envelope(string argument) + { + string[] arguments = argument.Length == 0 ? [] : [argument]; + + Assert.Equal(1, Run(arguments, out string output, out string error)); + + Assert.Equal(string.Empty, output); + AssertErrorCode("WORLDCUT_INVALID_ARGUMENT", error); + } + + [Fact] + public void More_than_one_positional_argument_is_rejected() + { + string path = Write("satisfied.json", Fixtures.InputWithVerdict("CONTRACT_SATISFIED")); + + Assert.Equal(1, Run([path, path], out _, out string error)); + AssertErrorCode("WORLDCUT_INVALID_ARGUMENT", error); + } + + [Fact] + public void A_missing_file_produces_a_read_failure() + { + Assert.Equal(1, Run([Path.Combine(_directory, "missing.json")], out _, out string error)); + + AssertErrorCode("WORLDCUT_FILE_READ_FAILED", error); + } + + [Fact] + public void A_directory_argument_produces_a_read_failure() + { + Assert.Equal(1, Run([_directory], out _, out string error)); + + AssertErrorCode("WORLDCUT_FILE_READ_FAILED", error); + } + + [Fact] + public void Invalid_json_produces_an_input_error() + { + string path = Write("invalid.json", "not json"); + + Assert.Equal(1, Run([path], out _, out string error)); + AssertErrorCode("WORLDCUT_INVALID_INPUT", error); + } + + [Fact] + public void Invalid_protocol_input_produces_an_input_error() + { + string path = Write("wrong-protocol.json", "{\"protocolVersion\":\"9.9\"}"); + + Assert.Equal(1, Run([path], out _, out string error)); + AssertErrorCode("WORLDCUT_INVALID_INPUT", error); + } + + [Fact] + public void Raw_unpaired_surrogate_input_produces_an_input_error() + { + string path = Path.Combine(_directory, "surrogate.json"); + File.WriteAllBytes(path, ConformanceCorpus.ReadBytes("raw/unpaired-high-surrogate.json")); + + Assert.Equal(1, Run([path], out _, out string error)); + AssertErrorCode("WORLDCUT_INVALID_INPUT", error); + } + + [Fact] + public void Non_ascii_output_is_written_as_utf8() + { + string json = Fixtures.CoherentInput() + .Replace("ci-status-passed", "ci-status-\u20ac\ud83d\udc0d", StringComparison.Ordinal); + string path = Write("unicode.json", json); + + Assert.Equal(0, Run([path], out string output, out _)); + + Assert.Contains("\u20ac\ud83d\udc0d", output, StringComparison.Ordinal); + } + + [Fact] + public void The_published_executable_matches_the_in_process_exit_codes() + { + string toolPath = ToolAssemblyPath(); + string path = Write("violated.json", Fixtures.InputWithVerdict("CONTRACT_VIOLATED")); + + (int exitCode, string output, string error) = RunProcess(toolPath, ["--require-satisfied", path]); + + Assert.Equal(2, exitCode); + Assert.Equal(string.Empty, error.Trim()); + Assert.Equal("CONTRACT_VIOLATED", JsonValue.Parse(output).GetProperty("verdict").GetString()); + } + + [Fact] + public void The_published_executable_writes_errors_to_standard_error() + { + (int exitCode, string output, string error) = RunProcess(ToolAssemblyPath(), ["--nope"]); + + Assert.Equal(1, exitCode); + Assert.Equal(string.Empty, output.Trim()); + AssertErrorCode("WORLDCUT_INVALID_ARGUMENT", error); + } + + [Fact] + public void The_published_executable_writes_non_ascii_output_as_utf8() + { + string json = Fixtures.CoherentInput() + .Replace("ci-status-passed", "ci-status-\u20ac\ud83d\udc0d", StringComparison.Ordinal); + string path = Write("unicode-process.json", json); + + (int exitCode, string output, _) = RunProcess(ToolAssemblyPath(), [path]); + + Assert.Equal(0, exitCode); + Assert.Contains("\u20ac\ud83d\udc0d", output, StringComparison.Ordinal); + } + + private static void AssertErrorCode(string expected, string error) + { + JsonValue envelope = JsonValue.Parse(error.Trim()); + Assert.True(envelope.TryGetProperty("error", out JsonValue? detail)); + Assert.Equal(expected, detail.GetProperty("code").GetString()); + Assert.NotEmpty(detail.GetProperty("message").GetString()); + } + + private static string ToolAssemblyPath() + { + string testDirectory = AppContext.BaseDirectory; + string candidate = Path.Combine(testDirectory, "WorldCut.Tool.dll"); + Assert.True(File.Exists(candidate), $"the CLI assembly is missing from {testDirectory}"); + return candidate; + } + + private static string DotnetMuxerPath() + { + string runtimeDirectory = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory(); + string? root = new DirectoryInfo(runtimeDirectory).Parent?.Parent?.Parent?.FullName; + string fileName = OperatingSystem.IsWindows() ? "dotnet.exe" : "dotnet"; + string candidate = root is null ? fileName : Path.Combine(root, fileName); + return File.Exists(candidate) ? candidate : fileName; + } + + private static (int ExitCode, string Output, string Error) RunProcess( + string assemblyPath, + string[] arguments) + { + var startInfo = new ProcessStartInfo(DotnetMuxerPath()) + { + RedirectStandardOutput = true, + RedirectStandardError = true, + StandardOutputEncoding = Encoding.UTF8, + StandardErrorEncoding = Encoding.UTF8, + UseShellExecute = false, + }; + + startInfo.ArgumentList.Add(assemblyPath); + foreach (string argument in arguments) + { + startInfo.ArgumentList.Add(argument); + } + + using Process process = Process.Start(startInfo)!; + string output = process.StandardOutput.ReadToEnd(); + string error = process.StandardError.ReadToEnd(); + process.WaitForExit(); + return (process.ExitCode, output, error); + } + + private static int Run(string[] arguments, out string output, out string error) + { + var outputWriter = new StringWriter(CultureInfo.InvariantCulture); + var errorWriter = new StringWriter(CultureInfo.InvariantCulture); + int exitCode = Program.Run(arguments, outputWriter, errorWriter); + output = outputWriter.ToString(); + error = errorWriter.ToString(); + return exitCode; + } + + private string Write(string name, string contents) + { + string path = Path.Combine(_directory, name); + File.WriteAllText(path, contents, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); + return path; + } +} diff --git a/ports/dotnet/tests/WorldCut.Tests/ConformanceCorpus.cs b/ports/dotnet/tests/WorldCut.Tests/ConformanceCorpus.cs new file mode 100644 index 0000000..068f3c8 --- /dev/null +++ b/ports/dotnet/tests/WorldCut.Tests/ConformanceCorpus.cs @@ -0,0 +1,65 @@ +using System.Reflection; +using System.Text; +using WorldCut.Json; + +namespace WorldCut.Tests; + +/// +/// Loads the mirrored conformance corpus from the test output directory. +/// +/// +/// The corpus is copied into this project by +/// scripts/generate-conformance.mjs, so the tests never read a file from +/// the parent repository and keep working from a source distribution. +/// +internal static class ConformanceCorpus +{ + internal static string Directory { get; } = Path.Combine( + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, + "data", + "conformance", + "0.1"); + + internal static byte[] ReadBytes(string relativePath) => + File.ReadAllBytes(Path.Combine(Directory, relativePath.Replace('/', Path.DirectorySeparatorChar))); + + internal static JsonValue ReadVectorFile(string name) => JsonValue.ParseUtf8(ReadBytes(name)); + + internal static IReadOnlyList Cases(string name) + { + JsonValue file = ReadVectorFile(name); + Assert.True(file.TryGetProperty("cases", out JsonValue? cases)); + return cases.Items; + } + + internal static IEnumerable> CaseNames(string name) + { + foreach (JsonValue vector in Cases(name)) + { + yield return new TheoryDataRow(Name(vector)); + } + } + + internal static JsonValue Case(string file, string caseName) + { + foreach (JsonValue vector in Cases(file)) + { + if (string.Equals(Name(vector), caseName, StringComparison.Ordinal)) + { + return vector; + } + } + + throw new InvalidOperationException($"conformance case {caseName} is missing from {file}"); + } + + internal static JsonValue Member(JsonValue value, string name) + { + Assert.True(value.TryGetProperty(name, out JsonValue? member), $"missing member {name}"); + return member; + } + + internal static string Name(JsonValue vector) => Member(vector, "name").GetString(); + + internal static byte[] Utf8(JsonValue value) => Encoding.UTF8.GetBytes(JsonText.Compact(value)); +} diff --git a/ports/dotnet/tests/WorldCut.Tests/ConformanceTests.cs b/ports/dotnet/tests/WorldCut.Tests/ConformanceTests.cs new file mode 100644 index 0000000..f4f6a4c --- /dev/null +++ b/ports/dotnet/tests/WorldCut.Tests/ConformanceTests.cs @@ -0,0 +1,150 @@ +using WorldCut.Json; + +namespace WorldCut.Tests; + +/// +/// Runs the complete shared conformance corpus from conformance/0.1. +/// +public sealed class ConformanceTests +{ + public static IEnumerable> VerificationCases() => + ConformanceCorpus.CaseNames("verification-vectors.json"); + + public static IEnumerable> InvalidCases() => + ConformanceCorpus.CaseNames("invalid-vectors.json"); + + public static IEnumerable> CanonicalizationCases() => + ConformanceCorpus.CaseNames("canonicalization-vectors.json"); + + public static IEnumerable> RawCases() => + ConformanceCorpus.CaseNames("raw-vectors.json"); + + [Theory] + [MemberData(nameof(VerificationCases))] + public void Verification_vector_produces_the_exact_golden_result(string name) + { + JsonValue vector = ConformanceCorpus.Case("verification-vectors.json", name); + JsonValue input = ConformanceCorpus.Member(vector, "input"); + JsonValue expected = ConformanceCorpus.Member(vector, "expected"); + + VerificationResult result = WorldCutVerifier.VerifyJsonUtf8(ConformanceCorpus.Utf8(input)); + + Assert.Equal(CanonicalJson.Serialize(expected), CanonicalJson.Serialize(result.ToJson())); + Assert.Equal( + ConformanceCorpus.Member(expected, "verificationRecordDigest").GetString(), + result.VerificationRecordDigest); + } + + [Theory] + [MemberData(nameof(VerificationCases))] + public void Verification_vector_is_independent_of_input_member_and_array_order(string name) + { + JsonValue vector = ConformanceCorpus.Case("verification-vectors.json", name); + JsonValue expected = ConformanceCorpus.Member(vector, "expected"); + JsonValue reordered = JsonReorder.Reverse(ConformanceCorpus.Member(vector, "input")); + + VerificationResult result = WorldCutVerifier.VerifyJsonUtf8(ConformanceCorpus.Utf8(reordered)); + + Assert.Equal(CanonicalJson.Serialize(expected), CanonicalJson.Serialize(result.ToJson())); + } + + [Theory] + [MemberData(nameof(InvalidCases))] + public void Invalid_vector_produces_the_exact_error_code(string name) + { + JsonValue vector = ConformanceCorpus.Case("invalid-vectors.json", name); + JsonValue input = ConformanceCorpus.Member(vector, "input"); + string expectedCode = ConformanceCorpus.Member(vector, "expectedErrorCode").GetString(); + + WorldCutException error = Assert.Throws( + () => WorldCutVerifier.VerifyJsonUtf8(ConformanceCorpus.Utf8(input))); + + Assert.Equal(expectedCode, error.WireCode); + } + + [Theory] + [MemberData(nameof(CanonicalizationCases))] + public void Canonicalization_vector_produces_the_exact_bytes_and_digest(string name) + { + JsonValue vector = ConformanceCorpus.Case("canonicalization-vectors.json", name); + JsonValue value = ConformanceCorpus.Member(vector, "value"); + + Assert.Equal( + ConformanceCorpus.Member(vector, "expectedCanonicalJson").GetString(), + CanonicalJson.Serialize(value)); + Assert.Equal( + ConformanceCorpus.Member(vector, "expectedSha256").GetString(), + CanonicalJson.ComputeSha256Hex(value)); + } + + [Theory] + [MemberData(nameof(RawCases))] + public void Raw_vector_is_rejected_with_an_accepted_outcome(string name) + { + JsonValue vector = ConformanceCorpus.Case("raw-vectors.json", name); + byte[] source = ConformanceCorpus.ReadBytes(ConformanceCorpus.Member(vector, "file").GetString()); + + Assert.Equal( + ConformanceCorpus.Member(vector, "sha256").GetString(), + Digest.Sha256Hex(source)); + + WorldCutException error = Assert.Throws( + () => WorldCutVerifier.VerifyJsonUtf8(source)); + + var accepted = ConformanceCorpus.Member(vector, "acceptedOutcomes").Items + .Select(item => item.GetString()) + .ToArray(); + Assert.Contains(error.WireCode, accepted); + } + + [Fact] + public void Manifest_hashes_and_counts_match_the_mirrored_corpus() + { + JsonValue manifest = ConformanceCorpus.ReadVectorFile("manifest.json"); + + Assert.Equal( + WorldCutProtocol.ProtocolVersion, + ConformanceCorpus.Member(manifest, "protocolVersion").GetString()); + Assert.Equal( + WorldCutProtocol.EngineVersion, + ConformanceCorpus.Member(manifest, "engineVersion").GetString()); + Assert.Equal( + WorldCutProtocol.Canonicalization, + ConformanceCorpus.Member(manifest, "canonicalization").GetString()); + + JsonValue files = ConformanceCorpus.Member(manifest, "files"); + Assert.NotEmpty(files.Members); + + foreach (KeyValuePair entry in files.Members) + { + byte[] source = ConformanceCorpus.ReadBytes(entry.Key); + + Assert.Equal( + ConformanceCorpus.Member(entry.Value, "sha256").GetString(), + Digest.Sha256Hex(source)); + + if (entry.Value.TryGetProperty("cases", out JsonValue? cases)) + { + Assert.Equal((int)cases.GetNumber(), ConformanceCorpus.Cases(entry.Key).Count); + } + + if (entry.Value.TryGetProperty("bytes", out JsonValue? bytes)) + { + Assert.Equal((int)bytes.GetNumber(), source.Length); + } + } + } + + [Fact] + public void Reordered_equivalent_input_keeps_the_same_verification_record_digest() + { + JsonValue coherent = ConformanceCorpus.Case("verification-vectors.json", "coherent"); + JsonValue reversed = ConformanceCorpus.Case("verification-vectors.json", "reversed-ordering"); + + Assert.Equal( + ConformanceCorpus.Member( + ConformanceCorpus.Member(coherent, "expected"), "verificationRecordDigest").GetString(), + ConformanceCorpus.Member( + ConformanceCorpus.Member(reversed, "expected"), "verificationRecordDigest").GetString()); + } +} diff --git a/ports/dotnet/tests/WorldCut.Tests/Digest.cs b/ports/dotnet/tests/WorldCut.Tests/Digest.cs new file mode 100644 index 0000000..4e9e9db --- /dev/null +++ b/ports/dotnet/tests/WorldCut.Tests/Digest.cs @@ -0,0 +1,17 @@ +using System.Security.Cryptography; + +namespace WorldCut.Tests; + +/// +/// Independent SHA-256 helper used by the tests so that manifest and raw-vector +/// checks never reuse the implementation under test. +/// +internal static class Digest +{ + internal static string Sha256Hex(byte[] source) + { +#pragma warning disable CA1308 // Conformance digests are specified as lowercase hexadecimal. + return Convert.ToHexString(SHA256.HashData(source)).ToLowerInvariant(); +#pragma warning restore CA1308 + } +} diff --git a/ports/dotnet/tests/WorldCut.Tests/Fixtures.cs b/ports/dotnet/tests/WorldCut.Tests/Fixtures.cs new file mode 100644 index 0000000..c3dfed0 --- /dev/null +++ b/ports/dotnet/tests/WorldCut.Tests/Fixtures.cs @@ -0,0 +1,36 @@ +using WorldCut.Json; + +namespace WorldCut.Tests; + +/// Shared fixture inputs drawn from the mirrored conformance corpus. +internal static class Fixtures +{ + internal static JsonValue Input(string caseName) => + ConformanceCorpus.Member( + ConformanceCorpus.Case("verification-vectors.json", caseName), + "input"); + + internal static JsonValue Expected(string caseName) => + ConformanceCorpus.Member( + ConformanceCorpus.Case("verification-vectors.json", caseName), + "expected"); + + internal static string CoherentInput() => JsonText.Compact(Input("coherent")); + + internal static string InputWithVerdict(string verdict) + { + foreach (JsonValue vector in ConformanceCorpus.Cases("verification-vectors.json")) + { + JsonValue expected = ConformanceCorpus.Member(vector, "expected"); + if (string.Equals( + ConformanceCorpus.Member(expected, "verdict").GetString(), + verdict, + StringComparison.Ordinal)) + { + return JsonText.Compact(ConformanceCorpus.Member(vector, "input")); + } + } + + throw new InvalidOperationException($"no conformance case produces {verdict}"); + } +} diff --git a/ports/dotnet/tests/WorldCut.Tests/GlobalUsings.cs b/ports/dotnet/tests/WorldCut.Tests/GlobalUsings.cs new file mode 100644 index 0000000..c802f44 --- /dev/null +++ b/ports/dotnet/tests/WorldCut.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/ports/dotnet/tests/WorldCut.Tests/JsonReorder.cs b/ports/dotnet/tests/WorldCut.Tests/JsonReorder.cs new file mode 100644 index 0000000..6bae315 --- /dev/null +++ b/ports/dotnet/tests/WorldCut.Tests/JsonReorder.cs @@ -0,0 +1,63 @@ +using WorldCut.Json; + +namespace WorldCut.Tests; + +/// +/// Produces an input that is semantically identical but ordered differently. +/// +/// +/// Object member order is never significant, and the verifier normalises +/// contract requirements by identifier and observations by role before +/// evaluating or hashing. Everything else — role lists, value paths, and +/// observed arrays — keeps its original order because the protocol gives that +/// order meaning. +/// +internal static class JsonReorder +{ + internal static JsonValue Reverse(JsonValue input) + { + JsonValue reordered = ReverseMembers(input); + var members = new List>(); + + foreach (KeyValuePair member in reordered.Members) + { + JsonValue value = member.Key switch + { + "observations" => ReverseArray(member.Value), + "contract" => ReverseRequirements(member.Value), + _ => member.Value, + }; + + members.Add(new KeyValuePair(member.Key, value)); + } + + return JsonValue.CreateObject(members); + } + + private static JsonValue ReverseRequirements(JsonValue contract) + { + var members = new List>(); + foreach (KeyValuePair member in contract.Members) + { + members.Add(string.Equals(member.Key, "requirements", StringComparison.Ordinal) + ? new KeyValuePair(member.Key, ReverseArray(member.Value)) + : member); + } + + return JsonValue.CreateObject(members); + } + + private static JsonValue ReverseArray(JsonValue value) => + JsonValue.CreateArray(value.Items.Reverse().ToArray()); + + private static JsonValue ReverseMembers(JsonValue value) => value.Kind switch + { + JsonKind.Object => JsonValue.CreateObject( + value.Members + .Select(member => new KeyValuePair(member.Key, ReverseMembers(member.Value))) + .Reverse() + .ToArray()), + JsonKind.Array => JsonValue.CreateArray(value.Items.Select(ReverseMembers).ToArray()), + _ => value, + }; +} diff --git a/ports/dotnet/tests/WorldCut.Tests/NormalizedTimestampTests.cs b/ports/dotnet/tests/WorldCut.Tests/NormalizedTimestampTests.cs new file mode 100644 index 0000000..82fbeb6 --- /dev/null +++ b/ports/dotnet/tests/WorldCut.Tests/NormalizedTimestampTests.cs @@ -0,0 +1,104 @@ +using WorldCut.Model; + +namespace WorldCut.Tests; + +/// +/// Timestamp grammar and ordering, including the ECMAScript year-zero domain +/// that cannot represent. +/// +public sealed class NormalizedTimestampTests +{ + [Theory] + [InlineData("0000-01-01T00:00:00.000Z", -62167219200000L)] + [InlineData("1970-01-01T00:00:00.000Z", 0L)] + [InlineData("1969-12-31T23:59:59.999Z", -1L)] + [InlineData("2026-09-02T18:00:00.000Z", 1788372000000L)] + [InlineData("2000-02-29T12:00:00.500Z", 951825600500L)] + [InlineData("9999-12-31T23:59:59.999Z", 253402300799999L)] + public void Accepted_timestamps_produce_the_ecmascript_epoch_ordinal(string text, long expected) + { + Assert.True(NormalizedTimestamp.TryParse(text, out NormalizedTimestamp timestamp)); + + Assert.Equal(expected, timestamp.EpochMilliseconds); + Assert.Equal(text, timestamp.Text); + } + + [Theory] + [InlineData("2026-09-02")] + [InlineData("2026-09-02T18:00:00Z")] + [InlineData("2026-09-02T18:00:00.000")] + [InlineData("2026-09-02T18:00:00.000+00:00")] + [InlineData("2026-09-02t18:00:00.000Z")] + [InlineData("2026-09-02T18:00:00.000z")] + [InlineData("2026-13-01T00:00:00.000Z")] + [InlineData("2026-00-01T00:00:00.000Z")] + [InlineData("2026-02-30T00:00:00.000Z")] + [InlineData("2026-09-00T00:00:00.000Z")] + [InlineData("2026-09-02T24:00:00.000Z")] + [InlineData("2026-09-02T18:60:00.000Z")] + [InlineData("2026-09-02T18:00:60.000Z")] + [InlineData("2026-09-02T18:00:00.0000Z")] + [InlineData("+2026-09-02T18:00:00.000Z")] + [InlineData("")] + [InlineData(null)] + public void Rejected_timestamps_are_not_parsed(string? text) => + Assert.False(NormalizedTimestamp.TryParse(text, out _)); + + [Theory] + [InlineData("1900-02-28T00:00:00.000Z", true)] + [InlineData("1900-02-29T00:00:00.000Z", false)] + [InlineData("2000-02-29T00:00:00.000Z", true)] + [InlineData("2024-02-29T00:00:00.000Z", true)] + [InlineData("2026-02-29T00:00:00.000Z", false)] + [InlineData("0000-02-29T00:00:00.000Z", true)] + public void Leap_days_follow_the_proleptic_gregorian_calendar(string text, bool accepted) => + Assert.Equal(accepted, NormalizedTimestamp.TryParse(text, out _)); + + [Fact] + public void Ordering_matches_chronological_order() + { + Assert.True(NormalizedTimestamp.TryParse("0000-01-01T00:00:00.000Z", out NormalizedTimestamp yearZero)); + Assert.True(NormalizedTimestamp.TryParse("2026-09-02T18:00:00.000Z", out NormalizedTimestamp later)); + Assert.True(NormalizedTimestamp.TryParse("0000-01-01T00:00:00.000Z", out NormalizedTimestamp sameAsYearZero)); + + Assert.True(yearZero < later); + Assert.True(later > yearZero); + Assert.True(yearZero <= sameAsYearZero); + Assert.True(later >= yearZero); + Assert.True(sameAsYearZero >= yearZero); + Assert.True(yearZero == sameAsYearZero); + Assert.False(yearZero != sameAsYearZero); + Assert.NotEqual(yearZero, later); + Assert.Equal(later, NormalizedTimestamp.Max(yearZero, later)); + Assert.Equal(yearZero, NormalizedTimestamp.Min(yearZero, later)); + Assert.True(yearZero.CompareTo(later) < 0); + Assert.Equal(yearZero.GetHashCode(), sameAsYearZero.GetHashCode()); + Assert.True(yearZero.Equals((object)sameAsYearZero)); + Assert.Equal("0000-01-01T00:00:00.000Z", yearZero.ToString()); + } + + [Fact] + public void Year_zero_timestamps_verify_end_to_end() + { + string json = Fixtures.CoherentInput().Replace("2026-", "0000-", StringComparison.Ordinal); + + VerificationResult result = WorldCutVerifier.VerifyJson(json); + + Assert.Equal(ContractVerdict.ContractSatisfied, result.Verdict); + Assert.Contains( + result.RequirementResults, + item => item.Details.TryGetProperty("commonWindow", out _)); + } + + [Fact] + public void Year_zero_timestamps_still_enforce_interval_and_timing_rules() + { + string json = Fixtures.CoherentInput() + .Replace("2026-", "0000-", StringComparison.Ordinal) + .Replace("\"decisionTime\":\"0000-09-02T18:00:00.000Z\"", "\"decisionTime\":\"0000-09-02T17:00:00.000Z\"", StringComparison.Ordinal); + + Assert.Equal( + WorldCutErrorCode.InvalidInput, + Assert.Throws(() => WorldCutVerifier.VerifyJson(json)).Code); + } +} diff --git a/ports/dotnet/tests/WorldCut.Tests/PropertyTests.cs b/ports/dotnet/tests/WorldCut.Tests/PropertyTests.cs new file mode 100644 index 0000000..515a6de --- /dev/null +++ b/ports/dotnet/tests/WorldCut.Tests/PropertyTests.cs @@ -0,0 +1,352 @@ +using System.Globalization; +using System.Text; +using WorldCut.Json; + +namespace WorldCut.Tests; + +/// +/// Deterministic randomized invariant checks. +/// +/// +/// Every case uses a fixed seed so a failure is reproducible from the test name +/// alone. These tests assert invariants, not golden values; the shared +/// conformance corpus owns the golden values. +/// +public sealed class PropertyTests +{ + private const int Seed = 20260903; + + [Fact] + public void Canonicalization_is_deterministic_and_stable_under_member_reordering() + { + var random = new Random(Seed); + + for (int iteration = 0; iteration < 2_000; iteration++) + { + JsonValue value = RandomJson(random, depth: 0); + + string first = CanonicalJson.Serialize(value); + string second = CanonicalJson.Serialize(value); + string reordered = CanonicalJson.Serialize(Shuffle(value, random)); + + Assert.Equal(first, second); + Assert.Equal(first, reordered); + Assert.Equal(64, CanonicalJson.ComputeSha256Hex(value).Length); + } + } + + [Fact] + public void Canonical_text_always_re_parses_to_the_same_canonical_text() + { + var random = new Random(Seed + 1); + + for (int iteration = 0; iteration < 2_000; iteration++) + { + JsonValue value = RandomJson(random, depth: 0); + string canonical = CanonicalJson.Serialize(value); + + Assert.Equal(canonical, CanonicalJson.Serialize(JsonValue.Parse(canonical))); + Assert.Equal(canonical, CanonicalJson.Serialize(JsonValue.Parse(JsonText.Indent(value)))); + Assert.Equal(canonical, CanonicalJson.Serialize(JsonValue.Parse(JsonText.Compact(value)))); + } + } + + [Fact] + public void Canonical_member_order_matches_ordinal_string_ordering() + { + var random = new Random(Seed + 2); + + for (int iteration = 0; iteration < 1_000; iteration++) + { + var names = new List(); + for (int index = 0; index < random.Next(2, 8); index++) + { + names.Add(RandomString(random)); + } + + var members = new List>(); + var seen = new HashSet(StringComparer.Ordinal); + foreach (string name in names) + { + if (seen.Add(name)) + { + members.Add(new KeyValuePair(name, JsonValue.Create(0))); + } + } + + string canonical = CanonicalJson.Serialize(JsonValue.CreateObject(members)); + + var expected = seen.ToList(); + expected.Sort(Utf16.Compare); + + var actual = ExtractMemberNames(canonical); + Assert.Equal(expected, actual); + } + } + + [Fact] + public void Every_double_round_trips_through_the_canonical_form() + { + var random = new Random(Seed + 3); + var buffer = new byte[8]; + + for (int iteration = 0; iteration < 20_000; iteration++) + { + random.NextBytes(buffer); + double value = BitConverter.ToDouble(buffer); + if (!double.IsFinite(value)) + { + continue; + } + + string canonical = CanonicalJson.Serialize(JsonValue.Create(value)); + double parsed = double.Parse(canonical, NumberStyles.Float, CultureInfo.InvariantCulture); + + Assert.Equal(value == 0d ? 0d : value, parsed); + } + } + + [Fact] + public void Arbitrary_transport_bytes_never_escape_as_unstructured_errors() + { + var random = new Random(Seed + 4); + + for (int iteration = 0; iteration < 5_000; iteration++) + { + var source = new byte[random.Next(0, 96)]; + random.NextBytes(source); + + try + { + WorldCutVerifier.VerifyJsonUtf8(source); + } + catch (WorldCutException error) + { + Assert.Equal(WorldCutErrorCode.InvalidInput, error.Code); + } + } + } + + [Fact] + public void Arbitrary_text_that_is_not_a_protocol_input_is_rejected() + { + var random = new Random(Seed + 5); + + for (int iteration = 0; iteration < 2_000; iteration++) + { + string source = RandomString(random); + + try + { + WorldCutVerifier.VerifyJson(source); + } + catch (WorldCutException error) + { + Assert.Equal(WorldCutErrorCode.InvalidInput, error.Code); + continue; + } + + Assert.Fail($"random text unexpectedly verified: {source}"); + } + } + + [Fact] + public void Verification_is_independent_of_observation_and_requirement_order() + { + var random = new Random(Seed + 6); + JsonValue input = Fixtures.Input("coherent"); + string expected = WorldCutVerifier.VerifyJsonUtf8(ConformanceCorpus.Utf8(input)) + .VerificationRecordDigest; + + for (int iteration = 0; iteration < 200; iteration++) + { + JsonValue permuted = PermuteInput(input, random); + + Assert.Equal( + expected, + WorldCutVerifier + .VerifyJsonUtf8(ConformanceCorpus.Utf8(permuted)) + .VerificationRecordDigest); + } + } + + [Fact] + public void Value_equals_agrees_with_canonical_equality() + { + var random = new Random(Seed + 7); + + for (int iteration = 0; iteration < 500; iteration++) + { + JsonValue observed = RandomJson(random, depth: 0); + JsonValue expected = random.Next(2) == 0 ? observed : RandomJson(random, depth: 0); + + VerificationResult result = WorldCutVerifier.VerifyJson( + ValueEqualsInput.Build( + JsonValue.CreateObject([new("field", observed)]), + ["field"], + expected)); + + RequirementStatus status = Assert.Single(result.RequirementResults).Status; + bool equal = string.Equals( + CanonicalJson.Serialize(observed), + CanonicalJson.Serialize(expected), + StringComparison.Ordinal); + + Assert.Equal(equal ? RequirementStatus.Satisfied : RequirementStatus.Violated, status); + } + } + + private static JsonValue PermuteInput(JsonValue input, Random random) + { + var members = new List>(); + foreach (KeyValuePair member in input.Members) + { + JsonValue value = member.Key switch + { + "observations" => ShuffleArray(member.Value, random), + "contract" => PermuteContract(member.Value, random), + _ => member.Value, + }; + members.Add(new KeyValuePair(member.Key, value)); + } + + Shuffle(members, random); + return JsonValue.CreateObject(members); + } + + private static JsonValue PermuteContract(JsonValue contract, Random random) + { + var members = new List>(); + foreach (KeyValuePair member in contract.Members) + { + members.Add(string.Equals(member.Key, "requirements", StringComparison.Ordinal) + ? new KeyValuePair(member.Key, ShuffleArray(member.Value, random)) + : member); + } + + Shuffle(members, random); + return JsonValue.CreateObject(members); + } + + private static JsonValue ShuffleArray(JsonValue value, Random random) + { + var items = value.Items.ToList(); + Shuffle(items, random); + return JsonValue.CreateArray(items.ToArray()); + } + + private static void Shuffle(List items, Random random) + { + for (int index = items.Count - 1; index > 0; index--) + { + int swap = random.Next(index + 1); + (items[index], items[swap]) = (items[swap], items[index]); + } + } + + private static JsonValue Shuffle(JsonValue value, Random random) + { + switch (value.Kind) + { + case JsonKind.Object: + var members = value.Members + .Select(member => new KeyValuePair( + member.Key, + Shuffle(member.Value, random))) + .ToList(); + Shuffle(members, random); + return JsonValue.CreateObject(members); + case JsonKind.Array: + return JsonValue.CreateArray(value.Items.Select(item => Shuffle(item, random)).ToArray()); + default: + return value; + } + } + + private static JsonValue RandomJson(Random random, int depth) + { + int choice = depth >= 4 ? random.Next(5) : random.Next(7); + switch (choice) + { + case 0: + return JsonValue.Null; + case 1: + return JsonValue.Create(random.Next(2) == 0); + case 2: + return JsonValue.Create(RandomString(random)); + case 3: + return JsonValue.Create(random.NextInt64(-1_000_000, 1_000_000)); + case 4: + return JsonValue.Create((random.NextDouble() - 0.5) * Math.Pow(10, random.Next(-30, 30))); + case 5: + int length = random.Next(0, 5); + var items = new JsonValue[length]; + for (int index = 0; index < length; index++) + { + items[index] = RandomJson(random, depth + 1); + } + + return JsonValue.CreateArray(items); + default: + var members = new List>(); + var names = new HashSet(StringComparer.Ordinal); + int count = random.Next(0, 5); + for (int index = 0; index < count; index++) + { + string name = RandomString(random); + if (names.Add(name)) + { + members.Add(new KeyValuePair( + name, + RandomJson(random, depth + 1))); + } + } + + return JsonValue.CreateObject(members); + } + } + + private static string RandomString(Random random) + { + int length = random.Next(0, 12); + var builder = new StringBuilder(length); + for (int index = 0; index < length; index++) + { + switch (random.Next(6)) + { + case 0: + builder.Append((char)random.Next(0x20, 0x7F)); + break; + case 1: + builder.Append((char)random.Next(0x00, 0x20)); + break; + case 2: + builder.Append((char)random.Next(0x00A0, 0x0800)); + break; + case 3: + builder.Append((char)random.Next(0x0800, 0xD800)); + break; + case 4: + builder.Append((char)random.Next(0xE000, 0xFFFE)); + break; + default: + builder.Append(char.ConvertFromUtf32(random.Next(0x10000, 0x110000))); + break; + } + } + + return builder.ToString(); + } + + private static List ExtractMemberNames(string canonical) + { + var names = new List(); + JsonValue value = JsonValue.Parse(canonical); + foreach (KeyValuePair member in value.Members) + { + names.Add(member.Key); + } + + return names; + } +} diff --git a/ports/dotnet/tests/WorldCut.Tests/PublicApiTests.cs b/ports/dotnet/tests/WorldCut.Tests/PublicApiTests.cs new file mode 100644 index 0000000..25dc479 --- /dev/null +++ b/ports/dotnet/tests/WorldCut.Tests/PublicApiTests.cs @@ -0,0 +1,284 @@ +using System.Collections; +using System.Collections.ObjectModel; +using System.Reflection; +using WorldCut.Json; +using WorldCut.Model; + +namespace WorldCut.Tests; + +/// +/// The public contract: stable versions, structured errors, and results that +/// cannot be mutated into a different later verification. +/// +public sealed class PublicApiTests +{ + [Fact] + public void Protocol_identifiers_are_stable() + { + Assert.Equal("0.1", WorldCutProtocol.ProtocolVersion); + Assert.Equal("0.1.2", WorldCutProtocol.EngineVersion); + Assert.Equal("worldcut-json-v1", WorldCutProtocol.Canonicalization); + Assert.Equal(1_000_000_000L, WorldCutProtocol.MaxAcquisitionCost); + Assert.Equal(64_000_000_000L, WorldCutProtocol.MaxPlanTotalCost); + Assert.Equal(64, WorldCutProtocol.MaxUnresolvedRequirements); + Assert.Equal(65_536, WorldCutProtocol.MaxOptionCombinations); + Assert.Equal(4_259_840, WorldCutProtocol.MaxSearchStates); + } + + [Fact] + public void The_package_version_matches_the_released_port_version() + { + var version = typeof(WorldCutVerifier).Assembly + .GetCustomAttribute()! + .InformationalVersion; + + Assert.StartsWith("0.1.0", version, StringComparison.Ordinal); + } + + [Fact] + public void Error_codes_round_trip_to_their_wire_spelling() + { + Assert.Equal("WORLDCUT_INVALID_INPUT", WorldCutErrorCode.InvalidInput.ToWireCode()); + Assert.Equal("WORLDCUT_INVALID_ARGUMENT", WorldCutErrorCode.InvalidArgument.ToWireCode()); + Assert.Equal("WORLDCUT_FILE_READ_FAILED", WorldCutErrorCode.FileReadFailed.ToWireCode()); + Assert.Equal("WORLDCUT_RUNTIME_ERROR", WorldCutErrorCode.RuntimeError.ToWireCode()); + Assert.Throws(() => ((WorldCutErrorCode)99).ToWireCode()); + } + + [Fact] + public void Result_names_use_the_protocol_spelling() + { + Assert.Equal("SATISFIED", RequirementStatus.Satisfied.ToWireName()); + Assert.Equal("VIOLATED", RequirementStatus.Violated.ToWireName()); + Assert.Equal("UNKNOWN", RequirementStatus.Unknown.ToWireName()); + Assert.Equal("CONTRACT_SATISFIED", ContractVerdict.ContractSatisfied.ToWireName()); + Assert.Equal("CONTRACT_VIOLATED", ContractVerdict.ContractViolated.ToWireName()); + Assert.Equal("INSUFFICIENT_EVIDENCE", ContractVerdict.InsufficientEvidence.ToWireName()); + Assert.Equal("REFRESH_OBSERVATION", AcquisitionActionType.RefreshObservation.ToWireName()); + Assert.Equal("FETCH_REQUIRED_METADATA", AcquisitionActionType.FetchRequiredMetadata.ToWireName()); + Assert.Equal("ACQUIRE_COMPATIBLE_EVIDENCE", AcquisitionActionType.AcquireCompatibleEvidence.ToWireName()); + Assert.Equal("NOT_NEEDED", AcquisitionPlanStatus.NotNeeded.ToWireName()); + Assert.Equal("AVAILABLE", AcquisitionPlanStatus.Available.ToWireName()); + Assert.Equal("INCOMPLETE", AcquisitionPlanStatus.Incomplete.ToWireName()); + } + + [Fact] + public void Parsed_input_has_no_public_constructor() + { + Assert.Empty(typeof(ParsedVerificationInput).GetConstructors(BindingFlags.Public | BindingFlags.Instance)); + Assert.Empty(typeof(VerificationResult).GetConstructors(BindingFlags.Public | BindingFlags.Instance)); + Assert.Empty(typeof(RequirementResult).GetConstructors(BindingFlags.Public | BindingFlags.Instance)); + Assert.Empty(typeof(AcquisitionPlan).GetConstructors(BindingFlags.Public | BindingFlags.Instance)); + Assert.Empty(typeof(Observation).GetConstructors(BindingFlags.Public | BindingFlags.Instance)); + } + + [Fact] + public void Every_public_property_on_the_result_graph_is_read_only() + { + Type[] resultTypes = + [ + typeof(VerificationResult), + typeof(VerificationCoverage), + typeof(RequirementResult), + typeof(AcquisitionPlan), + typeof(AcquisitionOption), + typeof(AcquisitionAction), + typeof(ParsedVerificationInput), + typeof(DecisionContract), + typeof(Observation), + typeof(ObservationWitness), + typeof(DependencyWitness), + typeof(ValidityInterval), + typeof(ResourceIdentity), + typeof(JsonValue), + ]; + + foreach (Type type in resultTypes) + { + Assert.Empty(type.GetFields(BindingFlags.Public | BindingFlags.Instance)); + + foreach (PropertyInfo property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + Assert.False( + property.CanWrite, + $"{type.Name}.{property.Name} must not be writable"); + } + } + } + + [Fact] + public void Result_collections_are_not_mutable_aliases() + { + VerificationResult result = WorldCutVerifier.VerifyJson( + Fixtures.InputWithVerdict("CONTRACT_VIOLATED")); + + AssertReadOnly(result.RequirementResults); + AssertReadOnly(result.AcquisitionPlan.Actions); + AssertReadOnly(result.AcquisitionPlan.SelectedOptionIds); + AssertReadOnly(result.AcquisitionPlan.CoveredRequirementIds); + AssertReadOnly(result.AcquisitionPlan.UnresolvedRequirementIds); + + foreach (RequirementResult requirement in result.RequirementResults) + { + AssertReadOnly(requirement.AcquisitionOptions); + foreach (AcquisitionOption option in requirement.AcquisitionOptions) + { + AssertReadOnly(option.Actions); + } + } + } + + [Fact] + public void Json_value_collections_are_not_mutable_aliases() + { + JsonValue value = JsonValue.Parse("{\"a\":[1,2,3]}"); + + AssertReadOnly(value.Members); + AssertReadOnly(value.GetProperty("a").Items); + } + + [Fact] + public void A_parsed_input_can_be_verified_repeatedly_with_identical_results() + { + ParsedVerificationInput input = ParsedVerificationInput.Parse(Fixtures.CoherentInput()); + + VerificationResult first = WorldCutVerifier.Verify(input); + VerificationResult second = WorldCutVerifier.Verify(input); + + Assert.NotSame(first, second); + Assert.Equal(first.VerificationRecordDigest, second.VerificationRecordDigest); + Assert.Equal( + CanonicalJson.Serialize(first.ToJson()), + CanonicalJson.Serialize(second.ToJson())); + Assert.Equal( + CanonicalJson.Serialize(Fixtures.Expected("coherent")), + CanonicalJson.Serialize(second.ToJson())); + } + + [Fact] + public void A_result_json_projection_is_a_fresh_immutable_value() + { + VerificationResult result = WorldCutVerifier.VerifyJson(Fixtures.CoherentInput()); + + JsonValue first = result.ToJson(); + JsonValue second = result.ToJson(); + + Assert.NotSame(first, second); + Assert.Equal(CanonicalJson.Serialize(first), CanonicalJson.Serialize(second)); + } + + [Fact] + public void Parsed_input_exposes_the_validated_snapshot() + { + ParsedVerificationInput input = ParsedVerificationInput.Parse(Fixtures.CoherentInput()); + + Assert.Equal("0.1", input.ProtocolVersion); + Assert.Equal("deploy-current-tested-release", input.Contract.Id); + Assert.Equal("1", input.Contract.Version); + Assert.Equal("2026-09-02T18:00:00.000Z", input.Contract.DecisionTime.Text); + Assert.Equal(4, input.Observations.Count); + Assert.Equal(3, input.Contract.Requirements.Count); + + Observation ci = input.Observations.Single(item => item.Role == "ci"); + Assert.Equal("obs-ci-b", ci.Id); + Assert.Equal(4, ci.AcquisitionCost); + Assert.Equal(WitnessProvenance.ProviderAsserted, ci.Witness.Provenance); + Assert.Equal("run-2041", ci.Witness.Version); + Assert.Equal("exact", DependencyWitness.Relation); + + DependencyWitness dependency = Assert.Single(ci.Witness.Dependencies); + Assert.Equal("tested_head", dependency.Name); + Assert.Equal("commit-B", dependency.Version); + Assert.Equal( + new ResourceIdentity("github", "acme", "branch_head", "payments/main"), + dependency.Resource); + } + + [Fact] + public void Resource_identity_compares_every_component() + { + var left = new ResourceIdentity("p", "a", "k", "key"); + + Assert.True(left == new ResourceIdentity("p", "a", "k", "key")); + Assert.False(left != new ResourceIdentity("p", "a", "k", "key")); + Assert.NotEqual(left, new ResourceIdentity("P", "a", "k", "key")); + Assert.NotEqual(left, new ResourceIdentity("p", "A", "k", "key")); + Assert.NotEqual(left, new ResourceIdentity("p", "a", "K", "key")); + Assert.NotEqual(left, new ResourceIdentity("p", "a", "k", "KEY")); + Assert.False(left == null); + Assert.True((ResourceIdentity?)null == null); + Assert.Equal( + left.GetHashCode(), + new ResourceIdentity("p", "a", "k", "key").GetHashCode()); + } + + [Fact] + public void Null_arguments_are_rejected_before_any_work_happens() + { + Assert.Throws(() => ParsedVerificationInput.Parse(null!)); + Assert.Throws(() => WorldCutVerifier.VerifyJson(null!)); + Assert.Throws(() => WorldCutVerifier.Verify(null!)); + Assert.Throws(() => JsonValue.Create((string)null!)); + Assert.Throws(() => JsonValue.CreateArray((IEnumerable)null!)); + Assert.Throws(() => JsonText.Indent(null!)); + } + + [Fact] + public void Reading_a_json_value_as_the_wrong_kind_throws() + { + JsonValue value = JsonValue.Parse("{\"a\":1}"); + + Assert.Throws(() => value.GetString()); + Assert.Throws(() => value.GetNumber()); + Assert.Throws(() => value.GetBoolean()); + Assert.Throws(() => value.Items); + Assert.Throws(() => value.GetProperty("missing")); + Assert.False(value.IsNull); + Assert.True(JsonValue.Null.IsNull); + } + + [Fact] + public void The_worldcut_exception_exposes_a_stable_code() + { + var runtime = new WorldCutException(); + Assert.Equal(WorldCutErrorCode.RuntimeError, runtime.Code); + Assert.Equal("WORLDCUT_RUNTIME_ERROR", runtime.WireCode); + + var wrapped = new WorldCutException("boom", new InvalidOperationException("cause")); + Assert.Equal("boom", wrapped.Message); + Assert.IsType(wrapped.InnerException); + + var input = new WorldCutException(WorldCutErrorCode.InvalidInput, "bad", null); + Assert.Equal("WORLDCUT_INVALID_INPUT", input.WireCode); + } + + [Fact] + public void No_public_type_leaks_the_vendored_canonicalizer() + { + Type[] exported = typeof(WorldCutVerifier).Assembly.GetExportedTypes(); + + Assert.DoesNotContain( + exported, + type => type.Namespace?.StartsWith("Jcs", StringComparison.Ordinal) == true); + Assert.All( + exported, + type => Assert.StartsWith("WorldCut", type.Namespace!, StringComparison.Ordinal)); + } + + private static void AssertReadOnly(IReadOnlyList collection) + { + Assert.IsNotType(collection); + Assert.IsNotType>(collection); + + if (collection is IList list) + { + Assert.True(list.IsReadOnly, $"{collection.GetType().Name} must be read-only"); + } + else + { + Assert.True( + collection is ReadOnlyCollection, + $"{collection.GetType().Name} must be a read-only collection"); + } + } +} diff --git a/ports/dotnet/tests/WorldCut.Tests/UnicodeTests.cs b/ports/dotnet/tests/WorldCut.Tests/UnicodeTests.cs new file mode 100644 index 0000000..dea8a65 --- /dev/null +++ b/ports/dotnet/tests/WorldCut.Tests/UnicodeTests.cs @@ -0,0 +1,175 @@ +using System.Text; +using WorldCut.Json; + +namespace WorldCut.Tests; + +/// +/// Unicode and transport-encoding rules that protect the digest from silent +/// repair by . +/// +public sealed class UnicodeTests +{ + [Fact] + public void Raw_invalid_utf8_is_a_structured_input_error() + { + byte[] source = [0x22, 0xC3, 0x28, 0x22]; + + WorldCutException error = Assert.Throws(() => JsonValue.ParseUtf8(source)); + + Assert.Equal(WorldCutErrorCode.InvalidInput, error.Code); + Assert.Equal("WORLDCUT_INVALID_INPUT", error.WireCode); + } + + [Fact] + public void Cesu8_encoded_surrogates_are_a_structured_input_error() + { + byte[] source = [0x22, 0xED, 0xA0, 0xBD, 0xED, 0xB8, 0x80, 0x22]; + + Assert.Equal( + WorldCutErrorCode.InvalidInput, + Assert.Throws(() => JsonValue.ParseUtf8(source)).Code); + } + + [Theory] + [InlineData("\"\\ud800\"")] + [InlineData("\"\\udc00\"")] + [InlineData("\"\\ud800\\ud800\"")] + [InlineData("\"\\ud800abc\"")] + [InlineData("{\"\\udfff\":1}")] + [InlineData("[\"a\",\"\\ud83d\"]")] + public void Escaped_unpaired_surrogates_are_rejected(string json) + { + Assert.Equal( + WorldCutErrorCode.InvalidInput, + Assert.Throws(() => JsonValue.Parse(json)).Code); + Assert.Equal( + WorldCutErrorCode.InvalidInput, + Assert.Throws( + () => JsonValue.ParseUtf8(Encoding.UTF8.GetBytes(json))).Code); + } + + [Fact] + public void Escaped_surrogate_pairs_are_accepted() + { + JsonValue value = JsonValue.Parse("\"\\ud83d\\ude00\""); + + Assert.Equal("\ud83d\ude00", value.GetString()); + Assert.Equal("\"\ud83d\ude00\"", CanonicalJson.Serialize(value)); + } + + [Fact] + public void Raw_unpaired_surrogates_in_a_dotnet_string_are_rejected() + { + Assert.Equal( + WorldCutErrorCode.InvalidInput, + Assert.Throws(() => JsonValue.Create("\ud800")).Code); + Assert.Equal( + WorldCutErrorCode.InvalidInput, + Assert.Throws(() => JsonValue.Create("\udc00")).Code); + Assert.Equal( + WorldCutErrorCode.InvalidInput, + Assert.Throws(() => JsonValue.Parse("\"\ud800\"")).Code); + } + + [Fact] + public void Unpaired_surrogates_in_member_names_are_rejected() + { + WorldCutException error = Assert.Throws(() => JsonValue.CreateObject( + [ + new("\ud800", JsonValue.Null), + ])); + + Assert.Equal(WorldCutErrorCode.InvalidInput, error.Code); + } + + [Fact] + public void An_unpaired_surrogate_never_becomes_a_replacement_character() + { + // System.Text.Json substitutes U+FFFD when writing an unpaired + // surrogate. WorldCut must refuse the value rather than repair it into + // a different digest. + string replacement = CanonicalJson.ComputeSha256Hex(JsonValue.Create("\ufffd")); + + Assert.Throws(() => JsonValue.Create("\ud800")); + Assert.NotEqual(replacement, CanonicalJson.ComputeSha256Hex(JsonValue.Create("\ud83d\ude00"))); + } + + [Fact] + public void A_byte_order_mark_is_not_valid_transport_json() + { + byte[] source = [0xEF, 0xBB, 0xBF, .. Encoding.UTF8.GetBytes("{}")]; + + Assert.Equal( + WorldCutErrorCode.InvalidInput, + Assert.Throws(() => JsonValue.ParseUtf8(source)).Code); + } + + [Fact] + public void More_than_one_json_value_is_rejected() + { + Assert.Equal( + WorldCutErrorCode.InvalidInput, + Assert.Throws(() => JsonValue.Parse("{} {}")).Code); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData("{")] + [InlineData("[1,]")] + [InlineData("// comment\n1")] + [InlineData("NaN")] + [InlineData("Infinity")] + [InlineData("'single'")] + [InlineData("{a:1}")] + public void Malformed_json_is_a_structured_input_error(string json) => + Assert.Equal( + WorldCutErrorCode.InvalidInput, + Assert.Throws(() => JsonValue.Parse(json)).Code); + + [Fact] + public void Excessive_nesting_is_a_structured_input_error_and_never_overflows_the_stack() + { + string json = new string('[', 200_000) + new string(']', 200_000); + + WorldCutException error = Assert.Throws( + () => WorldCutVerifier.VerifyJson(json)); + + Assert.Equal(WorldCutErrorCode.InvalidInput, error.Code); + } + + [Fact] + public void Parsing_accepts_the_documented_maximum_input_depth() + { + string json = new string('[', WorldCutProtocol.MaxJsonDepth) + + "0" + + new string(']', WorldCutProtocol.MaxJsonDepth); + + Assert.Equal(WorldCutProtocol.MaxJsonDepth, JsonValue.Parse(json).Depth); + } + + [Fact] + public void Parsing_rejects_one_level_beyond_the_documented_maximum_input_depth() + { + int depth = WorldCutProtocol.MaxJsonDepth + 1; + string json = new string('[', depth) + "0" + new string(']', depth); + + Assert.Equal( + WorldCutErrorCode.InvalidInput, + Assert.Throws(() => JsonValue.Parse(json)).Code); + } + + [Fact] + public void Non_ascii_content_survives_a_full_verification_round_trip() + { + string json = Fixtures.CoherentInput() + .Replace("ci-status-passed", "ci-status-passed-\ud83d\udc0d", StringComparison.Ordinal); + + VerificationResult result = WorldCutVerifier.VerifyJson(json); + + Assert.Contains( + result.RequirementResults, + item => item.RequirementId.Contains('\udc0d', StringComparison.Ordinal)); + Assert.Contains("\ud83d\udc0d", JsonText.Indent(result.ToJson()), StringComparison.Ordinal); + } +} diff --git a/ports/dotnet/tests/WorldCut.Tests/ValidationTests.cs b/ports/dotnet/tests/WorldCut.Tests/ValidationTests.cs new file mode 100644 index 0000000..1f8cae2 --- /dev/null +++ b/ports/dotnet/tests/WorldCut.Tests/ValidationTests.cs @@ -0,0 +1,273 @@ +using WorldCut.Json; + +namespace WorldCut.Tests; + +/// +/// Runtime invariants that the JSON Schema cannot express, mutated from a known +/// good fixture so that each test isolates exactly one rule. +/// +public sealed class ValidationTests +{ + [Fact] + public void The_reference_fixture_is_accepted() + { + VerificationResult result = WorldCutVerifier.VerifyJson(Fixtures.CoherentInput()); + + Assert.Equal(ContractVerdict.ContractSatisfied, result.Verdict); + Assert.Equal("0.1", result.ProtocolVersion); + Assert.Equal("0.1.2", result.EngineVersion); + Assert.Equal("worldcut-json-v1", result.Canonicalization); + Assert.Equal(3, result.Coverage.Required); + Assert.Equal(3, result.Coverage.Satisfied); + Assert.Equal(0, result.Coverage.Violated); + Assert.Equal(0, result.Coverage.Unknown); + Assert.Equal(0, result.Coverage.Advisory); + Assert.Equal(AcquisitionPlanStatus.NotNeeded, result.AcquisitionPlan.Status); + } + + [Theory] + // Transport shape. + [InlineData("[]")] + [InlineData("42")] + [InlineData("null")] + [InlineData("\"input\"")] + [InlineData("{}")] + // Closed field sets. + [InlineData("{\"protocolVersion\":\"0.1\",\"contract\":{},\"observations\":[],\"extra\":1}")] + public void Structurally_wrong_documents_are_rejected(string json) => + Assert.Equal( + WorldCutErrorCode.InvalidInput, + Assert.Throws(() => WorldCutVerifier.VerifyJson(json)).Code); + + [Theory] + [InlineData("\"protocolVersion\":\"0.1\"", "\"protocolVersion\":\"0.2\"")] + [InlineData("\"protocolVersion\":\"0.1\"", "\"protocolVersion\":0.1")] + [InlineData("\"clockModel\":\"trusted_normalized\"", "\"clockModel\":\"untrusted\"")] + [InlineData("\"intervalModel\":\"half_open\"", "\"intervalModel\":\"closed\"")] + [InlineData("\"metadataModel\":\"honest_but_possibly_incomplete\"", "\"metadataModel\":\"complete\"")] + [InlineData("\"provenance\":\"provider_asserted\"", "\"provenance\":\"guessed\"")] + [InlineData("\"relation\":\"exact\"", "\"relation\":\"compatible\"")] + [InlineData("\"acquisitionCost\":1", "\"acquisitionCost\":-1")] + [InlineData("\"acquisitionCost\":1", "\"acquisitionCost\":1.5")] + [InlineData("\"acquisitionCost\":1", "\"acquisitionCost\":1000000001")] + [InlineData("\"acquisitionCost\":1", "\"acquisitionCost\":\"1\"")] + [InlineData("\"acquisitionCost\":1", "\"acquisitionCost\":true")] + [InlineData("\"acquisitionCost\":1", "\"acquisitionCost\":9007199254740993")] + [InlineData("\"version\":\"commit-B\"", "\"version\":\"\"")] + [InlineData("\"id\":\"obs-head-b\"", "\"id\":\"\"")] + [InlineData("\"role\":\"head\"", "\"role\":\"\"")] + [InlineData("\"decisionTime\":\"2026-09-02T18:00:00.000Z\"", "\"decisionTime\":\"2026-09-02\"")] + [InlineData("\"observedAt\":\"2026-09-02T17:59:58.000Z\"", "\"observedAt\":\"2026-09-02T18:00:00.001Z\"")] + [InlineData("\"type\":\"value_equals\"", "\"type\":\"regex_matches\"")] + [InlineData("\"type\":\"value_equals\"", "\"type\":7")] + [InlineData("\"provider\":\"github\"", "\"provider\":\"\"")] + [InlineData("\"kind\":\"branch_head\"", "\"kind\":null")] + [InlineData("\"path\":[\"status\"]", "\"path\":[]")] + [InlineData("\"path\":[\"status\"]", "\"path\":[\"\"]")] + [InlineData("\"path\":[\"status\"]", "\"path\":\"status\"")] + [InlineData("\"roles\":[\"approval\",\"quote\"]", "\"roles\":[\"approval\"]")] + [InlineData("\"roles\":[\"approval\",\"quote\"]", "\"roles\":[\"approval\",\"approval\"]")] + [InlineData("\"observations\":[", "\"observations\":{\"0\":[")] + public void Mutating_one_protocol_invariant_is_rejected(string original, string replacement) + { + string json = Fixtures.CoherentInput(); + Assert.Contains(original, json, StringComparison.Ordinal); + + WorldCutException error = Assert.Throws( + () => WorldCutVerifier.VerifyJson(ReplaceFirst(json, original, replacement))); + + Assert.Equal(WorldCutErrorCode.InvalidInput, error.Code); + Assert.NotEmpty(error.Message); + } + + [Fact] + public void An_empty_validity_interval_is_rejected() + { + string json = Fixtures.CoherentInput().Replace( + "\"from\":\"2026-09-02T17:55:00.000Z\",\"until\":\"2026-09-02T18:00:00.001Z\"", + "\"from\":\"2026-09-02T17:55:00.000Z\",\"until\":\"2026-09-02T17:55:00.000Z\"", + StringComparison.Ordinal); + + Assert.Equal( + WorldCutErrorCode.InvalidInput, + Assert.Throws(() => WorldCutVerifier.VerifyJson(json)).Code); + } + + [Fact] + public void A_contract_of_only_advisory_requirements_is_rejected() + { + string json = Fixtures.CoherentInput() + .Replace("\"type\":\"", "\"required\":false,\"type\":\"", StringComparison.Ordinal); + + Assert.Equal( + WorldCutErrorCode.InvalidInput, + Assert.Throws(() => WorldCutVerifier.VerifyJson(json)).Code); + } + + [Fact] + public void A_non_boolean_required_flag_is_rejected() + { + string json = ReplaceFirst( + Fixtures.CoherentInput(), + "\"type\":\"dependency\"", + "\"required\":\"yes\",\"type\":\"dependency\""); + + Assert.Equal( + WorldCutErrorCode.InvalidInput, + Assert.Throws(() => WorldCutVerifier.VerifyJson(json)).Code); + } + + [Fact] + public void Duplicate_json_members_follow_last_value_wins_during_validation() + { + // The trailing member wins, so a leading unsupported protocol version is + // overwritten by an accepted one, exactly as JSON.parse behaves. + string json = ReplaceFirst( + Fixtures.CoherentInput(), + "{\"protocolVersion\":\"0.1\"", + "{\"protocolVersion\":\"9.9\",\"protocolVersion\":\"0.1\""); + + VerificationResult result = WorldCutVerifier.VerifyJson(json); + + Assert.Equal(ContractVerdict.ContractSatisfied, result.Verdict); + Assert.Equal( + CanonicalJson.Serialize(Fixtures.Expected("coherent")), + CanonicalJson.Serialize(result.ToJson())); + } + + [Fact] + public void Duplicate_members_do_not_trigger_the_rfc8785_duplicate_name_rule() + { + string json = ReplaceFirst( + Fixtures.CoherentInput(), + "\"value\":{\"commit\":\"commit-B\"}", + "\"value\":{\"commit\":\"commit-A\",\"commit\":\"commit-B\"}"); + + VerificationResult result = WorldCutVerifier.VerifyJson(json); + + Assert.Equal(ContractVerdict.ContractSatisfied, result.Verdict); + } + + [Theory] + [InlineData("version")] + [InlineData("validity")] + [InlineData("dependencies")] + public void Optional_witness_members_present_as_null_are_rejected(string member) + { + string json = ValueEqualsInput + .Build(JsonValue.Create("value"), ["ignored"], JsonValue.Create("value")) + .Replace( + "\"witness\":{\"provenance\":\"provider_asserted\"}", + $"\"witness\":{{\"provenance\":\"provider_asserted\",\"{member}\":null}}", + StringComparison.Ordinal); + + Assert.Contains($"\"{member}\":null", json, StringComparison.Ordinal); + Assert.Equal( + WorldCutErrorCode.InvalidInput, + Assert.Throws(() => WorldCutVerifier.VerifyJson(json)).Code); + } + + [Fact] + public void Advisory_requirements_do_not_change_the_verdict_or_the_plan() + { + JsonValue advisory = ConformanceCorpus.Member( + ConformanceCorpus.Case("verification-vectors.json", "advisory-unknown"), + "expected"); + + Assert.Equal("CONTRACT_SATISFIED", ConformanceCorpus.Member(advisory, "verdict").GetString()); + Assert.Equal( + 1, + ConformanceCorpus.Member(ConformanceCorpus.Member(advisory, "coverage"), "advisory").GetNumber()); + Assert.Equal( + "NOT_NEEDED", + ConformanceCorpus.Member( + ConformanceCorpus.Member(advisory, "acquisitionPlan"), "status").GetString()); + } + + [Fact] + public void A_violated_requirement_dominates_an_unknown_one() + { + JsonValue expected = ConformanceCorpus.Member( + ConformanceCorpus.Case("verification-vectors.json", "violation-dominates-unknown"), + "expected"); + + Assert.Equal("CONTRACT_VIOLATED", ConformanceCorpus.Member(expected, "verdict").GetString()); + } + + [Fact] + public void An_open_ended_validity_interval_is_positive_infinity() + { + JsonValue expected = ConformanceCorpus.Member( + ConformanceCorpus.Case("verification-vectors.json", "open-ended-overlap"), + "expected"); + + Assert.Equal("CONTRACT_SATISFIED", ConformanceCorpus.Member(expected, "verdict").GetString()); + } + + [Fact] + public void Fetch_required_metadata_costs_a_quarter_rounded_up() + { + JsonValue expected = ConformanceCorpus.Member( + ConformanceCorpus.Case("verification-vectors.json", "missing-dependency"), + "expected"); + JsonValue plan = ConformanceCorpus.Member(expected, "acquisitionPlan"); + + foreach (JsonValue action in ConformanceCorpus.Member(plan, "actions").Items) + { + if (string.Equals( + ConformanceCorpus.Member(action, "type").GetString(), + "FETCH_REQUIRED_METADATA", + StringComparison.Ordinal)) + { + Assert.True(ConformanceCorpus.Member(action, "cost").GetNumber() >= 1); + } + } + } + + [Fact] + public void Acquisition_action_identifiers_use_the_lowercase_type_role_and_digest() + { + VerificationResult result = WorldCutVerifier.VerifyJson( + Fixtures.InputWithVerdict("INSUFFICIENT_EVIDENCE")); + + Assert.NotEmpty(result.AcquisitionPlan.Actions); + foreach (AcquisitionAction action in result.AcquisitionPlan.Actions) + { + string[] parts = action.Id.Split(':'); + Assert.Equal(3, parts.Length); +#pragma warning disable CA1308 // The protocol specifies a lowercase action-type prefix. + Assert.Equal(action.Type.ToWireName().ToLowerInvariant(), parts[0]); +#pragma warning restore CA1308 + Assert.Equal(action.Role, parts[1]); + Assert.Equal(12, parts[2].Length); + + if (action.Expected is null) + { + Assert.Equal("none", parts[2]); + } + else + { + Assert.Equal(CanonicalJson.ComputeSha256Hex(action.Expected)[..12], parts[2]); + } + } + } + + [Fact] + public void Requirement_results_are_ordered_by_requirement_identifier() + { + VerificationResult result = WorldCutVerifier.VerifyJson(Fixtures.CoherentInput()); + + var identifiers = result.RequirementResults.Select(item => item.RequirementId).ToArray(); + var sorted = identifiers.ToArray(); + Array.Sort(sorted, Utf16.Compare); + + Assert.Equal(sorted, identifiers); + } + + private static string ReplaceFirst(string source, string original, string replacement) + { + int index = source.IndexOf(original, StringComparison.Ordinal); + Assert.True(index >= 0, $"{original} is not present in the fixture"); + return string.Concat(source.AsSpan(0, index), replacement, source.AsSpan(index + original.Length)); + } +} diff --git a/ports/dotnet/tests/WorldCut.Tests/ValueEqualsInput.cs b/ports/dotnet/tests/WorldCut.Tests/ValueEqualsInput.cs new file mode 100644 index 0000000..cec668e --- /dev/null +++ b/ports/dotnet/tests/WorldCut.Tests/ValueEqualsInput.cs @@ -0,0 +1,70 @@ +using WorldCut.Json; + +namespace WorldCut.Tests; + +/// +/// Builds minimal, valid verification inputs so that individual protocol rules +/// can be exercised without editing a fixture by hand. +/// +internal static class ValueEqualsInput +{ + internal const string DecisionTime = "2026-09-02T18:00:00.000Z"; + internal const string ObservedAt = "2026-09-02T17:00:00.000Z"; + + internal static string Build(JsonValue observed, IReadOnlyList path, JsonValue expected) => + JsonText.Compact(BuildValue(observed, path, expected)); + + internal static JsonValue BuildValue( + JsonValue observed, + IReadOnlyList path, + JsonValue expected) => + JsonValue.CreateObject( + [ + new("protocolVersion", JsonValue.Create("0.1")), + new("contract", JsonValue.CreateObject( + [ + new("id", JsonValue.Create("value-path")), + new("version", JsonValue.Create("1")), + new("decisionTime", JsonValue.Create(DecisionTime)), + new("assumptions", Assumptions), + new("requirements", JsonValue.CreateArray( + JsonValue.CreateObject( + [ + new("id", JsonValue.Create("value")), + new("type", JsonValue.Create("value_equals")), + new("description", JsonValue.Create("The observed value matches")), + new("role", JsonValue.Create("observed")), + new("path", JsonValue.CreateArray(path.Select(JsonValue.Create).ToArray())), + new("expected", expected), + ]))), + ])), + new("observations", JsonValue.CreateArray(Observation("observed", observed))), + ]); + + internal static JsonValue Assumptions => JsonValue.CreateObject( + [ + new("clockModel", JsonValue.Create("trusted_normalized")), + new("intervalModel", JsonValue.Create("half_open")), + new("metadataModel", JsonValue.Create("honest_but_possibly_incomplete")), + ]); + + internal static JsonValue Observation(string role, JsonValue value) => JsonValue.CreateObject( + [ + new("id", JsonValue.Create($"obs-{role}")), + new("role", JsonValue.Create(role)), + new("resource", JsonValue.CreateObject( + [ + new("provider", JsonValue.Create("example")), + new("account", JsonValue.Create("acme")), + new("kind", JsonValue.Create("record")), + new("key", JsonValue.Create(role)), + ])), + new("value", value), + new("observedAt", JsonValue.Create(ObservedAt)), + new("acquisitionCost", JsonValue.Create(1)), + new("witness", JsonValue.CreateObject( + [ + new("provenance", JsonValue.Create("provider_asserted")), + ])), + ]); +} diff --git a/ports/dotnet/tests/WorldCut.Tests/ValuePathTests.cs b/ports/dotnet/tests/WorldCut.Tests/ValuePathTests.cs new file mode 100644 index 0000000..3777060 --- /dev/null +++ b/ports/dotnet/tests/WorldCut.Tests/ValuePathTests.cs @@ -0,0 +1,96 @@ +using WorldCut.Json; + +namespace WorldCut.Tests; + +/// +/// The value_equals path grammar, including the array-index rules that +/// keep length and non-canonical numbers from addressing elements. +/// +public sealed class ValuePathTests +{ + [Theory] + [InlineData("[10,20,30]", "0", "10")] + [InlineData("[10,20,30]", "2", "30")] + [InlineData("{\"a\":{\"b\":1}}", "a", "{\"b\":1}")] + [InlineData("{\" \":\"space\"}", " ", "\"space\"")] + [InlineData("{\"0\":\"zero\"}", "0", "\"zero\"")] + [InlineData("{\"length\":7}", "length", "7")] + public void Resolvable_paths_return_the_addressed_value(string document, string segment, string expected) + { + JsonValue observed = JsonValue.Parse(document); + + Assert.True(Verify(observed, [segment], JsonValue.Parse(expected))); + } + + [Theory] + [InlineData("length")] + [InlineData("00")] + [InlineData("01")] + [InlineData("+1")] + [InlineData("-1")] + [InlineData("1.0")] + [InlineData("1e0")] + [InlineData(" 1")] + [InlineData("1 ")] + [InlineData("0x1")] + [InlineData("3")] + [InlineData("99999999999999999999")] + [InlineData("9007199254740992")] + public void Non_canonical_array_indexes_never_address_an_element(string segment) + { + JsonValue observed = JsonValue.Parse("[10,20,30]"); + + Assert.False(Resolves(observed, [segment])); + } + + [Fact] + public void Scalars_have_no_addressable_members() + { + Assert.False(Resolves(JsonValue.Parse("42"), ["0"])); + Assert.False(Resolves(JsonValue.Parse("\"text\""), ["0"])); + Assert.False(Resolves(JsonValue.Parse("null"), ["a"])); + Assert.False(Resolves(JsonValue.Parse("true"), ["a"])); + } + + [Fact] + public void Nested_paths_traverse_objects_and_arrays() + { + JsonValue observed = JsonValue.Parse("{\"runs\":[{\"status\":\"passed\"}]}"); + + Assert.True(Verify(observed, ["runs", "0", "status"], JsonValue.Create("passed"))); + Assert.False(Resolves(observed, ["runs", "1", "status"])); + Assert.False(Resolves(observed, ["runs", "0", "missing"])); + } + + [Fact] + public void The_array_index_grammar_matches_the_committed_vectors() + { + JsonValue index = ConformanceCorpus.Case("verification-vectors.json", "array-index"); + JsonValue length = ConformanceCorpus.Case( + "verification-vectors.json", + "array-length-is-not-a-value-path"); + + Assert.Equal( + "CONTRACT_SATISFIED", + ConformanceCorpus.Member(ConformanceCorpus.Member(index, "expected"), "verdict").GetString()); + Assert.Equal( + "INSUFFICIENT_EVIDENCE", + ConformanceCorpus.Member(ConformanceCorpus.Member(length, "expected"), "verdict").GetString()); + } + + private static readonly JsonValue Sentinel = + JsonValue.Parse("{\"__worldcut_unreachable_sentinel__\":true}"); + + private static bool Resolves(JsonValue observed, string[] path) => + Evaluate(observed, path, Sentinel).Status != RequirementStatus.Unknown; + + private static bool Verify(JsonValue observed, string[] path, JsonValue expected) => + Evaluate(observed, path, expected).Status == RequirementStatus.Satisfied; + + private static RequirementResult Evaluate(JsonValue observed, string[] path, JsonValue expected) + { + VerificationResult result = WorldCutVerifier.VerifyJson( + ValueEqualsInput.Build(observed, path, expected)); + return Assert.Single(result.RequirementResults); + } +} diff --git a/ports/dotnet/tests/WorldCut.Tests/WorldCut.Tests.csproj b/ports/dotnet/tests/WorldCut.Tests/WorldCut.Tests.csproj new file mode 100644 index 0000000..72433d2 --- /dev/null +++ b/ports/dotnet/tests/WorldCut.Tests/WorldCut.Tests.csproj @@ -0,0 +1,33 @@ + + + + net8.0;net10.0 + Exe + WorldCut.Tests + WorldCut.Tests + false + true + true + true + $(NoWarn);CS1591;CA1515 + + + + + + + + + + + + + + + + + diff --git a/ports/dotnet/tests/WorldCut.Tests/data/conformance/0.1/canonicalization-vectors.json b/ports/dotnet/tests/WorldCut.Tests/data/conformance/0.1/canonicalization-vectors.json new file mode 100644 index 0000000..9374a45 --- /dev/null +++ b/ports/dotnet/tests/WorldCut.Tests/data/conformance/0.1/canonicalization-vectors.json @@ -0,0 +1,60 @@ +{ + "canonicalization": "worldcut-json-v1", + "cases": [ + { + "name": "object-key-order", + "value": { + "z": 1, + "a": 2 + }, + "expectedCanonicalJson": "{\"a\":2,\"z\":1}", + "expectedSha256": "c2985c5ba6f7d2a55e768f92490ca09388e95bc4cccb9fdf11b15f4d42f93e73" + }, + { + "name": "nested", + "value": { + "b": [ + true, + null, + { + "y": "two", + "x": "one" + } + ], + "a": 0 + }, + "expectedCanonicalJson": "{\"a\":0,\"b\":[true,null,{\"x\":\"one\",\"y\":\"two\"}]}", + "expectedSha256": "c7b434323f6442528834059ff019ce0155446ab48085829dcfeb497934c89e61" + }, + { + "name": "unicode-key-order", + "value": { + "1": "one", + "\r": "cr", + "€": "euro", + "😀": "face", + "ö": "o" + }, + "expectedCanonicalJson": "{\"\\r\":\"cr\",\"1\":\"one\",\"ö\":\"o\",\"€\":\"euro\",\"😀\":\"face\"}", + "expectedSha256": "7d173b0f794426c197248618236ad042c878322f16664325ee097a6c697fd60d" + }, + { + "name": "numbers", + "value": [ + 333333333.3333333, + 1e+30, + 4.5, + 0.002, + 1e-27 + ], + "expectedCanonicalJson": "[333333333.3333333,1e+30,4.5,0.002,1e-27]", + "expectedSha256": "7c6bc86d861387d823ae596b79ca0b26567dddc22a77acb1f4e06d441f555adf" + }, + { + "name": "escaped-string", + "value": "€$\u000f\nA'B\"\\\\\"/", + "expectedCanonicalJson": "\"€$\\u000f\\nA'B\\\"\\\\\\\\\\\"/\"", + "expectedSha256": "48caa679d7f31885aac4a400aa52732f9b304c1a417dfa6e75631e81ca2f785c" + } + ] +} diff --git a/ports/dotnet/tests/WorldCut.Tests/data/conformance/0.1/invalid-vectors.json b/ports/dotnet/tests/WorldCut.Tests/data/conformance/0.1/invalid-vectors.json new file mode 100644 index 0000000..ddd5d3f --- /dev/null +++ b/ports/dotnet/tests/WorldCut.Tests/data/conformance/0.1/invalid-vectors.json @@ -0,0 +1,1833 @@ +{ + "protocolVersion": "0.1", + "cases": [ + { + "name": "unsupported-protocol", + "input": { + "protocolVersion": "1.0", + "contract": { + "id": "deploy-current-tested-release", + "version": "1", + "decisionTime": "2026-09-02T18:00:00.000Z", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "ci-tested-current-head", + "type": "dependency", + "description": "The passing CI run tested the selected branch head", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "ci-status-passed", + "type": "value_equals", + "description": "The CI status is passed", + "role": "ci", + "path": [ + "status" + ], + "expected": "passed" + }, + { + "id": "approval-and-quote-overlap", + "type": "common_valid_time", + "description": "The approval and quote were valid at a common time", + "roles": [ + "approval", + "quote" + ], + "within": { + "from": "2026-09-02T17:55:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + } + } + ] + }, + "observations": [ + { + "id": "obs-head-b", + "role": "head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "value": { + "commit": "commit-B" + }, + "observedAt": "2026-09-02T17:59:58.000Z", + "acquisitionCost": 1, + "witness": { + "provenance": "provider_asserted", + "version": "commit-B" + } + }, + { + "id": "obs-ci-b", + "role": "ci", + "resource": { + "provider": "ci.example", + "account": "acme", + "kind": "ci_run", + "key": "run-2041" + }, + "value": { + "status": "passed" + }, + "observedAt": "2026-09-02T17:59:58.500Z", + "acquisitionCost": 4, + "witness": { + "provenance": "provider_asserted", + "version": "run-2041", + "dependencies": [ + { + "name": "tested_head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "relation": "exact", + "version": "commit-B", + "provenance": "provider_asserted" + } + ] + } + }, + { + "id": "obs-approval", + "role": "approval", + "resource": { + "provider": "change.example", + "account": "acme", + "kind": "approval", + "key": "release-2041" + }, + "value": { + "approved": true + }, + "observedAt": "2026-09-02T17:59:59.000Z", + "acquisitionCost": 2, + "witness": { + "provenance": "provider_asserted", + "version": "approval-8", + "validity": { + "from": "2026-09-02T17:50:00.000Z", + "until": "2026-09-02T18:05:00.000Z" + } + } + }, + { + "id": "obs-quote", + "role": "quote", + "resource": { + "provider": "pricing.example", + "account": "acme", + "kind": "quote", + "key": "release-2041" + }, + "value": { + "amount": 1250, + "currency": "USD" + }, + "observedAt": "2026-09-02T17:59:59.500Z", + "acquisitionCost": 3, + "witness": { + "provenance": "provider_asserted", + "version": "quote-17", + "validity": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:03:00.000Z" + } + } + } + ] + }, + "expectedErrorCode": "WORLDCUT_INVALID_INPUT" + }, + { + "name": "duplicate-role", + "input": { + "protocolVersion": "0.1", + "contract": { + "id": "deploy-current-tested-release", + "version": "1", + "decisionTime": "2026-09-02T18:00:00.000Z", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "ci-tested-current-head", + "type": "dependency", + "description": "The passing CI run tested the selected branch head", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "ci-status-passed", + "type": "value_equals", + "description": "The CI status is passed", + "role": "ci", + "path": [ + "status" + ], + "expected": "passed" + }, + { + "id": "approval-and-quote-overlap", + "type": "common_valid_time", + "description": "The approval and quote were valid at a common time", + "roles": [ + "approval", + "quote" + ], + "within": { + "from": "2026-09-02T17:55:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + } + } + ] + }, + "observations": [ + { + "id": "obs-head-b", + "role": "head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "value": { + "commit": "commit-B" + }, + "observedAt": "2026-09-02T17:59:58.000Z", + "acquisitionCost": 1, + "witness": { + "provenance": "provider_asserted", + "version": "commit-B" + } + }, + { + "id": "obs-ci-b", + "role": "head", + "resource": { + "provider": "ci.example", + "account": "acme", + "kind": "ci_run", + "key": "run-2041" + }, + "value": { + "status": "passed" + }, + "observedAt": "2026-09-02T17:59:58.500Z", + "acquisitionCost": 4, + "witness": { + "provenance": "provider_asserted", + "version": "run-2041", + "dependencies": [ + { + "name": "tested_head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "relation": "exact", + "version": "commit-B", + "provenance": "provider_asserted" + } + ] + } + }, + { + "id": "obs-approval", + "role": "approval", + "resource": { + "provider": "change.example", + "account": "acme", + "kind": "approval", + "key": "release-2041" + }, + "value": { + "approved": true + }, + "observedAt": "2026-09-02T17:59:59.000Z", + "acquisitionCost": 2, + "witness": { + "provenance": "provider_asserted", + "version": "approval-8", + "validity": { + "from": "2026-09-02T17:50:00.000Z", + "until": "2026-09-02T18:05:00.000Z" + } + } + }, + { + "id": "obs-quote", + "role": "quote", + "resource": { + "provider": "pricing.example", + "account": "acme", + "kind": "quote", + "key": "release-2041" + }, + "value": { + "amount": 1250, + "currency": "USD" + }, + "observedAt": "2026-09-02T17:59:59.500Z", + "acquisitionCost": 3, + "witness": { + "provenance": "provider_asserted", + "version": "quote-17", + "validity": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:03:00.000Z" + } + } + } + ] + }, + "expectedErrorCode": "WORLDCUT_INVALID_INPUT" + }, + { + "name": "duplicate-observation-id", + "input": { + "protocolVersion": "0.1", + "contract": { + "id": "deploy-current-tested-release", + "version": "1", + "decisionTime": "2026-09-02T18:00:00.000Z", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "ci-tested-current-head", + "type": "dependency", + "description": "The passing CI run tested the selected branch head", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "ci-status-passed", + "type": "value_equals", + "description": "The CI status is passed", + "role": "ci", + "path": [ + "status" + ], + "expected": "passed" + }, + { + "id": "approval-and-quote-overlap", + "type": "common_valid_time", + "description": "The approval and quote were valid at a common time", + "roles": [ + "approval", + "quote" + ], + "within": { + "from": "2026-09-02T17:55:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + } + } + ] + }, + "observations": [ + { + "id": "obs-head-b", + "role": "head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "value": { + "commit": "commit-B" + }, + "observedAt": "2026-09-02T17:59:58.000Z", + "acquisitionCost": 1, + "witness": { + "provenance": "provider_asserted", + "version": "commit-B" + } + }, + { + "id": "obs-head-b", + "role": "ci", + "resource": { + "provider": "ci.example", + "account": "acme", + "kind": "ci_run", + "key": "run-2041" + }, + "value": { + "status": "passed" + }, + "observedAt": "2026-09-02T17:59:58.500Z", + "acquisitionCost": 4, + "witness": { + "provenance": "provider_asserted", + "version": "run-2041", + "dependencies": [ + { + "name": "tested_head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "relation": "exact", + "version": "commit-B", + "provenance": "provider_asserted" + } + ] + } + }, + { + "id": "obs-approval", + "role": "approval", + "resource": { + "provider": "change.example", + "account": "acme", + "kind": "approval", + "key": "release-2041" + }, + "value": { + "approved": true + }, + "observedAt": "2026-09-02T17:59:59.000Z", + "acquisitionCost": 2, + "witness": { + "provenance": "provider_asserted", + "version": "approval-8", + "validity": { + "from": "2026-09-02T17:50:00.000Z", + "until": "2026-09-02T18:05:00.000Z" + } + } + }, + { + "id": "obs-quote", + "role": "quote", + "resource": { + "provider": "pricing.example", + "account": "acme", + "kind": "quote", + "key": "release-2041" + }, + "value": { + "amount": 1250, + "currency": "USD" + }, + "observedAt": "2026-09-02T17:59:59.500Z", + "acquisitionCost": 3, + "witness": { + "provenance": "provider_asserted", + "version": "quote-17", + "validity": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:03:00.000Z" + } + } + } + ] + }, + "expectedErrorCode": "WORLDCUT_INVALID_INPUT" + }, + { + "name": "invalid-interval", + "input": { + "protocolVersion": "0.1", + "contract": { + "id": "deploy-current-tested-release", + "version": "1", + "decisionTime": "2026-09-02T18:00:00.000Z", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "ci-tested-current-head", + "type": "dependency", + "description": "The passing CI run tested the selected branch head", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "ci-status-passed", + "type": "value_equals", + "description": "The CI status is passed", + "role": "ci", + "path": [ + "status" + ], + "expected": "passed" + }, + { + "id": "approval-and-quote-overlap", + "type": "common_valid_time", + "description": "The approval and quote were valid at a common time", + "roles": [ + "approval", + "quote" + ], + "within": { + "from": "2026-09-02T17:55:00.000Z", + "until": "2026-09-02T17:55:00.000Z" + } + } + ] + }, + "observations": [ + { + "id": "obs-head-b", + "role": "head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "value": { + "commit": "commit-B" + }, + "observedAt": "2026-09-02T17:59:58.000Z", + "acquisitionCost": 1, + "witness": { + "provenance": "provider_asserted", + "version": "commit-B" + } + }, + { + "id": "obs-ci-b", + "role": "ci", + "resource": { + "provider": "ci.example", + "account": "acme", + "kind": "ci_run", + "key": "run-2041" + }, + "value": { + "status": "passed" + }, + "observedAt": "2026-09-02T17:59:58.500Z", + "acquisitionCost": 4, + "witness": { + "provenance": "provider_asserted", + "version": "run-2041", + "dependencies": [ + { + "name": "tested_head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "relation": "exact", + "version": "commit-B", + "provenance": "provider_asserted" + } + ] + } + }, + { + "id": "obs-approval", + "role": "approval", + "resource": { + "provider": "change.example", + "account": "acme", + "kind": "approval", + "key": "release-2041" + }, + "value": { + "approved": true + }, + "observedAt": "2026-09-02T17:59:59.000Z", + "acquisitionCost": 2, + "witness": { + "provenance": "provider_asserted", + "version": "approval-8", + "validity": { + "from": "2026-09-02T17:50:00.000Z", + "until": "2026-09-02T18:05:00.000Z" + } + } + }, + { + "id": "obs-quote", + "role": "quote", + "resource": { + "provider": "pricing.example", + "account": "acme", + "kind": "quote", + "key": "release-2041" + }, + "value": { + "amount": 1250, + "currency": "USD" + }, + "observedAt": "2026-09-02T17:59:59.500Z", + "acquisitionCost": 3, + "witness": { + "provenance": "provider_asserted", + "version": "quote-17", + "validity": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:03:00.000Z" + } + } + } + ] + }, + "expectedErrorCode": "WORLDCUT_INVALID_INPUT" + }, + { + "name": "future-observation", + "input": { + "protocolVersion": "0.1", + "contract": { + "id": "deploy-current-tested-release", + "version": "1", + "decisionTime": "2026-09-02T18:00:00.000Z", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "ci-tested-current-head", + "type": "dependency", + "description": "The passing CI run tested the selected branch head", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "ci-status-passed", + "type": "value_equals", + "description": "The CI status is passed", + "role": "ci", + "path": [ + "status" + ], + "expected": "passed" + }, + { + "id": "approval-and-quote-overlap", + "type": "common_valid_time", + "description": "The approval and quote were valid at a common time", + "roles": [ + "approval", + "quote" + ], + "within": { + "from": "2026-09-02T17:55:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + } + } + ] + }, + "observations": [ + { + "id": "obs-head-b", + "role": "head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "value": { + "commit": "commit-B" + }, + "observedAt": "2026-09-02T18:00:00.001Z", + "acquisitionCost": 1, + "witness": { + "provenance": "provider_asserted", + "version": "commit-B" + } + }, + { + "id": "obs-ci-b", + "role": "ci", + "resource": { + "provider": "ci.example", + "account": "acme", + "kind": "ci_run", + "key": "run-2041" + }, + "value": { + "status": "passed" + }, + "observedAt": "2026-09-02T17:59:58.500Z", + "acquisitionCost": 4, + "witness": { + "provenance": "provider_asserted", + "version": "run-2041", + "dependencies": [ + { + "name": "tested_head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "relation": "exact", + "version": "commit-B", + "provenance": "provider_asserted" + } + ] + } + }, + { + "id": "obs-approval", + "role": "approval", + "resource": { + "provider": "change.example", + "account": "acme", + "kind": "approval", + "key": "release-2041" + }, + "value": { + "approved": true + }, + "observedAt": "2026-09-02T17:59:59.000Z", + "acquisitionCost": 2, + "witness": { + "provenance": "provider_asserted", + "version": "approval-8", + "validity": { + "from": "2026-09-02T17:50:00.000Z", + "until": "2026-09-02T18:05:00.000Z" + } + } + }, + { + "id": "obs-quote", + "role": "quote", + "resource": { + "provider": "pricing.example", + "account": "acme", + "kind": "quote", + "key": "release-2041" + }, + "value": { + "amount": 1250, + "currency": "USD" + }, + "observedAt": "2026-09-02T17:59:59.500Z", + "acquisitionCost": 3, + "witness": { + "provenance": "provider_asserted", + "version": "quote-17", + "validity": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:03:00.000Z" + } + } + } + ] + }, + "expectedErrorCode": "WORLDCUT_INVALID_INPUT" + }, + { + "name": "excessive-acquisition-cost", + "input": { + "protocolVersion": "0.1", + "contract": { + "id": "deploy-current-tested-release", + "version": "1", + "decisionTime": "2026-09-02T18:00:00.000Z", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "ci-tested-current-head", + "type": "dependency", + "description": "The passing CI run tested the selected branch head", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "ci-status-passed", + "type": "value_equals", + "description": "The CI status is passed", + "role": "ci", + "path": [ + "status" + ], + "expected": "passed" + }, + { + "id": "approval-and-quote-overlap", + "type": "common_valid_time", + "description": "The approval and quote were valid at a common time", + "roles": [ + "approval", + "quote" + ], + "within": { + "from": "2026-09-02T17:55:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + } + } + ] + }, + "observations": [ + { + "id": "obs-head-b", + "role": "head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "value": { + "commit": "commit-B" + }, + "observedAt": "2026-09-02T17:59:58.000Z", + "acquisitionCost": 1000000001, + "witness": { + "provenance": "provider_asserted", + "version": "commit-B" + } + }, + { + "id": "obs-ci-b", + "role": "ci", + "resource": { + "provider": "ci.example", + "account": "acme", + "kind": "ci_run", + "key": "run-2041" + }, + "value": { + "status": "passed" + }, + "observedAt": "2026-09-02T17:59:58.500Z", + "acquisitionCost": 4, + "witness": { + "provenance": "provider_asserted", + "version": "run-2041", + "dependencies": [ + { + "name": "tested_head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "relation": "exact", + "version": "commit-B", + "provenance": "provider_asserted" + } + ] + } + }, + { + "id": "obs-approval", + "role": "approval", + "resource": { + "provider": "change.example", + "account": "acme", + "kind": "approval", + "key": "release-2041" + }, + "value": { + "approved": true + }, + "observedAt": "2026-09-02T17:59:59.000Z", + "acquisitionCost": 2, + "witness": { + "provenance": "provider_asserted", + "version": "approval-8", + "validity": { + "from": "2026-09-02T17:50:00.000Z", + "until": "2026-09-02T18:05:00.000Z" + } + } + }, + { + "id": "obs-quote", + "role": "quote", + "resource": { + "provider": "pricing.example", + "account": "acme", + "kind": "quote", + "key": "release-2041" + }, + "value": { + "amount": 1250, + "currency": "USD" + }, + "observedAt": "2026-09-02T17:59:59.500Z", + "acquisitionCost": 3, + "witness": { + "provenance": "provider_asserted", + "version": "quote-17", + "validity": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:03:00.000Z" + } + } + } + ] + }, + "expectedErrorCode": "WORLDCUT_INVALID_INPUT" + }, + { + "name": "fractional-acquisition-cost", + "input": { + "protocolVersion": "0.1", + "contract": { + "id": "deploy-current-tested-release", + "version": "1", + "decisionTime": "2026-09-02T18:00:00.000Z", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "ci-tested-current-head", + "type": "dependency", + "description": "The passing CI run tested the selected branch head", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "ci-status-passed", + "type": "value_equals", + "description": "The CI status is passed", + "role": "ci", + "path": [ + "status" + ], + "expected": "passed" + }, + { + "id": "approval-and-quote-overlap", + "type": "common_valid_time", + "description": "The approval and quote were valid at a common time", + "roles": [ + "approval", + "quote" + ], + "within": { + "from": "2026-09-02T17:55:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + } + } + ] + }, + "observations": [ + { + "id": "obs-head-b", + "role": "head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "value": { + "commit": "commit-B" + }, + "observedAt": "2026-09-02T17:59:58.000Z", + "acquisitionCost": 1.2, + "witness": { + "provenance": "provider_asserted", + "version": "commit-B" + } + }, + { + "id": "obs-ci-b", + "role": "ci", + "resource": { + "provider": "ci.example", + "account": "acme", + "kind": "ci_run", + "key": "run-2041" + }, + "value": { + "status": "passed" + }, + "observedAt": "2026-09-02T17:59:58.500Z", + "acquisitionCost": 4, + "witness": { + "provenance": "provider_asserted", + "version": "run-2041", + "dependencies": [ + { + "name": "tested_head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "relation": "exact", + "version": "commit-B", + "provenance": "provider_asserted" + } + ] + } + }, + { + "id": "obs-approval", + "role": "approval", + "resource": { + "provider": "change.example", + "account": "acme", + "kind": "approval", + "key": "release-2041" + }, + "value": { + "approved": true + }, + "observedAt": "2026-09-02T17:59:59.000Z", + "acquisitionCost": 2, + "witness": { + "provenance": "provider_asserted", + "version": "approval-8", + "validity": { + "from": "2026-09-02T17:50:00.000Z", + "until": "2026-09-02T18:05:00.000Z" + } + } + }, + { + "id": "obs-quote", + "role": "quote", + "resource": { + "provider": "pricing.example", + "account": "acme", + "kind": "quote", + "key": "release-2041" + }, + "value": { + "amount": 1250, + "currency": "USD" + }, + "observedAt": "2026-09-02T17:59:59.500Z", + "acquisitionCost": 3, + "witness": { + "provenance": "provider_asserted", + "version": "quote-17", + "validity": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:03:00.000Z" + } + } + } + ] + }, + "expectedErrorCode": "WORLDCUT_INVALID_INPUT" + }, + { + "name": "unsupported-field", + "input": { + "protocolVersion": "0.1", + "contract": { + "id": "deploy-current-tested-release", + "version": "1", + "decisionTime": "2026-09-02T18:00:00.000Z", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "ci-tested-current-head", + "type": "dependency", + "description": "The passing CI run tested the selected branch head", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "ci-status-passed", + "type": "value_equals", + "description": "The CI status is passed", + "role": "ci", + "path": [ + "status" + ], + "expected": "passed" + }, + { + "id": "approval-and-quote-overlap", + "type": "common_valid_time", + "description": "The approval and quote were valid at a common time", + "roles": [ + "approval", + "quote" + ], + "within": { + "from": "2026-09-02T17:55:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + } + } + ], + "unsupported": true + }, + "observations": [ + { + "id": "obs-head-b", + "role": "head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "value": { + "commit": "commit-B" + }, + "observedAt": "2026-09-02T17:59:58.000Z", + "acquisitionCost": 1, + "witness": { + "provenance": "provider_asserted", + "version": "commit-B" + } + }, + { + "id": "obs-ci-b", + "role": "ci", + "resource": { + "provider": "ci.example", + "account": "acme", + "kind": "ci_run", + "key": "run-2041" + }, + "value": { + "status": "passed" + }, + "observedAt": "2026-09-02T17:59:58.500Z", + "acquisitionCost": 4, + "witness": { + "provenance": "provider_asserted", + "version": "run-2041", + "dependencies": [ + { + "name": "tested_head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "relation": "exact", + "version": "commit-B", + "provenance": "provider_asserted" + } + ] + } + }, + { + "id": "obs-approval", + "role": "approval", + "resource": { + "provider": "change.example", + "account": "acme", + "kind": "approval", + "key": "release-2041" + }, + "value": { + "approved": true + }, + "observedAt": "2026-09-02T17:59:59.000Z", + "acquisitionCost": 2, + "witness": { + "provenance": "provider_asserted", + "version": "approval-8", + "validity": { + "from": "2026-09-02T17:50:00.000Z", + "until": "2026-09-02T18:05:00.000Z" + } + } + }, + { + "id": "obs-quote", + "role": "quote", + "resource": { + "provider": "pricing.example", + "account": "acme", + "kind": "quote", + "key": "release-2041" + }, + "value": { + "amount": 1250, + "currency": "USD" + }, + "observedAt": "2026-09-02T17:59:59.500Z", + "acquisitionCost": 3, + "witness": { + "provenance": "provider_asserted", + "version": "quote-17", + "validity": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:03:00.000Z" + } + } + } + ] + }, + "expectedErrorCode": "WORLDCUT_INVALID_INPUT" + }, + { + "name": "all-advisory", + "input": { + "protocolVersion": "0.1", + "contract": { + "id": "deploy-current-tested-release", + "version": "1", + "decisionTime": "2026-09-02T18:00:00.000Z", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "ci-tested-current-head", + "type": "dependency", + "description": "The passing CI run tested the selected branch head", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head", + "required": false + }, + { + "id": "ci-status-passed", + "type": "value_equals", + "description": "The CI status is passed", + "role": "ci", + "path": [ + "status" + ], + "expected": "passed", + "required": false + }, + { + "id": "approval-and-quote-overlap", + "type": "common_valid_time", + "description": "The approval and quote were valid at a common time", + "roles": [ + "approval", + "quote" + ], + "within": { + "from": "2026-09-02T17:55:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + }, + "required": false + } + ] + }, + "observations": [ + { + "id": "obs-head-b", + "role": "head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "value": { + "commit": "commit-B" + }, + "observedAt": "2026-09-02T17:59:58.000Z", + "acquisitionCost": 1, + "witness": { + "provenance": "provider_asserted", + "version": "commit-B" + } + }, + { + "id": "obs-ci-b", + "role": "ci", + "resource": { + "provider": "ci.example", + "account": "acme", + "kind": "ci_run", + "key": "run-2041" + }, + "value": { + "status": "passed" + }, + "observedAt": "2026-09-02T17:59:58.500Z", + "acquisitionCost": 4, + "witness": { + "provenance": "provider_asserted", + "version": "run-2041", + "dependencies": [ + { + "name": "tested_head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "relation": "exact", + "version": "commit-B", + "provenance": "provider_asserted" + } + ] + } + }, + { + "id": "obs-approval", + "role": "approval", + "resource": { + "provider": "change.example", + "account": "acme", + "kind": "approval", + "key": "release-2041" + }, + "value": { + "approved": true + }, + "observedAt": "2026-09-02T17:59:59.000Z", + "acquisitionCost": 2, + "witness": { + "provenance": "provider_asserted", + "version": "approval-8", + "validity": { + "from": "2026-09-02T17:50:00.000Z", + "until": "2026-09-02T18:05:00.000Z" + } + } + }, + { + "id": "obs-quote", + "role": "quote", + "resource": { + "provider": "pricing.example", + "account": "acme", + "kind": "quote", + "key": "release-2041" + }, + "value": { + "amount": 1250, + "currency": "USD" + }, + "observedAt": "2026-09-02T17:59:59.500Z", + "acquisitionCost": 3, + "witness": { + "provenance": "provider_asserted", + "version": "quote-17", + "validity": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:03:00.000Z" + } + } + } + ] + }, + "expectedErrorCode": "WORLDCUT_INVALID_INPUT" + }, + { + "name": "duplicate-requirement-id", + "input": { + "protocolVersion": "0.1", + "contract": { + "id": "deploy-current-tested-release", + "version": "1", + "decisionTime": "2026-09-02T18:00:00.000Z", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "ci-tested-current-head", + "type": "dependency", + "description": "The passing CI run tested the selected branch head", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "ci-tested-current-head", + "type": "value_equals", + "description": "The CI status is passed", + "role": "ci", + "path": [ + "status" + ], + "expected": "passed" + }, + { + "id": "approval-and-quote-overlap", + "type": "common_valid_time", + "description": "The approval and quote were valid at a common time", + "roles": [ + "approval", + "quote" + ], + "within": { + "from": "2026-09-02T17:55:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + } + } + ] + }, + "observations": [ + { + "id": "obs-head-b", + "role": "head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "value": { + "commit": "commit-B" + }, + "observedAt": "2026-09-02T17:59:58.000Z", + "acquisitionCost": 1, + "witness": { + "provenance": "provider_asserted", + "version": "commit-B" + } + }, + { + "id": "obs-ci-b", + "role": "ci", + "resource": { + "provider": "ci.example", + "account": "acme", + "kind": "ci_run", + "key": "run-2041" + }, + "value": { + "status": "passed" + }, + "observedAt": "2026-09-02T17:59:58.500Z", + "acquisitionCost": 4, + "witness": { + "provenance": "provider_asserted", + "version": "run-2041", + "dependencies": [ + { + "name": "tested_head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "relation": "exact", + "version": "commit-B", + "provenance": "provider_asserted" + } + ] + } + }, + { + "id": "obs-approval", + "role": "approval", + "resource": { + "provider": "change.example", + "account": "acme", + "kind": "approval", + "key": "release-2041" + }, + "value": { + "approved": true + }, + "observedAt": "2026-09-02T17:59:59.000Z", + "acquisitionCost": 2, + "witness": { + "provenance": "provider_asserted", + "version": "approval-8", + "validity": { + "from": "2026-09-02T17:50:00.000Z", + "until": "2026-09-02T18:05:00.000Z" + } + } + }, + { + "id": "obs-quote", + "role": "quote", + "resource": { + "provider": "pricing.example", + "account": "acme", + "kind": "quote", + "key": "release-2041" + }, + "value": { + "amount": 1250, + "currency": "USD" + }, + "observedAt": "2026-09-02T17:59:59.500Z", + "acquisitionCost": 3, + "witness": { + "provenance": "provider_asserted", + "version": "quote-17", + "validity": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:03:00.000Z" + } + } + } + ] + }, + "expectedErrorCode": "WORLDCUT_INVALID_INPUT" + }, + { + "name": "duplicate-dependency-name", + "input": { + "protocolVersion": "0.1", + "contract": { + "id": "deploy-current-tested-release", + "version": "1", + "decisionTime": "2026-09-02T18:00:00.000Z", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "ci-tested-current-head", + "type": "dependency", + "description": "The passing CI run tested the selected branch head", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "ci-status-passed", + "type": "value_equals", + "description": "The CI status is passed", + "role": "ci", + "path": [ + "status" + ], + "expected": "passed" + }, + { + "id": "approval-and-quote-overlap", + "type": "common_valid_time", + "description": "The approval and quote were valid at a common time", + "roles": [ + "approval", + "quote" + ], + "within": { + "from": "2026-09-02T17:55:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + } + } + ] + }, + "observations": [ + { + "id": "obs-head-b", + "role": "head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "value": { + "commit": "commit-B" + }, + "observedAt": "2026-09-02T17:59:58.000Z", + "acquisitionCost": 1, + "witness": { + "provenance": "provider_asserted", + "version": "commit-B" + } + }, + { + "id": "obs-ci-b", + "role": "ci", + "resource": { + "provider": "ci.example", + "account": "acme", + "kind": "ci_run", + "key": "run-2041" + }, + "value": { + "status": "passed" + }, + "observedAt": "2026-09-02T17:59:58.500Z", + "acquisitionCost": 4, + "witness": { + "provenance": "provider_asserted", + "version": "run-2041", + "dependencies": [ + { + "name": "tested_head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "relation": "exact", + "version": "commit-B", + "provenance": "provider_asserted" + }, + { + "name": "tested_head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "relation": "exact", + "version": "commit-B", + "provenance": "provider_asserted" + } + ] + } + }, + { + "id": "obs-approval", + "role": "approval", + "resource": { + "provider": "change.example", + "account": "acme", + "kind": "approval", + "key": "release-2041" + }, + "value": { + "approved": true + }, + "observedAt": "2026-09-02T17:59:59.000Z", + "acquisitionCost": 2, + "witness": { + "provenance": "provider_asserted", + "version": "approval-8", + "validity": { + "from": "2026-09-02T17:50:00.000Z", + "until": "2026-09-02T18:05:00.000Z" + } + } + }, + { + "id": "obs-quote", + "role": "quote", + "resource": { + "provider": "pricing.example", + "account": "acme", + "kind": "quote", + "key": "release-2041" + }, + "value": { + "amount": 1250, + "currency": "USD" + }, + "observedAt": "2026-09-02T17:59:59.500Z", + "acquisitionCost": 3, + "witness": { + "provenance": "provider_asserted", + "version": "quote-17", + "validity": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:03:00.000Z" + } + } + } + ] + }, + "expectedErrorCode": "WORLDCUT_INVALID_INPUT" + }, + { + "name": "invalid-timestamp", + "input": { + "protocolVersion": "0.1", + "contract": { + "id": "deploy-current-tested-release", + "version": "1", + "decisionTime": "2026-09-02", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "ci-tested-current-head", + "type": "dependency", + "description": "The passing CI run tested the selected branch head", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "ci-status-passed", + "type": "value_equals", + "description": "The CI status is passed", + "role": "ci", + "path": [ + "status" + ], + "expected": "passed" + }, + { + "id": "approval-and-quote-overlap", + "type": "common_valid_time", + "description": "The approval and quote were valid at a common time", + "roles": [ + "approval", + "quote" + ], + "within": { + "from": "2026-09-02T17:55:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + } + } + ] + }, + "observations": [ + { + "id": "obs-head-b", + "role": "head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "value": { + "commit": "commit-B" + }, + "observedAt": "2026-09-02T17:59:58.000Z", + "acquisitionCost": 1, + "witness": { + "provenance": "provider_asserted", + "version": "commit-B" + } + }, + { + "id": "obs-ci-b", + "role": "ci", + "resource": { + "provider": "ci.example", + "account": "acme", + "kind": "ci_run", + "key": "run-2041" + }, + "value": { + "status": "passed" + }, + "observedAt": "2026-09-02T17:59:58.500Z", + "acquisitionCost": 4, + "witness": { + "provenance": "provider_asserted", + "version": "run-2041", + "dependencies": [ + { + "name": "tested_head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "relation": "exact", + "version": "commit-B", + "provenance": "provider_asserted" + } + ] + } + }, + { + "id": "obs-approval", + "role": "approval", + "resource": { + "provider": "change.example", + "account": "acme", + "kind": "approval", + "key": "release-2041" + }, + "value": { + "approved": true + }, + "observedAt": "2026-09-02T17:59:59.000Z", + "acquisitionCost": 2, + "witness": { + "provenance": "provider_asserted", + "version": "approval-8", + "validity": { + "from": "2026-09-02T17:50:00.000Z", + "until": "2026-09-02T18:05:00.000Z" + } + } + }, + { + "id": "obs-quote", + "role": "quote", + "resource": { + "provider": "pricing.example", + "account": "acme", + "kind": "quote", + "key": "release-2041" + }, + "value": { + "amount": 1250, + "currency": "USD" + }, + "observedAt": "2026-09-02T17:59:59.500Z", + "acquisitionCost": 3, + "witness": { + "provenance": "provider_asserted", + "version": "quote-17", + "validity": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:03:00.000Z" + } + } + } + ] + }, + "expectedErrorCode": "WORLDCUT_INVALID_INPUT" + } + ] +} diff --git a/ports/dotnet/tests/WorldCut.Tests/data/conformance/0.1/manifest.json b/ports/dotnet/tests/WorldCut.Tests/data/conformance/0.1/manifest.json new file mode 100644 index 0000000..76e3259 --- /dev/null +++ b/ports/dotnet/tests/WorldCut.Tests/data/conformance/0.1/manifest.json @@ -0,0 +1,27 @@ +{ + "protocolVersion": "0.1", + "engineVersion": "0.1.2", + "canonicalization": "worldcut-json-v1", + "files": { + "verification-vectors.json": { + "sha256": "db6771b0bc3953dd74f0020ecabe72317a8a82879094c9ac1bc53f059e42e326", + "cases": 15 + }, + "invalid-vectors.json": { + "sha256": "cb91d4844e5405cf886427b0c7d42dfef9c09b82006991f65d6d6489cdda7bac", + "cases": 12 + }, + "canonicalization-vectors.json": { + "sha256": "bc63e52e23eb8d5e6d99142204158395098b0200d1e9f32a19a4755de2903987", + "cases": 5 + }, + "raw-vectors.json": { + "sha256": "b982317915adfe95761f889500fd553220edee5fe5205ed77dffa74e483e8e60", + "cases": 1 + }, + "raw/unpaired-high-surrogate.json": { + "sha256": "832c9023d610d289d60ef5a3c7d14f2bdff1a29d24bef0b7683c24efae1e4d23", + "bytes": 3635 + } + } +} diff --git a/ports/dotnet/tests/WorldCut.Tests/data/conformance/0.1/raw-vectors.json b/ports/dotnet/tests/WorldCut.Tests/data/conformance/0.1/raw-vectors.json new file mode 100644 index 0000000..c7bf051 --- /dev/null +++ b/ports/dotnet/tests/WorldCut.Tests/data/conformance/0.1/raw-vectors.json @@ -0,0 +1,14 @@ +{ + "protocolVersion": "0.1", + "cases": [ + { + "name": "unpaired-high-surrogate", + "file": "raw/unpaired-high-surrogate.json", + "sha256": "832c9023d610d289d60ef5a3c7d14f2bdff1a29d24bef0b7683c24efae1e4d23", + "acceptedOutcomes": [ + "PARSE_ERROR", + "WORLDCUT_INVALID_INPUT" + ] + } + ] +} diff --git a/ports/dotnet/tests/WorldCut.Tests/data/conformance/0.1/raw/unpaired-high-surrogate.json b/ports/dotnet/tests/WorldCut.Tests/data/conformance/0.1/raw/unpaired-high-surrogate.json new file mode 100644 index 0000000..ad0c0c8 --- /dev/null +++ b/ports/dotnet/tests/WorldCut.Tests/data/conformance/0.1/raw/unpaired-high-surrogate.json @@ -0,0 +1,145 @@ +{ + "protocolVersion": "0.1", + "contract": { + "id": "deploy-current-tested-release", + "version": "1", + "decisionTime": "2026-09-02T18:00:00.000Z", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "ci-tested-current-head", + "type": "dependency", + "description": "The passing CI run tested the selected branch head", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "ci-status-passed", + "type": "value_equals", + "description": "The CI status is passed", + "role": "ci", + "path": [ + "status" + ], + "expected": "passed" + }, + { + "id": "approval-and-quote-overlap", + "type": "common_valid_time", + "description": "The approval and quote were valid at a common time", + "roles": [ + "approval", + "quote" + ], + "within": { + "from": "2026-09-02T17:55:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + } + } + ] + }, + "observations": [ + { + "id": "obs-head-b", + "role": "head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "value": { + "commit": "commit-B" + }, + "observedAt": "2026-09-02T17:59:58.000Z", + "acquisitionCost": 1, + "witness": { + "provenance": "provider_asserted", + "version": "commit-B" + } + }, + { + "id": "obs-ci-b", + "role": "ci", + "resource": { + "provider": "ci.example", + "account": "acme", + "kind": "ci_run", + "key": "run-2041" + }, + "value": "\ud800", + "observedAt": "2026-09-02T17:59:58.500Z", + "acquisitionCost": 4, + "witness": { + "provenance": "provider_asserted", + "version": "run-2041", + "dependencies": [ + { + "name": "tested_head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "relation": "exact", + "version": "commit-B", + "provenance": "provider_asserted" + } + ] + } + }, + { + "id": "obs-approval", + "role": "approval", + "resource": { + "provider": "change.example", + "account": "acme", + "kind": "approval", + "key": "release-2041" + }, + "value": { + "approved": true + }, + "observedAt": "2026-09-02T17:59:59.000Z", + "acquisitionCost": 2, + "witness": { + "provenance": "provider_asserted", + "version": "approval-8", + "validity": { + "from": "2026-09-02T17:50:00.000Z", + "until": "2026-09-02T18:05:00.000Z" + } + } + }, + { + "id": "obs-quote", + "role": "quote", + "resource": { + "provider": "pricing.example", + "account": "acme", + "kind": "quote", + "key": "release-2041" + }, + "value": { + "amount": 1250, + "currency": "USD" + }, + "observedAt": "2026-09-02T17:59:59.500Z", + "acquisitionCost": 3, + "witness": { + "provenance": "provider_asserted", + "version": "quote-17", + "validity": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:03:00.000Z" + } + } + } + ] +} diff --git a/ports/dotnet/tests/WorldCut.Tests/data/conformance/0.1/verification-vectors.json b/ports/dotnet/tests/WorldCut.Tests/data/conformance/0.1/verification-vectors.json new file mode 100644 index 0000000..4033472 --- /dev/null +++ b/ports/dotnet/tests/WorldCut.Tests/data/conformance/0.1/verification-vectors.json @@ -0,0 +1,6422 @@ +{ + "protocolVersion": "0.1", + "engineVersion": "0.1.2", + "cases": [ + { + "name": "coherent", + "input": { + "protocolVersion": "0.1", + "contract": { + "id": "deploy-current-tested-release", + "version": "1", + "decisionTime": "2026-09-02T18:00:00.000Z", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "ci-tested-current-head", + "type": "dependency", + "description": "The passing CI run tested the selected branch head", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "ci-status-passed", + "type": "value_equals", + "description": "The CI status is passed", + "role": "ci", + "path": [ + "status" + ], + "expected": "passed" + }, + { + "id": "approval-and-quote-overlap", + "type": "common_valid_time", + "description": "The approval and quote were valid at a common time", + "roles": [ + "approval", + "quote" + ], + "within": { + "from": "2026-09-02T17:55:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + } + } + ] + }, + "observations": [ + { + "id": "obs-head-b", + "role": "head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "value": { + "commit": "commit-B" + }, + "observedAt": "2026-09-02T17:59:58.000Z", + "acquisitionCost": 1, + "witness": { + "provenance": "provider_asserted", + "version": "commit-B" + } + }, + { + "id": "obs-ci-b", + "role": "ci", + "resource": { + "provider": "ci.example", + "account": "acme", + "kind": "ci_run", + "key": "run-2041" + }, + "value": { + "status": "passed" + }, + "observedAt": "2026-09-02T17:59:58.500Z", + "acquisitionCost": 4, + "witness": { + "provenance": "provider_asserted", + "version": "run-2041", + "dependencies": [ + { + "name": "tested_head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "relation": "exact", + "version": "commit-B", + "provenance": "provider_asserted" + } + ] + } + }, + { + "id": "obs-approval", + "role": "approval", + "resource": { + "provider": "change.example", + "account": "acme", + "kind": "approval", + "key": "release-2041" + }, + "value": { + "approved": true + }, + "observedAt": "2026-09-02T17:59:59.000Z", + "acquisitionCost": 2, + "witness": { + "provenance": "provider_asserted", + "version": "approval-8", + "validity": { + "from": "2026-09-02T17:50:00.000Z", + "until": "2026-09-02T18:05:00.000Z" + } + } + }, + { + "id": "obs-quote", + "role": "quote", + "resource": { + "provider": "pricing.example", + "account": "acme", + "kind": "quote", + "key": "release-2041" + }, + "value": { + "amount": 1250, + "currency": "USD" + }, + "observedAt": "2026-09-02T17:59:59.500Z", + "acquisitionCost": 3, + "witness": { + "provenance": "provider_asserted", + "version": "quote-17", + "validity": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:03:00.000Z" + } + } + } + ] + }, + "expected": { + "protocolVersion": "0.1", + "engineVersion": "0.1.2", + "canonicalization": "worldcut-json-v1", + "contractId": "deploy-current-tested-release", + "contractVersion": "1", + "verdict": "CONTRACT_SATISFIED", + "coverage": { + "required": 3, + "satisfied": 3, + "violated": 0, + "unknown": 0, + "advisory": 0 + }, + "requirementResults": [ + { + "requirementId": "approval-and-quote-overlap", + "requirementType": "common_valid_time", + "required": true, + "status": "SATISFIED", + "summary": "The approval and quote were valid at a common time: a common valid time exists.", + "details": { + "roles": [ + "approval", + "quote" + ], + "commonWindow": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + } + }, + "acquisitionOptions": [] + }, + { + "requirementId": "ci-status-passed", + "requirementType": "value_equals", + "required": true, + "status": "SATISFIED", + "summary": "The CI status is passed: observed value matches the requirement.", + "details": { + "role": "ci", + "path": [ + "status" + ], + "expected": "passed" + }, + "acquisitionOptions": [] + }, + { + "requirementId": "ci-tested-current-head", + "requirementType": "dependency", + "required": true, + "status": "SATISFIED", + "summary": "The passing CI run tested the selected branch head: both roles are bound to commit-B.", + "details": { + "dependentRole": "ci", + "targetRole": "head", + "version": "commit-B" + }, + "acquisitionOptions": [] + } + ], + "acquisitionPlan": { + "status": "NOT_NEEDED", + "reason": null, + "actions": [], + "selectedOptionIds": [], + "totalCost": 0, + "coveredRequirementIds": [], + "unresolvedRequirementIds": [] + }, + "verificationRecordDigest": "f402861e5df539f5ea4b69681431c2e2bcbd18e7cb5a02c63a7cfca3b9bb3eee" + } + }, + { + "name": "dependency-mismatch", + "input": { + "protocolVersion": "0.1", + "contract": { + "id": "deploy-current-tested-head", + "version": "1", + "decisionTime": "2026-09-02T18:00:00.000Z", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "ci-tested-current-head", + "type": "dependency", + "description": "The passing CI run tested the selected branch head", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + } + ] + }, + "observations": [ + { + "id": "obs-head-b", + "role": "head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "value": { + "commit": "commit-B" + }, + "observedAt": "2026-09-02T17:59:59.000Z", + "acquisitionCost": 1, + "witness": { + "provenance": "provider_asserted", + "version": "commit-B" + } + }, + { + "id": "obs-ci-a", + "role": "ci", + "resource": { + "provider": "ci.example", + "account": "acme", + "kind": "ci_run", + "key": "run-2040" + }, + "value": { + "status": "passed" + }, + "observedAt": "2026-09-02T17:59:59.500Z", + "acquisitionCost": 4, + "witness": { + "provenance": "provider_asserted", + "version": "run-2040", + "dependencies": [ + { + "name": "tested_head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "relation": "exact", + "version": "commit-A", + "provenance": "provider_asserted" + } + ] + } + } + ] + }, + "expected": { + "protocolVersion": "0.1", + "engineVersion": "0.1.2", + "canonicalization": "worldcut-json-v1", + "contractId": "deploy-current-tested-head", + "contractVersion": "1", + "verdict": "CONTRACT_VIOLATED", + "coverage": { + "required": 1, + "satisfied": 0, + "violated": 1, + "unknown": 0, + "advisory": 0 + }, + "requirementResults": [ + { + "requirementId": "ci-tested-current-head", + "requirementType": "dependency", + "required": true, + "status": "VIOLATED", + "summary": "The passing CI run tested the selected branch head: commit-A does not equal commit-B.", + "details": { + "dependentRole": "ci", + "dependencyVersion": "commit-A", + "targetRole": "head", + "targetVersion": "commit-B", + "relation": "exact" + }, + "acquisitionOptions": [ + { + "id": "ci-tested-current-head:acquire-compatible-dependent", + "description": "Acquire dependent evidence bound to the selected target version.", + "actions": [ + { + "id": "acquire_compatible_evidence:ci:c969e13b988f", + "type": "ACQUIRE_COMPATIBLE_EVIDENCE", + "role": "ci", + "cost": 4, + "description": "Acquire ci evidence bound to commit-B.", + "expected": { + "targetRole": "head", + "targetVersion": "commit-B" + } + } + ] + }, + { + "id": "ci-tested-current-head:refresh-target", + "description": "Refresh the target before selecting compatible evidence.", + "actions": [ + { + "id": "refresh_observation:head:6df94fb5680d", + "type": "REFRESH_OBSERVATION", + "role": "head", + "cost": 1, + "description": "Refresh head before selecting compatible evidence.", + "expected": { + "dependentRole": "ci", + "dependentVersion": "commit-A" + } + } + ] + } + ] + } + ], + "acquisitionPlan": { + "status": "AVAILABLE", + "reason": null, + "actions": [ + { + "id": "refresh_observation:head:6df94fb5680d", + "type": "REFRESH_OBSERVATION", + "role": "head", + "cost": 1, + "description": "Refresh head before selecting compatible evidence.", + "expected": { + "dependentRole": "ci", + "dependentVersion": "commit-A" + } + } + ], + "selectedOptionIds": [ + "ci-tested-current-head:refresh-target" + ], + "totalCost": 1, + "coveredRequirementIds": [ + "ci-tested-current-head" + ], + "unresolvedRequirementIds": [] + }, + "verificationRecordDigest": "8bb1ccb80d72dd8cd540c9abb6e377c47c28cbcf0ae7b4c169df63f5279ee790" + } + }, + { + "name": "temporal-gap", + "input": { + "protocolVersion": "0.1", + "contract": { + "id": "approved-price-window", + "version": "1", + "decisionTime": "2026-09-02T18:00:00.000Z", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "approval-and-quote-overlap", + "type": "common_valid_time", + "description": "The approval and quote were valid at a common time", + "roles": [ + "approval", + "quote" + ], + "within": { + "from": "2026-09-02T17:55:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + } + } + ] + }, + "observations": [ + { + "id": "obs-approval-expired", + "role": "approval", + "resource": { + "provider": "change.example", + "account": "acme", + "kind": "approval", + "key": "release-2041" + }, + "value": { + "approved": true + }, + "observedAt": "2026-09-02T17:59:59.000Z", + "acquisitionCost": 2, + "witness": { + "provenance": "provider_asserted", + "version": "approval-7", + "validity": { + "from": "2026-09-02T17:55:00.000Z", + "until": "2026-09-02T17:58:00.000Z" + } + } + }, + { + "id": "obs-quote-later", + "role": "quote", + "resource": { + "provider": "pricing.example", + "account": "acme", + "kind": "quote", + "key": "release-2041" + }, + "value": { + "amount": 1250, + "currency": "USD" + }, + "observedAt": "2026-09-02T17:59:59.500Z", + "acquisitionCost": 3, + "witness": { + "provenance": "provider_asserted", + "version": "quote-17", + "validity": { + "from": "2026-09-02T17:58:00.001Z", + "until": "2026-09-02T18:03:00.000Z" + } + } + } + ] + }, + "expected": { + "protocolVersion": "0.1", + "engineVersion": "0.1.2", + "canonicalization": "worldcut-json-v1", + "contractId": "approved-price-window", + "contractVersion": "1", + "verdict": "CONTRACT_VIOLATED", + "coverage": { + "required": 1, + "satisfied": 0, + "violated": 1, + "unknown": 0, + "advisory": 0 + }, + "requirementResults": [ + { + "requirementId": "approval-and-quote-overlap", + "requirementType": "common_valid_time", + "required": true, + "status": "VIOLATED", + "summary": "The approval and quote were valid at a common time: the known validity intervals do not overlap.", + "details": { + "roles": [ + "approval", + "quote" + ], + "latestStart": "2026-09-02T17:58:00.001Z", + "earliestEnd": "2026-09-02T17:58:00.000Z", + "missingRoles": [], + "missingValidityRoles": [] + }, + "acquisitionOptions": [ + { + "id": "approval-and-quote-overlap:refresh-approval", + "description": "Refresh approval and acquire every other missing prerequisite.", + "actions": [ + { + "id": "refresh_observation:approval:03cb6375e080", + "type": "REFRESH_OBSERVATION", + "role": "approval", + "cost": 2, + "description": "Refresh approval to seek a compatible validity window.", + "expected": { + "within": { + "from": "2026-09-02T17:55:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + } + } + } + ] + }, + { + "id": "approval-and-quote-overlap:refresh-quote", + "description": "Refresh quote and acquire every other missing prerequisite.", + "actions": [ + { + "id": "refresh_observation:quote:03cb6375e080", + "type": "REFRESH_OBSERVATION", + "role": "quote", + "cost": 3, + "description": "Refresh quote to seek a compatible validity window.", + "expected": { + "within": { + "from": "2026-09-02T17:55:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + } + } + } + ] + } + ] + } + ], + "acquisitionPlan": { + "status": "AVAILABLE", + "reason": null, + "actions": [ + { + "id": "refresh_observation:approval:03cb6375e080", + "type": "REFRESH_OBSERVATION", + "role": "approval", + "cost": 2, + "description": "Refresh approval to seek a compatible validity window.", + "expected": { + "within": { + "from": "2026-09-02T17:55:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + } + } + } + ], + "selectedOptionIds": [ + "approval-and-quote-overlap:refresh-approval" + ], + "totalCost": 2, + "coveredRequirementIds": [ + "approval-and-quote-overlap" + ], + "unresolvedRequirementIds": [] + }, + "verificationRecordDigest": "da836f28a058ce0e3489637c743efe2617f873fab6600c5f2b1e839eb2779bdf" + } + }, + { + "name": "missing-dependency", + "input": { + "protocolVersion": "0.1", + "contract": { + "id": "deploy-current-tested-head", + "version": "1", + "decisionTime": "2026-09-02T18:00:00.000Z", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "ci-tested-current-head", + "type": "dependency", + "description": "The passing CI run tested the selected branch head", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + } + ] + }, + "observations": [ + { + "id": "obs-head-b", + "role": "head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "value": { + "commit": "commit-B" + }, + "observedAt": "2026-09-02T17:59:59.000Z", + "acquisitionCost": 1, + "witness": { + "provenance": "provider_asserted", + "version": "commit-B" + } + }, + { + "id": "obs-ci-unknown", + "role": "ci", + "resource": { + "provider": "ci.example", + "account": "acme", + "kind": "ci_run", + "key": "run-unknown" + }, + "value": { + "status": "passed" + }, + "observedAt": "2026-09-02T17:59:59.500Z", + "acquisitionCost": 4, + "witness": { + "provenance": "provider_asserted", + "version": "run-unknown" + } + } + ] + }, + "expected": { + "protocolVersion": "0.1", + "engineVersion": "0.1.2", + "canonicalization": "worldcut-json-v1", + "contractId": "deploy-current-tested-head", + "contractVersion": "1", + "verdict": "INSUFFICIENT_EVIDENCE", + "coverage": { + "required": 1, + "satisfied": 0, + "violated": 0, + "unknown": 1, + "advisory": 0 + }, + "requirementResults": [ + { + "requirementId": "ci-tested-current-head", + "requirementType": "dependency", + "required": true, + "status": "UNKNOWN", + "summary": "ci does not expose dependency tested_head.", + "details": { + "dependentRole": "ci", + "targetRole": "head", + "missingDependency": "tested_head" + }, + "acquisitionOptions": [ + { + "id": "ci-tested-current-head:fetch-dependency-metadata", + "description": "Fetch all metadata required to compare the dependency.", + "actions": [ + { + "id": "fetch_required_metadata:ci:1e7092ae3f55", + "type": "FETCH_REQUIRED_METADATA", + "role": "ci", + "cost": 1, + "description": "Fetch dependency metadata for ci.", + "expected": { + "dependencyName": "tested_head", + "targetResource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + } + } + } + ] + } + ] + } + ], + "acquisitionPlan": { + "status": "AVAILABLE", + "reason": null, + "actions": [ + { + "id": "fetch_required_metadata:ci:1e7092ae3f55", + "type": "FETCH_REQUIRED_METADATA", + "role": "ci", + "cost": 1, + "description": "Fetch dependency metadata for ci.", + "expected": { + "dependencyName": "tested_head", + "targetResource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + } + } + } + ], + "selectedOptionIds": [ + "ci-tested-current-head:fetch-dependency-metadata" + ], + "totalCost": 1, + "coveredRequirementIds": [ + "ci-tested-current-head" + ], + "unresolvedRequirementIds": [] + }, + "verificationRecordDigest": "f5a8f5f853b9db0a36e54e9738d8b3aceb102d062659d73752c456e1b74d10f6" + } + }, + { + "name": "value-mismatch", + "input": { + "protocolVersion": "0.1", + "contract": { + "id": "value-mismatch", + "version": "1", + "decisionTime": "2026-09-02T18:00:00.000Z", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "ci-tested-current-head", + "type": "dependency", + "description": "The passing CI run tested the selected branch head", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "ci-status-passed", + "type": "value_equals", + "description": "The CI status is passed", + "role": "ci", + "path": [ + "status" + ], + "expected": "failed" + }, + { + "id": "approval-and-quote-overlap", + "type": "common_valid_time", + "description": "The approval and quote were valid at a common time", + "roles": [ + "approval", + "quote" + ], + "within": { + "from": "2026-09-02T17:55:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + } + } + ] + }, + "observations": [ + { + "id": "obs-head-b", + "role": "head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "value": { + "commit": "commit-B" + }, + "observedAt": "2026-09-02T17:59:58.000Z", + "acquisitionCost": 1, + "witness": { + "provenance": "provider_asserted", + "version": "commit-B" + } + }, + { + "id": "obs-ci-b", + "role": "ci", + "resource": { + "provider": "ci.example", + "account": "acme", + "kind": "ci_run", + "key": "run-2041" + }, + "value": { + "status": "passed" + }, + "observedAt": "2026-09-02T17:59:58.500Z", + "acquisitionCost": 4, + "witness": { + "provenance": "provider_asserted", + "version": "run-2041", + "dependencies": [ + { + "name": "tested_head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "relation": "exact", + "version": "commit-B", + "provenance": "provider_asserted" + } + ] + } + }, + { + "id": "obs-approval", + "role": "approval", + "resource": { + "provider": "change.example", + "account": "acme", + "kind": "approval", + "key": "release-2041" + }, + "value": { + "approved": true + }, + "observedAt": "2026-09-02T17:59:59.000Z", + "acquisitionCost": 2, + "witness": { + "provenance": "provider_asserted", + "version": "approval-8", + "validity": { + "from": "2026-09-02T17:50:00.000Z", + "until": "2026-09-02T18:05:00.000Z" + } + } + }, + { + "id": "obs-quote", + "role": "quote", + "resource": { + "provider": "pricing.example", + "account": "acme", + "kind": "quote", + "key": "release-2041" + }, + "value": { + "amount": 1250, + "currency": "USD" + }, + "observedAt": "2026-09-02T17:59:59.500Z", + "acquisitionCost": 3, + "witness": { + "provenance": "provider_asserted", + "version": "quote-17", + "validity": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:03:00.000Z" + } + } + } + ] + }, + "expected": { + "protocolVersion": "0.1", + "engineVersion": "0.1.2", + "canonicalization": "worldcut-json-v1", + "contractId": "value-mismatch", + "contractVersion": "1", + "verdict": "CONTRACT_VIOLATED", + "coverage": { + "required": 3, + "satisfied": 2, + "violated": 1, + "unknown": 0, + "advisory": 0 + }, + "requirementResults": [ + { + "requirementId": "approval-and-quote-overlap", + "requirementType": "common_valid_time", + "required": true, + "status": "SATISFIED", + "summary": "The approval and quote were valid at a common time: a common valid time exists.", + "details": { + "roles": [ + "approval", + "quote" + ], + "commonWindow": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + } + }, + "acquisitionOptions": [] + }, + { + "requirementId": "ci-status-passed", + "requirementType": "value_equals", + "required": true, + "status": "VIOLATED", + "summary": "The CI status is passed: observed value does not equal the required value.", + "details": { + "role": "ci", + "path": [ + "status" + ], + "expected": "failed", + "actual": "passed" + }, + "acquisitionOptions": [ + { + "id": "ci-status-passed:refresh-value", + "description": "Refresh the observation before evaluating the value again.", + "actions": [ + { + "id": "refresh_observation:ci:b07de73b4894", + "type": "REFRESH_OBSERVATION", + "role": "ci", + "cost": 4, + "description": "Refresh ci before evaluating status.", + "expected": { + "path": [ + "status" + ], + "expected": "failed" + } + } + ] + } + ] + }, + { + "requirementId": "ci-tested-current-head", + "requirementType": "dependency", + "required": true, + "status": "SATISFIED", + "summary": "The passing CI run tested the selected branch head: both roles are bound to commit-B.", + "details": { + "dependentRole": "ci", + "targetRole": "head", + "version": "commit-B" + }, + "acquisitionOptions": [] + } + ], + "acquisitionPlan": { + "status": "AVAILABLE", + "reason": null, + "actions": [ + { + "id": "refresh_observation:ci:b07de73b4894", + "type": "REFRESH_OBSERVATION", + "role": "ci", + "cost": 4, + "description": "Refresh ci before evaluating status.", + "expected": { + "path": [ + "status" + ], + "expected": "failed" + } + } + ], + "selectedOptionIds": [ + "ci-status-passed:refresh-value" + ], + "totalCost": 4, + "coveredRequirementIds": [ + "ci-status-passed" + ], + "unresolvedRequirementIds": [] + }, + "verificationRecordDigest": "834df6c1741714f824dc6fca9f1001f075d7eeabe93512fa60148a93d90e5d92" + } + }, + { + "name": "value-path-missing", + "input": { + "protocolVersion": "0.1", + "contract": { + "id": "value-path-missing", + "version": "1", + "decisionTime": "2026-09-02T18:00:00.000Z", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "ci-tested-current-head", + "type": "dependency", + "description": "The passing CI run tested the selected branch head", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "ci-status-passed", + "type": "value_equals", + "description": "The CI status is passed", + "role": "ci", + "path": [ + "missing" + ], + "expected": "passed" + }, + { + "id": "approval-and-quote-overlap", + "type": "common_valid_time", + "description": "The approval and quote were valid at a common time", + "roles": [ + "approval", + "quote" + ], + "within": { + "from": "2026-09-02T17:55:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + } + } + ] + }, + "observations": [ + { + "id": "obs-head-b", + "role": "head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "value": { + "commit": "commit-B" + }, + "observedAt": "2026-09-02T17:59:58.000Z", + "acquisitionCost": 1, + "witness": { + "provenance": "provider_asserted", + "version": "commit-B" + } + }, + { + "id": "obs-ci-b", + "role": "ci", + "resource": { + "provider": "ci.example", + "account": "acme", + "kind": "ci_run", + "key": "run-2041" + }, + "value": { + "status": "passed" + }, + "observedAt": "2026-09-02T17:59:58.500Z", + "acquisitionCost": 4, + "witness": { + "provenance": "provider_asserted", + "version": "run-2041", + "dependencies": [ + { + "name": "tested_head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "relation": "exact", + "version": "commit-B", + "provenance": "provider_asserted" + } + ] + } + }, + { + "id": "obs-approval", + "role": "approval", + "resource": { + "provider": "change.example", + "account": "acme", + "kind": "approval", + "key": "release-2041" + }, + "value": { + "approved": true + }, + "observedAt": "2026-09-02T17:59:59.000Z", + "acquisitionCost": 2, + "witness": { + "provenance": "provider_asserted", + "version": "approval-8", + "validity": { + "from": "2026-09-02T17:50:00.000Z", + "until": "2026-09-02T18:05:00.000Z" + } + } + }, + { + "id": "obs-quote", + "role": "quote", + "resource": { + "provider": "pricing.example", + "account": "acme", + "kind": "quote", + "key": "release-2041" + }, + "value": { + "amount": 1250, + "currency": "USD" + }, + "observedAt": "2026-09-02T17:59:59.500Z", + "acquisitionCost": 3, + "witness": { + "provenance": "provider_asserted", + "version": "quote-17", + "validity": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:03:00.000Z" + } + } + } + ] + }, + "expected": { + "protocolVersion": "0.1", + "engineVersion": "0.1.2", + "canonicalization": "worldcut-json-v1", + "contractId": "value-path-missing", + "contractVersion": "1", + "verdict": "INSUFFICIENT_EVIDENCE", + "coverage": { + "required": 3, + "satisfied": 2, + "violated": 0, + "unknown": 1, + "advisory": 0 + }, + "requirementResults": [ + { + "requirementId": "approval-and-quote-overlap", + "requirementType": "common_valid_time", + "required": true, + "status": "SATISFIED", + "summary": "The approval and quote were valid at a common time: a common valid time exists.", + "details": { + "roles": [ + "approval", + "quote" + ], + "commonWindow": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + } + }, + "acquisitionOptions": [] + }, + { + "requirementId": "ci-status-passed", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "The CI status is passed: value path missing is missing.", + "details": { + "role": "ci", + "path": [ + "missing" + ], + "expected": "passed" + }, + "acquisitionOptions": [ + { + "id": "ci-status-passed:acquire-value", + "description": "Acquire evidence containing the required value path.", + "actions": [ + { + "id": "acquire_compatible_evidence:ci:b6c8b2f486b3", + "type": "ACQUIRE_COMPATIBLE_EVIDENCE", + "role": "ci", + "cost": 4, + "description": "Acquire ci evidence containing missing.", + "expected": { + "path": [ + "missing" + ], + "expected": "passed" + } + } + ] + } + ] + }, + { + "requirementId": "ci-tested-current-head", + "requirementType": "dependency", + "required": true, + "status": "SATISFIED", + "summary": "The passing CI run tested the selected branch head: both roles are bound to commit-B.", + "details": { + "dependentRole": "ci", + "targetRole": "head", + "version": "commit-B" + }, + "acquisitionOptions": [] + } + ], + "acquisitionPlan": { + "status": "AVAILABLE", + "reason": null, + "actions": [ + { + "id": "acquire_compatible_evidence:ci:b6c8b2f486b3", + "type": "ACQUIRE_COMPATIBLE_EVIDENCE", + "role": "ci", + "cost": 4, + "description": "Acquire ci evidence containing missing.", + "expected": { + "path": [ + "missing" + ], + "expected": "passed" + } + } + ], + "selectedOptionIds": [ + "ci-status-passed:acquire-value" + ], + "totalCost": 4, + "coveredRequirementIds": [ + "ci-status-passed" + ], + "unresolvedRequirementIds": [] + }, + "verificationRecordDigest": "ce443403acf879a7487377a1bc1a127034dcf6d1634cfe75562b8fde1531fae2" + } + }, + { + "name": "array-index", + "input": { + "protocolVersion": "0.1", + "contract": { + "id": "array-index", + "version": "1", + "decisionTime": "2026-09-02T18:00:00.000Z", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "first-status", + "type": "value_equals", + "description": "The first array value is passed", + "role": "ci", + "path": [ + "0" + ], + "expected": "passed" + } + ] + }, + "observations": [ + { + "id": "obs-head-b", + "role": "head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "value": { + "commit": "commit-B" + }, + "observedAt": "2026-09-02T17:59:58.000Z", + "acquisitionCost": 1, + "witness": { + "provenance": "provider_asserted", + "version": "commit-B" + } + }, + { + "id": "obs-ci-b", + "role": "ci", + "resource": { + "provider": "ci.example", + "account": "acme", + "kind": "ci_run", + "key": "run-2041" + }, + "value": [ + "passed" + ], + "observedAt": "2026-09-02T17:59:58.500Z", + "acquisitionCost": 4, + "witness": { + "provenance": "provider_asserted", + "version": "run-2041", + "dependencies": [ + { + "name": "tested_head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "relation": "exact", + "version": "commit-B", + "provenance": "provider_asserted" + } + ] + } + }, + { + "id": "obs-approval", + "role": "approval", + "resource": { + "provider": "change.example", + "account": "acme", + "kind": "approval", + "key": "release-2041" + }, + "value": { + "approved": true + }, + "observedAt": "2026-09-02T17:59:59.000Z", + "acquisitionCost": 2, + "witness": { + "provenance": "provider_asserted", + "version": "approval-8", + "validity": { + "from": "2026-09-02T17:50:00.000Z", + "until": "2026-09-02T18:05:00.000Z" + } + } + }, + { + "id": "obs-quote", + "role": "quote", + "resource": { + "provider": "pricing.example", + "account": "acme", + "kind": "quote", + "key": "release-2041" + }, + "value": { + "amount": 1250, + "currency": "USD" + }, + "observedAt": "2026-09-02T17:59:59.500Z", + "acquisitionCost": 3, + "witness": { + "provenance": "provider_asserted", + "version": "quote-17", + "validity": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:03:00.000Z" + } + } + } + ] + }, + "expected": { + "protocolVersion": "0.1", + "engineVersion": "0.1.2", + "canonicalization": "worldcut-json-v1", + "contractId": "array-index", + "contractVersion": "1", + "verdict": "CONTRACT_SATISFIED", + "coverage": { + "required": 1, + "satisfied": 1, + "violated": 0, + "unknown": 0, + "advisory": 0 + }, + "requirementResults": [ + { + "requirementId": "first-status", + "requirementType": "value_equals", + "required": true, + "status": "SATISFIED", + "summary": "The first array value is passed: observed value matches the requirement.", + "details": { + "role": "ci", + "path": [ + "0" + ], + "expected": "passed" + }, + "acquisitionOptions": [] + } + ], + "acquisitionPlan": { + "status": "NOT_NEEDED", + "reason": null, + "actions": [], + "selectedOptionIds": [], + "totalCost": 0, + "coveredRequirementIds": [], + "unresolvedRequirementIds": [] + }, + "verificationRecordDigest": "cc96a3f93e6f03e8981437ae6801cb19bf7a1dce13709152632c3178512f65ec" + } + }, + { + "name": "array-length-is-not-a-value-path", + "input": { + "protocolVersion": "0.1", + "contract": { + "id": "array-length-is-not-a-value-path", + "version": "1", + "decisionTime": "2026-09-02T18:00:00.000Z", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "first-status", + "type": "value_equals", + "description": "The first array value is passed", + "role": "ci", + "path": [ + "length" + ], + "expected": "passed" + } + ] + }, + "observations": [ + { + "id": "obs-head-b", + "role": "head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "value": { + "commit": "commit-B" + }, + "observedAt": "2026-09-02T17:59:58.000Z", + "acquisitionCost": 1, + "witness": { + "provenance": "provider_asserted", + "version": "commit-B" + } + }, + { + "id": "obs-ci-b", + "role": "ci", + "resource": { + "provider": "ci.example", + "account": "acme", + "kind": "ci_run", + "key": "run-2041" + }, + "value": [ + "passed" + ], + "observedAt": "2026-09-02T17:59:58.500Z", + "acquisitionCost": 4, + "witness": { + "provenance": "provider_asserted", + "version": "run-2041", + "dependencies": [ + { + "name": "tested_head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "relation": "exact", + "version": "commit-B", + "provenance": "provider_asserted" + } + ] + } + }, + { + "id": "obs-approval", + "role": "approval", + "resource": { + "provider": "change.example", + "account": "acme", + "kind": "approval", + "key": "release-2041" + }, + "value": { + "approved": true + }, + "observedAt": "2026-09-02T17:59:59.000Z", + "acquisitionCost": 2, + "witness": { + "provenance": "provider_asserted", + "version": "approval-8", + "validity": { + "from": "2026-09-02T17:50:00.000Z", + "until": "2026-09-02T18:05:00.000Z" + } + } + }, + { + "id": "obs-quote", + "role": "quote", + "resource": { + "provider": "pricing.example", + "account": "acme", + "kind": "quote", + "key": "release-2041" + }, + "value": { + "amount": 1250, + "currency": "USD" + }, + "observedAt": "2026-09-02T17:59:59.500Z", + "acquisitionCost": 3, + "witness": { + "provenance": "provider_asserted", + "version": "quote-17", + "validity": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:03:00.000Z" + } + } + } + ] + }, + "expected": { + "protocolVersion": "0.1", + "engineVersion": "0.1.2", + "canonicalization": "worldcut-json-v1", + "contractId": "array-length-is-not-a-value-path", + "contractVersion": "1", + "verdict": "INSUFFICIENT_EVIDENCE", + "coverage": { + "required": 1, + "satisfied": 0, + "violated": 0, + "unknown": 1, + "advisory": 0 + }, + "requirementResults": [ + { + "requirementId": "first-status", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "The first array value is passed: value path length is missing.", + "details": { + "role": "ci", + "path": [ + "length" + ], + "expected": "passed" + }, + "acquisitionOptions": [ + { + "id": "first-status:acquire-value", + "description": "Acquire evidence containing the required value path.", + "actions": [ + { + "id": "acquire_compatible_evidence:ci:4674a7eae86d", + "type": "ACQUIRE_COMPATIBLE_EVIDENCE", + "role": "ci", + "cost": 4, + "description": "Acquire ci evidence containing length.", + "expected": { + "path": [ + "length" + ], + "expected": "passed" + } + } + ] + } + ] + } + ], + "acquisitionPlan": { + "status": "AVAILABLE", + "reason": null, + "actions": [ + { + "id": "acquire_compatible_evidence:ci:4674a7eae86d", + "type": "ACQUIRE_COMPATIBLE_EVIDENCE", + "role": "ci", + "cost": 4, + "description": "Acquire ci evidence containing length.", + "expected": { + "path": [ + "length" + ], + "expected": "passed" + } + } + ], + "selectedOptionIds": [ + "first-status:acquire-value" + ], + "totalCost": 4, + "coveredRequirementIds": [ + "first-status" + ], + "unresolvedRequirementIds": [] + }, + "verificationRecordDigest": "62b3ec27dbd060e6ac452ada327c45b847f889a7e35de6d528e4fbf5de7563d7" + } + }, + { + "name": "whitespace-object-path", + "input": { + "protocolVersion": "0.1", + "contract": { + "id": "whitespace-object-path", + "version": "1", + "decisionTime": "2026-09-02T18:00:00.000Z", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "whitespace-key", + "type": "value_equals", + "description": "Whitespace object keys remain exact", + "role": "ci", + "path": [ + " " + ], + "expected": "passed" + } + ] + }, + "observations": [ + { + "id": "obs-head-b", + "role": "head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "value": { + "commit": "commit-B" + }, + "observedAt": "2026-09-02T17:59:58.000Z", + "acquisitionCost": 1, + "witness": { + "provenance": "provider_asserted", + "version": "commit-B" + } + }, + { + "id": "obs-ci-b", + "role": "ci", + "resource": { + "provider": "ci.example", + "account": "acme", + "kind": "ci_run", + "key": "run-2041" + }, + "value": { + " ": "passed" + }, + "observedAt": "2026-09-02T17:59:58.500Z", + "acquisitionCost": 4, + "witness": { + "provenance": "provider_asserted", + "version": "run-2041", + "dependencies": [ + { + "name": "tested_head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "relation": "exact", + "version": "commit-B", + "provenance": "provider_asserted" + } + ] + } + }, + { + "id": "obs-approval", + "role": "approval", + "resource": { + "provider": "change.example", + "account": "acme", + "kind": "approval", + "key": "release-2041" + }, + "value": { + "approved": true + }, + "observedAt": "2026-09-02T17:59:59.000Z", + "acquisitionCost": 2, + "witness": { + "provenance": "provider_asserted", + "version": "approval-8", + "validity": { + "from": "2026-09-02T17:50:00.000Z", + "until": "2026-09-02T18:05:00.000Z" + } + } + }, + { + "id": "obs-quote", + "role": "quote", + "resource": { + "provider": "pricing.example", + "account": "acme", + "kind": "quote", + "key": "release-2041" + }, + "value": { + "amount": 1250, + "currency": "USD" + }, + "observedAt": "2026-09-02T17:59:59.500Z", + "acquisitionCost": 3, + "witness": { + "provenance": "provider_asserted", + "version": "quote-17", + "validity": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:03:00.000Z" + } + } + } + ] + }, + "expected": { + "protocolVersion": "0.1", + "engineVersion": "0.1.2", + "canonicalization": "worldcut-json-v1", + "contractId": "whitespace-object-path", + "contractVersion": "1", + "verdict": "CONTRACT_SATISFIED", + "coverage": { + "required": 1, + "satisfied": 1, + "violated": 0, + "unknown": 0, + "advisory": 0 + }, + "requirementResults": [ + { + "requirementId": "whitespace-key", + "requirementType": "value_equals", + "required": true, + "status": "SATISFIED", + "summary": "Whitespace object keys remain exact: observed value matches the requirement.", + "details": { + "role": "ci", + "path": [ + " " + ], + "expected": "passed" + }, + "acquisitionOptions": [] + } + ], + "acquisitionPlan": { + "status": "NOT_NEEDED", + "reason": null, + "actions": [], + "selectedOptionIds": [], + "totalCost": 0, + "coveredRequirementIds": [], + "unresolvedRequirementIds": [] + }, + "verificationRecordDigest": "87caf905e01765a6e27743c71899e9c2a5e8e7d4cc287c4940b00b4cc2ac903b" + } + }, + { + "name": "advisory-unknown", + "input": { + "protocolVersion": "0.1", + "contract": { + "id": "advisory-unknown", + "version": "1", + "decisionTime": "2026-09-02T18:00:00.000Z", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "ci-tested-current-head", + "type": "dependency", + "description": "The passing CI run tested the selected branch head", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "ci-status-passed", + "type": "value_equals", + "description": "The CI status is passed", + "role": "ci", + "path": [ + "status" + ], + "expected": "passed" + }, + { + "id": "approval-and-quote-overlap", + "type": "common_valid_time", + "description": "The approval and quote were valid at a common time", + "roles": [ + "approval", + "quote" + ], + "within": { + "from": "2026-09-02T17:55:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + } + }, + { + "id": "optional-provider-note", + "type": "value_equals", + "description": "An optional provider note is present", + "required": false, + "role": "optional", + "path": [ + "note" + ], + "expected": "present" + } + ] + }, + "observations": [ + { + "id": "obs-head-b", + "role": "head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "value": { + "commit": "commit-B" + }, + "observedAt": "2026-09-02T17:59:58.000Z", + "acquisitionCost": 1, + "witness": { + "provenance": "provider_asserted", + "version": "commit-B" + } + }, + { + "id": "obs-ci-b", + "role": "ci", + "resource": { + "provider": "ci.example", + "account": "acme", + "kind": "ci_run", + "key": "run-2041" + }, + "value": { + "status": "passed" + }, + "observedAt": "2026-09-02T17:59:58.500Z", + "acquisitionCost": 4, + "witness": { + "provenance": "provider_asserted", + "version": "run-2041", + "dependencies": [ + { + "name": "tested_head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "relation": "exact", + "version": "commit-B", + "provenance": "provider_asserted" + } + ] + } + }, + { + "id": "obs-approval", + "role": "approval", + "resource": { + "provider": "change.example", + "account": "acme", + "kind": "approval", + "key": "release-2041" + }, + "value": { + "approved": true + }, + "observedAt": "2026-09-02T17:59:59.000Z", + "acquisitionCost": 2, + "witness": { + "provenance": "provider_asserted", + "version": "approval-8", + "validity": { + "from": "2026-09-02T17:50:00.000Z", + "until": "2026-09-02T18:05:00.000Z" + } + } + }, + { + "id": "obs-quote", + "role": "quote", + "resource": { + "provider": "pricing.example", + "account": "acme", + "kind": "quote", + "key": "release-2041" + }, + "value": { + "amount": 1250, + "currency": "USD" + }, + "observedAt": "2026-09-02T17:59:59.500Z", + "acquisitionCost": 3, + "witness": { + "provenance": "provider_asserted", + "version": "quote-17", + "validity": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:03:00.000Z" + } + } + } + ] + }, + "expected": { + "protocolVersion": "0.1", + "engineVersion": "0.1.2", + "canonicalization": "worldcut-json-v1", + "contractId": "advisory-unknown", + "contractVersion": "1", + "verdict": "CONTRACT_SATISFIED", + "coverage": { + "required": 3, + "satisfied": 3, + "violated": 0, + "unknown": 0, + "advisory": 1 + }, + "requirementResults": [ + { + "requirementId": "approval-and-quote-overlap", + "requirementType": "common_valid_time", + "required": true, + "status": "SATISFIED", + "summary": "The approval and quote were valid at a common time: a common valid time exists.", + "details": { + "roles": [ + "approval", + "quote" + ], + "commonWindow": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + } + }, + "acquisitionOptions": [] + }, + { + "requirementId": "ci-status-passed", + "requirementType": "value_equals", + "required": true, + "status": "SATISFIED", + "summary": "The CI status is passed: observed value matches the requirement.", + "details": { + "role": "ci", + "path": [ + "status" + ], + "expected": "passed" + }, + "acquisitionOptions": [] + }, + { + "requirementId": "ci-tested-current-head", + "requirementType": "dependency", + "required": true, + "status": "SATISFIED", + "summary": "The passing CI run tested the selected branch head: both roles are bound to commit-B.", + "details": { + "dependentRole": "ci", + "targetRole": "head", + "version": "commit-B" + }, + "acquisitionOptions": [] + }, + { + "requirementId": "optional-provider-note", + "requirementType": "value_equals", + "required": false, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): optional.", + "details": { + "missingRoles": [ + "optional" + ] + }, + "acquisitionOptions": [ + { + "id": "optional-provider-note:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:optional:none", + "type": "REFRESH_OBSERVATION", + "role": "optional", + "cost": 1, + "description": "Acquire an observation for role optional.", + "expected": null + } + ] + } + ] + } + ], + "acquisitionPlan": { + "status": "NOT_NEEDED", + "reason": null, + "actions": [], + "selectedOptionIds": [], + "totalCost": 0, + "coveredRequirementIds": [], + "unresolvedRequirementIds": [] + }, + "verificationRecordDigest": "7addbae456670e1a91025ca45188a99e269f8186b547f13d3a1331f761153c5b" + } + }, + { + "name": "violation-dominates-unknown", + "input": { + "protocolVersion": "0.1", + "contract": { + "id": "violation-dominates-unknown", + "version": "1", + "decisionTime": "2026-09-02T18:00:00.000Z", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "ci-tested-current-head", + "type": "dependency", + "description": "The passing CI run tested the selected branch head", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "missing-optional-system", + "type": "value_equals", + "description": "A required external approval is present", + "role": "external-approval", + "path": [ + "approved" + ], + "expected": true + } + ] + }, + "observations": [ + { + "id": "obs-head-b", + "role": "head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "value": { + "commit": "commit-B" + }, + "observedAt": "2026-09-02T17:59:59.000Z", + "acquisitionCost": 1, + "witness": { + "provenance": "provider_asserted", + "version": "commit-B" + } + }, + { + "id": "obs-ci-a", + "role": "ci", + "resource": { + "provider": "ci.example", + "account": "acme", + "kind": "ci_run", + "key": "run-2040" + }, + "value": { + "status": "passed" + }, + "observedAt": "2026-09-02T17:59:59.500Z", + "acquisitionCost": 4, + "witness": { + "provenance": "provider_asserted", + "version": "run-2040", + "dependencies": [ + { + "name": "tested_head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "relation": "exact", + "version": "commit-A", + "provenance": "provider_asserted" + } + ] + } + } + ] + }, + "expected": { + "protocolVersion": "0.1", + "engineVersion": "0.1.2", + "canonicalization": "worldcut-json-v1", + "contractId": "violation-dominates-unknown", + "contractVersion": "1", + "verdict": "CONTRACT_VIOLATED", + "coverage": { + "required": 2, + "satisfied": 0, + "violated": 1, + "unknown": 1, + "advisory": 0 + }, + "requirementResults": [ + { + "requirementId": "ci-tested-current-head", + "requirementType": "dependency", + "required": true, + "status": "VIOLATED", + "summary": "The passing CI run tested the selected branch head: commit-A does not equal commit-B.", + "details": { + "dependentRole": "ci", + "dependencyVersion": "commit-A", + "targetRole": "head", + "targetVersion": "commit-B", + "relation": "exact" + }, + "acquisitionOptions": [ + { + "id": "ci-tested-current-head:acquire-compatible-dependent", + "description": "Acquire dependent evidence bound to the selected target version.", + "actions": [ + { + "id": "acquire_compatible_evidence:ci:c969e13b988f", + "type": "ACQUIRE_COMPATIBLE_EVIDENCE", + "role": "ci", + "cost": 4, + "description": "Acquire ci evidence bound to commit-B.", + "expected": { + "targetRole": "head", + "targetVersion": "commit-B" + } + } + ] + }, + { + "id": "ci-tested-current-head:refresh-target", + "description": "Refresh the target before selecting compatible evidence.", + "actions": [ + { + "id": "refresh_observation:head:6df94fb5680d", + "type": "REFRESH_OBSERVATION", + "role": "head", + "cost": 1, + "description": "Refresh head before selecting compatible evidence.", + "expected": { + "dependentRole": "ci", + "dependentVersion": "commit-A" + } + } + ] + } + ] + }, + { + "requirementId": "missing-optional-system", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): external-approval.", + "details": { + "missingRoles": [ + "external-approval" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-optional-system:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:external-approval:none", + "type": "REFRESH_OBSERVATION", + "role": "external-approval", + "cost": 1, + "description": "Acquire an observation for role external-approval.", + "expected": null + } + ] + } + ] + } + ], + "acquisitionPlan": { + "status": "AVAILABLE", + "reason": null, + "actions": [ + { + "id": "refresh_observation:external-approval:none", + "type": "REFRESH_OBSERVATION", + "role": "external-approval", + "cost": 1, + "description": "Acquire an observation for role external-approval.", + "expected": null + }, + { + "id": "refresh_observation:head:6df94fb5680d", + "type": "REFRESH_OBSERVATION", + "role": "head", + "cost": 1, + "description": "Refresh head before selecting compatible evidence.", + "expected": { + "dependentRole": "ci", + "dependentVersion": "commit-A" + } + } + ], + "selectedOptionIds": [ + "ci-tested-current-head:refresh-target", + "missing-optional-system:acquire-missing-roles" + ], + "totalCost": 2, + "coveredRequirementIds": [ + "ci-tested-current-head", + "missing-optional-system" + ], + "unresolvedRequirementIds": [] + }, + "verificationRecordDigest": "44079bb20c75a56734ed25106e80831627c837ab9bbf99ddc1cafdf27fc52dc2" + } + }, + { + "name": "open-ended-overlap", + "input": { + "protocolVersion": "0.1", + "contract": { + "id": "open-ended-overlap", + "version": "1", + "decisionTime": "2026-09-02T18:00:00.000Z", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "ci-tested-current-head", + "type": "dependency", + "description": "The passing CI run tested the selected branch head", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "ci-status-passed", + "type": "value_equals", + "description": "The CI status is passed", + "role": "ci", + "path": [ + "status" + ], + "expected": "passed" + }, + { + "id": "approval-and-quote-overlap", + "type": "common_valid_time", + "description": "The approval and quote were valid at a common time", + "roles": [ + "approval", + "quote" + ], + "within": { + "from": "2026-09-02T17:55:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + } + } + ] + }, + "observations": [ + { + "id": "obs-head-b", + "role": "head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "value": { + "commit": "commit-B" + }, + "observedAt": "2026-09-02T17:59:58.000Z", + "acquisitionCost": 1, + "witness": { + "provenance": "provider_asserted", + "version": "commit-B" + } + }, + { + "id": "obs-ci-b", + "role": "ci", + "resource": { + "provider": "ci.example", + "account": "acme", + "kind": "ci_run", + "key": "run-2041" + }, + "value": { + "status": "passed" + }, + "observedAt": "2026-09-02T17:59:58.500Z", + "acquisitionCost": 4, + "witness": { + "provenance": "provider_asserted", + "version": "run-2041", + "dependencies": [ + { + "name": "tested_head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "relation": "exact", + "version": "commit-B", + "provenance": "provider_asserted" + } + ] + } + }, + { + "id": "obs-approval", + "role": "approval", + "resource": { + "provider": "change.example", + "account": "acme", + "kind": "approval", + "key": "release-2041" + }, + "value": { + "approved": true + }, + "observedAt": "2026-09-02T17:59:59.000Z", + "acquisitionCost": 2, + "witness": { + "provenance": "provider_asserted", + "version": "approval-8", + "validity": { + "from": "2026-09-02T17:50:00.000Z", + "until": null + } + } + }, + { + "id": "obs-quote", + "role": "quote", + "resource": { + "provider": "pricing.example", + "account": "acme", + "kind": "quote", + "key": "release-2041" + }, + "value": { + "amount": 1250, + "currency": "USD" + }, + "observedAt": "2026-09-02T17:59:59.500Z", + "acquisitionCost": 3, + "witness": { + "provenance": "provider_asserted", + "version": "quote-17", + "validity": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:03:00.000Z" + } + } + } + ] + }, + "expected": { + "protocolVersion": "0.1", + "engineVersion": "0.1.2", + "canonicalization": "worldcut-json-v1", + "contractId": "open-ended-overlap", + "contractVersion": "1", + "verdict": "CONTRACT_SATISFIED", + "coverage": { + "required": 3, + "satisfied": 3, + "violated": 0, + "unknown": 0, + "advisory": 0 + }, + "requirementResults": [ + { + "requirementId": "approval-and-quote-overlap", + "requirementType": "common_valid_time", + "required": true, + "status": "SATISFIED", + "summary": "The approval and quote were valid at a common time: a common valid time exists.", + "details": { + "roles": [ + "approval", + "quote" + ], + "commonWindow": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + } + }, + "acquisitionOptions": [] + }, + { + "requirementId": "ci-status-passed", + "requirementType": "value_equals", + "required": true, + "status": "SATISFIED", + "summary": "The CI status is passed: observed value matches the requirement.", + "details": { + "role": "ci", + "path": [ + "status" + ], + "expected": "passed" + }, + "acquisitionOptions": [] + }, + { + "requirementId": "ci-tested-current-head", + "requirementType": "dependency", + "required": true, + "status": "SATISFIED", + "summary": "The passing CI run tested the selected branch head: both roles are bound to commit-B.", + "details": { + "dependentRole": "ci", + "targetRole": "head", + "version": "commit-B" + }, + "acquisitionOptions": [] + } + ], + "acquisitionPlan": { + "status": "NOT_NEEDED", + "reason": null, + "actions": [], + "selectedOptionIds": [], + "totalCost": 0, + "coveredRequirementIds": [], + "unresolvedRequirementIds": [] + }, + "verificationRecordDigest": "990cf61f725948e7ee7fd0a17c14cd5748a92c1ecbe5fad007475291178a1cee" + } + }, + { + "name": "planner-requirement-limit", + "input": { + "protocolVersion": "0.1", + "contract": { + "id": "planner-requirement-limit", + "version": "1", + "decisionTime": "2026-09-02T18:00:00.000Z", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "missing-00", + "type": "value_equals", + "description": "Missing role 0", + "role": "missing-0", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-01", + "type": "value_equals", + "description": "Missing role 1", + "role": "missing-1", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-02", + "type": "value_equals", + "description": "Missing role 2", + "role": "missing-2", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-03", + "type": "value_equals", + "description": "Missing role 3", + "role": "missing-3", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-04", + "type": "value_equals", + "description": "Missing role 4", + "role": "missing-4", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-05", + "type": "value_equals", + "description": "Missing role 5", + "role": "missing-5", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-06", + "type": "value_equals", + "description": "Missing role 6", + "role": "missing-6", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-07", + "type": "value_equals", + "description": "Missing role 7", + "role": "missing-7", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-08", + "type": "value_equals", + "description": "Missing role 8", + "role": "missing-8", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-09", + "type": "value_equals", + "description": "Missing role 9", + "role": "missing-9", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-10", + "type": "value_equals", + "description": "Missing role 10", + "role": "missing-10", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-11", + "type": "value_equals", + "description": "Missing role 11", + "role": "missing-11", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-12", + "type": "value_equals", + "description": "Missing role 12", + "role": "missing-12", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-13", + "type": "value_equals", + "description": "Missing role 13", + "role": "missing-13", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-14", + "type": "value_equals", + "description": "Missing role 14", + "role": "missing-14", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-15", + "type": "value_equals", + "description": "Missing role 15", + "role": "missing-15", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-16", + "type": "value_equals", + "description": "Missing role 16", + "role": "missing-16", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-17", + "type": "value_equals", + "description": "Missing role 17", + "role": "missing-17", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-18", + "type": "value_equals", + "description": "Missing role 18", + "role": "missing-18", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-19", + "type": "value_equals", + "description": "Missing role 19", + "role": "missing-19", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-20", + "type": "value_equals", + "description": "Missing role 20", + "role": "missing-20", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-21", + "type": "value_equals", + "description": "Missing role 21", + "role": "missing-21", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-22", + "type": "value_equals", + "description": "Missing role 22", + "role": "missing-22", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-23", + "type": "value_equals", + "description": "Missing role 23", + "role": "missing-23", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-24", + "type": "value_equals", + "description": "Missing role 24", + "role": "missing-24", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-25", + "type": "value_equals", + "description": "Missing role 25", + "role": "missing-25", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-26", + "type": "value_equals", + "description": "Missing role 26", + "role": "missing-26", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-27", + "type": "value_equals", + "description": "Missing role 27", + "role": "missing-27", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-28", + "type": "value_equals", + "description": "Missing role 28", + "role": "missing-28", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-29", + "type": "value_equals", + "description": "Missing role 29", + "role": "missing-29", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-30", + "type": "value_equals", + "description": "Missing role 30", + "role": "missing-30", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-31", + "type": "value_equals", + "description": "Missing role 31", + "role": "missing-31", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-32", + "type": "value_equals", + "description": "Missing role 32", + "role": "missing-32", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-33", + "type": "value_equals", + "description": "Missing role 33", + "role": "missing-33", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-34", + "type": "value_equals", + "description": "Missing role 34", + "role": "missing-34", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-35", + "type": "value_equals", + "description": "Missing role 35", + "role": "missing-35", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-36", + "type": "value_equals", + "description": "Missing role 36", + "role": "missing-36", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-37", + "type": "value_equals", + "description": "Missing role 37", + "role": "missing-37", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-38", + "type": "value_equals", + "description": "Missing role 38", + "role": "missing-38", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-39", + "type": "value_equals", + "description": "Missing role 39", + "role": "missing-39", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-40", + "type": "value_equals", + "description": "Missing role 40", + "role": "missing-40", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-41", + "type": "value_equals", + "description": "Missing role 41", + "role": "missing-41", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-42", + "type": "value_equals", + "description": "Missing role 42", + "role": "missing-42", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-43", + "type": "value_equals", + "description": "Missing role 43", + "role": "missing-43", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-44", + "type": "value_equals", + "description": "Missing role 44", + "role": "missing-44", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-45", + "type": "value_equals", + "description": "Missing role 45", + "role": "missing-45", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-46", + "type": "value_equals", + "description": "Missing role 46", + "role": "missing-46", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-47", + "type": "value_equals", + "description": "Missing role 47", + "role": "missing-47", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-48", + "type": "value_equals", + "description": "Missing role 48", + "role": "missing-48", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-49", + "type": "value_equals", + "description": "Missing role 49", + "role": "missing-49", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-50", + "type": "value_equals", + "description": "Missing role 50", + "role": "missing-50", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-51", + "type": "value_equals", + "description": "Missing role 51", + "role": "missing-51", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-52", + "type": "value_equals", + "description": "Missing role 52", + "role": "missing-52", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-53", + "type": "value_equals", + "description": "Missing role 53", + "role": "missing-53", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-54", + "type": "value_equals", + "description": "Missing role 54", + "role": "missing-54", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-55", + "type": "value_equals", + "description": "Missing role 55", + "role": "missing-55", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-56", + "type": "value_equals", + "description": "Missing role 56", + "role": "missing-56", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-57", + "type": "value_equals", + "description": "Missing role 57", + "role": "missing-57", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-58", + "type": "value_equals", + "description": "Missing role 58", + "role": "missing-58", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-59", + "type": "value_equals", + "description": "Missing role 59", + "role": "missing-59", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-60", + "type": "value_equals", + "description": "Missing role 60", + "role": "missing-60", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-61", + "type": "value_equals", + "description": "Missing role 61", + "role": "missing-61", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-62", + "type": "value_equals", + "description": "Missing role 62", + "role": "missing-62", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-63", + "type": "value_equals", + "description": "Missing role 63", + "role": "missing-63", + "path": [ + "value" + ], + "expected": true + }, + { + "id": "missing-64", + "type": "value_equals", + "description": "Missing role 64", + "role": "missing-64", + "path": [ + "value" + ], + "expected": true + } + ] + }, + "observations": [] + }, + "expected": { + "protocolVersion": "0.1", + "engineVersion": "0.1.2", + "canonicalization": "worldcut-json-v1", + "contractId": "planner-requirement-limit", + "contractVersion": "1", + "verdict": "INSUFFICIENT_EVIDENCE", + "coverage": { + "required": 65, + "satisfied": 0, + "violated": 0, + "unknown": 65, + "advisory": 0 + }, + "requirementResults": [ + { + "requirementId": "missing-00", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-0.", + "details": { + "missingRoles": [ + "missing-0" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-00:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-0:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-0", + "cost": 1, + "description": "Acquire an observation for role missing-0.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-01", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-1.", + "details": { + "missingRoles": [ + "missing-1" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-01:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-1:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-1", + "cost": 1, + "description": "Acquire an observation for role missing-1.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-02", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-2.", + "details": { + "missingRoles": [ + "missing-2" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-02:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-2:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-2", + "cost": 1, + "description": "Acquire an observation for role missing-2.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-03", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-3.", + "details": { + "missingRoles": [ + "missing-3" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-03:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-3:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-3", + "cost": 1, + "description": "Acquire an observation for role missing-3.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-04", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-4.", + "details": { + "missingRoles": [ + "missing-4" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-04:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-4:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-4", + "cost": 1, + "description": "Acquire an observation for role missing-4.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-05", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-5.", + "details": { + "missingRoles": [ + "missing-5" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-05:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-5:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-5", + "cost": 1, + "description": "Acquire an observation for role missing-5.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-06", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-6.", + "details": { + "missingRoles": [ + "missing-6" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-06:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-6:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-6", + "cost": 1, + "description": "Acquire an observation for role missing-6.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-07", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-7.", + "details": { + "missingRoles": [ + "missing-7" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-07:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-7:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-7", + "cost": 1, + "description": "Acquire an observation for role missing-7.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-08", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-8.", + "details": { + "missingRoles": [ + "missing-8" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-08:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-8:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-8", + "cost": 1, + "description": "Acquire an observation for role missing-8.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-09", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-9.", + "details": { + "missingRoles": [ + "missing-9" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-09:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-9:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-9", + "cost": 1, + "description": "Acquire an observation for role missing-9.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-10", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-10.", + "details": { + "missingRoles": [ + "missing-10" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-10:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-10:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-10", + "cost": 1, + "description": "Acquire an observation for role missing-10.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-11", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-11.", + "details": { + "missingRoles": [ + "missing-11" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-11:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-11:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-11", + "cost": 1, + "description": "Acquire an observation for role missing-11.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-12", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-12.", + "details": { + "missingRoles": [ + "missing-12" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-12:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-12:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-12", + "cost": 1, + "description": "Acquire an observation for role missing-12.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-13", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-13.", + "details": { + "missingRoles": [ + "missing-13" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-13:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-13:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-13", + "cost": 1, + "description": "Acquire an observation for role missing-13.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-14", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-14.", + "details": { + "missingRoles": [ + "missing-14" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-14:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-14:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-14", + "cost": 1, + "description": "Acquire an observation for role missing-14.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-15", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-15.", + "details": { + "missingRoles": [ + "missing-15" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-15:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-15:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-15", + "cost": 1, + "description": "Acquire an observation for role missing-15.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-16", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-16.", + "details": { + "missingRoles": [ + "missing-16" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-16:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-16:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-16", + "cost": 1, + "description": "Acquire an observation for role missing-16.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-17", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-17.", + "details": { + "missingRoles": [ + "missing-17" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-17:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-17:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-17", + "cost": 1, + "description": "Acquire an observation for role missing-17.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-18", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-18.", + "details": { + "missingRoles": [ + "missing-18" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-18:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-18:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-18", + "cost": 1, + "description": "Acquire an observation for role missing-18.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-19", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-19.", + "details": { + "missingRoles": [ + "missing-19" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-19:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-19:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-19", + "cost": 1, + "description": "Acquire an observation for role missing-19.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-20", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-20.", + "details": { + "missingRoles": [ + "missing-20" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-20:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-20:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-20", + "cost": 1, + "description": "Acquire an observation for role missing-20.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-21", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-21.", + "details": { + "missingRoles": [ + "missing-21" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-21:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-21:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-21", + "cost": 1, + "description": "Acquire an observation for role missing-21.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-22", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-22.", + "details": { + "missingRoles": [ + "missing-22" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-22:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-22:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-22", + "cost": 1, + "description": "Acquire an observation for role missing-22.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-23", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-23.", + "details": { + "missingRoles": [ + "missing-23" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-23:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-23:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-23", + "cost": 1, + "description": "Acquire an observation for role missing-23.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-24", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-24.", + "details": { + "missingRoles": [ + "missing-24" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-24:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-24:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-24", + "cost": 1, + "description": "Acquire an observation for role missing-24.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-25", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-25.", + "details": { + "missingRoles": [ + "missing-25" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-25:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-25:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-25", + "cost": 1, + "description": "Acquire an observation for role missing-25.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-26", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-26.", + "details": { + "missingRoles": [ + "missing-26" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-26:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-26:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-26", + "cost": 1, + "description": "Acquire an observation for role missing-26.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-27", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-27.", + "details": { + "missingRoles": [ + "missing-27" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-27:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-27:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-27", + "cost": 1, + "description": "Acquire an observation for role missing-27.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-28", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-28.", + "details": { + "missingRoles": [ + "missing-28" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-28:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-28:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-28", + "cost": 1, + "description": "Acquire an observation for role missing-28.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-29", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-29.", + "details": { + "missingRoles": [ + "missing-29" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-29:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-29:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-29", + "cost": 1, + "description": "Acquire an observation for role missing-29.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-30", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-30.", + "details": { + "missingRoles": [ + "missing-30" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-30:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-30:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-30", + "cost": 1, + "description": "Acquire an observation for role missing-30.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-31", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-31.", + "details": { + "missingRoles": [ + "missing-31" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-31:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-31:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-31", + "cost": 1, + "description": "Acquire an observation for role missing-31.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-32", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-32.", + "details": { + "missingRoles": [ + "missing-32" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-32:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-32:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-32", + "cost": 1, + "description": "Acquire an observation for role missing-32.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-33", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-33.", + "details": { + "missingRoles": [ + "missing-33" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-33:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-33:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-33", + "cost": 1, + "description": "Acquire an observation for role missing-33.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-34", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-34.", + "details": { + "missingRoles": [ + "missing-34" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-34:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-34:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-34", + "cost": 1, + "description": "Acquire an observation for role missing-34.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-35", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-35.", + "details": { + "missingRoles": [ + "missing-35" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-35:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-35:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-35", + "cost": 1, + "description": "Acquire an observation for role missing-35.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-36", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-36.", + "details": { + "missingRoles": [ + "missing-36" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-36:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-36:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-36", + "cost": 1, + "description": "Acquire an observation for role missing-36.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-37", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-37.", + "details": { + "missingRoles": [ + "missing-37" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-37:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-37:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-37", + "cost": 1, + "description": "Acquire an observation for role missing-37.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-38", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-38.", + "details": { + "missingRoles": [ + "missing-38" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-38:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-38:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-38", + "cost": 1, + "description": "Acquire an observation for role missing-38.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-39", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-39.", + "details": { + "missingRoles": [ + "missing-39" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-39:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-39:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-39", + "cost": 1, + "description": "Acquire an observation for role missing-39.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-40", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-40.", + "details": { + "missingRoles": [ + "missing-40" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-40:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-40:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-40", + "cost": 1, + "description": "Acquire an observation for role missing-40.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-41", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-41.", + "details": { + "missingRoles": [ + "missing-41" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-41:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-41:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-41", + "cost": 1, + "description": "Acquire an observation for role missing-41.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-42", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-42.", + "details": { + "missingRoles": [ + "missing-42" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-42:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-42:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-42", + "cost": 1, + "description": "Acquire an observation for role missing-42.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-43", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-43.", + "details": { + "missingRoles": [ + "missing-43" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-43:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-43:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-43", + "cost": 1, + "description": "Acquire an observation for role missing-43.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-44", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-44.", + "details": { + "missingRoles": [ + "missing-44" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-44:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-44:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-44", + "cost": 1, + "description": "Acquire an observation for role missing-44.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-45", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-45.", + "details": { + "missingRoles": [ + "missing-45" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-45:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-45:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-45", + "cost": 1, + "description": "Acquire an observation for role missing-45.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-46", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-46.", + "details": { + "missingRoles": [ + "missing-46" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-46:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-46:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-46", + "cost": 1, + "description": "Acquire an observation for role missing-46.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-47", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-47.", + "details": { + "missingRoles": [ + "missing-47" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-47:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-47:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-47", + "cost": 1, + "description": "Acquire an observation for role missing-47.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-48", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-48.", + "details": { + "missingRoles": [ + "missing-48" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-48:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-48:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-48", + "cost": 1, + "description": "Acquire an observation for role missing-48.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-49", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-49.", + "details": { + "missingRoles": [ + "missing-49" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-49:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-49:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-49", + "cost": 1, + "description": "Acquire an observation for role missing-49.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-50", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-50.", + "details": { + "missingRoles": [ + "missing-50" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-50:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-50:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-50", + "cost": 1, + "description": "Acquire an observation for role missing-50.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-51", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-51.", + "details": { + "missingRoles": [ + "missing-51" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-51:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-51:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-51", + "cost": 1, + "description": "Acquire an observation for role missing-51.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-52", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-52.", + "details": { + "missingRoles": [ + "missing-52" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-52:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-52:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-52", + "cost": 1, + "description": "Acquire an observation for role missing-52.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-53", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-53.", + "details": { + "missingRoles": [ + "missing-53" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-53:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-53:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-53", + "cost": 1, + "description": "Acquire an observation for role missing-53.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-54", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-54.", + "details": { + "missingRoles": [ + "missing-54" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-54:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-54:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-54", + "cost": 1, + "description": "Acquire an observation for role missing-54.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-55", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-55.", + "details": { + "missingRoles": [ + "missing-55" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-55:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-55:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-55", + "cost": 1, + "description": "Acquire an observation for role missing-55.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-56", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-56.", + "details": { + "missingRoles": [ + "missing-56" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-56:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-56:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-56", + "cost": 1, + "description": "Acquire an observation for role missing-56.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-57", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-57.", + "details": { + "missingRoles": [ + "missing-57" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-57:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-57:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-57", + "cost": 1, + "description": "Acquire an observation for role missing-57.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-58", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-58.", + "details": { + "missingRoles": [ + "missing-58" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-58:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-58:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-58", + "cost": 1, + "description": "Acquire an observation for role missing-58.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-59", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-59.", + "details": { + "missingRoles": [ + "missing-59" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-59:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-59:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-59", + "cost": 1, + "description": "Acquire an observation for role missing-59.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-60", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-60.", + "details": { + "missingRoles": [ + "missing-60" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-60:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-60:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-60", + "cost": 1, + "description": "Acquire an observation for role missing-60.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-61", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-61.", + "details": { + "missingRoles": [ + "missing-61" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-61:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-61:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-61", + "cost": 1, + "description": "Acquire an observation for role missing-61.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-62", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-62.", + "details": { + "missingRoles": [ + "missing-62" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-62:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-62:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-62", + "cost": 1, + "description": "Acquire an observation for role missing-62.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-63", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-63.", + "details": { + "missingRoles": [ + "missing-63" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-63:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-63:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-63", + "cost": 1, + "description": "Acquire an observation for role missing-63.", + "expected": null + } + ] + } + ] + }, + { + "requirementId": "missing-64", + "requirementType": "value_equals", + "required": true, + "status": "UNKNOWN", + "summary": "No observations are bound to required role(s): missing-64.", + "details": { + "missingRoles": [ + "missing-64" + ] + }, + "acquisitionOptions": [ + { + "id": "missing-64:acquire-missing-roles", + "description": "Acquire every missing role required to evaluate this requirement.", + "actions": [ + { + "id": "refresh_observation:missing-64:none", + "type": "REFRESH_OBSERVATION", + "role": "missing-64", + "cost": 1, + "description": "Acquire an observation for role missing-64.", + "expected": null + } + ] + } + ] + } + ], + "acquisitionPlan": { + "status": "INCOMPLETE", + "reason": "Acquisition planning supports at most 64 unresolved requirements.", + "actions": [], + "selectedOptionIds": [], + "totalCost": 0, + "coveredRequirementIds": [], + "unresolvedRequirementIds": [ + "missing-00", + "missing-01", + "missing-02", + "missing-03", + "missing-04", + "missing-05", + "missing-06", + "missing-07", + "missing-08", + "missing-09", + "missing-10", + "missing-11", + "missing-12", + "missing-13", + "missing-14", + "missing-15", + "missing-16", + "missing-17", + "missing-18", + "missing-19", + "missing-20", + "missing-21", + "missing-22", + "missing-23", + "missing-24", + "missing-25", + "missing-26", + "missing-27", + "missing-28", + "missing-29", + "missing-30", + "missing-31", + "missing-32", + "missing-33", + "missing-34", + "missing-35", + "missing-36", + "missing-37", + "missing-38", + "missing-39", + "missing-40", + "missing-41", + "missing-42", + "missing-43", + "missing-44", + "missing-45", + "missing-46", + "missing-47", + "missing-48", + "missing-49", + "missing-50", + "missing-51", + "missing-52", + "missing-53", + "missing-54", + "missing-55", + "missing-56", + "missing-57", + "missing-58", + "missing-59", + "missing-60", + "missing-61", + "missing-62", + "missing-63", + "missing-64" + ] + }, + "verificationRecordDigest": "06818943c48b3bc9b1ccaa6431f29ea1b86f51ad2563fbe7e81a96095944cd10" + } + }, + { + "name": "planner-combination-limit", + "input": { + "protocolVersion": "0.1", + "contract": { + "id": "planner-combination-limit", + "version": "1", + "decisionTime": "2026-09-02T18:00:00.000Z", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "mismatch-00", + "type": "dependency", + "description": "Mismatched dependency 0", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "mismatch-01", + "type": "dependency", + "description": "Mismatched dependency 1", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "mismatch-02", + "type": "dependency", + "description": "Mismatched dependency 2", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "mismatch-03", + "type": "dependency", + "description": "Mismatched dependency 3", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "mismatch-04", + "type": "dependency", + "description": "Mismatched dependency 4", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "mismatch-05", + "type": "dependency", + "description": "Mismatched dependency 5", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "mismatch-06", + "type": "dependency", + "description": "Mismatched dependency 6", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "mismatch-07", + "type": "dependency", + "description": "Mismatched dependency 7", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "mismatch-08", + "type": "dependency", + "description": "Mismatched dependency 8", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "mismatch-09", + "type": "dependency", + "description": "Mismatched dependency 9", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "mismatch-10", + "type": "dependency", + "description": "Mismatched dependency 10", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "mismatch-11", + "type": "dependency", + "description": "Mismatched dependency 11", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "mismatch-12", + "type": "dependency", + "description": "Mismatched dependency 12", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "mismatch-13", + "type": "dependency", + "description": "Mismatched dependency 13", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "mismatch-14", + "type": "dependency", + "description": "Mismatched dependency 14", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "mismatch-15", + "type": "dependency", + "description": "Mismatched dependency 15", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + }, + { + "id": "mismatch-16", + "type": "dependency", + "description": "Mismatched dependency 16", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + } + ] + }, + "observations": [ + { + "id": "obs-head-b", + "role": "head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "value": { + "commit": "commit-B" + }, + "observedAt": "2026-09-02T17:59:59.000Z", + "acquisitionCost": 1, + "witness": { + "provenance": "provider_asserted", + "version": "commit-B" + } + }, + { + "id": "obs-ci-a", + "role": "ci", + "resource": { + "provider": "ci.example", + "account": "acme", + "kind": "ci_run", + "key": "run-2040" + }, + "value": { + "status": "passed" + }, + "observedAt": "2026-09-02T17:59:59.500Z", + "acquisitionCost": 4, + "witness": { + "provenance": "provider_asserted", + "version": "run-2040", + "dependencies": [ + { + "name": "tested_head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "relation": "exact", + "version": "commit-A", + "provenance": "provider_asserted" + } + ] + } + } + ] + }, + "expected": { + "protocolVersion": "0.1", + "engineVersion": "0.1.2", + "canonicalization": "worldcut-json-v1", + "contractId": "planner-combination-limit", + "contractVersion": "1", + "verdict": "CONTRACT_VIOLATED", + "coverage": { + "required": 17, + "satisfied": 0, + "violated": 17, + "unknown": 0, + "advisory": 0 + }, + "requirementResults": [ + { + "requirementId": "mismatch-00", + "requirementType": "dependency", + "required": true, + "status": "VIOLATED", + "summary": "Mismatched dependency 0: commit-A does not equal commit-B.", + "details": { + "dependentRole": "ci", + "dependencyVersion": "commit-A", + "targetRole": "head", + "targetVersion": "commit-B", + "relation": "exact" + }, + "acquisitionOptions": [ + { + "id": "mismatch-00:acquire-compatible-dependent", + "description": "Acquire dependent evidence bound to the selected target version.", + "actions": [ + { + "id": "acquire_compatible_evidence:ci:c969e13b988f", + "type": "ACQUIRE_COMPATIBLE_EVIDENCE", + "role": "ci", + "cost": 4, + "description": "Acquire ci evidence bound to commit-B.", + "expected": { + "targetRole": "head", + "targetVersion": "commit-B" + } + } + ] + }, + { + "id": "mismatch-00:refresh-target", + "description": "Refresh the target before selecting compatible evidence.", + "actions": [ + { + "id": "refresh_observation:head:6df94fb5680d", + "type": "REFRESH_OBSERVATION", + "role": "head", + "cost": 1, + "description": "Refresh head before selecting compatible evidence.", + "expected": { + "dependentRole": "ci", + "dependentVersion": "commit-A" + } + } + ] + } + ] + }, + { + "requirementId": "mismatch-01", + "requirementType": "dependency", + "required": true, + "status": "VIOLATED", + "summary": "Mismatched dependency 1: commit-A does not equal commit-B.", + "details": { + "dependentRole": "ci", + "dependencyVersion": "commit-A", + "targetRole": "head", + "targetVersion": "commit-B", + "relation": "exact" + }, + "acquisitionOptions": [ + { + "id": "mismatch-01:acquire-compatible-dependent", + "description": "Acquire dependent evidence bound to the selected target version.", + "actions": [ + { + "id": "acquire_compatible_evidence:ci:c969e13b988f", + "type": "ACQUIRE_COMPATIBLE_EVIDENCE", + "role": "ci", + "cost": 4, + "description": "Acquire ci evidence bound to commit-B.", + "expected": { + "targetRole": "head", + "targetVersion": "commit-B" + } + } + ] + }, + { + "id": "mismatch-01:refresh-target", + "description": "Refresh the target before selecting compatible evidence.", + "actions": [ + { + "id": "refresh_observation:head:6df94fb5680d", + "type": "REFRESH_OBSERVATION", + "role": "head", + "cost": 1, + "description": "Refresh head before selecting compatible evidence.", + "expected": { + "dependentRole": "ci", + "dependentVersion": "commit-A" + } + } + ] + } + ] + }, + { + "requirementId": "mismatch-02", + "requirementType": "dependency", + "required": true, + "status": "VIOLATED", + "summary": "Mismatched dependency 2: commit-A does not equal commit-B.", + "details": { + "dependentRole": "ci", + "dependencyVersion": "commit-A", + "targetRole": "head", + "targetVersion": "commit-B", + "relation": "exact" + }, + "acquisitionOptions": [ + { + "id": "mismatch-02:acquire-compatible-dependent", + "description": "Acquire dependent evidence bound to the selected target version.", + "actions": [ + { + "id": "acquire_compatible_evidence:ci:c969e13b988f", + "type": "ACQUIRE_COMPATIBLE_EVIDENCE", + "role": "ci", + "cost": 4, + "description": "Acquire ci evidence bound to commit-B.", + "expected": { + "targetRole": "head", + "targetVersion": "commit-B" + } + } + ] + }, + { + "id": "mismatch-02:refresh-target", + "description": "Refresh the target before selecting compatible evidence.", + "actions": [ + { + "id": "refresh_observation:head:6df94fb5680d", + "type": "REFRESH_OBSERVATION", + "role": "head", + "cost": 1, + "description": "Refresh head before selecting compatible evidence.", + "expected": { + "dependentRole": "ci", + "dependentVersion": "commit-A" + } + } + ] + } + ] + }, + { + "requirementId": "mismatch-03", + "requirementType": "dependency", + "required": true, + "status": "VIOLATED", + "summary": "Mismatched dependency 3: commit-A does not equal commit-B.", + "details": { + "dependentRole": "ci", + "dependencyVersion": "commit-A", + "targetRole": "head", + "targetVersion": "commit-B", + "relation": "exact" + }, + "acquisitionOptions": [ + { + "id": "mismatch-03:acquire-compatible-dependent", + "description": "Acquire dependent evidence bound to the selected target version.", + "actions": [ + { + "id": "acquire_compatible_evidence:ci:c969e13b988f", + "type": "ACQUIRE_COMPATIBLE_EVIDENCE", + "role": "ci", + "cost": 4, + "description": "Acquire ci evidence bound to commit-B.", + "expected": { + "targetRole": "head", + "targetVersion": "commit-B" + } + } + ] + }, + { + "id": "mismatch-03:refresh-target", + "description": "Refresh the target before selecting compatible evidence.", + "actions": [ + { + "id": "refresh_observation:head:6df94fb5680d", + "type": "REFRESH_OBSERVATION", + "role": "head", + "cost": 1, + "description": "Refresh head before selecting compatible evidence.", + "expected": { + "dependentRole": "ci", + "dependentVersion": "commit-A" + } + } + ] + } + ] + }, + { + "requirementId": "mismatch-04", + "requirementType": "dependency", + "required": true, + "status": "VIOLATED", + "summary": "Mismatched dependency 4: commit-A does not equal commit-B.", + "details": { + "dependentRole": "ci", + "dependencyVersion": "commit-A", + "targetRole": "head", + "targetVersion": "commit-B", + "relation": "exact" + }, + "acquisitionOptions": [ + { + "id": "mismatch-04:acquire-compatible-dependent", + "description": "Acquire dependent evidence bound to the selected target version.", + "actions": [ + { + "id": "acquire_compatible_evidence:ci:c969e13b988f", + "type": "ACQUIRE_COMPATIBLE_EVIDENCE", + "role": "ci", + "cost": 4, + "description": "Acquire ci evidence bound to commit-B.", + "expected": { + "targetRole": "head", + "targetVersion": "commit-B" + } + } + ] + }, + { + "id": "mismatch-04:refresh-target", + "description": "Refresh the target before selecting compatible evidence.", + "actions": [ + { + "id": "refresh_observation:head:6df94fb5680d", + "type": "REFRESH_OBSERVATION", + "role": "head", + "cost": 1, + "description": "Refresh head before selecting compatible evidence.", + "expected": { + "dependentRole": "ci", + "dependentVersion": "commit-A" + } + } + ] + } + ] + }, + { + "requirementId": "mismatch-05", + "requirementType": "dependency", + "required": true, + "status": "VIOLATED", + "summary": "Mismatched dependency 5: commit-A does not equal commit-B.", + "details": { + "dependentRole": "ci", + "dependencyVersion": "commit-A", + "targetRole": "head", + "targetVersion": "commit-B", + "relation": "exact" + }, + "acquisitionOptions": [ + { + "id": "mismatch-05:acquire-compatible-dependent", + "description": "Acquire dependent evidence bound to the selected target version.", + "actions": [ + { + "id": "acquire_compatible_evidence:ci:c969e13b988f", + "type": "ACQUIRE_COMPATIBLE_EVIDENCE", + "role": "ci", + "cost": 4, + "description": "Acquire ci evidence bound to commit-B.", + "expected": { + "targetRole": "head", + "targetVersion": "commit-B" + } + } + ] + }, + { + "id": "mismatch-05:refresh-target", + "description": "Refresh the target before selecting compatible evidence.", + "actions": [ + { + "id": "refresh_observation:head:6df94fb5680d", + "type": "REFRESH_OBSERVATION", + "role": "head", + "cost": 1, + "description": "Refresh head before selecting compatible evidence.", + "expected": { + "dependentRole": "ci", + "dependentVersion": "commit-A" + } + } + ] + } + ] + }, + { + "requirementId": "mismatch-06", + "requirementType": "dependency", + "required": true, + "status": "VIOLATED", + "summary": "Mismatched dependency 6: commit-A does not equal commit-B.", + "details": { + "dependentRole": "ci", + "dependencyVersion": "commit-A", + "targetRole": "head", + "targetVersion": "commit-B", + "relation": "exact" + }, + "acquisitionOptions": [ + { + "id": "mismatch-06:acquire-compatible-dependent", + "description": "Acquire dependent evidence bound to the selected target version.", + "actions": [ + { + "id": "acquire_compatible_evidence:ci:c969e13b988f", + "type": "ACQUIRE_COMPATIBLE_EVIDENCE", + "role": "ci", + "cost": 4, + "description": "Acquire ci evidence bound to commit-B.", + "expected": { + "targetRole": "head", + "targetVersion": "commit-B" + } + } + ] + }, + { + "id": "mismatch-06:refresh-target", + "description": "Refresh the target before selecting compatible evidence.", + "actions": [ + { + "id": "refresh_observation:head:6df94fb5680d", + "type": "REFRESH_OBSERVATION", + "role": "head", + "cost": 1, + "description": "Refresh head before selecting compatible evidence.", + "expected": { + "dependentRole": "ci", + "dependentVersion": "commit-A" + } + } + ] + } + ] + }, + { + "requirementId": "mismatch-07", + "requirementType": "dependency", + "required": true, + "status": "VIOLATED", + "summary": "Mismatched dependency 7: commit-A does not equal commit-B.", + "details": { + "dependentRole": "ci", + "dependencyVersion": "commit-A", + "targetRole": "head", + "targetVersion": "commit-B", + "relation": "exact" + }, + "acquisitionOptions": [ + { + "id": "mismatch-07:acquire-compatible-dependent", + "description": "Acquire dependent evidence bound to the selected target version.", + "actions": [ + { + "id": "acquire_compatible_evidence:ci:c969e13b988f", + "type": "ACQUIRE_COMPATIBLE_EVIDENCE", + "role": "ci", + "cost": 4, + "description": "Acquire ci evidence bound to commit-B.", + "expected": { + "targetRole": "head", + "targetVersion": "commit-B" + } + } + ] + }, + { + "id": "mismatch-07:refresh-target", + "description": "Refresh the target before selecting compatible evidence.", + "actions": [ + { + "id": "refresh_observation:head:6df94fb5680d", + "type": "REFRESH_OBSERVATION", + "role": "head", + "cost": 1, + "description": "Refresh head before selecting compatible evidence.", + "expected": { + "dependentRole": "ci", + "dependentVersion": "commit-A" + } + } + ] + } + ] + }, + { + "requirementId": "mismatch-08", + "requirementType": "dependency", + "required": true, + "status": "VIOLATED", + "summary": "Mismatched dependency 8: commit-A does not equal commit-B.", + "details": { + "dependentRole": "ci", + "dependencyVersion": "commit-A", + "targetRole": "head", + "targetVersion": "commit-B", + "relation": "exact" + }, + "acquisitionOptions": [ + { + "id": "mismatch-08:acquire-compatible-dependent", + "description": "Acquire dependent evidence bound to the selected target version.", + "actions": [ + { + "id": "acquire_compatible_evidence:ci:c969e13b988f", + "type": "ACQUIRE_COMPATIBLE_EVIDENCE", + "role": "ci", + "cost": 4, + "description": "Acquire ci evidence bound to commit-B.", + "expected": { + "targetRole": "head", + "targetVersion": "commit-B" + } + } + ] + }, + { + "id": "mismatch-08:refresh-target", + "description": "Refresh the target before selecting compatible evidence.", + "actions": [ + { + "id": "refresh_observation:head:6df94fb5680d", + "type": "REFRESH_OBSERVATION", + "role": "head", + "cost": 1, + "description": "Refresh head before selecting compatible evidence.", + "expected": { + "dependentRole": "ci", + "dependentVersion": "commit-A" + } + } + ] + } + ] + }, + { + "requirementId": "mismatch-09", + "requirementType": "dependency", + "required": true, + "status": "VIOLATED", + "summary": "Mismatched dependency 9: commit-A does not equal commit-B.", + "details": { + "dependentRole": "ci", + "dependencyVersion": "commit-A", + "targetRole": "head", + "targetVersion": "commit-B", + "relation": "exact" + }, + "acquisitionOptions": [ + { + "id": "mismatch-09:acquire-compatible-dependent", + "description": "Acquire dependent evidence bound to the selected target version.", + "actions": [ + { + "id": "acquire_compatible_evidence:ci:c969e13b988f", + "type": "ACQUIRE_COMPATIBLE_EVIDENCE", + "role": "ci", + "cost": 4, + "description": "Acquire ci evidence bound to commit-B.", + "expected": { + "targetRole": "head", + "targetVersion": "commit-B" + } + } + ] + }, + { + "id": "mismatch-09:refresh-target", + "description": "Refresh the target before selecting compatible evidence.", + "actions": [ + { + "id": "refresh_observation:head:6df94fb5680d", + "type": "REFRESH_OBSERVATION", + "role": "head", + "cost": 1, + "description": "Refresh head before selecting compatible evidence.", + "expected": { + "dependentRole": "ci", + "dependentVersion": "commit-A" + } + } + ] + } + ] + }, + { + "requirementId": "mismatch-10", + "requirementType": "dependency", + "required": true, + "status": "VIOLATED", + "summary": "Mismatched dependency 10: commit-A does not equal commit-B.", + "details": { + "dependentRole": "ci", + "dependencyVersion": "commit-A", + "targetRole": "head", + "targetVersion": "commit-B", + "relation": "exact" + }, + "acquisitionOptions": [ + { + "id": "mismatch-10:acquire-compatible-dependent", + "description": "Acquire dependent evidence bound to the selected target version.", + "actions": [ + { + "id": "acquire_compatible_evidence:ci:c969e13b988f", + "type": "ACQUIRE_COMPATIBLE_EVIDENCE", + "role": "ci", + "cost": 4, + "description": "Acquire ci evidence bound to commit-B.", + "expected": { + "targetRole": "head", + "targetVersion": "commit-B" + } + } + ] + }, + { + "id": "mismatch-10:refresh-target", + "description": "Refresh the target before selecting compatible evidence.", + "actions": [ + { + "id": "refresh_observation:head:6df94fb5680d", + "type": "REFRESH_OBSERVATION", + "role": "head", + "cost": 1, + "description": "Refresh head before selecting compatible evidence.", + "expected": { + "dependentRole": "ci", + "dependentVersion": "commit-A" + } + } + ] + } + ] + }, + { + "requirementId": "mismatch-11", + "requirementType": "dependency", + "required": true, + "status": "VIOLATED", + "summary": "Mismatched dependency 11: commit-A does not equal commit-B.", + "details": { + "dependentRole": "ci", + "dependencyVersion": "commit-A", + "targetRole": "head", + "targetVersion": "commit-B", + "relation": "exact" + }, + "acquisitionOptions": [ + { + "id": "mismatch-11:acquire-compatible-dependent", + "description": "Acquire dependent evidence bound to the selected target version.", + "actions": [ + { + "id": "acquire_compatible_evidence:ci:c969e13b988f", + "type": "ACQUIRE_COMPATIBLE_EVIDENCE", + "role": "ci", + "cost": 4, + "description": "Acquire ci evidence bound to commit-B.", + "expected": { + "targetRole": "head", + "targetVersion": "commit-B" + } + } + ] + }, + { + "id": "mismatch-11:refresh-target", + "description": "Refresh the target before selecting compatible evidence.", + "actions": [ + { + "id": "refresh_observation:head:6df94fb5680d", + "type": "REFRESH_OBSERVATION", + "role": "head", + "cost": 1, + "description": "Refresh head before selecting compatible evidence.", + "expected": { + "dependentRole": "ci", + "dependentVersion": "commit-A" + } + } + ] + } + ] + }, + { + "requirementId": "mismatch-12", + "requirementType": "dependency", + "required": true, + "status": "VIOLATED", + "summary": "Mismatched dependency 12: commit-A does not equal commit-B.", + "details": { + "dependentRole": "ci", + "dependencyVersion": "commit-A", + "targetRole": "head", + "targetVersion": "commit-B", + "relation": "exact" + }, + "acquisitionOptions": [ + { + "id": "mismatch-12:acquire-compatible-dependent", + "description": "Acquire dependent evidence bound to the selected target version.", + "actions": [ + { + "id": "acquire_compatible_evidence:ci:c969e13b988f", + "type": "ACQUIRE_COMPATIBLE_EVIDENCE", + "role": "ci", + "cost": 4, + "description": "Acquire ci evidence bound to commit-B.", + "expected": { + "targetRole": "head", + "targetVersion": "commit-B" + } + } + ] + }, + { + "id": "mismatch-12:refresh-target", + "description": "Refresh the target before selecting compatible evidence.", + "actions": [ + { + "id": "refresh_observation:head:6df94fb5680d", + "type": "REFRESH_OBSERVATION", + "role": "head", + "cost": 1, + "description": "Refresh head before selecting compatible evidence.", + "expected": { + "dependentRole": "ci", + "dependentVersion": "commit-A" + } + } + ] + } + ] + }, + { + "requirementId": "mismatch-13", + "requirementType": "dependency", + "required": true, + "status": "VIOLATED", + "summary": "Mismatched dependency 13: commit-A does not equal commit-B.", + "details": { + "dependentRole": "ci", + "dependencyVersion": "commit-A", + "targetRole": "head", + "targetVersion": "commit-B", + "relation": "exact" + }, + "acquisitionOptions": [ + { + "id": "mismatch-13:acquire-compatible-dependent", + "description": "Acquire dependent evidence bound to the selected target version.", + "actions": [ + { + "id": "acquire_compatible_evidence:ci:c969e13b988f", + "type": "ACQUIRE_COMPATIBLE_EVIDENCE", + "role": "ci", + "cost": 4, + "description": "Acquire ci evidence bound to commit-B.", + "expected": { + "targetRole": "head", + "targetVersion": "commit-B" + } + } + ] + }, + { + "id": "mismatch-13:refresh-target", + "description": "Refresh the target before selecting compatible evidence.", + "actions": [ + { + "id": "refresh_observation:head:6df94fb5680d", + "type": "REFRESH_OBSERVATION", + "role": "head", + "cost": 1, + "description": "Refresh head before selecting compatible evidence.", + "expected": { + "dependentRole": "ci", + "dependentVersion": "commit-A" + } + } + ] + } + ] + }, + { + "requirementId": "mismatch-14", + "requirementType": "dependency", + "required": true, + "status": "VIOLATED", + "summary": "Mismatched dependency 14: commit-A does not equal commit-B.", + "details": { + "dependentRole": "ci", + "dependencyVersion": "commit-A", + "targetRole": "head", + "targetVersion": "commit-B", + "relation": "exact" + }, + "acquisitionOptions": [ + { + "id": "mismatch-14:acquire-compatible-dependent", + "description": "Acquire dependent evidence bound to the selected target version.", + "actions": [ + { + "id": "acquire_compatible_evidence:ci:c969e13b988f", + "type": "ACQUIRE_COMPATIBLE_EVIDENCE", + "role": "ci", + "cost": 4, + "description": "Acquire ci evidence bound to commit-B.", + "expected": { + "targetRole": "head", + "targetVersion": "commit-B" + } + } + ] + }, + { + "id": "mismatch-14:refresh-target", + "description": "Refresh the target before selecting compatible evidence.", + "actions": [ + { + "id": "refresh_observation:head:6df94fb5680d", + "type": "REFRESH_OBSERVATION", + "role": "head", + "cost": 1, + "description": "Refresh head before selecting compatible evidence.", + "expected": { + "dependentRole": "ci", + "dependentVersion": "commit-A" + } + } + ] + } + ] + }, + { + "requirementId": "mismatch-15", + "requirementType": "dependency", + "required": true, + "status": "VIOLATED", + "summary": "Mismatched dependency 15: commit-A does not equal commit-B.", + "details": { + "dependentRole": "ci", + "dependencyVersion": "commit-A", + "targetRole": "head", + "targetVersion": "commit-B", + "relation": "exact" + }, + "acquisitionOptions": [ + { + "id": "mismatch-15:acquire-compatible-dependent", + "description": "Acquire dependent evidence bound to the selected target version.", + "actions": [ + { + "id": "acquire_compatible_evidence:ci:c969e13b988f", + "type": "ACQUIRE_COMPATIBLE_EVIDENCE", + "role": "ci", + "cost": 4, + "description": "Acquire ci evidence bound to commit-B.", + "expected": { + "targetRole": "head", + "targetVersion": "commit-B" + } + } + ] + }, + { + "id": "mismatch-15:refresh-target", + "description": "Refresh the target before selecting compatible evidence.", + "actions": [ + { + "id": "refresh_observation:head:6df94fb5680d", + "type": "REFRESH_OBSERVATION", + "role": "head", + "cost": 1, + "description": "Refresh head before selecting compatible evidence.", + "expected": { + "dependentRole": "ci", + "dependentVersion": "commit-A" + } + } + ] + } + ] + }, + { + "requirementId": "mismatch-16", + "requirementType": "dependency", + "required": true, + "status": "VIOLATED", + "summary": "Mismatched dependency 16: commit-A does not equal commit-B.", + "details": { + "dependentRole": "ci", + "dependencyVersion": "commit-A", + "targetRole": "head", + "targetVersion": "commit-B", + "relation": "exact" + }, + "acquisitionOptions": [ + { + "id": "mismatch-16:acquire-compatible-dependent", + "description": "Acquire dependent evidence bound to the selected target version.", + "actions": [ + { + "id": "acquire_compatible_evidence:ci:c969e13b988f", + "type": "ACQUIRE_COMPATIBLE_EVIDENCE", + "role": "ci", + "cost": 4, + "description": "Acquire ci evidence bound to commit-B.", + "expected": { + "targetRole": "head", + "targetVersion": "commit-B" + } + } + ] + }, + { + "id": "mismatch-16:refresh-target", + "description": "Refresh the target before selecting compatible evidence.", + "actions": [ + { + "id": "refresh_observation:head:6df94fb5680d", + "type": "REFRESH_OBSERVATION", + "role": "head", + "cost": 1, + "description": "Refresh head before selecting compatible evidence.", + "expected": { + "dependentRole": "ci", + "dependentVersion": "commit-A" + } + } + ] + } + ] + } + ], + "acquisitionPlan": { + "status": "INCOMPLETE", + "reason": "Acquisition search exceeds the 65536 combination limit.", + "actions": [], + "selectedOptionIds": [], + "totalCost": 0, + "coveredRequirementIds": [], + "unresolvedRequirementIds": [ + "mismatch-00", + "mismatch-01", + "mismatch-02", + "mismatch-03", + "mismatch-04", + "mismatch-05", + "mismatch-06", + "mismatch-07", + "mismatch-08", + "mismatch-09", + "mismatch-10", + "mismatch-11", + "mismatch-12", + "mismatch-13", + "mismatch-14", + "mismatch-15", + "mismatch-16" + ] + }, + "verificationRecordDigest": "c637dc64eddfccf6617fb47aad5bb4f9e1f33efd7ac31b8b038b155f54428969" + } + }, + { + "name": "reversed-ordering", + "input": { + "protocolVersion": "0.1", + "contract": { + "id": "deploy-current-tested-release", + "version": "1", + "decisionTime": "2026-09-02T18:00:00.000Z", + "assumptions": { + "clockModel": "trusted_normalized", + "intervalModel": "half_open", + "metadataModel": "honest_but_possibly_incomplete" + }, + "requirements": [ + { + "id": "approval-and-quote-overlap", + "type": "common_valid_time", + "description": "The approval and quote were valid at a common time", + "roles": [ + "approval", + "quote" + ], + "within": { + "from": "2026-09-02T17:55:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + } + }, + { + "id": "ci-status-passed", + "type": "value_equals", + "description": "The CI status is passed", + "role": "ci", + "path": [ + "status" + ], + "expected": "passed" + }, + { + "id": "ci-tested-current-head", + "type": "dependency", + "description": "The passing CI run tested the selected branch head", + "dependentRole": "ci", + "targetRole": "head", + "dependencyName": "tested_head" + } + ] + }, + "observations": [ + { + "id": "obs-quote", + "role": "quote", + "resource": { + "provider": "pricing.example", + "account": "acme", + "kind": "quote", + "key": "release-2041" + }, + "value": { + "amount": 1250, + "currency": "USD" + }, + "observedAt": "2026-09-02T17:59:59.500Z", + "acquisitionCost": 3, + "witness": { + "provenance": "provider_asserted", + "version": "quote-17", + "validity": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:03:00.000Z" + } + } + }, + { + "id": "obs-approval", + "role": "approval", + "resource": { + "provider": "change.example", + "account": "acme", + "kind": "approval", + "key": "release-2041" + }, + "value": { + "approved": true + }, + "observedAt": "2026-09-02T17:59:59.000Z", + "acquisitionCost": 2, + "witness": { + "provenance": "provider_asserted", + "version": "approval-8", + "validity": { + "from": "2026-09-02T17:50:00.000Z", + "until": "2026-09-02T18:05:00.000Z" + } + } + }, + { + "id": "obs-ci-b", + "role": "ci", + "resource": { + "provider": "ci.example", + "account": "acme", + "kind": "ci_run", + "key": "run-2041" + }, + "value": { + "status": "passed" + }, + "observedAt": "2026-09-02T17:59:58.500Z", + "acquisitionCost": 4, + "witness": { + "provenance": "provider_asserted", + "version": "run-2041", + "dependencies": [ + { + "name": "tested_head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "relation": "exact", + "version": "commit-B", + "provenance": "provider_asserted" + } + ] + } + }, + { + "id": "obs-head-b", + "role": "head", + "resource": { + "provider": "github", + "account": "acme", + "kind": "branch_head", + "key": "payments/main" + }, + "value": { + "commit": "commit-B" + }, + "observedAt": "2026-09-02T17:59:58.000Z", + "acquisitionCost": 1, + "witness": { + "provenance": "provider_asserted", + "version": "commit-B" + } + } + ] + }, + "expected": { + "protocolVersion": "0.1", + "engineVersion": "0.1.2", + "canonicalization": "worldcut-json-v1", + "contractId": "deploy-current-tested-release", + "contractVersion": "1", + "verdict": "CONTRACT_SATISFIED", + "coverage": { + "required": 3, + "satisfied": 3, + "violated": 0, + "unknown": 0, + "advisory": 0 + }, + "requirementResults": [ + { + "requirementId": "approval-and-quote-overlap", + "requirementType": "common_valid_time", + "required": true, + "status": "SATISFIED", + "summary": "The approval and quote were valid at a common time: a common valid time exists.", + "details": { + "roles": [ + "approval", + "quote" + ], + "commonWindow": { + "from": "2026-09-02T17:58:00.000Z", + "until": "2026-09-02T18:00:00.001Z" + } + }, + "acquisitionOptions": [] + }, + { + "requirementId": "ci-status-passed", + "requirementType": "value_equals", + "required": true, + "status": "SATISFIED", + "summary": "The CI status is passed: observed value matches the requirement.", + "details": { + "role": "ci", + "path": [ + "status" + ], + "expected": "passed" + }, + "acquisitionOptions": [] + }, + { + "requirementId": "ci-tested-current-head", + "requirementType": "dependency", + "required": true, + "status": "SATISFIED", + "summary": "The passing CI run tested the selected branch head: both roles are bound to commit-B.", + "details": { + "dependentRole": "ci", + "targetRole": "head", + "version": "commit-B" + }, + "acquisitionOptions": [] + } + ], + "acquisitionPlan": { + "status": "NOT_NEEDED", + "reason": null, + "actions": [], + "selectedOptionIds": [], + "totalCost": 0, + "coveredRequirementIds": [], + "unresolvedRequirementIds": [] + }, + "verificationRecordDigest": "f402861e5df539f5ea4b69681431c2e2bcbd18e7cb5a02c63a7cfca3b9bb3eee" + } + } + ] +} diff --git a/ports/dotnet/tests/WorldCut.Tests/packages.lock.json b/ports/dotnet/tests/WorldCut.Tests/packages.lock.json new file mode 100644 index 0000000..f59f01b --- /dev/null +++ b/ports/dotnet/tests/WorldCut.Tests/packages.lock.json @@ -0,0 +1,281 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "xunit.v3": { + "type": "Direct", + "requested": "[4.0.0, )", + "resolved": "4.0.0", + "contentHash": "czH4MaZ2k2eLjetuN5W1fUEnNVADsTOeYxDvrqIFh04XO6H3Y8Yu1FrSy3psF1q06DZV33wftjqZVv4acwIp9Q==", + "dependencies": { + "xunit.v3.mtp-v2": "[4.0.0]" + } + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" + }, + "Microsoft.Testing.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "2.3.3", + "contentHash": "nY8ceQyPWB9TRE1WE5Oe/sks2e10SxgPv61vBHsFYpgCeDtNwhpMZwmL29GklO3/etTdOsLdrCmNr4zJWaR2fg==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.23.0", + "Microsoft.Testing.Platform": "2.3.3" + } + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "2.3.3", + "contentHash": "dceVNxnfTEjKnrIreLcMfkgrzzBY8kgCCJX5NAv7Lq/6vPlTP4VB5zxKeuYy0yfCoBtWuPkLjUG6qtOBMfpypA==", + "dependencies": { + "Microsoft.Testing.Platform": "2.3.3" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "2.3.3", + "contentHash": "ENbH4BQh9riXtOKc25KKITfiGGWhWMBJA7pZuNaF9zxzzSaVkp5AMeZxGQ6sXYaR6xb724NLz985Is6XIYqLSg==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "2.3.3", + "contentHash": "iVAvNbZ5JPDZSrTcIrCDoCsAkzjDxwDEt2mDuMd8ng1P6e2P2NCd0g0BsoBVG+nJLSmK//V+yeEvjCa3E2fxHw==", + "dependencies": { + "Microsoft.Testing.Platform": "2.3.3" + } + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw==" + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "2UtauxWDa9C6bT7MvFfZkNoFulfflb00jnDU2xeVO9Y58l4Ah2Mv/HiMs4b0zdpK/SfAxpajkgKNMN8zBKa+7Q==" + }, + "xunit.v3.assert": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "QxYfC+98lCMe7Kl9iWDUeUn+gmiPJ2Sz/r+trSdp1mvKyDMXj8S1kYdfkFNJSHyUSQ6KWomenSDgfWhGpR8zEQ==" + }, + "xunit.v3.common": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "cjaNGmOVA5QJxcp6uuSsDhbt0lNmxezFo226mbYOuXm9E9Owq8bYTKxa9wnAMZRnbvyCmCYhAvc/+UAzf0M2vA==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "6.0.0" + } + }, + "xunit.v3.core.mtp-v2": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "2I7apws+6HPz5aYi4choAn7c8P4jPAvwbfAtLSy0JPH89oeY/z1Zqz65Cm3HJmput5l56Z1sJOinTxsAQmbyWQ==", + "dependencies": { + "Microsoft.Testing.Extensions.Telemetry": "2.3.3", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.3.3", + "Microsoft.Testing.Platform": "2.3.3", + "Microsoft.Testing.Platform.MSBuild": "2.3.3", + "xunit.v3.extensibility.core": "[4.0.0]", + "xunit.v3.runner.inproc.console": "[4.0.0]" + } + }, + "xunit.v3.extensibility.core": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "+tTe9VX2vwrUHGI48FE4ly956Ry084wPH4I/7g5D36AkAMGjQRZZDVz8eH2GwX/CLS9PDQRSca534WyrAYsyzw==", + "dependencies": { + "xunit.v3.common": "[4.0.0]" + } + }, + "xunit.v3.mtp-v2": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "svYjct2c3VbLyUSB2r9VWiIkYwemwXHhOgIVAor8RvupcZBFcdiGdmq8G519ZCu30yjPCr0EP3Hy4mvgEXcXpQ==", + "dependencies": { + "xunit.analyzers": "2.0.0", + "xunit.v3.assert": "[4.0.0]", + "xunit.v3.core.mtp-v2": "[4.0.0]" + } + }, + "xunit.v3.runner.common": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "1IEIAVRgnPo9nihd9D0TvxxsLKVRySa+K0wLy/m0SJ4RMdveRCUj/mICFboruO+ILUJ10fUfCCyp7MC5/y7cGw==", + "dependencies": { + "Microsoft.Win32.Registry": "[5.0.0]", + "System.Security.AccessControl": "[6.0.1]", + "xunit.v3.common": "[4.0.0]" + } + }, + "xunit.v3.runner.inproc.console": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "Sp5AALIlZUf2U5pEt2LHs+ebn18eAPSFnXEouJTltLez5p+NQvBm7kzsKSkZOM6iyoS6nuN/wKdsJt2LvixjsQ==", + "dependencies": { + "xunit.v3.extensibility.core": "[4.0.0]", + "xunit.v3.runner.common": "[4.0.0]" + } + }, + "worldcut": { + "type": "Project" + }, + "worldcut.tool": { + "type": "Project", + "dependencies": { + "WorldCut": "[0.1.0, )" + } + } + }, + "net8.0": { + "xunit.v3": { + "type": "Direct", + "requested": "[4.0.0, )", + "resolved": "4.0.0", + "contentHash": "czH4MaZ2k2eLjetuN5W1fUEnNVADsTOeYxDvrqIFh04XO6H3Y8Yu1FrSy3psF1q06DZV33wftjqZVv4acwIp9Q==", + "dependencies": { + "xunit.v3.mtp-v2": "[4.0.0]" + } + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" + }, + "Microsoft.Testing.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "2.3.3", + "contentHash": "nY8ceQyPWB9TRE1WE5Oe/sks2e10SxgPv61vBHsFYpgCeDtNwhpMZwmL29GklO3/etTdOsLdrCmNr4zJWaR2fg==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.23.0", + "Microsoft.Testing.Platform": "2.3.3" + } + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "2.3.3", + "contentHash": "dceVNxnfTEjKnrIreLcMfkgrzzBY8kgCCJX5NAv7Lq/6vPlTP4VB5zxKeuYy0yfCoBtWuPkLjUG6qtOBMfpypA==", + "dependencies": { + "Microsoft.Testing.Platform": "2.3.3" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "2.3.3", + "contentHash": "ENbH4BQh9riXtOKc25KKITfiGGWhWMBJA7pZuNaF9zxzzSaVkp5AMeZxGQ6sXYaR6xb724NLz985Is6XIYqLSg==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "2.3.3", + "contentHash": "iVAvNbZ5JPDZSrTcIrCDoCsAkzjDxwDEt2mDuMd8ng1P6e2P2NCd0g0BsoBVG+nJLSmK//V+yeEvjCa3E2fxHw==", + "dependencies": { + "Microsoft.Testing.Platform": "2.3.3" + } + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw==" + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "2UtauxWDa9C6bT7MvFfZkNoFulfflb00jnDU2xeVO9Y58l4Ah2Mv/HiMs4b0zdpK/SfAxpajkgKNMN8zBKa+7Q==" + }, + "xunit.v3.assert": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "QxYfC+98lCMe7Kl9iWDUeUn+gmiPJ2Sz/r+trSdp1mvKyDMXj8S1kYdfkFNJSHyUSQ6KWomenSDgfWhGpR8zEQ==" + }, + "xunit.v3.common": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "cjaNGmOVA5QJxcp6uuSsDhbt0lNmxezFo226mbYOuXm9E9Owq8bYTKxa9wnAMZRnbvyCmCYhAvc/+UAzf0M2vA==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "6.0.0" + } + }, + "xunit.v3.core.mtp-v2": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "2I7apws+6HPz5aYi4choAn7c8P4jPAvwbfAtLSy0JPH89oeY/z1Zqz65Cm3HJmput5l56Z1sJOinTxsAQmbyWQ==", + "dependencies": { + "Microsoft.Testing.Extensions.Telemetry": "2.3.3", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.3.3", + "Microsoft.Testing.Platform": "2.3.3", + "Microsoft.Testing.Platform.MSBuild": "2.3.3", + "xunit.v3.extensibility.core": "[4.0.0]", + "xunit.v3.runner.inproc.console": "[4.0.0]" + } + }, + "xunit.v3.extensibility.core": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "+tTe9VX2vwrUHGI48FE4ly956Ry084wPH4I/7g5D36AkAMGjQRZZDVz8eH2GwX/CLS9PDQRSca534WyrAYsyzw==", + "dependencies": { + "xunit.v3.common": "[4.0.0]" + } + }, + "xunit.v3.mtp-v2": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "svYjct2c3VbLyUSB2r9VWiIkYwemwXHhOgIVAor8RvupcZBFcdiGdmq8G519ZCu30yjPCr0EP3Hy4mvgEXcXpQ==", + "dependencies": { + "xunit.analyzers": "2.0.0", + "xunit.v3.assert": "[4.0.0]", + "xunit.v3.core.mtp-v2": "[4.0.0]" + } + }, + "xunit.v3.runner.common": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "1IEIAVRgnPo9nihd9D0TvxxsLKVRySa+K0wLy/m0SJ4RMdveRCUj/mICFboruO+ILUJ10fUfCCyp7MC5/y7cGw==", + "dependencies": { + "Microsoft.Win32.Registry": "[5.0.0]", + "System.Security.AccessControl": "[6.0.1]", + "xunit.v3.common": "[4.0.0]" + } + }, + "xunit.v3.runner.inproc.console": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "Sp5AALIlZUf2U5pEt2LHs+ebn18eAPSFnXEouJTltLez5p+NQvBm7kzsKSkZOM6iyoS6nuN/wKdsJt2LvixjsQ==", + "dependencies": { + "xunit.v3.extensibility.core": "[4.0.0]", + "xunit.v3.runner.common": "[4.0.0]" + } + }, + "worldcut": { + "type": "Project" + }, + "worldcut.tool": { + "type": "Project", + "dependencies": { + "WorldCut": "[0.1.0, )" + } + } + } + } +} \ No newline at end of file diff --git a/scripts/generate-conformance.mjs b/scripts/generate-conformance.mjs index ff1d831..21f43a5 100644 --- a/scripts/generate-conformance.mjs +++ b/scripts/generate-conformance.mjs @@ -14,6 +14,16 @@ const outputDirectory = join(projectRoot, "conformance", "0.1"); const mirrorDirectories = [ join(projectRoot, "ports", "go", "testdata", "conformance", "0.1"), join(projectRoot, "ports", "python", "tests", "data", "conformance", "0.1"), + join( + projectRoot, + "ports", + "dotnet", + "tests", + "WorldCut.Tests", + "data", + "conformance", + "0.1", + ), ]; const writeMode = process.argv.includes("--write");