Skip to content

Take exactly one verdict from each creation validator - #6430

Merged
rbuergi merged 1 commit into
mainfrom
fix/6391-validator-verdict-totality
Oct 10, 2026
Merged

rbuergi merged 1 commit into
mainfrom
fix/6391-validator-verdict-totality

Conversation

@rbuergi

@rbuergi rbuergi commented Oct 10, 2026

Copy link
Copy Markdown
Contributor

What changed

RunCreationValidatorsObs (src/MeshWeaver.Mesh.Contract/MeshExtensions.cs) now holds every creation validator to the INodeValidator contract ("emits exactly one result and completes") instead of trusting it:

Supersedes the genuine half of #6423

#6423 bundled these two fixes with a per-validator timeout (NestedTimeout on each validator). That part is deliberately left out. It turns a stall into a refusal one rung earlier without finding out why the validator does not answer. The create's own deadline already answers that case, and since #6421 it names the stalled validator (validators: <TypeName> (i/n)). The root cause of the production stall (why that validator's read never answers) stays open on #6391. #6423 also resolved a service with GetService ?? new, which this PR does not carry.

How tested

New test/MeshWeaver.Hosting.Test/CreationValidatorGivesExactlyOneVerdictTest.cs. No sleeps; each assertion checks what the verdict contains, inside the operation budget:

Test With the fix On main (fix reverted, same test binary)
validator answers Valid and stays subscribed, then a refusing validator the refusal arrives (ValidationFailed), nothing stored FAIL: Unavailable, "reached no verdict within 6s … stage 'validators: MisbehavingCreationValidator'"
validator answers Valid and stays subscribed, nothing refuses commits FAIL: same stall
validator completes empty Unavailable, validator and path named, nothing stored FAIL: success=True, the node was CREATED
control: the same validators armed for other paths commits passes

LifecycleRequestsAlwaysReachAVerdictTest (5 tests) still passes, and so does LocalizationTest. Builds are clean with -c Release -warnaserror for Messaging.Hub, Mesh.Contract, Documentation, Hosting.Test and Messaging.Hub.Test.

Doc: Doc/Architecture/WriteVerdictTotality gains the section "Each validator gives EXACTLY ONE verdict"; the AccessControl #2742 paragraph now points to it.

Nothing to recycle: Mesh.Contract is framework code and takes effect when a portal rolls.

Mirror-sync: none — the new key activity.node.create.validatorGaveNoVerdict is server-side create-refusal text, which no React client renders from the catalog; the next routine npm run sync:i18n picks it up.

Refs #6391

🤖 Generated with Claude Code

RunCreationValidatorsObs now takes each validator's FIRST emission as its
verdict (Take(1) per validator), so a validator that answers and stays
subscribed no longer holds every validator after it - and the create - until
the create's deadline. A validator that completes WITHOUT a verdict is now a
refusal (Unavailable, naming the validator), never a pass: no verdict is not
consent, the #2742 rule generalised from RLS to every validator.

A validator that never answers is deliberately not bounded per validator: the
create's deadline already answers and names it, and why its read stalls is the
open root cause on #6391.

Refs #6391

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 10, 2026 08:26
@rbuergi
rbuergi enabled auto-merge October 10, 2026 08:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The implementation is reactive, fail-closed, localized, documented, and covered by focused integration tests.

0 open findings

What changed in this PR

Enforces one definitive result per creation validator, preventing live validators from stalling creation and empty validators from failing open.

Changes:

  • Takes only each validator’s first emission and rejects empty completion as unavailable.
  • Adds English/German refusal localization and architecture documentation.
  • Adds integration tests covering stalled, empty, refusing, and healthy validators.
