From 9d7670eeaf1e5c443eae38acf41116ae223992b7 Mon Sep 17 00:00:00 2001
From: piotr-blue
Date: Mon, 27 Jul 2026 23:26:43 +0200
Subject: [PATCH 01/16] refactor(processor): remove obsolete classes and
migrate alias preprocessor for benchmarks
Remove outdated classes including `StaticPayloadValidator`, `CoordinationRepositoryCompatibilityNodeProvider`, and legacy `RepositoryTypeAliasPreprocessor`. Migrate a tailored version of `RepositoryTypeAliasPreprocessor` to benchmarks for alias-related tests. Introduce new processors for timelines and coordination document splitting.
---
README.md | 259 ++-
build.gradle | 26 +
docs/coordination-v2-layered-delivery-plan.md | 39 +
settings.gradle | 16 +
.../processor/ComputeEffectPlanBenchmark.java | 19 +-
.../RepositoryTypeAliasPreprocessor.java | 84 +
.../ResolvedProcessingHostStoryBenchmark.java | 12 +-
.../AllTimelinesChannelProcessor.java | 21 +-
...imelinesExternalSubscriptionFunctions.java | 149 ++
.../processor/BlueSemanticIdentity.java | 16 +-
.../ChatWorkflowOperationProcessor.java | 7 +
.../CompositeTimelineChannelProcessor.java | 21 +-
...TimelineExternalSubscriptionFunctions.java | 140 ++
.../CoordinationDocumentSplitter.java | 1470 +++++++++++++++++
.../processor/CoordinationEventNodes.java | 540 ++++--
...onRepositoryCompatibilityNodeProvider.java | 77 -
.../processor/OperationRequestMatcher.java | 48 +-
.../OperationRequestRoutingFunctions.java | 150 ++
.../SequentialWorkflowOperationProcessor.java | 7 +
.../SequentialWorkflowProcessor.java | 15 +-
.../processor/TimelineChannelProcessor.java | 15 +-
...TimelineExternalSubscriptionFunctions.java | 183 ++
.../TimelineMemberSubscriptions.java | 135 ++
.../processor/TimelineProviderSupport.java | 227 ++-
.../bex/BexWorkflowContextFactory.java | 11 +-
.../processor/workflow/ComputeEffectPlan.java | 17 +
.../workflow/ComputeProgramNormalizer.java | 35 +-
.../workflow/ComputeResultEmitter.java | 89 +-
.../workflow/ComputeStepExecutor.java | 20 +-
.../processor/workflow/FrozenNodeUtil.java | 47 +-
.../processor/workflow/NodeUtil.java | 35 +-
.../workflow/SequentialWorkflowPlan.java | 93 +-
.../workflow/SequentialWorkflowRunner.java | 24 +-
.../workflow/StaticPayloadValidator.java | 49 -
.../processor/workflow/StaticUpdatePlan.java | 76 +-
.../TerminateProcessingStepExecutor.java | 21 +-
.../workflow/TriggerEventStepExecutor.java | 82 +-
.../workflow/UpdateDocumentStepExecutor.java | 93 +-
.../workflow/WorkflowPatchEntry.java | 9 +-
.../AllTimelinesChannelProcessorTest.java | 120 +-
...otstrapDocumentTransportRoundTripTest.java | 11 +-
...CompositeTimelineChannelProcessorTest.java | 224 +--
...ationDocumentSplitterDeepLocalityTest.java | 794 +++++++++
...rdinationDocumentSplitterLocalityTest.java | 489 ++++++
...nDocumentSplitterProcessingMatrixTest.java | 1270 ++++++++++++++
.../CoordinationDocumentSplitterTest.java | 853 ++++++++++
.../processor/CoordinationProcessorsTest.java | 18 +-
.../processor/CoordinationTestResources.java | 4 +-
.../CounterSnapshotRoundTripStressTest.java | 56 +-
.../DeclaredTypeEventMatchingTest.java | 8 +-
.../EmbeddedTerminationWorkflowTest.java | 21 +-
.../InheritedStaticUpdateDocumentTest.java | 18 +-
.../MustUnderstandContractsTest.java | 17 +-
.../OperationRequestLogicalRoutingTest.java | 787 +++++++++
...OperationRequestRoutingEvaluationTest.java | 160 +-
...perationRequestRoutingIntegrationTest.java | 116 +-
.../ProcessingResultTestSupport.java | 52 +
...ublishedTimelineChannelResolutionTest.java | 71 +-
.../RepositoryStyleCounterDocumentTest.java | 53 +-
.../RepositoryTypeAliasPreprocessor.java | 15 +-
.../processor/RuntimeChannelsTest.java | 38 +-
...SelectiveProcessingReportArtifactTest.java | 596 +++++++
.../SelectiveProcessingReportWriter.java | 551 ++++++
.../SelectiveProcessingReportWriterTest.java | 317 ++++
.../SequentialWorkflowExecutionTest.java | 14 +-
.../processor/Task9PublishedArtifactTest.java | 17 +-
.../processor/TestTimelineProvider.java | 12 +-
.../TimelineChannelBindingMatchingTest.java | 13 +-
.../TimelineChannelProcessorTest.java | 161 +-
.../TimelineCheckpointSubjectTest.java | 159 ++
.../TriggerEventStepExecutorTest.java | 102 +-
.../BexCounterPersistenceRoundTripTest.java | 29 +-
.../BexCounterResourceWorkflowTest.java | 6 +-
...puteFrozenPatchHandoffIntegrationTest.java | 22 +-
.../ComputeProgramPlanIntegrationTest.java | 22 +-
.../ComputeTerminationWorkflowTest.java | 158 +-
.../compute/ComputeWorkflowExecutionTest.java | 35 +-
.../CustomerPaynoteLatestBexFixtureTest.java | 13 +-
...namicEmbeddedParticipantsWorkflowTest.java | 19 +-
.../compute/Ed25519IntrinsicWorkflowTest.java | 10 +-
.../LanguageAdoptionMetricsArtifactTest.java | 36 +-
...LanguageAdoptionMetricsArtifactWriter.java | 9 +-
.../MandateDeclaredTypeEventMatchingTest.java | 28 +-
.../MandateProcessingEventBindingTest.java | 10 +-
.../MandateTerminationWorkflowTest.java | 38 +-
...fferPaynoteEmbeddedOrdersWorkflowTest.java | 93 +-
.../PaynoteReducedDefinitionWorkflowTest.java | 68 +-
.../compute/ProcessingEventBindingTest.java | 4 +-
...resentativeWorkflowLifecycleSmokeTest.java | 16 +-
.../TerminateProcessingWorkflowTest.java | 137 +-
...dateDocumentBatchApplyIntegrationTest.java | 21 +-
.../workflow/ComputeEffectPlanTest.java | 182 +-
.../FrozenComputeDifferentialTest.java | 64 +-
.../FrozenUpdateDocumentDifferentialTest.java | 39 +-
.../processor/workflow/NodeUtilTest.java | 59 +
.../SequentialWorkflowPlanCacheTest.java | 26 +
...SequentialWorkflowRunnerLifecycleTest.java | 138 +-
.../workflow/StaticUpdatePlanTest.java | 65 +-
.../workflow/WorkflowPatchEntryTest.java | 16 +-
.../processor/CoordinationRoutingHarness.java | 142 ++
.../dynamic-embedded-participants-bex.yaml | 4 +-
.../offer-paynote-embedded-orders-bex.yaml | 4 +-
.../selective-processing-report.schema.json | 232 +++
...-snapshot.document.compute.latest-bex.yaml | 6 +-
.../customer-paynote-snapshot.event.yaml | 4 +-
105 files changed, 11612 insertions(+), 1657 deletions(-)
create mode 100644 src/jmh/java/blue/coordination/processor/RepositoryTypeAliasPreprocessor.java
create mode 100644 src/main/java/blue/coordination/processor/AllTimelinesExternalSubscriptionFunctions.java
create mode 100644 src/main/java/blue/coordination/processor/CompositeTimelineExternalSubscriptionFunctions.java
create mode 100644 src/main/java/blue/coordination/processor/CoordinationDocumentSplitter.java
delete mode 100644 src/main/java/blue/coordination/processor/CoordinationRepositoryCompatibilityNodeProvider.java
create mode 100644 src/main/java/blue/coordination/processor/OperationRequestRoutingFunctions.java
create mode 100644 src/main/java/blue/coordination/processor/TimelineExternalSubscriptionFunctions.java
create mode 100644 src/main/java/blue/coordination/processor/TimelineMemberSubscriptions.java
delete mode 100644 src/main/java/blue/coordination/processor/workflow/StaticPayloadValidator.java
create mode 100644 src/test/java/blue/coordination/processor/CoordinationDocumentSplitterDeepLocalityTest.java
create mode 100644 src/test/java/blue/coordination/processor/CoordinationDocumentSplitterLocalityTest.java
create mode 100644 src/test/java/blue/coordination/processor/CoordinationDocumentSplitterProcessingMatrixTest.java
create mode 100644 src/test/java/blue/coordination/processor/CoordinationDocumentSplitterTest.java
create mode 100644 src/test/java/blue/coordination/processor/OperationRequestLogicalRoutingTest.java
create mode 100644 src/test/java/blue/coordination/processor/ProcessingResultTestSupport.java
rename src/{main => test}/java/blue/coordination/processor/RepositoryTypeAliasPreprocessor.java (85%)
create mode 100644 src/test/java/blue/coordination/processor/SelectiveProcessingReportArtifactTest.java
create mode 100644 src/test/java/blue/coordination/processor/SelectiveProcessingReportWriter.java
create mode 100644 src/test/java/blue/coordination/processor/SelectiveProcessingReportWriterTest.java
create mode 100644 src/test/java/blue/coordination/processor/TimelineCheckpointSubjectTest.java
create mode 100644 src/test/java/blue/coordination/processor/workflow/NodeUtilTest.java
create mode 100644 src/test/java/blue/language/processor/CoordinationRoutingHarness.java
create mode 100644 src/test/resources/coordination/selective-processing-report.schema.json
diff --git a/README.md b/README.md
index 2b8ad85..f0a8e2b 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ embedded scopes, and checkpoints.
The processor is deterministic. Given the same initialized document and the
same ordered input events, it produces the same canonical output document,
-triggered events, gas accounting, and output BlueId.
+Root event sequence, gas accounting, status, and diagnostic.
## Install
@@ -34,6 +34,72 @@ api "blue.repo:blue-repo-java:3.0.0-rc.10"
api "blue.bex:blue-bex-java:1.1.0-rc.2"
```
+## Contracts 1.0 Development Build
+
+This worktree can compile against the sibling Language runtime:
+
+```bash
+./gradlew test -PuseLocalBlueLanguage=true
+```
+
+The property substitutes every direct and transitive
+`blue.language:blue-language-java` dependency with `../blue-language-java` and
+fails configuration if that sibling checkout is absent.
+
+The selective-processing development baseline is the exact Language commit
+`0a6a40d18578df784f674148d1e8b6a4319bfe49`
+(`feat: support fragmented processing and logical delivery`). It adds verified
+pure-reference Root/Event admission, event-scoped exact-reference
+materialization, lazy executable-body demand, and generic logical-delivery
+routing. Coordination is wired to those generic functions so Timeline source
+eligibility and checkpoints can remain separate from the effective Operation
+Request handler channel.
+
+That local Language commit is not yet a working cross-channel PROCESS baseline.
+During verified Phase-B classification it rebuilds an External Channel bundle
+containing only the accepting source. Consequently the documented
+`ExternalChannelFunctionContext.membersByEffectiveType(...)` call cannot see a
+same-scope peer target that was visible during header evaluation. The focused
+Coordination regression currently has 7 tests: 4 fallback/evidence cases pass
+and the 3 valid peer-routing cases remain red at that Language boundary. The
+tests deliberately retain the required target validation; they do not route an
+unknown target optimistically.
+
+There is a second generic API boundary for the full requested target rule:
+the current function context exposes External Channel peers, while the
+Coordination requirement accepts any effective same-scope Channel. Until
+Language supplies a Phase-B view of the header-declared dependency surface and
+a read-only same-scope Channel lookup, this worktree must not be described as a
+complete Operation Request routing implementation.
+
+The immutable request parser also recognizes a bare Operation Request, but the
+production external functions currently accept Timeline Entries through
+Timeline, Composite Timeline, and All Timelines source channels. A bare request
+therefore has no production source-classification path yet; that case is
+reachable only through custom kernel plumbing.
+
+The migration is still not release-ready. The supplied handoff contains the
+final Language and generic Contracts registries, but no final Coordination
+registry. In particular, `blue-repo-java:3.0.0-rc.10` still has the preview
+`Terminate Processing` shape without the required application `cause`.
+The published dependency remains `blue-language-java:3.1.0-rc.18`; the commit
+above is a local development input, not a released artifact identity.
+
+Two other development limitations remain:
+
+- ordinary Timeline Channels still use a conservative type-wide preselection
+ key, so 1,025 otherwise valid channels can exceed the Contracts
+ 1,024-occurrence preselection bound;
+- the current BEX dependency does not expose a manifest-bound named-counter
+ stream, so Compute fails closed instead of submitting BEX's legacy aggregate
+ `gasUsed` value.
+
+Two historical processor-delay fixtures still contain preview `lastEvents`
+checkpoint records. Their final checkpoint subjects are derivable, but their
+domains are not: the required final Coordination type and source-contribution
+identities were not supplied. The focused fixture test removes those stale
+records before processing.
+
## Register Processors
Most applications should register the Coordination processor set on a
@@ -45,7 +111,9 @@ import blue.language.Blue;
import blue.repo.BlueRepository;
BlueRepository repository = BlueRepository.latest();
-Blue blue = repository.configure(new Blue());
+Blue blue = new Blue()
+ .nodeProvider(repository.nodeProvider())
+ .typeClassResolver(repository.typeClassResolver());
CoordinationProcessors.registerWith(blue);
```
@@ -138,15 +206,45 @@ message:
request: 5
```
-After processing, `/counter` is `5`, the workflow emits a chat message, and the
-channel checkpoint records the delivered timeline entry so duplicates do not
-run twice.
+With a Contracts 1.0-compatible BEX runtime adapter, processing changes
+`/counter` to `5`, emits a chat message, and records the Timeline ordering
+subject so duplicates do not run twice.
The request's required `channel` is its effective same-scope handler channel.
The Timeline Channel that accepts the entry still owns source eligibility and
-checkpointing; routing preserves the full Timeline Entry and does not evaluate
-the target channel as another external source. This V2 repository behavior is
-outside the current Blue Contracts 1.0 conformance surface.
+checkpointing. Coordination computes Language's generic
+`handlerChannelKey` and `logicalDeliveryKey` outputs without evaluating or
+checkpointing the target as a second external source. The development-kernel
+gate described above currently prevents a valid peer target from surviving
+verified Phase-B classification.
+
+## Routed Operation Requests
+
+The intended routed Operation Request model has two channel roles:
+
+- the **source channel** accepts the exact Timeline Entry, authenticates its
+ timeline and actor, determines freshness, preserves the complete original
+ attribution, and owns its checkpoint;
+- `Operation Request.channel` identifies the **effective handler channel** in
+ the same scope. Its operation handlers are discovered, but the target channel
+ is not re-evaluated as another external occurrence.
+
+Once the Language gate is corrected, fresh accepted sources that resolve to the
+same target, operation, and exact payload form one logical delivery. The target
+operation executes once and every participating source retains its own
+checkpoint. A stale source never piggybacks on a fresh one. Failure, application
+termination, or gas exhaustion commits none of the grouped source checkpoints.
+Unknown targets, non-Channel targets, and malformed routing fields preserve
+ordinary source-channel delivery; a known target with an unknown operation
+runs no handler but may still advance the accepted source checkpoint.
+
+Mandate and feeder eligibility stays outside PROCESS. A feeder observes the
+Root and transitively declared embedded external channels, establishes
+Timeline completeness and ordering, and derives eligible source occurrences.
+It may apply Mandate/direct eligibility before constructing revision-bound
+`VerifiedExecutionEvidence`. The processor revalidates that immutable evidence;
+it does not query a Mandate database or replace the source Timeline attribution
+with target-channel data.
## Processing Model
@@ -158,14 +256,112 @@ Input:
Output:
1. one canonical Blue document;
-2. zero or more triggered events;
+2. zero or more ordered Root events;
3. total gas usage;
-4. processing metadata, including the output BlueId.
+4. one completed processing status;
+5. an optional structured diagnostic.
Processors operate on canonical snapshots instead of process-local mutable
state. You can serialize a processed document, load it again, and continue
processing from the same resolved state.
+`VerifiedExecutionEvidence` is revision-bound environment evidence, not a third
+semantic PROCESS argument. The semantic inputs remain exactly Root and Event.
+
+## Selective Fragmentation
+
+`CoordinationDocumentSplitter` prepares ordinary content-addressed Blue
+fragments without executing contracts or deriving a different delivery plan.
+`splitDocument` retains immutable dispatch headers and cuts only:
+
+- exact embedded roots declared by a directly authored
+ `Process Embedded.paths`;
+- executable-body fields declared by the handler's exact registered runtime
+ type, including the `steps` bodies of Sequential Workflow Operation, Chat
+ Workflow Operation, and Sequential Workflow.
+
+Each parent edge becomes a pure reference to the same exact child or body
+BlueId, so the fragmented Root and fully inline Root have the same BlueId.
+Application subtrees are not cut merely because they contain a `contracts`
+property. Handler channel, operation, order, request/event patterns, and body
+BlueId remain available without inspecting the body. `splitEvent` uses
+Language's exact direct-node fragments for ordinary graphs and an
+identity-equivalent shallow form when a Coordination event retains an external
+cyclic-member type reference.
+
+The resulting Root and Timeline Entry can both be passed to PROCESS as pure
+references:
+
+```java
+CoordinationDocumentSplitter splitter = new CoordinationDocumentSplitter();
+CoordinationDocumentSplitter.SplitGraph documentGraph =
+ splitter.splitDocument(exactRoot);
+CoordinationDocumentSplitter.SplitGraph eventGraph =
+ splitter.splitEvent(exactTimelineEntry);
+
+NodeProvider fragments = new SequentialNodeProvider(
+ documentGraph.provider(),
+ eventGraph.provider(),
+ repository.nodeProvider());
+
+CoordinationDocumentSplitter.PreparedProcessingInput input =
+ splitter.prepareForProcessing(
+ documentGraph.rootBlueId(),
+ eventGraph.rootBlueId(),
+ evidence,
+ fragments);
+
+Blue blue = new Blue()
+ .nodeProvider(input.provider())
+ .typeClassResolver(repository.typeClassResolver());
+CoordinationProcessors.registerWith(blue);
+
+DocumentProcessingResult result =
+ blue.getDocumentProcessor().processDocument(
+ input.document(), input.event(), input.evidence());
+```
+
+The example's `evidence` is supplied by the feeder boundary described above.
+The splitter checks that it is bound to the exact Root and Event identities and
+installs no new routing argument. Preparation does not fetch either fragment;
+the returned verifying provider validates BlueId evidence lazily on PROCESS
+demand.
+
+The physical-locality tests prove that a Root-only selection demands no
+embedded child merely because it is declared. For a selected deep leaf, they
+prove that the prepared fragment allow-list can be restricted to the
+Root-to-leaf scope chain and selected/causally allowed bodies. Those tests are
+structural provider-demand proofs; they do not substitute for the still-missing
+deep semantic processing matrix or ultra-complex causal fixture.
+
+The current no-embedding semantic matrix is one JUnit case containing eight
+actual `DocumentProcessor` invocations over inline, referenced, direct-fragment,
+and cold/warm representations. It uses a deterministic static test Handler and
+a fragment-aware adapter; it is not the required Alice-source/Bob-target
+production Operation Request fixture, and it does not cover one-fragment or
+batched providers.
+
+The current splitter discovers authored contract entries and their exact direct
+runtime types. Its public input does not yet provide an effective resolved
+contract view, so a `Process Embedded` declaration or executable handler body
+that exists only through type inheritance is not cut by this implementation.
+That effective-contract case remains an explicit design/API gap.
+
+Fragmented and inline representations must have identical status, resulting
+Root value and BlueId, ordered Root events, gas, conformance trace, and source
+checkpoints. Provider tests distinguish semantic demands from optional
+allow-listed backend prefetch and fail immediately on a forbidden demand.
+Cache warmth, batching, and fragment iteration order are physical concerns and
+must not change semantics. An unavailable selected body remains unavailable
+and retryable; an incomplete provider view must never turn unknown content into
+a semantic no-match.
+
+Only events explicitly emitted at Root appear in `ProcessResult.events`.
+Descendant events may drive local and ancestor reactions, but are not
+automatically published. The deterministic deep-fixture design and required
+evidence are documented in
+[`docs/fragmented-processing-ultra-complex-walkthrough.md`](docs/fragmented-processing-ultra-complex-walkthrough.md).
+
## Supported Contracts
This library provides executable behavior for:
@@ -211,6 +407,12 @@ Common workflow bindings:
`Coordination/Update Document` accepts literal patch lists only.
`Coordination/Trigger Event` accepts literal event payloads only.
+Literal payloads are not interpreted as BEX; `$`-prefixed application keys
+remain exact data.
+Patch operations are exact lowercase `add`, `replace`, or `remove`; `val` is
+required for add/replace and must be absent for remove. Compute termination and
+`Coordination/Terminate Processing` use an application-defined non-empty
+`cause` plus an optional Text `reason`.
## Build And Test
@@ -224,6 +426,40 @@ Run tests:
./gradlew test
```
+Run focused routing characterization, direct-declaration splitter regressions,
+physical-locality tests, the limited no-embedding PROCESS parity matrix, and
+the deterministic partial report against the exact sibling Language checkout:
+
+```bash
+./gradlew selectiveCoordinationProcessingTest \
+ -PuseLocalBlueLanguage=true
+```
+
+These tests do not require Compute/BEX. With Language commit
+`0a6a40d18578df784f674148d1e8b6a4319bfe49`, the task is expected to remain red
+only at the enabled valid-peer routing regressions described above: the latest
+run executes 55 tests, with 52 passing and 3 failing. Do not present the task as
+green. Until the final Coordination registry and compatible BEX counter stream
+are available, failures in named Compute/Mandate runtime suites are reported
+separately from fragmentation results.
+
+The deterministic selective-processing report schema is
+[`src/test/resources/coordination/selective-processing-report.schema.json`](src/test/resources/coordination/selective-processing-report.schema.json).
+Generate the current truthful partial artifact with:
+
+```bash
+./gradlew test -PuseLocalBlueLanguage=true \
+ --tests blue.coordination.processor.SelectiveProcessingReportArtifactTest
+```
+
+It writes `build/reports/coordination-selective-processing/report.json` without
+timestamps or machine-specific paths. The artifact records its own exact test
+count and splitter smoke identities. It also records separately observed
+passing no-embedding/locality evidence, the enabled routing blocker, and the
+still-unexecuted embedded semantic and ultra-complex sections. It does not turn
+the report mechanism into execution evidence; each section names its evidence
+scope and command.
+
Run the focused correctness and bounded-memory suites:
```bash
@@ -288,6 +524,9 @@ Current test areas:
- must-understand failures;
- test timeline provider behavior;
- composite timeline routing;
+- logical source-to-effective-channel Operation Request routing;
+- exact Coordination document/event fragmentation API and invariants;
+- structural fragment-provider locality and reconstruction;
- operation request matching;
- sequential workflow execution;
- compute and BEX execution;
diff --git a/build.gradle b/build.gradle
index ccad22f..3108234 100644
--- a/build.gradle
+++ b/build.gradle
@@ -56,6 +56,14 @@ tasks.withType(JavaCompile).configureEach {
options.release = 8
}
+tasks.named('compileJava', JavaCompile) {
+ options.compilerArgs.addAll([
+ '-Xlint:deprecation',
+ '-Xlint:-options',
+ '-Werror'
+ ])
+}
+
configurations {
binaryCompatibilityBaseline {
canBeConsumed = false
@@ -193,6 +201,24 @@ tasks.register('languageAdoptionMetricsArtifactTest', Test) { focusedTest ->
layout.buildDirectory.file('reports/language-adoption/scenario-metrics.csv'))
}
+tasks.register('selectiveCoordinationProcessingTest', Test) { focusedTest ->
+ description = 'Runs focused routing characterization, direct-declaration splitter regressions, physical-locality tests, a limited no-embedding PROCESS parity matrix, and the partial report.'
+ configureFocusedTest(focusedTest, [
+ 'blue.coordination.processor.OperationRequestLogicalRoutingTest',
+ 'blue.coordination.processor.OperationRequestRoutingEvaluationTest',
+ 'blue.coordination.processor.CoordinationDocumentSplitterTest',
+ 'blue.coordination.processor.CoordinationDocumentSplitterLocalityTest',
+ 'blue.coordination.processor.CoordinationDocumentSplitterDeepLocalityTest',
+ 'blue.coordination.processor.CoordinationDocumentSplitterProcessingMatrixTest',
+ 'blue.coordination.processor.SelectiveProcessingReportWriterTest',
+ 'blue.coordination.processor.SelectiveProcessingReportArtifactTest',
+ 'blue.coordination.processor.TimelineCheckpointSubjectTest'
+ ])
+ outputs.file(
+ layout.buildDirectory.file(
+ 'reports/coordination-selective-processing/report.json'))
+}
+
// Minimal class-file reader used by the verification tasks below. Keeping this
// in the build avoids adding a release plugin solely for two deterministic
// checks, and compares JVM descriptors rather than source formatting.
diff --git a/docs/coordination-v2-layered-delivery-plan.md b/docs/coordination-v2-layered-delivery-plan.md
index cdbc4c6..5c164d1 100644
--- a/docs/coordination-v2-layered-delivery-plan.md
+++ b/docs/coordination-v2-layered-delivery-plan.md
@@ -6,6 +6,29 @@ Audience: maintainers of `blue-repository`, `blue-repository-java`,
`blue-language-java`, `blue-coordination-java`, and the Blue Contracts and
Coordination specifications
+> **Historical routing note (2026-07-27).** This plan predates the final generic
+> routing API delivered by `blue-language-java` commit
+> `0a6a40d18578df784f674148d1e8b6a4319bfe49`. Task 2 and the routing mechanics
+> in Task 4 remain useful semantic background, but references to extending
+> `ChannelDelivery`, copying fields through `ChannelEvaluation`, or returning
+> routed delivery objects are superseded. The implemented API uses
+> `ExternalChannelSubscriptionFunctions.handlerChannelKey(...)` and
+> `logicalDeliveryKey(...)`, with immutable member lookup through
+> `ExternalChannelFunctionContext`. New work must target those function outputs,
+> not restore the historical delivery-object design. Artifact coordinates and
+> remaining registry/BEX gates elsewhere in this document are also historical
+> plan inputs, not a statement that the final Coordination registry is present.
+>
+> **Open kernel defect.** At commit `0a6a40d18578`, verified Phase-B
+> classification filters the runtime bundle to the accepting source before
+> re-evaluating these event functions. A declared
+> `membersByEffectiveType(...)` dependency can therefore see peers during
+> header evaluation but not during delivery classification. The enabled
+> Coordination cross-channel regressions remain red until Language carries the
+> header-declared dependency surface into Phase B. The current context also
+> enumerates External Channels only, while the final target rule requires a
+> read-only lookup for any same-scope Channel.
+
## Objective
Coordination V2 is a layered contract and runtime change. It is not one feature
@@ -457,6 +480,11 @@ Reviewers should verify:
# Task 2: Add Routed External-Channel Delivery to Core
+> **Superseded API shape.** Language commit `0a6a40d18578` implements this
+> task's source/checkpoint-versus-handler semantics through external-channel
+> subscription function outputs. The `ChannelDelivery` and `ChannelEvaluation`
+> changes below describe the earlier proposal only.
+
## Goal
Allow a trusted external Channel processor to accept a Processing Event through
@@ -787,6 +815,17 @@ Reviewers should verify:
# Task 4: Route Operation Requests to Effective Channels
+> **Current implementation direction.** Coordination supplies the target
+> channel and source-independent logical route through the generic function
+> outputs delivered in Language `0a6a40d18578`. Timeline, Composite Timeline,
+> and All Timelines sources retain their own acceptance and checkpoint
+> identities; they do not construct routed `ChannelDelivery` instances. This
+> wiring is implemented, but the open Phase-B dependency-surface defect
+> described at the top of this document currently blocks a valid peer target
+> in an actual verified PROCESS run. The parser recognizes a bare Operation
+> Request, but the installed production external functions preselect Timeline
+> Entries only, so a bare request currently has no production source path.
+
## Goal
Use Task 2's generic routed-delivery primitive so a V2 Operation Request can be
diff --git a/settings.gradle b/settings.gradle
index 58270ff..1268687 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -4,6 +4,22 @@ plugins {
rootProject.name = 'blue-coordination-java'
+def localBlueLanguage = file('../blue-language-java')
+def useLocalBlueLanguage = providers.gradleProperty('useLocalBlueLanguage')
+ .map { it.toBoolean() }
+ .getOrElse(false)
+if (useLocalBlueLanguage) {
+ if (!localBlueLanguage.isDirectory()) {
+ throw new GradleException(
+ "Local blue-language-java build is missing at ${localBlueLanguage}")
+ }
+ includeBuild(localBlueLanguage) {
+ dependencySubstitution {
+ substitute module('blue.language:blue-language-java') using project(':')
+ }
+ }
+}
+
def localBlueRepository = file('../blue-repository-java')
if (providers.gradleProperty('useLocalBlueRepository').map { it.toBoolean() }.getOrElse(false)
&& localBlueRepository.isDirectory()) {
diff --git a/src/jmh/java/blue/coordination/processor/ComputeEffectPlanBenchmark.java b/src/jmh/java/blue/coordination/processor/ComputeEffectPlanBenchmark.java
index c81566d..e22e060 100644
--- a/src/jmh/java/blue/coordination/processor/ComputeEffectPlanBenchmark.java
+++ b/src/jmh/java/blue/coordination/processor/ComputeEffectPlanBenchmark.java
@@ -44,7 +44,9 @@ public class ComputeEffectPlanBenchmark {
@Setup(Level.Trial)
public void setUp() {
repository = BlueRepository.latest();
- blue = repository.configure(new Blue());
+ blue = new Blue()
+ .nodeProvider(repository.nodeProvider())
+ .typeClassResolver(repository.typeClassResolver());
CoordinationProcessors.registerWith(blue, CoordinationProcessorOptions.builder().build());
Node source = sourceDocument(effects).blue(repository.typeAliasBlue());
@@ -52,7 +54,7 @@ public void setUp() {
ResolvedSnapshot selected = blue.resolveToSnapshot(blue.preprocess(aliasesResolved));
DocumentProcessingResult initialized = blue.initializeDocument(selected);
requireSuccess(initialized);
- initializedSnapshot = initialized.snapshot();
+ initializedSnapshot = blue.resolveToSnapshot(initialized.document());
event = operationEvent();
}
@@ -73,16 +75,16 @@ public void verify() {
throw new IllegalStateException("Compute changeset was not applied");
}
Object cause = valueAt(lastResult.document(), "/contracts/terminated/cause");
- if (termination != "graceful".equals(cause)) {
+ if (termination != "benchmark-complete".equals(cause)) {
throw new IllegalStateException("Unexpected termination result: " + cause);
}
int expectedTriggeredEvents = (events ? 1 : 0) + (termination ? 1 : 0);
- if (lastResult.triggeredEvents().size() != expectedTriggeredEvents) {
+ if (lastResult.events().size() != expectedTriggeredEvents) {
throw new IllegalStateException("Unexpected triggered event count: "
- + lastResult.triggeredEvents().size());
+ + lastResult.events().size());
}
int benchmarkEvents = 0;
- for (Node emitted : lastResult.triggeredEvents()) {
+ for (Node emitted : lastResult.events()) {
Node type = emitted.getType();
boolean expectedType = type != null
&& (Event.qualifiedName().equals(type.getValue())
@@ -117,6 +119,7 @@ private static Node sourceDocument(String effects) {
}
if (termination) {
result.properties("termination", new Node()
+ .properties("cause", new Node().value("benchmark-complete"))
.properties("reason", new Node().value("benchmark-complete")));
}
statements.add(new Node().properties("$return", result));
@@ -172,7 +175,9 @@ private Node operationEvent() {
private static void requireSuccess(DocumentProcessingResult result) {
if (result == null || result.status() != ProcessorStatus.SUCCESS) {
- throw new IllegalStateException(result != null ? result.failureReason() : "missing result");
+ throw new IllegalStateException(result != null && result.diagnostic() != null
+ ? result.diagnostic().message()
+ : "missing result");
}
}
}
diff --git a/src/jmh/java/blue/coordination/processor/RepositoryTypeAliasPreprocessor.java b/src/jmh/java/blue/coordination/processor/RepositoryTypeAliasPreprocessor.java
new file mode 100644
index 0000000..9dc77bf
--- /dev/null
+++ b/src/jmh/java/blue/coordination/processor/RepositoryTypeAliasPreprocessor.java
@@ -0,0 +1,84 @@
+package blue.coordination.processor;
+
+import blue.language.model.Node;
+import blue.repo.BlueRepository;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Benchmark-fixture migration helper for preview repository aliases.
+ *
+ * This source set is not included in the published runtime artifact.
+ */
+final class RepositoryTypeAliasPreprocessor {
+ private final Map aliases;
+
+ RepositoryTypeAliasPreprocessor(BlueRepository repository) {
+ this.aliases = repository != null
+ ? new LinkedHashMap(
+ repository.typeAliases())
+ : new LinkedHashMap();
+ }
+
+ Node preprocess(Node node) {
+ if (node == null) {
+ return null;
+ }
+ Node copy = node.clone();
+ resolve(copy);
+ return copy;
+ }
+
+ private void resolve(Node node) {
+ if (node == null) {
+ return;
+ }
+ String blueId = aliasFor(node.getBlueId());
+ if (blueId != null) {
+ node.blueId(blueId);
+ }
+
+ node.type(resolveTypeNode(node.getType()));
+ node.itemType(resolveTypeNode(node.getItemType()));
+ node.keyType(resolveTypeNode(node.getKeyType()));
+ node.valueType(resolveTypeNode(node.getValueType()));
+
+ if (node.getItems() != null) {
+ for (Node item : node.getItems()) {
+ resolve(item);
+ }
+ }
+ if (node.getProperties() != null) {
+ for (Node value : node.getProperties().values()) {
+ resolve(value);
+ }
+ }
+ resolve(node.getContracts());
+ resolve(node.getBlue());
+ }
+
+ private Node resolveTypeNode(Node typeNode) {
+ if (typeNode == null) {
+ return null;
+ }
+ String blueId = aliasFor(inlineText(typeNode));
+ if (blueId != null) {
+ return new Node().blueId(blueId);
+ }
+ resolve(typeNode);
+ return typeNode;
+ }
+
+ private String inlineText(Node node) {
+ if (node == null || !node.isInlineValue()
+ || node.getValue() == null) {
+ return null;
+ }
+ return String.valueOf(node.getValue());
+ }
+
+ private String aliasFor(String value) {
+ return value != null ? aliases.get(value) : null;
+ }
+}
diff --git a/src/jmh/java/blue/coordination/processor/ResolvedProcessingHostStoryBenchmark.java b/src/jmh/java/blue/coordination/processor/ResolvedProcessingHostStoryBenchmark.java
index 4ddbfa3..ba55707 100644
--- a/src/jmh/java/blue/coordination/processor/ResolvedProcessingHostStoryBenchmark.java
+++ b/src/jmh/java/blue/coordination/processor/ResolvedProcessingHostStoryBenchmark.java
@@ -49,7 +49,9 @@ public class ResolvedProcessingHostStoryBenchmark {
@Setup(Level.Trial)
public void setUp() {
BlueRepository repository = BlueRepository.latest();
- blue = repository.configure(new Blue());
+ blue = new Blue()
+ .nodeProvider(repository.nodeProvider())
+ .typeClassResolver(repository.typeClassResolver());
CoordinationProcessors.registerWith(blue, CoordinationProcessorOptions.builder().build());
sourceDocument = preprocess(repository, document());
@@ -121,7 +123,7 @@ private void trace(String phase, long started, Node node) {
private void assertExpectedResult(DocumentProcessingResult result) {
requireSuccess(result, "verification");
- Node resolved = result.resolvedDocument();
+ Node resolved = blue.resolve(result.document());
for (int workflow = 1; workflow <= WORKFLOWS; workflow++) {
Integer actual = resolved.getAsInteger("/workflow" + workflow + "Counter");
if (!Integer.valueOf(EXPECTED_COUNTER).equals(actual)) {
@@ -131,7 +133,7 @@ private void assertExpectedResult(DocumentProcessingResult result) {
}
private static Node storedEpoch(DocumentProcessingResult result, String phase) {
- Node canonical = result.canonicalDocument();
+ Node canonical = result.document();
if (canonical == null) {
throw new IllegalStateException(phase + " did not produce a canonical epoch");
}
@@ -141,7 +143,9 @@ private static Node storedEpoch(DocumentProcessingResult result, String phase) {
private static void requireSuccess(DocumentProcessingResult result, String phase) {
if (result == null || result.status() != ProcessorStatus.SUCCESS) {
throw new IllegalStateException(phase + " failed: "
- + (result != null ? result.failureReason() : "missing result"));
+ + (result != null && result.diagnostic() != null
+ ? result.diagnostic().message()
+ : "missing result"));
}
}
diff --git a/src/main/java/blue/coordination/processor/AllTimelinesChannelProcessor.java b/src/main/java/blue/coordination/processor/AllTimelinesChannelProcessor.java
index b280425..7906d1b 100644
--- a/src/main/java/blue/coordination/processor/AllTimelinesChannelProcessor.java
+++ b/src/main/java/blue/coordination/processor/AllTimelinesChannelProcessor.java
@@ -5,6 +5,7 @@
import blue.language.processor.ChannelEvaluation;
import blue.language.processor.ChannelEvaluationContext;
import blue.language.processor.ChannelProcessor;
+import blue.language.processor.ExternalChannelSubscriptionFunctions;
import blue.language.processor.model.ChannelContract;
import blue.repo.coordination.AllTimelinesChannel;
import blue.repo.coordination.TimelineChannel;
@@ -16,6 +17,12 @@ public Class contractType() {
return AllTimelinesChannel.class;
}
+ @Override
+ public ExternalChannelSubscriptionFunctions
+ externalSubscriptionFunctions() {
+ return AllTimelinesExternalSubscriptionFunctions.INSTANCE;
+ }
+
@Override
public ChannelEvaluation evaluate(AllTimelinesChannel contract, ChannelEvaluationContext context) {
Node event = context.event();
@@ -26,10 +33,8 @@ public ChannelEvaluation evaluate(AllTimelinesChannel contract, ChannelEvaluatio
if (matching == null) {
return ChannelEvaluation.noMatch();
}
- return TimelineProviderSupport.preserveUnionDelivery(matching.evaluation,
- event,
- "allTimelinesSourceChannelKey",
- matching.channelKey);
+ return TimelineProviderSupport.preserveUnionPayload(
+ matching.evaluation, event);
}
private MatchingTimeline matchingTimeline(ChannelEvaluationContext context) {
@@ -66,8 +71,12 @@ private ChannelEvaluation evaluateChild(ChannelProcessor processor,
}
@Override
- public boolean isNewerEvent(AllTimelinesChannel contract, ChannelCheckpointContext context) {
- return TimelineProviderSupport.isNewerOrDifferentTimelineEvent(context);
+ public boolean isNewerEvent(AllTimelinesChannel contract,
+ ChannelCheckpointContext context) {
+ return TimelineProviderSupport.isNewerTimelineSubject(
+ context,
+ AllTimelinesExternalSubscriptionFunctions
+ .ORDER_SUBJECT_VERSION);
}
private int order(ChannelContract contract) {
diff --git a/src/main/java/blue/coordination/processor/AllTimelinesExternalSubscriptionFunctions.java b/src/main/java/blue/coordination/processor/AllTimelinesExternalSubscriptionFunctions.java
new file mode 100644
index 0000000..59762c8
--- /dev/null
+++ b/src/main/java/blue/coordination/processor/AllTimelinesExternalSubscriptionFunctions.java
@@ -0,0 +1,149 @@
+package blue.coordination.processor;
+
+import blue.language.model.Node;
+import blue.language.processor.ExternalChannelFunctionContext;
+import blue.language.processor.ExternalChannelMemberSnapshot;
+import blue.language.processor.ExternalChannelSubscriptionFunctions;
+import blue.repo.coordination.AllTimelinesChannel;
+
+import java.util.Collections;
+import java.util.List;
+
+final class AllTimelinesExternalSubscriptionFunctions
+ implements ExternalChannelSubscriptionFunctions<
+ AllTimelinesChannel> {
+
+ static final AllTimelinesExternalSubscriptionFunctions INSTANCE =
+ new AllTimelinesExternalSubscriptionFunctions();
+ static final String ALL_TIMELINES_KEY =
+ "blue.coordination/1.0/all-timelines";
+ static final String ORDER_SUBJECT_VERSION =
+ "blue.coordination/1.0/all-timelines-order-subject";
+
+ private AllTimelinesExternalSubscriptionFunctions() {
+ }
+
+ @Override
+ public List channelKeys(
+ AllTimelinesChannel immutableContractSnapshot,
+ ExternalChannelFunctionContext context) {
+ OperationRequestRoutingFunctions
+ .declareTargetChannelFamilies(
+ immutableContractSnapshot,
+ context);
+ /*
+ * Enumerating the exact Timeline type family records the membership
+ * dependency, including an empty family. Event evaluation can select
+ * any one of those members, so promote every Timeline member header
+ * now as well: the header dependency proof must cover the selected
+ * member's exact checkpoint domain. This remains local to the exact
+ * Timeline runtime family and never resolves unrelated channel types.
+ */
+ for (ExternalChannelMemberSnapshot member : members(context)) {
+ member.checkpointDomainBlueId();
+ }
+ return Collections.singletonList(ALL_TIMELINES_KEY);
+ }
+
+ @Override
+ public List eventKeys(
+ Node exactEvent,
+ ExternalChannelFunctionContext context) {
+ return !TimelineMemberSubscriptions.timelineEventKeys(
+ exactEvent, context).isEmpty()
+ ? Collections.singletonList(ALL_TIMELINES_KEY)
+ : Collections.emptyList();
+ }
+
+ @Override
+ public boolean accepts(
+ AllTimelinesChannel immutableContractSnapshot,
+ Node exactEvent,
+ ExternalChannelFunctionContext context) {
+ return winning(exactEvent, context) != null;
+ }
+
+ @Override
+ public Node payload(
+ AllTimelinesChannel immutableContractSnapshot,
+ Node exactEvent,
+ ExternalChannelFunctionContext context) {
+ return OperationRequestRoutingFunctions
+ .payload(exactEvent, context);
+ }
+
+ @Override
+ public Node checkpointSubject(
+ AllTimelinesChannel immutableContractSnapshot,
+ Node exactEvent,
+ Node exactPayload,
+ ExternalChannelFunctionContext context) {
+ TimelineMemberSubscriptions.WinningMember winner =
+ requireWinner(exactEvent, context);
+ return TimelineProviderSupport.memberTimelineOrderSubject(
+ ORDER_SUBJECT_VERSION,
+ winner.member(),
+ winner.evaluation().checkpointSubject());
+ }
+
+ @Override
+ public String handlerChannelKey(
+ AllTimelinesChannel immutableContractSnapshot,
+ Node exactEvent,
+ Node exactPayload,
+ ExternalChannelFunctionContext context) {
+ return OperationRequestRoutingFunctions
+ .handlerChannelKey(
+ immutableContractSnapshot,
+ exactEvent,
+ context);
+ }
+
+ @Override
+ public String logicalDeliveryKey(
+ AllTimelinesChannel immutableContractSnapshot,
+ Node exactEvent,
+ Node exactPayload,
+ ExternalChannelFunctionContext context) {
+ return OperationRequestRoutingFunctions
+ .logicalDeliveryKey(
+ immutableContractSnapshot,
+ exactEvent,
+ context);
+ }
+
+ @Override
+ public String checkpointDomainDiscriminator(
+ AllTimelinesChannel immutableContractSnapshot,
+ ExternalChannelFunctionContext context) {
+ return "coordination.all-timelines:"
+ + "timeline-type-family-v1"
+ + "|subject="
+ + ORDER_SUBJECT_VERSION;
+ }
+
+ private TimelineMemberSubscriptions.WinningMember winning(
+ Node exactEvent,
+ ExternalChannelFunctionContext context) {
+ return TimelineMemberSubscriptions.winning(
+ members(context), exactEvent);
+ }
+
+ private TimelineMemberSubscriptions.WinningMember requireWinner(
+ Node exactEvent,
+ ExternalChannelFunctionContext context) {
+ TimelineMemberSubscriptions.WinningMember winner =
+ winning(exactEvent, context);
+ if (winner == null) {
+ throw new IllegalStateException(
+ "All Timelines payload requires an accepting member");
+ }
+ return winner;
+ }
+
+ private List members(
+ ExternalChannelFunctionContext context) {
+ return TimelineMemberSubscriptions.allTimelineMembers(
+ context);
+ }
+}
diff --git a/src/main/java/blue/coordination/processor/BlueSemanticIdentity.java b/src/main/java/blue/coordination/processor/BlueSemanticIdentity.java
index 7584a8f..410abd5 100644
--- a/src/main/java/blue/coordination/processor/BlueSemanticIdentity.java
+++ b/src/main/java/blue/coordination/processor/BlueSemanticIdentity.java
@@ -24,17 +24,6 @@ static boolean equals(Node left, Node right) {
return left != null && right != null && identity(left).equals(identity(right));
}
- static boolean matchesType(Node node, Node expectedType) {
- if (node == null || expectedType == null) {
- return false;
- }
- if (node.isReferenceOnly()) {
- identity(node);
- return true;
- }
- return CONTEXT.get().blue.nodeMatchesType(node, expectedType);
- }
-
private static String identity(Node node) {
if (node.isReferenceOnly()) {
return BlueIds.requireBlueIdOrCyclicMember(node.getBlueId(), "Semantic identity reference");
@@ -67,7 +56,10 @@ private static void cacheIdentity(String representationIdentity, String valueIde
}
private static final class IdentityContext {
- private final Blue blue = BlueRepository.latest().configure(new Blue());
+ private final BlueRepository repository = BlueRepository.latest();
+ private final Blue blue = new Blue()
+ .nodeProvider(repository.nodeProvider())
+ .typeClassResolver(repository.typeClassResolver());
private final Map valueIdentities =
new LinkedHashMap(IDENTITY_CACHE_SIZE, 0.75f, true) {
@Override
diff --git a/src/main/java/blue/coordination/processor/ChatWorkflowOperationProcessor.java b/src/main/java/blue/coordination/processor/ChatWorkflowOperationProcessor.java
index a302de2..6d7893b 100644
--- a/src/main/java/blue/coordination/processor/ChatWorkflowOperationProcessor.java
+++ b/src/main/java/blue/coordination/processor/ChatWorkflowOperationProcessor.java
@@ -7,6 +7,8 @@
import blue.language.processor.ProcessorExecutionContext;
import blue.repo.coordination.ChatWorkflowOperation;
import blue.repo.coordination.SequentialWorkflow;
+import java.util.Collections;
+import java.util.List;
public final class ChatWorkflowOperationProcessor implements HandlerProcessor {
private final SequentialWorkflowRunner runner;
@@ -28,6 +30,11 @@ public Class contractType() {
return ChatWorkflowOperation.class;
}
+ @Override
+ public List executableBodyFields() {
+ return Collections.singletonList("steps");
+ }
+
@Override
public String deriveChannel(ChatWorkflowOperation contract, HandlerRegistrationContext context) {
String channel = trimToNull(contract.getChannel());
diff --git a/src/main/java/blue/coordination/processor/CompositeTimelineChannelProcessor.java b/src/main/java/blue/coordination/processor/CompositeTimelineChannelProcessor.java
index 4a6093d..4281b50 100644
--- a/src/main/java/blue/coordination/processor/CompositeTimelineChannelProcessor.java
+++ b/src/main/java/blue/coordination/processor/CompositeTimelineChannelProcessor.java
@@ -5,6 +5,7 @@
import blue.language.processor.ChannelEvaluation;
import blue.language.processor.ChannelEvaluationContext;
import blue.language.processor.ChannelProcessor;
+import blue.language.processor.ExternalChannelSubscriptionFunctions;
import blue.language.processor.model.ChannelContract;
import blue.repo.coordination.CompositeTimelineChannel;
import blue.repo.coordination.TimelineChannel;
@@ -18,6 +19,12 @@ public Class contractType() {
return CompositeTimelineChannel.class;
}
+ @Override
+ public ExternalChannelSubscriptionFunctions<
+ CompositeTimelineChannel> externalSubscriptionFunctions() {
+ return CompositeTimelineExternalSubscriptionFunctions.INSTANCE;
+ }
+
@Override
public ChannelEvaluation evaluate(CompositeTimelineChannel contract, ChannelEvaluationContext context) {
List channels = contract.getChannels();
@@ -63,10 +70,8 @@ public ChannelEvaluation evaluate(CompositeTimelineChannel contract, ChannelEval
if (matching == null) {
return ChannelEvaluation.noMatch();
}
- return TimelineProviderSupport.preserveUnionDelivery(matching.evaluation,
- context.event(),
- "compositeSourceChannelKey",
- matching.channelKey);
+ return TimelineProviderSupport.preserveUnionPayload(
+ matching.evaluation, context.event());
}
@SuppressWarnings({"rawtypes", "unchecked"})
@@ -77,8 +82,12 @@ private ChannelEvaluation evaluateChild(ChannelProcessor processor,
}
@Override
- public boolean isNewerEvent(CompositeTimelineChannel contract, ChannelCheckpointContext context) {
- return TimelineProviderSupport.isNewerOrDifferentTimelineEvent(context);
+ public boolean isNewerEvent(CompositeTimelineChannel contract,
+ ChannelCheckpointContext context) {
+ return TimelineProviderSupport.isNewerTimelineSubject(
+ context,
+ CompositeTimelineExternalSubscriptionFunctions
+ .ORDER_SUBJECT_VERSION);
}
private String trimToNull(String value) {
diff --git a/src/main/java/blue/coordination/processor/CompositeTimelineExternalSubscriptionFunctions.java b/src/main/java/blue/coordination/processor/CompositeTimelineExternalSubscriptionFunctions.java
new file mode 100644
index 0000000..142c835
--- /dev/null
+++ b/src/main/java/blue/coordination/processor/CompositeTimelineExternalSubscriptionFunctions.java
@@ -0,0 +1,140 @@
+package blue.coordination.processor;
+
+import blue.language.model.Node;
+import blue.language.processor.ExternalChannelFunctionContext;
+import blue.language.processor.ExternalChannelMemberSnapshot;
+import blue.language.processor.ExternalChannelSubscriptionFunctions;
+import blue.repo.coordination.CompositeTimelineChannel;
+
+import java.util.List;
+
+final class CompositeTimelineExternalSubscriptionFunctions
+ implements ExternalChannelSubscriptionFunctions<
+ CompositeTimelineChannel> {
+
+ static final CompositeTimelineExternalSubscriptionFunctions INSTANCE =
+ new CompositeTimelineExternalSubscriptionFunctions();
+ static final String ORDER_SUBJECT_VERSION =
+ "blue.coordination/1.0/composite-timeline-order-subject";
+
+ private CompositeTimelineExternalSubscriptionFunctions() {
+ }
+
+ @Override
+ public List channelKeys(
+ CompositeTimelineChannel immutableContractSnapshot,
+ ExternalChannelFunctionContext context) {
+ OperationRequestRoutingFunctions
+ .declareTargetChannelFamilies(
+ immutableContractSnapshot,
+ context);
+ return TimelineMemberSubscriptions.unionChannelKeys(
+ members(immutableContractSnapshot, context));
+ }
+
+ @Override
+ public List eventKeys(
+ Node exactEvent,
+ ExternalChannelFunctionContext context) {
+ return TimelineMemberSubscriptions.timelineEventKeys(
+ exactEvent, context);
+ }
+
+ @Override
+ public boolean accepts(
+ CompositeTimelineChannel immutableContractSnapshot,
+ Node exactEvent,
+ ExternalChannelFunctionContext context) {
+ return winning(immutableContractSnapshot,
+ exactEvent, context) != null;
+ }
+
+ @Override
+ public Node payload(
+ CompositeTimelineChannel immutableContractSnapshot,
+ Node exactEvent,
+ ExternalChannelFunctionContext context) {
+ return OperationRequestRoutingFunctions
+ .payload(exactEvent, context);
+ }
+
+ @Override
+ public Node checkpointSubject(
+ CompositeTimelineChannel immutableContractSnapshot,
+ Node exactEvent,
+ Node exactPayload,
+ ExternalChannelFunctionContext context) {
+ TimelineMemberSubscriptions.WinningMember winner =
+ requireWinner(immutableContractSnapshot,
+ exactEvent, context);
+ return TimelineProviderSupport.memberTimelineOrderSubject(
+ ORDER_SUBJECT_VERSION,
+ winner.member(),
+ winner.evaluation().checkpointSubject());
+ }
+
+ @Override
+ public String handlerChannelKey(
+ CompositeTimelineChannel immutableContractSnapshot,
+ Node exactEvent,
+ Node exactPayload,
+ ExternalChannelFunctionContext context) {
+ return OperationRequestRoutingFunctions
+ .handlerChannelKey(
+ immutableContractSnapshot,
+ exactEvent,
+ context);
+ }
+
+ @Override
+ public String logicalDeliveryKey(
+ CompositeTimelineChannel immutableContractSnapshot,
+ Node exactEvent,
+ Node exactPayload,
+ ExternalChannelFunctionContext context) {
+ return OperationRequestRoutingFunctions
+ .logicalDeliveryKey(
+ immutableContractSnapshot,
+ exactEvent,
+ context);
+ }
+
+ @Override
+ public String checkpointDomainDiscriminator(
+ CompositeTimelineChannel immutableContractSnapshot,
+ ExternalChannelFunctionContext context) {
+ return "coordination.composite-timeline:"
+ + "direct-timeline-members-v1"
+ + "|subject="
+ + ORDER_SUBJECT_VERSION;
+ }
+
+ private TimelineMemberSubscriptions.WinningMember winning(
+ CompositeTimelineChannel contract,
+ Node exactEvent,
+ ExternalChannelFunctionContext context) {
+ return TimelineMemberSubscriptions.winning(
+ members(contract, context), exactEvent);
+ }
+
+ private TimelineMemberSubscriptions.WinningMember requireWinner(
+ CompositeTimelineChannel contract,
+ Node exactEvent,
+ ExternalChannelFunctionContext context) {
+ TimelineMemberSubscriptions.WinningMember winner =
+ winning(contract, exactEvent, context);
+ if (winner == null) {
+ throw new IllegalStateException(
+ "Composite Timeline payload requires an accepting "
+ + "member");
+ }
+ return winner;
+ }
+
+ private List members(
+ CompositeTimelineChannel contract,
+ ExternalChannelFunctionContext context) {
+ return TimelineMemberSubscriptions.compositeMembers(
+ contract, context);
+ }
+}
diff --git a/src/main/java/blue/coordination/processor/CoordinationDocumentSplitter.java b/src/main/java/blue/coordination/processor/CoordinationDocumentSplitter.java
new file mode 100644
index 0000000..cd80518
--- /dev/null
+++ b/src/main/java/blue/coordination/processor/CoordinationDocumentSplitter.java
@@ -0,0 +1,1470 @@
+package blue.coordination.processor;
+
+import blue.coordination.processor.workflow.SequentialWorkflowRunner;
+import blue.language.NodeProvider;
+import blue.language.model.Node;
+import blue.language.model.Schema;
+import blue.language.processor.ContractProcessor;
+import blue.language.processor.ContractProcessorRegistry;
+import blue.language.processor.ContractProcessorRegistryBuilder;
+import blue.language.processor.HandlerProcessor;
+import blue.language.processor.VerifiedExecutionEvidence;
+import blue.language.processor.model.Contract;
+import blue.language.processor.registry.RuntimeBlueIds;
+import blue.language.processor.util.PointerUtils;
+import blue.language.provider.ExactNodeGraphFragments;
+import blue.language.provider.VerifyingNodeProvider;
+import blue.language.utils.BlueIdCalculator;
+import blue.language.utils.BlueIds;
+import blue.language.utils.JsonPointer;
+import blue.language.utils.NodePathEditor;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Deque;
+import java.util.IdentityHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+/**
+ * Coordination-specific physical fragmentation for the two semantic PROCESS
+ * inputs.
+ *
+ * The splitter does not derive a delivery plan or execute a contract. It
+ * keeps Coordination dispatch headers inline and replaces only declared
+ * embedded roots and exact registered Handler executable-body fields with
+ * ordinary BlueId references. Every replacement is checked to preserve the
+ * containing Root's exact BlueId.
+ *
+ * Event splitting uses Language's direct-node graph fragments. Document
+ * splitting deliberately uses coarser fragments: an embedded scope remains a
+ * header-bearing exact fragment and an executable body remains complete. This
+ * lets generic selected-body admission hand the complete body to the
+ * registered Handler after its matcher succeeds, without fragmenting unrelated
+ * application subtrees.
+ */
+public final class CoordinationDocumentSplitter {
+
+ private final Map> executableBodyFieldsByType;
+
+ /**
+ * Creates a splitter with the executable-body declarations used by the
+ * standard Coordination processors.
+ */
+ public CoordinationDocumentSplitter() {
+ this(defaultExecutableBodyFields());
+ }
+
+ /**
+ * Creates a splitter from the exact executable-body declarations captured
+ * by an application processor registry.
+ *
+ * @param registry registry used by the corresponding processor
+ */
+ public CoordinationDocumentSplitter(ContractProcessorRegistry registry) {
+ this(snapshotExecutableBodyFields(
+ Objects.requireNonNull(registry, "registry")));
+ }
+
+ private CoordinationDocumentSplitter(
+ Map> executableBodyFieldsByType) {
+ this.executableBodyFieldsByType =
+ immutableBodyFieldSnapshot(executableBodyFieldsByType);
+ }
+
+ /**
+ * Splits one exact Coordination Root according to Process Embedded and
+ * registered executable-body declarations.
+ */
+ public SplitGraph splitDocument(Node admittedRoot) {
+ Node exactRoot = requireExactContent(
+ admittedRoot, "admittedRoot");
+ String rootBlueId =
+ BlueIdCalculator.calculateBlueId(
+ exactRoot);
+
+ DocumentPlan plan = discoverDocumentPlan(exactRoot);
+ SortedMap fragments = new TreeMap<>();
+ List metadata = new ArrayList<>();
+
+ for (ScopePlan scope : plan.scopes.values()) {
+ Node scopeFragment = fragmentScope(scope);
+ String scopeBlueId =
+ BlueIdCalculator.calculateBlueId(scope.exactScope);
+ requireIdentity(
+ scopeBlueId,
+ scopeFragment,
+ "Coordination scope " + scope.scopePath);
+ fragments.put(scopeBlueId, scopeFragment);
+ metadata.add(new FragmentMetadata(
+ scopeBlueId,
+ "/".equals(scope.scopePath)
+ ? FragmentKind.DOCUMENT_ROOT
+ : FragmentKind.EMBEDDED_ROOT,
+ scope.scopePath,
+ scope.scopePath,
+ null,
+ null));
+ }
+
+ /*
+ * Bodies intentionally override a shallower identity-equivalent
+ * fragment. Contract conversion needs the selected complete body after
+ * admission, while its direct step children are not dispatch headers.
+ */
+ for (BodyCut body : plan.bodies) {
+ if (body.exactBody == null
+ || body.exactBody.isReferenceOnly()) {
+ continue;
+ }
+ String bodyBlueId =
+ BlueIdCalculator.calculateBlueId(body.exactBody);
+ fragments.put(bodyBlueId, body.exactBody.clone());
+ metadata.add(new FragmentMetadata(
+ bodyBlueId,
+ FragmentKind.EXECUTABLE_BODY,
+ body.scopePath,
+ body.absolutePointer,
+ body.handlerTypeBlueId,
+ body.field));
+ }
+
+ Node fragmentedRoot = fragments.get(rootBlueId);
+ if (fragmentedRoot == null) {
+ throw new IllegalStateException(
+ "Coordination Root fragment was not retained");
+ }
+ requireIdentity(
+ rootBlueId,
+ fragmentedRoot,
+ "Coordination Root");
+ return new SplitGraph(
+ rootBlueId,
+ exactRoot,
+ fragmentedRoot,
+ fragments,
+ metadata);
+ }
+
+ /**
+ * Splits one exact Event into Language direct-node fragments.
+ */
+ public SplitGraph splitEvent(Node admittedEvent) {
+ Node exactEvent = requireExactContent(
+ admittedEvent, "admittedEvent");
+ if (containsCyclicMemberReference(
+ exactEvent)) {
+ return splitCyclicAwareEvent(
+ exactEvent);
+ }
+ ExactNodeGraphFragments exactGraph =
+ new ExactNodeGraphFragments(exactEvent);
+ ExactNodeGraphFragments.RootRepresentation root =
+ exactGraph.roots().get(0);
+ List metadata = new ArrayList<>();
+ for (String blueId : exactGraph.blueIds()) {
+ boolean eventRoot = root.blueId().equals(blueId);
+ metadata.add(new FragmentMetadata(
+ blueId,
+ eventRoot
+ ? FragmentKind.EVENT_ROOT
+ : FragmentKind.EVENT_FRAGMENT,
+ "/",
+ eventRoot ? "/" : null,
+ null,
+ null));
+ }
+ return new SplitGraph(
+ root.blueId(),
+ root.original(),
+ root.directFragment(),
+ exactGraph.fragments(),
+ metadata);
+ }
+
+ private SplitGraph splitCyclicAwareEvent(
+ Node exactEvent) {
+ CyclicAwareEventFragments eventGraph =
+ new CyclicAwareEventFragments(
+ exactEvent);
+ List metadata =
+ new ArrayList<>();
+ for (String blueId
+ : eventGraph.fragments.keySet()) {
+ boolean eventRoot =
+ eventGraph.rootBlueId.equals(
+ blueId);
+ metadata.add(new FragmentMetadata(
+ blueId,
+ eventRoot
+ ? FragmentKind.EVENT_ROOT
+ : FragmentKind.EVENT_FRAGMENT,
+ "/",
+ eventRoot ? "/" : null,
+ null,
+ null));
+ }
+ return new SplitGraph(
+ eventGraph.rootBlueId,
+ exactEvent,
+ eventGraph.directRoot,
+ eventGraph.fragments,
+ metadata);
+ }
+
+ /**
+ * Prepares the exact two PROCESS arguments and a lazily verified fragment
+ * provider. Preparation itself does not consume either fragment; BlueId
+ * evidence is verified when PROCESS first demands it. Execution evidence
+ * remains out-of-band environment evidence, not a third semantic input.
+ */
+ public PreparedProcessingInput prepareForProcessing(
+ String rootBlueId,
+ String eventBlueId,
+ VerifiedExecutionEvidence evidence,
+ NodeProvider fragmentProvider) {
+ String checkedRoot = BlueIds.requirePlainBlueId(
+ rootBlueId, "rootBlueId");
+ String checkedEvent = BlueIds.requirePlainBlueId(
+ eventBlueId, "eventBlueId");
+ VerifiedExecutionEvidence checkedEvidence =
+ Objects.requireNonNull(evidence, "evidence");
+ if (!checkedRoot.equals(checkedEvidence.rootBlueId())
+ || !checkedEvent.equals(
+ checkedEvidence.eventBlueId())) {
+ throw new IllegalArgumentException(
+ "Execution evidence is not bound to the prepared Root and Event");
+ }
+
+ NodeProvider verifiedProvider =
+ new VerifyingNodeProvider(
+ Objects.requireNonNull(
+ fragmentProvider, "fragmentProvider"));
+
+ return new PreparedProcessingInput(
+ new Node().blueId(checkedRoot),
+ new Node().blueId(checkedEvent),
+ checkedEvidence,
+ verifiedProvider);
+ }
+
+ private DocumentPlan discoverDocumentPlan(Node root) {
+ SortedMap scopes =
+ new TreeMap<>();
+ Deque pending = new ArrayDeque<>();
+ ScopePlan rootScope =
+ new ScopePlan("/", root);
+ scopes.put("/", rootScope);
+ pending.add(rootScope);
+ List bodies = new ArrayList<>();
+
+ while (!pending.isEmpty()) {
+ ScopePlan scope = pending.removeFirst();
+ discoverBodies(scope, bodies);
+ List declaredPaths =
+ new ArrayList<>(
+ declaredEmbeddedPaths(scope));
+ Collections.sort(
+ declaredPaths,
+ new Comparator() {
+ @Override
+ public int compare(
+ String left,
+ String right) {
+ int depthComparison =
+ Integer.compare(
+ JsonPointer.split(left)
+ .size(),
+ JsonPointer.split(right)
+ .size());
+ return depthComparison != 0
+ ? depthComparison
+ : left.compareTo(right);
+ }
+ });
+ for (String relativePath
+ : declaredPaths) {
+ String absolutePath =
+ PointerUtils.resolvePointer(
+ scope.scopePath,
+ relativePath);
+ Node child = NodePathEditor.getOrNull(
+ root, absolutePath);
+ if (child == null) {
+ throw new IllegalArgumentException(
+ "Process Embedded path "
+ + relativePath
+ + " at "
+ + scope.scopePath
+ + " selects no child");
+ }
+ if (child.isReferenceOnly()) {
+ throw new IllegalArgumentException(
+ "Process Embedded child "
+ + absolutePath
+ + " must be admitted as exact content before splitting");
+ }
+ if (child.getRawValue() != null
+ || child.getItems() != null) {
+ throw new IllegalArgumentException(
+ "Process Embedded child "
+ + absolutePath
+ + " must be an object Root");
+ }
+ if (scopes.containsKey(absolutePath)) {
+ throw new IllegalArgumentException(
+ "Process Embedded scope is declared more than once: "
+ + absolutePath);
+ }
+ ScopePlan childScope =
+ new ScopePlan(absolutePath, child);
+ ScopePlan containingScope =
+ nearestDeclaredAncestor(
+ scopes,
+ absolutePath);
+ scopes.put(absolutePath, childScope);
+ pending.addLast(childScope);
+ containingScope.embeddedCuts.add(
+ new EmbeddedCut(
+ PointerUtils.relativizePointer(
+ containingScope.scopePath,
+ absolutePath),
+ child));
+ }
+ }
+
+ Collections.sort(
+ bodies,
+ Comparator.comparing(
+ body -> body.absolutePointer));
+ return new DocumentPlan(scopes, bodies);
+ }
+
+ private static ScopePlan nearestDeclaredAncestor(
+ SortedMap scopes,
+ String absolutePath) {
+ ScopePlan nearest = null;
+ int nearestDepth = -1;
+ for (ScopePlan candidate : scopes.values()) {
+ if (!PointerUtils.strictlyInside(
+ absolutePath,
+ candidate.scopePath)) {
+ continue;
+ }
+ int candidateDepth =
+ JsonPointer.split(
+ candidate.scopePath)
+ .size();
+ if (candidateDepth > nearestDepth) {
+ nearest = candidate;
+ nearestDepth = candidateDepth;
+ }
+ }
+ if (nearest == null) {
+ throw new IllegalStateException(
+ "Process Embedded child "
+ + absolutePath
+ + " has no declared ancestor scope");
+ }
+ return nearest;
+ }
+
+ private void discoverBodies(
+ ScopePlan scope,
+ List allBodies) {
+ Node contracts = scope.exactScope.getContracts();
+ if (contracts == null) {
+ return;
+ }
+ if (contracts.isReferenceOnly()) {
+ throw new IllegalArgumentException(
+ "Coordination contract headers at "
+ + scope.scopePath
+ + " must be admitted before splitting");
+ }
+ if (contracts.getProperties() == null) {
+ return;
+ }
+
+ SortedMap ordered =
+ new TreeMap<>(contracts.getProperties());
+ for (Map.Entry entry
+ : ordered.entrySet()) {
+ Node contract = entry.getValue();
+ if (contract == null) {
+ continue;
+ }
+ if (contract.isReferenceOnly()) {
+ throw new IllegalArgumentException(
+ "Coordination contract header '"
+ + entry.getKey()
+ + "' at "
+ + scope.scopePath
+ + " must be admitted before splitting");
+ }
+ String typeBlueId =
+ exactTypeBlueId(contract);
+ List fields =
+ executableBodyFieldsByType.get(
+ typeBlueId);
+ if (fields == null || fields.isEmpty()) {
+ continue;
+ }
+ for (String field : fields) {
+ Node body = contract.getProperties() != null
+ ? contract.getProperties().get(field)
+ : null;
+ if (body == null) {
+ continue;
+ }
+ String relativePointer =
+ JsonPointer.toPointer(
+ Arrays.asList(
+ "contracts",
+ entry.getKey(),
+ field));
+ String absolutePointer =
+ PointerUtils.resolvePointer(
+ scope.scopePath,
+ relativePointer);
+ BodyCut cut = new BodyCut(
+ scope.scopePath,
+ relativePointer,
+ absolutePointer,
+ typeBlueId,
+ field,
+ body);
+ scope.bodyCuts.add(cut);
+ allBodies.add(cut);
+ }
+ }
+ }
+
+ private List declaredEmbeddedPaths(
+ ScopePlan scope) {
+ Node contracts = scope.exactScope.getContracts();
+ if (contracts == null
+ || contracts.getProperties() == null) {
+ return Collections.emptyList();
+ }
+ Node embedded = contracts.getProperties().get(
+ blue.language.processor.util
+ .ProcessorContractConstants.KEY_EMBEDDED);
+ if (embedded == null) {
+ return Collections.emptyList();
+ }
+ String typeBlueId = exactTypeBlueId(embedded);
+ if (!RuntimeBlueIds.PROCESS_EMBEDDED.equals(
+ typeBlueId)) {
+ throw new IllegalArgumentException(
+ "Reserved embedded contract at "
+ + scope.scopePath
+ + " is not the exact Process Embedded runtime type");
+ }
+ Node paths = embedded.getProperties() != null
+ ? embedded.getProperties().get("paths")
+ : null;
+ if (paths == null) {
+ return Collections.emptyList();
+ }
+ if (paths.getItems() == null) {
+ throw new IllegalArgumentException(
+ "Process Embedded paths at "
+ + scope.scopePath
+ + " must be a List of Text");
+ }
+
+ List result =
+ new ArrayList<>(paths.getItems().size());
+ Set unique = new LinkedHashSet<>();
+ for (Node pathNode : paths.getItems()) {
+ Object raw =
+ pathNode != null
+ ? pathNode.getRawValue()
+ : null;
+ if (!(raw instanceof String)) {
+ throw new IllegalArgumentException(
+ "Process Embedded path at "
+ + scope.scopePath
+ + " must be Text");
+ }
+ String normalized =
+ PointerUtils.assertValidRuntimePointer(
+ (String) raw);
+ if ("/".equals(normalized)) {
+ throw new IllegalArgumentException(
+ "Process Embedded path '/' cannot embed its declaring scope");
+ }
+ if (!unique.add(normalized)) {
+ throw new IllegalArgumentException(
+ "Process Embedded paths must be unique at "
+ + scope.scopePath);
+ }
+ result.add(normalized);
+ }
+ return result;
+ }
+
+ private Node fragmentScope(ScopePlan scope) {
+ Node fragment = scope.exactScope.clone();
+ List embedded =
+ new ArrayList<>(scope.embeddedCuts);
+ Collections.sort(
+ embedded,
+ Comparator.comparing(
+ cut -> cut.relativePointer));
+ for (EmbeddedCut cut : embedded) {
+ String childBlueId =
+ BlueIdCalculator.calculateBlueId(
+ cut.exactChild);
+ NodePathEditor.put(
+ fragment,
+ cut.relativePointer,
+ new Node().blueId(
+ childBlueId));
+ }
+
+ List bodies =
+ new ArrayList<>(scope.bodyCuts);
+ Collections.sort(
+ bodies,
+ Comparator.comparing(
+ cut -> cut.relativePointer));
+ for (BodyCut cut : bodies) {
+ if (cut.exactBody.isReferenceOnly()) {
+ continue;
+ }
+ NodePathEditor.put(
+ fragment,
+ cut.relativePointer,
+ new Node().blueId(
+ BlueIdCalculator.calculateBlueId(
+ cut.exactBody)));
+ }
+ return fragment;
+ }
+
+ private static String exactTypeBlueId(Node contract) {
+ Node type =
+ contract != null ? contract.getType() : null;
+ if (type == null) {
+ return null;
+ }
+ return type.isReferenceOnly()
+ ? type.getBlueId()
+ : BlueIdCalculator.calculateBlueId(type);
+ }
+
+ private static boolean containsCyclicMemberReference(
+ Node root) {
+ Set visited =
+ Collections.newSetFromMap(
+ new IdentityHashMap());
+ return containsCyclicMemberReference(
+ root, visited);
+ }
+
+ private static boolean containsCyclicMemberReference(
+ Node node,
+ Set visited) {
+ if (node == null || !visited.add(node)) {
+ return false;
+ }
+ if (isCyclicMemberId(node.getBlueId())
+ || isCyclicMemberId(
+ node.getPreviousBlueId())) {
+ return true;
+ }
+ if (containsCyclicMemberReference(
+ node.getType(), visited)
+ || containsCyclicMemberReference(
+ node.getItemType(), visited)
+ || containsCyclicMemberReference(
+ node.getKeyType(), visited)
+ || containsCyclicMemberReference(
+ node.getValueType(), visited)
+ || containsCyclicMemberReference(
+ node.getContracts(), visited)
+ || containsCyclicMemberReference(
+ node.getBlue(), visited)
+ || containsCyclicMemberReference(
+ node.getSchema(), visited)) {
+ return true;
+ }
+ if (node.getItems() != null) {
+ for (Node item : node.getItems()) {
+ if (containsCyclicMemberReference(
+ item, visited)) {
+ return true;
+ }
+ }
+ }
+ if (node.getProperties() != null) {
+ for (Node property
+ : node.getProperties().values()) {
+ if (containsCyclicMemberReference(
+ property, visited)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private static boolean containsCyclicMemberReference(
+ Schema schema,
+ Set visited) {
+ if (schema == null) {
+ return false;
+ }
+ if (isCyclicMemberId(schema.getBlueId())) {
+ return true;
+ }
+ if (containsCyclicMemberReference(
+ schema.getRequired(), visited)
+ || containsCyclicMemberReference(
+ schema.getMinLength(), visited)
+ || containsCyclicMemberReference(
+ schema.getMaxLength(), visited)
+ || containsCyclicMemberReference(
+ schema.getMinimum(), visited)
+ || containsCyclicMemberReference(
+ schema.getMaximum(), visited)
+ || containsCyclicMemberReference(
+ schema.getExclusiveMinimum(), visited)
+ || containsCyclicMemberReference(
+ schema.getExclusiveMaximum(), visited)
+ || containsCyclicMemberReference(
+ schema.getMultipleOf(), visited)
+ || containsCyclicMemberReference(
+ schema.getMinItems(), visited)
+ || containsCyclicMemberReference(
+ schema.getMaxItems(), visited)
+ || containsCyclicMemberReference(
+ schema.getUniqueItems(), visited)
+ || containsCyclicMemberReference(
+ schema.getMinFields(), visited)
+ || containsCyclicMemberReference(
+ schema.getMaxFields(), visited)) {
+ return true;
+ }
+ if (schema.getEnum() != null) {
+ for (Node value : schema.getEnum()) {
+ if (containsCyclicMemberReference(
+ value, visited)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private static boolean isCyclicMemberId(
+ String blueId) {
+ return blueId != null
+ && blueId.indexOf('#') >= 0;
+ }
+
+ private static Node requireExactContent(
+ Node node,
+ String label) {
+ Node checked =
+ Objects.requireNonNull(node, label);
+ if (checked.isReferenceOnly()) {
+ throw new IllegalArgumentException(
+ label + " must contain exact admitted content");
+ }
+ return checked.clone();
+ }
+
+ private static void requireIdentity(
+ String expectedBlueId,
+ Node fragment,
+ String label) {
+ String actualBlueId =
+ BlueIdCalculator.calculateBlueId(fragment);
+ if (!expectedBlueId.equals(actualBlueId)) {
+ throw new IllegalStateException(
+ label
+ + " fragmentation changed BlueId from "
+ + expectedBlueId
+ + " to "
+ + actualBlueId);
+ }
+ }
+
+ private static Map>
+ defaultExecutableBodyFields() {
+ SequentialWorkflowRunner runner =
+ new SequentialWorkflowRunner(
+ Collections.emptyList());
+ try {
+ ContractProcessorRegistry registry =
+ ContractProcessorRegistryBuilder
+ .create()
+ .register(
+ new SequentialWorkflowProcessor(
+ runner))
+ .register(
+ new SequentialWorkflowOperationProcessor(
+ runner))
+ .register(
+ new ChatWorkflowOperationProcessor(
+ runner))
+ .build();
+ return snapshotExecutableBodyFields(
+ registry);
+ } finally {
+ runner.close();
+ }
+ }
+
+ private static Map>
+ snapshotExecutableBodyFields(
+ ContractProcessorRegistry registry) {
+ SortedMap> result =
+ new TreeMap<>();
+ for (Map.Entry>
+ entry : registry.processors().entrySet()) {
+ if (!(entry.getValue()
+ instanceof HandlerProcessor)) {
+ continue;
+ }
+ List fields =
+ registry.executableBodyFields(
+ entry.getKey());
+ if (fields != null && !fields.isEmpty()) {
+ result.put(
+ entry.getKey(),
+ new ArrayList<>(fields));
+ }
+ }
+ return result;
+ }
+
+ private static Map>
+ immutableBodyFieldSnapshot(
+ Map> source) {
+ SortedMap> result =
+ new TreeMap<>();
+ for (Map.Entry>
+ entry : source.entrySet()) {
+ String typeBlueId =
+ BlueIds.requireBlueIdOrCyclicMember(
+ entry.getKey(),
+ "handlerTypeBlueId");
+ List fields =
+ new ArrayList<>();
+ for (String field : entry.getValue()) {
+ if (field == null
+ || field.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Executable body field must be non-empty");
+ }
+ fields.add(field);
+ }
+ result.put(
+ typeBlueId,
+ Collections.unmodifiableList(
+ fields));
+ }
+ return Collections.unmodifiableMap(
+ result);
+ }
+
+ /**
+ * The exact physical fragment inventory for one semantic input.
+ */
+ public static final class SplitGraph {
+
+ private final String rootBlueId;
+ private final Node originalRoot;
+ private final Node fragmentedRoot;
+ private final SortedMap fragments;
+ private final List metadata;
+ private final NodeProvider provider;
+
+ private SplitGraph(
+ String rootBlueId,
+ Node originalRoot,
+ Node fragmentedRoot,
+ Map fragments,
+ Collection metadata) {
+ this.rootBlueId =
+ Objects.requireNonNull(
+ rootBlueId, "rootBlueId");
+ this.originalRoot =
+ Objects.requireNonNull(
+ originalRoot, "originalRoot")
+ .clone();
+ this.fragmentedRoot =
+ Objects.requireNonNull(
+ fragmentedRoot,
+ "fragmentedRoot")
+ .clone();
+ this.fragments =
+ immutableFragments(fragments);
+ List ordered =
+ new ArrayList<>(metadata);
+ Collections.sort(
+ ordered,
+ Comparator
+ .comparing(
+ FragmentMetadata::blueId)
+ .thenComparing(
+ value -> value.kind().name())
+ .thenComparing(
+ value -> nullToEmpty(
+ value.pointer())));
+ this.metadata =
+ Collections.unmodifiableList(
+ ordered);
+ this.provider =
+ verifiedProvider(this.fragments);
+ requireIdentity(
+ rootBlueId,
+ this.fragmentedRoot,
+ "Split Root");
+ }
+
+ public String rootBlueId() {
+ return rootBlueId;
+ }
+
+ public Node originalRoot() {
+ return originalRoot.clone();
+ }
+
+ public Node fragmentedRoot() {
+ return fragmentedRoot.clone();
+ }
+
+ public Node pureReference() {
+ return new Node().blueId(
+ rootBlueId);
+ }
+
+ public Map fragments() {
+ return immutableFragments(
+ fragments);
+ }
+
+ public NodeProvider provider() {
+ return provider;
+ }
+
+ public List metadata() {
+ return metadata;
+ }
+ }
+
+ public enum FragmentKind {
+ DOCUMENT_ROOT,
+ EMBEDDED_ROOT,
+ EXECUTABLE_BODY,
+ EVENT_ROOT,
+ EVENT_FRAGMENT
+ }
+
+ /**
+ * Non-semantic diagnostic information for one retained fragment occurrence.
+ */
+ public static final class FragmentMetadata {
+
+ private final String blueId;
+ private final FragmentKind kind;
+ private final String scopePath;
+ private final String pointer;
+ private final String handlerTypeBlueId;
+ private final String executableBodyField;
+
+ private FragmentMetadata(
+ String blueId,
+ FragmentKind kind,
+ String scopePath,
+ String pointer,
+ String handlerTypeBlueId,
+ String executableBodyField) {
+ this.blueId =
+ Objects.requireNonNull(
+ blueId, "blueId");
+ this.kind =
+ Objects.requireNonNull(
+ kind, "kind");
+ this.scopePath = scopePath;
+ this.pointer = pointer;
+ this.handlerTypeBlueId =
+ handlerTypeBlueId;
+ this.executableBodyField =
+ executableBodyField;
+ }
+
+ public String blueId() {
+ return blueId;
+ }
+
+ public FragmentKind kind() {
+ return kind;
+ }
+
+ public String scopePath() {
+ return scopePath;
+ }
+
+ public String pointer() {
+ return pointer;
+ }
+
+ public String handlerTypeBlueId() {
+ return handlerTypeBlueId;
+ }
+
+ public String executableBodyField() {
+ return executableBodyField;
+ }
+ }
+
+ /**
+ * Exact PROCESS inputs plus the provider and revision-bound evidence needed
+ * by a configured Language processor.
+ */
+ public static final class PreparedProcessingInput {
+
+ private final Node document;
+ private final Node event;
+ private final VerifiedExecutionEvidence evidence;
+ private final NodeProvider provider;
+
+ private PreparedProcessingInput(
+ Node document,
+ Node event,
+ VerifiedExecutionEvidence evidence,
+ NodeProvider provider) {
+ this.document = document.clone();
+ this.event = event.clone();
+ this.evidence = evidence;
+ this.provider = provider;
+ }
+
+ public Node document() {
+ return document.clone();
+ }
+
+ public Node event() {
+ return event.clone();
+ }
+
+ public VerifiedExecutionEvidence evidence() {
+ return evidence;
+ }
+
+ public NodeProvider provider() {
+ return provider;
+ }
+ }
+
+ private static SortedMap
+ immutableFragments(
+ Map source) {
+ SortedMap result =
+ new TreeMap<>();
+ for (Map.Entry
+ entry : source.entrySet()) {
+ Node fragment =
+ Objects.requireNonNull(
+ entry.getValue(),
+ "fragment")
+ .clone();
+ requireIdentity(
+ entry.getKey(),
+ fragment,
+ "Exact fragment");
+ result.put(
+ entry.getKey(),
+ fragment);
+ }
+ return Collections.unmodifiableSortedMap(
+ result);
+ }
+
+ private static NodeProvider verifiedProvider(
+ Map fragments) {
+ final SortedMap retained =
+ immutableFragments(fragments);
+ NodeProvider raw = blueId -> {
+ Node fragment = retained.get(blueId);
+ return fragment != null
+ ? Collections.singletonList(
+ fragment.clone())
+ : null;
+ };
+ return new VerifyingNodeProvider(raw);
+ }
+
+ private static String nullToEmpty(
+ String value) {
+ return value != null ? value : "";
+ }
+
+ /**
+ * Event fragments may contain published cyclic-set member type references.
+ * Those references are external exact identities, not local fragments.
+ * Language's ordinary graph helper intentionally rejects them because it
+ * cannot prove cyclic-set content; this builder never claims or expands
+ * that content and records only locally inline nodes under plain BlueIds.
+ */
+ private static final class CyclicAwareEventFragments {
+
+ private final IdentityHashMap records =
+ new IdentityHashMap<>();
+ private final IdentityHashMap active =
+ new IdentityHashMap<>();
+ private final SortedMap fragments =
+ new TreeMap<>();
+ private final SortedMap> edges =
+ new TreeMap<>();
+ private final String rootBlueId;
+ private final Node directRoot;
+
+ private CyclicAwareEventFragments(
+ Node exactEvent) {
+ EventFragmentRecord root =
+ record(exactEvent, "event");
+ rejectLocalFragmentCycles();
+ this.rootBlueId = root.blueId;
+ this.directRoot =
+ root.directFragment.clone();
+ }
+
+ private EventFragmentRecord record(
+ Node node,
+ String path) {
+ if (node.isReferenceOnly()) {
+ throw new IllegalArgumentException(
+ "Exact event content at "
+ + path
+ + " must not be a pure reference");
+ }
+ if (node.getBlueId() != null) {
+ throw new IllegalArgumentException(
+ "Exact event content at "
+ + path
+ + " must not mix its own BlueId with inline content");
+ }
+ EventFragmentRecord retained =
+ records.get(node);
+ if (retained != null) {
+ return retained;
+ }
+ String activePath =
+ active.put(node, path);
+ if (activePath != null) {
+ throw new IllegalArgumentException(
+ "Blue object cycle between "
+ + activePath
+ + " and "
+ + path
+ + " cannot be fragmented");
+ }
+ try {
+ String originalBlueId =
+ BlueIds.requirePlainBlueId(
+ BlueIdCalculator
+ .calculateBlueId(
+ node),
+ path);
+ SortedSet directEdges =
+ new TreeSet<>();
+ Node direct = node.clone();
+
+ direct.type(referenceFor(
+ node.getType(),
+ path + "/type",
+ directEdges));
+ direct.itemType(referenceFor(
+ node.getItemType(),
+ path + "/itemType",
+ directEdges));
+ direct.keyType(referenceFor(
+ node.getKeyType(),
+ path + "/keyType",
+ directEdges));
+ direct.valueType(referenceFor(
+ node.getValueType(),
+ path + "/valueType",
+ directEdges));
+ direct.contracts(referenceFor(
+ node.getContracts(),
+ path + "/contracts",
+ directEdges));
+ direct.blue(referenceFor(
+ node.getBlue(),
+ path + "/blue",
+ directEdges));
+
+ if (node.getItems() != null) {
+ List items =
+ new ArrayList<>(
+ node.getItems().size());
+ for (int index = 0;
+ index < node.getItems().size();
+ index++) {
+ items.add(referenceFor(
+ node.getItems().get(index),
+ path + "/items/" + index,
+ directEdges));
+ }
+ direct.items(items);
+ }
+
+ if (node.getProperties() != null) {
+ SortedMap ordered =
+ new TreeMap<>(
+ node.getProperties());
+ Map properties =
+ new TreeMap<>();
+ for (Map.Entry entry
+ : ordered.entrySet()) {
+ Node child = entry.getValue();
+ properties.put(
+ entry.getKey(),
+ isRawHashProperty(
+ entry.getKey())
+ ? cloneOrNull(child)
+ : referenceFor(
+ child,
+ path + "/"
+ + entry.getKey(),
+ directEdges));
+ }
+ direct.properties(properties);
+ }
+
+ direct.schema(fragmentSchema(
+ node.getSchema(),
+ path + "/schema",
+ directEdges));
+ if (node.getPreviousBlueId() != null) {
+ String previous =
+ BlueIds
+ .requireBlueIdOrCyclicMember(
+ node.getPreviousBlueId(),
+ path
+ + "/$previous/blueId");
+ directEdges.add(previous);
+ }
+
+ requireIdentity(
+ originalBlueId,
+ direct,
+ "Event fragment at " + path);
+ Node existing =
+ fragments.get(originalBlueId);
+ if (existing == null) {
+ fragments.put(
+ originalBlueId,
+ direct.clone());
+ }
+ SortedSet retainedEdges =
+ edges.computeIfAbsent(
+ originalBlueId,
+ ignored -> new TreeSet<>());
+ retainedEdges.addAll(directEdges);
+ EventFragmentRecord created =
+ new EventFragmentRecord(
+ originalBlueId,
+ direct);
+ records.put(node, created);
+ return created;
+ } finally {
+ active.remove(node);
+ }
+ }
+
+ private Node referenceFor(
+ Node child,
+ String path,
+ Set directEdges) {
+ if (child == null) {
+ return null;
+ }
+ String childBlueId;
+ if (child.isReferenceOnly()) {
+ childBlueId =
+ BlueIds
+ .requireBlueIdOrCyclicMember(
+ child.getBlueId(),
+ path + "/blueId");
+ } else {
+ childBlueId =
+ record(child, path).blueId;
+ }
+ directEdges.add(childBlueId);
+ return new Node().blueId(
+ childBlueId);
+ }
+
+ private Schema fragmentSchema(
+ Schema schema,
+ String path,
+ Set directEdges) {
+ if (schema == null) {
+ return null;
+ }
+ if (schema.isReferenceOnly()) {
+ String schemaBlueId =
+ BlueIds
+ .requireBlueIdOrCyclicMember(
+ schema.getBlueId(),
+ path + "/blueId");
+ directEdges.add(schemaBlueId);
+ return new Schema().blueId(
+ schemaBlueId);
+ }
+ if (schema.getBlueId() != null) {
+ throw new IllegalArgumentException(
+ "Exact event schema at "
+ + path
+ + " must not mix its own BlueId with inline content");
+ }
+
+ Schema direct = schema.clone();
+ direct.minimum(fragmentSchemaValue(
+ schema.getMinimum(),
+ path + "/minimum",
+ directEdges));
+ direct.maximum(fragmentSchemaValue(
+ schema.getMaximum(),
+ path + "/maximum",
+ directEdges));
+ direct.exclusiveMinimum(
+ fragmentSchemaValue(
+ schema.getExclusiveMinimum(),
+ path
+ + "/exclusiveMinimum",
+ directEdges));
+ direct.exclusiveMaximum(
+ fragmentSchemaValue(
+ schema.getExclusiveMaximum(),
+ path
+ + "/exclusiveMaximum",
+ directEdges));
+ direct.multipleOf(
+ fragmentSchemaValue(
+ schema.getMultipleOf(),
+ path + "/multipleOf",
+ directEdges));
+ if (schema.getEnum() != null) {
+ List values =
+ new ArrayList<>(
+ schema.getEnum().size());
+ for (int index = 0;
+ index < schema.getEnum().size();
+ index++) {
+ values.add(fragmentSchemaValue(
+ schema.getEnum().get(index),
+ path + "/enum/" + index,
+ directEdges));
+ }
+ direct.enumValues(values);
+ }
+ return direct;
+ }
+
+ private Node fragmentSchemaValue(
+ Node value,
+ String path,
+ Set directEdges) {
+ if (value == null) {
+ return null;
+ }
+ return isPlainSchemaScalar(value)
+ ? value.clone()
+ : referenceFor(
+ value, path, directEdges);
+ }
+
+ private void rejectLocalFragmentCycles() {
+ Map states =
+ new TreeMap<>();
+ for (String blueId : fragments.keySet()) {
+ rejectLocalFragmentCycles(
+ blueId,
+ states,
+ new ArrayList());
+ }
+ }
+
+ private void rejectLocalFragmentCycles(
+ String blueId,
+ Map states,
+ List path) {
+ LocalVisitState state =
+ states.get(blueId);
+ if (state == LocalVisitState.COMPLETE) {
+ return;
+ }
+ if (state == LocalVisitState.ACTIVE) {
+ path.add(blueId);
+ throw new IllegalArgumentException(
+ "Mixed reference/object cycle cannot be fragmented: "
+ + path);
+ }
+ states.put(
+ blueId, LocalVisitState.ACTIVE);
+ path.add(blueId);
+ SortedSet targets =
+ edges.get(blueId);
+ if (targets != null) {
+ for (String target : targets) {
+ if (fragments.containsKey(target)) {
+ rejectLocalFragmentCycles(
+ target,
+ states,
+ new ArrayList<>(
+ path));
+ }
+ }
+ }
+ states.put(
+ blueId, LocalVisitState.COMPLETE);
+ }
+
+ private static boolean isRawHashProperty(
+ String key) {
+ return "name".equals(key)
+ || "description".equals(key)
+ || "value".equals(key);
+ }
+
+ private static Node cloneOrNull(
+ Node node) {
+ return node != null
+ ? node.clone()
+ : null;
+ }
+ }
+
+ private static boolean isPlainSchemaScalar(
+ Node node) {
+ return node != null
+ && node.getRawValue() != null
+ && node.getName() == null
+ && node.getDescription() == null
+ && node.getType() == null
+ && node.getItemType() == null
+ && node.getKeyType() == null
+ && node.getValueType() == null
+ && node.getItems() == null
+ && node.getProperties() == null
+ && node.getContracts() == null
+ && node.getBlueId() == null
+ && node.getSchema() == null
+ && node.getMergePolicy() == null
+ && node.getPreviousBlueId() == null
+ && node.getPosition() == null
+ && node.getBlue() == null;
+ }
+
+ private static final class EventFragmentRecord {
+
+ private final String blueId;
+ private final Node directFragment;
+
+ private EventFragmentRecord(
+ String blueId,
+ Node directFragment) {
+ this.blueId = blueId;
+ this.directFragment =
+ directFragment.clone();
+ }
+ }
+
+ private enum LocalVisitState {
+ ACTIVE,
+ COMPLETE
+ }
+
+ private static final class DocumentPlan {
+
+ private final SortedMap scopes;
+ private final List bodies;
+
+ private DocumentPlan(
+ SortedMap scopes,
+ List bodies) {
+ this.scopes = scopes;
+ this.bodies = bodies;
+ }
+ }
+
+ private static final class ScopePlan {
+
+ private final String scopePath;
+ private final Node exactScope;
+ private final List embeddedCuts =
+ new ArrayList<>();
+ private final List bodyCuts =
+ new ArrayList<>();
+
+ private ScopePlan(
+ String scopePath,
+ Node exactScope) {
+ this.scopePath = scopePath;
+ this.exactScope = exactScope;
+ }
+ }
+
+ private static final class EmbeddedCut {
+
+ private final String relativePointer;
+ private final Node exactChild;
+
+ private EmbeddedCut(
+ String relativePointer,
+ Node exactChild) {
+ this.relativePointer =
+ relativePointer;
+ this.exactChild = exactChild;
+ }
+ }
+
+ private static final class BodyCut {
+
+ private final String scopePath;
+ private final String relativePointer;
+ private final String absolutePointer;
+ private final String handlerTypeBlueId;
+ private final String field;
+ private final Node exactBody;
+
+ private BodyCut(
+ String scopePath,
+ String relativePointer,
+ String absolutePointer,
+ String handlerTypeBlueId,
+ String field,
+ Node exactBody) {
+ this.scopePath = scopePath;
+ this.relativePointer =
+ relativePointer;
+ this.absolutePointer =
+ absolutePointer;
+ this.handlerTypeBlueId =
+ handlerTypeBlueId;
+ this.field = field;
+ this.exactBody = exactBody;
+ }
+ }
+}
diff --git a/src/main/java/blue/coordination/processor/CoordinationEventNodes.java b/src/main/java/blue/coordination/processor/CoordinationEventNodes.java
index 899e8c8..eb5d8ab 100644
--- a/src/main/java/blue/coordination/processor/CoordinationEventNodes.java
+++ b/src/main/java/blue/coordination/processor/CoordinationEventNodes.java
@@ -2,12 +2,11 @@
import blue.language.Blue;
import blue.language.model.Node;
+import blue.language.model.Schema;
+import blue.language.processor.ExternalChannelFunctionContext;
+import blue.language.processor.HandlerMatchContext;
import blue.repo.BlueRepository;
-import blue.repo.coordination.Actor;
-import blue.repo.coordination.ChatMessage;
import blue.repo.coordination.OperationRequest;
-import blue.repo.coordination.StatusCompleted;
-import blue.repo.coordination.Timeline;
import blue.repo.coordination.TimelineEntry;
import java.math.BigDecimal;
import java.math.BigInteger;
@@ -19,11 +18,29 @@ final class CoordinationEventNodes {
private static final ThreadLocal BINDING_CONVERTER = new ThreadLocal() {
@Override
protected Blue initialValue() {
- return REPOSITORY.configure(new Blue());
+ return new Blue()
+ .nodeProvider(REPOSITORY.nodeProvider())
+ .typeClassResolver(REPOSITORY.typeClassResolver());
}
};
- private static final Node TIMELINE_TYPE = repositoryType(Timeline.qualifiedName());
- private static final Node ACTOR_TYPE = repositoryType(Actor.qualifiedName());
+ private static final ThreadLocal FINAL_BINDING_CONVERTER =
+ new ThreadLocal() {
+ @Override
+ protected Blue initialValue() {
+ return new Blue();
+ }
+ };
+ private static final ThreadLocal LEGACY_TYPE_MATCHER =
+ new ThreadLocal() {
+ @Override
+ protected Blue initialValue() {
+ return new Blue()
+ .nodeProvider(REPOSITORY.nodeProvider())
+ .typeClassResolver(REPOSITORY.typeClassResolver());
+ }
+ };
+ private static final Node TIMELINE_ENTRY_TYPE = new Node()
+ .type(new Node().blueId(TimelineEntry.blueId()));
private static final Node OPERATION_REQUEST_TYPE = new Node()
.type(new Node().blueId(OperationRequest.blueId()));
@@ -34,12 +51,34 @@ static TimelineEntryView timelineEntry(Node node) {
if (!isTimelineEntry(node)) {
return null;
}
+ return timelineEntryHeader(node);
+ }
+
+ static TimelineEntryView timelineEntry(
+ Node node,
+ ExternalChannelFunctionContext context) {
+ Node projected = projectTimelineEntry(
+ node, context);
+ if (!isTimelineEntry(projected, context)) {
+ return null;
+ }
+ return timelineEntryHeader(projected);
+ }
+
+ static TimelineEntryView timelineEntryHeader(
+ Node node,
+ ExternalChannelFunctionContext context) {
+ return timelineEntryHeader(
+ projectTimelineEntry(node, context));
+ }
+
+ static TimelineEntryView timelineEntryHeader(Node node) {
Node timeline = property(node, "timeline");
Node actor = property(node, "actor");
BigInteger timestamp = timestamp(node);
Node message = property(node, "message");
- if (!BlueSemanticIdentity.matchesType(timeline, TIMELINE_TYPE)
- || !BlueSemanticIdentity.matchesType(actor, ACTOR_TYPE)
+ if (timeline == null
+ || actor == null
|| timestamp == null
|| message == null) {
return null;
@@ -48,9 +87,42 @@ static TimelineEntryView timelineEntry(Node node) {
}
static boolean isTimelineEntry(Node node) {
- return node != null
- && node.getType() != null
- && TimelineEntry.blueId().equals(node.getType().getBlueId());
+ if (node == null || node.getType() == null) {
+ return false;
+ }
+ Node type = node.getType();
+ if (TimelineEntry.blueId().equals(type.getBlueId())) {
+ return true;
+ }
+ try {
+ if (BlueSemanticIdentity.equals(
+ type,
+ new Node().blueId(TimelineEntry.blueId()))) {
+ return true;
+ }
+ return LEGACY_TYPE_MATCHER.get().nodeMatchesType(
+ new Node().type(type.clone()),
+ TIMELINE_ENTRY_TYPE);
+ } catch (RuntimeException invalidTypeEvidence) {
+ return false;
+ }
+ }
+
+ static boolean isTimelineEntry(
+ Node node,
+ ExternalChannelFunctionContext context) {
+ if (node == null || node.getType() == null) {
+ return false;
+ }
+ if (TimelineEntry.blueId().equals(
+ node.getType().getBlueId())) {
+ return true;
+ }
+ return context.matchesPattern(
+ node,
+ new Node().type(
+ new Node().blueId(
+ TimelineEntry.blueId())));
}
static BigInteger timestamp(Node node) {
@@ -58,23 +130,226 @@ static BigInteger timestamp(Node node) {
}
static boolean matchesGeneratedBinding(Node candidate, Object configuredBinding) {
+ if (configuredBinding == null || candidate == null) {
+ return false;
+ }
+ Node pattern = BINDING_CONVERTER.get().objectToNode(configuredBinding);
+ return candidate.isReferenceOnly()
+ ? BlueSemanticIdentity.equals(candidate, pattern)
+ : matchesPattern(candidate, pattern);
+ }
+
+ static boolean matchesGeneratedBinding(
+ Node candidate,
+ Object configuredBinding,
+ ExternalChannelFunctionContext context) {
return configuredBinding != null
- && matchesPattern(candidate, BINDING_CONVERTER.get().objectToNode(configuredBinding));
+ && candidate != null
+ && context.matchesPattern(
+ candidate,
+ FINAL_BINDING_CONVERTER.get().objectToNode(
+ configuredBinding));
}
static OperationRequestView operationRequest(Node event) {
if (matchesOperationRequestType(event)) {
- return OperationRequestView.from(event, false);
+ return OperationRequestView.from(event);
}
if (!isTimelineEntry(event)) {
return null;
}
Node message = property(event, "message");
return matchesOperationRequestType(message)
- ? OperationRequestView.from(message, true)
+ ? OperationRequestView.from(message)
+ : null;
+ }
+
+ static OperationRequestView operationRequest(
+ Node event,
+ ExternalChannelFunctionContext context) {
+ if (event == null || context == null) {
+ return null;
+ }
+ Node projectedEvent =
+ materializeIfReference(event, context);
+ if (declaresExactType(
+ projectedEvent,
+ TimelineEntry.blueId())) {
+ Node message = materializeIfReference(
+ property(projectedEvent, "message"),
+ context);
+ return matchesOperationRequestType(
+ message, context)
+ ? OperationRequestView.from(
+ message, context)
+ : null;
+ }
+ if (matchesOperationRequestType(
+ projectedEvent, context)) {
+ return OperationRequestView.from(
+ projectedEvent, context);
+ }
+ if (!isTimelineEntry(
+ projectedEvent, context)) {
+ return null;
+ }
+ Node message = materializeIfReference(
+ property(projectedEvent, "message"),
+ context);
+ return matchesOperationRequestType(
+ message, context)
+ ? OperationRequestView.from(
+ message, context)
: null;
}
+ static Node operationRequestRoutingPayload(
+ Node event,
+ ExternalChannelFunctionContext context) {
+ Node projectedEvent =
+ materializeIfReference(event, context);
+ if (projectedEvent == null) {
+ return null;
+ }
+ if (declaresExactType(
+ projectedEvent,
+ TimelineEntry.blueId())) {
+ return projectTimelineOperationRequestPayload(
+ projectedEvent, context);
+ }
+ if (matchesOperationRequestType(
+ projectedEvent, context)) {
+ return projectOperationRequestFields(
+ projectedEvent, context);
+ }
+ if (!isTimelineEntry(
+ projectedEvent, context)) {
+ return projectedEvent.clone();
+ }
+ return projectTimelineOperationRequestPayload(
+ projectedEvent, context);
+ }
+
+ private static Node projectTimelineOperationRequestPayload(
+ Node projectedEvent,
+ ExternalChannelFunctionContext context) {
+ Node suppliedMessage =
+ property(projectedEvent, "message");
+ Node projectedMessage =
+ materializeIfReference(
+ suppliedMessage, context);
+ if (!matchesOperationRequestType(
+ projectedMessage, context)) {
+ return projectedEvent.clone();
+ }
+ Node payload = projectedEvent.clone();
+ payload.getProperties().put(
+ "message",
+ projectOperationRequestFields(
+ projectedMessage, context));
+ return payload;
+ }
+
+ static boolean matchesOperationRequest(
+ Node event,
+ String operation,
+ String channel,
+ Node request,
+ HandlerMatchContext context) {
+ if (event == null
+ || operation == null
+ || channel == null
+ || context == null) {
+ return false;
+ }
+ Node requestPattern = new Node()
+ .type(new Node().blueId(
+ OperationRequest.blueId()))
+ .properties("operation", new Node().value(operation))
+ .properties("channel", new Node().value(channel));
+ if (request != null) {
+ Node presencePattern = requestPattern.clone()
+ .properties("request", new Node()
+ .schema(new Schema().required(true)));
+ if (!matchesDirectOrTimelineOperationRequest(
+ presencePattern, context)) {
+ return false;
+ }
+ requestPattern.properties("request", request.clone());
+ }
+ return matchesDirectOrTimelineOperationRequest(
+ requestPattern, context);
+ }
+
+ static boolean isRoutableOperationRequestForChannel(
+ Node event,
+ String channel,
+ HandlerMatchContext context) {
+ if (event == null
+ || channel == null
+ || context == null) {
+ return false;
+ }
+ OperationRequestView direct =
+ operationRequest(event);
+ if (direct != null && direct.routable()) {
+ return channel.equals(
+ direct.channel());
+ }
+ if (direct != null
+ && !hasReferencedRoutingFields(event)) {
+ return false;
+ }
+ Node requestPattern = new Node()
+ .type(new Node().blueId(
+ OperationRequest.blueId()))
+ .properties(
+ "operation",
+ new Node().schema(
+ new Schema()
+ .required(true)
+ .minLength(1)))
+ .properties(
+ "channel",
+ new Node().value(channel));
+ return matchesDirectOrTimelineOperationRequest(
+ requestPattern, context);
+ }
+
+ private static boolean hasReferencedRoutingFields(
+ Node event) {
+ Node request = event;
+ if (isTimelineEntry(event)) {
+ request = property(event, "message");
+ }
+ if (request == null) {
+ return false;
+ }
+ if (request.isReferenceOnly()) {
+ return true;
+ }
+ Node operation = property(
+ request, "operation");
+ Node channel = property(
+ request, "channel");
+ return operation != null
+ && operation.isReferenceOnly()
+ || channel != null
+ && channel.isReferenceOnly();
+ }
+
+ private static boolean matchesDirectOrTimelineOperationRequest(
+ Node requestPattern,
+ HandlerMatchContext context) {
+ if (context.matchesEventPattern(requestPattern)) {
+ return true;
+ }
+ return context.matchesEventPattern(new Node()
+ .type(new Node().blueId(
+ TimelineEntry.blueId()))
+ .properties("message", requestPattern));
+ }
+
static boolean matchesPattern(Node node, Node pattern) {
if (pattern == null) {
return true;
@@ -108,26 +383,115 @@ private static boolean matchesOperationRequestType(Node node) {
if (node == null || node.getType() == null) {
return false;
}
- String typeBlueId = node.getType().getBlueId();
- if (OperationRequest.blueId().equals(typeBlueId)) {
+ Node exactType = node.getType();
+ if (OperationRequest.blueId().equals(exactType.getBlueId())) {
return true;
}
- if (typeBlueId == null) {
- return false;
- }
try {
- Node resolvedType = node.getType().isReferenceOnly()
- ? REPOSITORY.nodeByBlueId(typeBlueId).orElse(null)
- : node.getType();
- return resolvedType != null
- && BINDING_CONVERTER.get().nodeMatchesType(
- new Node().type(resolvedType.clone().blueId(null)),
- OPERATION_REQUEST_TYPE);
+ if (BlueSemanticIdentity.equals(
+ exactType,
+ new Node().blueId(
+ OperationRequest.blueId()))) {
+ return true;
+ }
+ return LEGACY_TYPE_MATCHER.get().nodeMatchesType(
+ new Node().type(exactType.clone()),
+ OPERATION_REQUEST_TYPE);
} catch (RuntimeException ignored) {
return false;
}
}
+ private static boolean matchesOperationRequestType(
+ Node node,
+ ExternalChannelFunctionContext context) {
+ if (node == null || node.getType() == null) {
+ return false;
+ }
+ if (OperationRequest.blueId().equals(
+ node.getType().getBlueId())) {
+ return true;
+ }
+ return context.matchesPattern(
+ node,
+ new Node().type(
+ new Node().blueId(
+ OperationRequest.blueId())));
+ }
+
+ private static boolean declaresExactType(
+ Node node,
+ String typeBlueId) {
+ return node != null
+ && node.getType() != null
+ && typeBlueId.equals(
+ node.getType().getBlueId());
+ }
+
+ private static Node materializeIfReference(
+ Node node,
+ ExternalChannelFunctionContext context) {
+ return node != null && node.isReferenceOnly()
+ ? context.materializeExactReference(node)
+ : node;
+ }
+
+ private static Node projectTimelineEntry(
+ Node node,
+ ExternalChannelFunctionContext context) {
+ Node projected =
+ materializeIfReference(node, context);
+ if (projected == null
+ || projected.getProperties() == null) {
+ return projected;
+ }
+ String[] fragmentFields = new String[] {
+ "timeline",
+ "actor",
+ "timestamp",
+ "message"
+ };
+ Node mutable = projected;
+ boolean cloned = false;
+ for (String field : fragmentFields) {
+ Node value = property(projected, field);
+ if (value == null || !value.isReferenceOnly()) {
+ continue;
+ }
+ if (!cloned) {
+ mutable = projected.clone();
+ cloned = true;
+ }
+ mutable.getProperties().put(
+ field,
+ context.materializeExactReference(value));
+ }
+ return mutable;
+ }
+
+ private static Node projectOperationRequestFields(
+ Node request,
+ ExternalChannelFunctionContext context) {
+ Node projected = request.clone();
+ String[] routingFields =
+ new String[] {
+ "operation",
+ "channel"
+ };
+ for (String field : routingFields) {
+ Node supplied = property(
+ request, field);
+ if (supplied != null
+ && supplied.isReferenceOnly()) {
+ projected.getProperties().put(
+ field,
+ context.materializeExactReference(
+ supplied));
+ }
+ }
+ return projected;
+ }
+
private static String nonBlankTextProperty(Node node, String key) {
Node property = property(node, key);
Object value = property != null ? property.getValue() : null;
@@ -138,6 +502,23 @@ private static String nonBlankTextProperty(Node node, String key) {
return text.trim().isEmpty() ? null : text;
}
+ private static String nonBlankTextProperty(
+ Node node,
+ String key,
+ ExternalChannelFunctionContext context) {
+ Node exactProperty = materializeIfReference(
+ property(node, key), context);
+ Object value =
+ exactProperty != null
+ ? exactProperty.getValue()
+ : null;
+ if (!(value instanceof String)) {
+ return null;
+ }
+ String text = (String) value;
+ return text.trim().isEmpty() ? null : text;
+ }
+
private static BigInteger integerProperty(Node node, String key) {
Node property = property(node, key);
Object value = property != null ? property.getValue() : null;
@@ -155,46 +536,15 @@ private static boolean typeMatches(Node nodeType, Node patternType) {
if (patternType == null) {
return true;
}
- String expected = typeIdentity(patternType);
- if (expected == null) {
- return true;
- }
if (nodeType == null) {
- return true;
- }
- String actual = typeIdentity(nodeType);
- return expected.equals(actual);
- }
-
- private static String typeIdentity(Node type) {
- if (type == null) {
- return null;
- }
- if (type.getBlueId() != null) {
- return type.getBlueId();
- }
- Object value = type.getValue();
- if (value instanceof String) {
- String knownBlueId = knownCoordinationTypeBlueId((String) value);
- return knownBlueId != null ? knownBlueId : (String) value;
- }
- return null;
- }
-
- private static String knownCoordinationTypeBlueId(String qualifiedName) {
- if (TimelineEntry.qualifiedName().equals(qualifiedName)) {
- return TimelineEntry.blueId();
- }
- if (ChatMessage.qualifiedName().equals(qualifiedName)) {
- return ChatMessage.blueId();
- }
- if (OperationRequest.qualifiedName().equals(qualifiedName)) {
- return OperationRequest.blueId();
+ return false;
}
- if (StatusCompleted.qualifiedName().equals(qualifiedName)) {
- return StatusCompleted.blueId();
+ try {
+ return BlueSemanticIdentity.equals(
+ nodeType, patternType);
+ } catch (RuntimeException invalidTypeEvidence) {
+ return false;
}
- return null;
}
private static boolean valueMatches(Object actual, Object expected) {
@@ -265,16 +615,23 @@ private static boolean requiresPresence(Node pattern) {
if (pattern == null) {
return false;
}
- if (pattern.isReferenceOnly() || pattern.getValue() != null) {
+ if (pattern.getName() != null
+ || pattern.getDescription() != null
+ || pattern.getType() != null
+ || pattern.getItemType() != null
+ || pattern.getKeyType() != null
+ || pattern.getValueType() != null
+ || pattern.getValue() != null
+ || pattern.getContracts() != null
+ || pattern.getBlueId() != null
+ || pattern.getSchema() != null
+ || pattern.getMergePolicy() != null
+ || pattern.getPreviousBlueId() != null
+ || pattern.getPosition() != null
+ || pattern.getBlue() != null
+ || pattern.getItems() != null) {
return true;
}
- if (pattern.getItems() != null) {
- for (Node item : pattern.getItems()) {
- if (requiresPresence(item)) {
- return true;
- }
- }
- }
if (pattern.getProperties() != null) {
for (Node property : pattern.getProperties().values()) {
if (requiresPresence(property)) {
@@ -318,28 +675,35 @@ BigInteger timestamp() {
}
static final class OperationRequestView {
- private final Node requestNode;
- private final boolean timelineMessage;
private final String operation;
private final String channel;
- private OperationRequestView(Node requestNode,
- boolean timelineMessage,
- String operation,
+ private OperationRequestView(String operation,
String channel) {
- this.requestNode = requestNode;
- this.timelineMessage = timelineMessage;
this.operation = operation;
this.channel = channel;
}
- private static OperationRequestView from(Node requestNode, boolean timelineMessage) {
- return new OperationRequestView(requestNode,
- timelineMessage,
+ private static OperationRequestView from(Node requestNode) {
+ return new OperationRequestView(
nonBlankTextProperty(requestNode, "operation"),
nonBlankTextProperty(requestNode, "channel"));
}
+ private static OperationRequestView from(
+ Node requestNode,
+ ExternalChannelFunctionContext context) {
+ return new OperationRequestView(
+ nonBlankTextProperty(
+ requestNode,
+ "operation",
+ context),
+ nonBlankTextProperty(
+ requestNode,
+ "channel",
+ context));
+ }
+
boolean routable() {
return operation != null && channel != null;
}
@@ -352,17 +716,5 @@ String channel() {
return channel;
}
- Node request() {
- return property(requestNode, "request");
- }
-
- Node patternFor(Node requestPattern) {
- Node request = requestPattern.clone();
- if (!timelineMessage) {
- return new Node().properties("request", request);
- }
- return new Node().properties("message", new Node()
- .properties("request", request));
- }
}
}
diff --git a/src/main/java/blue/coordination/processor/CoordinationRepositoryCompatibilityNodeProvider.java b/src/main/java/blue/coordination/processor/CoordinationRepositoryCompatibilityNodeProvider.java
deleted file mode 100644
index dbeff07..0000000
--- a/src/main/java/blue/coordination/processor/CoordinationRepositoryCompatibilityNodeProvider.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package blue.coordination.processor;
-
-import blue.language.NodeProvider;
-import blue.language.model.Node;
-import blue.language.provider.SequentialNodeProvider;
-import blue.repo.coordination.Compute;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-public final class CoordinationRepositoryCompatibilityNodeProvider implements NodeProvider {
- private final NodeProvider delegate;
-
- public CoordinationRepositoryCompatibilityNodeProvider(NodeProvider delegate) {
- if (delegate == null) {
- throw new IllegalArgumentException("delegate must not be null");
- }
- this.delegate = delegate;
- }
-
- public static boolean isInstalled(NodeProvider provider) {
- if (provider instanceof CoordinationRepositoryCompatibilityNodeProvider) {
- return true;
- }
- if (provider instanceof SequentialNodeProvider) {
- for (NodeProvider child : ((SequentialNodeProvider) provider).getNodeProviders()) {
- if (isInstalled(child)) {
- return true;
- }
- }
- }
- return false;
- }
-
- @Override
- public List fetchByBlueId(String blueId) {
- List nodes = delegate.fetchByBlueId(blueId);
- if (nodes == null || nodes.isEmpty() || !Compute.blueId().equals(baseBlueId(blueId))) {
- return nodes;
- }
- List compatible = new ArrayList(nodes.size());
- for (Node node : nodes) {
- compatible.add(sanitizeComputeDefinition(node));
- }
- return compatible;
- }
-
- private Node sanitizeComputeDefinition(Node node) {
- Node sanitized = node.clone();
- Map properties = sanitized.getProperties();
- if (properties == null || properties.isEmpty()) {
- return sanitized;
- }
- stripRuntimeDefault(properties, "emitEvents");
- stripRuntimeDefault(properties, "returnResult");
- return sanitized;
- }
-
- private void stripRuntimeDefault(Map properties, String key) {
- Node field = properties.get(key);
- if (field == null || !Boolean.TRUE.equals(field.getValue())) {
- return;
- }
- Node sanitized = field.clone();
- sanitized.value((Object) null);
- properties.put(key, sanitized);
- }
-
- private String baseBlueId(String blueId) {
- if (blueId == null) {
- return null;
- }
- int fragment = blueId.indexOf('#');
- return fragment >= 0 ? blueId.substring(0, fragment) : blueId;
- }
-}
diff --git a/src/main/java/blue/coordination/processor/OperationRequestMatcher.java b/src/main/java/blue/coordination/processor/OperationRequestMatcher.java
index 3c3be3a..e56d2c0 100644
--- a/src/main/java/blue/coordination/processor/OperationRequestMatcher.java
+++ b/src/main/java/blue/coordination/processor/OperationRequestMatcher.java
@@ -13,46 +13,40 @@ boolean matches(SequentialWorkflowOperation contract, HandlerMatchContext contex
if (!SequentialWorkflowEventMatcher.matches(contract.getEvent(), context)) {
return false;
}
- CoordinationEventNodes.OperationRequestView request =
- CoordinationEventNodes.operationRequest(context.event());
- if (request == null || !request.routable()) {
- return false;
- }
String operationKey = nonBlank(contract.getKey());
- if (operationKey == null || !operationKey.equals(request.operation())) {
- return false;
- }
- if (!request.channel().equals(context.channelKey())) {
- return false;
- }
- return requestMatches(contract.getRequest(), request, context);
- }
-
- private boolean requestMatches(Node requestPattern,
- CoordinationEventNodes.OperationRequestView request,
- HandlerMatchContext context) {
- if (requestPattern == null) {
- return true;
- }
- if (isEmptyRequestPattern(requestPattern)) {
- return true;
- }
- if (request.request() == null) {
+ String channelKey = nonBlank(context.channelKey());
+ if (operationKey == null || channelKey == null) {
return false;
}
- return context.matchesEventPattern(request.patternFor(requestPattern));
+ Node requestPattern = contract.getRequest();
+ return CoordinationEventNodes.matchesOperationRequest(
+ context.event(),
+ operationKey,
+ channelKey,
+ requestPattern == null
+ || isEmptyRequestPattern(requestPattern)
+ ? null
+ : requestPattern,
+ context);
}
private boolean isEmptyRequestPattern(Node requestPattern) {
- return requestPattern.getType() == null
+ return requestPattern.getName() == null
+ && requestPattern.getDescription() == null
+ && requestPattern.getType() == null
&& requestPattern.getItemType() == null
&& requestPattern.getKeyType() == null
&& requestPattern.getValueType() == null
&& requestPattern.getValue() == null
&& requestPattern.getItems() == null
&& (requestPattern.getProperties() == null || requestPattern.getProperties().isEmpty())
+ && requestPattern.getContracts() == null
&& requestPattern.getBlueId() == null
- && requestPattern.getSchema() == null;
+ && requestPattern.getSchema() == null
+ && requestPattern.getMergePolicy() == null
+ && requestPattern.getPreviousBlueId() == null
+ && requestPattern.getPosition() == null
+ && requestPattern.getBlue() == null;
}
private static String nonBlank(String value) {
diff --git a/src/main/java/blue/coordination/processor/OperationRequestRoutingFunctions.java b/src/main/java/blue/coordination/processor/OperationRequestRoutingFunctions.java
new file mode 100644
index 0000000..20196a7
--- /dev/null
+++ b/src/main/java/blue/coordination/processor/OperationRequestRoutingFunctions.java
@@ -0,0 +1,150 @@
+package blue.coordination.processor;
+
+import blue.language.model.Node;
+import blue.language.processor.ExternalChannelFunctionContext;
+import blue.language.processor.ExternalChannelMemberSnapshot;
+import blue.language.processor.model.ChannelContract;
+import blue.language.utils.BlueIdCalculator;
+import blue.repo.coordination.AllTimelinesChannel;
+import blue.repo.coordination.CompositeTimelineChannel;
+import blue.repo.coordination.TimelineChannel;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Shared immutable routing projection for Coordination Operation Requests.
+ */
+final class OperationRequestRoutingFunctions {
+ private static final String LOGICAL_DELIVERY_PREFIX =
+ "blue.coordination/1.0/operation-request:";
+
+ private OperationRequestRoutingFunctions() {
+ }
+
+ static void declareTargetChannelFamilies(
+ ChannelContract immutableContractSnapshot,
+ ExternalChannelFunctionContext context) {
+ for (String typeBlueId : targetTypeFamilies(
+ immutableContractSnapshot)) {
+ context.membersByEffectiveType(typeBlueId);
+ }
+ }
+
+ static String handlerChannelKey(
+ ChannelContract immutableContractSnapshot,
+ Node exactEvent,
+ ExternalChannelFunctionContext context) {
+ Route route = route(
+ immutableContractSnapshot,
+ exactEvent,
+ context);
+ return route != null
+ ? route.channel
+ : context.channelKey();
+ }
+
+ static Node payload(
+ Node exactEvent,
+ ExternalChannelFunctionContext context) {
+ return CoordinationEventNodes
+ .operationRequestRoutingPayload(
+ exactEvent, context);
+ }
+
+ static String logicalDeliveryKey(
+ ChannelContract immutableContractSnapshot,
+ Node exactEvent,
+ ExternalChannelFunctionContext context) {
+ Route route = route(
+ immutableContractSnapshot,
+ exactEvent,
+ context);
+ if (route == null) {
+ return context.channelKey();
+ }
+ Node identity = new Node()
+ .properties(
+ "operation",
+ new Node().value(
+ route.operation))
+ .properties(
+ "channel",
+ new Node().value(
+ route.channel));
+ return LOGICAL_DELIVERY_PREFIX
+ + BlueIdCalculator.calculateBlueId(identity);
+ }
+
+ private static Route route(
+ ChannelContract immutableContractSnapshot,
+ Node exactEvent,
+ ExternalChannelFunctionContext context) {
+ CoordinationEventNodes.OperationRequestView request =
+ CoordinationEventNodes.operationRequest(
+ exactEvent, context);
+ if (request == null
+ || !request.routable()
+ || !isChannelTarget(
+ immutableContractSnapshot,
+ request.channel(),
+ context)) {
+ return null;
+ }
+ return new Route(
+ request.operation(),
+ request.channel());
+ }
+
+ private static boolean isChannelTarget(
+ ChannelContract immutableContractSnapshot,
+ String targetKey,
+ ExternalChannelFunctionContext context) {
+ if (context.channelKey().equals(targetKey)) {
+ return true;
+ }
+ for (String typeBlueId : targetTypeFamilies(
+ immutableContractSnapshot)) {
+ List members =
+ context.membersByEffectiveType(
+ typeBlueId);
+ for (ExternalChannelMemberSnapshot member : members) {
+ if (member.channelKey().equals(targetKey)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private static Set targetTypeFamilies(
+ ChannelContract immutableContractSnapshot) {
+ Set typeBlueIds =
+ new LinkedHashSet();
+ if (immutableContractSnapshot != null
+ && immutableContractSnapshot.getTypeBlueId() != null
+ && !immutableContractSnapshot
+ .getTypeBlueId().isEmpty()) {
+ typeBlueIds.add(
+ immutableContractSnapshot
+ .getTypeBlueId());
+ }
+ typeBlueIds.add(TimelineChannel.blueId());
+ typeBlueIds.add(CompositeTimelineChannel.blueId());
+ typeBlueIds.add(AllTimelinesChannel.blueId());
+ return typeBlueIds;
+ }
+
+ private static final class Route {
+ private final String operation;
+ private final String channel;
+
+ private Route(
+ String operation,
+ String channel) {
+ this.operation = operation;
+ this.channel = channel;
+ }
+ }
+}
diff --git a/src/main/java/blue/coordination/processor/SequentialWorkflowOperationProcessor.java b/src/main/java/blue/coordination/processor/SequentialWorkflowOperationProcessor.java
index 45739d0..11f3d1d 100644
--- a/src/main/java/blue/coordination/processor/SequentialWorkflowOperationProcessor.java
+++ b/src/main/java/blue/coordination/processor/SequentialWorkflowOperationProcessor.java
@@ -7,6 +7,8 @@
import blue.language.processor.ProcessorExecutionContext;
import blue.repo.coordination.SequentialWorkflow;
import blue.repo.coordination.SequentialWorkflowOperation;
+import java.util.Collections;
+import java.util.List;
public final class SequentialWorkflowOperationProcessor implements HandlerProcessor {
private final SequentialWorkflowRunner runner;
@@ -28,6 +30,11 @@ public Class contractType() {
return SequentialWorkflowOperation.class;
}
+ @Override
+ public List executableBodyFields() {
+ return Collections.singletonList("steps");
+ }
+
@Override
public String deriveChannel(SequentialWorkflowOperation contract, HandlerRegistrationContext context) {
String channel = trimToNull(contract.getChannel());
diff --git a/src/main/java/blue/coordination/processor/SequentialWorkflowProcessor.java b/src/main/java/blue/coordination/processor/SequentialWorkflowProcessor.java
index 3033ffe..35eabf0 100644
--- a/src/main/java/blue/coordination/processor/SequentialWorkflowProcessor.java
+++ b/src/main/java/blue/coordination/processor/SequentialWorkflowProcessor.java
@@ -6,6 +6,8 @@
import blue.language.processor.HandlerRegistrationContext;
import blue.language.processor.ProcessorExecutionContext;
import blue.repo.coordination.SequentialWorkflow;
+import java.util.Collections;
+import java.util.List;
public final class SequentialWorkflowProcessor implements HandlerProcessor {
private final SequentialWorkflowRunner runner;
@@ -26,6 +28,11 @@ public Class contractType() {
return SequentialWorkflow.class;
}
+ @Override
+ public List executableBodyFields() {
+ return Collections.singletonList("steps");
+ }
+
@Override
public String deriveChannel(SequentialWorkflow contract, HandlerRegistrationContext context) {
return contract != null ? contract.getChannel() : null;
@@ -33,7 +40,13 @@ public String deriveChannel(SequentialWorkflow contract, HandlerRegistrationCont
@Override
public boolean matches(SequentialWorkflow contract, HandlerMatchContext context) {
- return SequentialWorkflowEventMatcher.matches(contract.getEvent(), context);
+ return !CoordinationEventNodes
+ .isRoutableOperationRequestForChannel(
+ context.event(),
+ context.channelKey(),
+ context)
+ && SequentialWorkflowEventMatcher.matches(
+ contract.getEvent(), context);
}
@Override
diff --git a/src/main/java/blue/coordination/processor/TimelineChannelProcessor.java b/src/main/java/blue/coordination/processor/TimelineChannelProcessor.java
index 0418ecf..e1414a4 100644
--- a/src/main/java/blue/coordination/processor/TimelineChannelProcessor.java
+++ b/src/main/java/blue/coordination/processor/TimelineChannelProcessor.java
@@ -4,6 +4,7 @@
import blue.language.processor.ChannelEvaluation;
import blue.language.processor.ChannelEvaluationContext;
import blue.language.processor.ChannelProcessor;
+import blue.language.processor.ExternalChannelSubscriptionFunctions;
import blue.repo.coordination.TimelineChannel;
public final class TimelineChannelProcessor implements ChannelProcessor {
@@ -12,6 +13,12 @@ public Class contractType() {
return TimelineChannel.class;
}
+ @Override
+ public ExternalChannelSubscriptionFunctions
+ externalSubscriptionFunctions() {
+ return TimelineExternalSubscriptionFunctions.INSTANCE;
+ }
+
@Override
public ChannelEvaluation evaluate(TimelineChannel contract, ChannelEvaluationContext context) {
return TimelineProviderSupport.evaluateTimelineEntry(contract, context);
@@ -23,7 +30,11 @@ public String eventId(TimelineChannel contract, ChannelEvaluationContext context
}
@Override
- public boolean isNewerEvent(TimelineChannel contract, ChannelCheckpointContext context) {
- return TimelineProviderSupport.isNewerOrSameTimelineEvent(context);
+ public boolean isNewerEvent(TimelineChannel contract,
+ ChannelCheckpointContext context) {
+ return TimelineProviderSupport.isNewerTimelineSubject(
+ context,
+ TimelineExternalSubscriptionFunctions
+ .TIMELINE_ORDER_SUBJECT_VERSION);
}
}
diff --git a/src/main/java/blue/coordination/processor/TimelineExternalSubscriptionFunctions.java b/src/main/java/blue/coordination/processor/TimelineExternalSubscriptionFunctions.java
new file mode 100644
index 0000000..1cc4c17
--- /dev/null
+++ b/src/main/java/blue/coordination/processor/TimelineExternalSubscriptionFunctions.java
@@ -0,0 +1,183 @@
+package blue.coordination.processor;
+
+import blue.language.model.Node;
+import blue.language.processor.ExternalChannelFunctionContext;
+import blue.language.processor.ExternalChannelSubscriptionFunctions;
+import blue.repo.coordination.TimelineChannel;
+import blue.repo.coordination.TimelineEntry;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Immutable Contracts 1.0 subscription functions for a Timeline Channel.
+ *
+ * The explicit Timeline Entry key is a conservative finite preselection
+ * key. Complete immutable Timeline and Actor matching is authoritative and
+ * uses the processor-owned verified pattern matcher so inline and pure
+ * reference representations are equivalent.
+ */
+final class TimelineExternalSubscriptionFunctions
+ implements ExternalChannelSubscriptionFunctions {
+
+ static final TimelineExternalSubscriptionFunctions INSTANCE =
+ new TimelineExternalSubscriptionFunctions();
+
+ static final String TIMELINE_ENTRY_KEY =
+ "blue.coordination/1.0/timeline-entry";
+ static final String TIMELINE_ORDER_SUBJECT_VERSION =
+ "blue.coordination/1.0/timeline-order-subject";
+
+ private TimelineExternalSubscriptionFunctions() {
+ }
+
+ @Override
+ public List channelKeys(TimelineChannel immutableContractSnapshot) {
+ if (immutableContractSnapshot == null
+ || immutableContractSnapshot.getTimeline() == null
+ || immutableContractSnapshot.getActor() == null) {
+ throw new IllegalArgumentException(
+ "Timeline Channel requires immutable timeline and actor headers");
+ }
+ return Collections.singletonList(TIMELINE_ENTRY_KEY);
+ }
+
+ @Override
+ public List channelKeys(
+ TimelineChannel immutableContractSnapshot,
+ ExternalChannelFunctionContext context) {
+ List keys =
+ channelKeys(immutableContractSnapshot);
+ OperationRequestRoutingFunctions
+ .declareTargetChannelFamilies(
+ immutableContractSnapshot,
+ context);
+ return keys;
+ }
+
+ @Override
+ public List eventKeys(Node exactEvent) {
+ return CoordinationEventNodes.isTimelineEntry(exactEvent)
+ ? Collections.singletonList(TIMELINE_ENTRY_KEY)
+ : Collections.emptyList();
+ }
+
+ @Override
+ public List eventKeys(
+ Node exactEvent,
+ ExternalChannelFunctionContext context) {
+ return CoordinationEventNodes.isTimelineEntry(
+ exactEvent, context)
+ ? Collections.singletonList(TIMELINE_ENTRY_KEY)
+ : Collections.emptyList();
+ }
+
+ @Override
+ public boolean accepts(TimelineChannel immutableContractSnapshot,
+ Node exactEvent) {
+ if (!eventKeys(exactEvent).contains(TIMELINE_ENTRY_KEY)) {
+ return false;
+ }
+ CoordinationEventNodes.TimelineEntryView entry =
+ CoordinationEventNodes.timelineEntry(exactEvent);
+ return TimelineProviderSupport.matchesTimelineAndActor(
+ immutableContractSnapshot, entry);
+ }
+
+ @Override
+ public boolean accepts(
+ TimelineChannel immutableContractSnapshot,
+ Node exactEvent,
+ ExternalChannelFunctionContext context) {
+ CoordinationEventNodes.TimelineEntryView entry =
+ CoordinationEventNodes.timelineEntry(
+ exactEvent, context);
+ return immutableContractSnapshot != null
+ && entry != null
+ && CoordinationEventNodes.matchesGeneratedBinding(
+ entry.timeline(),
+ immutableContractSnapshot.getTimeline(),
+ context)
+ && CoordinationEventNodes.matchesGeneratedBinding(
+ entry.actor(),
+ immutableContractSnapshot.getActor(),
+ context);
+ }
+
+ @Override
+ public Node payload(
+ TimelineChannel immutableContractSnapshot,
+ Node exactEvent,
+ ExternalChannelFunctionContext context) {
+ return OperationRequestRoutingFunctions
+ .payload(exactEvent, context);
+ }
+
+ @Override
+ public Node checkpointSubject(TimelineChannel immutableContractSnapshot,
+ Node exactEvent,
+ Node exactPayload) {
+ if (!accepts(
+ immutableContractSnapshot,
+ exactEvent)) {
+ throw new IllegalArgumentException(
+ "Timeline checkpoint subject requires an accepted "
+ + "Timeline Entry");
+ }
+ return TimelineProviderSupport.timelineOrderSubject(
+ CoordinationEventNodes.timelineEntry(exactEvent));
+ }
+
+ @Override
+ public Node checkpointSubject(
+ TimelineChannel immutableContractSnapshot,
+ Node exactEvent,
+ Node exactPayload,
+ ExternalChannelFunctionContext context) {
+ CoordinationEventNodes.TimelineEntryView entry =
+ CoordinationEventNodes.timelineEntryHeader(
+ exactEvent, context);
+ if (entry == null) {
+ throw new IllegalArgumentException(
+ "Timeline checkpoint subject requires an accepted "
+ + "Timeline Entry");
+ }
+ return TimelineProviderSupport.timelineOrderSubject(entry);
+ }
+
+ @Override
+ public String handlerChannelKey(
+ TimelineChannel immutableContractSnapshot,
+ Node exactEvent,
+ Node exactPayload,
+ ExternalChannelFunctionContext context) {
+ return OperationRequestRoutingFunctions
+ .handlerChannelKey(
+ immutableContractSnapshot,
+ exactEvent,
+ context);
+ }
+
+ @Override
+ public String logicalDeliveryKey(
+ TimelineChannel immutableContractSnapshot,
+ Node exactEvent,
+ Node exactPayload,
+ ExternalChannelFunctionContext context) {
+ return OperationRequestRoutingFunctions
+ .logicalDeliveryKey(
+ immutableContractSnapshot,
+ exactEvent,
+ context);
+ }
+
+ @Override
+ public String checkpointDomainDiscriminator(
+ TimelineChannel immutableContractSnapshot) {
+ channelKeys(immutableContractSnapshot);
+ return "coordination.timeline-entry:"
+ + TimelineEntry.blueId()
+ + "|subject="
+ + TIMELINE_ORDER_SUBJECT_VERSION;
+ }
+}
diff --git a/src/main/java/blue/coordination/processor/TimelineMemberSubscriptions.java b/src/main/java/blue/coordination/processor/TimelineMemberSubscriptions.java
new file mode 100644
index 0000000..7eb026f
--- /dev/null
+++ b/src/main/java/blue/coordination/processor/TimelineMemberSubscriptions.java
@@ -0,0 +1,135 @@
+package blue.coordination.processor;
+
+import blue.language.model.Node;
+import blue.language.processor.ExternalChannelFunctionContext;
+import blue.language.processor.ExternalChannelMemberEvaluation;
+import blue.language.processor.ExternalChannelMemberSnapshot;
+import blue.language.processor.ExternalOrderKey;
+import blue.repo.coordination.CompositeTimelineChannel;
+import blue.repo.coordination.TimelineChannel;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+final class TimelineMemberSubscriptions {
+ private TimelineMemberSubscriptions() {
+ }
+
+ static List compositeMembers(
+ CompositeTimelineChannel contract,
+ ExternalChannelFunctionContext context) {
+ if (contract == null
+ || contract.getChannels() == null
+ || contract.getChannels().isEmpty()) {
+ throw new IllegalArgumentException(
+ "Composite Timeline Channel requires at least one member");
+ }
+ Set uniqueKeys = new LinkedHashSet();
+ List members =
+ new ArrayList();
+ for (String key : contract.getChannels()) {
+ if (key == null || key.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Composite Timeline Channel member key must be "
+ + "non-empty Text");
+ }
+ if (!uniqueKeys.add(key)) {
+ continue;
+ }
+ ExternalChannelMemberSnapshot member =
+ context.member(key);
+ if (!TimelineChannel.blueId().equals(
+ member.effectiveTypeBlueId())) {
+ throw new IllegalArgumentException(
+ "Composite Timeline Channel member '" + key
+ + "' must have exact Timeline Channel "
+ + "runtime semantics");
+ }
+ members.add(member);
+ }
+ Collections.sort(members, MEMBER_ORDER);
+ return Collections.unmodifiableList(members);
+ }
+
+ static List allTimelineMembers(
+ ExternalChannelFunctionContext context) {
+ return context.membersByEffectiveType(
+ TimelineChannel.blueId());
+ }
+
+ static List unionChannelKeys(
+ List members) {
+ Set keys = new LinkedHashSet();
+ for (ExternalChannelMemberSnapshot member : members) {
+ keys.addAll(member.channelKeys());
+ }
+ return Collections.unmodifiableList(
+ new ArrayList(keys));
+ }
+
+ static WinningMember winning(
+ List members,
+ Node exactEvent) {
+ for (ExternalChannelMemberSnapshot member : members) {
+ ExternalChannelMemberEvaluation evaluation =
+ member.evaluate(exactEvent);
+ if (evaluation.accepts()) {
+ return new WinningMember(member, evaluation);
+ }
+ }
+ return null;
+ }
+
+ static List timelineEventKeys(
+ Node exactEvent,
+ ExternalChannelFunctionContext context) {
+ return TimelineExternalSubscriptionFunctions.INSTANCE
+ .eventKeys(exactEvent, context);
+ }
+
+ static final class WinningMember {
+ private final ExternalChannelMemberSnapshot member;
+ private final ExternalChannelMemberEvaluation evaluation;
+
+ private WinningMember(
+ ExternalChannelMemberSnapshot member,
+ ExternalChannelMemberEvaluation evaluation) {
+ this.member = member;
+ this.evaluation = evaluation;
+ }
+
+ ExternalChannelMemberSnapshot member() {
+ return member;
+ }
+
+ ExternalChannelMemberEvaluation evaluation() {
+ return evaluation;
+ }
+ }
+
+ private static final Comparator
+ MEMBER_ORDER =
+ new Comparator() {
+ @Override
+ public int compare(ExternalChannelMemberSnapshot left,
+ ExternalChannelMemberSnapshot right) {
+ int order = Integer.compare(
+ left.order(), right.order());
+ if (order != 0) {
+ return order;
+ }
+ int key = ExternalOrderKey.compareTextCodePoints(
+ left.channelKey(), right.channelKey());
+ if (key != 0) {
+ return key;
+ }
+ return ExternalOrderKey.compareTextCodePoints(
+ left.effectiveTypeBlueId(),
+ right.effectiveTypeBlueId());
+ }
+ };
+}
diff --git a/src/main/java/blue/coordination/processor/TimelineProviderSupport.java b/src/main/java/blue/coordination/processor/TimelineProviderSupport.java
index 83df9b2..c50d6c4 100644
--- a/src/main/java/blue/coordination/processor/TimelineProviderSupport.java
+++ b/src/main/java/blue/coordination/processor/TimelineProviderSupport.java
@@ -2,15 +2,12 @@
import blue.language.model.Node;
import blue.language.processor.ChannelCheckpointContext;
-import blue.language.processor.ChannelDelivery;
import blue.language.processor.ChannelEvaluation;
import blue.language.processor.ChannelEvaluationContext;
+import blue.language.processor.ExternalChannelMemberSnapshot;
import blue.language.utils.BlueIdCalculator;
-import blue.repo.coordination.OperationRequest;
import blue.repo.coordination.TimelineChannel;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
+import java.math.BigInteger;
public final class TimelineProviderSupport {
private TimelineProviderSupport() {
@@ -22,26 +19,11 @@ public static ChannelEvaluation evaluateTimelineEntry(TimelineChannel contract,
if (entry == null) {
return ChannelEvaluation.noMatch();
}
- if (!matchesTimelineAndActor(contract, entry) || !matchesEventFilter(contract, eventNode)) {
+ if (!TimelineExternalSubscriptionFunctions.INSTANCE
+ .accepts(contract, eventNode)) {
return ChannelEvaluation.noMatch();
}
- return acceptedTimelineEntry(eventNode, context);
- }
-
- private static ChannelEvaluation acceptedTimelineEntry(Node eventNode,
- ChannelEvaluationContext context) {
- CoordinationEventNodes.OperationRequestView request =
- CoordinationEventNodes.operationRequest(eventNode);
- if (request == null || !request.routable() || context.channel(request.channel()) == null) {
- return ChannelEvaluation.match(eventNode);
- }
- ChannelDelivery delivery = ChannelDelivery.of(eventNode,
- null,
- null,
- null,
- request.channel(),
- OperationRequest.blueId() + ":" + request.operation());
- return ChannelEvaluation.matchDeliveries(Collections.singletonList(delivery));
+ return ChannelEvaluation.match(eventNode, eventId(eventNode));
}
static boolean matchesTimelineAndActor(TimelineChannel contract,
@@ -54,37 +36,13 @@ static boolean matchesTimelineAndActor(TimelineChannel contract,
entry.actor(), contract.getActor());
}
- public static boolean matchesEventFilter(TimelineChannel contract, Node eventNode) {
- Node definition = contract.getDefinition();
- return definition == null || CoordinationEventNodes.matchesPattern(eventNode, definition);
- }
-
- static ChannelEvaluation preserveUnionDelivery(ChannelEvaluation childEvaluation,
- Node fallbackEvent,
- String metadataKey,
- String sourceChannelKey) {
- List childDeliveries = childEvaluation.deliveries();
- if (!childDeliveries.isEmpty()) {
- List unionDeliveries =
- new ArrayList(childDeliveries.size());
- for (ChannelDelivery childDelivery : childDeliveries) {
- unionDeliveries.add(ChannelDelivery.of(
- withSourceMetadata(childDelivery.event(), metadataKey, sourceChannelKey),
- childDelivery.eventId(),
- null,
- childDelivery.shouldProcess(),
- childDelivery.handlerChannelKey(),
- childDelivery.logicalDeliveryKey()));
- }
- return ChannelEvaluation.matchDeliveries(unionDeliveries);
- }
+ static ChannelEvaluation preserveUnionPayload(ChannelEvaluation childEvaluation,
+ Node fallbackEvent) {
Node deliveryEvent = childEvaluation.event() != null
? childEvaluation.event()
: fallbackEvent;
return deliveryEvent != null
- ? ChannelEvaluation.match(
- withSourceMetadata(deliveryEvent, metadataKey, sourceChannelKey),
- childEvaluation.eventId())
+ ? ChannelEvaluation.match(deliveryEvent, childEvaluation.eventId())
: ChannelEvaluation.noMatch();
}
@@ -92,27 +50,6 @@ public static String eventId(Node eventNode) {
return eventNode != null ? BlueIdCalculator.calculateBlueId(eventNode.clone().blue(null)) : null;
}
- private static Node withSourceMetadata(Node event,
- String metadataKey,
- String sourceChannelKey) {
- Node copy = event.clone();
- Node meta = property(copy, "meta");
- if (meta == null) {
- meta = new Node();
- copy.properties("meta", meta);
- }
- meta.properties(metadataKey, new Node().value(sourceChannelKey));
- return copy;
- }
-
- public static boolean isNewerOrSameTimelineEvent(ChannelCheckpointContext context) {
- return isNewerTimelineEvent(context, false);
- }
-
- public static boolean isNewerOrDifferentTimelineEvent(ChannelCheckpointContext context) {
- return isNewerTimelineEvent(context, true);
- }
-
public static Node property(Node node, String key) {
if (node == null || node.getProperties() == null) {
return null;
@@ -126,29 +63,145 @@ public static String textProperty(Node node, String key) {
return value instanceof String ? (String) value : null;
}
- private static boolean isNewerTimelineEvent(ChannelCheckpointContext context,
- boolean acceptDifferentTimeline) {
- CoordinationEventNodes.TimelineEntryView current =
- CoordinationEventNodes.timelineEntry(context.event());
- if (current == null) {
- return false;
+ static Node timelineOrderSubject(
+ CoordinationEventNodes.TimelineEntryView entry) {
+ if (entry == null) {
+ throw new IllegalArgumentException(
+ "Timeline order subject requires a Timeline Entry");
}
- Node previousEvent = context.lastEvent();
- if (previousEvent == null) {
- return true;
+ return new Node()
+ .properties("semantics",
+ new Node().value(
+ TimelineExternalSubscriptionFunctions
+ .TIMELINE_ORDER_SUBJECT_VERSION))
+ .properties("timestamp",
+ new Node().value(entry.timestamp()));
+ }
+
+ static Node memberTimelineOrderSubject(
+ String semantics,
+ ExternalChannelMemberSnapshot member,
+ Node exactMemberSubject) {
+ TimelineOrder memberOrder = timelineOrder(
+ exactMemberSubject,
+ TimelineExternalSubscriptionFunctions
+ .TIMELINE_ORDER_SUBJECT_VERSION);
+ if (memberOrder == null) {
+ throw new IllegalArgumentException(
+ "Timeline member order subject requires the selected "
+ + "member's exact Timeline subject");
+ }
+ return new Node()
+ .properties("semantics", new Node().value(semantics))
+ .properties("timestamp",
+ new Node().value(memberOrder.timestamp))
+ .properties("memberKey",
+ new Node().value(member.channelKey()))
+ .properties("memberDomain",
+ new Node().value(
+ member.checkpointDomainBlueId()));
+ }
+
+ static boolean isNewerTimelineSubject(
+ ChannelCheckpointContext context,
+ String expectedSemantics) {
+ TimelineOrder current = timelineOrder(
+ context.currentSubject(), expectedSemantics);
+ if (current == null) {
+ throw new IllegalArgumentException(
+ "Current Timeline checkpoint has no exact order "
+ + "subject");
}
if (context.eventSignature() != null
- && context.eventSignature().equals(context.lastEventSignature())) {
+ && context.eventSignature().equals(
+ context.lastEventSignature())) {
return false;
}
- CoordinationEventNodes.TimelineEntryView previous =
- CoordinationEventNodes.timelineEntry(previousEvent);
+ Node previousSubject = context.lastEvent();
+ if (previousSubject == null) {
+ return true;
+ }
+ TimelineOrder previous =
+ timelineOrder(previousSubject, expectedSemantics);
if (previous == null) {
- return false;
+ throw new IllegalArgumentException(
+ "Stored Timeline checkpoint subject is malformed");
+ }
+ if (current.memberKey == null) {
+ return current.timestamp.compareTo(
+ previous.timestamp) > 0;
+ }
+ boolean sameMember =
+ current.memberKey.equals(previous.memberKey)
+ && current.memberDomain.equals(
+ previous.memberDomain);
+ if (!sameMember) {
+ /*
+ * Composite and All Timelines preserve the established Timeline
+ * policy: each selected semantic member is an independent source.
+ * The generic feeder owns cross-source canonical ordering; this
+ * checkpoint only rejects replays/non-increasing timestamps from
+ * the same frozen member lineage.
+ */
+ return true;
+ }
+ return current.timestamp.compareTo(
+ previous.timestamp) > 0;
+ }
+
+ private static TimelineOrder timelineOrder(Node node,
+ String expectedSemantics) {
+ String semantics = textProperty(node, "semantics");
+ if (!expectedSemantics.equals(semantics)) {
+ return null;
}
- if (!BlueSemanticIdentity.equals(current.timeline(), previous.timeline())) {
- return acceptDifferentTimeline;
+ Node timestampNode = property(node, "timestamp");
+ Object rawTimestamp =
+ timestampNode != null
+ ? timestampNode.getValue()
+ : null;
+ BigInteger timestamp = integer(rawTimestamp);
+ String memberKey = textProperty(node, "memberKey");
+ String memberDomain = textProperty(node, "memberDomain");
+ boolean direct = TimelineExternalSubscriptionFunctions
+ .TIMELINE_ORDER_SUBJECT_VERSION.equals(
+ expectedSemantics);
+ if (timestamp == null
+ || direct && (memberKey != null || memberDomain != null)
+ || !direct && (memberKey == null
+ || memberKey.isEmpty()
+ || memberDomain == null
+ || memberDomain.isEmpty())) {
+ return null;
+ }
+ return new TimelineOrder(
+ timestamp, memberKey, memberDomain);
+ }
+
+ private static BigInteger integer(Object value) {
+ if (value instanceof BigInteger) {
+ return (BigInteger) value;
+ }
+ if (value instanceof Byte || value instanceof Short
+ || value instanceof Integer || value instanceof Long) {
+ return BigInteger.valueOf(
+ ((Number) value).longValue());
}
- return current.timestamp().compareTo(previous.timestamp()) > 0;
+ return null;
}
+
+ private static final class TimelineOrder {
+ private final BigInteger timestamp;
+ private final String memberKey;
+ private final String memberDomain;
+
+ private TimelineOrder(BigInteger timestamp,
+ String memberKey,
+ String memberDomain) {
+ this.timestamp = timestamp;
+ this.memberKey = memberKey;
+ this.memberDomain = memberDomain;
+ }
+ }
+
}
diff --git a/src/main/java/blue/coordination/processor/bex/BexWorkflowContextFactory.java b/src/main/java/blue/coordination/processor/bex/BexWorkflowContextFactory.java
index 245cae8..f14ddb0 100644
--- a/src/main/java/blue/coordination/processor/bex/BexWorkflowContextFactory.java
+++ b/src/main/java/blue/coordination/processor/bex/BexWorkflowContextFactory.java
@@ -67,17 +67,8 @@ public BexStepResults stepResults(Map workflowStepResults) {
}
public BexValue currentContractBinding(StepExecutionContext context) {
- BexValue base = context.currentContractFrozenNode() != null
+ return context.currentContractFrozenNode() != null
? BexValues.frozen(context.currentContractFrozenNode())
: BexValues.nodeCursorTrustedImmutable(context.currentContractNodeRef());
- String channel = context.workflow().getChannelKey();
- if (channel == null || channel.trim().isEmpty()) {
- return base;
- }
- BexValue existing = base.get("channel");
- if (!existing.isUndefined() && existing.isScalar() && !existing.asText().trim().isEmpty()) {
- return base;
- }
- return BexValues.overlay(base, "channel", BexValues.scalar(channel.trim()));
}
}
diff --git a/src/main/java/blue/coordination/processor/workflow/ComputeEffectPlan.java b/src/main/java/blue/coordination/processor/workflow/ComputeEffectPlan.java
index 758edd9..d2870ab 100644
--- a/src/main/java/blue/coordination/processor/workflow/ComputeEffectPlan.java
+++ b/src/main/java/blue/coordination/processor/workflow/ComputeEffectPlan.java
@@ -19,6 +19,7 @@ final class ComputeEffectPlan {
private final List patches;
private final List events;
private final boolean terminationRequested;
+ private final String terminationCause;
private final String terminationReason;
private final boolean changesetHandled;
private final AtomicBoolean bufferingClaimed = new AtomicBoolean();
@@ -26,6 +27,7 @@ final class ComputeEffectPlan {
ComputeEffectPlan(List patches,
List events,
boolean terminationRequested,
+ String terminationCause,
String terminationReason,
boolean changesetHandled) {
List frozenPatches = new ArrayList(patches.size());
@@ -46,7 +48,18 @@ final class ComputeEffectPlan {
frozenEvents.add(FrozenNode.fromResolvedNode(event));
}
this.events = Collections.unmodifiableList(frozenEvents);
+ if (terminationRequested
+ && (terminationCause == null || terminationCause.isEmpty())) {
+ throw new IllegalArgumentException(
+ "Compute termination cause must be non-empty Text");
+ }
+ if (!terminationRequested
+ && (terminationCause != null || terminationReason != null)) {
+ throw new IllegalArgumentException(
+ "Absent Compute termination cannot carry cause or reason");
+ }
this.terminationRequested = terminationRequested;
+ this.terminationCause = terminationCause;
this.terminationReason = terminationReason;
this.changesetHandled = changesetHandled;
}
@@ -63,6 +76,10 @@ boolean terminationRequested() {
return terminationRequested;
}
+ String terminationCause() {
+ return terminationCause;
+ }
+
String terminationReason() {
return terminationReason;
}
diff --git a/src/main/java/blue/coordination/processor/workflow/ComputeProgramNormalizer.java b/src/main/java/blue/coordination/processor/workflow/ComputeProgramNormalizer.java
index b8e208e..65b9709 100644
--- a/src/main/java/blue/coordination/processor/workflow/ComputeProgramNormalizer.java
+++ b/src/main/java/blue/coordination/processor/workflow/ComputeProgramNormalizer.java
@@ -1,6 +1,5 @@
package blue.coordination.processor.workflow;
-import blue.coordination.processor.RepositoryTypeAliasPreprocessor;
import blue.coordination.processor.bex.BexProcessingMetrics;
import blue.language.model.Node;
import blue.language.snapshot.FrozenNode;
@@ -10,29 +9,15 @@
final class ComputeProgramNormalizer {
private static final String NORMALIZATION_VERSION =
- "compute-program-v2|repository-aliases-3.0.0-rc.10";
+ "compute-program-v3|exact-registered-types";
- private final RepositoryTypeAliasPreprocessor typeAliasPreprocessor;
private final BexProcessingMetrics metrics;
ComputeProgramNormalizer() {
- this(new RepositoryTypeAliasPreprocessor(), null);
+ this(null);
}
ComputeProgramNormalizer(BexProcessingMetrics metrics) {
- this(new RepositoryTypeAliasPreprocessor(), metrics);
- }
-
- ComputeProgramNormalizer(RepositoryTypeAliasPreprocessor typeAliasPreprocessor) {
- this(typeAliasPreprocessor, null);
- }
-
- private ComputeProgramNormalizer(RepositoryTypeAliasPreprocessor typeAliasPreprocessor,
- BexProcessingMetrics metrics) {
- if (typeAliasPreprocessor == null) {
- throw new IllegalArgumentException("typeAliasPreprocessor must not be null");
- }
- this.typeAliasPreprocessor = typeAliasPreprocessor;
this.metrics = metrics;
}
@@ -42,10 +27,9 @@ String normalizationVersion() {
/**
* Normalizes only the authored Compute projection of a frozen step. This
- * avoids materializing unrelated resolved-contract content. The selected
- * subtrees still use the mutable alias preprocessor as a conservative,
- * semantics-preserving cold-path fallback; the resulting frozen plan is
- * reused on every warm invocation.
+ * avoids materializing unrelated resolved-contract content. Registered
+ * type identities are preserved exactly; no runtime alias rewriting is
+ * applied. The resulting frozen plan is reused on every warm invocation.
*/
FrozenNode program(FrozenNode stepNode) {
if (metrics != null) {
@@ -78,7 +62,7 @@ Node program(Node stepNode) {
if (!properties.isEmpty()) {
program.properties(properties);
}
- return typeAliasPreprocessor.preprocess(program);
+ return program;
}
Node definition(Node definitionNode) {
@@ -90,7 +74,7 @@ Node definition(Node definitionNode) {
if (!properties.isEmpty()) {
definition.properties(properties);
}
- return typeAliasPreprocessor.preprocess(definition);
+ return definition;
}
private Node frozenProgramInput(FrozenNode source) {
@@ -193,10 +177,7 @@ private void putIfMeaningful(Map properties, String key, Node valu
}
private boolean hasAuthoredContent(Node node) {
- return node != null
- && (node.getValue() != null
- || (node.getItems() != null && !node.getItems().isEmpty())
- || (node.getProperties() != null && !node.getProperties().isEmpty()));
+ return !NodeUtil.isEmpty(node);
}
private void copyMetadata(Node target, Node source) {
diff --git a/src/main/java/blue/coordination/processor/workflow/ComputeResultEmitter.java b/src/main/java/blue/coordination/processor/workflow/ComputeResultEmitter.java
index 433aca1..057006f 100644
--- a/src/main/java/blue/coordination/processor/workflow/ComputeResultEmitter.java
+++ b/src/main/java/blue/coordination/processor/workflow/ComputeResultEmitter.java
@@ -52,6 +52,7 @@ ComputeEffectPlan plan(BexExecutionResult result,
return new ComputeEffectPlan(patches,
events,
termination.requested,
+ termination.cause,
termination.reason,
returnedChangeset || !patches.isEmpty());
} catch (ComputeResultValidationException ex) {
@@ -88,7 +89,9 @@ void buffer(ComputeEffectPlan plan, StepExecutionContext context) {
}
}
if (plan.terminationRequested()) {
- context.processorContext().terminateGracefully(plan.terminationReason());
+ context.processorContext().terminate(
+ plan.terminationCause(),
+ plan.terminationReason());
if (metrics != null) {
metrics.incrementSuccessfulComputeTerminationRequests();
}
@@ -117,9 +120,6 @@ private List eventNodes(BexExecutionResult result) {
if (event == null || event.isUndefined() || event.isNull()) {
throw invalid("Compute result events cannot contain undefined/null entries");
}
- if (!event.isObject()) {
- throw invalid("Compute result events must contain object entries");
- }
try {
converted.add(BexNodeWriter.toNode(event));
} catch (RuntimeException ex) {
@@ -141,18 +141,25 @@ private Termination termination(BexExecutionResult result) {
throw invalid("Compute result termination must be an object");
}
for (String key : termination.keys()) {
- if (!"reason".equals(key)) {
+ if (!"cause".equals(key) && !"reason".equals(key)) {
throw invalid("Compute result termination contains unsupported properties");
}
}
+ BexValue cause = termination.get("cause");
+ if (cause == null || cause.isUndefined() || cause.isNull()
+ || !"text".equals(BexValues.kind(cause))
+ || cause.asText().isEmpty()) {
+ throw invalid(
+ "Compute result termination cause must be non-empty Text");
+ }
BexValue reason = termination.get("reason");
if (reason == null || reason.isUndefined() || reason.isNull()) {
- return Termination.requested(null);
+ return Termination.requested(cause.asText(), null);
}
if (!"text".equals(BexValues.kind(reason))) {
throw invalid("Compute result termination reason must be Text");
}
- return Termination.requested(reason.asText());
+ return Termination.requested(cause.asText(), reason.asText());
}
private List changesetPatches(BexExecutionResult result,
@@ -209,18 +216,23 @@ private WorkflowPatchEntry patchEntry(BexValue item, int index) {
if (item == null || item.isUndefined() || item.isNull() || !item.isObject()) {
throw invalid("Compute result changeset entry " + index + " must be an object");
}
- String op = textValue(item.get("op"));
- String path = textValue(item.get("path"));
+ String op = patchTextValue(item.get("op"), index, "op");
+ String path = patchTextValue(item.get("path"), index, "path");
if (!"add".equals(op) && !"replace".equals(op) && !"remove".equals(op)) {
throw invalid("Invalid patch op in Compute result changeset");
}
- if (path == null || path.trim().isEmpty()) {
+ if (path == null || path.isEmpty()) {
throw invalid("Compute result changeset entry " + index + " missing path");
}
FrozenNode nodeValue = null;
- if (!"remove".equals(op)) {
- BexValue val = item.get("val");
- if (val.isUndefined()) {
+ BexValue val = item.get("val");
+ if ("remove".equals(op)) {
+ if (item.keys().contains("val")) {
+ throw invalid("Compute result changeset entry " + index
+ + " val must be absent for remove");
+ }
+ } else {
+ if (val == null || val.isUndefined()) {
throw invalid("Compute result changeset entry " + index + " missing val");
}
nodeValue = freezePatchValue(val);
@@ -228,14 +240,26 @@ private WorkflowPatchEntry patchEntry(BexValue item, int index) {
return new WorkflowPatchEntry(op, path, nodeValue);
}
+ private String patchTextValue(BexValue value,
+ int index,
+ String field) {
+ if (value == null || value.isUndefined() || value.isNull()) {
+ return null;
+ }
+ if (!"text".equals(BexValues.kind(value))) {
+ throw invalid("Compute result changeset entry " + index
+ + " field '" + field + "' must be Text");
+ }
+ return value.asText();
+ }
+
private FrozenJsonPatch toPatch(WorkflowPatchEntry entry,
StepExecutionContext context) {
- String normalizedOp = entry.op().trim().toLowerCase();
String path = resolvedPointer(entry.path(), context);
- if ("remove".equals(normalizedOp)) {
+ if ("remove".equals(entry.op())) {
return FrozenJsonPatch.remove(path);
}
- if ("add".equals(normalizedOp)) {
+ if ("add".equals(entry.op())) {
return FrozenJsonPatch.add(path, entry.val());
}
// patchEntry has already restricted this branch to replace.
@@ -247,8 +271,14 @@ private FrozenJsonPatch toPatch(BexPatchEntry entry,
if (entry == null) {
throw invalid("Compute result accumulated patch is incomplete");
}
- String normalizedOp = entry.op().trim().toLowerCase();
- boolean remove = "remove".equals(normalizedOp);
+ String op = entry.op();
+ boolean remove = "remove".equals(op);
+ if (!remove && !"add".equals(op) && !"replace".equals(op)) {
+ throw invalid("Invalid accumulated patch op in Compute result");
+ }
+ if (remove && entry.val() != null && !entry.val().isUndefined()) {
+ throw invalid("Compute result accumulated remove patch val must be absent");
+ }
if (!remove && (entry.val() == null || entry.val().isUndefined())) {
throw invalid("Compute result patch value is required");
}
@@ -257,7 +287,7 @@ private FrozenJsonPatch toPatch(BexPatchEntry entry,
return FrozenJsonPatch.remove(path);
}
FrozenNode value = freezePatchValue(entry.val());
- if ("add".equals(normalizedOp)) {
+ if ("add".equals(op)) {
return FrozenJsonPatch.add(path, value);
}
// BexPatchEntry has already restricted this branch to replace.
@@ -284,13 +314,10 @@ private void applyPatches(List patches,
if (preview == null) {
return;
}
- if (metrics != null) {
- metrics.addMetric("frozenPatchesHandedToLanguage", patches.size());
- metrics.addMetric("frozenPatchValuesHandedToLanguage", frozenValueCount);
- }
context.processorContext().applyPreviewedFrozenPatches(patches, preview);
previewTransferred = true;
if (metrics != null) {
+ metrics.addMetric("frozenPatchesHandedToLanguage", patches.size());
metrics.addMetric("frozenPatchValuesHandedToLanguage", frozenValueCount);
}
applied = true;
@@ -340,7 +367,7 @@ private boolean isAccumulatedChangesetValue(BexValue value, BexChangeset changes
}
BexValue val = item.get("val");
if (entry.val() == null || entry.val().isUndefined()) {
- if (val != null && !val.isUndefined() && !val.isNull()) {
+ if (item.keys().contains("val")) {
return false;
}
} else if (val == null || val.isUndefined()) {
@@ -415,13 +442,18 @@ private String boundedDetail(RuntimeException exception) {
}
private static final class Termination {
- private static final Termination ABSENT = new Termination(false, null);
+ private static final Termination ABSENT =
+ new Termination(false, null, null);
private final boolean requested;
+ private final String cause;
private final String reason;
- private Termination(boolean requested, String reason) {
+ private Termination(boolean requested,
+ String cause,
+ String reason) {
this.requested = requested;
+ this.cause = cause;
this.reason = reason;
}
@@ -429,8 +461,9 @@ private static Termination absent() {
return ABSENT;
}
- private static Termination requested(String reason) {
- return new Termination(true, reason);
+ private static Termination requested(String cause,
+ String reason) {
+ return new Termination(true, cause, reason);
}
}
}
diff --git a/src/main/java/blue/coordination/processor/workflow/ComputeStepExecutor.java b/src/main/java/blue/coordination/processor/workflow/ComputeStepExecutor.java
index 168be75..216bac0 100644
--- a/src/main/java/blue/coordination/processor/workflow/ComputeStepExecutor.java
+++ b/src/main/java/blue/coordination/processor/workflow/ComputeStepExecutor.java
@@ -82,6 +82,13 @@ public WorkflowStepResult execute(Compute step, StepExecutionContext context) {
if (metrics != null) {
metrics.incrementComputeStepsExecuted();
}
+ if (!supportsManifestBoundRuntimeCounters()) {
+ context.processorContext().throwFatal(
+ "Compute runtime capability is unavailable: "
+ + "blue-bex-java 1.1 does not expose "
+ + "manifest-bound named runtime counters");
+ return WorkflowStepResult.none();
+ }
FrozenNode rawStepNode = context.stepFrozenNode();
if (rawStepNode == null) {
Node mutableStepNode = context.stepNodeRef();
@@ -126,9 +133,6 @@ public ComputeProgramPlan create() {
metrics.addComputeCompileExecuteNanos(System.nanoTime() - executeStart);
metrics.addBexMetrics(result.metrics());
}
- if (result.gasUsed() > 0L) {
- context.processorContext().consumeGas(result.gasUsed());
- }
ComputeEffectPlan effectPlan = resultEmitter.plan(result,
context,
computePlan.emitEvents());
@@ -179,6 +183,16 @@ private long computeGasLimit(FrozenNode stepNode) {
return parsed.longValue();
}
+ /**
+ * The Contracts 1.0 child-ledger API cannot accept BEX's legacy aggregate
+ * {@code gasUsed()} value. Keep the evaluator fail-closed until the runtime
+ * supplies a closed, manifest-bound named counter stream that can be
+ * admitted live by the parent ledger.
+ */
+ private boolean supportsManifestBoundRuntimeCounters() {
+ return false;
+ }
+
/** Clears reusable Compute plans while keeping this executor usable. */
public void clearPlanCache() {
planCache.clear();
diff --git a/src/main/java/blue/coordination/processor/workflow/FrozenNodeUtil.java b/src/main/java/blue/coordination/processor/workflow/FrozenNodeUtil.java
index c26f818..96d996a 100644
--- a/src/main/java/blue/coordination/processor/workflow/FrozenNodeUtil.java
+++ b/src/main/java/blue/coordination/processor/workflow/FrozenNodeUtil.java
@@ -13,9 +13,22 @@ static FrozenNode property(FrozenNode node, String key) {
}
static boolean isEmpty(FrozenNode node) {
- return node == null || (node.getValue() == null
- && (node.getItems() == null || node.getItems().isEmpty())
- && (node.getProperties() == null || node.getProperties().isEmpty()));
+ return node == null || (node.getName() == null
+ && node.getDescription() == null
+ && node.getType() == null
+ && node.getItemType() == null
+ && node.getKeyType() == null
+ && node.getValueType() == null
+ && node.getValue() == null
+ && node.getItems() == null
+ && (node.getProperties() == null || node.getProperties().isEmpty())
+ && node.getContracts() == null
+ && node.getReferenceBlueId() == null
+ && node.getSchema() == null
+ && node.getMergePolicy() == null
+ && node.getPreviousBlueId() == null
+ && node.getPosition() == null
+ && node.getBlue() == null);
}
static Object rawScalar(FrozenNode node) {
@@ -33,7 +46,13 @@ static Object rawScalar(FrozenNode node) {
static String text(FrozenNode node) {
Object raw = rawScalar(node);
- return raw != null ? String.valueOf(raw) : null;
+ if (raw == null) {
+ return null;
+ }
+ if (!(raw instanceof String)) {
+ throw new IllegalArgumentException("Expected Text scalar");
+ }
+ return (String) raw;
}
static String textProperty(FrozenNode node, String key) {
@@ -48,27 +67,21 @@ static boolean booleanProperty(FrozenNode node, String key, boolean defaultValue
if (raw instanceof Boolean) {
return ((Boolean) raw).booleanValue();
}
- if (raw instanceof String) {
- return Boolean.parseBoolean((String) raw);
- }
- return defaultValue;
+ throw new IllegalArgumentException("Expected Boolean scalar for " + key);
}
static Long integer(FrozenNode node) {
Object raw = rawScalar(node);
if (raw instanceof BigInteger) {
- return Long.valueOf(((BigInteger) raw).longValue());
+ return Long.valueOf(((BigInteger) raw).longValueExact());
}
- if (raw instanceof Number) {
+ if (raw instanceof Byte || raw instanceof Short
+ || raw instanceof Integer || raw instanceof Long) {
return Long.valueOf(((Number) raw).longValue());
}
- if (raw instanceof String) {
- try {
- return Long.valueOf((String) raw);
- } catch (NumberFormatException ignored) {
- return null;
- }
+ if (raw == null) {
+ return null;
}
- return null;
+ throw new IllegalArgumentException("Expected Integer scalar");
}
}
diff --git a/src/main/java/blue/coordination/processor/workflow/NodeUtil.java b/src/main/java/blue/coordination/processor/workflow/NodeUtil.java
index 1b1a9e2..5589236 100644
--- a/src/main/java/blue/coordination/processor/workflow/NodeUtil.java
+++ b/src/main/java/blue/coordination/processor/workflow/NodeUtil.java
@@ -17,9 +17,22 @@ static Node property(Node node, String key) {
static boolean isEmpty(Node node) {
return node == null
- || (node.getValue() == null
- && empty(node.getItems())
- && empty(node.getProperties()));
+ || (node.getName() == null
+ && node.getDescription() == null
+ && node.getType() == null
+ && node.getItemType() == null
+ && node.getKeyType() == null
+ && node.getValueType() == null
+ && node.getValue() == null
+ && node.getItems() == null
+ && empty(node.getProperties())
+ && node.getContracts() == null
+ && node.getBlueId() == null
+ && node.getSchema() == null
+ && node.getMergePolicy() == null
+ && node.getPreviousBlueId() == null
+ && node.getPosition() == null
+ && node.getBlue() == null);
}
static Object rawScalar(Node node) {
@@ -37,7 +50,13 @@ static Object rawScalar(Node node) {
static String text(Node node) {
Object raw = rawScalar(node);
- return raw != null ? String.valueOf(raw) : null;
+ if (raw == null) {
+ return null;
+ }
+ if (!(raw instanceof String)) {
+ throw new IllegalArgumentException("Expected Text scalar");
+ }
+ return (String) raw;
}
static String textProperty(Node node, String key) {
@@ -52,17 +71,11 @@ static boolean booleanProperty(Node node, String key, boolean defaultValue) {
if (raw instanceof Boolean) {
return ((Boolean) raw).booleanValue();
}
- if (raw instanceof String) {
- return Boolean.parseBoolean((String) raw);
- }
- return defaultValue;
+ throw new IllegalArgumentException("Expected Boolean scalar for " + key);
}
private static boolean empty(Map, ?> map) {
return map == null || map.isEmpty();
}
- private static boolean empty(Iterable> items) {
- return items == null || !items.iterator().hasNext();
- }
}
diff --git a/src/main/java/blue/coordination/processor/workflow/SequentialWorkflowPlan.java b/src/main/java/blue/coordination/processor/workflow/SequentialWorkflowPlan.java
index 0295de4..656405a 100644
--- a/src/main/java/blue/coordination/processor/workflow/SequentialWorkflowPlan.java
+++ b/src/main/java/blue/coordination/processor/workflow/SequentialWorkflowPlan.java
@@ -8,18 +8,15 @@
import blue.repo.coordination.TriggerEvent;
import blue.repo.coordination.UpdateDocument;
-import java.math.BigDecimal;
-import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.IdentityHashMap;
import java.util.List;
-import java.util.Map;
/** Immutable execution structure for one exact frozen workflow contract. */
final class SequentialWorkflowPlan {
private static final long PLAN_BASE_BYTES = 128L;
private static final long STEP_PLAN_BYTES = 96L;
+ private static final long RETAINED_EXACT_STEP_BYTES = 96L;
private final FrozenNode.ResolvedStructuralKey contractIdentity;
private final List steps;
@@ -135,17 +132,22 @@ private static String stepName(SequentialWorkflowStep step) {
}
private static long estimateWeight(FrozenNode contractNode, List steps) {
- WeightEstimator identityEstimator = new WeightEstimator();
- WeightEstimator retainedStepEstimator = new WeightEstimator();
- long weight = PLAN_BASE_BYTES;
- // The exact structural key retains representation data comparable to
- // one traversal of the frozen graph.
- weight = saturatedAdd(weight, identityEstimator.node(contractNode));
+ /*
+ * The cache key already owns the exact structural identity and each
+ * step retains an exact frozen node. Weight bookkeeping is deliberately
+ * shallow: workflow planning must not recursively traverse exact
+ * Update/Trigger payloads merely to estimate their size.
+ */
+ long weight = PLAN_BASE_BYTES
+ + (contractNode != null ? RETAINED_EXACT_STEP_BYTES : 0L);
for (StepPlan step : steps) {
weight = saturatedAdd(weight, STEP_PLAN_BYTES);
- weight = saturatedAdd(weight, retainedStepEstimator.string(step.key));
- weight = saturatedAdd(weight, retainedStepEstimator.string(step.kind));
- weight = saturatedAdd(weight, retainedStepEstimator.node(step.frozenStep));
+ weight = saturatedAdd(weight, stringWeight(step.key));
+ weight = saturatedAdd(weight, stringWeight(step.kind));
+ if (step.frozenStep != null) {
+ weight = saturatedAdd(
+ weight, RETAINED_EXACT_STEP_BYTES);
+ }
if (step.staticUpdatePlan != null) {
weight = saturatedAdd(weight, step.staticUpdatePlan.approximateWeightBytes());
}
@@ -160,6 +162,12 @@ private static long saturatedAdd(long left, long right) {
return left + right;
}
+ private static long stringWeight(String value) {
+ return value == null
+ ? 0L
+ : 40L + 2L * value.length();
+ }
+
static final class StepPlan {
private final int index;
private final String key;
@@ -221,63 +229,4 @@ StaticUpdatePlan staticUpdatePlan() {
}
}
- private static final class WeightEstimator {
- private final IdentityHashMap visited = new IdentityHashMap();
-
- private long node(FrozenNode node) {
- if (node == null || visited.put(node, Boolean.TRUE) != null) {
- return 0L;
- }
- long weight = 160L;
- weight = saturatedAdd(weight, string(node.getName()));
- weight = saturatedAdd(weight, string(node.getDescription()));
- weight = saturatedAdd(weight, string(node.getReferenceBlueId()));
- weight = saturatedAdd(weight, string(node.getMergePolicy()));
- weight = saturatedAdd(weight, string(node.getPreviousBlueId()));
- weight = saturatedAdd(weight, scalar(node.getValue()));
- weight = saturatedAdd(weight, node(node.getType()));
- weight = saturatedAdd(weight, node(node.getItemType()));
- weight = saturatedAdd(weight, node(node.getKeyType()));
- weight = saturatedAdd(weight, node(node.getValueType()));
- weight = saturatedAdd(weight, node(node.getContracts()));
- weight = saturatedAdd(weight, node(node.getBlue()));
- if (node.getSchema() != null) {
- weight = saturatedAdd(weight, 256L);
- }
- if (node.getItems() != null) {
- weight = saturatedAdd(weight, 24L + 8L * node.getItems().size());
- for (FrozenNode item : node.getItems()) {
- weight = saturatedAdd(weight, node(item));
- }
- }
- if (node.getProperties() != null) {
- weight = saturatedAdd(weight, 48L + 48L * node.getProperties().size());
- for (Map.Entry entry : node.getProperties().entrySet()) {
- weight = saturatedAdd(weight, string(entry.getKey()));
- weight = saturatedAdd(weight, node(entry.getValue()));
- }
- }
- return weight;
- }
-
- private long string(String value) {
- return value == null ? 0L : 40L + 2L * value.length();
- }
-
- private long scalar(Object value) {
- if (value == null) {
- return 0L;
- }
- if (value instanceof String) {
- return string((String) value);
- }
- if (value instanceof BigInteger) {
- return 48L + ((BigInteger) value).bitLength() / 8L;
- }
- if (value instanceof BigDecimal) {
- return 64L;
- }
- return 24L;
- }
- }
}
diff --git a/src/main/java/blue/coordination/processor/workflow/SequentialWorkflowRunner.java b/src/main/java/blue/coordination/processor/workflow/SequentialWorkflowRunner.java
index 510259a..1918141 100644
--- a/src/main/java/blue/coordination/processor/workflow/SequentialWorkflowRunner.java
+++ b/src/main/java/blue/coordination/processor/workflow/SequentialWorkflowRunner.java
@@ -243,12 +243,7 @@ public void close() {
}
private FrozenNode rawContractNode(ProcessorExecutionContext context) {
- String pointer = contractPointer(context);
- if (pointer == null) {
- return context.frozenContractNode();
- }
- FrozenNode frozen = context.canonicalFrozenAt(pointer);
- return frozen != null ? frozen : context.frozenContractNode();
+ return context.frozenContractNode();
}
private WorkingDocument rootWorkingDocument(ProcessorExecutionContext context) {
@@ -263,21 +258,4 @@ private WorkingDocument rootWorkingDocument(ProcessorExecutionContext context) {
return workingDocument;
}
- private String contractPointer(ProcessorExecutionContext context) {
- String key = context.contractKey();
- if (key == null || key.trim().isEmpty()) {
- return null;
- }
- String scope = context.scopePath();
- String contracts = appendPointer(scope == null || scope.trim().isEmpty() ? "/" : scope, "contracts");
- return appendPointer(contracts, key.trim());
- }
-
- private String appendPointer(String parent, String segment) {
- String escaped = segment.replace("~", "~0").replace("/", "~1");
- if (parent == null || parent.isEmpty() || "/".equals(parent)) {
- return "/" + escaped;
- }
- return parent + "/" + escaped;
- }
}
diff --git a/src/main/java/blue/coordination/processor/workflow/StaticPayloadValidator.java b/src/main/java/blue/coordination/processor/workflow/StaticPayloadValidator.java
deleted file mode 100644
index 31bad7d..0000000
--- a/src/main/java/blue/coordination/processor/workflow/StaticPayloadValidator.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package blue.coordination.processor.workflow;
-
-import blue.language.snapshot.FrozenNode;
-
-import java.util.Map;
-
-final class StaticPayloadValidator {
- private StaticPayloadValidator() {
- }
-
- static boolean rejectBexOperators(FrozenNode node, StepExecutionContext context, String fieldName) {
- String path = firstBexOperatorPath(node, "");
- if (path == null) {
- return false;
- }
- context.processorContext().throwFatal(fieldName + " must be static; BEX operator object is not allowed at " + path);
- return true;
- }
-
- static String firstBexOperatorPath(FrozenNode node, String path) {
- if (node == null) {
- return null;
- }
- Map properties = node.getProperties();
- if (properties != null) {
- if (properties.size() == 1) {
- String key = properties.keySet().iterator().next();
- if (key != null && key.startsWith("$")) {
- return path.isEmpty() ? "/" : path;
- }
- }
- for (Map.Entry entry : properties.entrySet()) {
- String found = firstBexOperatorPath(entry.getValue(), path + "/" + entry.getKey());
- if (found != null) {
- return found;
- }
- }
- }
- if (node.getItems() != null) {
- for (int i = 0; i < node.getItems().size(); i++) {
- String found = firstBexOperatorPath(node.getItems().get(i), path + "/" + i);
- if (found != null) {
- return found;
- }
- }
- }
- return null;
- }
-}
diff --git a/src/main/java/blue/coordination/processor/workflow/StaticUpdatePlan.java b/src/main/java/blue/coordination/processor/workflow/StaticUpdatePlan.java
index 002bc63..12baaef 100644
--- a/src/main/java/blue/coordination/processor/workflow/StaticUpdatePlan.java
+++ b/src/main/java/blue/coordination/processor/workflow/StaticUpdatePlan.java
@@ -5,11 +5,10 @@
import blue.language.processor.model.FrozenJsonPatch;
import blue.language.processor.model.JsonPatch;
import blue.language.snapshot.FrozenNode;
-import blue.language.utils.MergeReverser;
+import blue.language.utils.MinimizedOverlayBuilder;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
@@ -22,6 +21,8 @@
* execution.
*/
final class StaticUpdatePlan {
+ private static final long RETAINED_EXACT_VALUE_BYTES = 96L;
+
private final List patches;
private final String validationFailure;
private final long approximateWeightBytes;
@@ -39,22 +40,17 @@ static StaticUpdatePlan compile(FrozenNode changeset) {
}
static StaticUpdatePlan compile(FrozenNode changeset, BexProcessingMetrics metrics) {
- String bexPath = StaticPayloadValidator.firstBexOperatorPath(changeset, "");
- if (bexPath != null) {
- return invalid("Update Document changeset must be static; BEX operator object is not allowed at "
- + bexPath);
- }
if (changeset == null || changeset.getItems() == null) {
return invalid("Update Document changeset must be a static patch list");
}
List templates = new ArrayList(changeset.getItems().size());
- MergeReverser mergeReverser = new MergeReverser();
+ MinimizedOverlayBuilder overlayBuilder = new MinimizedOverlayBuilder();
long weight = 96L;
for (int index = 0; index < changeset.getItems().size(); index++) {
FrozenNode item = changeset.getItems().get(index);
boolean resolvedConstruction = item != null && !item.isStrictCanonical();
if (resolvedConstruction) {
- Node authoredItem = mergeReverser.reverseToMinimizedOverlay(item.toNode());
+ Node authoredItem = overlayBuilder.build(item.toNode());
item = authoredItem != null ? FrozenNode.fromNode(authoredItem) : null;
}
Map properties = item != null ? item.getProperties() : null;
@@ -72,25 +68,29 @@ static StaticUpdatePlan compile(FrozenNode changeset, BexProcessingMetrics metri
return invalid("Update Document changeset entry " + index
+ " field 'path' must be text");
}
- if (op.value == null || op.value.trim().isEmpty()) {
+ if (op.value == null || op.value.isEmpty()) {
return invalid("Update Document patch operation is required");
}
- if (path.value == null || path.value.trim().isEmpty()) {
+ if (path.value == null || path.value.isEmpty()) {
return invalid("Update Document patch path is required");
}
- String normalizedOp = op.value.trim().toLowerCase(java.util.Locale.ROOT);
JsonPatch.Op patchOp;
- if ("add".equals(normalizedOp)) {
+ if ("add".equals(op.value)) {
patchOp = JsonPatch.Op.ADD;
- } else if ("replace".equals(normalizedOp)) {
+ } else if ("replace".equals(op.value)) {
patchOp = JsonPatch.Op.REPLACE;
- } else if ("remove".equals(normalizedOp)) {
+ } else if ("remove".equals(op.value)) {
patchOp = JsonPatch.Op.REMOVE;
} else {
return invalid("Unsupported Update Document patch operation: " + op.value);
}
FrozenNode value = null;
- if (patchOp != JsonPatch.Op.REMOVE) {
+ if (patchOp == JsonPatch.Op.REMOVE) {
+ if (properties.containsKey("val")) {
+ return invalid(
+ "Update Document patch value must be absent for remove");
+ }
+ } else {
value = properties.get("val");
if (value == null) {
return invalid("Update Document patch value is required for operation: "
@@ -108,7 +108,12 @@ static StaticUpdatePlan compile(FrozenNode changeset, BexProcessingMetrics metri
}
}
templates.add(new PatchTemplate(patchOp, path.value, value));
- weight += 72L + stringWeight(path.value) + frozenWeight(value);
+ /*
+ * Exact admitted values are retained by identity. Cache
+ * bookkeeping must not recursively walk or charge their payload.
+ */
+ weight += 72L + stringWeight(path.value)
+ + (value != null ? RETAINED_EXACT_VALUE_BYTES : 0L);
}
return new StaticUpdatePlan(templates, null, weight);
}
@@ -150,43 +155,6 @@ private static long stringWeight(String value) {
return value != null ? 40L + (long) value.length() * 2L : 0L;
}
- private static long frozenWeight(FrozenNode node) {
- return frozenWeight(node, new IdentityHashMap());
- }
-
- private static long frozenWeight(FrozenNode node,
- IdentityHashMap visited) {
- if (node == null || visited.put(node, Boolean.TRUE) != null) {
- return 0L;
- }
- long weight = 96L
- + stringWeight(node.getName())
- + stringWeight(node.getDescription())
- + stringWeight(node.getReferenceBlueId());
- if (node.getValue() instanceof String) {
- weight += stringWeight((String) node.getValue());
- } else if (node.getValue() != null) {
- weight += 32L;
- }
- if (node.getItems() != null) {
- weight += 16L + (long) node.getItems().size() * 8L;
- for (FrozenNode item : node.getItems()) {
- weight += frozenWeight(item, visited);
- }
- }
- if (node.getProperties() != null) {
- weight += 32L + (long) node.getProperties().size() * 40L;
- for (Map.Entry entry : node.getProperties().entrySet()) {
- weight += stringWeight(entry.getKey()) + frozenWeight(entry.getValue(), visited);
- }
- }
- weight += frozenWeight(node.getType(), visited);
- weight += frozenWeight(node.getItemType(), visited);
- weight += frozenWeight(node.getKeyType(), visited);
- weight += frozenWeight(node.getValueType(), visited);
- return weight;
- }
-
static final class PatchTemplate {
private final JsonPatch.Op op;
private final String authoredPath;
diff --git a/src/main/java/blue/coordination/processor/workflow/TerminateProcessingStepExecutor.java b/src/main/java/blue/coordination/processor/workflow/TerminateProcessingStepExecutor.java
index e92fade..0e4f54a 100644
--- a/src/main/java/blue/coordination/processor/workflow/TerminateProcessingStepExecutor.java
+++ b/src/main/java/blue/coordination/processor/workflow/TerminateProcessingStepExecutor.java
@@ -1,10 +1,11 @@
package blue.coordination.processor.workflow;
import blue.coordination.processor.bex.BexProcessingMetrics;
+import blue.language.snapshot.FrozenNode;
import blue.repo.coordination.SequentialWorkflowStep;
import blue.repo.coordination.TerminateProcessing;
-/** Buffers a graceful current-scope termination request. */
+/** Buffers an application-caused successful current-scope termination request. */
public final class TerminateProcessingStepExecutor implements WorkflowStepExecutor {
private final BexProcessingMetrics metrics;
@@ -23,7 +24,23 @@ public boolean supports(SequentialWorkflowStep step) {
@Override
public WorkflowStepResult execute(TerminateProcessing step, StepExecutionContext context) {
- context.processorContext().terminateGracefully(step.getReason());
+ FrozenNode rawStep = context.stepFrozenNode();
+ String cause;
+ String reason;
+ try {
+ cause = FrozenNodeUtil.textProperty(rawStep, "cause");
+ reason = FrozenNodeUtil.textProperty(rawStep, "reason");
+ } catch (IllegalArgumentException exception) {
+ context.processorContext().throwFatal(
+ "Terminate Processing cause and reason must be Text");
+ return WorkflowStepResult.none();
+ }
+ if (cause == null || cause.isEmpty()) {
+ context.processorContext().throwFatal(
+ "Terminate Processing cause must be non-empty Text");
+ return WorkflowStepResult.none();
+ }
+ context.processorContext().terminate(cause, reason);
if (metrics != null) {
metrics.incrementDeclarativeTerminationSteps();
}
diff --git a/src/main/java/blue/coordination/processor/workflow/TriggerEventStepExecutor.java b/src/main/java/blue/coordination/processor/workflow/TriggerEventStepExecutor.java
index 18cc471..fb1e8cb 100644
--- a/src/main/java/blue/coordination/processor/workflow/TriggerEventStepExecutor.java
+++ b/src/main/java/blue/coordination/processor/workflow/TriggerEventStepExecutor.java
@@ -6,8 +6,6 @@
import blue.repo.coordination.SequentialWorkflowStep;
import blue.repo.coordination.TriggerEvent;
-import java.util.Map;
-
public final class TriggerEventStepExecutor implements WorkflowStepExecutor {
private final BexProcessingMetrics metrics;
@@ -35,23 +33,31 @@ public WorkflowStepResult execute(TriggerEvent step, StepExecutionContext contex
if (metrics != null) {
metrics.incrementTriggerEventStepsExecuted();
}
- FrozenNode rawEvent = FrozenNodeUtil.property(context.stepFrozenNode(), "event");
- if (!hasDeclaredEvent(context.stepFrozenNode())) {
- context.processorContext().throwFatal("Trigger Event step must declare event payload");
- return WorkflowStepResult.none();
- }
- if (StaticPayloadValidator.rejectBexOperators(rawEvent,
- context,
- "Trigger Event event")) {
- return WorkflowStepResult.none();
- }
- Node event = step.getEvent();
- if (isEmpty(event)) {
+ FrozenNode rawStep = context.stepFrozenNode();
+ Node event;
+ if (rawStep != null) {
+ if (rawStep.getProperties() == null
+ || !rawStep.getProperties().containsKey("event")) {
+ context.processorContext().throwFatal(
+ "Trigger Event step must declare event payload");
+ return WorkflowStepResult.none();
+ }
+ FrozenNode rawEvent =
+ rawStep.getProperties().get("event");
+ if (rawEvent == null) {
+ context.processorContext().throwFatal(
+ "Trigger Event step must declare event payload");
+ return WorkflowStepResult.none();
+ }
+ event = rawEvent.toNode();
+ } else if (step.getEvent() != null) {
+ event = step.getEvent().clone();
+ } else {
context.processorContext().throwFatal("Trigger Event step must declare event payload");
return WorkflowStepResult.none();
}
long emitStart = System.nanoTime();
- context.processorContext().emitEvent(event.clone());
+ context.processorContext().emitEvent(event);
if (metrics != null) {
metrics.addTriggerEmitEventNanos(System.nanoTime() - emitStart);
}
@@ -63,50 +69,4 @@ public WorkflowStepResult execute(TriggerEvent step, StepExecutionContext contex
}
}
- private static boolean hasDeclaredEvent(Node stepNode) {
- if (stepNode == null) {
- return true;
- }
- if (stepNode.getProperties() == null || !stepNode.getProperties().containsKey("event")) {
- return false;
- }
- return !isEmpty(stepNode.getProperties().get("event"));
- }
-
- private static boolean hasDeclaredEvent(FrozenNode stepNode) {
- if (stepNode == null) {
- return true;
- }
- if (stepNode.getProperties() == null || !stepNode.getProperties().containsKey("event")) {
- return false;
- }
- return !FrozenNodeUtil.isEmpty(stepNode.getProperties().get("event"));
- }
-
- private static boolean isEmpty(Node node) {
- if (node == null) {
- return true;
- }
- return node.getType() == null
- && node.getItemType() == null
- && node.getKeyType() == null
- && node.getValueType() == null
- && node.getValue() == null
- && empty(node.getItems())
- && empty(node.getProperties())
- && node.getBlueId() == null
- && node.getSchema() == null
- && node.getMergePolicy() == null
- && node.getPreviousBlueId() == null
- && node.getPosition() == null
- && node.getBlue() == null;
- }
-
- private static boolean empty(Map, ?> map) {
- return map == null || map.isEmpty();
- }
-
- private static boolean empty(Iterable> items) {
- return items == null || !items.iterator().hasNext();
- }
}
diff --git a/src/main/java/blue/coordination/processor/workflow/UpdateDocumentStepExecutor.java b/src/main/java/blue/coordination/processor/workflow/UpdateDocumentStepExecutor.java
index 976923a..ba996e9 100644
--- a/src/main/java/blue/coordination/processor/workflow/UpdateDocumentStepExecutor.java
+++ b/src/main/java/blue/coordination/processor/workflow/UpdateDocumentStepExecutor.java
@@ -5,13 +5,11 @@
import blue.language.processor.WorkingDocument;
import blue.language.processor.model.FrozenJsonPatch;
import blue.language.snapshot.FrozenNode;
-import blue.language.utils.MergeReverser;
+import blue.language.utils.MinimizedOverlayBuilder;
import blue.repo.coordination.SequentialWorkflowStep;
import blue.repo.coordination.UpdateDocument;
-import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
-import java.util.Locale;
public final class UpdateDocumentStepExecutor implements WorkflowStepExecutor {
private final BexProcessingMetrics metrics;
@@ -45,11 +43,6 @@ public WorkflowStepResult execute(UpdateDocument step, StepExecutionContext cont
return WorkflowStepResult.none();
}
FrozenNode rawFrozenChangeset = FrozenNodeUtil.property(context.stepFrozenNode(), "changeset");
- if (StaticPayloadValidator.rejectBexOperators(rawFrozenChangeset,
- context,
- "Update Document changeset")) {
- return WorkflowStepResult.none();
- }
if (rawFrozenChangeset != null
&& rawFrozenChangeset.getItems() == null
&& step.getChangeset() == null) {
@@ -82,12 +75,12 @@ private List literalChangeset(UpdateDocument step, StepExecu
if (frozenChangeset != null && frozenChangeset.getItems() != null) {
List entries =
new ArrayList(frozenChangeset.getItems().size());
- MergeReverser mergeReverser = new MergeReverser();
+ MinimizedOverlayBuilder overlayBuilder = new MinimizedOverlayBuilder();
for (int i = 0; i < frozenChangeset.getItems().size(); i++) {
FrozenNode item = frozenChangeset.getItems().get(i);
Node literal = item == null ? null : item.toNode();
if (item != null && !item.isStrictCanonical()) {
- literal = mergeReverser.reverseToMinimizedOverlay(literal);
+ literal = overlayBuilder.build(literal);
}
entries.add(literalPatchEntry(literal, i, context));
}
@@ -96,7 +89,7 @@ private List literalChangeset(UpdateDocument step, StepExecu
if (step == null || step.getChangeset() == null) {
return java.util.Collections.emptyList();
}
- List> rawChangeset = step.getChangeset();
+ List rawChangeset = step.getChangeset();
List entries = new ArrayList(rawChangeset.size());
for (int i = 0; i < rawChangeset.size(); i++) {
entries.add(literalPatchEntry(rawChangeset.get(i), i, context));
@@ -104,47 +97,10 @@ private List literalChangeset(UpdateDocument step, StepExecu
return entries;
}
- private WorkflowPatchEntry literalPatchEntry(Object item, int index, StepExecutionContext context) {
+ private WorkflowPatchEntry literalPatchEntry(Node item, int index, StepExecutionContext context) {
if (item == null) {
return null;
}
- if (item instanceof WorkflowPatchEntry) {
- return (WorkflowPatchEntry) item;
- }
- if (item instanceof Node) {
- return literalPatchEntry((Node) item, index, context);
- }
- try {
- if (metrics != null) {
- metrics.incrementUpdateReflectionFallbacks();
- }
- String op = (String) invokeNoArg(item, "getOp");
- String path = (String) invokeNoArg(item, "getPath");
- if (isRemove(op)) {
- return new WorkflowPatchEntry(op, path, (FrozenNode) null);
- }
- Object val = invokeNoArg(item, "getVal");
- if (val == null || val instanceof Node) {
- return new WorkflowPatchEntry(op, path, (Node) val);
- }
- if (val instanceof FrozenNode) {
- return new WorkflowPatchEntry(op, path, (FrozenNode) val);
- }
- context.processorContext().throwFatal("Update Document changeset entry " + index
- + " field 'val' must be a node");
- return null;
- } catch (ReflectiveOperationException ex) {
- context.processorContext().throwFatal("Update Document changeset entry " + index
- + " cannot be read as a patch entry: " + ex.getMessage());
- return null;
- } catch (ClassCastException ex) {
- context.processorContext().throwFatal("Update Document changeset entry " + index
- + " has invalid patch entry field types");
- return null;
- }
- }
-
- private WorkflowPatchEntry literalPatchEntry(Node item, int index, StepExecutionContext context) {
if (item.getProperties() == null) {
context.processorContext().throwFatal("Update Document changeset entry " + index
+ " must be a static patch object");
@@ -152,7 +108,13 @@ private WorkflowPatchEntry literalPatchEntry(Node item, int index, StepExecution
}
String op = stringProperty(item, "op", index, context);
String path = stringProperty(item, "path", index, context);
- Node val = isRemove(op) ? null : item.getProperties().get("val");
+ if ("remove".equals(op)
+ && item.getProperties().containsKey("val")) {
+ context.processorContext().throwFatal(
+ "Update Document patch value must be absent for remove");
+ return null;
+ }
+ Node val = item.getProperties().get("val");
return new WorkflowPatchEntry(op, path, val);
}
@@ -170,11 +132,6 @@ private String stringProperty(Node item, String key, int index, StepExecutionCon
return (String) value;
}
- private Object invokeNoArg(Object target, String methodName) throws ReflectiveOperationException {
- Method method = target.getClass().getMethod(methodName);
- return method.invoke(target);
- }
-
private FrozenJsonPatch toPatch(WorkflowPatchEntry entry, StepExecutionContext context) {
if (entry == null) {
context.processorContext().throwFatal("Update Document changeset contains a null patch entry");
@@ -182,36 +139,35 @@ private FrozenJsonPatch toPatch(WorkflowPatchEntry entry, StepExecutionContext c
}
String op = entry.op();
String path = entry.path();
- if (op == null || op.trim().isEmpty()) {
+ if (op == null || op.isEmpty()) {
context.processorContext().throwFatal("Update Document patch operation is required");
return null;
}
- if (path == null || path.trim().isEmpty()) {
+ if (path == null || path.isEmpty()) {
context.processorContext().throwFatal("Update Document patch path is required");
return null;
}
String absolutePath = context.processorContext().resolvePointer(path);
- String normalizedOp = op.trim().toLowerCase(Locale.ROOT);
- if ("remove".equals(normalizedOp)) {
+ if ("remove".equals(op)) {
return FrozenJsonPatch.remove(absolutePath);
}
+ if (!"add".equals(op) && !"replace".equals(op)) {
+ context.processorContext().throwFatal(
+ "Unsupported Update Document patch operation: " + op);
+ return null;
+ }
FrozenNode value = entry.val();
if (value == null) {
context.processorContext().throwFatal("Update Document patch value is required for operation: " + op);
return null;
}
- if ("add".equals(normalizedOp)) {
+ if ("add".equals(op)) {
return FrozenJsonPatch.add(absolutePath, value);
}
- if ("replace".equals(normalizedOp)) {
+ if ("replace".equals(op)) {
return FrozenJsonPatch.replace(absolutePath, value);
}
- context.processorContext().throwFatal("Unsupported Update Document patch operation: " + op);
- return null;
- }
-
- private static boolean isRemove(String op) {
- return op != null && "remove".equals(op.trim().toLowerCase(Locale.ROOT));
+ throw new IllegalStateException("Unreachable Update Document patch operation");
}
private void applyPatches(List patches, StepExecutionContext context) {
@@ -227,9 +183,6 @@ private void applyPatches(List patches, StepExecutionContext co
if (preview == null) {
return;
}
- if (metrics != null) {
- metrics.addMetric("frozenPatchValuesHandedToLanguage", valuePatchCount(patches));
- }
context.processorContext().applyPreviewedFrozenPatches(patches, preview);
previewTransferred = true;
if (metrics != null) {
diff --git a/src/main/java/blue/coordination/processor/workflow/WorkflowPatchEntry.java b/src/main/java/blue/coordination/processor/workflow/WorkflowPatchEntry.java
index 4dc9c82..ac97cef 100644
--- a/src/main/java/blue/coordination/processor/workflow/WorkflowPatchEntry.java
+++ b/src/main/java/blue/coordination/processor/workflow/WorkflowPatchEntry.java
@@ -3,8 +3,6 @@
import blue.language.model.Node;
import blue.language.snapshot.FrozenNode;
-import java.util.Locale;
-
final class WorkflowPatchEntry {
private final String op;
private final String path;
@@ -13,13 +11,13 @@ final class WorkflowPatchEntry {
WorkflowPatchEntry(String op, String path, Node val) {
this.op = op;
this.path = path;
- this.val = isRemove(op) || val == null ? null : FrozenNode.fromNode(val);
+ this.val = val != null ? FrozenNode.fromNode(val) : null;
}
WorkflowPatchEntry(String op, String path, FrozenNode val) {
this.op = op;
this.path = path;
- this.val = isRemove(op) ? null : canonicalSnapshot(val);
+ this.val = canonicalSnapshot(val);
}
String op() {
@@ -41,7 +39,4 @@ private static FrozenNode canonicalSnapshot(FrozenNode value) {
return FrozenNode.fromNode(value.toNode());
}
- private static boolean isRemove(String op) {
- return op != null && "remove".equals(op.trim().toLowerCase(Locale.ROOT));
- }
}
diff --git a/src/test/java/blue/coordination/processor/AllTimelinesChannelProcessorTest.java b/src/test/java/blue/coordination/processor/AllTimelinesChannelProcessorTest.java
index 4489099..17c1ab5 100644
--- a/src/test/java/blue/coordination/processor/AllTimelinesChannelProcessorTest.java
+++ b/src/test/java/blue/coordination/processor/AllTimelinesChannelProcessorTest.java
@@ -24,7 +24,7 @@ void allTimelinesWithSeveralMatchingChildrenDeliversOnce() {
Fixture fixture = configuredFixture();
Map contracts = matchingChildren();
contracts.put("all", allTimelines());
- contracts.put("handler", sourceReportingHandler());
+ contracts.put("handler", fixedHandler("union"));
Node initialized = initializedDocument(fixture, contracts);
DocumentProcessingResult result = process(fixture,
@@ -34,8 +34,11 @@ void allTimelinesWithSeveralMatchingChildrenDeliversOnce() {
10,
"hello");
- assertChatCount(result.triggeredEvents(), "childA", 1);
- assertNotNull(checkpoint(result.document(), "all"));
+ assertChatCount(result.events(), "union", 1);
+ assertAllCheckpointSubject(
+ checkpoint(result.document(), "all"),
+ BigInteger.TEN,
+ "childA");
assertNull(checkpoint(result.document(), "all::childA"));
assertNull(checkpoint(result.document(), "all::childB"));
}
@@ -46,7 +49,7 @@ void allTimelinesMatchingChildSelectionUsesOrderThenKey() {
Map ordered = matchingChildren();
ordered.get("childB").properties("order", new Node().value(-1));
ordered.put("all", allTimelines());
- ordered.put("handler", sourceReportingHandler());
+ ordered.put("handler", fixedHandler("union"));
DocumentProcessingResult orderWinner = process(fixture,
initializedDocument(fixture, ordered),
@@ -55,14 +58,18 @@ void allTimelinesMatchingChildSelectionUsesOrderThenKey() {
1,
"order");
- assertChatCount(orderWinner.triggeredEvents(), "childB", 1);
+ assertChatCount(orderWinner.events(), "union", 1);
+ assertAllCheckpointSubject(
+ checkpoint(orderWinner.document(), "all"),
+ BigInteger.ONE,
+ "childB");
Fixture keyFixture = configuredFixture();
Map tied = new LinkedHashMap();
tied.put("childB", TestTimelineProvider.channel(TIMELINE, ACTOR));
tied.put("childA", TestTimelineProvider.channel(TIMELINE, ACTOR));
tied.put("all", allTimelines());
- tied.put("handler", sourceReportingHandler());
+ tied.put("handler", fixedHandler("union"));
DocumentProcessingResult keyWinner = process(keyFixture,
initializedDocument(keyFixture, tied),
@@ -71,7 +78,11 @@ void allTimelinesMatchingChildSelectionUsesOrderThenKey() {
1,
"key");
- assertChatCount(keyWinner.triggeredEvents(), "childA", 1);
+ assertChatCount(keyWinner.events(), "union", 1);
+ assertAllCheckpointSubject(
+ checkpoint(keyWinner.document(), "all"),
+ BigInteger.ONE,
+ "childA");
}
@Test
@@ -96,11 +107,16 @@ void allTimelinesAcceptsEqualTimestampFromDifferentTimeline() {
100,
"bob");
- assertEquals("bob-timeline",
- checkpoint(bob.document(), "all").getAsText("/timeline/timelineId"));
- assertEquals(BigInteger.valueOf(100), checkpoint(bob.document(), "all").get("/timestamp"));
- assertEquals(BigInteger.valueOf(100), checkpoint(bob.document(), "alice").get("/timestamp"));
- assertEquals(BigInteger.valueOf(100), checkpoint(bob.document(), "bob").get("/timestamp"));
+ assertAllCheckpointSubject(
+ checkpoint(bob.document(), "all"),
+ BigInteger.valueOf(100),
+ "bob");
+ assertDirectCheckpointSubject(
+ checkpoint(bob.document(), "alice"),
+ BigInteger.valueOf(100));
+ assertDirectCheckpointSubject(
+ checkpoint(bob.document(), "bob"),
+ BigInteger.valueOf(100));
}
@Test
@@ -121,6 +137,29 @@ void allTimelinesRejectsEntryThatMatchesNoDeclaredTimelineChannel() {
assertNull(checkpoint(result.document(), "all"));
}
+ @Test
+ void allTimelinesWithNoTimelineMembersAcceptsNothing() {
+ Fixture fixture = configuredFixture();
+ Map contracts = new LinkedHashMap();
+ contracts.put("all", allTimelines());
+ Node initialized = initializedDocument(fixture, contracts);
+
+ DocumentProcessingResult result = process(
+ fixture,
+ initialized,
+ TIMELINE,
+ ACTOR,
+ 1,
+ "unmatched");
+
+ assertEquals(
+ ProcessorStatus.SUCCESS,
+ result.status(),
+ blue.coordination.processor.ProcessingResultTestSupport
+ .diagnosticMessage(result));
+ assertNull(checkpoint(result.document(), "all"));
+ }
+
private static Map matchingChildren() {
Map contracts = new LinkedHashMap();
contracts.put("childA", TestTimelineProvider.channel(TIMELINE, ACTOR));
@@ -132,21 +171,12 @@ private static Node allTimelines() {
return new Node().type("Coordination/All Timelines Channel");
}
- private static Node sourceReportingHandler() {
- return handler(new Node().properties("$binding",
- new Node().value("event/meta/allTimelinesSourceChannelKey")));
- }
-
- private static Node handler(Node message) {
- Node append = new Node().properties("$appendEvent",
- new Node().properties("$merge", new Node().items(
- new Node().properties("type", new Node().value("Coordination/Chat Message")),
- new Node().properties("message", message))));
+ private static Node fixedHandler(String message) {
Node step = new Node()
- .type("Coordination/Compute")
- .properties("do", new Node().items(
- append,
- new Node().properties("$return", new Node().value(true))));
+ .type("Coordination/Trigger Event")
+ .properties(
+ "event",
+ TestTimelineProvider.chatMessage(message));
return new Node()
.type("Coordination/Sequential Workflow")
.properties("channel", new Node().value("all"))
@@ -159,7 +189,7 @@ private static Node initializedDocument(Fixture fixture, Map contr
.name("All Timelines V2 Test")
.properties("contracts", new Node().properties(contracts));
DocumentProcessingResult initialized = fixture.blue.initializeDocument(fixture.blue.preprocess(document));
- assertEquals(ProcessorStatus.SUCCESS, initialized.status(), initialized.failureReason());
+ assertEquals(ProcessorStatus.SUCCESS, initialized.status(), blue.coordination.processor.ProcessingResultTestSupport.diagnosticMessage(initialized));
return initialized.document();
}
@@ -188,12 +218,46 @@ private static Node event(Fixture fixture,
private static Node checkpoint(Node document, String key) {
try {
- return document.getAsNode("/contracts/checkpoint/lastEvents/" + key);
+ return document.getAsNode(
+ "/contracts/checkpoint/entries/"
+ + escapePointerSegment(key)
+ + "/subject");
} catch (IllegalArgumentException ex) {
return null;
}
}
+ private static String escapePointerSegment(String value) {
+ return value.replace("~", "~0").replace("/", "~1");
+ }
+
+ private static void assertAllCheckpointSubject(
+ Node subject,
+ BigInteger timestamp,
+ String memberKey) {
+ assertNotNull(subject);
+ assertEquals(4, subject.getProperties().size());
+ assertEquals(
+ AllTimelinesExternalSubscriptionFunctions
+ .ORDER_SUBJECT_VERSION,
+ subject.getAsText("/semantics"));
+ assertEquals(timestamp, subject.get("/timestamp"));
+ assertEquals(memberKey, subject.getAsText("/memberKey"));
+ assertNotNull(subject.getAsText("/memberDomain"));
+ }
+
+ private static void assertDirectCheckpointSubject(
+ Node subject,
+ BigInteger timestamp) {
+ assertNotNull(subject);
+ assertEquals(2, subject.getProperties().size());
+ assertEquals(
+ TimelineExternalSubscriptionFunctions
+ .TIMELINE_ORDER_SUBJECT_VERSION,
+ subject.getAsText("/semantics"));
+ assertEquals(timestamp, subject.get("/timestamp"));
+ }
+
private static void assertChatCount(List events, String message, int expected) {
int count = 0;
for (Node event : events) {
diff --git a/src/test/java/blue/coordination/processor/BootstrapDocumentTransportRoundTripTest.java b/src/test/java/blue/coordination/processor/BootstrapDocumentTransportRoundTripTest.java
index 89a4048..6c32196 100644
--- a/src/test/java/blue/coordination/processor/BootstrapDocumentTransportRoundTripTest.java
+++ b/src/test/java/blue/coordination/processor/BootstrapDocumentTransportRoundTripTest.java
@@ -4,7 +4,7 @@
import blue.language.model.Node;
import blue.language.processor.DocumentProcessingResult;
import blue.language.snapshot.ResolvedSnapshot;
-import blue.language.utils.MergeReverser;
+import blue.language.utils.MinimizedOverlayBuilder;
import blue.language.utils.NodeToMapListOrValue;
import blue.repo.BlueRepository;
import org.junit.jupiter.api.Test;
@@ -28,12 +28,13 @@ void initializedBootstrapDocumentRoundTripsThroughMinimizedTransportAcrossFreshR
ResolvedSnapshot authored = writer.resolveToSnapshot(source);
DocumentProcessingResult initialization = writer.initializeDocument(authored);
- ResolvedSnapshot initialized = initialization.snapshot();
+ ResolvedSnapshot initialized =
+ ProcessingResultTestSupport.snapshot(writer, initialization);
assertNotNull(initialized.resolvedRoot().getAsNode(
"/contracts/declineBootstrap/request/type/type/inResponseTo/type/requestId"),
"cold resolution must fully materialize nested inherited Request metadata");
- Node minimized = new MergeReverser().reverseToMinimizedOverlay(
+ Node minimized = new MinimizedOverlayBuilder().build(
initialized.resolvedRoot());
assertFalse(minimized.getContracts().getProperties().containsKey("declineBootstrap"),
"the minimized overlay must omit type-derived bootstrap operations");
@@ -54,7 +55,9 @@ void initializedBootstrapDocumentRoundTripsThroughMinimizedTransportAcrossFreshR
}
private static Blue configured(BlueRepository repository) {
- Blue blue = repository.configure(new Blue());
+ Blue blue = new Blue()
+ .nodeProvider(repository.nodeProvider())
+ .typeClassResolver(repository.typeClassResolver());
CoordinationProcessors.registerWith(blue);
return blue;
}
diff --git a/src/test/java/blue/coordination/processor/CompositeTimelineChannelProcessorTest.java b/src/test/java/blue/coordination/processor/CompositeTimelineChannelProcessorTest.java
index 4ad16a9..33f967f 100644
--- a/src/test/java/blue/coordination/processor/CompositeTimelineChannelProcessorTest.java
+++ b/src/test/java/blue/coordination/processor/CompositeTimelineChannelProcessorTest.java
@@ -2,14 +2,11 @@
import blue.language.Blue;
import blue.language.model.Node;
-import blue.language.processor.ChannelCheckpointContext;
import blue.language.processor.ChannelEvaluation;
import blue.language.processor.ChannelEvaluationContext;
import blue.language.processor.DocumentProcessingResult;
import blue.language.processor.ProcessorStatus;
import blue.language.processor.model.ChannelContract;
-import blue.language.processor.model.ChannelEventCheckpoint;
-import blue.language.processor.model.MarkerContract;
import blue.language.processor.ChannelEvaluationContextFactory;
import blue.repo.BlueRepository;
import blue.repo.coordination.ChatMessage;
@@ -40,29 +37,31 @@ void compositeWithSeveralMatchingChildrenDeliversOnce() {
Fixture fixture = configuredFixture();
Map contracts = matchingChildren();
contracts.put("inbox", composite("childB", "childA", "childA"));
- contracts.put("handler", sourceReportingHandler("inbox", "compositeSourceChannelKey"));
+ contracts.put("handler", fixedHandler("inbox", "union"));
Node initialized = initializedDocument(fixture, contracts);
DocumentProcessingResult result = process(fixture, initialized, 10, "hello");
- assertChatCount(result.triggeredEvents(), "childA", 1);
- assertNotNull(checkpoint(result.document(), "inbox"));
+ assertChatCount(result.events(), "union", 1);
+ assertCompositeCheckpointSubject(
+ checkpoint(result.document(), "inbox"),
+ BigInteger.TEN,
+ "childA");
assertNull(checkpoint(result.document(), "inbox::childA"));
assertNull(checkpoint(result.document(), "inbox::childB"));
}
@Test
- void unionCheckpointIsIndependentFromChildCheckpoint() {
+ void compositeEvaluationUsesItsOwnExactPayload() {
Fixture fixture = configuredFixture();
TimelineChannel child = timelineContract();
Map channels = singletonChannel("child", child);
- ChannelEventCheckpoint checkpoint = new ChannelEventCheckpoint()
- .putEvent("child", eventNode(fixture, 100, "child ahead"));
+ Node event = eventNode(fixture, 99, "composite");
ChannelEvaluationContext context = ChannelEvaluationContextFactory.create(
"inbox",
- eventNode(fixture, 99, "union backfill"),
+ event,
channels,
- singletonMarker(checkpoint),
+ Collections.emptyMap(),
new TimelineChannelProcessor());
CompositeTimelineChannel union = new CompositeTimelineChannel()
.channels(Collections.singletonList("child"));
@@ -71,30 +70,35 @@ void unionCheckpointIsIndependentFromChildCheckpoint() {
assertTrue(evaluation.matches());
assertEquals(BigInteger.valueOf(99), evaluation.event().get("/timestamp"));
+ assertEquals(TimelineProviderSupport.eventId(event), evaluation.eventId());
}
@Test
- void childCheckpointIsIndependentFromUnionCheckpoint() {
+ void directChildAndCompositeBothEvaluateTheExactOccurrence() {
Fixture fixture = configuredFixture();
TimelineChannel child = timelineContract();
- Node current = eventNode(fixture, 1, "child backfill");
- Node unionAhead = eventNode(fixture, 100, "union ahead");
- ChannelEventCheckpoint checkpoint = new ChannelEventCheckpoint().putEvent("inbox", unionAhead);
- Map markers = singletonMarker(checkpoint);
+ Node current = eventNode(fixture, 1, "shared");
+ Map channels = singletonChannel("child", child);
ChannelEvaluationContext evaluationContext = ChannelEvaluationContextFactory.create(
"child",
current,
- singletonChannel("child", child),
- markers,
+ channels,
+ Collections.emptyMap(),
new TimelineChannelProcessor());
TimelineChannelProcessor processor = new TimelineChannelProcessor();
+ CompositeTimelineChannel composite = new CompositeTimelineChannel()
+ .channels(Collections.singletonList("child"));
+ ChannelEvaluationContext compositeContext =
+ ChannelEvaluationContextFactory.create(
+ "inbox",
+ current,
+ channels,
+ Collections.emptyMap(),
+ new TimelineChannelProcessor());
assertTrue(processor.evaluate(child, evaluationContext).matches());
- assertTrue(processor.isNewerEvent(child,
- ChannelCheckpointContext.of("/", "child", current, "current", null, null, markers)));
- assertFalse(new CompositeTimelineChannelProcessor().isNewerEvent(
- new CompositeTimelineChannel().channels(Collections.singletonList("child")),
- ChannelCheckpointContext.of("/", "inbox", current, "current", unionAhead, "ahead", markers)));
+ assertTrue(new CompositeTimelineChannelProcessor()
+ .evaluate(composite, compositeContext).matches());
}
@Test
@@ -109,10 +113,15 @@ void directChildAndUnionHandlersMayBothRun() {
DocumentProcessingResult result = process(fixture, initialized, 1, "hello");
- assertChatCount(result.triggeredEvents(), "direct", 1);
- assertChatCount(result.triggeredEvents(), "union", 1);
- assertNotNull(checkpoint(result.document(), "child"));
- assertNotNull(checkpoint(result.document(), "inbox"));
+ assertChatCount(result.events(), "direct", 1);
+ assertChatCount(result.events(), "union", 1);
+ assertDirectCheckpointSubject(
+ checkpoint(result.document(), "child"),
+ BigInteger.ONE);
+ assertCompositeCheckpointSubject(
+ checkpoint(result.document(), "inbox"),
+ BigInteger.ONE,
+ "child");
}
@Test
@@ -121,38 +130,41 @@ void matchingChildSelectionIsDeterministic() {
Map ordered = matchingChildren();
ordered.get("childB").properties("order", new Node().value(-1));
ordered.put("inbox", composite("childA", "childB"));
- ordered.put("handler", sourceReportingHandler("inbox", "compositeSourceChannelKey"));
+ ordered.put("handler", fixedHandler("inbox", "union"));
DocumentProcessingResult orderWinner = process(fixture,
initializedDocument(fixture, ordered),
1,
"order");
- assertChatCount(orderWinner.triggeredEvents(), "childB", 1);
+ assertChatCount(orderWinner.events(), "union", 1);
+ assertCompositeCheckpointSubject(
+ checkpoint(orderWinner.document(), "inbox"),
+ BigInteger.ONE,
+ "childB");
Fixture keyFixture = configuredFixture();
Map tied = matchingChildren();
tied.put("inbox", composite("childB", "childA"));
- tied.put("handler", sourceReportingHandler("inbox", "compositeSourceChannelKey"));
+ tied.put("handler", fixedHandler("inbox", "union"));
DocumentProcessingResult keyWinner = process(keyFixture,
initializedDocument(keyFixture, tied),
1,
"key");
- assertChatCount(keyWinner.triggeredEvents(), "childA", 1);
+ assertChatCount(keyWinner.events(), "union", 1);
+ assertCompositeCheckpointSubject(
+ checkpoint(keyWinner.document(), "inbox"),
+ BigInteger.ONE,
+ "childA");
}
@Test
- void newUnionWithNoCheckpointCanBackfillIndependently() {
+ void newCompositeEvaluatesWithoutCheckpointState() {
Fixture fixture = configuredFixture();
TimelineChannel child = timelineContract();
Node current = eventNode(fixture, 50, "backfill");
- Node ahead = eventNode(fixture, 100, "ahead");
- ChannelEventCheckpoint checkpoint = new ChannelEventCheckpoint()
- .putEvent("child", ahead)
- .putEvent("existingUnion", ahead);
- Map markers = singletonMarker(checkpoint);
CompositeTimelineChannel union = new CompositeTimelineChannel()
.channels(Collections.singletonList("child"));
CompositeTimelineChannelProcessor processor = new CompositeTimelineChannelProcessor();
@@ -160,12 +172,10 @@ void newUnionWithNoCheckpointCanBackfillIndependently() {
"newUnion",
current,
singletonChannel("child", child),
- markers,
+ Collections.emptyMap(),
new TimelineChannelProcessor());
assertTrue(processor.evaluate(union, context).matches());
- assertTrue(processor.isNewerEvent(union,
- ChannelCheckpointContext.of("/", "newUnion", current, "current", null, null, markers)));
}
@Test
@@ -174,12 +184,9 @@ void missingChildChannelFailsClearly() {
Map contracts = new LinkedHashMap();
contracts.put("inbox", composite("missing"));
- DocumentProcessingResult result = process(fixture,
- initializedDocument(fixture, contracts),
- 1,
- "hello");
+ DocumentProcessingResult result = initializeDocument(fixture, contracts);
- assertRuntimeFatal(result, "references missing child channel 'missing'");
+ assertSubscriptionSurfaceInvalid(result);
}
@Test
@@ -189,12 +196,9 @@ void nonTimelineChildFailsClearly() {
contracts.put("triggered", new Node().type("Triggered Event Channel"));
contracts.put("inbox", composite("triggered"));
- DocumentProcessingResult result = process(fixture,
- initializedDocument(fixture, contracts),
- 1,
- "hello");
+ DocumentProcessingResult result = initializeDocument(fixture, contracts);
- assertRuntimeFatal(result, "must be a Timeline Channel");
+ assertSubscriptionSurfaceInvalid(result);
}
@Test
@@ -203,16 +207,24 @@ void selfReferenceFailsClearly() {
Map contracts = new LinkedHashMap();
contracts.put("inbox", composite("inbox"));
- DocumentProcessingResult result = process(fixture,
- initializedDocument(fixture, contracts),
- 1,
- "hello");
+ DocumentProcessingResult result = initializeDocument(fixture, contracts);
- assertRuntimeFatal(result, "cannot include itself");
+ assertSubscriptionSurfaceInvalid(result);
}
@Test
- void childChannelEventFilterIsHonored() {
+ void emptyCompositeFailsSubscriptionSurfaceValidation() {
+ Fixture fixture = configuredFixture();
+ Map contracts = new LinkedHashMap();
+ contracts.put("inbox", composite());
+
+ DocumentProcessingResult result = initializeDocument(fixture, contracts);
+
+ assertSubscriptionSurfaceInvalid(result);
+ }
+
+ @Test
+ void previewChannelDefinitionDoesNotParticipateInExternalAcceptance() {
Fixture fixture = configuredFixture();
TimelineChannel filtered = timelineContract();
filtered.setDefinition(new Node()
@@ -230,18 +242,18 @@ void childChannelEventFilterIsHonored() {
"inbox",
eventNode(fixture, 1, "allowed"),
channels,
- Collections.emptyMap(),
+ Collections.emptyMap(),
new TimelineChannelProcessor()));
ChannelEvaluation denied = processor.evaluate(union,
ChannelEvaluationContextFactory.create(
"inbox",
eventNode(fixture, 2, "denied"),
channels,
- Collections.emptyMap(),
+ Collections.emptyMap(),
new TimelineChannelProcessor()));
assertTrue(allowed.matches());
- assertFalse(denied.matches());
+ assertTrue(denied.matches());
}
private static Map matchingChildren() {
@@ -263,12 +275,6 @@ private static Map singletonChannel(String key, Channel
return channels;
}
- private static Map singletonMarker(ChannelEventCheckpoint checkpoint) {
- Map markers = new LinkedHashMap();
- markers.put("checkpoint", checkpoint);
- return markers;
- }
-
private static Node composite(String... channels) {
return new Node()
.type("Coordination/Composite Timeline Channel")
@@ -283,44 +289,35 @@ private static Node stringList(String... values) {
return new Node().items(nodes);
}
- private static Node sourceReportingHandler(String channel, String metadataKey) {
- return new Node()
- .type("Coordination/Sequential Workflow")
- .properties("channel", new Node().value(channel))
- .properties("steps", new Node().items(
- appendChatMessageStep(bexBinding("event", "/meta/" + metadataKey))));
- }
-
private static Node fixedHandler(String channel, String message) {
return new Node()
.type("Coordination/Sequential Workflow")
.properties("channel", new Node().value(channel))
.properties("steps", new Node().items(
- appendChatMessageStep(new Node().value(message))));
+ new Node()
+ .type("Coordination/Trigger Event")
+ .properties(
+ "event",
+ TestTimelineProvider.chatMessage(
+ message))));
}
- private static Node appendChatMessageStep(Node message) {
- return new Node()
- .type("Coordination/Compute")
- .properties("do", new Node().items(
- new Node().properties("$appendEvent", new Node().properties("$merge", new Node().items(
- new Node().properties("type", new Node().value("Coordination/Chat Message")),
- new Node().properties("message", message)))),
- new Node().properties("$return", new Node().value(true))));
- }
-
- private static Node bexBinding(String name, String path) {
- return new Node().properties("$binding", new Node().value(name + path));
+ private static Node initializedDocument(Fixture fixture, Map contracts) {
+ DocumentProcessingResult initialized =
+ initializeDocument(fixture, contracts);
+ assertEquals(ProcessorStatus.SUCCESS, initialized.status(), blue.coordination.processor.ProcessingResultTestSupport.diagnosticMessage(initialized));
+ return initialized.document();
}
- private static Node initializedDocument(Fixture fixture, Map contracts) {
+ private static DocumentProcessingResult initializeDocument(
+ Fixture fixture,
+ Map contracts) {
Node document = new Node()
.blue(fixture.repository.typeAliasBlue())
.name("Composite Timeline V2 Test")
.properties("contracts", new Node().properties(contracts));
- DocumentProcessingResult initialized = fixture.blue.initializeDocument(fixture.blue.preprocess(document));
- assertEquals(ProcessorStatus.SUCCESS, initialized.status(), initialized.failureReason());
- return initialized.document();
+ return fixture.blue.initializeDocument(
+ fixture.blue.preprocess(document));
}
private static DocumentProcessingResult process(Fixture fixture,
@@ -343,12 +340,46 @@ private static Node eventNode(Fixture fixture,
private static Node checkpoint(Node document, String key) {
try {
- return document.getAsNode("/contracts/checkpoint/lastEvents/" + key);
+ return document.getAsNode(
+ "/contracts/checkpoint/entries/"
+ + escapePointerSegment(key)
+ + "/subject");
} catch (IllegalArgumentException ex) {
return null;
}
}
+ private static String escapePointerSegment(String value) {
+ return value.replace("~", "~0").replace("/", "~1");
+ }
+
+ private static void assertCompositeCheckpointSubject(
+ Node subject,
+ BigInteger timestamp,
+ String memberKey) {
+ assertNotNull(subject);
+ assertEquals(4, subject.getProperties().size());
+ assertEquals(
+ CompositeTimelineExternalSubscriptionFunctions
+ .ORDER_SUBJECT_VERSION,
+ subject.getAsText("/semantics"));
+ assertEquals(timestamp, subject.get("/timestamp"));
+ assertEquals(memberKey, subject.getAsText("/memberKey"));
+ assertNotNull(subject.getAsText("/memberDomain"));
+ }
+
+ private static void assertDirectCheckpointSubject(
+ Node subject,
+ BigInteger timestamp) {
+ assertNotNull(subject);
+ assertEquals(2, subject.getProperties().size());
+ assertEquals(
+ TimelineExternalSubscriptionFunctions
+ .TIMELINE_ORDER_SUBJECT_VERSION,
+ subject.getAsText("/semantics"));
+ assertEquals(timestamp, subject.get("/timestamp"));
+ }
+
private static void assertChatCount(List events, String message, int expected) {
int count = 0;
for (Node event : events) {
@@ -362,10 +393,13 @@ private static void assertChatCount(List events, String message, int expec
assertEquals(expected, count);
}
- private static void assertRuntimeFatal(DocumentProcessingResult result, String expectedMessage) {
- assertEquals(ProcessorStatus.RUNTIME_FATAL, result.status(), result.failureReason());
- assertTrue(result.failureReason() != null && result.failureReason().contains(expectedMessage),
- result.failureReason());
+ private static void assertSubscriptionSurfaceInvalid(
+ DocumentProcessingResult result) {
+ assertEquals(
+ ProcessorStatus.SUBSCRIPTION_SURFACE_INVALID,
+ result.status(),
+ blue.coordination.processor.ProcessingResultTestSupport
+ .diagnosticMessage(result));
}
private static Fixture configuredFixture() {
diff --git a/src/test/java/blue/coordination/processor/CoordinationDocumentSplitterDeepLocalityTest.java b/src/test/java/blue/coordination/processor/CoordinationDocumentSplitterDeepLocalityTest.java
new file mode 100644
index 0000000..c4d01a8
--- /dev/null
+++ b/src/test/java/blue/coordination/processor/CoordinationDocumentSplitterDeepLocalityTest.java
@@ -0,0 +1,794 @@
+package blue.coordination.processor;
+
+import blue.language.NodeProvider;
+import blue.language.model.Node;
+import blue.language.processor.registry.RuntimeBlueIds;
+import blue.language.provider.NodeProviderOutcome;
+import blue.language.provider.NodeProviderResult;
+import blue.language.utils.BlueIdCalculator;
+import blue.language.utils.NodePathEditor;
+import blue.language.utils.NodeToMapListOrValue;
+import blue.language.utils.NodeTransformer;
+import blue.repo.coordination.ChatWorkflowOperation;
+import blue.repo.coordination.SequentialWorkflow;
+import blue.repo.coordination.SequentialWorkflowOperation;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Structural evidence for deep Coordination selection surfaces.
+ *
+ * This test models physical provider demand only. A selected target admits
+ * the Root-to-target scope chain, the selected operation body at the target,
+ * and the explicitly allow-listed causal body at every scope on that chain.
+ * It does not execute Contracts and therefore makes no processing-parity
+ * claim.
+ */
+class CoordinationDocumentSplitterDeepLocalityTest {
+
+ private static final String ROOT = "/";
+ private static final String EMB1 = "/emb1";
+ private static final String EMB2 = "/emb1/emb2";
+ private static final String EMB3 =
+ "/emb1/emb2/emb3";
+ private static final List ACTIVE_SCOPE_PATHS =
+ Collections.unmodifiableList(
+ Arrays.asList(
+ ROOT,
+ EMB1,
+ EMB2,
+ EMB3));
+ private static final int SIBLINGS_PER_SCOPE = 2;
+ private static final int BODY_BYTES = 4096;
+
+ @Test
+ void completeFragmentInventoryReconstructsExactDeepRoot() {
+ Fixture fixture = Fixture.create();
+ CoordinationDocumentSplitter.SplitGraph split =
+ new CoordinationDocumentSplitter()
+ .splitDocument(fixture.root);
+
+ Node reconstructed =
+ NodeTransformer.transform(
+ split.pureReference(),
+ node -> {
+ if (!node.isReferenceOnly()) {
+ return node;
+ }
+ Node fragment =
+ split.fragments().get(
+ node.getBlueId());
+ return fragment != null
+ ? fragment
+ : node;
+ });
+
+ assertEquals(
+ NodeToMapListOrValue.get(
+ fixture.root),
+ NodeToMapListOrValue.get(
+ reconstructed));
+ assertEquals(
+ BlueIdCalculator.calculateBlueId(
+ fixture.root),
+ split.rootBlueId());
+ assertEquals(
+ split.rootBlueId(),
+ BlueIdCalculator.calculateBlueId(
+ reconstructed));
+
+ for (Map.Entry fragment
+ : split.fragments().entrySet()) {
+ assertEquals(
+ fragment.getKey(),
+ BlueIdCalculator.calculateBlueId(
+ fragment.getValue()));
+ NodeProviderResult result =
+ split.provider()
+ .fetchResultByBlueId(
+ fragment.getKey());
+ assertEquals(
+ NodeProviderOutcome.FOUND,
+ result.outcome());
+ assertEquals(1, result.nodes().size());
+ }
+
+ assertEquals(
+ ACTIVE_SCOPE_PATHS.size()
+ * SIBLINGS_PER_SCOPE,
+ fixture.siblingRootBlueIds.size(),
+ "every active level declares two sibling embedded roots");
+ for (String siblingBlueId
+ : fixture.siblingRootBlueIds) {
+ assertTrue(
+ hasMetadata(
+ split.metadata(),
+ CoordinationDocumentSplitter
+ .FragmentKind
+ .EMBEDDED_ROOT,
+ siblingBlueId));
+ }
+ assertEquals(
+ ACTIVE_SCOPE_PATHS.size() * 4,
+ executableBodyMetadataCount(
+ split.metadata()),
+ "every active scope retains selected, causal, and two decoy bodies");
+ }
+
+ @Test
+ void rootOnlySurfaceDemandsNoChildOrSiblingRoot() {
+ DemandProof proof =
+ demandSurface(
+ Collections.singletonList(
+ ROOT));
+ Set childAndSiblingRoots =
+ new LinkedHashSet<>(
+ proof.fixture.scopeBlueIds
+ .values());
+ childAndSiblingRoots.remove(
+ proof.fixture.scopeBlueIds.get(
+ ROOT));
+ childAndSiblingRoots.addAll(
+ proof.fixture.siblingRootBlueIds);
+
+ assertTrue(
+ Collections.disjoint(
+ childAndSiblingRoots,
+ proof.provider
+ .demandedBlueIds()));
+ assertEquals(
+ 3,
+ proof.provider.calls(),
+ "Root header plus selected and causal Root bodies only");
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("selectionSurfaces")
+ void selectedScopeUnionDemandsOnlyChainAndAllowListedBodies(
+ String label,
+ List selectedScopePaths) {
+ DemandProof proof =
+ demandSurface(
+ selectedScopePaths);
+
+ assertEquals(
+ proof.expectedBlueIds,
+ proof.provider.demandedBlueIds(),
+ label);
+ assertEquals(
+ proof.expectedBlueIds.size(),
+ proof.provider.calls(),
+ "each exact fragment is demanded at most once");
+
+ Set forbidden =
+ new LinkedHashSet<>(
+ proof.split.fragments()
+ .keySet());
+ forbidden.removeAll(
+ proof.expectedBlueIds);
+ assertTrue(
+ Collections.disjoint(
+ forbidden,
+ proof.provider
+ .demandedBlueIds()),
+ "no fragment outside the selected-chain union may be demanded");
+ assertTrue(
+ Collections.disjoint(
+ proof.fixture.siblingRootBlueIds,
+ proof.provider
+ .demandedBlueIds()),
+ "declared sibling embedded roots remain references");
+ assertTrue(
+ Collections.disjoint(
+ proof.fixture.decoyBodyBlueIds,
+ proof.provider
+ .demandedBlueIds()),
+ "decoy operation and reactive bodies remain references");
+
+ for (String selectedPath
+ : selectedScopePaths) {
+ assertTrue(
+ proof.provider
+ .demandedBlueIds()
+ .contains(
+ proof.fixture
+ .selectedBodyBlueIds
+ .get(selectedPath)));
+ }
+ for (String chainPath
+ : selectedChainUnion(
+ selectedScopePaths)) {
+ assertTrue(
+ proof.provider
+ .demandedBlueIds()
+ .contains(
+ proof.fixture
+ .causalBodyBlueIds
+ .get(chainPath)));
+ }
+ }
+
+ private static Stream selectionSurfaces() {
+ return Stream.of(
+ Arguments.of(
+ "Root",
+ Collections.singletonList(
+ ROOT)),
+ Arguments.of(
+ "Emb1",
+ Collections.singletonList(
+ EMB1)),
+ Arguments.of(
+ "Emb2",
+ Collections.singletonList(
+ EMB2)),
+ Arguments.of(
+ "Emb3",
+ Collections.singletonList(
+ EMB3)),
+ Arguments.of(
+ "Root + Emb3",
+ Arrays.asList(
+ ROOT,
+ EMB3)),
+ Arguments.of(
+ "Root + Emb1 + Emb2 + Emb3",
+ ACTIVE_SCOPE_PATHS));
+ }
+
+ private static DemandProof demandSurface(
+ List selectedScopePaths) {
+ Fixture fixture = Fixture.create();
+ CoordinationDocumentSplitter.SplitGraph split =
+ new CoordinationDocumentSplitter()
+ .splitDocument(fixture.root);
+ Set expectedBlueIds =
+ expectedBlueIds(
+ fixture,
+ selectedScopePaths);
+ StrictRecordingProvider provider =
+ new StrictRecordingProvider(
+ split.provider(),
+ expectedBlueIds);
+ DemandSession session =
+ new DemandSession(provider);
+
+ for (String selectedPath
+ : selectedScopePaths) {
+ List chain =
+ chainTo(selectedPath);
+ Node scope = null;
+ String priorPath = null;
+ for (String scopePath : chain) {
+ if (priorPath == null) {
+ scope = session.demand(
+ fixture.scopeBlueIds.get(
+ ROOT));
+ } else {
+ String childKey =
+ lastSegment(scopePath);
+ Node childReference =
+ NodePathEditor.getOrNull(
+ scope,
+ "/" + childKey);
+ assertNotNull(childReference);
+ assertTrue(
+ childReference
+ .isReferenceOnly());
+ assertEquals(
+ fixture.scopeBlueIds.get(
+ scopePath),
+ childReference.getBlueId());
+ scope = session.demand(
+ childReference.getBlueId());
+ }
+
+ Node causalReference =
+ NodePathEditor.getOrNull(
+ scope,
+ "/contracts/causalReaction/steps");
+ assertNotNull(causalReference);
+ assertTrue(
+ causalReference
+ .isReferenceOnly());
+ assertEquals(
+ fixture.causalBodyBlueIds
+ .get(scopePath),
+ causalReference.getBlueId());
+ session.demand(
+ causalReference.getBlueId());
+ priorPath = scopePath;
+ }
+
+ Node selectedReference =
+ NodePathEditor.getOrNull(
+ scope,
+ "/contracts/selectedOperation/steps");
+ assertNotNull(selectedReference);
+ assertTrue(
+ selectedReference
+ .isReferenceOnly());
+ assertEquals(
+ fixture.selectedBodyBlueIds
+ .get(selectedPath),
+ selectedReference.getBlueId());
+ session.demand(
+ selectedReference.getBlueId());
+ }
+
+ return new DemandProof(
+ fixture,
+ split,
+ provider,
+ expectedBlueIds);
+ }
+
+ private static Set expectedBlueIds(
+ Fixture fixture,
+ List selectedScopePaths) {
+ Set expected =
+ new LinkedHashSet<>();
+ for (String selectedPath
+ : selectedScopePaths) {
+ for (String chainPath
+ : chainTo(selectedPath)) {
+ expected.add(
+ fixture.scopeBlueIds.get(
+ chainPath));
+ expected.add(
+ fixture.causalBodyBlueIds.get(
+ chainPath));
+ }
+ expected.add(
+ fixture.selectedBodyBlueIds.get(
+ selectedPath));
+ }
+ return expected;
+ }
+
+ private static Set selectedChainUnion(
+ List selectedScopePaths) {
+ Set result =
+ new LinkedHashSet<>();
+ for (String path : selectedScopePaths) {
+ result.addAll(chainTo(path));
+ }
+ return result;
+ }
+
+ private static List chainTo(
+ String selectedPath) {
+ int targetIndex =
+ ACTIVE_SCOPE_PATHS.indexOf(
+ selectedPath);
+ if (targetIndex < 0) {
+ throw new IllegalArgumentException(
+ "Unknown selected scope path: "
+ + selectedPath);
+ }
+ return new ArrayList<>(
+ ACTIVE_SCOPE_PATHS.subList(
+ 0, targetIndex + 1));
+ }
+
+ private static String lastSegment(
+ String path) {
+ return path.substring(
+ path.lastIndexOf('/') + 1);
+ }
+
+ private static boolean hasMetadata(
+ List metadata,
+ CoordinationDocumentSplitter.FragmentKind kind,
+ String blueId) {
+ for (CoordinationDocumentSplitter.FragmentMetadata entry
+ : metadata) {
+ if (entry.kind() == kind
+ && blueId.equals(entry.blueId())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static int executableBodyMetadataCount(
+ List metadata) {
+ int result = 0;
+ for (CoordinationDocumentSplitter.FragmentMetadata entry
+ : metadata) {
+ if (entry.kind()
+ == CoordinationDocumentSplitter
+ .FragmentKind
+ .EXECUTABLE_BODY) {
+ result++;
+ }
+ }
+ return result;
+ }
+
+ private static Node processEmbedded(
+ List paths) {
+ List values =
+ new ArrayList<>();
+ for (String path : paths) {
+ values.add(scalar(path));
+ }
+ return new Node()
+ .type(reference(
+ RuntimeBlueIds.PROCESS_EMBEDDED))
+ .properties(
+ "paths",
+ new Node().items(values));
+ }
+
+ private static Node handler(
+ String typeBlueId,
+ String channel,
+ String operation,
+ Node body) {
+ return new Node()
+ .type(reference(typeBlueId))
+ .properties(
+ "channel", scalar(channel),
+ "operation", scalar(operation),
+ "order", scalar(0L),
+ "steps", body);
+ }
+
+ private static Node body(
+ String label) {
+ return new Node().items(
+ new Node().properties(
+ "label", scalar(label),
+ "payload", scalar(
+ repeat(
+ (char) ('A'
+ + Math.abs(
+ label.hashCode())
+ % 20),
+ BODY_BYTES))));
+ }
+
+ private static Node scalar(
+ Object value) {
+ return new Node().value(value);
+ }
+
+ private static Node reference(
+ String blueId) {
+ return new Node().blueId(blueId);
+ }
+
+ private static String repeat(
+ char value,
+ int count) {
+ char[] characters =
+ new char[count];
+ Arrays.fill(characters, value);
+ return new String(characters);
+ }
+
+ private static String appendPath(
+ String parent,
+ String child) {
+ return ROOT.equals(parent)
+ ? ROOT + child
+ : parent + "/" + child;
+ }
+
+ private static final class Fixture {
+
+ private final Node root;
+ private final Map scopeBlueIds;
+ private final Map selectedBodyBlueIds;
+ private final Map causalBodyBlueIds;
+ private final Set decoyBodyBlueIds;
+ private final Set siblingRootBlueIds;
+
+ private Fixture(
+ Node root,
+ Map scopeBlueIds,
+ Map selectedBodyBlueIds,
+ Map causalBodyBlueIds,
+ Set decoyBodyBlueIds,
+ Set siblingRootBlueIds) {
+ this.root = root;
+ this.scopeBlueIds =
+ Collections.unmodifiableMap(
+ new LinkedHashMap<>(
+ scopeBlueIds));
+ this.selectedBodyBlueIds =
+ Collections.unmodifiableMap(
+ new LinkedHashMap<>(
+ selectedBodyBlueIds));
+ this.causalBodyBlueIds =
+ Collections.unmodifiableMap(
+ new LinkedHashMap<>(
+ causalBodyBlueIds));
+ this.decoyBodyBlueIds =
+ Collections.unmodifiableSet(
+ new LinkedHashSet<>(
+ decoyBodyBlueIds));
+ this.siblingRootBlueIds =
+ Collections.unmodifiableSet(
+ new LinkedHashSet<>(
+ siblingRootBlueIds));
+ }
+
+ private static Fixture create() {
+ FixtureBuilder builder =
+ new FixtureBuilder();
+ Node root =
+ builder.activeScope(
+ 0, ROOT);
+ return new Fixture(
+ root,
+ builder.scopeBlueIds,
+ builder.selectedBodyBlueIds,
+ builder.causalBodyBlueIds,
+ builder.decoyBodyBlueIds,
+ builder.siblingRootBlueIds);
+ }
+ }
+
+ private static final class FixtureBuilder {
+
+ private final Map scopeBlueIds =
+ new LinkedHashMap<>();
+ private final Map selectedBodyBlueIds =
+ new LinkedHashMap<>();
+ private final Map causalBodyBlueIds =
+ new LinkedHashMap<>();
+ private final Set decoyBodyBlueIds =
+ new LinkedHashSet<>();
+ private final Set siblingRootBlueIds =
+ new LinkedHashSet<>();
+
+ private Node activeScope(
+ int depth,
+ String scopePath) {
+ Map properties =
+ new LinkedHashMap<>();
+ properties.put(
+ "scope",
+ scalar(scopePath));
+ List embeddedPaths =
+ new ArrayList<>();
+
+ if (depth
+ < ACTIVE_SCOPE_PATHS.size() - 1) {
+ String selectedChild =
+ "emb" + (depth + 1);
+ String selectedChildPath =
+ appendPath(
+ scopePath,
+ selectedChild);
+ properties.put(
+ selectedChild,
+ activeScope(
+ depth + 1,
+ selectedChildPath));
+ embeddedPaths.add(
+ "/" + selectedChild);
+ }
+
+ for (int sibling = 1;
+ sibling <= SIBLINGS_PER_SCOPE;
+ sibling++) {
+ String key =
+ "sibling" + sibling;
+ Node siblingRoot = new Node()
+ .properties(
+ "owner", scalar(scopePath),
+ "branch", scalar(key),
+ "payload", scalar(
+ repeat(
+ (char) ('k'
+ + sibling),
+ BODY_BYTES)));
+ properties.put(key, siblingRoot);
+ embeddedPaths.add("/" + key);
+ siblingRootBlueIds.add(
+ BlueIdCalculator
+ .calculateBlueId(
+ siblingRoot));
+ }
+
+ Node selectedBody =
+ body("selected-" + depth);
+ Node causalBody =
+ body("causal-" + depth);
+ Node decoyOperationBody =
+ body("decoy-operation-"
+ + depth);
+ Node decoyReactionBody =
+ body("decoy-reaction-"
+ + depth);
+ selectedBodyBlueIds.put(
+ scopePath,
+ BlueIdCalculator.calculateBlueId(
+ selectedBody));
+ causalBodyBlueIds.put(
+ scopePath,
+ BlueIdCalculator.calculateBlueId(
+ causalBody));
+ decoyBodyBlueIds.add(
+ BlueIdCalculator.calculateBlueId(
+ decoyOperationBody));
+ decoyBodyBlueIds.add(
+ BlueIdCalculator.calculateBlueId(
+ decoyReactionBody));
+
+ Map contracts =
+ new LinkedHashMap<>();
+ contracts.put(
+ "embedded",
+ processEmbedded(
+ embeddedPaths));
+ contracts.put(
+ "selectedOperation",
+ handler(
+ SequentialWorkflowOperation
+ .blueId(),
+ "timeline-" + depth,
+ "selected-" + depth,
+ selectedBody));
+ contracts.put(
+ "causalReaction",
+ handler(
+ SequentialWorkflow.blueId(),
+ "causal-" + depth,
+ "react-" + depth,
+ causalBody));
+ contracts.put(
+ "decoyOperation",
+ handler(
+ ChatWorkflowOperation.blueId(),
+ "timeline-" + depth,
+ "decoy-" + depth,
+ decoyOperationBody));
+ contracts.put(
+ "decoyReaction",
+ handler(
+ SequentialWorkflow.blueId(),
+ "decoy-" + depth,
+ "ignore-" + depth,
+ decoyReactionBody));
+
+ Node scope = new Node()
+ .properties(properties)
+ .contracts(
+ new Node().properties(
+ contracts));
+ scopeBlueIds.put(
+ scopePath,
+ BlueIdCalculator.calculateBlueId(
+ scope));
+ return scope;
+ }
+ }
+
+ private static final class DemandSession {
+
+ private final NodeProvider provider;
+ private final Map cache =
+ new LinkedHashMap<>();
+
+ private DemandSession(
+ NodeProvider provider) {
+ this.provider = provider;
+ }
+
+ private Node demand(
+ String blueId) {
+ Node cached = cache.get(blueId);
+ if (cached != null) {
+ return cached.clone();
+ }
+ List nodes =
+ provider.fetchByBlueId(
+ blueId);
+ assertNotNull(nodes);
+ assertEquals(1, nodes.size());
+ Node exact = nodes.get(0);
+ assertEquals(
+ blueId,
+ BlueIdCalculator.calculateBlueId(
+ exact));
+ cache.put(blueId, exact.clone());
+ return exact;
+ }
+ }
+
+ private static final class StrictRecordingProvider
+ implements NodeProvider {
+
+ private final NodeProvider delegate;
+ private final Set allowedBlueIds;
+ private final Set demandedBlueIds =
+ new LinkedHashSet<>();
+ private int calls;
+
+ private StrictRecordingProvider(
+ NodeProvider delegate,
+ Set allowedBlueIds) {
+ this.delegate = delegate;
+ this.allowedBlueIds =
+ Collections.unmodifiableSet(
+ new LinkedHashSet<>(
+ allowedBlueIds));
+ }
+
+ @Override
+ public List fetchByBlueId(
+ String blueId) {
+ assertTrue(
+ allowedBlueIds.contains(
+ blueId),
+ "forbidden fragment demand: "
+ + blueId);
+ assertTrue(
+ demandedBlueIds.add(
+ blueId),
+ "duplicate provider demand: "
+ + blueId);
+ calls++;
+ List nodes =
+ delegate.fetchByBlueId(
+ blueId);
+ assertNotNull(
+ nodes,
+ "allowed exact fragment is missing: "
+ + blueId);
+ return nodes;
+ }
+
+ private Set demandedBlueIds() {
+ return Collections.unmodifiableSet(
+ demandedBlueIds);
+ }
+
+ private int calls() {
+ return calls;
+ }
+ }
+
+ private static final class DemandProof {
+
+ private final Fixture fixture;
+ private final CoordinationDocumentSplitter.SplitGraph split;
+ private final StrictRecordingProvider provider;
+ private final Set expectedBlueIds;
+
+ private DemandProof(
+ Fixture fixture,
+ CoordinationDocumentSplitter.SplitGraph split,
+ StrictRecordingProvider provider,
+ Set expectedBlueIds) {
+ this.fixture = fixture;
+ this.split = split;
+ this.provider = provider;
+ this.expectedBlueIds =
+ Collections.unmodifiableSet(
+ new LinkedHashSet<>(
+ expectedBlueIds));
+ }
+ }
+}
diff --git a/src/test/java/blue/coordination/processor/CoordinationDocumentSplitterLocalityTest.java b/src/test/java/blue/coordination/processor/CoordinationDocumentSplitterLocalityTest.java
new file mode 100644
index 0000000..b4b69a9
--- /dev/null
+++ b/src/test/java/blue/coordination/processor/CoordinationDocumentSplitterLocalityTest.java
@@ -0,0 +1,489 @@
+package blue.coordination.processor;
+
+import blue.language.NodeProvider;
+import blue.language.model.Node;
+import blue.language.processor.registry.RuntimeBlueIds;
+import blue.language.utils.BlueIdCalculator;
+import blue.language.utils.NodePathEditor;
+import blue.language.utils.NodeToMapListOrValue;
+import blue.language.utils.UncheckedObjectMapper;
+import blue.repo.coordination.ChatWorkflowOperation;
+import blue.repo.coordination.SequentialWorkflow;
+import blue.repo.coordination.SequentialWorkflowOperation;
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Non-time-based structural locality evidence for the public Coordination
+ * splitter. The fixture has a branching factor of five along a seven-scope
+ * selected spine, five 16 KiB operation bodies per active scope, and four
+ * unrelated embedded siblings at each non-leaf scope.
+ */
+class CoordinationDocumentSplitterLocalityTest {
+
+ private static final int DEPTH = 6;
+ private static final int BRANCHING_FACTOR = 5;
+ private static final int OPERATIONS_PER_SCOPE = 5;
+ private static final int BODY_BYTES = 16 * 1024;
+
+ @Test
+ void providerDemandIsProportionalToSelectedSpineAndBodies() {
+ Node root = selectedSpine(0);
+ CoordinationDocumentSplitter.SplitGraph split =
+ new CoordinationDocumentSplitter().splitDocument(root);
+ RecordingProvider provider =
+ new RecordingProvider(split.provider());
+ Set expectedDemands =
+ new LinkedHashSet();
+
+ String scopeBlueId = split.rootBlueId();
+ for (int depth = 0; depth <= DEPTH; depth++) {
+ Node scope = fetch(provider, scopeBlueId);
+ expectedDemands.add(scopeBlueId);
+
+ Node selectedBody = NodePathEditor.getOrNull(
+ scope, "/contracts/selected/steps");
+ assertNotNull(selectedBody);
+ assertTrue(selectedBody.isReferenceOnly());
+ fetch(provider, selectedBody.getBlueId());
+ expectedDemands.add(selectedBody.getBlueId());
+
+ if (depth < DEPTH) {
+ Node selectedChild =
+ NodePathEditor.getOrNull(scope, "/selected");
+ assertNotNull(selectedChild);
+ assertTrue(selectedChild.isReferenceOnly());
+ scopeBlueId = selectedChild.getBlueId();
+ }
+ }
+
+ assertEquals(expectedDemands, provider.demandedBlueIds());
+ assertEquals(
+ (DEPTH + 1) * 2,
+ provider.calls(),
+ "one scope fragment and one selected body are read per active scope");
+
+ Set forbidden =
+ new LinkedHashSet(split.fragments().keySet());
+ forbidden.removeAll(expectedDemands);
+ assertFalse(forbidden.isEmpty());
+ assertTrue(
+ Collections.disjoint(
+ forbidden, provider.demandedBlueIds()),
+ "no embedded sibling or decoy body may be demanded");
+
+ long totalGraphBytes = encodedBytes(split.fragments().values());
+ long selectedFragmentBytes = provider.returnedBytes();
+ assertTrue(
+ selectedFragmentBytes * 3L < totalGraphBytes,
+ "selected bytes must remain structurally below the transitive graph: "
+ + selectedFragmentBytes + " selected of "
+ + totalGraphBytes + " total");
+ assertEquals(0, provider.forbiddenDemands(forbidden));
+
+ System.out.println(
+ "Coordination splitter scale locality: branchingFactor="
+ + BRANCHING_FACTOR
+ + ", depth=" + DEPTH
+ + ", operationsPerScope="
+ + OPERATIONS_PER_SCOPE
+ + ", bodyBytes=" + BODY_BYTES
+ + ", totalGraphBytes="
+ + totalGraphBytes
+ + ", selectedFragmentBytes="
+ + selectedFragmentBytes
+ + ", providerCalls="
+ + provider.calls()
+ + ", exactDemandedBlueIds="
+ + provider.demandedBlueIds());
+ }
+
+ @Test
+ void rootOnlyPreparationDoesNotReadAnyEmbeddedRoot() {
+ Node root = selectedSpine(0);
+ CoordinationDocumentSplitter.SplitGraph split =
+ new CoordinationDocumentSplitter().splitDocument(root);
+ RecordingProvider provider =
+ new RecordingProvider(split.provider());
+
+ Node rootFragment =
+ fetch(provider, split.rootBlueId());
+ Node rootBody = NodePathEditor.getOrNull(
+ rootFragment, "/contracts/selected/steps");
+ fetch(provider, rootBody.getBlueId());
+
+ Set embeddedRootBlueIds =
+ blueIdsOfKind(
+ split.metadata(),
+ CoordinationDocumentSplitter.FragmentKind.EMBEDDED_ROOT);
+ assertTrue(
+ Collections.disjoint(
+ embeddedRootBlueIds,
+ provider.demandedBlueIds()));
+ assertEquals(
+ Arrays.asList(
+ split.rootBlueId(),
+ rootBody.getBlueId()),
+ new ArrayList(
+ provider.demandedBlueIds()));
+ }
+
+ @Test
+ void allRetainedFragmentsReconstructTheExactGraphAndSharedBodiesDeduplicate() {
+ Node sharedBody = body("shared", BODY_BYTES);
+ Node root = new Node()
+ .properties("state", scalar("root"))
+ .contracts(new Node().properties(
+ "first",
+ workflow(
+ SequentialWorkflowOperation.blueId(),
+ sharedBody),
+ "second",
+ workflow(
+ ChatWorkflowOperation.blueId(),
+ sharedBody.clone()),
+ "reactive",
+ workflow(
+ SequentialWorkflow.blueId(),
+ body("reactive", 128))));
+
+ CoordinationDocumentSplitter.SplitGraph split =
+ new CoordinationDocumentSplitter().splitDocument(root);
+ Node reconstructed = expandKnownFragments(
+ split.pureReference(),
+ split.fragments(),
+ new LinkedHashSet());
+
+ assertEquals(
+ NodeToMapListOrValue.get(root),
+ NodeToMapListOrValue.get(reconstructed));
+ assertEquals(
+ BlueIdCalculator.calculateBlueId(root),
+ BlueIdCalculator.calculateBlueId(reconstructed));
+
+ String sharedBodyBlueId =
+ BlueIdCalculator.calculateBlueId(sharedBody);
+ assertTrue(split.fragments().containsKey(sharedBodyBlueId));
+ int sharedOccurrences = 0;
+ for (CoordinationDocumentSplitter.FragmentMetadata metadata
+ : split.metadata()) {
+ if (metadata.kind()
+ == CoordinationDocumentSplitter.FragmentKind.EXECUTABLE_BODY
+ && sharedBodyBlueId.equals(metadata.blueId())) {
+ sharedOccurrences++;
+ }
+ }
+ assertEquals(
+ 2,
+ sharedOccurrences,
+ "two handler headers retain distinct diagnostic occurrences");
+ assertEquals(
+ 1,
+ countKey(split.fragments(), sharedBodyBlueId),
+ "content-addressed storage retains the shared body once");
+ }
+
+ private static Node selectedSpine(int depth) {
+ Map properties =
+ new LinkedHashMap();
+ properties.put("depth", scalar(depth));
+ Map contracts =
+ operationContracts(depth);
+ if (depth < DEPTH) {
+ List embeddedPaths =
+ new ArrayList();
+ properties.put("selected", selectedSpine(depth + 1));
+ embeddedPaths.add("/selected");
+ for (int sibling = 1;
+ sibling < BRANCHING_FACTOR;
+ sibling++) {
+ String key = "other" + sibling;
+ properties.put(
+ key,
+ new Node()
+ .properties(
+ "depth", scalar(depth + 1),
+ "branch", scalar(key),
+ "largeUnrelatedData",
+ scalar(repeat(
+ (char) ('a' + sibling),
+ BODY_BYTES))));
+ embeddedPaths.add("/" + key);
+ }
+ contracts.put(
+ "embedded",
+ processEmbedded(embeddedPaths));
+ }
+ return new Node()
+ .properties(properties)
+ .contracts(new Node().properties(contracts));
+ }
+
+ private static Map operationContracts(
+ int depth) {
+ Map contracts =
+ new LinkedHashMap();
+ contracts.put(
+ "selected",
+ workflow(
+ SequentialWorkflowOperation.blueId(),
+ body("selected-" + depth, BODY_BYTES)));
+ for (int operation = 1;
+ operation < OPERATIONS_PER_SCOPE;
+ operation++) {
+ String typeBlueId = operation % 2 == 0
+ ? ChatWorkflowOperation.blueId()
+ : SequentialWorkflow.blueId();
+ contracts.put(
+ "decoy" + operation,
+ workflow(
+ typeBlueId,
+ body(
+ "decoy-" + depth + "-"
+ + operation,
+ BODY_BYTES)));
+ }
+ return contracts;
+ }
+
+ private static Node workflow(
+ String typeBlueId,
+ Node steps) {
+ return new Node()
+ .type(reference(typeBlueId))
+ .properties(
+ "channel", scalar("timeline"),
+ "order", scalar(0),
+ "steps", steps);
+ }
+
+ private static Node processEmbedded(
+ List paths) {
+ List values =
+ new ArrayList();
+ for (String path : paths) {
+ values.add(scalar(path));
+ }
+ return new Node()
+ .type(reference(
+ RuntimeBlueIds.PROCESS_EMBEDDED))
+ .properties(
+ "paths",
+ new Node().items(values));
+ }
+
+ private static Node body(
+ String label,
+ int bytes) {
+ return new Node().items(
+ new Node().properties(
+ "label", scalar(label),
+ "payload", scalar(
+ repeat(
+ (char) ('A'
+ + Math.abs(
+ label.hashCode())
+ % 20),
+ bytes))));
+ }
+
+ private static String repeat(
+ char value,
+ int count) {
+ char[] chars = new char[count];
+ Arrays.fill(chars, value);
+ return new String(chars);
+ }
+
+ private static Node scalar(Object value) {
+ return new Node().value(value);
+ }
+
+ private static Node reference(String blueId) {
+ return new Node().blueId(blueId);
+ }
+
+ private static Node fetch(
+ NodeProvider provider,
+ String blueId) {
+ List nodes =
+ provider.fetchByBlueId(blueId);
+ assertNotNull(nodes);
+ assertEquals(1, nodes.size());
+ return nodes.get(0);
+ }
+
+ private static Set blueIdsOfKind(
+ List metadata,
+ CoordinationDocumentSplitter.FragmentKind kind) {
+ Set result =
+ new LinkedHashSet();
+ for (CoordinationDocumentSplitter.FragmentMetadata entry
+ : metadata) {
+ if (entry.kind() == kind) {
+ result.add(entry.blueId());
+ }
+ }
+ return result;
+ }
+
+ private static long encodedBytes(
+ Iterable nodes) {
+ long total = 0L;
+ for (Node node : nodes) {
+ total += encodedBytes(node);
+ }
+ return total;
+ }
+
+ private static long encodedBytes(Node node) {
+ try {
+ return UncheckedObjectMapper.JSON_MAPPER
+ .writeValueAsString(node)
+ .getBytes(StandardCharsets.UTF_8)
+ .length;
+ } catch (Exception exception) {
+ throw new IllegalStateException(
+ "Could not encode exact fragment",
+ exception);
+ }
+ }
+
+ private static int countKey(
+ Map fragments,
+ String blueId) {
+ int count = 0;
+ for (String key : fragments.keySet()) {
+ if (blueId.equals(key)) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ private static Node expandKnownFragments(
+ Node node,
+ Map fragments,
+ Set active) {
+ if (node == null) {
+ return null;
+ }
+ if (node.isReferenceOnly()) {
+ Node fragment = fragments.get(node.getBlueId());
+ if (fragment == null) {
+ return node.clone();
+ }
+ assertTrue(
+ active.add(node.getBlueId()),
+ "fragment cycle at " + node.getBlueId());
+ try {
+ return expandKnownFragments(
+ fragment, fragments, active);
+ } finally {
+ active.remove(node.getBlueId());
+ }
+ }
+
+ Node expanded = node.clone();
+ expanded.type(expandKnownFragments(
+ node.getType(), fragments, active));
+ expanded.itemType(expandKnownFragments(
+ node.getItemType(), fragments, active));
+ expanded.keyType(expandKnownFragments(
+ node.getKeyType(), fragments, active));
+ expanded.valueType(expandKnownFragments(
+ node.getValueType(), fragments, active));
+ expanded.contracts(expandKnownFragments(
+ node.getContracts(), fragments, active));
+ expanded.blue(expandKnownFragments(
+ node.getBlue(), fragments, active));
+ if (node.getItems() != null) {
+ List items =
+ new ArrayList();
+ for (Node item : node.getItems()) {
+ items.add(expandKnownFragments(
+ item, fragments, active));
+ }
+ expanded.items(items);
+ }
+ if (node.getProperties() != null) {
+ Map properties =
+ new LinkedHashMap();
+ for (Map.Entry property
+ : node.getProperties().entrySet()) {
+ properties.put(
+ property.getKey(),
+ expandKnownFragments(
+ property.getValue(),
+ fragments,
+ active));
+ }
+ expanded.properties(properties);
+ }
+ return expanded;
+ }
+
+ private static final class RecordingProvider
+ implements NodeProvider {
+
+ private final NodeProvider delegate;
+ private final Set demandedBlueIds =
+ new LinkedHashSet();
+ private int calls;
+ private long returnedBytes;
+
+ private RecordingProvider(NodeProvider delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public List fetchByBlueId(String blueId) {
+ calls++;
+ demandedBlueIds.add(blueId);
+ List nodes =
+ delegate.fetchByBlueId(blueId);
+ if (nodes != null) {
+ returnedBytes += encodedBytes(nodes);
+ }
+ return nodes;
+ }
+
+ private int calls() {
+ return calls;
+ }
+
+ private long returnedBytes() {
+ return returnedBytes;
+ }
+
+ private Set demandedBlueIds() {
+ return Collections.unmodifiableSet(
+ demandedBlueIds);
+ }
+
+ private int forbiddenDemands(
+ Set forbidden) {
+ int result = 0;
+ for (String demanded : demandedBlueIds) {
+ if (forbidden.contains(demanded)) {
+ result++;
+ }
+ }
+ return result;
+ }
+ }
+}
diff --git a/src/test/java/blue/coordination/processor/CoordinationDocumentSplitterProcessingMatrixTest.java b/src/test/java/blue/coordination/processor/CoordinationDocumentSplitterProcessingMatrixTest.java
new file mode 100644
index 0000000..38a8e49
--- /dev/null
+++ b/src/test/java/blue/coordination/processor/CoordinationDocumentSplitterProcessingMatrixTest.java
@@ -0,0 +1,1270 @@
+package blue.coordination.processor;
+
+import blue.language.Blue;
+import blue.language.NodeProvider;
+import blue.language.model.Node;
+import blue.language.processor.CheckpointDomain;
+import blue.language.processor.ChannelEvaluation;
+import blue.language.processor.ChannelEvaluationContext;
+import blue.language.processor.ChannelProcessor;
+import blue.language.processor.ContractMatchingService;
+import blue.language.processor.ContractProcessorRegistry;
+import blue.language.processor.ContractProcessorRegistryBuilder;
+import blue.language.processor.CoordinationRoutingHarness;
+import blue.language.processor.DocumentProcessingResult;
+import blue.language.processor.DocumentProcessor;
+import blue.language.processor.ExternalDeliveryPlan;
+import blue.language.processor.ExternalDeliverySnapshot;
+import blue.language.processor.ExternalChannelSubscriptionFunctions;
+import blue.language.processor.ExternalOrderKey;
+import blue.language.processor.GasSchedule;
+import blue.language.processor.GasTraceEntry;
+import blue.language.processor.HandlerMatchContext;
+import blue.language.processor.HandlerProcessor;
+import blue.language.processor.ProcessingConformanceTrace;
+import blue.language.processor.ProcessingDebugResult;
+import blue.language.processor.ProcessingTraceRecord;
+import blue.language.processor.ProcessorDiagnostic;
+import blue.language.processor.ProcessorExecutionContext;
+import blue.language.processor.ProcessorStatus;
+import blue.language.processor.SubscriptionDelta;
+import blue.language.processor.conformance.MockExternalChannel;
+import blue.language.processor.conformance.MockHandler;
+import blue.language.processor.conformance.MockHandlerProcessor;
+import blue.language.processor.conformance.MockTypeBlueIds;
+import blue.language.processor.registry.BlueRuntimeTypeRegistry;
+import blue.language.processor.registry.RuntimeBlueIds;
+import blue.language.processor.registry.RuntimeTypeKey;
+import blue.language.provider.SequentialNodeProvider;
+import blue.language.utils.BlueIdCalculator;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * End-to-end proof that the public Coordination splitter feeds Language's
+ * two-BlueId PROCESS boundary without changing execution semantics.
+ *
+ * The fixture deliberately has no Process Embedded declaration. Its five
+ * Handler headers are all reactive, while only one executable body can be
+ * selected by the immutable delivery plan. A large archive and four decoy
+ * bodies remain available to the strict provider, but any demand for them
+ * fails the run immediately.
+ */
+final class CoordinationDocumentSplitterProcessingMatrixTest {
+
+ private static final String SELECTED_CHANNEL = "incoming";
+ private static final String REJECTED_CHANNEL = "rejected";
+ private static final String SELECTED_HANDLER = "selectedWorkflow";
+ private static final String SUBSCRIPTION_KEY =
+ "coordination-fragment-matrix";
+ private static final String CHECKPOINT_DISCRIMINATOR =
+ "coordination-fragment-matrix-v1";
+ private static final int LARGE_VALUE_SIZE = 24_000;
+ private static final ExternalOrderKey EVENT_ORDER =
+ ExternalOrderKey.of(Arrays.asList(
+ 8128, "coordination-fragment-matrix", 1));
+
+ @Test
+ void splitRootAndEventPreserveProcessSemanticsAcrossRepresentations() {
+ Scenario scenario = Scenario.create();
+ SemanticProjection baseline = null;
+
+ for (Variant variant : Variant.matrix()) {
+ Run run = execute(scenario, variant);
+ assertLocalityAndCheckpoint(run);
+ SemanticProjection projection =
+ SemanticProjection.of(run.debug);
+ if (baseline == null) {
+ baseline = projection;
+ } else {
+ assertEquals(
+ baseline,
+ projection,
+ "semantic drift for " + variant);
+ }
+ }
+
+ assertNotNull(baseline);
+ assertEquals(ProcessorStatus.SUCCESS, baseline.status);
+ assertEquals("processed", baseline.rootValue);
+ assertEquals(8, Variant.matrix().size());
+ }
+
+ private static Run execute(
+ Scenario scenario,
+ Variant variant) {
+ StrictFragmentProvider fragments =
+ new StrictFragmentProvider(
+ scenario.allowedFragments,
+ scenario.forbiddenFragments);
+ if (variant.warm) {
+ fragments.warmAllowed();
+ }
+
+ BlueRuntimeTypeRegistry runtimeTypes =
+ BlueRuntimeTypeRegistry.getDefault();
+ Blue blue = new Blue(new SequentialNodeProvider(
+ runtimeTypes.asProvider(),
+ fragments));
+ CountingMockHandlerProcessor handlers =
+ new CountingMockHandlerProcessor();
+ DocumentProcessor processor = DocumentProcessor.builder()
+ .withMatchingService(
+ new ContractMatchingService(blue))
+ .withConformanceEngine(
+ blue.conformanceEngine())
+ .withSnapshotManager(
+ CoordinationRoutingHarness
+ .snapshotManager(blue))
+ .withGasSchedule(GasSchedule.contracts10())
+ .withRuntimeRegistryIdentity(
+ RuntimeBlueIds
+ .REGISTRY_PACKAGE_IDENTITY)
+ .registerContractProcessor(
+ MockTypeBlueIds.MOCK_EXTERNAL_CHANNEL,
+ runtimeTypes.node(
+ RuntimeTypeKey
+ .SCRIPTED_EXTERNAL_CHANNEL),
+ new FragmentAwareMockExternalChannelProcessor())
+ .registerContractProcessor(
+ MockTypeBlueIds.MOCK_HANDLER,
+ runtimeTypes.node(
+ RuntimeTypeKey.SCRIPTED_HANDLER),
+ handlers)
+ .withExternalDeliveryPlanDeriver(
+ (root, event) -> scenario.plan)
+ .build();
+ try {
+ fragments.resetRequests();
+ ProcessingDebugResult debug =
+ processor.processDocumentWithTrace(
+ variant.document(scenario),
+ variant.event(scenario));
+ return new Run(
+ variant,
+ scenario,
+ debug,
+ fragments.requests(),
+ handlers.executions());
+ } finally {
+ processor.close();
+ blue.close();
+ }
+ }
+
+ private static void assertLocalityAndCheckpoint(
+ Run run) {
+ String context = run.variant.toString();
+ DocumentProcessingResult result =
+ run.debug.processResult();
+
+ assertEquals(
+ ProcessorStatus.SUCCESS,
+ result.status(),
+ context + ": "
+ + diagnosticProjection(
+ result.diagnostic()));
+ assertEquals(
+ "processed",
+ textAt(result.document(), "state"),
+ context);
+ assertEquals(
+ 1,
+ run.handlerExecutions,
+ context + ": exactly one Handler must execute");
+ assertEquals(
+ run.variant.documentForm
+ == DocumentForm.INLINE
+ ? 0
+ : 1,
+ frequency(
+ run.providerRequests,
+ run.scenario.selectedBodyBlueId),
+ context + ": selected executable-body demand");
+ assertTrue(
+ Collections.disjoint(
+ run.providerRequests,
+ run.scenario.forbiddenBlueIds),
+ context + ": forbidden demand "
+ + run.providerRequests);
+ assertFalse(
+ run.providerRequests.contains(
+ run.scenario.archiveBlueId),
+ context + ": large unrelated archive was fetched");
+
+ assertEquals(
+ Collections.singletonList(
+ run.scenario.emittedEventBlueId),
+ nodeBlueIds(result.events()),
+ context + ": Root event drift");
+ assertEquals(
+ 1,
+ run.debug.trace()
+ .records(
+ ProcessingTraceRecord.Kind
+ .CHECKPOINT_WRITE)
+ .size(),
+ context + ": checkpoint write count");
+ assertEquals(
+ SELECTED_CHANNEL,
+ run.debug.trace()
+ .records(
+ ProcessingTraceRecord.Kind
+ .CHECKPOINT_WRITE)
+ .get(0)
+ .contractKey(),
+ context + ": checkpoint source ownership");
+
+ Node checkpoint = result.document()
+ .getContracts()
+ .getProperties()
+ .get("checkpoint");
+ assertNotNull(checkpoint, context);
+ Node entries = checkpoint.getProperties()
+ .get("entries");
+ assertNotNull(entries, context);
+ Node selected = entries.getProperties()
+ .get(SELECTED_CHANNEL);
+ assertNotNull(selected, context);
+ assertEquals(
+ run.scenario.selectedCheckpointDomain,
+ selected.getProperties()
+ .get("domain")
+ .getBlueId(),
+ context);
+ assertEquals(
+ run.scenario.eventBlueId,
+ BlueIdCalculator.calculateBlueId(
+ selected.getProperties()
+ .get("subject")),
+ context);
+ assertNull(
+ entries.getProperties()
+ .get(REJECTED_CHANNEL),
+ context + ": rejected source acquired a checkpoint");
+ }
+
+ private enum DocumentForm {
+ INLINE,
+ PURE_REFERENCE,
+ DIRECT_FRAGMENT
+ }
+
+ private enum EventForm {
+ INLINE,
+ PURE_REFERENCE,
+ DIRECT_FRAGMENT
+ }
+
+ private static final class Variant {
+ private final String label;
+ private final DocumentForm documentForm;
+ private final EventForm eventForm;
+ private final boolean warm;
+
+ private Variant(
+ String label,
+ DocumentForm documentForm,
+ EventForm eventForm,
+ boolean warm) {
+ this.label = label;
+ this.documentForm = documentForm;
+ this.eventForm = eventForm;
+ this.warm = warm;
+ }
+
+ private static List matrix() {
+ return Arrays.asList(
+ new Variant(
+ "A inline/inline/cold",
+ DocumentForm.INLINE,
+ EventForm.INLINE,
+ false),
+ new Variant(
+ "B Root-ref/inline/cold",
+ DocumentForm.PURE_REFERENCE,
+ EventForm.INLINE,
+ false),
+ new Variant(
+ "C inline/Event-ref/cold",
+ DocumentForm.INLINE,
+ EventForm.PURE_REFERENCE,
+ false),
+ new Variant(
+ "D Root-ref/Event-ref/cold",
+ DocumentForm.PURE_REFERENCE,
+ EventForm.PURE_REFERENCE,
+ false),
+ new Variant(
+ "E direct/direct/cold",
+ DocumentForm.DIRECT_FRAGMENT,
+ EventForm.DIRECT_FRAGMENT,
+ false),
+ new Variant(
+ "F Root-ref/Event-ref/warm",
+ DocumentForm.PURE_REFERENCE,
+ EventForm.PURE_REFERENCE,
+ true),
+ new Variant(
+ "G direct/Event-ref/cold",
+ DocumentForm.DIRECT_FRAGMENT,
+ EventForm.PURE_REFERENCE,
+ false),
+ new Variant(
+ "H Root-ref/direct/cold",
+ DocumentForm.PURE_REFERENCE,
+ EventForm.DIRECT_FRAGMENT,
+ false));
+ }
+
+ private Node document(
+ Scenario scenario) {
+ switch (documentForm) {
+ case INLINE:
+ return scenario.inlineRoot.clone();
+ case PURE_REFERENCE:
+ return new Node().blueId(
+ scenario.rootBlueId);
+ case DIRECT_FRAGMENT:
+ return scenario.directRoot.clone();
+ default:
+ throw new IllegalStateException(
+ "Unhandled document form");
+ }
+ }
+
+ private Node event(
+ Scenario scenario) {
+ switch (eventForm) {
+ case INLINE:
+ return scenario.inlineEvent.clone();
+ case PURE_REFERENCE:
+ return new Node().blueId(
+ scenario.eventBlueId);
+ case DIRECT_FRAGMENT:
+ return scenario.directEvent.clone();
+ default:
+ throw new IllegalStateException(
+ "Unhandled event form");
+ }
+ }
+
+ @Override
+ public String toString() {
+ return label;
+ }
+ }
+
+ private static final class Scenario {
+ private final Node inlineRoot;
+ private final Node directRoot;
+ private final Node inlineEvent;
+ private final Node directEvent;
+ private final String rootBlueId;
+ private final String eventBlueId;
+ private final String selectedBodyBlueId;
+ private final String archiveBlueId;
+ private final String emittedEventBlueId;
+ private final String selectedCheckpointDomain;
+ private final Map allowedFragments;
+ private final Map forbiddenFragments;
+ private final Set forbiddenBlueIds;
+ private final ExternalDeliveryPlan plan;
+
+ private Scenario(
+ Node inlineRoot,
+ Node directRoot,
+ Node inlineEvent,
+ Node directEvent,
+ String rootBlueId,
+ String eventBlueId,
+ String selectedBodyBlueId,
+ String archiveBlueId,
+ String emittedEventBlueId,
+ String selectedCheckpointDomain,
+ Map allowedFragments,
+ Map forbiddenFragments,
+ ExternalDeliveryPlan plan) {
+ this.inlineRoot = inlineRoot;
+ this.directRoot = directRoot;
+ this.inlineEvent = inlineEvent;
+ this.directEvent = directEvent;
+ this.rootBlueId = rootBlueId;
+ this.eventBlueId = eventBlueId;
+ this.selectedBodyBlueId =
+ selectedBodyBlueId;
+ this.archiveBlueId = archiveBlueId;
+ this.emittedEventBlueId =
+ emittedEventBlueId;
+ this.selectedCheckpointDomain =
+ selectedCheckpointDomain;
+ this.allowedFragments =
+ immutableNodes(allowedFragments);
+ this.forbiddenFragments =
+ immutableNodes(forbiddenFragments);
+ this.forbiddenBlueIds =
+ Collections.unmodifiableSet(
+ new LinkedHashSet<>(
+ forbiddenFragments.keySet()));
+ this.plan = plan;
+ }
+
+ private static Scenario create() {
+ Node emitted = new Node()
+ .properties(
+ "kind",
+ scalar("matrix-result"))
+ .properties(
+ "id",
+ scalar("result-1"));
+ String emittedEventBlueId =
+ BlueIdCalculator.calculateBlueId(
+ emitted);
+ Node selectedBody = new Node()
+ .properties(
+ "patches",
+ list(new Node()
+ .properties(
+ "op",
+ scalar("replace"))
+ .properties(
+ "path",
+ scalar("/state"))
+ .properties(
+ "val",
+ scalar("processed"))))
+ .properties(
+ "events",
+ list(emitted));
+ String selectedBodyBlueId =
+ BlueIdCalculator.calculateBlueId(
+ selectedBody);
+
+ List unselectedBodies =
+ new ArrayList<>();
+ for (int index = 0; index < 4; index++) {
+ unselectedBodies.add(
+ largeBody(
+ "unselected-" + index,
+ (char) ('a' + index)));
+ }
+
+ Node archive = new Node()
+ .properties(
+ "kind",
+ scalar("unrelated-archive"))
+ .properties(
+ "payload",
+ scalar(padding(
+ LARGE_VALUE_SIZE * 3,
+ 'z')));
+ String archiveBlueId =
+ BlueIdCalculator.calculateBlueId(
+ archive);
+
+ Node contracts = new Node()
+ .properties(
+ "initialized",
+ new Node()
+ .type(reference(
+ RuntimeBlueIds
+ .PROCESSING_INITIALIZED_MARKER))
+ .properties(
+ "documentId",
+ scalar(
+ "coordination-fragment-matrix")));
+ Node selectedChannel = channel(
+ 0, true, CHECKPOINT_DISCRIMINATOR);
+ Node rejectedChannel = channel(
+ 1, false, "rejected-domain");
+ contracts.properties(
+ SELECTED_CHANNEL,
+ selectedChannel);
+ contracts.properties(
+ REJECTED_CHANNEL,
+ rejectedChannel);
+ contracts.properties(
+ SELECTED_HANDLER,
+ handler(
+ SELECTED_CHANNEL,
+ 0,
+ null,
+ selectedBody));
+ for (int index = 0; index < 4; index++) {
+ contracts.properties(
+ "unselectedWorkflow" + index,
+ handler(
+ REJECTED_CHANNEL,
+ index + 1,
+ "never-" + index,
+ unselectedBodies.get(index)));
+ }
+
+ Node inlineRoot = new Node()
+ .properties(
+ "state",
+ scalar("pending"))
+ .properties(
+ "archive",
+ archive.clone())
+ .contracts(contracts);
+ String rootBlueId =
+ BlueIdCalculator.calculateBlueId(
+ inlineRoot);
+
+ ContractProcessorRegistry splitterRegistry =
+ ContractProcessorRegistryBuilder
+ .create()
+ .register(
+ new MockHandlerProcessor())
+ .build();
+ CoordinationDocumentSplitter splitter =
+ new CoordinationDocumentSplitter(
+ splitterRegistry);
+ CoordinationDocumentSplitter.SplitGraph document =
+ splitter.splitDocument(inlineRoot);
+ assertEquals(rootBlueId, document.rootBlueId());
+ assertEquals(
+ 5,
+ bodyFragmentCount(
+ document.metadata()),
+ "all five Handler bodies must be independently retained");
+
+ Node directRoot =
+ document.fragmentedRoot();
+ directRoot.getProperties().put(
+ "archive",
+ reference(archiveBlueId));
+ assertEquals(
+ rootBlueId,
+ BlueIdCalculator.calculateBlueId(
+ directRoot),
+ "unrelated archive cut must preserve the Root BlueId");
+
+ Node eventMessage = new Node()
+ .properties(
+ "kind",
+ scalar("unrelated-event-message"))
+ .properties(
+ "payload",
+ scalar(padding(
+ LARGE_VALUE_SIZE,
+ 'm')));
+ Node inlineEvent = new Node()
+ .properties(
+ "subscriptionKey",
+ scalar(SUBSCRIPTION_KEY))
+ .properties(
+ "kind",
+ scalar("selected"))
+ .properties(
+ "id",
+ scalar("fragment-event-1"))
+ .properties(
+ "message",
+ eventMessage);
+ String eventBlueId =
+ BlueIdCalculator.calculateBlueId(
+ inlineEvent);
+ CoordinationDocumentSplitter.SplitGraph event =
+ splitter.splitEvent(inlineEvent);
+ CoordinationDocumentSplitter.SplitGraph message =
+ splitter.splitEvent(eventMessage);
+ assertEquals(eventBlueId, event.rootBlueId());
+
+ Map forbidden =
+ new LinkedHashMap<>();
+ forbidden.put(
+ archiveBlueId,
+ archive.clone());
+ for (Node unselectedBody : unselectedBodies) {
+ putExact(
+ forbidden,
+ unselectedBody);
+ }
+ forbidden.putAll(
+ message.fragments());
+
+ Map allowed =
+ new LinkedHashMap<>(
+ document.fragments());
+ allowed.put(
+ rootBlueId,
+ directRoot.clone());
+ allowed.putAll(
+ event.fragments());
+ for (String forbiddenBlueId :
+ forbidden.keySet()) {
+ allowed.remove(forbiddenBlueId);
+ }
+ assertTrue(
+ allowed.containsKey(
+ selectedBodyBlueId),
+ "selected body must remain provider-available");
+
+ String selectedContribution =
+ BlueIdCalculator.calculateBlueId(
+ selectedChannel);
+ String rejectedContribution =
+ BlueIdCalculator.calculateBlueId(
+ rejectedChannel);
+ String selectedDomain =
+ CheckpointDomain.derive(
+ MockTypeBlueIds
+ .MOCK_EXTERNAL_CHANNEL,
+ Collections.singletonList(
+ selectedContribution),
+ CHECKPOINT_DISCRIMINATOR);
+ String rejectedDomain =
+ CheckpointDomain.derive(
+ MockTypeBlueIds
+ .MOCK_EXTERNAL_CHANNEL,
+ Collections.singletonList(
+ rejectedContribution),
+ "rejected-domain");
+ ExternalDeliveryPlan plan =
+ ExternalDeliveryPlan.builder()
+ .revisions(41L, 41L)
+ .eventOrderKey(EVENT_ORDER)
+ .delivery(delivery(
+ SELECTED_CHANNEL,
+ 0,
+ selectedContribution,
+ selectedDomain,
+ eventBlueId))
+ .delivery(delivery(
+ REJECTED_CHANNEL,
+ 1,
+ rejectedContribution,
+ rejectedDomain,
+ eventBlueId))
+ .activeSubscriptionInterval(
+ active(
+ SELECTED_CHANNEL,
+ 0,
+ selectedContribution,
+ selectedDomain))
+ .activeSubscriptionInterval(
+ active(
+ REJECTED_CHANNEL,
+ 1,
+ rejectedContribution,
+ rejectedDomain))
+ .exactRuntimeState()
+ .build();
+
+ return new Scenario(
+ inlineRoot,
+ directRoot,
+ inlineEvent,
+ event.fragmentedRoot(),
+ rootBlueId,
+ eventBlueId,
+ selectedBodyBlueId,
+ archiveBlueId,
+ emittedEventBlueId,
+ selectedDomain,
+ allowed,
+ forbidden,
+ plan);
+ }
+ }
+
+ private static final class CountingMockHandlerProcessor
+ implements HandlerProcessor {
+ private final MockHandlerProcessor delegate =
+ new MockHandlerProcessor();
+ private final AtomicInteger executions =
+ new AtomicInteger();
+
+ @Override
+ public Class contractType() {
+ return delegate.contractType();
+ }
+
+ @Override
+ public List executableBodyFields() {
+ return delegate.executableBodyFields();
+ }
+
+ @Override
+ public boolean matches(
+ MockHandler contract,
+ HandlerMatchContext context) {
+ return delegate.matches(contract, context);
+ }
+
+ @Override
+ public void execute(
+ MockHandler contract,
+ ProcessorExecutionContext context) {
+ executions.incrementAndGet();
+ delegate.execute(contract, context);
+ }
+
+ private int executions() {
+ return executions.get();
+ }
+ }
+
+ /**
+ * The published fixture's legacy evaluate method reads the event as an
+ * already expanded object. This adapter keeps its immutable subscription
+ * functions and makes the occurrence evaluation representation-blind; the
+ * verified plan has already performed exact key preselection.
+ */
+ private static final class FragmentAwareMockExternalChannelProcessor
+ implements ChannelProcessor {
+ private final ExternalChannelSubscriptionFunctions<
+ MockExternalChannel> subscriptions =
+ new ExternalChannelSubscriptionFunctions<
+ MockExternalChannel>() {
+ @Override
+ public List channelKeys(
+ MockExternalChannel contract) {
+ return Collections.singletonList(
+ contract.getSubscriptionKey());
+ }
+
+ @Override
+ public boolean accepts(
+ MockExternalChannel contract,
+ Node exactEvent) {
+ /*
+ * This method is the compatibility projection used
+ * after the revision-complete plan has already
+ * selected the exact occurrence.
+ */
+ return !Boolean.FALSE.equals(
+ contract.getAccept());
+ }
+
+ @Override
+ public boolean accepts(
+ MockExternalChannel contract,
+ Node exactEvent,
+ blue.language.processor
+ .ExternalChannelFunctionContext
+ context) {
+ return !Boolean.FALSE.equals(
+ contract.getAccept())
+ && preselects(
+ contract,
+ exactEvent,
+ context);
+ }
+
+ @Override
+ public String checkpointDomainDiscriminator(
+ MockExternalChannel contract) {
+ return contract
+ .getCheckpointDomain();
+ }
+ };
+
+ @Override
+ public Class contractType() {
+ return MockExternalChannel.class;
+ }
+
+ @Override
+ public ExternalChannelSubscriptionFunctions<
+ MockExternalChannel> externalSubscriptionFunctions() {
+ return subscriptions;
+ }
+
+ @Override
+ public ChannelEvaluation evaluate(
+ MockExternalChannel contract,
+ ChannelEvaluationContext context) {
+ return Boolean.FALSE.equals(
+ contract.getAccept())
+ ? ChannelEvaluation.noMatch()
+ : ChannelEvaluation.match(
+ context.event(), null);
+ }
+ }
+
+ private static final class StrictFragmentProvider
+ implements NodeProvider {
+ private final Map allowed;
+ private final Map forbidden;
+ private final Map cache =
+ new LinkedHashMap<>();
+ private final List requests =
+ new ArrayList<>();
+
+ private StrictFragmentProvider(
+ Map allowed,
+ Map forbidden) {
+ this.allowed =
+ new LinkedHashMap<>(allowed);
+ this.forbidden =
+ new LinkedHashMap<>(forbidden);
+ }
+
+ @Override
+ public synchronized List fetchByBlueId(
+ String blueId) {
+ if (forbidden.containsKey(blueId)) {
+ throw new AssertionError(
+ "PROCESS demanded forbidden fragment "
+ + blueId);
+ }
+ Node exact = allowed.get(blueId);
+ if (exact == null) {
+ throw new AssertionError(
+ "PROCESS escaped the exact-fragment "
+ + "allow-list: " + blueId);
+ }
+ requests.add(blueId);
+ Node cached = cache.get(blueId);
+ if (cached == null) {
+ cached = exact.clone();
+ cache.put(blueId, cached);
+ }
+ return Collections.singletonList(
+ cached.clone());
+ }
+
+ private synchronized void warmAllowed() {
+ for (Map.Entry entry :
+ allowed.entrySet()) {
+ cache.put(
+ entry.getKey(),
+ entry.getValue().clone());
+ }
+ }
+
+ private synchronized void resetRequests() {
+ requests.clear();
+ }
+
+ private synchronized List requests() {
+ return Collections.unmodifiableList(
+ new ArrayList<>(requests));
+ }
+ }
+
+ private static final class Run {
+ private final Variant variant;
+ private final Scenario scenario;
+ private final ProcessingDebugResult debug;
+ private final List providerRequests;
+ private final int handlerExecutions;
+
+ private Run(
+ Variant variant,
+ Scenario scenario,
+ ProcessingDebugResult debug,
+ List providerRequests,
+ int handlerExecutions) {
+ this.variant = variant;
+ this.scenario = scenario;
+ this.debug = debug;
+ this.providerRequests = providerRequests;
+ this.handlerExecutions =
+ handlerExecutions;
+ }
+ }
+
+ private static final class SemanticProjection {
+ private final ProcessorStatus status;
+ private final String rootValue;
+ private final String resultingRootBlueId;
+ private final List rootEventBlueIds;
+ private final String diagnostic;
+ private final long totalGas;
+ private final List gas;
+ private final List trace;
+ private final String checkpointBlueId;
+
+ private SemanticProjection(
+ ProcessorStatus status,
+ String rootValue,
+ String resultingRootBlueId,
+ List rootEventBlueIds,
+ String diagnostic,
+ long totalGas,
+ List gas,
+ List trace,
+ String checkpointBlueId) {
+ this.status = status;
+ this.rootValue = rootValue;
+ this.resultingRootBlueId =
+ resultingRootBlueId;
+ this.rootEventBlueIds =
+ rootEventBlueIds;
+ this.diagnostic = diagnostic;
+ this.totalGas = totalGas;
+ this.gas = gas;
+ this.trace = trace;
+ this.checkpointBlueId =
+ checkpointBlueId;
+ }
+
+ private static SemanticProjection of(
+ ProcessingDebugResult debug) {
+ DocumentProcessingResult result =
+ debug.processResult();
+ Node checkpoint = result.document()
+ .getContracts()
+ .getProperties()
+ .get("checkpoint");
+ return new SemanticProjection(
+ result.status(),
+ textAt(result.document(), "state"),
+ BlueIdCalculator.calculateBlueId(
+ result.document()),
+ nodeBlueIds(result.events()),
+ diagnosticProjection(
+ result.diagnostic()),
+ result.totalGas(),
+ gasProjection(debug.trace()),
+ traceProjection(debug.trace()),
+ BlueIdCalculator.calculateBlueId(
+ checkpoint));
+ }
+
+ @Override
+ public boolean equals(
+ Object other) {
+ if (!(other
+ instanceof SemanticProjection)) {
+ return false;
+ }
+ SemanticProjection that =
+ (SemanticProjection) other;
+ return status == that.status
+ && totalGas == that.totalGas
+ && Objects.equals(
+ rootValue, that.rootValue)
+ && resultingRootBlueId.equals(
+ that.resultingRootBlueId)
+ && rootEventBlueIds.equals(
+ that.rootEventBlueIds)
+ && Objects.equals(
+ diagnostic, that.diagnostic)
+ && gas.equals(that.gas)
+ && trace.equals(that.trace)
+ && checkpointBlueId.equals(
+ that.checkpointBlueId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ status,
+ rootValue,
+ resultingRootBlueId,
+ rootEventBlueIds,
+ diagnostic,
+ totalGas,
+ gas,
+ trace,
+ checkpointBlueId);
+ }
+
+ @Override
+ public String toString() {
+ return "SemanticProjection{"
+ + "status=" + status
+ + ", rootValue=" + rootValue
+ + ", rootBlueId="
+ + resultingRootBlueId
+ + ", events="
+ + rootEventBlueIds
+ + ", totalGas="
+ + totalGas
+ + '}';
+ }
+ }
+
+ private static List gasProjection(
+ ProcessingConformanceTrace trace) {
+ List projection =
+ new ArrayList<>();
+ for (GasTraceEntry entry : trace.gas()) {
+ projection.add(
+ entry.sequence()
+ + "|" + entry.namespace()
+ + "|" + entry.counter()
+ + "|" + entry.quantity()
+ + "|" + entry.weight()
+ + "|" + entry.subtotal()
+ + "|" + entry.scopePath()
+ + "|" + entry.contractKey()
+ + "|" + entry.logicalPath()
+ + "|" + entry.reason());
+ }
+ return Collections.unmodifiableList(
+ projection);
+ }
+
+ private static List traceProjection(
+ ProcessingConformanceTrace trace) {
+ List projection =
+ new ArrayList<>();
+ for (ProcessingTraceRecord record :
+ trace.records()) {
+ Node node = record.node();
+ projection.add(
+ record.sequence()
+ + "|" + record.kind()
+ + "|" + record.scopePath()
+ + "|" + record.contractKey()
+ + "|" + record.logicalPath()
+ + "|" + record.details()
+ + "|" + (node != null
+ ? BlueIdCalculator
+ .calculateBlueId(node)
+ : null));
+ }
+ return Collections.unmodifiableList(
+ projection);
+ }
+
+ private static Node channel(
+ int order,
+ boolean accept,
+ String domain) {
+ return new Node()
+ .type(reference(
+ MockTypeBlueIds
+ .MOCK_EXTERNAL_CHANNEL))
+ .properties(
+ "order",
+ scalar(order))
+ .properties(
+ "subscriptionKey",
+ scalar(SUBSCRIPTION_KEY))
+ .properties(
+ "eventKey",
+ scalar(SUBSCRIPTION_KEY))
+ .properties(
+ "accept",
+ scalar(accept))
+ .properties(
+ "checkpointDomain",
+ scalar(domain));
+ }
+
+ private static Node handler(
+ String channel,
+ int order,
+ String eventKind,
+ Node body) {
+ Node handler = new Node()
+ .type(reference(
+ MockTypeBlueIds.MOCK_HANDLER))
+ .properties(
+ "channel",
+ scalar(channel))
+ .properties(
+ "order",
+ scalar(order))
+ .properties(
+ "result",
+ body.clone());
+ if (eventKind != null) {
+ handler.properties(
+ "event",
+ new Node().properties(
+ "kind",
+ scalar(eventKind)));
+ }
+ return handler;
+ }
+
+ private static Node largeBody(
+ String tag,
+ char padding) {
+ return new Node()
+ .properties(
+ "patches",
+ new Node().items(
+ Collections
+ .emptyList()))
+ .properties(
+ "events",
+ new Node().items(
+ Collections
+ .emptyList()))
+ .properties(
+ "tag",
+ scalar(tag))
+ .properties(
+ "payload",
+ scalar(padding(
+ LARGE_VALUE_SIZE,
+ padding)));
+ }
+
+ private static ExternalDeliverySnapshot delivery(
+ String channel,
+ int order,
+ String contribution,
+ String domain,
+ String eventBlueId) {
+ return ExternalDeliverySnapshot.builder(
+ "/", channel)
+ .order(order)
+ .sourceContribution(contribution)
+ .effectiveTypeBlueId(
+ MockTypeBlueIds
+ .MOCK_EXTERNAL_CHANNEL)
+ .subscriptionKey(
+ SUBSCRIPTION_KEY)
+ .checkpointDomainBlueId(domain)
+ .checkpointSubjectBlueId(
+ eventBlueId)
+ .build();
+ }
+
+ private static SubscriptionDelta.Entry active(
+ String channel,
+ int order,
+ String contribution,
+ String domain) {
+ return new SubscriptionDelta.Entry(
+ "/",
+ channel,
+ MockTypeBlueIds.MOCK_EXTERNAL_CHANNEL,
+ Collections.singletonList(
+ contribution),
+ order,
+ Collections.singletonList(
+ SUBSCRIPTION_KEY),
+ domain,
+ 0L,
+ null,
+ null);
+ }
+
+ private static int bodyFragmentCount(
+ List
+ metadata) {
+ int count = 0;
+ for (CoordinationDocumentSplitter.FragmentMetadata entry :
+ metadata) {
+ if (entry.kind()
+ == CoordinationDocumentSplitter
+ .FragmentKind.EXECUTABLE_BODY) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ private static Map immutableNodes(
+ Map source) {
+ Map result =
+ new LinkedHashMap<>();
+ for (Map.Entry entry :
+ source.entrySet()) {
+ result.put(
+ entry.getKey(),
+ entry.getValue().clone());
+ }
+ return Collections.unmodifiableMap(
+ result);
+ }
+
+ private static String putExact(
+ Map target,
+ Node exact) {
+ String blueId =
+ BlueIdCalculator.calculateBlueId(exact);
+ target.put(blueId, exact.clone());
+ return blueId;
+ }
+
+ private static Node list(
+ Node... values) {
+ return new Node().items(
+ Arrays.asList(values));
+ }
+
+ private static Node scalar(
+ Object value) {
+ return new Node().value(value);
+ }
+
+ private static Node reference(
+ String blueId) {
+ return new Node().blueId(blueId);
+ }
+
+ private static String padding(
+ int length,
+ char value) {
+ char[] values = new char[length];
+ Arrays.fill(values, value);
+ return new String(values);
+ }
+
+ private static String textAt(
+ Node root,
+ String property) {
+ Node value = root != null
+ && root.getProperties() != null
+ ? root.getProperties().get(
+ property)
+ : null;
+ return value != null
+ && value.getValue() != null
+ ? String.valueOf(
+ value.getValue())
+ : null;
+ }
+
+ private static List nodeBlueIds(
+ List nodes) {
+ List result =
+ new ArrayList<>(nodes.size());
+ for (Node node : nodes) {
+ result.add(
+ BlueIdCalculator.calculateBlueId(
+ node));
+ }
+ return Collections.unmodifiableList(
+ result);
+ }
+
+ private static int frequency(
+ List values,
+ String expected) {
+ int count = 0;
+ for (String value : values) {
+ if (expected.equals(value)) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ private static String diagnosticProjection(
+ ProcessorDiagnostic diagnostic) {
+ return diagnostic == null
+ ? null
+ : diagnostic.category()
+ + "|" + diagnostic.message()
+ + "|" + diagnostic.details();
+ }
+}
diff --git a/src/test/java/blue/coordination/processor/CoordinationDocumentSplitterTest.java b/src/test/java/blue/coordination/processor/CoordinationDocumentSplitterTest.java
new file mode 100644
index 0000000..f0b84f6
--- /dev/null
+++ b/src/test/java/blue/coordination/processor/CoordinationDocumentSplitterTest.java
@@ -0,0 +1,853 @@
+package blue.coordination.processor;
+
+import blue.language.NodeProvider;
+import blue.language.model.Node;
+import blue.language.processor.ExternalOrderKey;
+import blue.language.processor.VerifiedExecutionEvidence;
+import blue.language.processor.registry.RuntimeBlueIds;
+import blue.language.provider.NodeProviderOutcome;
+import blue.language.provider.NodeProviderResult;
+import blue.language.provider.SequentialNodeProvider;
+import blue.language.utils.BlueIdCalculator;
+import blue.language.utils.NodePathEditor;
+import blue.language.utils.NodeTransformer;
+import blue.language.utils.UncheckedObjectMapper;
+import blue.repo.coordination.ChatWorkflowOperation;
+import blue.repo.coordination.Operation;
+import blue.repo.coordination.OperationRequest;
+import blue.repo.coordination.SequentialWorkflow;
+import blue.repo.coordination.SequentialWorkflowOperation;
+import blue.repo.coordination.TimelineEntry;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class CoordinationDocumentSplitterTest {
+
+ private final CoordinationDocumentSplitter splitter =
+ new CoordinationDocumentSplitter();
+
+ @Test
+ void documentSplittingCutsEmbeddedRootsAndRegisteredBodiesOnly() {
+ Fixture fixture = fixture();
+ String exactRootBlueId =
+ BlueIdCalculator.calculateBlueId(fixture.root);
+
+ CoordinationDocumentSplitter.SplitGraph split =
+ splitter.splitDocument(fixture.root);
+
+ assertEquals(exactRootBlueId, split.rootBlueId());
+ assertEquals(
+ exactRootBlueId,
+ BlueIdCalculator.calculateBlueId(
+ split.fragmentedRoot()));
+ assertEquals(
+ exactRootBlueId,
+ split.pureReference().getBlueId());
+
+ Node fragmentedRoot = split.fragmentedRoot();
+ Node childReference =
+ NodePathEditor.getOrNull(
+ fragmentedRoot, "/child");
+ assertNotNull(childReference);
+ assertTrue(childReference.isReferenceOnly());
+ assertEquals(
+ fixture.childBlueId,
+ childReference.getBlueId());
+
+ Node sibling =
+ NodePathEditor.getOrNull(
+ fragmentedRoot, "/sibling");
+ assertNotNull(sibling);
+ assertFalse(
+ sibling.isReferenceOnly(),
+ "an unrelated application sibling remains inline");
+
+ Node rootSelectedBody =
+ NodePathEditor.getOrNull(
+ fragmentedRoot,
+ "/contracts/rootOperation/steps");
+ assertTrue(rootSelectedBody.isReferenceOnly());
+ assertEquals(
+ fixture.rootBodyBlueId,
+ rootSelectedBody.getBlueId());
+
+ Node chatSelectedBody =
+ NodePathEditor.getOrNull(
+ fragmentedRoot,
+ "/contracts/chatOperation/steps");
+ assertTrue(chatSelectedBody.isReferenceOnly());
+ assertEquals(
+ fixture.rootBodyBlueId,
+ chatSelectedBody.getBlueId(),
+ "identical bodies share one content identity");
+
+ Node referencedBody =
+ NodePathEditor.getOrNull(
+ fragmentedRoot,
+ "/contracts/referencedOperation/steps");
+ assertTrue(referencedBody.isReferenceOnly());
+ assertEquals(
+ fixture.referencedBodyBlueId,
+ referencedBody.getBlueId());
+ assertFalse(
+ split.fragments().containsKey(
+ fixture.referencedBodyBlueId),
+ "an already-referenced body is not claimed as local content");
+
+ Node unregisteredSteps =
+ NodePathEditor.getOrNull(
+ fragmentedRoot,
+ "/contracts/plainOperation/steps");
+ assertNotNull(unregisteredSteps);
+ assertFalse(
+ unregisteredSteps.isReferenceOnly(),
+ "a steps-shaped field is not executable without exact registry metadata");
+
+ Node childFragment =
+ fetchOne(
+ split.provider(),
+ fixture.childBlueId);
+ assertEquals(
+ fixture.childBlueId,
+ BlueIdCalculator.calculateBlueId(
+ childFragment));
+ Node grandchildReference =
+ NodePathEditor.getOrNull(
+ childFragment, "/grandchild");
+ assertNotNull(grandchildReference);
+ assertTrue(
+ grandchildReference.isReferenceOnly());
+ assertEquals(
+ fixture.grandchildBlueId,
+ grandchildReference.getBlueId());
+ assertTrue(
+ NodePathEditor.getOrNull(
+ childFragment,
+ "/contracts/childWorkflow/steps")
+ .isReferenceOnly());
+
+ Node rootBody =
+ fetchOne(
+ split.provider(),
+ fixture.rootBodyBlueId);
+ assertEquals(
+ fixture.rootBodyBlueId,
+ BlueIdCalculator.calculateBlueId(
+ rootBody));
+ assertFalse(
+ rootBody.getItems().get(0)
+ .isReferenceOnly(),
+ "an executable body is retained as one complete coarse fragment");
+
+ assertTrue(hasMetadata(
+ split.metadata(),
+ CoordinationDocumentSplitter.FragmentKind.EMBEDDED_ROOT,
+ "/child"));
+ assertTrue(hasMetadata(
+ split.metadata(),
+ CoordinationDocumentSplitter.FragmentKind.EMBEDDED_ROOT,
+ "/child/grandchild"));
+ assertTrue(hasMetadata(
+ split.metadata(),
+ CoordinationDocumentSplitter.FragmentKind.EXECUTABLE_BODY,
+ "/contracts/rootOperation/steps"));
+ assertTrue(hasMetadata(
+ split.metadata(),
+ CoordinationDocumentSplitter.FragmentKind.EXECUTABLE_BODY,
+ "/child/contracts/childWorkflow/steps"));
+ assertTrue(hasMetadata(
+ split.metadata(),
+ CoordinationDocumentSplitter.FragmentKind.EXECUTABLE_BODY,
+ "/contracts/chatOperation/steps"));
+ assertEquals(
+ 2,
+ metadataCount(
+ split.metadata(),
+ CoordinationDocumentSplitter.FragmentKind.EXECUTABLE_BODY,
+ fixture.rootBodyBlueId),
+ "both registered handlers retain occurrence metadata");
+ assertEquals(
+ 1,
+ fragmentKeyCount(
+ split.fragments(),
+ fixture.rootBodyBlueId),
+ "identical executable body content is stored once");
+
+ Node reconstructed =
+ reconstructAvailable(
+ split.pureReference(),
+ split.provider());
+ assertEquals(
+ UncheckedObjectMapper.JSON_MAPPER.valueToTree(
+ split.originalRoot()),
+ UncheckedObjectMapper.JSON_MAPPER.valueToTree(
+ reconstructed),
+ "recursively materializing every local fragment reconstructs the document");
+ assertEquals(
+ split.rootBlueId(),
+ BlueIdCalculator.calculateBlueId(
+ reconstructed));
+
+ Map defensive =
+ split.fragments();
+ defensive.get(split.rootBlueId())
+ .properties("tampered", scalar("yes"));
+ assertEquals(
+ split.rootBlueId(),
+ BlueIdCalculator.calculateBlueId(
+ fetchOne(
+ split.provider(),
+ split.rootBlueId())));
+ }
+
+ @Test
+ void eventSplittingUsesExactDirectFragments() {
+ Node message = new Node()
+ .properties(
+ "operation", scalar("increment"),
+ "channel", scalar("bob"),
+ "payload", new Node().properties(
+ "amount", scalar(3L)));
+ Node event = new Node()
+ .properties(
+ "timeline", scalar("alice"),
+ "actor", scalar("alice"),
+ "message", message);
+
+ CoordinationDocumentSplitter.SplitGraph split =
+ splitter.splitEvent(event);
+
+ assertEquals(
+ BlueIdCalculator.calculateBlueId(event),
+ split.rootBlueId());
+ Node fragmented =
+ split.fragmentedRoot();
+ Node messageReference =
+ NodePathEditor.getOrNull(
+ fragmented, "/message");
+ assertNotNull(messageReference);
+ assertTrue(messageReference.isReferenceOnly());
+ assertEquals(
+ BlueIdCalculator.calculateBlueId(message),
+ messageReference.getBlueId());
+ assertEquals(
+ split.rootBlueId(),
+ BlueIdCalculator.calculateBlueId(fragmented));
+ assertEquals(
+ NodeProviderOutcome.FOUND,
+ split.provider()
+ .fetchResultByBlueId(
+ messageReference.getBlueId())
+ .outcome());
+ assertEquals(
+ NodeProviderOutcome.NOT_FOUND,
+ split.provider()
+ .fetchResultByBlueId(
+ SequentialWorkflow.blueId())
+ .outcome());
+ assertTrue(hasMetadata(
+ split.metadata(),
+ CoordinationDocumentSplitter.FragmentKind.EVENT_ROOT,
+ "/"));
+ Node reconstructed =
+ reconstructAvailable(
+ split.pureReference(),
+ split.provider());
+ assertEquals(
+ UncheckedObjectMapper.JSON_MAPPER.valueToTree(
+ split.originalRoot()),
+ UncheckedObjectMapper.JSON_MAPPER.valueToTree(
+ reconstructed));
+ assertEquals(
+ split.rootBlueId(),
+ BlueIdCalculator.calculateBlueId(
+ reconstructed));
+ }
+
+ @Test
+ void typedTimelineEntryRetainsExternalCyclicMemberType() {
+ assertTrue(
+ TimelineEntry.blueId().contains("#"),
+ "the published Timeline Entry type is a cyclic-set member");
+ Node operationRequest = new Node()
+ .type(reference(
+ OperationRequest.blueId()))
+ .properties(
+ "operation", scalar("increment"),
+ "channel", scalar("bob"),
+ "request", new Node().properties(
+ "amount", scalar(3L)));
+ Node timelineEntry = new Node()
+ .type(reference(
+ TimelineEntry.blueId()))
+ .properties(
+ "timeline", scalar("alice"),
+ "actor", scalar("alice"),
+ "message", operationRequest);
+
+ CoordinationDocumentSplitter.SplitGraph split =
+ splitter.splitEvent(timelineEntry);
+
+ assertEquals(
+ TimelineEntry.blueId(),
+ split.fragmentedRoot()
+ .getType()
+ .getBlueId());
+ Node messageReference =
+ NodePathEditor.getOrNull(
+ split.fragmentedRoot(),
+ "/message");
+ assertNotNull(messageReference);
+ assertTrue(messageReference.isReferenceOnly());
+ Node messageFragment =
+ fetchOne(
+ split.provider(),
+ messageReference.getBlueId());
+ assertEquals(
+ OperationRequest.blueId(),
+ messageFragment.getType().getBlueId());
+ assertEquals(
+ NodeProviderOutcome.NOT_FOUND,
+ split.provider()
+ .fetchResultByBlueId(
+ TimelineEntry.blueId())
+ .outcome(),
+ "external cyclic type content is never claimed as a local fragment");
+ for (String blueId
+ : split.fragments().keySet()) {
+ assertFalse(
+ blueId.contains("#"),
+ "only ordinary exact local fragment identities are retained");
+ }
+
+ Node reconstructed =
+ reconstructAvailable(
+ split.pureReference(),
+ split.provider());
+ assertEquals(
+ UncheckedObjectMapper.JSON_MAPPER.valueToTree(
+ timelineEntry),
+ UncheckedObjectMapper.JSON_MAPPER.valueToTree(
+ reconstructed));
+ assertEquals(
+ split.rootBlueId(),
+ BlueIdCalculator.calculateBlueId(
+ reconstructed));
+ }
+
+ @Test
+ void preparedInputContainsOnlyTwoPureReferencesAndLazyVerifiedProvider() {
+ Fixture fixture = fixture();
+ Node event = new Node()
+ .properties(
+ "timeline", scalar("alice"),
+ "message", scalar("hello"));
+ CoordinationDocumentSplitter.SplitGraph document =
+ splitter.splitDocument(fixture.root);
+ CoordinationDocumentSplitter.SplitGraph splitEvent =
+ splitter.splitEvent(event);
+ NodeProvider combined =
+ new SequentialNodeProvider(
+ document.provider(),
+ splitEvent.provider());
+ int[] providerCalls = {0};
+ NodeProvider counted = blueId -> {
+ providerCalls[0]++;
+ return combined.fetchByBlueId(blueId);
+ };
+ VerifiedExecutionEvidence evidence =
+ evidence(
+ document.rootBlueId(),
+ splitEvent.rootBlueId());
+
+ CoordinationDocumentSplitter.PreparedProcessingInput prepared =
+ splitter.prepareForProcessing(
+ document.rootBlueId(),
+ splitEvent.rootBlueId(),
+ evidence,
+ counted);
+
+ assertTrue(prepared.document().isReferenceOnly());
+ assertTrue(prepared.event().isReferenceOnly());
+ assertEquals(
+ 0,
+ providerCalls[0],
+ "preparation must not consume cold provider fragments");
+ assertEquals(
+ document.rootBlueId(),
+ prepared.document().getBlueId());
+ assertEquals(
+ splitEvent.rootBlueId(),
+ prepared.event().getBlueId());
+ assertSame(evidence, prepared.evidence());
+ assertEquals(
+ NodeProviderOutcome.FOUND,
+ prepared.provider()
+ .fetchResultByBlueId(
+ document.rootBlueId())
+ .outcome());
+ assertEquals(
+ NodeProviderOutcome.FOUND,
+ prepared.provider()
+ .fetchResultByBlueId(
+ splitEvent.rootBlueId())
+ .outcome());
+ assertEquals(2, providerCalls[0]);
+
+ prepared.document().blueId(
+ SequentialWorkflow.blueId());
+ assertEquals(
+ document.rootBlueId(),
+ prepared.document().getBlueId(),
+ "prepared semantic inputs are defensive copies");
+
+ VerifiedExecutionEvidence wrongEvent =
+ evidence(
+ document.rootBlueId(),
+ fixture.childBlueId);
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> splitter.prepareForProcessing(
+ document.rootBlueId(),
+ splitEvent.rootBlueId(),
+ wrongEvent,
+ combined));
+ CoordinationDocumentSplitter.PreparedProcessingInput
+ missingEvent =
+ splitter.prepareForProcessing(
+ document.rootBlueId(),
+ splitEvent.rootBlueId(),
+ evidence,
+ document.provider());
+ assertEquals(
+ NodeProviderOutcome.NOT_FOUND,
+ missingEvent.provider()
+ .fetchResultByBlueId(
+ splitEvent.rootBlueId())
+ .outcome());
+
+ CoordinationDocumentSplitter.PreparedProcessingInput
+ missingRoot =
+ splitter.prepareForProcessing(
+ document.rootBlueId(),
+ splitEvent.rootBlueId(),
+ evidence,
+ splitEvent.provider());
+ assertEquals(
+ NodeProviderOutcome.NOT_FOUND,
+ missingRoot.provider()
+ .fetchResultByBlueId(
+ document.rootBlueId())
+ .outcome());
+
+ NodeProvider invalidRoot = blueId ->
+ document.rootBlueId().equals(blueId)
+ ? Collections.singletonList(
+ scalar("wrong-root"))
+ : combined.fetchByBlueId(blueId);
+ CoordinationDocumentSplitter.PreparedProcessingInput
+ invalidRootInput =
+ splitter.prepareForProcessing(
+ document.rootBlueId(),
+ splitEvent.rootBlueId(),
+ evidence,
+ invalidRoot);
+ assertEquals(
+ NodeProviderOutcome.INVALID_EVIDENCE,
+ invalidRootInput.provider()
+ .fetchResultByBlueId(
+ document.rootBlueId())
+ .outcome());
+
+ NodeProvider invalidEvent = blueId ->
+ splitEvent.rootBlueId().equals(blueId)
+ ? Collections.singletonList(
+ scalar("wrong-event"))
+ : combined.fetchByBlueId(blueId);
+ CoordinationDocumentSplitter.PreparedProcessingInput
+ invalidEventInput =
+ splitter.prepareForProcessing(
+ document.rootBlueId(),
+ splitEvent.rootBlueId(),
+ evidence,
+ invalidEvent);
+ assertEquals(
+ NodeProviderOutcome.INVALID_EVIDENCE,
+ invalidEventInput.provider()
+ .fetchResultByBlueId(
+ splitEvent.rootBlueId())
+ .outcome());
+ }
+
+ @Test
+ void malformedEmbeddedPathsFailBeforeProducingFragments() {
+ Node root = new Node()
+ .contracts(new Node().properties(
+ "embedded",
+ processEmbedded("/")));
+
+ IllegalArgumentException invalid =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> splitter.splitDocument(root));
+
+ assertTrue(
+ invalid.getMessage().contains(
+ "cannot embed its declaring scope"));
+ }
+
+ @Test
+ void overlappingEmbeddedPathsCutAtNearestDeclaredAncestor() {
+ Node grandchild = new Node()
+ .properties(
+ "state",
+ scalar("grandchild"));
+ Node child = new Node()
+ .properties(
+ "state",
+ scalar("child"),
+ "grandchild",
+ grandchild);
+ Node root = new Node()
+ .properties(
+ "state",
+ scalar("root"),
+ "child",
+ child)
+ .contracts(new Node().properties(
+ "embedded",
+ processEmbedded(
+ "/child",
+ "/child/grandchild")));
+ String rootBlueId =
+ BlueIdCalculator.calculateBlueId(root);
+ String childBlueId =
+ BlueIdCalculator.calculateBlueId(child);
+ String grandchildBlueId =
+ BlueIdCalculator.calculateBlueId(
+ grandchild);
+
+ CoordinationDocumentSplitter.SplitGraph split =
+ splitter.splitDocument(root);
+
+ Node rootFragment =
+ fetchOne(
+ split.provider(),
+ rootBlueId);
+ Node childReference =
+ NodePathEditor.getOrNull(
+ rootFragment,
+ "/child");
+ assertNotNull(childReference);
+ assertTrue(
+ childReference.isReferenceOnly(),
+ "the ancestor cut remains a pure reference");
+ assertEquals(
+ childBlueId,
+ childReference.getBlueId());
+
+ Node childFragment =
+ fetchOne(
+ split.provider(),
+ childBlueId);
+ Node grandchildReference =
+ NodePathEditor.getOrNull(
+ childFragment,
+ "/grandchild");
+ assertNotNull(grandchildReference);
+ assertTrue(
+ grandchildReference.isReferenceOnly(),
+ "the descendant is cut inside its nearest declared ancestor fragment");
+ assertEquals(
+ grandchildBlueId,
+ grandchildReference.getBlueId());
+ assertEquals(
+ childBlueId,
+ BlueIdCalculator.calculateBlueId(
+ childFragment));
+
+ Node grandchildFragment =
+ fetchOne(
+ split.provider(),
+ grandchildBlueId);
+ assertEquals(
+ grandchildBlueId,
+ BlueIdCalculator.calculateBlueId(
+ grandchildFragment));
+ assertEquals(
+ 3,
+ split.fragments().size(),
+ "every declared Root is retained exactly once");
+
+ Node reconstructed =
+ reconstructAvailable(
+ split.pureReference(),
+ split.provider());
+ assertEquals(
+ UncheckedObjectMapper.JSON_MAPPER.valueToTree(
+ root),
+ UncheckedObjectMapper.JSON_MAPPER.valueToTree(
+ reconstructed));
+ assertEquals(
+ rootBlueId,
+ BlueIdCalculator.calculateBlueId(
+ reconstructed));
+ }
+
+ private static Fixture fixture() {
+ Node grandchildBody =
+ body("grandchild-step");
+ Node grandchild = new Node()
+ .properties(
+ "state", scalar("grandchild"))
+ .contracts(new Node().properties(
+ "grandchildWorkflow",
+ workflow(
+ SequentialWorkflow.blueId(),
+ grandchildBody)));
+
+ Node childBody =
+ body("child-step");
+ Node child = new Node()
+ .properties(
+ "state", scalar("child"),
+ "grandchild", grandchild)
+ .contracts(new Node().properties(
+ "embedded",
+ processEmbedded("/grandchild"),
+ "childWorkflow",
+ workflow(
+ SequentialWorkflow.blueId(),
+ childBody)));
+
+ Node rootBody =
+ body("root-step");
+ Node rootOperation = workflow(
+ SequentialWorkflowOperation.blueId(),
+ rootBody);
+ Node chatOperation = workflow(
+ ChatWorkflowOperation.blueId(),
+ rootBody.clone());
+ String referencedBodyBlueId =
+ BlueIdCalculator.calculateBlueId(
+ body("already-external"));
+ Node referencedOperation = workflow(
+ SequentialWorkflowOperation.blueId(),
+ reference(referencedBodyBlueId));
+ Node plainSteps =
+ body("must-remain-inline");
+ Node plainOperation = workflow(
+ Operation.blueId(),
+ plainSteps);
+ Node sibling = new Node()
+ .properties(
+ "largeData",
+ scalar(
+ "this application subtree is not a Coordination scope"));
+ Node rootContracts = new Node()
+ .properties(
+ "embedded",
+ processEmbedded("/child"),
+ "rootOperation",
+ rootOperation,
+ "chatOperation",
+ chatOperation,
+ "referencedOperation",
+ referencedOperation)
+ .properties(
+ "plainOperation",
+ plainOperation);
+ Node root = new Node()
+ .properties(
+ "state", scalar("root"),
+ "child", child,
+ "sibling", sibling)
+ .contracts(rootContracts);
+ return new Fixture(
+ root,
+ BlueIdCalculator.calculateBlueId(
+ child),
+ BlueIdCalculator.calculateBlueId(
+ grandchild),
+ BlueIdCalculator.calculateBlueId(
+ rootBody),
+ referencedBodyBlueId);
+ }
+
+ private static Node processEmbedded(
+ String... paths) {
+ List pathNodes =
+ new ArrayList<>();
+ for (String path : paths) {
+ pathNodes.add(scalar(path));
+ }
+ return new Node()
+ .type(reference(
+ RuntimeBlueIds.PROCESS_EMBEDDED))
+ .properties(
+ "paths",
+ new Node().items(pathNodes));
+ }
+
+ private static Node workflow(
+ String typeBlueId,
+ Node steps) {
+ return new Node()
+ .type(reference(typeBlueId))
+ .properties(
+ "channel", scalar("timeline"),
+ "steps", steps);
+ }
+
+ private static Node body(
+ String label) {
+ return new Node()
+ .items(Collections.singletonList(
+ new Node().properties(
+ "label", scalar(label),
+ "payload",
+ new Node().properties(
+ "amount",
+ scalar(label)))));
+ }
+
+ private static Node scalar(
+ Object value) {
+ return new Node().value(value);
+ }
+
+ private static Node reference(
+ String blueId) {
+ return new Node().blueId(blueId);
+ }
+
+ private static Node fetchOne(
+ NodeProvider provider,
+ String blueId) {
+ NodeProviderResult result =
+ provider.fetchResultByBlueId(blueId);
+ assertEquals(
+ NodeProviderOutcome.FOUND,
+ result.outcome());
+ assertEquals(1, result.nodes().size());
+ return result.nodes().get(0);
+ }
+
+ private static Node reconstructAvailable(
+ Node root,
+ NodeProvider provider) {
+ return NodeTransformer.transform(
+ root,
+ node -> {
+ if (!node.isReferenceOnly()) {
+ return node;
+ }
+ NodeProviderResult result =
+ provider.fetchResultByBlueId(
+ node.getBlueId());
+ if (result.outcome()
+ == NodeProviderOutcome.NOT_FOUND) {
+ return node;
+ }
+ assertEquals(
+ NodeProviderOutcome.FOUND,
+ result.outcome());
+ assertEquals(1, result.nodes().size());
+ return result.nodes().get(0);
+ });
+ }
+
+ private static boolean hasMetadata(
+ List metadata,
+ CoordinationDocumentSplitter.FragmentKind kind,
+ String pointer) {
+ for (CoordinationDocumentSplitter.FragmentMetadata entry
+ : metadata) {
+ if (entry.kind() == kind
+ && pointer.equals(entry.pointer())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static int metadataCount(
+ List metadata,
+ CoordinationDocumentSplitter.FragmentKind kind,
+ String blueId) {
+ int count = 0;
+ for (CoordinationDocumentSplitter.FragmentMetadata entry
+ : metadata) {
+ if (entry.kind() == kind
+ && blueId.equals(entry.blueId())) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ private static int fragmentKeyCount(
+ Map fragments,
+ String blueId) {
+ int count = 0;
+ for (String key : fragments.keySet()) {
+ if (blueId.equals(key)) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ private static VerifiedExecutionEvidence evidence(
+ String rootBlueId,
+ String eventBlueId) {
+ return VerifiedExecutionEvidence
+ .builder(rootBlueId, eventBlueId)
+ .revisions(7L, 7L)
+ .runtimeRegistryIdentity(
+ "coordination-splitter-test")
+ .eventOrderKey(
+ ExternalOrderKey.of(
+ Arrays.asList(
+ 12L,
+ "entry")))
+ .build();
+ }
+
+ private static final class Fixture {
+
+ private final Node root;
+ private final String childBlueId;
+ private final String grandchildBlueId;
+ private final String rootBodyBlueId;
+ private final String referencedBodyBlueId;
+
+ private Fixture(
+ Node root,
+ String childBlueId,
+ String grandchildBlueId,
+ String rootBodyBlueId,
+ String referencedBodyBlueId) {
+ this.root = root;
+ this.childBlueId = childBlueId;
+ this.grandchildBlueId =
+ grandchildBlueId;
+ this.rootBodyBlueId =
+ rootBodyBlueId;
+ this.referencedBodyBlueId =
+ referencedBodyBlueId;
+ }
+ }
+}
diff --git a/src/test/java/blue/coordination/processor/CoordinationProcessorsTest.java b/src/test/java/blue/coordination/processor/CoordinationProcessorsTest.java
index ef3d123..c6be57e 100644
--- a/src/test/java/blue/coordination/processor/CoordinationProcessorsTest.java
+++ b/src/test/java/blue/coordination/processor/CoordinationProcessorsTest.java
@@ -50,6 +50,18 @@ void configureBuilderRegistersCoordinationProcessors() {
assertCoordinationProcessorsRegistered(processor);
}
+ @Test
+ void workflowProcessorsDeclareOnlyStepsAsDeferredExecutableBody() {
+ assertEquals(Collections.singletonList("steps"),
+ new SequentialWorkflowProcessor().executableBodyFields());
+ assertEquals(Collections.singletonList("steps"),
+ new SequentialWorkflowOperationProcessor()
+ .executableBodyFields());
+ assertEquals(Collections.singletonList("steps"),
+ new ChatWorkflowOperationProcessor()
+ .executableBodyFields());
+ }
+
@Test
void registerWithBlueInstallsOptionsMetricsAsLanguageSink() {
BexProcessingMetrics metrics = new BexProcessingMetrics();
@@ -143,7 +155,7 @@ void optionsMetricsReceiveRealLanguageMultiPatchSequenceCallbacks() {
"increment", "ownerChannel", new Node().value(7))));
BexProcessingMetrics.Snapshot after = metrics.snapshot();
- assertFalse(processed.capabilityFailure(), processed.failureReason());
+ assertFalse(blue.coordination.processor.ProcessingResultTestSupport.isCapabilityFailure(processed), blue.coordination.processor.ProcessingResultTestSupport.diagnosticMessage(processed));
assertEquals(BigInteger.valueOf(3), processed.document().get("/counter"));
assertEquals(1L, after.preparedPatchSequences - before.preparedPatchSequences);
assertEquals(3L, after.preparedPatches - before.preparedPatches);
@@ -182,7 +194,7 @@ void realRepositoryCoordinationContractsLoadAndInitialize() {
DocumentProcessingResult result = fixture.blue.initializeDocument(preprocessed);
- assertFalse(result.capabilityFailure(), result.failureReason());
+ assertFalse(blue.coordination.processor.ProcessingResultTestSupport.isCapabilityFailure(result), blue.coordination.processor.ProcessingResultTestSupport.diagnosticMessage(result));
assertTrue(fixture.blue.isInitialized(result.document()));
assertEquals(BigInteger.ZERO, result.document().getProperties().get("counter").getValue());
assertFalse(contracts(result.document()).containsKey("checkpoint"));
@@ -203,7 +215,7 @@ void sequentialWorkflowOperationWithMissingChannelDoesNotRun() {
CoordinationTestResources.operationRequest(
"increment", "missingChannel", new Node().value(7))));
- assertFalse(processed.capabilityFailure(), processed.failureReason());
+ assertFalse(blue.coordination.processor.ProcessingResultTestSupport.isCapabilityFailure(processed), blue.coordination.processor.ProcessingResultTestSupport.diagnosticMessage(processed));
assertEquals(BigInteger.ZERO, processed.document().getProperties().get("counter").getValue());
}
diff --git a/src/test/java/blue/coordination/processor/CoordinationTestResources.java b/src/test/java/blue/coordination/processor/CoordinationTestResources.java
index 279c9c4..aec4afc 100644
--- a/src/test/java/blue/coordination/processor/CoordinationTestResources.java
+++ b/src/test/java/blue/coordination/processor/CoordinationTestResources.java
@@ -66,7 +66,9 @@ public static Map testTypeAliases(BlueRepository repository) {
}
public static Blue configuredBlue(BlueRepository repository) {
- return repository.configure(new Blue());
+ return new Blue()
+ .nodeProvider(repository.nodeProvider())
+ .typeClassResolver(repository.typeClassResolver());
}
public static String simpleTimelineChannelYaml(String key, String timelineId, int indent) {
diff --git a/src/test/java/blue/coordination/processor/CounterSnapshotRoundTripStressTest.java b/src/test/java/blue/coordination/processor/CounterSnapshotRoundTripStressTest.java
index 1dcff03..94fbd94 100644
--- a/src/test/java/blue/coordination/processor/CounterSnapshotRoundTripStressTest.java
+++ b/src/test/java/blue/coordination/processor/CounterSnapshotRoundTripStressTest.java
@@ -12,7 +12,6 @@
import blue.language.snapshot.ResolvedSnapshot;
import blue.language.provider.BasicNodeProvider;
import blue.language.provider.SequentialNodeProvider;
-import blue.language.utils.NodeProviderWrapper;
import blue.repo.BlueRepository;
import blue.repo.coordination.ChatMessage;
import blue.repo.coordination.TimelineChannel;
@@ -34,7 +33,8 @@ void bexOnlyCounterUpdatesSurviveCanonicalSnapshotRoundTrips() {
DocumentProcessingResult initialized = fixture.blue.initializeDocument(
fixture.blue.preprocess(bexOnlyCounterDocument(fixture.counterIncrementHandlerBlueId)
.blue(fixture.repository.typeAliasBlue())));
- ResolvedSnapshot currentSnapshot = initialized.snapshot();
+ ResolvedSnapshot currentSnapshot =
+ ProcessingResultTestSupport.snapshot(fixture.blue, initialized);
assertNotNull(currentSnapshot);
long started = System.nanoTime();
@@ -50,19 +50,23 @@ void bexOnlyCounterUpdatesSurviveCanonicalSnapshotRoundTrips() {
i,
chatMessage("tick " + i));
if (i > 1) {
- // Canonical child fragments may omit context-derived types; compare the
- // resolved event view when treating the nested timeline as a document.
- Node previous = currentSnapshot.resolvedNodeAt(
- "/contracts/checkpoint/lastEvents/ownerChannel");
- CoordinationEventNodes.TimelineEntryView previousEntry =
- CoordinationEventNodes.timelineEntry(previous);
+ Node previousSubject = currentSnapshot.resolvedNodeAt(
+ "/contracts/checkpoint/entries/ownerChannel/subject");
CoordinationEventNodes.TimelineEntryView currentEntry =
CoordinationEventNodes.timelineEntry(event);
- assertNotNull(previousEntry);
+ assertNotNull(previousSubject);
assertNotNull(currentEntry);
- assertTrue(BlueSemanticIdentity.equals(
- currentEntry.timeline(), previousEntry.timeline()));
- assertTrue(currentEntry.timestamp().compareTo(previousEntry.timestamp()) > 0);
+ assertEquals(
+ TimelineExternalSubscriptionFunctions
+ .TIMELINE_ORDER_SUBJECT_VERSION,
+ TimelineProviderSupport.textProperty(
+ previousSubject, "semantics"));
+ Node previousTimestamp =
+ TimelineProviderSupport.property(
+ previousSubject, "timestamp");
+ assertNotNull(previousTimestamp);
+ assertTrue(currentEntry.timestamp().compareTo(
+ (BigInteger) previousTimestamp.getValue()) > 0);
TimelineChannel channel = fixture.blue.nodeToObject(
currentSnapshot.resolvedNodeAt("/contracts/ownerChannel"),
TimelineChannel.class);
@@ -71,25 +75,33 @@ void bexOnlyCounterUpdatesSurviveCanonicalSnapshotRoundTrips() {
DocumentProcessingResult result = fixture.blue.processDocument(currentSnapshot, event);
- assertNotNull(result.snapshot(), "iteration " + i + " should return a snapshot");
- assertNotNull(result.blueId(), "iteration " + i + " should return a BlueId");
+ ResolvedSnapshot resultSnapshot =
+ ProcessingResultTestSupport.snapshot(fixture.blue, result);
+ String resultBlueId = ProcessingResultTestSupport.blueId(result);
+ assertNotNull(resultSnapshot,
+ "iteration " + i + " should return a snapshot");
+ assertNotNull(resultBlueId,
+ "iteration " + i + " should return a BlueId");
assertTrue(result.totalGas() > 0, "iteration " + i + " should charge gas");
- assertEquals(1, result.triggeredEvents().size(), "iteration " + i + " should emit one event");
- assertEquals(BigInteger.valueOf(i), result.resolvedDocument().get("/counter"));
- assertCounterMessage(result.triggeredEvents().get(0), i);
+ assertEquals(1, result.events().size(), "iteration " + i + " should emit one event");
+ assertEquals(BigInteger.valueOf(i),
+ ProcessingResultTestSupport.resolvedDocument(
+ fixture.blue, result).get("/counter"));
+ assertCounterMessage(result.events().get(0), i);
totalGas += result.totalGas();
maxGas = Math.max(maxGas, result.totalGas());
minGas = Math.min(minGas, result.totalGas());
- finalBlueId = result.blueId();
+ finalBlueId = resultBlueId;
- String canonicalJson = fixture.blue.nodeToJson(result.canonicalDocument());
+ String canonicalJson = fixture.blue.nodeToJson(result.document());
Fixture coldFixture = configuredFixture();
Node parsedCanonical = coldFixture.blue.parseSourceJson(canonicalJson);
ResolvedSnapshot loadedSnapshot = coldFixture.blue.loadSnapshot(parsedCanonical);
- assertEquals(result.blueId(), loadedSnapshot.blueId(), "iteration " + i + " should preserve BlueId");
- assertSnapshotRoundTrip(result.snapshot(), loadedSnapshot);
+ assertEquals(resultBlueId, loadedSnapshot.blueId(),
+ "iteration " + i + " should preserve BlueId");
+ assertSnapshotRoundTrip(resultSnapshot, loadedSnapshot);
currentSnapshot = loadedSnapshot;
fixture = coldFixture;
}
@@ -182,7 +194,7 @@ private static Fixture configuredFixture() {
String counterIncrementHandlerBlueId = testTypes.getBlueIdByName(
"Counter Increment Handler");
blue.nodeProvider(new SequentialNodeProvider(
- NodeProviderWrapper.unverified(testTypes), repositoryProvider));
+ testTypes, repositoryProvider));
CoordinationProcessors.registerWith(blue);
blue.registerExternalContractType(counterIncrementHandlerBlueId,
counterIncrementHandlerType,
diff --git a/src/test/java/blue/coordination/processor/DeclaredTypeEventMatchingTest.java b/src/test/java/blue/coordination/processor/DeclaredTypeEventMatchingTest.java
index df44a33..0399dc7 100644
--- a/src/test/java/blue/coordination/processor/DeclaredTypeEventMatchingTest.java
+++ b/src/test/java/blue/coordination/processor/DeclaredTypeEventMatchingTest.java
@@ -8,7 +8,6 @@
import blue.language.processor.HandlerMatchContextFactory;
import blue.language.provider.SequentialNodeProvider;
import blue.language.utils.BlueIdCalculator;
-import blue.language.utils.NodeProviderWrapper;
import blue.repo.BlueRepository;
import blue.repo.coordination.ChatWorkflowOperation;
import blue.repo.coordination.OperationRequest;
@@ -313,10 +312,13 @@ private static TypeFixture create() {
}
private Blue configuredBlue() {
- Blue blue = BlueRepository.latest().configure(new Blue());
+ BlueRepository repository = BlueRepository.latest();
+ Blue blue = new Blue()
+ .nodeProvider(repository.nodeProvider())
+ .typeClassResolver(repository.typeClassResolver());
NodeProvider repositoryProvider = blue.getNodeProvider();
blue.nodeProvider(new SequentialNodeProvider(
- NodeProviderWrapper.unverified(new MapProvider(definitions)),
+ new MapProvider(definitions),
repositoryProvider));
return blue;
}
diff --git a/src/test/java/blue/coordination/processor/EmbeddedTerminationWorkflowTest.java b/src/test/java/blue/coordination/processor/EmbeddedTerminationWorkflowTest.java
index ef51c0e..a8140c6 100644
--- a/src/test/java/blue/coordination/processor/EmbeddedTerminationWorkflowTest.java
+++ b/src/test/java/blue/coordination/processor/EmbeddedTerminationWorkflowTest.java
@@ -26,7 +26,8 @@ void terminateProcessingInEmbeddedWorkflowTerminatesOnlyEmbeddedScope() {
assertSuccess(childResult);
assertEquals("changed-before-stop", childResult.document().get("/child/status"));
- assertEquals("graceful", childResult.document().get("/child/contracts/terminated/cause"));
+ assertEquals("embedded-workflow-complete",
+ childResult.document().get("/child/contracts/terminated/cause"));
assertEquals("embedded-complete", childResult.document().get("/child/contracts/terminated/reason"));
assertNull(nodeAt(childResult.document(), "/contracts/terminated"));
assertEquals(1L, fixture.metrics.declarativeTerminationSteps());
@@ -37,7 +38,8 @@ void terminateProcessingInEmbeddedWorkflowTerminatesOnlyEmbeddedScope() {
assertSuccess(rootResult);
assertEquals("root-still-active", rootResult.document().get("/rootStatus"));
assertNull(nodeAt(rootResult.document(), "/contracts/terminated"));
- assertEquals("graceful", rootResult.document().get("/child/contracts/terminated/cause"));
+ assertEquals("embedded-workflow-complete",
+ rootResult.document().get("/child/contracts/terminated/cause"));
}
@Test
@@ -78,8 +80,11 @@ private static Node documentWithEmbeddedTermination(boolean computeTermination)
childContracts.put("childChannel", TestTimelineProvider.channel("child"));
childContracts.put("runChild", operation("childChannel",
updateStep("/status", "changed-before-stop"),
- computeTermination ? computeTerminateStep("embedded-complete")
- : declarativeTerminateStep("embedded-complete"),
+ computeTermination
+ ? computeTerminateStep(
+ "embedded-workflow-complete", "embedded-complete")
+ : declarativeTerminateStep(
+ "embedded-workflow-complete", "embedded-complete"),
updateStep("/status", "must-not-run")));
return new Node()
@@ -109,18 +114,20 @@ private static Node updateStep(String path, String value) {
.properties("val", new Node().value(value))));
}
- private static Node declarativeTerminateStep(String reason) {
+ private static Node declarativeTerminateStep(String cause, String reason) {
return new Node()
.type("Coordination/Terminate Processing")
+ .properties("cause", new Node().value(cause))
.properties("reason", new Node().value(reason));
}
- private static Node computeTerminateStep(String reason) {
+ private static Node computeTerminateStep(String cause, String reason) {
return new Node()
.type("Coordination/Compute")
.properties("do", new Node().items(new Node()
.properties("$return", new Node()
.properties("termination", new Node()
+ .properties("cause", new Node().value(cause))
.properties("reason", new Node().value(reason))))));
}
@@ -134,7 +141,7 @@ private static Node nodeAt(Node node, String pointer) {
}
private static void assertSuccess(DocumentProcessingResult result) {
- assertEquals(ProcessorStatus.SUCCESS, result.status(), result.failureReason());
+ assertEquals(ProcessorStatus.SUCCESS, result.status(), blue.coordination.processor.ProcessingResultTestSupport.diagnosticMessage(result));
}
private static Fixture fixture() {
diff --git a/src/test/java/blue/coordination/processor/InheritedStaticUpdateDocumentTest.java b/src/test/java/blue/coordination/processor/InheritedStaticUpdateDocumentTest.java
index 9ce1530..b6ef4d5 100644
--- a/src/test/java/blue/coordination/processor/InheritedStaticUpdateDocumentTest.java
+++ b/src/test/java/blue/coordination/processor/InheritedStaticUpdateDocumentTest.java
@@ -8,7 +8,6 @@
import blue.language.processor.registry.RuntimeBlueIds;
import blue.language.provider.BasicNodeProvider;
import blue.language.provider.SequentialNodeProvider;
-import blue.language.utils.NodeProviderWrapper;
import blue.repo.BlueRepository;
import blue.repo.coordination.DocumentStatus;
import blue.repo.coordination.SequentialWorkflow;
@@ -25,7 +24,10 @@ class InheritedStaticUpdateDocumentTest {
@Test
void inheritedStaticPatchWritesItsAuthoredValueFromTheResolvedContractView() {
- Blue blue = BlueRepository.latest().configure(new Blue());
+ BlueRepository repository = BlueRepository.latest();
+ Blue blue = new Blue()
+ .nodeProvider(repository.nodeProvider())
+ .typeClassResolver(repository.typeClassResolver());
NodeProvider repositoryProvider = blue.getNodeProvider();
BasicNodeProvider documentTypes = new BasicNodeProvider();
documentTypes.addSingleNodes(documentType(new Node()
@@ -33,19 +35,21 @@ void inheritedStaticPatchWritesItsAuthoredValueFromTheResolvedContractView() {
.type(reference(StatusInProgress.blueId()))));
String documentTypeId = documentTypes.getBlueIdByName("Inherited Static Update Document");
blue.nodeProvider(new SequentialNodeProvider(
- NodeProviderWrapper.unverified(documentTypes),
+ documentTypes,
repositoryProvider));
CoordinationProcessors.registerWith(blue);
DocumentProcessingResult result = blue.initializeDocument(
blue.resolveToSnapshot(new Node().type(reference(documentTypeId))));
- assertEquals(ProcessorStatus.SUCCESS, result.status(), result.failureReason());
- assertNull(result.failureReason());
- Node canonicalStatus = result.canonicalDocument().getProperties().get("status");
+ assertEquals(ProcessorStatus.SUCCESS, result.status(),
+ ProcessingResultTestSupport.diagnosticMessage(result));
+ assertNull(result.diagnostic());
+ Node canonicalStatus = result.document().getProperties().get("status");
assertEquals(StatusInProgress.blueId(), canonicalStatus.getType().getBlueId());
assertEquals("Authored status", canonicalStatus.getName());
- assertEquals("active", result.resolvedDocument().getAsText("/status/mode"));
+ assertEquals("active", ProcessingResultTestSupport
+ .resolvedDocument(blue, result).getAsText("/status/mode"));
assertNull(canonicalStatus.getDescription(),
"metadata inherited by Json Patch Entry.val must not become document content");
}
diff --git a/src/test/java/blue/coordination/processor/MustUnderstandContractsTest.java b/src/test/java/blue/coordination/processor/MustUnderstandContractsTest.java
index c82b76d..e5139c0 100644
--- a/src/test/java/blue/coordination/processor/MustUnderstandContractsTest.java
+++ b/src/test/java/blue/coordination/processor/MustUnderstandContractsTest.java
@@ -46,7 +46,7 @@ void timelineChannelIsSupportedWhenUsedDirectly() {
DocumentProcessingResult result = initialize(fixture, document);
- assertFalse(result.capabilityFailure(), result.failureReason());
+ assertFalse(blue.coordination.processor.ProcessingResultTestSupport.isCapabilityFailure(result), blue.coordination.processor.ProcessingResultTestSupport.diagnosticMessage(result));
assertTrue(fixture.blue.isInitialized(result.document()));
}
@@ -62,7 +62,7 @@ void handlerBoundToTimelineChannelInitializes() {
DocumentProcessingResult result = initialize(fixture, document);
- assertFalse(result.capabilityFailure(), result.failureReason());
+ assertFalse(blue.coordination.processor.ProcessingResultTestSupport.isCapabilityFailure(result), blue.coordination.processor.ProcessingResultTestSupport.diagnosticMessage(result));
assertTrue(fixture.blue.isInitialized(result.document()));
}
@@ -95,7 +95,7 @@ void simpleTimelineProviderWorksWhenRegistered() {
1,
TestTimelineProvider.chatMessage("hello")));
- assertFalse(result.capabilityFailure(), result.failureReason());
+ assertFalse(blue.coordination.processor.ProcessingResultTestSupport.isCapabilityFailure(result), blue.coordination.processor.ProcessingResultTestSupport.diagnosticMessage(result));
assertNotNull(checkpointEvent(result.document(), "owner"));
}
@@ -104,10 +104,10 @@ private static DocumentProcessingResult initialize(Fixture fixture, Node documen
}
private static void assertCapabilityFailure(DocumentProcessingResult result, String reason) {
- assertTrue(result.capabilityFailure(), result.failureReason());
- assertTrue(result.failureReason().contains(reason), result.failureReason());
+ assertTrue(blue.coordination.processor.ProcessingResultTestSupport.isCapabilityFailure(result), blue.coordination.processor.ProcessingResultTestSupport.diagnosticMessage(result));
+ assertTrue(blue.coordination.processor.ProcessingResultTestSupport.diagnosticMessage(result).contains(reason), blue.coordination.processor.ProcessingResultTestSupport.diagnosticMessage(result));
assertEquals(0L, result.totalGas());
- assertTrue(result.triggeredEvents().isEmpty());
+ assertTrue(result.events().isEmpty());
assertFalse(hasInitializedMarker(result.document()));
}
@@ -119,8 +119,9 @@ private static boolean hasInitializedMarker(Node document) {
private static Node checkpointEvent(Node document, String key) {
Node contracts = property(document, "contracts");
Node checkpoint = property(contracts, "checkpoint");
- Node lastEvents = property(checkpoint, "lastEvents");
- return property(lastEvents, key);
+ Node entries = property(checkpoint, "entries");
+ Node entry = property(entries, key);
+ return property(entry, "subject");
}
private static Map contract(String key, Node contract) {
diff --git a/src/test/java/blue/coordination/processor/OperationRequestLogicalRoutingTest.java b/src/test/java/blue/coordination/processor/OperationRequestLogicalRoutingTest.java
new file mode 100644
index 0000000..4f86b91
--- /dev/null
+++ b/src/test/java/blue/coordination/processor/OperationRequestLogicalRoutingTest.java
@@ -0,0 +1,787 @@
+package blue.coordination.processor;
+
+import blue.language.Blue;
+import blue.language.NodeProvider;
+import blue.language.model.Node;
+import blue.language.processor.ChannelProcessor;
+import blue.language.processor.ContractMatchingService;
+import blue.language.processor.CoordinationRoutingHarness;
+import blue.language.processor.DocumentProcessingResult;
+import blue.language.processor.DocumentProcessor;
+import blue.language.processor.ExternalChannelFunctionContext;
+import blue.language.processor.ExternalChannelSubscriptionFunctions;
+import blue.language.processor.HandlerMatchContext;
+import blue.language.processor.HandlerMatchContextFactory;
+import blue.language.processor.HandlerProcessor;
+import blue.language.processor.ProcessingMetricsSink;
+import blue.language.processor.ProcessorExecutionContext;
+import blue.language.processor.ProcessorStatus;
+import blue.language.processor.model.ChannelContract;
+import blue.language.utils.BlueIdCalculator;
+import blue.repo.coordination.OperationRequest;
+import blue.repo.coordination.SequentialWorkflow;
+import blue.repo.coordination.SequentialWorkflowOperation;
+import blue.repo.coordination.TimelineEntry;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigInteger;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+final class OperationRequestLogicalRoutingTest {
+ private static final Node CHANNEL_TYPE =
+ new Node().name(
+ "Coordination Logical Routing Test Channel");
+ private static final String CHANNEL_TYPE_BLUE_ID =
+ BlueIdCalculator.calculateBlueId(CHANNEL_TYPE);
+ private static final Node OPERATION_TYPE =
+ new Node().name(
+ "Coordination Logical Routing Test Operation");
+ private static final String OPERATION_TYPE_BLUE_ID =
+ BlueIdCalculator.calculateBlueId(OPERATION_TYPE);
+ private static final Node OBSERVER_TYPE =
+ new Node().name(
+ "Coordination Logical Routing Test Observer");
+ private static final String OBSERVER_TYPE_BLUE_ID =
+ BlueIdCalculator.calculateBlueId(OBSERVER_TYPE);
+
+ @Test
+ void twoSourcesRouteOnceSuppressOrdinaryHandlersAndOwnCheckpoints() {
+ Fixture fixture = new Fixture(null);
+ Node initialized = fixture.initialize(document());
+
+ DocumentProcessingResult result =
+ fixture.process(
+ initialized,
+ request("increment", "target"));
+
+ assertSuccess(result);
+ assertEquals(
+ 1,
+ fixture.operations.executions,
+ "full-bundle routing="
+ + fixture.preparedRouting
+ + ", Phase-B routing="
+ + fixture.channels.lastHandlerChannel
+ + ", total handlers="
+ + fixture.metrics.handlersExecuted);
+ assertEquals(1, fixture.metrics.handlersExecuted);
+ assertTrue(hasCheckpoint(
+ result.document(), "source-a"));
+ assertTrue(hasCheckpoint(
+ result.document(), "source-b"));
+ assertFalse(hasCheckpoint(
+ result.document(), "target"));
+ }
+
+ @Test
+ void malformedUnknownAndNonChannelTargetsKeepIndependentOrdinaryDelivery() {
+ Node[] events = new Node[] {
+ request(null, "target"),
+ request("increment", null),
+ request("increment", "missing"),
+ request("increment", "observer-a")
+ };
+ for (Node event : events) {
+ Fixture fixture = new Fixture(null);
+ Node initialized =
+ fixture.initialize(document());
+
+ DocumentProcessingResult result =
+ fixture.process(
+ initialized, event);
+
+ assertSuccess(result);
+ assertEquals(0, fixture.operations.executions);
+ assertEquals(2, fixture.metrics.handlersExecuted);
+ assertTrue(hasCheckpoint(
+ result.document(), "source-a"));
+ assertTrue(hasCheckpoint(
+ result.document(), "source-b"));
+ assertFalse(hasCheckpoint(
+ result.document(), "target"));
+ }
+ }
+
+ @Test
+ void validTargetWithUnknownOperationSuppressesOrdinaryWorkflow() {
+ Fixture fixture = new Fixture(null);
+ Node initialized = fixture.initialize(document());
+
+ DocumentProcessingResult result =
+ fixture.process(
+ initialized,
+ request("missing-operation", "target"));
+
+ assertSuccess(result);
+ assertEquals(0, fixture.operations.executions);
+ assertEquals(0, fixture.metrics.handlersExecuted);
+ assertTrue(hasCheckpoint(
+ result.document(), "source-a"));
+ assertTrue(hasCheckpoint(
+ result.document(), "source-b"));
+ assertFalse(hasCheckpoint(
+ result.document(), "target"));
+ }
+
+ @Test
+ void fragmentedTimelineAndOperationRequestProjectWithoutLosingRoute() {
+ FragmentedEvent fragments =
+ fragmentedTimelineRequest(
+ "increment", "target");
+ Fixture fixture =
+ new Fixture(fragments.provider);
+ Node initialized = fixture.initialize(document());
+
+ DocumentProcessingResult result =
+ fixture.process(
+ initialized,
+ fragments.event);
+
+ assertSuccess(result);
+ assertEquals(1, fixture.operations.executions);
+ assertEquals(1, fixture.metrics.handlersExecuted);
+ assertNotNull(fixture.operations.lastEvent);
+ assertTrue(hasCheckpoint(
+ result.document(), "source-a"));
+ assertTrue(hasCheckpoint(
+ result.document(), "source-b"));
+ assertFalse(hasCheckpoint(
+ result.document(), "target"));
+ }
+
+ @Test
+ void missingRequiredFragmentFailsInsteadOfFallingBackToOrdinaryDelivery() {
+ String missingMessageBlueId =
+ BlueIdCalculator.calculateBlueId(
+ new Node()
+ .type(new Node().blueId(
+ OperationRequest.blueId()))
+ .properties(
+ "operation",
+ new Node().value(
+ "increment"))
+ .properties(
+ "channel",
+ new Node().value(
+ "target")));
+ Fixture fixture = new Fixture(null);
+ Node initialized = fixture.initialize(document());
+ Node event = timelineShell(
+ new Node().blueId(
+ missingMessageBlueId));
+
+ boolean failed = false;
+ try {
+ fixture.process(initialized, event);
+ } catch (RuntimeException expected) {
+ failed = true;
+ }
+
+ assertTrue(failed);
+ assertEquals(0, fixture.operations.executions);
+ assertEquals(0, fixture.metrics.handlersExecuted);
+ assertFalse(hasCheckpoint(
+ initialized, "source-a"));
+ assertFalse(hasCheckpoint(
+ initialized, "source-b"));
+ }
+
+ @Test
+ void fragmentedWhitespaceOperationKeepsOrdinarySourceDelivery() {
+ FragmentedEvent fragments =
+ fragmentedTimelineRequest(
+ " \t", "source-a");
+ Fixture fixture =
+ new Fixture(fragments.provider);
+ Node initialized =
+ fixture.initialize(document());
+
+ DocumentProcessingResult result =
+ fixture.process(
+ initialized,
+ fragments.event);
+
+ assertSuccess(result);
+ assertEquals(0, fixture.operations.executions);
+ assertEquals(2, fixture.metrics.handlersExecuted);
+ assertTrue(hasCheckpoint(
+ result.document(), "source-a"));
+ assertTrue(hasCheckpoint(
+ result.document(), "source-b"));
+ assertFalse(hasCheckpoint(
+ result.document(), "target"));
+ }
+
+ @Test
+ void productionOrdinaryWorkflowSuppressesOnlyEffectiveRoutableTarget() {
+ SequentialWorkflow workflow =
+ new SequentialWorkflow();
+ SequentialWorkflowProcessor processor =
+ new SequentialWorkflowProcessor();
+ Node routed =
+ request("increment", "target");
+
+ assertFalse(processor.matches(
+ workflow,
+ HandlerMatchContextFactory.create(
+ new Blue(),
+ "observer-target",
+ "target",
+ routed)));
+ assertTrue(processor.matches(
+ workflow,
+ HandlerMatchContextFactory.create(
+ new Blue(),
+ "observer-source",
+ "source-a",
+ routed)));
+ assertTrue(processor.matches(
+ workflow,
+ HandlerMatchContextFactory.create(
+ new Blue(),
+ "observer-source",
+ "source-a",
+ request(" \t", "source-a"))));
+ }
+
+ private static Node document() {
+ Map contracts =
+ new LinkedHashMap();
+ contracts.put(
+ "source-a",
+ channel(0, "topic"));
+ contracts.put(
+ "source-b",
+ channel(1, "topic"));
+ contracts.put(
+ "target",
+ channel(2, "other"));
+ contracts.put(
+ "increment",
+ new Node()
+ .type(reference(
+ OPERATION_TYPE_BLUE_ID))
+ .properties(
+ "channel",
+ new Node().value(
+ "target")));
+ contracts.put(
+ "observer-a",
+ observer("source-a"));
+ contracts.put(
+ "observer-b",
+ observer("source-b"));
+ contracts.put(
+ "observer-target",
+ observer("target"));
+ return new Node()
+ .name("Coordination Logical Routing Test")
+ .contracts(new Node()
+ .properties(contracts));
+ }
+
+ private static Node channel(
+ int order,
+ String subscriptionKey) {
+ return new Node()
+ .type(reference(
+ CHANNEL_TYPE_BLUE_ID))
+ .properties(
+ "order",
+ new Node().value(order))
+ .properties(
+ "subscriptionKey",
+ new Node().value(
+ subscriptionKey));
+ }
+
+ private static Node observer(String channel) {
+ return new Node()
+ .type(reference(
+ OBSERVER_TYPE_BLUE_ID))
+ .properties(
+ "channel",
+ new Node().value(channel))
+ .properties(
+ "steps",
+ new Node().items());
+ }
+
+ private static Node request(
+ String operation,
+ String channel) {
+ Node request = new Node()
+ .type(reference(
+ OperationRequest.blueId()))
+ .properties(
+ "subscriptionKey",
+ new Node().value("topic"));
+ if (operation != null) {
+ request.properties(
+ "operation",
+ new Node().value(operation))
+ .properties(
+ "testOperation",
+ new Node().value(operation));
+ }
+ if (channel != null) {
+ request.properties(
+ "channel",
+ new Node().value(channel));
+ }
+ return request;
+ }
+
+ private static FragmentedEvent fragmentedTimelineRequest(
+ String operation,
+ String channel) {
+ final Map exact =
+ new LinkedHashMap();
+ String subscriptionKey = addFragment(
+ exact,
+ new Node().value("topic"));
+ String timeline = addFragment(
+ exact,
+ new Node().value("timeline"));
+ String actor = addFragment(
+ exact,
+ new Node().value("actor"));
+ String timestamp = addFragment(
+ exact,
+ new Node().value(
+ BigInteger.TEN));
+ String operationValue = addFragment(
+ exact,
+ new Node().value(operation));
+ String channelValue = addFragment(
+ exact,
+ new Node().value(channel));
+ String specialized = addFragment(
+ exact,
+ new Node().value("retained"));
+ Node message = new Node()
+ .type(reference(
+ OperationRequest.blueId()))
+ .properties(
+ "operation",
+ reference(operationValue))
+ .properties(
+ "channel",
+ reference(channelValue))
+ .properties(
+ "specializedField",
+ reference(specialized));
+ String messageBlueId =
+ addFragment(exact, message);
+ String attribution = addFragment(
+ exact,
+ new Node().value("preserved"));
+ Node event = new Node()
+ .type(reference(
+ TimelineEntry.blueId()))
+ .properties(
+ "testOperation",
+ new Node().value(operation))
+ .properties(
+ "subscriptionKey",
+ reference(subscriptionKey))
+ .properties(
+ "timeline",
+ reference(timeline))
+ .properties(
+ "actor",
+ reference(actor))
+ .properties(
+ "timestamp",
+ reference(timestamp))
+ .properties(
+ "message",
+ reference(messageBlueId))
+ .properties(
+ "onBehalfOf",
+ reference(attribution));
+ NodeProvider provider = blueId -> {
+ Node content = exact.get(blueId);
+ return content != null
+ ? Collections.singletonList(
+ content.clone())
+ : null;
+ };
+ return new FragmentedEvent(
+ event, provider);
+ }
+
+ private static String addFragment(
+ Map exact,
+ Node content) {
+ String blueId =
+ BlueIdCalculator.calculateBlueId(
+ content);
+ exact.put(blueId, content.clone());
+ return blueId;
+ }
+
+ private static Node timelineShell(Node message) {
+ return new Node()
+ .type(reference(
+ TimelineEntry.blueId()))
+ .properties(
+ "subscriptionKey",
+ new Node().value("topic"))
+ .properties(
+ "timeline",
+ new Node().value(
+ "timeline"))
+ .properties(
+ "actor",
+ new Node().value("actor"))
+ .properties(
+ "timestamp",
+ new Node().value(
+ BigInteger.TEN))
+ .properties(
+ "message",
+ message)
+ .properties(
+ "onBehalfOf",
+ new Node().value(
+ "preserved"));
+ }
+
+ private static Node reference(String blueId) {
+ return new Node().blueId(blueId);
+ }
+
+ private static boolean hasCheckpoint(
+ Node document,
+ String channelKey) {
+ Node contracts =
+ document != null
+ ? document.getContracts()
+ : null;
+ Node checkpoint =
+ property(contracts, "checkpoint");
+ Node entries =
+ property(checkpoint, "entries");
+ return property(entries, channelKey) != null;
+ }
+
+ private static Node property(
+ Node node,
+ String key) {
+ return node != null
+ && node.getProperties() != null
+ ? node.getProperties().get(key)
+ : null;
+ }
+
+ private static void assertSuccess(
+ DocumentProcessingResult result) {
+ assertEquals(
+ ProcessorStatus.SUCCESS,
+ result.status(),
+ ProcessingResultTestSupport
+ .diagnosticMessage(result));
+ }
+
+ private static final class FragmentedEvent {
+ private final Node event;
+ private final NodeProvider provider;
+
+ private FragmentedEvent(
+ Node event,
+ NodeProvider provider) {
+ this.event = event;
+ this.provider = provider;
+ }
+ }
+
+ public static final class RoutingTestChannel
+ extends ChannelContract {
+ private String subscriptionKey;
+
+ public String getSubscriptionKey() {
+ return subscriptionKey;
+ }
+
+ public void setSubscriptionKey(
+ String subscriptionKey) {
+ this.subscriptionKey =
+ subscriptionKey;
+ }
+ }
+
+ public static final class RoutingTestOperation
+ extends SequentialWorkflowOperation {
+ }
+
+ private static final class RoutingChannelProcessor
+ implements ChannelProcessor<
+ RoutingTestChannel> {
+ private String lastHandlerChannel;
+ private final ExternalChannelSubscriptionFunctions<
+ RoutingTestChannel> functions =
+ new ExternalChannelSubscriptionFunctions<
+ RoutingTestChannel>() {
+ @Override
+ public List channelKeys(
+ RoutingTestChannel contract,
+ ExternalChannelFunctionContext context) {
+ OperationRequestRoutingFunctions
+ .declareTargetChannelFamilies(
+ contract, context);
+ return Collections.singletonList(
+ contract
+ .getSubscriptionKey());
+ }
+
+ @Override
+ public boolean accepts(
+ RoutingTestChannel contract,
+ Node exactEvent,
+ ExternalChannelFunctionContext context) {
+ if (!ExternalChannelSubscriptionFunctions
+ .super.accepts(
+ contract,
+ exactEvent,
+ context)) {
+ return false;
+ }
+ return !declaresTimelineEntry(
+ exactEvent)
+ || CoordinationEventNodes
+ .timelineEntry(
+ exactEvent,
+ context) != null;
+ }
+
+ @Override
+ public Node checkpointSubject(
+ RoutingTestChannel contract,
+ Node exactEvent,
+ Node exactPayload,
+ ExternalChannelFunctionContext context) {
+ CoordinationEventNodes
+ .TimelineEntryView entry =
+ declaresTimelineEntry(
+ exactEvent)
+ ? CoordinationEventNodes
+ .timelineEntry(
+ exactEvent,
+ context)
+ : null;
+ return entry != null
+ ? new Node().value(
+ entry.timestamp())
+ : ExternalChannelSubscriptionFunctions
+ .super.checkpointSubject(
+ contract,
+ exactEvent,
+ exactPayload,
+ context);
+ }
+
+ @Override
+ public Node payload(
+ RoutingTestChannel contract,
+ Node exactEvent,
+ ExternalChannelFunctionContext context) {
+ return OperationRequestRoutingFunctions
+ .payload(
+ exactEvent,
+ context);
+ }
+
+ @Override
+ public String handlerChannelKey(
+ RoutingTestChannel contract,
+ Node exactEvent,
+ Node exactPayload,
+ ExternalChannelFunctionContext context) {
+ lastHandlerChannel =
+ OperationRequestRoutingFunctions
+ .handlerChannelKey(
+ contract,
+ exactEvent,
+ context);
+ return lastHandlerChannel;
+ }
+
+ @Override
+ public String logicalDeliveryKey(
+ RoutingTestChannel contract,
+ Node exactEvent,
+ Node exactPayload,
+ ExternalChannelFunctionContext context) {
+ return OperationRequestRoutingFunctions
+ .logicalDeliveryKey(
+ contract,
+ exactEvent,
+ context);
+ }
+
+ @Override
+ public String checkpointDomainDiscriminator(
+ RoutingTestChannel contract) {
+ return "coordination-logical-routing-test";
+ }
+ };
+
+ private boolean declaresTimelineEntry(
+ Node event) {
+ return event != null
+ && event.getType() != null
+ && TimelineEntry.blueId().equals(
+ event.getType().getBlueId());
+ }
+
+ @Override
+ public Class contractType() {
+ return RoutingTestChannel.class;
+ }
+
+ @Override
+ public ExternalChannelSubscriptionFunctions<
+ RoutingTestChannel>
+ externalSubscriptionFunctions() {
+ return functions;
+ }
+ }
+
+ private static final class RoutingOperationProcessor
+ implements HandlerProcessor<
+ RoutingTestOperation> {
+ private int executions;
+ private Node lastEvent;
+
+ @Override
+ public Class
+ contractType() {
+ return RoutingTestOperation.class;
+ }
+
+ @Override
+ public boolean matches(
+ RoutingTestOperation contract,
+ HandlerMatchContext context) {
+ Object operation = context.event().get(
+ "/testOperation");
+ return contract != null
+ && contract.getKey() != null
+ && contract.getKey().equals(
+ operation);
+ }
+
+ @Override
+ public void execute(
+ RoutingTestOperation contract,
+ ProcessorExecutionContext context) {
+ executions++;
+ lastEvent = context.event().clone();
+ }
+ }
+
+ private static final class Fixture {
+ private final Blue language;
+ private final DocumentProcessor processor;
+ private final RoutingChannelProcessor channels =
+ new RoutingChannelProcessor();
+ private final RoutingOperationProcessor operations =
+ new RoutingOperationProcessor();
+ private final RecordingMetrics metrics =
+ new RecordingMetrics();
+ private List preparedRouting;
+
+ private Fixture(
+ NodeProvider provider) {
+ language = provider != null
+ ? new Blue(provider)
+ : new Blue();
+ SequentialWorkflowProcessor workflows =
+ new SequentialWorkflowProcessor();
+ language.registerExternalContractType(
+ CHANNEL_TYPE_BLUE_ID,
+ CHANNEL_TYPE,
+ channels);
+ language.registerExternalContractType(
+ OPERATION_TYPE_BLUE_ID,
+ OPERATION_TYPE,
+ operations);
+ language.registerExternalContractType(
+ OBSERVER_TYPE_BLUE_ID,
+ OBSERVER_TYPE,
+ workflows);
+ processor = DocumentProcessor.builder()
+ .registerContractProcessor(
+ CHANNEL_TYPE_BLUE_ID,
+ CHANNEL_TYPE,
+ channels)
+ .registerContractProcessor(
+ OPERATION_TYPE_BLUE_ID,
+ OPERATION_TYPE,
+ operations)
+ .registerContractProcessor(
+ OBSERVER_TYPE_BLUE_ID,
+ OBSERVER_TYPE,
+ workflows)
+ .withMatchingService(
+ new ContractMatchingService(
+ language))
+ .withSnapshotManager(
+ CoordinationRoutingHarness
+ .snapshotManager(
+ language))
+ .withProcessingMetricsSink(
+ metrics)
+ .withExternalDeliveryEvidenceVerifier(
+ (root, event, evidence) -> {
+ // Exact binding is revalidated by evidence.
+ })
+ .build();
+ }
+
+ private Node initialize(Node document) {
+ DocumentProcessingResult result =
+ processor.initializeDocument(document);
+ assertSuccess(result);
+ return result.document();
+ }
+
+ private DocumentProcessingResult process(
+ Node document,
+ Node event) {
+ preparedRouting =
+ CoordinationRoutingHarness
+ .routingProjection(
+ processor,
+ document,
+ event,
+ "source-b");
+ return CoordinationRoutingHarness
+ .process(
+ processor,
+ document,
+ event,
+ "source-a",
+ "source-b");
+ }
+ }
+
+ private static final class RecordingMetrics
+ implements ProcessingMetricsSink {
+ private int handlersExecuted;
+
+ @Override
+ public void incrementHandlersExecuted() {
+ handlersExecuted++;
+ }
+ }
+}
diff --git a/src/test/java/blue/coordination/processor/OperationRequestRoutingEvaluationTest.java b/src/test/java/blue/coordination/processor/OperationRequestRoutingEvaluationTest.java
index 4570a61..8bf5a27 100644
--- a/src/test/java/blue/coordination/processor/OperationRequestRoutingEvaluationTest.java
+++ b/src/test/java/blue/coordination/processor/OperationRequestRoutingEvaluationTest.java
@@ -2,13 +2,14 @@
import blue.language.Blue;
import blue.language.model.Node;
-import blue.language.processor.ChannelDelivery;
import blue.language.processor.ChannelEvaluation;
import blue.language.processor.ChannelEvaluationContext;
import blue.language.processor.ChannelEvaluationContextFactory;
import blue.language.processor.ChannelProcessor;
import blue.language.processor.HandlerMatchContextFactory;
import blue.language.processor.model.ChannelContract;
+import blue.language.provider.BasicNodeProvider;
+import blue.language.provider.SequentialNodeProvider;
import blue.repo.BlueRepository;
import blue.repo.coordination.ChatMessage;
import blue.repo.coordination.OperationRequest;
@@ -20,7 +21,6 @@
import java.math.BigInteger;
import java.util.Collections;
import java.util.LinkedHashMap;
-import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
@@ -37,46 +37,63 @@ class OperationRequestRoutingEvaluationTest {
private static final String ACTOR = "alice-account";
@Test
- void generatedOperationRequestRoutesToDeclaredChannel() {
+ void generatedOperationRequestRemainsTheExactSingleTimelinePayload() {
Fixture fixture = fixture();
Node event = entry(fixture, request("increment", TARGET, new Node().value(7)));
ChannelEvaluation evaluation = evaluate(fixture, event, channels());
- ChannelDelivery delivery = onlyDelivery(evaluation);
- assertEquals(TARGET, delivery.handlerChannelKey());
- assertEquals(OperationRequest.blueId() + ":increment", delivery.logicalDeliveryKey());
- assertNull(delivery.checkpointKey());
- assertNull(delivery.shouldProcess());
- assertNull(delivery.eventId());
- assertEquals(BigInteger.TEN, delivery.event().get("/timestamp"));
- assertEquals(BigInteger.valueOf(7), delivery.event().get("/message/request"));
+ assertOrdinary(evaluation, event);
+ assertEquals(BigInteger.TEN, evaluation.event().get("/timestamp"));
+ assertEquals(BigInteger.valueOf(7),
+ evaluation.event().get("/message/request"));
}
@Test
- void sameChannelTimelineRequestUsesTheSameRoutedPath() {
+ void sameChannelTimelineRequestAlsoRemainsAnExactPayload() {
Fixture fixture = fixture();
Map channels = channels();
Node event = entry(fixture, request("increment", SOURCE, new Node().value(7)));
- ChannelDelivery delivery = onlyDelivery(evaluate(fixture, event, channels));
-
- assertEquals(SOURCE, delivery.handlerChannelKey());
- assertEquals(OperationRequest.blueId() + ":increment", delivery.logicalDeliveryKey());
+ assertOrdinary(evaluate(fixture, event, channels), event);
}
@Test
- void compatibleOperationRequestSubtypeInheritsRoutingAndRetainsFields() {
+ void compatibleOperationRequestSubtypeRetainsExactFields() {
Fixture fixture = fixture();
- Node message = requestWithType(compatibleSubtype(fixture), "increment", TARGET, new Node().value(7))
+ Node message = requestWithType(compatibleSubtype(), "increment", TARGET, new Node().value(7))
.properties("specializedField", new Node().value("preserved"));
Node event = entry(fixture, TestTimelineProvider.chatMessage("placeholder"))
.properties("message", message);
- ChannelDelivery delivery = onlyDelivery(evaluate(fixture, event, channels()));
+ ChannelEvaluation evaluation = evaluate(fixture, event, channels());
- assertEquals(TARGET, delivery.handlerChannelKey());
- assertEquals("preserved", delivery.event().get("/message/specializedField"));
+ assertOrdinary(evaluation, event);
+ assertEquals("preserved",
+ evaluation.event().get("/message/specializedField"));
+ }
+
+ @Test
+ void repositoryRc10MaterializedOperationRequestTypeFailsClosed() {
+ Fixture fixture = fixture();
+ Node materializedType = fixture.repository
+ .nodeByBlueId(OperationRequest.blueId())
+ .orElseThrow(() -> new AssertionError(
+ "Operation Request type is absent"))
+ .clone()
+ .blueId(null);
+ Node request = requestWithType(
+ materializedType,
+ "increment",
+ TARGET,
+ new Node().value(7));
+
+ CoordinationEventNodes.OperationRequestView view =
+ CoordinationEventNodes.operationRequest(request);
+
+ assertNull(view,
+ "rc10 materialized Coordination identities are invalid under "
+ + "the final Language verifier; the final registry is required");
}
@Test
@@ -196,58 +213,43 @@ void targetExternalAcceptanceEvaluatorIsNotInvoked() {
}
@Test
- void unionDeliveryPreservesRouteMetadataAndOwnsItsCheckpoint() {
+ void unionPreservesTheExactChildPayloadWithoutSyntheticMetadata() {
Node event = new Node()
.properties("payload", new Node().value("selected"))
.properties("meta", new Node()
.properties("existing", new Node().value("retained")));
- ChannelDelivery child = ChannelDelivery.of(event,
- "child-event-id",
- "child-checkpoint",
- Boolean.FALSE,
- TARGET,
- "logical-route");
+ ChannelEvaluation evaluation = TimelineProviderSupport.preserveUnionPayload(
+ ChannelEvaluation.match(event, "child-event-id"),
+ new Node().properties("fallback", new Node().value(true)));
- ChannelEvaluation evaluation = TimelineProviderSupport.preserveUnionDelivery(
- ChannelEvaluation.matchDeliveries(Collections.singletonList(child)),
- new Node().properties("fallback", new Node().value(true)),
- "compositeSourceChannelKey",
- SOURCE);
-
- ChannelDelivery union = onlyDelivery(evaluation);
- assertEquals("selected", union.event().get("/payload"));
- assertEquals("retained", union.event().get("/meta/existing"));
- assertEquals(SOURCE, union.event().get("/meta/compositeSourceChannelKey"));
- assertEquals("child-event-id", union.eventId());
- assertNull(union.checkpointKey());
- assertEquals(Boolean.FALSE, union.shouldProcess());
- assertEquals(TARGET, union.handlerChannelKey());
- assertEquals("logical-route", union.logicalDeliveryKey());
+ assertTrue(evaluation.matches());
+ assertEquals("selected", evaluation.event().get("/payload"));
+ assertEquals("retained", evaluation.event().get("/meta/existing"));
+ assertNull(TimelineProviderSupport.property(
+ evaluation.event().getAsNode("/meta"),
+ "compositeSourceChannelKey"));
+ assertEquals("child-event-id", evaluation.eventId());
}
@Test
void unionOrdinaryDeliveryUsesFallbackAndPreservesEventId() {
Node fallback = new Node().properties("payload", new Node().value("fallback"));
- ChannelEvaluation evaluation = TimelineProviderSupport.preserveUnionDelivery(
+ ChannelEvaluation evaluation = TimelineProviderSupport.preserveUnionPayload(
ChannelEvaluation.match(null, "ordinary-id"),
- fallback,
- "compositeSourceChannelKey",
- SOURCE);
+ fallback);
assertTrue(evaluation.matches());
assertEquals("fallback", evaluation.event().get("/payload"));
- assertEquals(SOURCE, evaluation.event().get("/meta/compositeSourceChannelKey"));
+ assertNull(TimelineProviderSupport.property(evaluation.event(), "meta"));
assertEquals("ordinary-id", evaluation.eventId());
}
@Test
void unionWithoutChildOrFallbackEventDoesNotMatch() {
- ChannelEvaluation evaluation = TimelineProviderSupport.preserveUnionDelivery(
+ ChannelEvaluation evaluation = TimelineProviderSupport.preserveUnionPayload(
ChannelEvaluation.match(null),
- null,
- "compositeSourceChannelKey",
- SOURCE);
+ null);
assertFalse(evaluation.matches());
}
@@ -271,6 +273,39 @@ void operationMatcherRequiresExactEffectiveChannelAndOperationKey() {
HandlerMatchContextFactory.create(fixture.blue, "increment", TARGET, event)));
}
+ @Test
+ void operationMatcherTreatsPureReferenceMessageLikeInlineRequest() {
+ Fixture fixture = fixture();
+ Node requestContent = new Node()
+ .name("Referenced Operation Request")
+ .type(new Node().blueId(OperationRequest.blueId()))
+ .properties("operation", new Node().value("increment"))
+ .properties("channel", new Node().value(TARGET))
+ .properties("request", new Node().value(7));
+ BasicNodeProvider requestProvider =
+ new BasicNodeProvider(requestContent);
+ String requestBlueId = requestProvider.getBlueIdByName(
+ "Referenced Operation Request");
+ fixture.blue.nodeProvider(new SequentialNodeProvider(
+ requestProvider,
+ fixture.blue.getNodeProvider()));
+ Node event = entry(
+ fixture,
+ new Node().blueId(requestBlueId));
+ SequentialWorkflowOperation operation =
+ new SequentialWorkflowOperation();
+ operation.request(resolvedPattern(fixture, "Integer"));
+ operation.setKey("increment");
+
+ assertTrue(new OperationRequestMatcher().matches(
+ operation,
+ HandlerMatchContextFactory.create(
+ fixture.blue,
+ "increment",
+ TARGET,
+ event)));
+ }
+
@Test
void requestMayBeAbsentOnlyForAnEmptyOperationPattern() {
Fixture fixture = fixture();
@@ -285,6 +320,14 @@ void requestMayBeAbsentOnlyForAnEmptyOperationPattern() {
operation.request(resolvedPattern(fixture, "Integer"));
assertFalse(matcher.matches(operation,
HandlerMatchContextFactory.create(fixture.blue, "run", TARGET, event)));
+
+ operation.request(new Node().name("Required Request"));
+ assertFalse(matcher.matches(operation,
+ HandlerMatchContextFactory.create(
+ fixture.blue,
+ "run",
+ TARGET,
+ event)));
}
@Test
@@ -343,19 +386,11 @@ private static ChannelEvaluation evaluate(Fixture fixture,
private static void assertOrdinary(ChannelEvaluation evaluation, Node expectedEvent) {
assertTrue(evaluation.matches());
- assertTrue(evaluation.deliveries().isEmpty());
assertNotNull(evaluation.event());
assertEquals(TimelineProviderSupport.eventId(expectedEvent),
TimelineProviderSupport.eventId(evaluation.event()));
}
- private static ChannelDelivery onlyDelivery(ChannelEvaluation evaluation) {
- assertTrue(evaluation.matches());
- List deliveries = evaluation.deliveries();
- assertEquals(1, deliveries.size());
- return deliveries.get(0);
- }
-
private static Map channels() {
Map channels = new LinkedHashMap();
channels.put(SOURCE, sourceContract());
@@ -411,11 +446,10 @@ private static Node requestWithType(Node type, String operation, String channel,
.properties("request", payload);
}
- private static Node compatibleSubtype(Fixture fixture) {
- Node subtype = new Node()
+ private static Node compatibleSubtype() {
+ return new Node()
.name("Specialized Operation Request")
.type(new Node().blueId(OperationRequest.blueId()));
- return subtype.blueId(blue.language.utils.BlueIdCalculator.calculateBlueId(subtype));
}
private static Node resolvedPattern(Fixture fixture, String type) {
diff --git a/src/test/java/blue/coordination/processor/OperationRequestRoutingIntegrationTest.java b/src/test/java/blue/coordination/processor/OperationRequestRoutingIntegrationTest.java
index 1beed51..3f327e1 100644
--- a/src/test/java/blue/coordination/processor/OperationRequestRoutingIntegrationTest.java
+++ b/src/test/java/blue/coordination/processor/OperationRequestRoutingIntegrationTest.java
@@ -7,13 +7,14 @@
import blue.language.Blue;
import blue.language.model.Node;
import blue.language.processor.ChannelCheckpointContext;
-import blue.language.processor.ChannelDelivery;
import blue.language.processor.ChannelEvaluation;
import blue.language.processor.ChannelEvaluationContext;
import blue.language.processor.ChannelProcessor;
import blue.language.processor.DocumentProcessingResult;
+import blue.language.processor.ExternalChannelSubscriptionFunctions;
import blue.language.processor.ProcessingMetricsSink;
import blue.language.processor.ProcessorStatus;
+import blue.language.utils.JsonPointer;
import blue.repo.BlueRepository;
import blue.repo.coordination.Authority;
import blue.repo.coordination.Compute;
@@ -94,7 +95,7 @@ void sourceTimelineMismatchRejectsBeforeRouting() {
}
@Test
- void sourceEventFilterRejectsBeforeRouting() {
+ void sourceDefinitionDoesNotFilterExternalAcceptance() {
Fixture fixture = fixture();
Map contracts = baseContracts();
contracts.get(ALICE_CHANNEL).properties("definition", new Node()
@@ -113,7 +114,8 @@ void sourceEventFilterRejectsBeforeRouting() {
assertSuccess(result);
assertEquals(BigInteger.ZERO, result.document().get("/counter"));
- assertNull(checkpoint(result.document(), ALICE_CHANNEL));
+ assertEquals(BigInteger.valueOf(1_001),
+ checkpoint(result.document(), ALICE_CHANNEL).get("/timestamp"));
}
@Test
@@ -274,33 +276,30 @@ void targetOperationEventPatternRemainsMandatory() {
}
@Test
- void firstCompositeSourceSuppliesPayloadWhileDuplicateSourcesCheckpoint() {
+ void compositeAndDirectSourcesInvokeTargetOnceAndPersistOwnCheckpoints() {
RecordingMetrics metrics = new RecordingMetrics();
Fixture fixture = fixture(metrics, null);
Map contracts = baseContracts();
contracts.get(ALICE_CHANNEL).properties("order", new Node().value(0));
contracts.put("aliceComposite", composite(-10, ALICE_CHANNEL));
- contracts.put("recordWinner", recordMetadataOperation(
- BOB_CHANNEL, "compositeSourceChannelKey"));
+ contracts.put("increment", incrementOperation(BOB_CHANNEL));
Node initialized = initialize(fixture, contracts);
DocumentProcessingResult result = process(fixture,
initialized,
1,
- request("recordWinner", BOB_CHANNEL, new Node().value(7)));
+ request("increment", BOB_CHANNEL, new Node().value(7)));
assertSuccess(result);
- assertEquals(ALICE_CHANNEL, result.document().get("/winner"));
+ assertEquals(BigInteger.ONE, result.document().get("/counter"));
assertNotNull(checkpoint(result.document(), ALICE_CHANNEL));
assertNotNull(checkpoint(result.document(), "aliceComposite"));
assertNull(checkpoint(result.document(), "aliceComposite::" + ALICE_CHANNEL));
- assertEquals(1, metrics.routedDeliveries);
- assertEquals(1, metrics.deduplicatedDeliveries);
assertEquals(1, metrics.handlersExecuted);
}
@Test
- void firstAllTimelinesSourceSuppliesPayloadWhileDuplicateSourcesCheckpoint() {
+ void allTimelinesAndDirectSourcesInvokeTargetOnceAndPersistOwnCheckpoints() {
RecordingMetrics metrics = new RecordingMetrics();
Fixture fixture = fixture(metrics, null);
Map contracts = baseContracts();
@@ -308,22 +307,19 @@ void firstAllTimelinesSourceSuppliesPayloadWhileDuplicateSourcesCheckpoint() {
contracts.put("all", new Node()
.type("Coordination/All Timelines Channel")
.properties("order", new Node().value(-10)));
- contracts.put("recordWinner", recordMetadataOperation(
- BOB_CHANNEL, "allTimelinesSourceChannelKey"));
+ contracts.put("increment", incrementOperation(BOB_CHANNEL));
Node initialized = initialize(fixture, contracts);
DocumentProcessingResult result = process(fixture,
initialized,
1,
- request("recordWinner", BOB_CHANNEL, new Node().value(7)));
+ request("increment", BOB_CHANNEL, new Node().value(7)));
assertSuccess(result);
- assertEquals(ALICE_CHANNEL, result.document().get("/winner"));
+ assertEquals(BigInteger.ONE, result.document().get("/counter"));
assertNotNull(checkpoint(result.document(), ALICE_CHANNEL));
assertNotNull(checkpoint(result.document(), "all"));
assertNull(checkpoint(result.document(), "all::" + ALICE_CHANNEL));
- assertEquals(1, metrics.routedDeliveries);
- assertEquals(1, metrics.deduplicatedDeliveries);
assertEquals(1, metrics.handlersExecuted);
}
@@ -344,8 +340,6 @@ void severalMatchingDirectSourcesInvokeTargetOnceAndPersistOwnCheckpoints() {
assertEquals(BigInteger.ONE, result.document().get("/counter"));
assertNotNull(checkpoint(result.document(), ALICE_CHANNEL));
assertNotNull(checkpoint(result.document(), "aliceMirror"));
- assertEquals(1, metrics.routedDeliveries);
- assertEquals(1, metrics.deduplicatedDeliveries);
assertEquals(1, metrics.handlersExecuted);
}
@@ -386,15 +380,15 @@ void targetHandlerFailurePersistsNoSourceCheckpoint() {
request("fail", BOB_CHANNEL, new Node().value(7)));
assertEquals(ProcessorStatus.RUNTIME_FATAL, result.status());
- assertTrue(result.failureReason().contains("target handler failed"), result.failureReason());
+ assertTrue(blue.coordination.processor.ProcessingResultTestSupport.diagnosticMessage(result).contains("target handler failed"), blue.coordination.processor.ProcessingResultTestSupport.diagnosticMessage(result));
assertNull(checkpoint(result.document(), ALICE_CHANNEL));
}
@Test
- void targetGracefulTerminationPersistsNoSourceCheckpoint() {
+ void targetApplicationTerminationPersistsNoSourceCheckpoint() {
SequentialWorkflowRunner runner = new SequentialWorkflowRunner(
Collections.>singletonList(
- new GracefulTerminationExecutor()));
+ new ApplicationTerminationExecutor()));
Fixture fixture = fixture(null, runner);
Map contracts = baseContracts();
contracts.put("finish", operation(BOB_CHANNEL,
@@ -407,7 +401,7 @@ void targetGracefulTerminationPersistsNoSourceCheckpoint() {
1,
request("finish", BOB_CHANNEL, new Node().value(7)));
- assertEquals(ProcessorStatus.SUCCESS, result.status(), result.failureReason());
+ assertEquals(ProcessorStatus.SUCCESS, result.status(), blue.coordination.processor.ProcessingResultTestSupport.diagnosticMessage(result));
assertNull(checkpoint(result.document(), ALICE_CHANNEL));
}
@@ -434,27 +428,6 @@ void replayAfterCommittedSourceCheckpointsRunsNothing() {
assertTrue(replay.totalGas() < first.totalGas());
}
- @Test
- void invalidTrustedRouteMetadataRemainsCoreFatal() {
- Fixture fixture = fixture();
- fixture.blue.registerContractProcessor(TimelineChannel.blueId(),
- new InvalidRouteTimelineProcessor());
- Map contracts = new LinkedHashMap();
- contracts.put(ALICE_CHANNEL, timelineChannel(ALICE_TIMELINE, ALICE_ACTOR));
- Node initialized = initialize(fixture, contracts);
-
- DocumentProcessingResult result = fixture.blue.processDocument(initialized,
- timelineEntry(fixture,
- ALICE_TIMELINE,
- ALICE_ACTOR,
- 1,
- TestTimelineProvider.chatMessage("route")));
-
- assertEquals(ProcessorStatus.RUNTIME_FATAL, result.status());
- assertTrue(result.failureReason().contains("same-scope Channel"), result.failureReason());
- assertNull(checkpoint(result.document(), ALICE_CHANNEL));
- }
-
private static Map baseContracts() {
Map contracts = new LinkedHashMap();
contracts.put(ALICE_CHANNEL, timelineChannel(ALICE_TIMELINE, ALICE_ACTOR));
@@ -491,12 +464,6 @@ private static Node captureEventOperation(String channel) {
replaceStep("/captured", bexBinding("event")));
}
- private static Node recordMetadataOperation(String channel, String metadataKey) {
- return operation(channel,
- new Node().type("Integer"),
- replaceStep("/winner", bexBinding("event/meta/" + metadataKey)));
- }
-
private static Node operation(String channel, Node requestPattern, Node... steps) {
return new Node()
.type("Coordination/Sequential Workflow Operation")
@@ -609,7 +576,9 @@ private static Node timelineEntry(Fixture fixture,
private static Node checkpoint(Node document, String key) {
try {
- return document.getAsNode("/contracts/checkpoint/lastEvents/" + key);
+ return document.getAsNode("/contracts/checkpoint/entries/"
+ + JsonPointer.escape(key)
+ + "/subject");
} catch (IllegalArgumentException ex) {
return null;
}
@@ -633,7 +602,7 @@ private static Fixture fixture(RecordingMetrics metrics, SequentialWorkflowRunne
}
private static void assertSuccess(DocumentProcessingResult result) {
- assertEquals(ProcessorStatus.SUCCESS, result.status(), result.failureReason());
+ assertEquals(ProcessorStatus.SUCCESS, result.status(), blue.coordination.processor.ProcessingResultTestSupport.diagnosticMessage(result));
}
private static final class Fixture {
@@ -647,27 +616,15 @@ private Fixture(BlueRepository repository, Blue blue) {
}
private static final class RecordingMetrics implements ProcessingMetricsSink {
- private int routedDeliveries;
- private int deduplicatedDeliveries;
private int handlersExecuted;
- @Override
- public void incrementRoutedChannelDeliveries() {
- routedDeliveries++;
- }
-
- @Override
- public void incrementDeduplicatedChannelDeliveries() {
- deduplicatedDeliveries++;
- }
-
@Override
public void incrementHandlersExecuted() {
handlersExecuted++;
}
}
- private static final class GracefulTerminationExecutor
+ private static final class ApplicationTerminationExecutor
implements WorkflowStepExecutor {
@Override
public boolean supports(SequentialWorkflowStep step) {
@@ -676,12 +633,14 @@ public boolean supports(SequentialWorkflowStep step) {
@Override
public WorkflowStepResult execute(Compute step, StepExecutionContext context) {
- context.processorContext().terminateGracefully("operation complete");
+ context.processorContext().terminate(
+ "operation-complete",
+ "operation complete");
return WorkflowStepResult.none();
}
}
- private static final class InvalidRouteTimelineProcessor
+ private static final class SelectiveFreshnessTimelineProcessor
implements ChannelProcessor {
@Override
public Class contractType() {
@@ -689,23 +648,9 @@ public Class contractType() {
}
@Override
- public ChannelEvaluation evaluate(TimelineChannel contract,
- ChannelEvaluationContext context) {
- return ChannelEvaluation.matchDeliveries(Collections.singletonList(
- ChannelDelivery.of(context.event(),
- null,
- null,
- null,
- "missing",
- "invalid-route")));
- }
- }
-
- private static final class SelectiveFreshnessTimelineProcessor
- implements ChannelProcessor {
- @Override
- public Class contractType() {
- return TimelineChannel.class;
+ public ExternalChannelSubscriptionFunctions
+ externalSubscriptionFunctions() {
+ return TimelineExternalSubscriptionFunctions.INSTANCE;
}
@Override
@@ -721,8 +666,7 @@ public String eventId(TimelineChannel contract, ChannelEvaluationContext context
@Override
public boolean isNewerEvent(TimelineChannel contract, ChannelCheckpointContext context) {
- return !ALICE_CHANNEL.equals(context.channelKey())
- && TimelineProviderSupport.isNewerOrSameTimelineEvent(context);
+ return !ALICE_CHANNEL.equals(context.channelKey());
}
}
}
diff --git a/src/test/java/blue/coordination/processor/ProcessingResultTestSupport.java b/src/test/java/blue/coordination/processor/ProcessingResultTestSupport.java
new file mode 100644
index 0000000..4ca21da
--- /dev/null
+++ b/src/test/java/blue/coordination/processor/ProcessingResultTestSupport.java
@@ -0,0 +1,52 @@
+package blue.coordination.processor;
+
+import blue.language.Blue;
+import blue.language.model.Node;
+import blue.language.processor.DocumentProcessingResult;
+import blue.language.processor.ProcessorDiagnostic;
+import blue.language.processor.ProcessorErrorCategory;
+import blue.language.processor.ProcessorStatus;
+import blue.language.snapshot.ResolvedSnapshot;
+import blue.language.utils.BlueIdCalculator;
+
+/**
+ * Test-only views over the final five-field Contracts 1.0 process result.
+ *
+ * Snapshots and resolved documents are deliberately derived out of band;
+ * neither is a semantic ProcessResult field.
+ */
+public final class ProcessingResultTestSupport {
+ private ProcessingResultTestSupport() {
+ }
+
+ public static String diagnosticMessage(DocumentProcessingResult result) {
+ ProcessorDiagnostic diagnostic = result != null ? result.diagnostic() : null;
+ return diagnostic != null && diagnostic.message() != null
+ ? diagnostic.message()
+ : "";
+ }
+
+ public static ProcessorErrorCategory diagnosticCategory(
+ DocumentProcessingResult result) {
+ ProcessorDiagnostic diagnostic = result != null ? result.diagnostic() : null;
+ return diagnostic != null ? diagnostic.category() : null;
+ }
+
+ public static boolean isCapabilityFailure(DocumentProcessingResult result) {
+ return result != null && result.status() == ProcessorStatus.CAPABILITY_FAILURE;
+ }
+
+ public static String blueId(DocumentProcessingResult result) {
+ return BlueIdCalculator.calculateBlueId(result.document());
+ }
+
+ public static ResolvedSnapshot snapshot(Blue blue,
+ DocumentProcessingResult result) {
+ return blue.resolveToSnapshot(result.document());
+ }
+
+ public static Node resolvedDocument(Blue blue,
+ DocumentProcessingResult result) {
+ return snapshot(blue, result).resolvedRoot();
+ }
+}
diff --git a/src/test/java/blue/coordination/processor/PublishedTimelineChannelResolutionTest.java b/src/test/java/blue/coordination/processor/PublishedTimelineChannelResolutionTest.java
index 53ced23..3003073 100644
--- a/src/test/java/blue/coordination/processor/PublishedTimelineChannelResolutionTest.java
+++ b/src/test/java/blue/coordination/processor/PublishedTimelineChannelResolutionTest.java
@@ -2,8 +2,6 @@
import blue.language.Blue;
import blue.language.model.Node;
-import blue.language.processor.ChannelEvaluationContext;
-import blue.language.processor.ChannelProcessor;
import blue.language.processor.DocumentProcessingResult;
import blue.language.processor.ProcessorStatus;
import blue.repo.BlueRepository;
@@ -51,7 +49,7 @@ void publishedMaterializedTimelineChannelInitializesAsContract() {
DocumentProcessingResult result = fixture.blue.initializeDocument(
fixture.blue.preprocess(document(fixture)));
- assertSuccessfulSnapshot(result);
+ assertSuccessfulSnapshot(fixture, result);
assertResolvedBinding(fixture, result.document().getAsNode("/contracts/timeline"));
}
@@ -73,24 +71,26 @@ void publishedCheckpointedTimelineEntrySurvivesClonedDocumentRebuild() {
DocumentProcessingResult first = fixture.blue.processDocument(initialized,
timelineEntry(fixture.blue, BigInteger.ONE, "first"));
- assertSuccessfulSnapshot(first);
- assertCheckpoint(first.document(), BigInteger.ONE, "first");
+ assertSuccessfulSnapshot(fixture, first);
+ assertCheckpoint(first.document(), BigInteger.ONE);
DocumentProcessingResult second = fixture.blue.processDocument(first.document().clone(),
timelineEntry(fixture.blue, BigInteger.valueOf(2), "second"));
- assertSuccessfulSnapshot(second);
- assertCheckpoint(second.document(), BigInteger.valueOf(2), "second");
+ assertSuccessfulSnapshot(fixture, second);
+ assertCheckpoint(second.document(), BigInteger.valueOf(2));
assertFinitePrevEntryBoundary(fixture.blue.resolve(
timelineEntry(fixture.blue, BigInteger.valueOf(2), "second")));
}
- private static Fixture fixture(boolean alwaysMatchingProcessor) {
+ private static Fixture fixture(boolean timelineProcessorOnly) {
BlueRepository repository = BlueRepository.latest();
- Blue blue = repository.configure(new Blue());
- if (alwaysMatchingProcessor) {
+ Blue blue = new Blue()
+ .nodeProvider(repository.nodeProvider())
+ .typeClassResolver(repository.typeClassResolver());
+ if (timelineProcessorOnly) {
blue.registerContractProcessor(TimelineChannel.blueId(),
- new AlwaysMatchingTimelineChannelProcessor());
+ new TimelineChannelProcessor());
} else {
CoordinationProcessors.registerWith(blue);
}
@@ -117,24 +117,28 @@ private static Node timelineEntry(Blue blue, BigInteger timestamp, String messag
return blue.preprocess(blue.objectToNode(entry));
}
- private static void assertSuccessfulSnapshot(DocumentProcessingResult result) {
- assertFalse(result.capabilityFailure(), result.failureReason());
- assertEquals(ProcessorStatus.SUCCESS, result.status(), result.failureReason());
- assertNotNull(result.snapshot());
- assertEquals(result.snapshot().blueId(), result.snapshot().frozenCanonicalRoot().blueId());
+ private static void assertSuccessfulSnapshot(Fixture fixture,
+ DocumentProcessingResult result) {
+ assertFalse(blue.coordination.processor.ProcessingResultTestSupport.isCapabilityFailure(result), blue.coordination.processor.ProcessingResultTestSupport.diagnosticMessage(result));
+ assertEquals(ProcessorStatus.SUCCESS, result.status(), blue.coordination.processor.ProcessingResultTestSupport.diagnosticMessage(result));
+ assertNotNull(ProcessingResultTestSupport.snapshot(fixture.blue, result));
+ assertEquals(ProcessingResultTestSupport.blueId(result),
+ ProcessingResultTestSupport.snapshot(fixture.blue, result)
+ .frozenCanonicalRoot().blueId());
}
- private static void assertCheckpoint(Node document, BigInteger timestamp, String message) {
- Node event = document.getAsNode("/contracts/checkpoint/lastEvents/timeline");
- assertNotNull(event);
- assertNotNull(event.getType());
- assertTrue(event.getType().isReferenceOnly());
- assertEquals(TimelineEntry.blueId(), event.getType().getBlueId());
- assertEquals("timeline-1", event.getAsText("/timeline/timelineId"));
- assertEquals("account-1", event.getAsText("/actor/accountId"));
- assertFalse(event.getProperties().containsKey("sequence"));
- assertEquals(timestamp, event.get("/timestamp"));
- assertEquals(message, event.getAsText("/message/message"));
+ private static void assertCheckpoint(
+ Node document,
+ BigInteger timestamp) {
+ Node subject = document.getAsNode(
+ "/contracts/checkpoint/entries/timeline/subject");
+ assertNotNull(subject);
+ assertEquals(2, subject.getProperties().size());
+ assertEquals(
+ TimelineExternalSubscriptionFunctions
+ .TIMELINE_ORDER_SUBJECT_VERSION,
+ subject.getAsText("/semantics"));
+ assertEquals(timestamp, subject.get("/timestamp"));
}
private static void assertFinitePrevEntryBoundary(Node resolvedTimelineEntry) {
@@ -158,19 +162,6 @@ private static void assertResolvedBinding(Fixture fixture, Node channel) {
assertTrue(fixture.blue.nodeMatchesType(channel.getAsNode("/actor"), actorType));
}
- private static final class AlwaysMatchingTimelineChannelProcessor
- implements ChannelProcessor {
- @Override
- public Class contractType() {
- return TimelineChannel.class;
- }
-
- @Override
- public boolean matches(TimelineChannel contract, ChannelEvaluationContext context) {
- return true;
- }
- }
-
private static final class Fixture {
private final BlueRepository repository;
private final Blue blue;
diff --git a/src/test/java/blue/coordination/processor/RepositoryStyleCounterDocumentTest.java b/src/test/java/blue/coordination/processor/RepositoryStyleCounterDocumentTest.java
index b9dac9d..1abcfc6 100644
--- a/src/test/java/blue/coordination/processor/RepositoryStyleCounterDocumentTest.java
+++ b/src/test/java/blue/coordination/processor/RepositoryStyleCounterDocumentTest.java
@@ -28,13 +28,16 @@ void richCounterDocumentInitializesAndProcessesIncrementOperation() {
DocumentProcessingResult initialized = fixture.blue.initializeDocument(authored);
- assertFalse(initialized.capabilityFailure(), initialized.failureReason());
+ assertFalse(blue.coordination.processor.ProcessingResultTestSupport.isCapabilityFailure(initialized), blue.coordination.processor.ProcessingResultTestSupport.diagnosticMessage(initialized));
assertTrue(fixture.blue.isInitialized(initialized.document()));
- assertNotNull(initialized.snapshot());
- assertNotNull(initialized.blueId());
- String initializedDocumentId = initialized.resolvedDocument().getAsText("/contracts/initialized/documentId");
+ assertNotNull(ProcessingResultTestSupport.snapshot(fixture.blue, initialized));
+ assertNotNull(ProcessingResultTestSupport.blueId(initialized));
+ String initializedDocumentId = ProcessingResultTestSupport
+ .resolvedDocument(fixture.blue, initialized)
+ .getAsText("/contracts/initialized/documentId");
assertNotNull(initializedDocumentId);
- assertNull(property(property(initialized.resolvedDocument(), "contracts"), "checkpoint"));
+ assertNull(property(property(ProcessingResultTestSupport.resolvedDocument(
+ fixture.blue, initialized), "contracts"), "checkpoint"));
Node event = TestTimelineProvider.timelineEntry(fixture.blue,
fixture.repository,
@@ -42,26 +45,36 @@ void richCounterDocumentInitializesAndProcessesIncrementOperation() {
1777987926,
operationRequest("increment", 5));
- DocumentProcessingResult result = fixture.blue.processDocument(initialized.snapshot(), event);
+ DocumentProcessingResult result = fixture.blue.processDocument(
+ ProcessingResultTestSupport.snapshot(fixture.blue, initialized), event);
- assertFalse(result.capabilityFailure(), result.failureReason());
- assertNotNull(result.snapshot());
- assertNotNull(result.blueId());
- assertEquals(BigInteger.valueOf(5), result.resolvedDocument().get("/counter"));
- assertEquals(1, result.triggeredEvents().size());
+ assertFalse(blue.coordination.processor.ProcessingResultTestSupport.isCapabilityFailure(result), blue.coordination.processor.ProcessingResultTestSupport.diagnosticMessage(result));
+ assertNotNull(ProcessingResultTestSupport.snapshot(fixture.blue, result));
+ assertNotNull(ProcessingResultTestSupport.blueId(result));
+ assertEquals(BigInteger.valueOf(5),
+ ProcessingResultTestSupport.resolvedDocument(fixture.blue, result)
+ .get("/counter"));
+ assertEquals(1, result.events().size());
assertEquals("Counter was incremented by 5 and is now 5",
- result.triggeredEvents().get(0).getAsText("/message"));
+ result.events().get(0).getAsText("/message"));
- Node resolved = result.resolvedDocument();
+ Node resolved = ProcessingResultTestSupport.resolvedDocument(
+ fixture.blue, result);
assertEquals(initializedDocumentId, resolved.getAsText("/contracts/initialized/documentId"));
- assertEquals(TIMELINE_ID, resolved.getAsText("/contracts/checkpoint/lastEvents/ownerChannel/timeline/timelineId"));
+ Node checkpoint = property(
+ property(resolved, "contracts"), "checkpoint");
+ Node checkpointEntries = property(checkpoint, "entries");
+ Node checkpointEntry = property(checkpointEntries, "ownerChannel");
+ Node checkpointSubject = property(checkpointEntry, "subject");
+ assertNotNull(checkpointSubject);
+ assertEquals(
+ TimelineExternalSubscriptionFunctions
+ .TIMELINE_ORDER_SUBJECT_VERSION,
+ checkpointSubject.getAsText("/semantics"));
assertEquals(BigInteger.valueOf(1777987926L),
- resolved.get("/contracts/checkpoint/lastEvents/ownerChannel/timestamp"));
- assertEquals("increment",
- resolved.getAsText("/contracts/checkpoint/lastEvents/ownerChannel/message/operation"));
- assertEquals(BigInteger.valueOf(5),
- resolved.get("/contracts/checkpoint/lastEvents/ownerChannel/message/request"));
- assertNotNull(resolved.get("/contracts/checkpoint/lastEvents/ownerChannel"));
+ checkpointSubject.get("/timestamp"));
+ assertNull(property(checkpointSubject, "timeline"));
+ assertNull(property(checkpointSubject, "message"));
}
private static Node richCounterDocument(Fixture fixture) {
diff --git a/src/main/java/blue/coordination/processor/RepositoryTypeAliasPreprocessor.java b/src/test/java/blue/coordination/processor/RepositoryTypeAliasPreprocessor.java
similarity index 85%
rename from src/main/java/blue/coordination/processor/RepositoryTypeAliasPreprocessor.java
rename to src/test/java/blue/coordination/processor/RepositoryTypeAliasPreprocessor.java
index 1c5fb45..d657592 100644
--- a/src/main/java/blue/coordination/processor/RepositoryTypeAliasPreprocessor.java
+++ b/src/test/java/blue/coordination/processor/RepositoryTypeAliasPreprocessor.java
@@ -6,6 +6,12 @@
import java.util.LinkedHashMap;
import java.util.Map;
+/**
+ * Test-fixture migration helper for preview repository aliases.
+ *
+ * This deliberately lives outside the published runtime artifact. Final
+ * canonical execution must consume exact registry references.
+ */
public final class RepositoryTypeAliasPreprocessor {
private final Map aliases;
@@ -73,17 +79,14 @@ private Node resolveTypeNode(Node typeNode) {
}
private String inlineText(Node node) {
- if (node == null || !node.isInlineValue() || node.getValue() == null) {
+ if (node == null || !node.isInlineValue()
+ || node.getValue() == null) {
return null;
}
return String.valueOf(node.getValue());
}
private String aliasFor(String value) {
- if (value == null) {
- return null;
- }
- return aliases.get(value);
+ return value != null ? aliases.get(value) : null;
}
-
}
diff --git a/src/test/java/blue/coordination/processor/RuntimeChannelsTest.java b/src/test/java/blue/coordination/processor/RuntimeChannelsTest.java
index b61420d..b85be33 100644
--- a/src/test/java/blue/coordination/processor/RuntimeChannelsTest.java
+++ b/src/test/java/blue/coordination/processor/RuntimeChannelsTest.java
@@ -3,6 +3,7 @@
import blue.language.Blue;
import blue.language.model.Node;
import blue.language.processor.DocumentProcessingResult;
+import blue.language.processor.ProcessorStatus;
import blue.language.processor.registry.RuntimeBlueIds;
import blue.repo.BlueRepository;
import java.math.BigInteger;
@@ -34,7 +35,7 @@ void runtimeDocumentUpdateChannelReceivesUpdateEvents() {
DocumentProcessingResult result = processChat(fixture, document, 1);
assertEquals(BigInteger.valueOf(5), result.document().get("/counter"));
- assertContainsChatMessage(result.triggeredEvents(), "updated /counter from 0 to 5");
+ assertContainsChatMessage(result.events(), "updated /counter from 0 to 5");
}
@Test
@@ -54,8 +55,8 @@ void documentUpdateChannelPathFilteringUsesRepositoryTypes() {
DocumentProcessingResult result = processChat(fixture, document, 1);
- assertContainsChatMessage(result.triggeredEvents(), "counter updated");
- assertNoChatMessage(result.triggeredEvents(), "name updated");
+ assertContainsChatMessage(result.events(), "counter updated");
+ assertNoChatMessage(result.events(), "name updated");
}
@Test
@@ -79,7 +80,7 @@ void nestedUpdatesPropagateToParentWatchers() {
.getProperties().get("profile")
.getProperties().get("name")
.getValue());
- assertContainsChatMessage(result.triggeredEvents(), "updated /profile/name from Grace to Ada");
+ assertContainsChatMessage(result.events(), "updated /profile/name from Grace to Ada");
}
@Test
@@ -102,7 +103,7 @@ void updateEventCanBeMatchedMoreSpecifically() {
assertEquals(BigInteger.valueOf(5), result.document().get("/counter"));
assertEquals(BigInteger.valueOf(9), result.document().get("/other"));
- assertSingleChatMessage(result.triggeredEvents(), "specific replace");
+ assertSingleChatMessage(result.events(), "specific replace");
}
@Test
@@ -126,11 +127,18 @@ void parentCannotPatchIntoEmbeddedScope() {
updateDocumentStep("replace", "/child/counter", new Node().value(99))));
Node document = initializedDocument(fixture, document(fixture.repository, 0, contracts)
.properties("child", childDocument(1, new LinkedHashMap())));
+ String inputJson = fixture.blue.nodeToJson(document);
DocumentProcessingResult result = processChat(fixture, document, 1);
+ assertEquals(ProcessorStatus.RUNTIME_FATAL,
+ result.status(),
+ ProcessingResultTestSupport.diagnosticMessage(result));
+ assertEquals(inputJson, fixture.blue.nodeToJson(result.document()),
+ "the boundary violation must roll back the complete invocation");
assertEquals(BigInteger.valueOf(1), result.document().get("/child/counter"));
- assertEquals("fatal", result.document().get("/contracts/terminated/cause"));
+ assertTrue(result.events().isEmpty());
+ assertNull(nodeAt(result.document(), "/contracts/terminated"));
}
@Test
@@ -157,7 +165,7 @@ void replacingEmbeddedNodeCutsOffChildScopeWithinRun() {
assertEquals("Replacement Child", nodeAt(result.document(), "/child").getName());
assertNull(nodeAt(result.document(), "/child/marker"));
- assertNoChatMessage(result.triggeredEvents(), "post-cutoff");
+ assertNoChatMessage(result.events(), "post-cutoff");
}
@Test
@@ -167,8 +175,8 @@ void embeddedNodeChannelBridgesConfiguredChildEmissions() {
DocumentProcessingResult result = processChat(fixture, document, 1);
- assertContainsChatMessage(result.triggeredEvents(), "parent saw child emitted");
- assertNoChatMessage(result.triggeredEvents(), "parent saw other child emitted");
+ assertContainsChatMessage(result.events(), "parent saw child emitted");
+ assertNoChatMessage(result.events(), "parent saw other child emitted");
}
@Test
@@ -178,8 +186,8 @@ void embeddedNodeChannelDoesNotBridgeWrongChildPath() {
DocumentProcessingResult result = processChat(fixture, document, 1);
- assertNoChatMessage(result.triggeredEvents(), "parent saw child emitted");
- assertNoChatMessage(result.triggeredEvents(), "parent saw other child emitted");
+ assertNoChatMessage(result.events(), "parent saw child emitted");
+ assertNoChatMessage(result.events(), "parent saw other child emitted");
}
@Test
@@ -197,7 +205,7 @@ void duplicateExternalEventsAreSkippedWithRealRepositoryChannelCheckpointShape()
assertEquals(BigInteger.ONE, afterSecond.get("/counter"));
Node checkpoint = nodeAt(afterSecond, "/contracts/checkpoint");
assertNotNull(checkpoint);
- assertNotNull(nodeAt(checkpoint, "/lastEvents/owner"));
+ assertNotNull(nodeAt(checkpoint, "/entries/owner/subject"));
}
@Test
@@ -219,7 +227,7 @@ void multipleCheckpointMarkersInOneScopeFail() {
Node initialized = initializedDocument(fixture, document(fixture.repository, 0, contracts));
initialized.getContracts().properties("checkpoint", new Node()
.type(new Node().blueId(RuntimeBlueIds.CHANNEL_EVENT_CHECKPOINT))
- .properties("lastEvents", new Node().properties(new LinkedHashMap())));
+ .properties("entries", new Node().properties(new LinkedHashMap())));
initialized.getContracts().properties("extraCheckpoint", new Node()
.type(new Node().blueId(RuntimeBlueIds.CHANNEL_EVENT_CHECKPOINT)));
@@ -239,7 +247,7 @@ private static Node embeddedOperationDocument(BlueRepository repository) {
.properties("child", childDocument(0, childContracts));
}
- private static Node embeddedBridgeDocument(BlueRepository repository, String childPath) {
+ private static Node embeddedBridgeDocument(BlueRepository repository, String sourcePath) {
Map childContracts = ownerChannelContracts();
childContracts.put("emit", directWorkflow("owner", triggerEventStep(chatMessageEvent("child emitted"))));
@@ -254,7 +262,7 @@ private static Node embeddedBridgeDocument(BlueRepository repository, String chi
new Node().value("/otherChild"))));
rootContracts.put("embeddedEvents", new Node()
.type("Embedded Node Channel")
- .properties("childPath", new Node().value(childPath)));
+ .properties("sourcePath", new Node().value(sourcePath)));
rootContracts.put("childObserver", directWorkflowMatching("embeddedEvents",
new Node()
.type("Coordination/Chat Message")
diff --git a/src/test/java/blue/coordination/processor/SelectiveProcessingReportArtifactTest.java b/src/test/java/blue/coordination/processor/SelectiveProcessingReportArtifactTest.java
new file mode 100644
index 0000000..e53df5d
--- /dev/null
+++ b/src/test/java/blue/coordination/processor/SelectiveProcessingReportArtifactTest.java
@@ -0,0 +1,596 @@
+package blue.coordination.processor;
+
+import blue.language.model.Node;
+import blue.language.utils.BlueIdCalculator;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Writes a truthful partial selective-processing report at the documented
+ * build path.
+ *
+ * The report counts only this artifact-producing JUnit test. Sections that
+ * require the final fixture matrix remain explicitly {@code not-run}; later
+ * fixture suites can replace them with observed evidence through the same
+ * writer.
+ */
+class SelectiveProcessingReportArtifactTest {
+ private static final Path REPORT_DIRECTORY = Paths.get(
+ System.getProperty("user.dir"),
+ "build",
+ "reports",
+ "coordination-selective-processing");
+
+ @Test
+ void writesTruthfulPartialReportWithObservedSplitterSmokeEvidence()
+ throws Exception {
+ Node exactRoot = new Node()
+ .name("Selective processing report smoke Root")
+ .properties(
+ "application",
+ new Node()
+ .properties(
+ "counter",
+ new Node().value(0))
+ .properties(
+ "unrelated",
+ new Node().value(
+ "must remain exact data")));
+ Node exactEvent = new Node()
+ .name("Selective processing report smoke Event")
+ .properties(
+ "message",
+ new Node()
+ .properties(
+ "operation",
+ new Node().value("observe"))
+ .properties(
+ "channel",
+ new Node().value("source")));
+
+ CoordinationDocumentSplitter splitter =
+ new CoordinationDocumentSplitter();
+ CoordinationDocumentSplitter.SplitGraph document =
+ splitter.splitDocument(exactRoot);
+ CoordinationDocumentSplitter.SplitGraph event =
+ splitter.splitEvent(exactEvent);
+
+ String expectedRootBlueId =
+ BlueIdCalculator.calculateBlueId(exactRoot);
+ String expectedEventBlueId =
+ BlueIdCalculator.calculateBlueId(exactEvent);
+ assertEquals(expectedRootBlueId, document.rootBlueId());
+ assertEquals(expectedEventBlueId, event.rootBlueId());
+ assertEquals(
+ expectedRootBlueId,
+ BlueIdCalculator.calculateBlueId(
+ document.fragmentedRoot()));
+ assertEquals(
+ expectedEventBlueId,
+ BlueIdCalculator.calculateBlueId(
+ event.fragmentedRoot()));
+ assertEquals(
+ expectedRootBlueId,
+ document.pureReference().getBlueId());
+ assertEquals(
+ expectedEventBlueId,
+ event.pureReference().getBlueId());
+ assertNotNull(
+ document.provider().fetchFirstByBlueId(
+ expectedRootBlueId));
+ assertNotNull(
+ event.provider().fetchFirstByBlueId(
+ expectedEventBlueId));
+
+ SelectiveProcessingReportWriter.Report report =
+ report(document, event);
+ SelectiveProcessingReportWriter.write(
+ REPORT_DIRECTORY, report);
+
+ Path artifact = REPORT_DIRECTORY.resolve(
+ SelectiveProcessingReportWriter.FILE_NAME);
+ assertTrue(Files.isRegularFile(artifact));
+ JsonNode serialized =
+ new ObjectMapper().readTree(
+ Files.readAllBytes(artifact));
+ assertEquals("partial", serialized.path("status").asText());
+ assertEquals(
+ "SelectiveProcessingReportArtifactTest only",
+ serialized.path("testCountScope").asText());
+ assertEquals(
+ 1,
+ serialized.path("testCounts")
+ .path("total")
+ .asInt());
+ JsonNode splitterEvidence =
+ section(serialized, "splitter-smoke");
+ assertEquals(
+ "splitter-smoke",
+ splitterEvidence.path("id")
+ .asText());
+ assertEquals(
+ expectedRootBlueId,
+ splitterEvidence.path("facts")
+ .path("rootBlueId")
+ .asText());
+ }
+
+ private static SelectiveProcessingReportWriter.Report report(
+ CoordinationDocumentSplitter.SplitGraph document,
+ CoordinationDocumentSplitter.SplitGraph event) {
+ Map identities =
+ new LinkedHashMap();
+ identities.put(
+ "blueBexDependency",
+ "blue-bex-java:1.1.0-rc.2");
+ identities.put(
+ "blueLanguageDevelopmentGitCommit",
+ "0a6a40d18578df784f674148d1e8b6a4319bfe49");
+ identities.put(
+ "blueLanguageDevelopmentJarSha256",
+ "sha256:7726c13cce7156a1b2f3ea600cd3225612704e83579f0c1613d03d447a057f31");
+ identities.put(
+ "blueLanguageReleasedDependency",
+ "blue-language-java:3.1.0-rc.18");
+ identities.put(
+ "blueRepositoryDependency",
+ "blue-repo-java:3.0.0-rc.10");
+ identities.put(
+ "coordinationRegistry",
+ "final-registry-unavailable; current=blue-repo-java:3.0.0-rc.10");
+ identities.put(
+ "coordinationSourceBaselineGitCommit",
+ "437e0861bb9780619a2b2e2f1c9a9c6fe5cdefef");
+ identities.put(
+ "handoffContractsFixturePackage",
+ "sha256:e35f94c329850f39c705cc3c0222c431e8d6f07142740e39e6b529c228fc96e5");
+ identities.put(
+ "handoffContractsGasPackage",
+ "sha256:88c7bbe77d531c9e973cae13002c3464a2c14568833adf5d804d13b7b3d26af5");
+ identities.put(
+ "handoffContractsRegistryPackage",
+ "sha256:14d5537efbece502ebf430e09805650dd7ea460415a7aa0a8279c2c11d1d6366");
+ identities.put(
+ "handoffLanguageFixturePackage",
+ "sha256:277418303ae10aade4029a398f880a8d0f2b321d4943492ac811287c21eb3dbb");
+ identities.put(
+ "handoffLanguageRegistryPackage",
+ "sha256:b705171a6ca62c990792bcb78db9d921caf5b0ed06370648b9a81769d69dd71e");
+ identities.put(
+ "reportProducer",
+ SelectiveProcessingReportArtifactTest.class.getName());
+
+ List sections =
+ Arrays.asList(
+ directSplitterRegressionSection(),
+ effectiveContractFragmentationSection(),
+ notRun(
+ "embedded-representation-matrix",
+ "Deep physical locality passes, but deep inline-versus-fragmented PROCESS parity is not implemented"),
+ deepLocalitySection(),
+ noEmbeddingMatrixSection(),
+ reportArtifactSection(),
+ rootOnlyEventsSection(),
+ routingBlockedSection(),
+ routingEvaluationSection(),
+ splitterSmokeSection(document, event),
+ scaleLocalitySection(),
+ notRun(
+ "ultra-complex",
+ "Design and evidence guide exists; runtime fixture not run"));
+
+ List unavailable =
+ Arrays.asList(
+ new SelectiveProcessingReportWriter.UnavailableSuite(
+ "blue-language-phase-b-peer-routing",
+ "Language commit 0a6a40d18578 filters event classification to the source channel, hiding declared peer families"),
+ new SelectiveProcessingReportWriter.UnavailableSuite(
+ "compute-bex-runtime-suites",
+ "Manifest-bound named-counter stream is unavailable"),
+ new SelectiveProcessingReportWriter.UnavailableSuite(
+ "coordination-final-registry-conformance",
+ "Final Coordination registry and final Terminate Processing shape are unavailable"));
+
+ return new SelectiveProcessingReportWriter.Report(
+ "partial",
+ identities,
+ "SelectiveProcessingReportArtifactTest only",
+ new SelectiveProcessingReportWriter.TestCounts(
+ 1, 1, 0, 0),
+ sections,
+ unavailable);
+ }
+
+ private static SelectiveProcessingReportWriter.Section
+ directSplitterRegressionSection() {
+ Map facts =
+ new LinkedHashMap();
+ facts.put(
+ "evidenceScope",
+ "direct authored Process Embedded declarations, exact registered body fields, event fragments, overlap reconstruction, and lazy provider preparation");
+ facts.put(
+ "observedCommand",
+ "./gradlew test --tests "
+ + CoordinationDocumentSplitterTest
+ .class.getName()
+ + " -PuseLocalBlueLanguage=true --no-daemon");
+
+ Map metrics =
+ new LinkedHashMap();
+ metrics.put("failed", Long.valueOf(0L));
+ metrics.put("passed", Long.valueOf(6L));
+ metrics.put("total", Long.valueOf(6L));
+
+ return new SelectiveProcessingReportWriter.Section(
+ "direct-splitter-regressions",
+ "passed",
+ Arrays.asList(
+ "cyclic-timeline-entry-type",
+ "direct-document-cuts",
+ "event-direct-fragments",
+ "lazy-verified-preparation",
+ "malformed-embedded-paths",
+ "overlapping-embedded-paths"),
+ facts,
+ metrics,
+ Collections.>emptyMap(),
+ Collections.>emptyMap());
+ }
+
+ private static SelectiveProcessingReportWriter.Section
+ effectiveContractFragmentationSection() {
+ return notRun(
+ "effective-inherited-fragmentation",
+ "Public splitter input has no effective resolved contract view; inherited Process Embedded declarations and inherited executable bodies are not cut");
+ }
+
+ private static SelectiveProcessingReportWriter.Section
+ deepLocalitySection() {
+ Map facts =
+ new LinkedHashMap();
+ facts.put(
+ "evidenceScope",
+ "physical provider demand and reconstruction; not PROCESS parity");
+ facts.put(
+ "observedCommand",
+ "./gradlew test --tests "
+ + CoordinationDocumentSplitterDeepLocalityTest
+ .class.getName()
+ + " -PuseLocalBlueLanguage=true --no-daemon");
+
+ Map metrics =
+ new LinkedHashMap();
+ metrics.put("junitInvocations", Long.valueOf(8L));
+ metrics.put("selectedScopes", Long.valueOf(4L));
+ metrics.put("selectionSurfaces", Long.valueOf(6L));
+
+ return new SelectiveProcessingReportWriter.Section(
+ "deep-physical-locality",
+ "passed",
+ Arrays.asList(
+ "exact-full-reconstruction",
+ "root-only-zero-child-demand",
+ "selected-chain-union-only",
+ "siblings-and-decoy-bodies-forbidden"),
+ facts,
+ metrics,
+ Collections.>emptyMap(),
+ Collections.>emptyMap());
+ }
+
+ private static SelectiveProcessingReportWriter.Section
+ noEmbeddingMatrixSection() {
+ Map facts =
+ new LinkedHashMap