Enhanced Validation & Semantic Detection¶
This page covers the implementation of the chain validation engine and the semantic link detection system. These two subsystems are tightly coupled — the validation engine consumes the output of the semantic link query — so they are documented together.
Architecture¶
ChainBuilder.tsx
loads semantic links on mount via /v1/dmns/enhanced-chain-links
on chain change → runValidation()
│
▼
Validation Engine (inline in ChainBuilder.tsx)
for each DMN in chain, for each input variable:
check availableOutputs Set (exact identifier matches from prior steps)
check semanticLinks array (semantic matches from prior steps)
if neither → user must provide input
determines:
isDrdCompatible = no semantic matches used
missingInputs = inputs requiring user input
│
▼
Backend: findEnhancedChainLinks (sparql.service.ts)
SPARQL query joining:
Output Variables → Output Concepts → Shared Concepts
↑
Input Variables → Input Concepts ────┘
returns matchType: 'exact' | 'semantic' | 'both'
Backend SPARQL query¶
The core of semantic detection is the findEnhancedChainLinks query. It joins output and input variables through their SKOS concepts to find shared concept URIs:
PREFIX cprmv: <https://cprmv.open-regels.nl/0.3.0/>
PREFIX cpsv: <http://www.w3.org/ns/regorg#>
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
SELECT ?dmn1Identifier ?dmn2Identifier
?outputVarId ?inputVarId
?variableType ?sharedConcept
?matchType
WHERE {
# DMN1 produces an output variable
?outputVar a cpsv:Output ;
cpsv:produces ?dmn1 ;
dct:identifier ?outputVarId ;
dct:type ?variableType .
# DMN2 requires an input variable of the same type
?inputVar a cpsv:Input ;
cpsv:isRequiredBy ?dmn2 ;
dct:identifier ?inputVarId ;
dct:type ?variableType .
FILTER(?dmn1 != ?dmn2)
?dmn1 a cprmv:DecisionModel ; dct:identifier ?dmn1Identifier .
?dmn2 a cprmv:DecisionModel ; dct:identifier ?dmn2Identifier .
# Check for semantic matching via shared concept
OPTIONAL {
?outputConcept a skos:Concept ;
skos:exactMatch ?sharedConcept ;
dct:subject ?outputVar .
?inputConcept a skos:Concept ;
skos:exactMatch ?sharedConcept ;
dct:subject ?inputVar .
}
BIND(
IF(?outputVarId = ?inputVarId && BOUND(?sharedConcept), "both",
IF(?outputVarId = ?inputVarId, "exact",
IF(BOUND(?sharedConcept), "semantic", "none")))
AS ?matchType
)
FILTER(?matchType != "none")
}
Critical requirements for correctness:
- Type compatibility (
dct:typemust match) — preventsBooleanoutputs being linked toIntegerinputs - Shared concept — both concepts must point to the identical third URI via
skos:exactMatch, not just any concept dct:subjectintegrity — the concept'sdct:subjectmust correctly point to the variable URI, not a DMN URI
Post-query processing in sparql.service.ts expands "both" records into two separate entries (one "exact", one "semantic"), so the frontend validation engine can process each match type independently:
if (matchType === 'both') {
results.push({ ...link, matchType: 'exact' });
results.push({ ...link, matchType: 'semantic' });
} else {
results.push({ ...link, matchType });
}
Frontend validation engine¶
ChainBuilder.tsx loads semantic links once on mount (or when the endpoint changes) and stores them in semanticLinks state. On every chain change, runValidation() evaluates the current chain:
const runValidation = () => {
const availableOutputs = new Set<string>();
const semanticMatches: SemanticMatch[] = [];
const missingInputs: string[] = [];
const drdIssues: string[] = [];
for (let i = 0; i < chain.length; i++) {
const dmn = chain[i];
const prevDmn = chain[i - 1];
for (const input of dmn.inputs) {
// Exact match
if (availableOutputs.has(input.identifier)) continue;
// Semantic match
const semanticLink = semanticLinks.find(
link =>
link.dmn2.identifier === dmn.identifier &&
link.dmn1.identifier === prevDmn?.identifier &&
link.inputVariable === input.identifier &&
link.matchType === 'semantic'
);
if (semanticLink) {
semanticMatches.push({ ... });
drdIssues.push(`'${input.identifier}' requires semantic match`);
continue;
}
// User must supply
missingInputs.push(input.identifier);
}
// Add this DMN's outputs to the available set
for (const output of dmn.outputs) {
availableOutputs.add(output.identifier);
}
}
const isDrdCompatible = drdIssues.length === 0;
setValidationResult({ isDrdCompatible, semanticMatches, missingInputs });
};
The isDrdCompatible flag controls which save path is offered — "Save as DRD" or "Save as Sequential Template" — and whether the DRD deployment flow is triggered.
React state dependency tracking¶
runValidation has a useEffect dependency on [chain, userInputs, semanticLinks]. The semanticLinks dependency is intentional: when the endpoint changes and new links load, validation re-runs even if the chain hasn't changed. This ensures the validation state always reflects the current endpoint's concept graph.
Debugging notes¶
During development, a common failure mode was that semantic chains appeared to have zero semantic links despite correct SPARQL syntax. The root causes discovered through debugging were:
dct:subjectpointing to the DMN URI instead of the variable URI — the CPSV Editor at one point generated concepts withdct:subject <.../dmn>instead ofdct:subject <.../dmn/output/1>. The SPARQL join then silently returned nothing.- Mismatched
dct:typevalues —"Boolean"from one agency and"boolean"(lowercase) from another prevented the type filter from matching. RDF datatype URIs are case-sensitive. skos:exactMatchpointing to the concept's own URI — a circular reference that produces no shared concept.
The validation console logs ([Validation] ✓ DRD | X missing | 0 semantic and [SemanticLinks] ✓ Loaded 26 links (16 semantic)) were added to make these failure modes visible during QA.