File Description
src/​MeshWeaver.Mesh.Contract/​MeshExtensions.cs Enforces one creation-validation verdict.
src/​MeshWeaver.Messaging.Hub/​Localization/​strings.en.json Adds English refusal text.
src/​MeshWeaver.Messaging.Hub/​Localization/​strings.de.json Adds German refusal text.
test/​MeshWeaver.Hosting.Test/​CreationValidatorGivesExactlyOneVerdictTest.cs Tests validator terminal behavior.
src/​MeshWeaver.Documentation/​Data/​Architecture/​WriteVerdictTotality.md Documents the enforced contract.
src/​MeshWeaver.Documentation/​Data/​Architecture/​AccessControl.md Updates fail-closed guidance.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

    20 files   -   4      20 suites   - 4   51m 28s ⏱️ -48s
11 189 tests  - 290  11 189 ✅  - 97  0 💤  - 193  0 ❌ ±0 
11 203 runs   - 290  11 203 ✅  - 97  0 💤  - 193  0 ❌ ±0 

Results for commit a5d2d72. ± Comparison against base commit 98a6d57.

This pull request removes 334 and adds 20 tests. Note that renamed tests count towards both.

   --- End of inner exception stack trace ---
   --- End of inner exception stack trace ---, expected: True)
   --- End of inner exception stack trace ---, isDenial: True)
 ---> (Inner Exception #1) MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.<---
 ---> (Inner Exception #1) MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432<---
 ---> (Inner Exception #1) System.ArgumentException: Value does not fall within the expected range.<---
 ---> (Inner Exception #1) System.InvalidOperationException: boom<---
 ---> (Inner Exception #1) System.InvalidOperationException: source B is misconfigured<---
 ---> (Inner Exception #1) System.Net.Sockets.SocketException (0xFFFDFFFF): Name or service not known<---
…
Memex.Portal.Shared.Test.InstanceIdRulesMatchTheRegistryTest ‑ TheSetupHostAgreesWithTheRegistry(candidate: "522b1a60-bd69-4810-a074-f51c589c3ea3")
Memex.Portal.Shared.Test.SessionDenialIsAnAnswerTest ‑ OnlyAVerdictReadsAsADenial(shape: "the same verdict nested, as a late denial dispatch"···, failure: System.InvalidOperationException: write failed
 ---> System.UnauthorizedAccessException: Access denied
   --- End of inner exception stack trace ---, isDenial: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall as an aggregate's FIRST member", fault: System.AggregateException: One or more errors occurred. (Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.)
 ---> MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.
   --- End of inner exception stack trace ---, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall as an aggregate's SECOND member", fault: System.AggregateException: One or more errors occurred. (The operation has timed out.) (Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.)
 ---> System.TimeoutException: The operation has timed out.
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.<---
, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall nested in an aggregate inside a wrapper", fault: System.InvalidOperationException: outer
 ---> System.AggregateException: One or more errors occurred. (Value does not fall within the expected range.) (Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.)
 ---> System.ArgumentException: Value does not fall within the expected range.
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.<---

   --- End of inner exception stack trace ---, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall wrapped as InnerException", fault: System.InvalidOperationException: outer
 ---> MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.
   --- End of inner exception stack trace ---, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "an aggregate of unrelated faults", fault: System.AggregateException: One or more errors occurred. (The operation has timed out.) (Value does not fall within the expected range.)
 ---> System.TimeoutException: The operation has timed out.
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) System.ArgumentException: Value does not fall within the expected range.<---
, expected: False)
MeshWeaver.Hosting.Test.CreationValidatorGivesExactlyOneVerdictTest ‑ ACreateNoValidatorMisbehavesFor_StillSucceeds
MeshWeaver.Hosting.Test.CreationValidatorGivesExactlyOneVerdictTest ‑ AValidatorThatAnswersAndStaysSubscribed_DoesNotBlockTheNextValidator
MeshWeaver.Hosting.Test.CreationValidatorGivesExactlyOneVerdictTest ‑ AValidatorThatAnswersValidAndStaysSubscribed_LetsTheCreateCommit
…

@rbuergi
rbuergi merged commit 179c3f0 into main Oct 10, 2026
@rbuergi
rbuergi deleted the fix/6391-validator-verdict-totality branch October 10, 2026 13:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants