001package gudusoft.gsqlparser.slpc.validation;
002
003import gudusoft.gsqlparser.slpc.identity.SlpcIdentity;
004import gudusoft.gsqlparser.slpc.model.SlpcDocument;
005import gudusoft.gsqlparser.slpc.model.SlpcObject;
006
007import java.math.BigInteger;
008import java.nio.charset.StandardCharsets;
009import java.security.MessageDigest;
010import java.security.NoSuchAlgorithmException;
011import java.util.ArrayDeque;
012import java.util.ArrayList;
013import java.util.Arrays;
014import java.util.Collections;
015import java.util.Comparator;
016import java.util.HashMap;
017import java.util.HashSet;
018import java.util.LinkedHashMap;
019import java.util.LinkedHashSet;
020import java.util.List;
021import java.util.Map;
022import java.util.Set;
023import java.util.regex.Pattern;
024
025/** Executable cross-record invariants for SLPC v0.2. */
026public final class SlpcSemanticValidator {
027
028    public static final List<String> CAPABILITIES;
029    private static final Set<String> CORE_DIAGNOSTICS;
030    private static final Pattern SECRET = Pattern.compile(
031            "(?i)(?:aws[_-]?(?:secret|access)[_-]?key|secret[_-]?key|access[_-]?token|password)\\s*[:=]\\s*(?!<SLPC-REDACTED)[^\\s,;)]+");
032
033    static {
034        ArrayList<String> values = new ArrayList<String>(Arrays.asList(
035                "FACT_VALUE", "FACT_ROW_FILTER", "FACT_ROW_JOIN", "FACT_GROUP_BY", "FACT_WINDOW",
036                "DATASET_SORT", "PHYSICAL_HOP_COLLAPSE", "DYNAMIC_SQL", "PROCEDURAL_COMPOSITION",
037                "SOURCE_LOCATION", "TRANSFORM_EVIDENCE", "EXACT_IDENTIFIER_IDENTITY",
038                "AUTHORITATIVE_CATALOG_IDENTITY", "SCOPED_OBJECTS", "OBJECT_RELATIONSHIP",
039                "FOREIGN_KEY_RELATIONSHIP", "JOIN_SEMANTICS", "ROW_IMPACT_DETAIL", "STATEMENT_OPERATION",
040                "INTERMEDIATE_GRAPH", "INTERACTIVE_EXPLAIN", "LITERAL_PROVENANCE", "SECRET_REDACTION",
041                "STORAGE_ASSET_IDENTITY", "ROUTINE_CALL", "ROUTINE_PARAMETER_FLOW",
042                "ROUTINE_OVERLOAD_IDENTITY", "STREAM_DEFINITION", "OBJECT_RENAME_SWAP_CLONE",
043                "COPY_CLASSIFICATION"));
044        Collections.sort(values);
045        CAPABILITIES = Collections.unmodifiableList(values);
046        CORE_DIAGNOSTICS = Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(
047                "SLPC_ADAPTER_CAPABILITY_GAP", "SLPC_IDENTITY_APPROXIMATE", "SLPC_IDENTITY_UNAVAILABLE",
048                "SLPC_MALFORMED_IDENTIFIER", "SLPC_ENDPOINT_AMBIGUOUS", "SLPC_ENDPOINT_UNRESOLVED",
049                "SLPC_ENDPOINT_NOT_PUBLISHABLE", "SLPC_UNBOUND_RESULT_SET", "SLPC_SCOPE_UNAVAILABLE",
050                "SLPC_PSEUDO_COLUMN_REJECTED", "SLPC_INTERMEDIATE_HOP_COLLAPSED",
051                "SLPC_INTERMEDIATE_HOP_UNCOLLAPSIBLE", "SLPC_INVALID_AXIS_ROLE_OPERATION", "SLPC_ROLE_UNKNOWN",
052                "SLPC_OPERATION_UNSUPPORTED", "SLPC_RELATIONSHIP_UNMAPPED", "SLPC_DYNAMIC_LOW_CONFIDENCE",
053                "SLPC_INVALID_REFERENCE", "SLPC_ID_COLLISION", "SLPC_EXTENSION_UNSUPPORTED",
054                "SLPC_OBJECT_RELATIONSHIP_UNPUBLISHABLE", "SLPC_LEGACY_FK_DATAFLOW_RECLASSIFIED",
055                "SLPC_COMPOSITE_FK_PAIRING_UNPROVEN", "SLPC_COMPOSITE_FK_PAIRING_PROVEN",
056                "SLPC_ROUTINE_BINDING_UNPROVEN",
057                "SLPC_JOIN_SEMANTICS_PARTIAL", "SLPC_DERIVED_FIELD_EXPANSION", "SLPC_LITERAL_TRUNCATED",
058                "SLPC_SECRET_REDACTED", "SLPC_COPY_CLASSIFICATION_CONFLICT", "SLPC_STORAGE_IDENTITY_UNAVAILABLE",
059                "SLPC_NON_FACT_TRAVERSAL_UNSUPPORTED", "SLPC_STATEMENT_OPERATION_MISMATCH",
060                "SLPC_OBJECT_REBOUND_RISK", "SLPC_INTERMEDIATE_GRAPH_INCOMPLETE")));
061    }
062
063    public List<SlpcValidationError> validate(SlpcDocument document) {
064        try {
065            validateOrThrow(document.root());
066            return Collections.emptyList();
067        } catch (Failure failure) {
068            return Collections.singletonList(new SlpcValidationError(
069                    SlpcValidationError.Stage.SEMANTIC, failure.path, failure.getMessage()));
070        } catch (IllegalArgumentException failure) {
071            return Collections.singletonList(new SlpcValidationError(
072                    SlpcValidationError.Stage.SEMANTIC, "$", failure.getMessage()));
073        }
074    }
075
076    public List<SlpcValidationError> validateProjectionReport(SlpcDocument document, SlpcObject report) {
077        try {
078            validateProjectionReportOrThrow(document.root(), report);
079            return Collections.emptyList();
080        } catch (Failure failure) {
081            return Collections.singletonList(new SlpcValidationError(
082                    SlpcValidationError.Stage.SEMANTIC, failure.path, failure.getMessage()));
083        } catch (IllegalArgumentException failure) {
084            return Collections.singletonList(new SlpcValidationError(
085                    SlpcValidationError.Stage.SEMANTIC, "$", failure.getMessage()));
086        }
087    }
088
089    public void validateProjectionReportOrThrow(SlpcObject document, SlpcObject report) {
090        String[] collections = {"facts", "factCandidates", "datasetOperationInfluences", "datasetOperationCandidates",
091                "objectRelationships", "objectRelationshipCandidates", "statementOperations", "joinSemantics", "intermediateGraphs"};
092        String[] kinds = {"FACT", "FACT_CANDIDATE", "DATASET_OPERATION", "DATASET_OPERATION_CANDIDATE",
093                "OBJECT_RELATIONSHIP", "OBJECT_RELATIONSHIP_CANDIDATE", "STATEMENT_OPERATION", "JOIN_SEMANTICS", "INTERMEDIATE_GRAPH"};
094        String[] ids = {"factId", "candidateId", "influenceId", "candidateId", "relationshipId", "candidateId",
095                "statementOperationId", "joinSemanticsId", "graphRef"};
096        SlpcObject inputCounts = object(report, "inputCounts");
097        Set<String> expectedOwners = new HashSet<String>(); int total = 0;
098        for (int index = 0; index < collections.length; index++) {
099            List<Object> records = array(document, collections[index]); int count = records.size(); total += count;
100            equal(integer(inputCounts, collections[index]), (long) count, "projection report inputCounts." + collections[index]);
101            for (Object value : records) expectedOwners.add(kinds[index] + "\u0000" + string(asObject(value), ids[index]));
102        }
103        equal(integer(inputCounts, "total"), (long) total, "projection report inputCounts.total");
104        List<Object> records = array(report, "records");
105        equal(records.size(), total, "projection report records length");
106        Set<String> observedOwners = new HashSet<String>(); List<String> order = new ArrayList<String>();
107        Map<String, Integer> outcomes = new HashMap<String, Integer>(), diagnostics = new HashMap<String, Integer>();
108        for (Object value : records) {
109            SlpcObject record = asObject(value), owner = object(record, "ownerRef");
110            String ownerKey = string(owner, "kind") + "\u0000" + string(owner, "id");
111            if (!observedOwners.add(ownerKey)) fail("$.records", "projection report has duplicate owner: " + ownerKey.replace('\u0000', ':'));
112            order.add(ownerKey); String outcome = string(record, "outcome");
113            outcomes.put(outcome, outcomes.containsKey(outcome) ? outcomes.get(outcome) + 1 : 1);
114            List<String> extensionTypes = strings(array(record, "extensionTypes"));
115            List<String> diagnosticCodes = strings(array(record, "diagnosticCodes"));
116            equal(extensionTypes, sorted(extensionTypes), "projection report extensionTypes order");
117            equal(diagnosticCodes, sorted(diagnosticCodes), "projection report diagnosticCodes order");
118            if (ownerKey.startsWith("FACT_CANDIDATE\u0000") || ownerKey.startsWith("DATASET_OPERATION_CANDIDATE\u0000")
119                    || ownerKey.startsWith("OBJECT_RELATIONSHIP_CANDIDATE\u0000")) {
120                if (!"INELIGIBLE".equals(outcome) || integer(record, "standardOutputCount") != 0)
121                    fail("$.records", "projection candidate must be INELIGIBLE with standardOutputCount 0: " + ownerKey.replace('\u0000', ':'));
122            }
123            for (String code : diagnosticCodes) diagnostics.put(code, diagnostics.containsKey(code) ? diagnostics.get(code) + 1 : 1);
124        }
125        equal(observedOwners, expectedOwners, "projection report owner coverage");
126        equal(order, sorted(order), "projection report record order");
127        SlpcObject outcomeCounts = object(report, "outcomeCounts"); long outcomeTotal = 0;
128        for (String code : Arrays.asList("STANDARD", "EXTENSION_ONLY", "CONDITIONAL", "INELIGIBLE", "UNSUPPORTED")) {
129            long actual = integer(outcomeCounts, code); int expected = outcomes.containsKey(code) ? outcomes.get(code) : 0;
130            equal(actual, (long) expected, "projection report outcomeCounts." + code); outcomeTotal += actual;
131        }
132        equal(outcomeTotal, (long) total, "projection report outcome total");
133        List<Object> diagnosticCounts = array(report, "diagnosticCounts"); List<String> diagnosticOrder = new ArrayList<String>();
134        Set<String> seenCodes = new HashSet<String>();
135        for (Object value : diagnosticCounts) {
136            SlpcObject count = asObject(value); String code = string(count, "code");
137            if (!seenCodes.add(code)) fail("$.diagnosticCounts", "duplicate projection diagnostic count: " + code);
138            diagnosticOrder.add(code); equal(integer(count, "count"), (long) (diagnostics.containsKey(code) ? diagnostics.get(code) : 0),
139                    "projection report diagnosticCounts." + code);
140        }
141        equal(diagnosticOrder, sorted(diagnosticOrder), "projection report diagnosticCounts order");
142        equal(seenCodes, diagnostics.keySet(), "projection report diagnostic code coverage");
143    }
144
145    public void validateOrThrow(SlpcObject document) {
146        String version = string(document, "contractVersion");
147        if (!version.startsWith("0.2.")) fail("$", "semantic validator only accepts contractVersion 0.2.x");
148
149        SlpcObject producer = object(document, "producer");
150        List<String> capabilityCodes = fields(array(producer, "capabilities"), "code");
151        equal(capabilityCodes, CAPABILITIES, "capability registry/order");
152        Map<String, String> capabilityStatus = new HashMap<String, String>();
153        for (Object value : array(producer, "capabilities")) {
154            SlpcObject capability = asObject(value);
155            capabilityStatus.put(string(capability, "code"), string(capability, "status"));
156        }
157        if (capabilityStatus.containsValue("PARTIAL") && !hasDiagnosticCode(document, "SLPC_ADAPTER_CAPABILITY_GAP")) {
158            fail("$.producer.capabilities", "PARTIAL capability requires SLPC_ADAPTER_CAPABILITY_GAP diagnostic");
159        }
160        if (!"UNSUPPORTED".equals(capabilityStatus.get("INTERACTIVE_EXPLAIN"))
161                && "UNSUPPORTED".equals(capabilityStatus.get("INTERMEDIATE_GRAPH"))) {
162            fail("$.producer.capabilities", "INTERACTIVE_EXPLAIN requires INTERMEDIATE_GRAPH capability");
163        }
164        validateProfileLocations(document);
165
166        List<Object> artifactsList = array(document, "artifacts");
167        List<Object> statementsList = array(document, "statements");
168        List<Object> endpointsList = array(document, "endpoints");
169        Map<String, SlpcObject> artifacts = uniqueIndex(artifactsList, "artifactId", "artifact");
170        Map<String, SlpcObject> statements = uniqueIndex(statementsList, "statementId", "statement");
171        Map<String, SlpcObject> endpoints = uniqueIndex(endpointsList, "endpointRef", "endpoint");
172        equal(fields(artifactsList, "artifactId"), sorted(artifacts.keySet()), "artifact order");
173        equal(fields(statementsList, "statementId"), sorted(statements.keySet()), "statement order");
174        for (Object value : artifactsList) {
175            SlpcObject item = asObject(value);
176            equal(string(item, "artifactId"), SlpcIdentity.artifactId(item), "artifactId");
177        }
178        for (Object value : statementsList) {
179            SlpcObject item = asObject(value);
180            requireRef(artifacts, string(item, "artifactId"), "statement references missing artifact");
181            equal(string(item, "statementId"), SlpcIdentity.statementId(item), "statementId");
182        }
183
184        List<String> endpointSort = new ArrayList<String>();
185        for (Object value : endpointsList) {
186            SlpcObject endpoint = asObject(value);
187            String canonical = endpoint.string("canonicalEndpointId");
188            if (canonical != null) {
189                equal(canonical, SlpcIdentity.endpointId(endpoint), string(endpoint, "endpointRef") + " canonicalEndpointId");
190                endpointSort.add("0\u0000" + canonical);
191            } else {
192                endpointSort.add("1\u0000" + SlpcIdentity.candidateEndpointKey(endpoint));
193            }
194            validateUriDigest(endpoint);
195        }
196        validateRoutineParameterProfiles(endpointsList);
197        equal(endpointSort, sorted(endpointSort), "endpoint order");
198        localIds(fields(endpointsList, "endpointRef"), "ep-", "endpoint");
199
200        List<Object> facts = array(document, "facts");
201        List<Object> factCandidates = array(document, "factCandidates");
202        List<Object> influences = array(document, "datasetOperationInfluences");
203        List<Object> operationCandidates = array(document, "datasetOperationCandidates");
204        List<Object> relationships = array(document, "objectRelationships");
205        List<Object> relationshipCandidates = array(document, "objectRelationshipCandidates");
206        Map<String, SlpcObject> factById = uniqueIndex(facts, "factId", "factId");
207        Map<String, SlpcObject> factCandidateById = uniqueIndex(factCandidates, "candidateId", "fact candidateId");
208        Map<String, SlpcObject> influenceById = uniqueIndex(influences, "influenceId", "influenceId");
209        Map<String, SlpcObject> operationCandidateById = uniqueIndex(operationCandidates, "candidateId", "operation candidateId");
210        Map<String, SlpcObject> relationshipById = uniqueIndex(relationships, "relationshipId", "object relationshipId");
211        Map<String, SlpcObject> relationshipCandidateById = uniqueIndex(relationshipCandidates, "candidateId", "object relationship candidateId");
212        unique(fields(factCandidates, "candidateFingerprint"), "fact candidate fingerprint");
213        unique(fields(operationCandidates, "candidateFingerprint"), "operation candidate fingerprint");
214        unique(fields(relationshipCandidates, "candidateFingerprint"), "object relationship candidate fingerprint");
215
216        for (Object value : facts) {
217            SlpcObject item = asObject(value); validateFactEndpointLevels(item, endpoints);
218            equal(string(item, "factId"), SlpcIdentity.factId(item, endpoints), "factId");
219        }
220        for (Object value : factCandidates) {
221            SlpcObject item = asObject(value); validateFactEndpointLevels(item, endpoints);
222            equal(string(item, "candidateFingerprint"), SlpcIdentity.factCandidateFingerprint(item, endpoints), "fact candidate fingerprint");
223        }
224        for (Object value : influences) {
225            SlpcObject item = asObject(value); equal(string(item, "influenceId"), SlpcIdentity.datasetOperationInfluenceId(item, endpoints), "dataset influenceId");
226        }
227        for (Object value : operationCandidates) {
228            SlpcObject item = asObject(value); equal(string(item, "candidateFingerprint"), SlpcIdentity.datasetOperationCandidateFingerprint(item, endpoints), "dataset operation candidate fingerprint");
229        }
230        equal(fields(facts, "factId"), sorted(factById.keySet()), "fact order");
231        equal(fields(influences, "influenceId"), sorted(influenceById.keySet()), "influence order");
232        equal(fields(factCandidates, "candidateFingerprint"), sorted(fields(factCandidates, "candidateFingerprint")), "fact candidate order");
233        localIds(fields(factCandidates, "candidateId"), "fact-cand-", "fact candidate");
234        equal(fields(operationCandidates, "candidateFingerprint"), sorted(fields(operationCandidates, "candidateFingerprint")), "operation candidate order");
235        localIds(fields(operationCandidates, "candidateId"), "op-cand-", "operation candidate");
236
237        for (Object value : relationships) {
238            SlpcObject item = asObject(value); validateRelationshipRoles(item);
239            equal(string(item, "relationshipId"), SlpcIdentity.objectRelationshipId(item, endpoints), "object relationshipId");
240        }
241        for (Object value : relationshipCandidates) {
242            SlpcObject item = asObject(value); validateRelationshipRoles(item);
243            equal(string(item, "candidateFingerprint"), SlpcIdentity.objectRelationshipCandidateFingerprint(item, endpoints), "object relationship candidate fingerprint");
244        }
245        equal(fields(relationships, "relationshipId"), sorted(relationshipById.keySet()), "object relationship order");
246        equal(fields(relationshipCandidates, "candidateFingerprint"), sorted(fields(relationshipCandidates, "candidateFingerprint")), "object relationship candidate order");
247        localIds(fields(relationshipCandidates, "candidateId"), "objrel-cand-", "object relationship candidate");
248
249        List<Object> operations = array(document, "statementOperations");
250        Map<String, SlpcObject> operationById = uniqueIndex(operations, "statementOperationId", "statement operationId");
251        validateGroupedOrdinals(operations, "statementId", "operationOrdinal", statements, "statement operation references missing statement", " operation");
252        for (Object value : operations) {
253            SlpcObject item = asObject(value); validateOperationRoles(item, endpoints);
254            equal(string(item, "statementOperationId"), SlpcIdentity.statementOperationId(item, endpoints), "statement operationId");
255        }
256        equal(fields(operations, "statementOperationId"), sorted(operationById.keySet()), "statement operation order");
257
258        List<Object> joins = array(document, "joinSemantics");
259        Map<String, SlpcObject> joinById = uniqueIndex(joins, "joinSemanticsId", "joinSemanticsId");
260        validateGroupedOrdinals(joins, "statementId", "joinOrdinal", statements, "join semantics references missing statement", " join");
261        Set<String> factCandidateFingerprints = new HashSet<String>(fields(factCandidates, "candidateFingerprint"));
262        for (Object value : joins) {
263            SlpcObject item = asObject(value);
264            contiguous(integers(array(item, "keyPairs"), "ordinal"), string(item, "joinSemanticsId") + " key-pair");
265            equal(string(item, "joinSemanticsId"), SlpcIdentity.joinSemanticsId(item, endpoints), "joinSemanticsId");
266            refsExist(strings(array(item, "refinesFactIds")), factById.keySet(), "join semantics references missing fact");
267            refsExist(strings(array(item, "refinesFactCandidateFingerprints")), factCandidateFingerprints, "join semantics references missing fact candidate fingerprint");
268        }
269        equal(fields(joins, "joinSemanticsId"), sorted(joinById.keySet()), "join semantics order");
270
271        List<Object> graphs = array(document, "intermediateGraphs");
272        Map<String, SlpcObject> graphByRef = uniqueIndex(graphs, "graphRef", "intermediate graphRef");
273        unique(fields(graphs, "graphFingerprint"), "intermediate graph fingerprint");
274        validateGroupedOrdinals(graphs, "statementId", "graphOrdinal", statements, "intermediate graph references missing statement", " graph");
275        List<String> graphSort = new ArrayList<String>();
276        for (Object value : graphs) {
277            SlpcObject graph = asObject(value);
278            validateGraph(graph, endpoints, document, capabilityStatus);
279            graphSort.add(string(graph, "statementId") + "\u0000" + string(graph, "graphFingerprint"));
280        }
281        equal(graphSort, sorted(graphSort), "intermediate graph order");
282        localIds(fields(graphs, "graphRef"), "graph-", "intermediate graph");
283
284        Map<String, String> ownerKeys = new HashMap<String, String>();
285        addOwnerKeys(ownerKeys, facts, "FACT", "factId", "factId");
286        addOwnerKeys(ownerKeys, factCandidates, "FACT_CANDIDATE", "candidateId", "candidateFingerprint");
287        addOwnerKeys(ownerKeys, influences, "DATASET_OPERATION", "influenceId", "influenceId");
288        addOwnerKeys(ownerKeys, operationCandidates, "DATASET_OPERATION_CANDIDATE", "candidateId", "candidateFingerprint");
289        addOwnerKeys(ownerKeys, relationships, "OBJECT_RELATIONSHIP", "relationshipId", "relationshipId");
290        addOwnerKeys(ownerKeys, relationshipCandidates, "OBJECT_RELATIONSHIP_CANDIDATE", "candidateId", "candidateFingerprint");
291
292        List<Object> occurrences = array(document, "occurrences");
293        Map<String, List<SlpcObject>> occurrencesByOwner = new HashMap<String, List<SlpcObject>>();
294        List<String> occurrenceIds = new ArrayList<String>();
295        for (Object value : occurrences) {
296            SlpcObject occurrence = asObject(value);
297            SlpcObject owner = object(occurrence, "ownerRef");
298            String ownerKey = string(owner, "kind") + "\u0000" + string(owner, "id");
299            if (!ownerKeys.containsKey(ownerKey)) fail("$.occurrences", "occurrence references missing owner: " + ownerKey.replace('\u0000', ':'));
300            requireRef(artifacts, string(occurrence, "artifactId"), "occurrence references missing artifact/statement");
301            if (occurrence.string("statementId") != null) requireRef(statements, occurrence.string("statementId"), "occurrence references missing artifact/statement");
302            equal(string(occurrence, "occurrenceId"), SlpcIdentity.occurrenceId(occurrence, ownerKeys), "occurrenceId");
303            validateSupportPaths(occurrence, graphByRef);
304            validateLiterals(occurrence, artifacts);
305            if (!occurrencesByOwner.containsKey(ownerKey)) occurrencesByOwner.put(ownerKey, new ArrayList<SlpcObject>());
306            occurrencesByOwner.get(ownerKey).add(occurrence);
307            occurrenceIds.add(string(occurrence, "occurrenceId"));
308        }
309        equal(occurrenceIds, sorted(occurrenceIds), "occurrence order");
310        unique(occurrenceIds, "occurrenceId");
311        validateQuality(facts, "FACT", "factId", false, occurrencesByOwner);
312        validateQuality(factCandidates, "FACT_CANDIDATE", "candidateId", true, occurrencesByOwner);
313        validateQuality(influences, "DATASET_OPERATION", "influenceId", false, occurrencesByOwner);
314        validateQuality(operationCandidates, "DATASET_OPERATION_CANDIDATE", "candidateId", true, occurrencesByOwner);
315        validateQuality(relationships, "OBJECT_RELATIONSHIP", "relationshipId", false, occurrencesByOwner);
316        validateQuality(relationshipCandidates, "OBJECT_RELATIONSHIP_CANDIDATE", "candidateId", true, occurrencesByOwner);
317
318        validateRenameSwap(operations, facts);
319        validateCreateStream(operations, relationships);
320        validateDiagnostics(document, artifacts.keySet(), statements.keySet(), endpoints.keySet(), factById.keySet(),
321                factCandidateById.keySet(), influenceById.keySet(), operationCandidateById.keySet(), relationshipById.keySet(),
322                relationshipCandidateById.keySet(), operationById.keySet(), joinById.keySet(), graphByRef, occurrenceIds);
323        validateSecretHygiene(document);
324    }
325
326    private void validateGraph(SlpcObject graph, Map<String, SlpcObject> endpoints, SlpcObject document,
327            Map<String, String> capabilityStatus) {
328        List<Object> nodeList = array(graph, "nodes"), edgeList = array(graph, "edges"), pathList = array(graph, "paths");
329        Map<String, SlpcObject> nodes = uniqueIndex(nodeList, "nodeRef", string(graph, "graphRef") + " nodeRef");
330        Map<String, SlpcObject> edges = uniqueIndex(edgeList, "edgeRef", string(graph, "graphRef") + " edgeRef");
331        unique(fields(nodeList, "nodeFingerprint"), string(graph, "graphRef") + " node fingerprint");
332        unique(fields(edgeList, "edgeFingerprint"), string(graph, "graphRef") + " edge fingerprint");
333        unique(fields(pathList, "pathRef"), string(graph, "graphRef") + " pathRef");
334        unique(fields(pathList, "pathFingerprint"), string(graph, "graphRef") + " path fingerprint");
335        contiguous(sortedIntegers(nodeList, "nodeOrdinal"), string(graph, "graphRef") + " node");
336        for (Object value : nodeList) {
337            SlpcObject node = asObject(value); String parentRef = nullableString(node, "parentNodeRef");
338            if (parentRef != null) {
339                SlpcObject parent = nodes.get(parentRef);
340                if (parent == null) fail("$.intermediateGraphs", "intermediate node references missing parent: " + parentRef);
341                if ("BOUNDARY_ENDPOINT".equals(string(parent, "nodeKind"))) fail("$.intermediateGraphs", "BOUNDARY_ENDPOINT cannot contain intermediate nodes");
342                if (integer(parent, "nodeOrdinal") >= integer(node, "nodeOrdinal")) fail("$.intermediateGraphs", "intermediate containment must be acyclic and parent ordinal must precede child");
343            }
344            equal(string(node, "nodeFingerprint"), SlpcIdentity.intermediateNodeFingerprint(node, graph, endpoints), "intermediate node fingerprint");
345        }
346        equal(fields(nodeList, "nodeFingerprint"), sorted(fields(nodeList, "nodeFingerprint")), string(graph, "graphRef") + " node order");
347        localIds(fields(nodeList, "nodeRef"), "ign-", string(graph, "graphRef") + " node");
348        for (Object value : edgeList) {
349            SlpcObject edge = asObject(value);
350            validateAnchor(object(edge, "source"), nodes, endpoints, "edge");
351            validateAnchor(object(edge, "target"), nodes, endpoints, "edge");
352            equal(string(edge, "edgeFingerprint"), SlpcIdentity.intermediateEdgeFingerprint(edge, endpoints, nodes), "intermediate edge fingerprint");
353        }
354        equal(fields(edgeList, "edgeFingerprint"), sorted(fields(edgeList, "edgeFingerprint")), string(graph, "graphRef") + " edge order");
355        localIds(fields(edgeList, "edgeRef"), "ige-", string(graph, "graphRef") + " edge");
356        for (Object value : pathList) {
357            SlpcObject path = asObject(value);
358            refsExist(strings(array(path, "edgeRefs")), edges.keySet(), "path references missing graph edge");
359            equal(string(path, "pathFingerprint"), SlpcIdentity.intermediatePathFingerprint(path, edges), "intermediate path fingerprint");
360        }
361        equal(fields(pathList, "pathFingerprint"), sorted(fields(pathList, "pathFingerprint")), string(graph, "graphRef") + " path order");
362        localIds(fields(pathList, "pathRef"), "igp-", string(graph, "graphRef") + " path");
363        equal(string(graph, "graphFingerprint"), SlpcIdentity.intermediateGraphFingerprint(graph), "intermediate graph fingerprint");
364
365        List<SlpcObject> profiles = extensions(graph, "gsp.interactive-explain.v1");
366        if (profiles.size() > 1) fail("$.intermediateGraphs", string(graph, "graphRef") + " has duplicate interactive explain profiles");
367        if (!profiles.isEmpty()) {
368            if ("UNSUPPORTED".equals(capabilityStatus.get("INTERACTIVE_EXPLAIN"))) fail("$.intermediateGraphs", "interactive explain profile requires declared capability");
369            List<Object> steps = array(object(profiles.get(0), "payload"), "steps");
370            contiguous(integers(steps, "stepOrdinal"), string(graph, "graphRef") + " interactive step");
371            localIds(fields(steps, "stepRef"), "igs-", string(graph, "graphRef") + " interactive step");
372            unique(fields(steps, "stepFingerprint"), string(graph, "graphRef") + " interactive step fingerprint");
373            Set<String> usedEdges = new HashSet<String>();
374            for (Object value : steps) {
375                SlpcObject step = asObject(value); List<Object> bindings = array(step, "inputBindings");
376                contiguous(integers(bindings, "ordinal"), string(step, "stepRef") + " input binding");
377                for (Object bindingValue : bindings) {
378                    SlpcObject binding = asObject(bindingValue); String edgeRef = string(binding, "edgeRef");
379                    SlpcObject edge = edges.get(edgeRef);
380                    if (edge == null) fail("$.intermediateGraphs", "interactive step references missing graph edge: " + edgeRef);
381                    if (!usedEdges.add(edgeRef)) fail("$.intermediateGraphs", "interactive edge is bound to multiple steps: " + edgeRef);
382                    if (!object(edge, "target").equals(object(step, "resultAnchor"))) fail("$.intermediateGraphs", "interactive input edge target does not match resultAnchor: " + edgeRef);
383                }
384                validateAnchor(object(step, "resultAnchor"), nodes, endpoints, "interactive step");
385                equal(string(step, "stepFingerprint"), SlpcIdentity.interactiveStepFingerprint(step, graph, endpoints, nodes, edges), "interactive step fingerprint");
386            }
387        }
388        validateGraphCompleteness(graph, array(document, "diagnostics"));
389    }
390
391    private void validateGraphCompleteness(SlpcObject graph, List<Object> diagnostics) {
392        List<SlpcObject> incomplete = new ArrayList<SlpcObject>(), bare = new ArrayList<SlpcObject>();
393        String graphRef = string(graph, "graphRef");
394        for (Object value : diagnostics) {
395            SlpcObject diagnostic = asObject(value);
396            String ref = nullableString(diagnostic, "ref");
397            if ("SLPC_INTERMEDIATE_GRAPH_INCOMPLETE".equals(string(diagnostic, "code"))
398                    && "INTERMEDIATE_GRAPH".equals(string(diagnostic, "scope")) && ref != null
399                    && (ref.equals(graphRef) || ref.startsWith(graphRef + "/"))) {
400                incomplete.add(diagnostic); if (ref.equals(graphRef)) bare.add(diagnostic);
401            }
402        }
403        String completeness = string(graph, "analysisCompleteness");
404        if ("COMPLETE".equals(completeness) && !incomplete.isEmpty()) fail("$.intermediateGraphs", "COMPLETE graph cannot carry an incomplete diagnostic");
405        if (("PARTIAL".equals(completeness) || "UNKNOWN".equals(completeness)) && bare.isEmpty()) {
406            fail("$.intermediateGraphs", "PARTIAL/UNKNOWN graph requires a bare graph incomplete diagnostic");
407        }
408        for (SlpcObject diagnostic : bare) {
409            if (!"WARN".equals(string(diagnostic, "severity")) && !"ERROR".equals(string(diagnostic, "severity")))
410                fail("$.diagnostics", "incomplete graph diagnostic severity must be WARN or ERROR");
411        }
412    }
413
414    private void validateLiterals(SlpcObject occurrence, Map<String, SlpcObject> artifacts) {
415        List<Object> literals = optionalArray(occurrence, "literalInputs");
416        contiguous(integers(literals, "inputOrdinal"), string(occurrence, "occurrenceId") + " literal input");
417        String dialect = string(artifacts.get(string(occurrence, "artifactId")), "sqlDialect");
418        for (Object value : literals) {
419            SlpcObject literal = asObject(value); SlpcObject fingerprint = object(literal, "fingerprint");
420            String algorithm = string(fingerprint, "algorithm"), expected;
421            if ("SLPC-REDACTED-LITERAL-V1".equals(algorithm)) {
422                long ordinal = integer(literal, "redactionOrdinal");
423                expected = sha256("SLPC-REDACTED-LITERAL-V1", SlpcIdentity.string(dialect),
424                        SlpcIdentity.string(string(literal, "literalKind")), SlpcIdentity.integer(ordinal));
425                equal(string(fingerprint, "value"), expected, "redacted literal fingerprint");
426                if (literal.contains("text")) equal(literal.string("text"), "<SLPC-REDACTED:" + string(literal, "literalKind") + ":" + ordinal + ">", "redacted literal marker");
427            } else if ("SLPC-LITERAL-TEXT-V1".equals(algorithm) && literal.contains("text")) {
428                expected = sha256("SLPC-LITERAL-TEXT-V1", SlpcIdentity.string(dialect),
429                        SlpcIdentity.string(string(literal, "literalKind")), SlpcIdentity.string(literal.string("text")));
430                equal(string(fingerprint, "value"), expected, "literal fingerprint");
431            }
432        }
433    }
434
435    private void validateSupportPaths(SlpcObject occurrence, Map<String, SlpcObject> graphs) {
436        for (Object value : optionalArray(occurrence, "supportPathRefs")) {
437            SlpcObject support = asObject(value); SlpcObject graph = graphs.get(string(support, "graphRef"));
438            if (graph == null || !fields(array(graph, "paths"), "pathRef").contains(string(support, "pathRef")))
439                fail("$.occurrences", "occurrence has dangling support path: " + support.asMap());
440        }
441    }
442
443    private void validateQuality(List<Object> records, String ownerKind, String idField, boolean alwaysIneligible,
444            Map<String, List<SlpcObject>> occurrencesByOwner) {
445        for (Object value : records) {
446            SlpcObject record = asObject(value); String owner = ownerKind + "\u0000" + string(record, idField);
447            List<SlpcObject> evidence = occurrencesByOwner.get(owner);
448            if (evidence == null || evidence.isEmpty()) fail("$", "owner has no occurrence: " + owner.replace('\u0000', ':'));
449            boolean ineligible = alwaysIneligible || ("UNKNOWN".equals(record.string("role"))
450                    && "NONE".equals(record.string("operationKind")) && optionalArray(record, "extensions").isEmpty());
451            equal(object(record, "qualitySummary"), aggregateQuality(evidence, ineligible), owner.replace('\u0000', ':') + " qualitySummary");
452        }
453    }
454
455    private SlpcObject aggregateQuality(List<SlpcObject> occurrences, boolean ineligible) {
456        String resolution = worst(occurrences, "resolutionGrade", Arrays.asList("NOT_APPLICABLE", "RESOLVED", "GUESSED", "PARTIAL", "UNRESOLVED", "AMBIGUOUS", "PARSER_UNSUPPORTED"));
457        String evaluation = worst(occurrences, "evaluationModel", Arrays.asList("EXACT", "OVER_APPROXIMATE", "HEURISTIC", "UNKNOWN"));
458        String proof = worst(occurrences, "semanticProof", Arrays.asList("PROVEN", "INFERRED", "ASSUMED", "UNKNOWN"));
459        boolean dynamic = false; long confidence = 1000000;
460        Set<String> ids = new HashSet<String>();
461        for (SlpcObject occurrence : occurrences) {
462            SlpcObject quality = object(occurrence, "quality");
463            dynamic |= Boolean.TRUE.equals(quality.get("dynamic"));
464            confidence = Math.min(confidence, integer(quality, "confidenceMicros")); ids.add(string(occurrence, "occurrenceId"));
465        }
466        String eligibility = ineligible ? "INELIGIBLE" :
467                (("RESOLVED".equals(resolution) || "NOT_APPLICABLE".equals(resolution)) && "EXACT".equals(evaluation)
468                        && "PROVEN".equals(proof) && !dynamic && confidence == 1000000 ? "ELIGIBLE" : "CONDITIONAL");
469        return SlpcObject.builder().put("projectionEligibility", eligibility).put("resolutionGrade", resolution)
470                .put("evaluationModel", evaluation).put("semanticProof", proof).put("dynamic", dynamic)
471                .put("confidenceMicros", confidence).put("evidenceCount", ids.size()).build();
472    }
473
474    private String worst(List<SlpcObject> occurrences, String field, List<String> rank) {
475        String worst = rank.get(0);
476        for (SlpcObject occurrence : occurrences) {
477            String value = string(object(occurrence, "quality"), field);
478            if (rank.indexOf(value) > rank.indexOf(worst)) worst = value;
479        }
480        return worst;
481    }
482
483    private void validateRelationshipRoles(SlpcObject record) {
484        List<String> actual = fields(array(record, "participants"), "role");
485        List<String> expected = relationshipRoles(string(record, "relationshipKind"));
486        equal(actual, expected, (record.string("relationshipId") == null ? record.string("candidateId") : record.string("relationshipId")) + " participant roles");
487        List<Object> details = record.array("keyFields"); if (details == null || details.isEmpty()) details = optionalArray(record, "keyPairs");
488        if (!details.isEmpty()) contiguous(integers(details, "ordinal"), "relationship detail");
489    }
490
491    private void validateOperationRoles(SlpcObject record, Map<String, SlpcObject> endpoints) {
492        List<RoleRule> rules = operationRoles(string(record, "operationKind"));
493        Map<String, List<SlpcObject>> byRole = new LinkedHashMap<String, List<SlpcObject>>();
494        for (Object value : array(record, "participants")) {
495            SlpcObject participant = asObject(value); String role = string(participant, "role");
496            if (!byRole.containsKey(role)) byRole.put(role, new ArrayList<SlpcObject>());
497            byRole.get(role).add(participant);
498        }
499        Set<String> known = new HashSet<String>(); for (RoleRule rule : rules) known.add(rule.role);
500        Set<String> unknown = new HashSet<String>(byRole.keySet()); unknown.removeAll(known);
501        if (!unknown.isEmpty()) fail("$.statementOperations", string(record, "statementOperationId") + " has invalid participant roles: " + unknown);
502        List<Object> expected = new ArrayList<Object>();
503        for (RoleRule rule : rules) {
504            List<SlpcObject> values = byRole.containsKey(rule.role) ? byRole.get(rule.role) : new ArrayList<SlpcObject>();
505            if (rule.required && values.size() != 1) fail("$.statementOperations", string(record, "statementOperationId") + " requires exactly one " + rule.role);
506            if (!rule.setLike && values.size() > 1) fail("$.statementOperations", string(record, "statementOperationId") + " repeats singleton role " + rule.role);
507            if (rule.setLike) Collections.sort(values, endpointParticipantComparator(endpoints));
508            expected.addAll(values);
509        }
510        equal(array(record, "participants"), expected, string(record, "statementOperationId") + " participant order");
511    }
512
513    private void validateRenameSwap(List<Object> operations, List<Object> facts) {
514        for (Object value : operations) {
515            SlpcObject operation = asObject(value); String kind = string(operation, "operationKind");
516            if (!"RENAME".equals(kind) && !"SWAP".equals(kind)) continue;
517            Set<String> refs = new HashSet<String>(fields(array(operation, "participants"), "endpointRef"));
518            for (Object factValue : facts) {
519                SlpcObject fact = asObject(factValue);
520                if ("VALUE".equals(string(fact, "axis")) && "DIRECT".equals(string(fact, "role"))
521                        && refs.contains(fact.string("sourceEndpointRef")) && refs.contains(string(fact, "targetEndpointRef")))
522                    fail("$.facts", kind + " must not create a default VALUE/DIRECT fact");
523            }
524        }
525    }
526
527    private void validateCreateStream(List<Object> operations, List<Object> relationships) {
528        Set<String> index = new HashSet<String>();
529        for (Object value : relationships) {
530            SlpcObject relation = asObject(value); StringBuilder key = new StringBuilder(string(relation, "relationshipKind"));
531            for (Object p : array(relation, "participants")) { SlpcObject participant = asObject(p); key.append('|').append(string(participant, "role")).append('=').append(string(participant, "endpointRef")); }
532            index.add(key.toString());
533        }
534        for (Object value : operations) {
535            SlpcObject operation = asObject(value); if (!"CREATE_STREAM".equals(string(operation, "operationKind"))) continue;
536            Map<String, String> roles = new HashMap<String, String>();
537            for (Object p : array(operation, "participants")) { SlpcObject participant = asObject(p); roles.put(string(participant, "role"), string(participant, "endpointRef")); }
538            String expected = "STREAMS_FROM|STREAM=" + roles.get("CREATED_STREAM") + "|BASE_RELATION=" + roles.get("BASE_RELATION");
539            if (!index.contains(expected)) fail("$.statementOperations", "CREATE_STREAM requires a STREAMS_FROM object relationship");
540        }
541    }
542
543    private void validateDiagnostics(SlpcObject document, Set<String> artifacts, Set<String> statements, Set<String> endpoints,
544            Set<String> facts, Set<String> factCandidates, Set<String> influences, Set<String> operationCandidates,
545            Set<String> relationships, Set<String> relationshipCandidates, Set<String> operations, Set<String> joins,
546            Map<String, SlpcObject> graphs, List<String> occurrences) {
547        Set<String> factLike = union(facts, factCandidates, influences, operationCandidates);
548        Set<String> relationshipLike = union(relationships, relationshipCandidates);
549        Set<String> intermediate = new HashSet<String>(graphs.keySet());
550        for (SlpcObject graph : graphs.values()) {
551            String prefix = string(graph, "graphRef") + "/";
552            for (String ref : fields(array(graph, "nodes"), "nodeRef")) intermediate.add(prefix + ref);
553            for (String ref : fields(array(graph, "edges"), "edgeRef")) intermediate.add(prefix + ref);
554            for (String ref : fields(array(graph, "paths"), "pathRef")) intermediate.add(prefix + ref);
555            for (SlpcObject extension : extensions(graph, "gsp.interactive-explain.v1"))
556                for (String ref : fields(array(object(extension, "payload"), "steps"), "stepRef")) intermediate.add(prefix + ref);
557        }
558        Map<String, Set<String>> targets = new HashMap<String, Set<String>>();
559        targets.put("ARTIFACT", artifacts); targets.put("STATEMENT", statements); targets.put("ENDPOINT", endpoints);
560        targets.put("FACT", factLike); targets.put("OCCURRENCE", new HashSet<String>(occurrences));
561        targets.put("OBJECT_RELATIONSHIP", relationshipLike); targets.put("STATEMENT_OPERATION", operations);
562        targets.put("JOIN_SEMANTICS", joins); targets.put("INTERMEDIATE_GRAPH", intermediate);
563        Set<String> extensionTargets = union(factLike, relationshipLike, operations, joins, graphs.keySet(), new HashSet<String>(occurrences), endpoints);
564        for (Object value : array(document, "diagnostics")) {
565            SlpcObject diagnostic = asObject(value); String scope = string(diagnostic, "scope"), ref = nullableString(diagnostic, "ref");
566            String code = string(diagnostic, "code");
567            if (code.startsWith("SLPC_") && !CORE_DIAGNOSTICS.contains(code))
568                fail("$.diagnostics", "unregistered core diagnostic code: " + code);
569            if ("DOCUMENT".equals(scope) && ref != null) fail("$.diagnostics", "DOCUMENT diagnostic ref must be null");
570            if (targets.containsKey(scope) && !targets.get(scope).contains(ref)) fail("$.diagnostics", "diagnostic references missing " + scope + " owner: " + ref);
571            if ("EXTENSION".equals(scope) && ref != null && !extensionTargets.contains(ref)) fail("$.diagnostics", "diagnostic references missing EXTENSION owner: " + ref);
572        }
573    }
574
575    private void validateSecretHygiene(SlpcObject document) {
576        ArrayDeque<PathValue> stack = new ArrayDeque<PathValue>(); stack.push(new PathValue("$", document));
577        while (!stack.isEmpty()) {
578            PathValue current = stack.pop(); Object value = current.value;
579            if (value instanceof String && SECRET.matcher((String) value).find()) fail(current.path, "secret-bearing text is not redacted at " + current.path);
580            if (value instanceof SlpcObject) for (String name : ((SlpcObject) value).names()) stack.push(new PathValue(current.path + "." + name, ((SlpcObject) value).get(name)));
581            else if (value instanceof List<?>) for (int index = 0; index < ((List<?>) value).size(); index++) stack.push(new PathValue(current.path + "[" + index + "]", ((List<?>) value).get(index)));
582        }
583    }
584
585    private boolean hasDiagnosticCode(SlpcObject document, String expected) {
586        for (Object value : array(document, "diagnostics")) {
587            if (expected.equals(asObject(value).string("code"))) return true;
588        }
589        return false;
590    }
591
592    private void validateFactEndpointLevels(SlpcObject fact, Map<String, SlpcObject> endpoints) {
593        if (!"ROW".equals(string(fact, "axis"))) return;
594        String targetRef = string(fact, "targetEndpointRef");
595        SlpcObject target = endpoints.get(targetRef);
596        if (target == null) fail("$.facts", "ROW fact references missing target endpoint: " + targetRef);
597        if (!"RELATION".equals(string(target, "endpointLevel")))
598            fail("$.facts", "ROW fact target must be a holder RELATION endpoint: " + targetRef);
599    }
600
601    private void validateProfileLocations(SlpcObject document) {
602        ArrayDeque<PathValue> stack = new ArrayDeque<PathValue>(); stack.push(new PathValue("$", document));
603        Pattern allowed = Pattern.compile("\\$\\.intermediateGraphs\\[[0-9]+\\]\\.extensions\\[[0-9]+\\]");
604        while (!stack.isEmpty()) {
605            PathValue current = stack.pop(); Object value = current.value;
606            if (value instanceof SlpcObject) {
607                SlpcObject object = (SlpcObject) value;
608                if ("gsp.interactive-explain.v1".equals(object.string("extensionType")) && object.contains("schemaVersion")
609                        && object.contains("criticality") && object.contains("payload") && !allowed.matcher(current.path).matches())
610                    fail(current.path, "interactive explain profile must be graph-local: " + current.path);
611                for (String name : object.names()) stack.push(new PathValue(current.path + "." + name, object.get(name)));
612            } else if (value instanceof List<?>) for (int index = 0; index < ((List<?>) value).size(); index++) stack.push(new PathValue(current.path + "[" + index + "]", ((List<?>) value).get(index)));
613        }
614    }
615
616    private void validateRoutineParameterProfiles(List<Object> endpoints) {
617        for (Object value : endpoints) {
618            SlpcObject endpoint = asObject(value); int profiles = 0;
619            for (Object extensionValue : array(endpoint, "extensions")) {
620                SlpcObject extension = asObject(extensionValue);
621                if ("gsp.routine-parameter.v1".equals(extension.string("extensionType"))) profiles++;
622            }
623            boolean parameter = "PARAMETER".equals(endpoint.string("objectKind"));
624            if (parameter && profiles != 1) fail("$.endpoints",
625                    "PARAMETER endpoint requires exactly one gsp.routine-parameter.v1 profile");
626            if (!parameter && profiles != 0) fail("$.endpoints",
627                    "gsp.routine-parameter.v1 is allowed only on PARAMETER endpoints");
628        }
629    }
630
631    private void validateUriDigest(SlpcObject endpoint) {
632        SlpcObject identity = object(endpoint, "identity"); if (!"URI_EXACT".equals(string(identity, "kind"))) return;
633        for (SlpcObject extension : extensions(endpoint, "gsp.storage-asset.v1")) {
634            String locator = object(extension, "payload").string("secretFreeNormalizedLocator");
635            if (locator != null) {
636                String expected = sha256("SLPC-URI-LOCATOR-DIGEST-V1",
637                        SlpcIdentity.string(string(identity, "normalizationPolicyId")), SlpcIdentity.string(string(identity, "scheme")), SlpcIdentity.string(locator));
638                equal(string(identity, "locatorDigest"), expected, string(endpoint, "endpointRef") + " locatorDigest");
639            }
640        }
641    }
642
643    private static String sha256(String domain, byte[]... payload) {
644        try {
645            MessageDigest digest = MessageDigest.getInstance("SHA-256"); digest.update(domain.getBytes(StandardCharsets.US_ASCII)); digest.update((byte) 0);
646            for (byte[] value : payload) digest.update(value); StringBuilder hex = new StringBuilder();
647            for (byte value : digest.digest()) hex.append(String.format("%02x", value & 255)); return hex.toString();
648        } catch (NoSuchAlgorithmException impossible) { throw new IllegalStateException(impossible); }
649    }
650
651    private void validateGroupedOrdinals(List<Object> records, String groupField, String ordinalField,
652            Map<String, SlpcObject> validGroups, String missingMessage, String suffix) {
653        Map<String, List<Long>> groups = new HashMap<String, List<Long>>();
654        for (Object value : records) { SlpcObject record = asObject(value); String group = string(record, groupField);
655            requireRef(validGroups, group, missingMessage); if (!groups.containsKey(group)) groups.put(group, new ArrayList<Long>()); groups.get(group).add(integer(record, ordinalField)); }
656        for (Map.Entry<String, List<Long>> entry : groups.entrySet()) { Collections.sort(entry.getValue()); contiguous(entry.getValue(), entry.getKey() + suffix); }
657    }
658
659    private void validateAnchor(SlpcObject anchor, Map<String, SlpcObject> nodes, Map<String, SlpcObject> endpoints, String kind) {
660        if ("NODE".equals(string(anchor, "kind")) && !nodes.containsKey(string(anchor, "nodeRef"))) fail("$.intermediateGraphs", kind + " references missing graph node: " + anchor.string("nodeRef"));
661        if ("ENDPOINT".equals(string(anchor, "kind")) && !endpoints.containsKey(string(anchor, "endpointRef"))) fail("$.intermediateGraphs", kind + " references missing endpoint: " + anchor.string("endpointRef"));
662    }
663
664    private static void addOwnerKeys(Map<String, String> target, List<Object> records, String kind, String idField, String semanticField) {
665        for (Object value : records) { SlpcObject record = asObject(value); target.put(kind + "\u0000" + string(record, idField), string(record, semanticField)); }
666    }
667
668    private static List<SlpcObject> extensions(SlpcObject owner, String type) {
669        ArrayList<SlpcObject> result = new ArrayList<SlpcObject>();
670        for (Object value : optionalArray(owner, "extensions")) { SlpcObject extension = asObject(value); if (type.equals(extension.string("extensionType"))) result.add(extension); }
671        return result;
672    }
673
674    private static Comparator<SlpcObject> endpointParticipantComparator(final Map<String, SlpcObject> endpoints) {
675        return new Comparator<SlpcObject>() { @Override public int compare(SlpcObject left, SlpcObject right) {
676            String a = SlpcIdentity.candidateEndpointKey(endpoints.get(string(left, "endpointRef")));
677            String b = SlpcIdentity.candidateEndpointKey(endpoints.get(string(right, "endpointRef")));
678            return utf8Compare(a, b);
679        }};
680    }
681
682    private static int utf8Compare(String left, String right) {
683        byte[] a = left.getBytes(StandardCharsets.UTF_8), b = right.getBytes(StandardCharsets.UTF_8); int common = Math.min(a.length, b.length);
684        for (int index = 0; index < common; index++) { int difference = (a[index] & 255) - (b[index] & 255); if (difference != 0) return difference; }
685        return a.length - b.length;
686    }
687
688    private static Map<String, SlpcObject> uniqueIndex(List<Object> records, String field, String label) {
689        LinkedHashMap<String, SlpcObject> result = new LinkedHashMap<String, SlpcObject>();
690        for (Object value : records) { SlpcObject record = asObject(value); String key = string(record, field); if (result.put(key, record) != null) fail("$", "duplicate " + label); }
691        return result;
692    }
693
694    private static List<String> relationshipRoles(String kind) {
695        if ("PRIMARY_KEY".equals(kind) || "UNIQUE_KEY".equals(kind)) return Arrays.asList("KEY_RELATION");
696        if ("FOREIGN_KEY_REFERENCE".equals(kind)) return Arrays.asList("FOREIGN_KEY_RELATION", "REFERENCED_RELATION");
697        if ("CALLS".equals(kind)) return Arrays.asList("CALLER_ROUTINE", "CALLEE_ROUTINE");
698        if ("STREAMS_FROM".equals(kind)) return Arrays.asList("STREAM", "BASE_RELATION");
699        if ("SYNONYM_OF".equals(kind)) return Arrays.asList("SYNONYM", "BASE_OBJECT");
700        if ("READS_FROM".equals(kind)) return Arrays.asList("CONSUMER_RELATION", "STORAGE_ASSET");
701        if ("WRITES_TO".equals(kind)) return Arrays.asList("PRODUCER_RELATION", "STORAGE_ASSET");
702        fail("$", "unsupported relationship kind: " + kind); return Collections.emptyList();
703    }
704
705    private static List<RoleRule> operationRoles(String kind) {
706        if ("QUERY".equals(kind)) return rules("READ_RELATION", true, false, "OUTPUT_RELATION", true, false);
707        if ("INSERT".equals(kind) || "UPDATE".equals(kind) || "DELETE".equals(kind)) return rules("WRITE_TARGET", false, true, "READ_SOURCE", true, false);
708        if ("MERGE".equals(kind)) return rules("MERGE_TARGET", false, true, "MERGE_SOURCE", true, false);
709        if ("CREATE_TABLE".equals(kind)) return rules("CREATED_OBJECT", false, true);
710        if ("CREATE_TABLE_AS".equals(kind) || "CREATE_VIEW".equals(kind) || "CREATE_MATERIALIZED_VIEW".equals(kind)) return rules("CREATED_OBJECT", false, true, "READ_SOURCE", true, false);
711        if ("CREATE_EXTERNAL_TABLE".equals(kind) || "CREATE_STAGE".equals(kind)) return rules("CREATED_OBJECT", false, true, "STORAGE_SOURCE", true, false);
712        if ("CREATE_STREAM".equals(kind)) return rules("CREATED_STREAM", false, true, "BASE_RELATION", false, true);
713        if ("CREATE_ROUTINE".equals(kind)) return rules("CREATED_ROUTINE", false, true);
714        if ("CREATE_SYNONYM".equals(kind)) return rules("SYNONYM", false, true, "BASE_OBJECT", false, true);
715        if ("CREATE_INDEX".equals(kind)) return rules("CREATED_INDEX", false, true, "INDEXED_RELATION", false, true);
716        if ("RENAME".equals(kind)) return rules("BEFORE_OBJECT", false, true, "AFTER_OBJECT", false, true);
717        if ("SWAP".equals(kind)) return rules("SWAP_LEFT_OBJECT", false, true, "SWAP_RIGHT_OBJECT", false, true);
718        if ("CLONE".equals(kind)) return rules("CLONE_SOURCE", false, true, "CLONE_TARGET", false, true);
719        if ("LOAD".equals(kind)) return rules("STORAGE_SOURCE", true, false, "WRITE_TARGET", false, true);
720        if ("UNLOAD".equals(kind) || "INSERT_OVERWRITE_DIRECTORY".equals(kind)) return rules("READ_SOURCE", true, false, "STORAGE_TARGET", false, true);
721        if ("CALL_ROUTINE".equals(kind)) return rules("CALLEE_ROUTINE", false, true, "ARGUMENT_SOURCE", true, false);
722        if ("ALTER".equals(kind) || "DROP".equals(kind) || "TRUNCATE".equals(kind)) return rules("AFFECTED_OBJECT", false, true);
723        if ("OTHER".equals(kind)) return rules("AFFECTED_OBJECT", true, false);
724        fail("$", "unsupported operation kind: " + kind); return Collections.emptyList();
725    }
726
727    private static List<RoleRule> rules(Object... values) {
728        ArrayList<RoleRule> result = new ArrayList<RoleRule>();
729        for (int index = 0; index < values.length; index += 3) result.add(new RoleRule((String) values[index], (Boolean) values[index + 1], (Boolean) values[index + 2]));
730        return result;
731    }
732
733    @SafeVarargs private static Set<String> union(Set<String>... values) {
734        HashSet<String> result = new HashSet<String>(); for (Set<String> value : values) result.addAll(value); return result;
735    }
736
737    private static List<String> sorted(Iterable<String> values) { ArrayList<String> result = new ArrayList<String>(); for (String value : values) result.add(value); Collections.sort(result); return result; }
738    private static List<Long> sortedIntegers(List<Object> values, String field) { ArrayList<Long> result = new ArrayList<Long>(integers(values, field)); Collections.sort(result); return result; }
739    private static List<String> fields(List<Object> values, String field) { ArrayList<String> result = new ArrayList<String>(); for (Object value : values) result.add(string(asObject(value), field)); return result; }
740    private static List<String> strings(List<Object> values) { ArrayList<String> result = new ArrayList<String>(); for (Object value : values) result.add((String) value); return result; }
741    private static List<Long> integers(List<Object> values, String field) { ArrayList<Long> result = new ArrayList<Long>(); for (Object value : values) result.add(integer(asObject(value), field)); return result; }
742    private static List<Object> optionalArray(SlpcObject object, String field) { List<Object> result = object.array(field); return result == null ? Collections.<Object>emptyList() : result; }
743    private static List<Object> array(SlpcObject object, String field) { List<Object> result = object.array(field); if (result == null) fail("$", "missing array field: " + field); return result; }
744    private static SlpcObject object(SlpcObject object, String field) { SlpcObject result = object.object(field); if (result == null) fail("$", "missing object field: " + field); return result; }
745    private static String string(SlpcObject object, String field) { String result = object == null ? null : object.string(field); if (result == null) fail("$", "missing string field: " + field); return result; }
746    private static String nullableString(SlpcObject object, String field) { if (!object.contains(field)) fail("$", "missing nullable field: " + field); Object result = object.get(field); if (result != null && !(result instanceof String)) fail("$", "field is not string/null: " + field); return (String) result; }
747    private static long integer(SlpcObject object, String field) { BigInteger result = object.integer(field); if (result == null) fail("$", "missing integer field: " + field); return result.longValueExact(); }
748    private static SlpcObject asObject(Object value) { if (!(value instanceof SlpcObject)) fail("$", "expected object"); return (SlpcObject) value; }
749    private static void requireRef(Map<String, SlpcObject> values, String ref, String message) { if (!values.containsKey(ref)) fail("$", message + ": " + ref); }
750    private static void refsExist(List<String> refs, Set<String> values, String message) { for (String ref : refs) if (!values.contains(ref)) fail("$", message + ": " + ref); }
751    private static void unique(List<String> values, String label) { if (new HashSet<String>(values).size() != values.size()) fail("$", "duplicate " + label); }
752    private static void contiguous(List<Long> values, String label) { for (int index = 0; index < values.size(); index++) if (values.get(index) != index + 1L) fail("$", label + " ordinals: expected contiguous values starting at 1, got " + values); }
753    private static void localIds(List<String> values, String prefix, String label) { for (int index = 0; index < values.size(); index++) { String expected = String.format("%s%06d", prefix, index + 1); if (!expected.equals(values.get(index))) fail("$", label + " local IDs: expected " + expected + ", got " + values.get(index)); } }
754    private static void equal(Object actual, Object expected, String message) { if (actual == null ? expected != null : !actual.equals(expected)) fail("$", message + ": expected " + expected + ", got " + actual); }
755    private static void fail(String path, String message) { throw new Failure(path, message); }
756
757    private static final class RoleRule { private final String role; private final boolean setLike; private final boolean required; private RoleRule(String role, boolean setLike, boolean required) { this.role = role; this.setLike = setLike; this.required = required; } }
758    private static final class PathValue { private final String path; private final Object value; private PathValue(String path, Object value) { this.path = path; this.value = value; } }
759    private static final class Failure extends IllegalArgumentException { private static final long serialVersionUID = 1L; private final String path; private Failure(String path, String message) { super(message); this.path = path; } }
760}