001package gudusoft.gsqlparser.lineage.v2.identity;
002
003import gudusoft.gsqlparser.lineage.v2.Finding;
004import gudusoft.gsqlparser.lineage.v2.SqllrException;
005import gudusoft.gsqlparser.lineage.v2.SqllrResources;
006import gudusoft.gsqlparser.lineage.v2.json.CanonicalJson;
007
008import java.util.ArrayList;
009import java.util.Arrays;
010import java.util.Collections;
011import java.util.HashMap;
012import java.util.HashSet;
013import java.util.LinkedHashMap;
014import java.util.List;
015import java.util.Map;
016import java.util.Set;
017import java.util.TreeSet;
018
019/**
020 * Recomputes every id of a lineage document from the identity inputs it carries ({@code identity-v1.md},
021 * Section 6), as the reference's {@code check-ids} and {@code fill-ids} do ({@code sqllr_ids.py},
022 * {@code DocumentIds} and {@code check_document}).
023 *
024 * <p>Canonical segment keys are read from the document ({@code canonicalKey}); checking them against the
025 * identifier-rule data is the IDENTITY stage's job. An endpoint declared {@code LOCAL} whose class is portable
026 * is accepted and listed as unchecked, because the qualification policy needs the profile data.</p>
027 *
028 * <p>A finding is reported where the reference reports it; every input the class cannot check is listed as a
029 * note. Owner chains and derived-unit chains are walked with explicit lists, so their length is not limited by
030 * the thread stack; a chain that returns to itself is a note (owners) or {@code REF_OWNER_CYCLE} (units).</p>
031 *
032 * @since 4.2.17
033 */
034public final class DocumentIds {
035
036    /** Findings and notes of one run. */
037    public static final class Report {
038        private final List<Finding> findings;
039        private final List<String> notes;
040        private final Object document;
041
042        Report(List<Finding> findings, List<String> notes, Object document) {
043            this.findings = Collections.unmodifiableList(findings);
044            this.notes = Collections.unmodifiableList(notes);
045            this.document = document;
046        }
047
048        /** Mismatches ({@code ID_MISMATCH}, {@code ID_MISSING}, ...) and rejected inputs. */
049        public List<Finding> getFindings() {
050            return findings;
051        }
052
053        /** The inputs that could not be checked. */
054        public List<String> getNotes() {
055            return notes;
056        }
057
058        /** For {@link #fill(Object)}: the document with every computable id filled in; null otherwise. */
059        public Object getDocument() {
060            return document;
061        }
062    }
063
064    /**
065     * {@code check-ids}: recomputes every id of a document, and the id of the dependency snapshot each analysis
066     * context names (looked up among the snapshots this library ships).
067     */
068    public static Report check(Object document) {
069        return check(document, null);
070    }
071
072    /** As {@link #check(Object)}, with the given dependency snapshot (a parsed snapshot file) for every context. */
073    public static Report check(Object document, Object snapshot) {
074        DocumentIds ids = new DocumentIds(document, false);
075        Set<Object> seen = new HashSet<Object>();
076        for (Object item : list(map(document).get("analysisContexts"))) {
077            Object snapshotId = item instanceof Map<?, ?> ? ((Map<?, ?>) item).get("dependencySnapshotId") : null;
078            if (!seen.add(snapshotId == null ? NULL_KEY : snapshotId)) {
079                continue;
080            }
081            String where = "analysisContexts ref " + map(item).get("ref") + ": dependencySnapshotId";
082            String path = "snapshot";
083            Map<?, ?> found = null;
084            if (snapshot != null) {
085                found = map(snapshot);
086            } else {
087                for (String candidate : shippedSnapshots()) {
088                    Map<?, ?> parsed = map(SqllrResources.json(candidate));
089                    if (snapshotId != null && snapshotId.equals(parsed.get("snapshotId"))) {
090                        found = parsed;
091                        path = candidate;
092                        break;
093                    }
094                }
095            }
096            if (found == null) {
097                ids.notes.add("dependency snapshot " + snapshotId + " not found among the shipped snapshots; its id is used"
098                        + " as declared");
099                continue;
100            }
101            String recomputed = snapshotIdOf(found);
102            if (!recomputed.equals(found.get("snapshotId"))) {
103                ids.findings.add(new Finding("ID_MISMATCH", path, "snapshotId " + found.get("snapshotId") + ", computed "
104                        + recomputed));
105            }
106            if (!recomputed.equals(snapshotId)) {
107                ids.findings.add(new Finding("ID_MISMATCH", where, "declared " + snapshotId + ", snapshot " + path
108                        + " computes " + recomputed));
109            }
110        }
111        ids.run();
112        return new Report(ids.findings, ids.notes, null);
113    }
114
115    /**
116     * {@code fill-ids}: a copy of the document with every computable id, numeric key, key and record id set,
117     * and every id that is not computed removed.
118     */
119    public static Report fill(Object document) {
120        Object copy = mutableCopy(document);
121        DocumentIds ids = new DocumentIds(copy, true);
122        ids.run();
123        return new Report(ids.findings, ids.notes, copy);
124    }
125
126    /** The id of a dependency snapshot: {@code DependencySnapshotIdV1} of the file without {@code snapshotId}. */
127    public static String snapshotIdOf(Object snapshot) {
128        Map<String, Object> manifest = new LinkedHashMap<String, Object>();
129        for (Map.Entry<?, ?> entry : map(snapshot).entrySet()) {
130            if (!"snapshotId".equals(entry.getKey())) {
131                manifest.put((String) entry.getKey(), entry.getValue());
132            }
133        }
134        Map<String, Object> inputs = new LinkedHashMap<String, Object>();
135        inputs.put("manifest", manifest);
136        return IdentityV1.compute(IdentityV1.DEPENDENCY_SNAPSHOT, inputs).getId();
137    }
138
139    /** The shipped snapshot files, sorted by path. */
140    public static List<String> shippedSnapshots() {
141        List<String> out = new ArrayList<String>();
142        for (String path : new TreeSet<String>(SqllrResources.index().keySet())) {
143            if (path.startsWith("snapshots/") && path.endsWith(".json")) {
144                out.add(path);
145            }
146        }
147        return out;
148    }
149
150    // ---------------------------------------------------------------------------------------------
151
152    private static final Object NULL_KEY = new Object();
153    private static final List<String> DIGEST_PRECEDENCE = IdentityV1.DIGEST_DOMAINS;
154
155    private final Map<?, ?> doc;
156    private final boolean fill;
157    private final List<Finding> findings = new ArrayList<Finding>();
158    private final List<String> notes = new ArrayList<String>();
159    private final Map<Object, Object[]> byRef = new HashMap<Object, Object[]>();
160    private final Map<?, ?> header;
161    private final Map<Object, String> units = new HashMap<Object, String>();
162    private final Map<Object, String> contexts = new HashMap<Object, String>();
163    private final Map<Object, String[]> endpoints = new HashMap<Object, String[]>();
164    private final Map<Object, String[]> facts = new HashMap<Object, String[]>();
165
166    private DocumentIds(Object document, boolean fill) {
167        this.doc = map(document);
168        this.fill = fill;
169        this.header = map(doc.get("header"));
170        Map<?, ?> explain = map(doc.get("explainGraph"));
171        Object[][] collections = {
172                {"analysisContexts", doc.get("analysisContexts")}, {"sourceUnits", doc.get("sourceUnits")},
173                {"statements", doc.get("statements")}, {"scopes", doc.get("scopes")}, {"endpoints", doc.get("endpoints")},
174                {"facts", doc.get("facts")}, {"factCandidates", doc.get("factCandidates")},
175                {"candidateSets", doc.get("candidateSets")},
176                {"occurrences", doc.get("occurrences")}, {"targetFieldOccurrences", doc.get("targetFieldOccurrences")},
177                {"diagnostics", doc.get("diagnostics")}, {"explainGraph.nodes", explain.get("nodes")},
178                {"explainGraph.microEdges", explain.get("microEdges")},
179                {"explainGraph.candidateSets", explain.get("candidateSets")},
180                {"explainGraph.supportPaths", explain.get("supportPaths")}};
181        for (Object[] collection : collections) {
182            for (Object item : list(collection[1])) {
183                index((String) collection[0], item);
184            }
185            if ("factCandidates".equals(collection[0])) {
186                for (Object item : list(collection[1])) {
187                    for (String side : new String[] {"source", "target"}) {
188                        Object end = map(item).get(side);
189                        if (end instanceof Map<?, ?> && ((Map<?, ?>) end).containsKey("candidateSet")) {
190                            index("candidateSets", ((Map<?, ?>) end).get("candidateSet"));
191                        }
192                    }
193                }
194            }
195        }
196    }
197
198    private static Map<?, ?> map(Object value) {
199        return value instanceof Map<?, ?> ? (Map<?, ?>) value : Collections.emptyMap();
200    }
201
202    private static List<?> list(Object value) {
203        return value instanceof List<?> ? (List<?>) value : Collections.emptyList();
204    }
205
206    /**
207     * The map key of a reference value: integers by numeric value whatever their Java type (a producer may
208     * build maps with Integer or BigInteger), booleans and everything else as they are.
209     */
210    private static Object refKey(Object value) {
211        if (value == null) {
212            return NULL_KEY;
213        }
214        if (value instanceof Integer || value instanceof Short || value instanceof Byte) {
215            return Long.valueOf(((Number) value).longValue());
216        }
217        if (value instanceof java.math.BigInteger && ((java.math.BigInteger) value).bitLength() < 64) {
218            return Long.valueOf(((java.math.BigInteger) value).longValue());
219        }
220        return value;
221    }
222
223    private void index(String collection, Object item) {
224        Object ref = item instanceof Map<?, ?> ? ((Map<?, ?>) item).get("ref") : null;
225        if (byRef.containsKey(refKey(ref))) {
226            findings.add(new Finding("REF_DUPLICATE", collection + " ref " + ref, "ref used twice"));
227        }
228        byRef.put(refKey(ref), new Object[] {collection, item});
229    }
230
231    private Map<?, ?> get(Object ref, String collection) {
232        Object[] found = byRef.get(refKey(ref));
233        if (found == null || !collection.equals(found[0])) {
234            throw new SqllrException("REF_DANGLING", "ref " + ref + " is not a " + collection + " object");
235        }
236        return map(found[1]);
237    }
238
239    private void compare(String location, Object declared, String computed, String what) {
240        if (computed == null && declared != null) {
241            findings.add(new Finding("ID_UNEXPECTED", location, "declares " + what + " " + declared + ", but none is computed"));
242        } else if (computed != null && declared == null) {
243            findings.add(new Finding("ID_MISSING", location, what + " " + computed + " is computed but not declared"));
244        } else if (computed != null && !computed.equals(declared)) {
245            findings.add(new Finding("ID_MISMATCH", location, "declared " + declared + ", computed " + computed));
246        }
247    }
248
249    private void compareNumeric(String location, Object declared, String id) {
250        if (declared != null && (id == null || !declared.equals(IdentityV1.numericKey(id)))) {
251            findings.add(new Finding("ID_NUMERIC_KEY_MISMATCH", location, "numericKey " + declared + " is not derived from "
252                    + id));
253        }
254    }
255
256    private static Map<String, Object> inputs(Object... pairs) {
257        Map<String, Object> out = new LinkedHashMap<String, Object>();
258        for (int i = 0; i < pairs.length; i += 2) {
259            out.put((String) pairs[i], pairs[i + 1]);
260        }
261        return out;
262    }
263
264    private static Map<String, Object> status(String status) {
265        return inputs("status", status);
266    }
267
268    // -- source units and analysis contexts
269
270    private Map<?, ?> contextOf(Map<?, ?> unit) {
271        return get(unit.get("analysisContextRef"), "analysisContexts");
272    }
273
274    private Map<String, Object> scopeOf(Map<?, ?> unit) {
275        Map<?, ?> context = contextOf(unit);
276        Object profile = context.get("pathProfile");
277        Object producer = map(context.get("producer")).get("name");
278        return inputs("profile", profile, "producer", IdentityV1.PATH_PROFILE_V1.equals(profile) ? null : producer);
279    }
280
281    /**
282     * The {@code SourceUnitIdV1} of a unit. A {@code DERIVED} unit needs its parent's id first; the parent
283     * chain is walked with an explicit list, and a chain that returns to a unit is {@code REF_OWNER_CYCLE}.
284     */
285    private String unitId(Map<?, ?> unit) {
286        List<Map<?, ?>> chain = new ArrayList<Map<?, ?>>();
287        Set<Map<?, ?>> onChain = Collections.newSetFromMap(new java.util.IdentityHashMap<Map<?, ?>, Boolean>());
288        Map<?, ?> current = unit;
289        while (!units.containsKey(refKey(current.get("ref")))) {
290            if (!onChain.add(current)) {
291                throw new SqllrException("REF_OWNER_CYCLE", "the parent chain of source unit " + unit.get("ref")
292                        + " returns to unit " + current.get("ref"));
293            }
294            chain.add(current);
295            if (!"DERIVED".equals(current.get("identityKind"))) {
296                break;
297            }
298            current = get(current.get("parentUnitRef"), "sourceUnits");
299        }
300        for (int i = chain.size() - 1; i >= 0; i--) {
301            Map<?, ?> item = chain.get(i);
302            Object kind = item.get("identityKind");
303            Map<String, Object> in;
304            if ("NAMED".equals(kind)) {
305                in = inputs("kind", "NAMED", "namespace", item.get("namespace"), "logicalName", item.get("logicalName"));
306            } else if ("CONTENT_ADDRESSED".equals(kind)) {
307                in = inputs("kind", "CONTENT_ADDRESSED", "contextDomain", item.get("contextDomain"), "contentDigest",
308                        item.get("contentDigest"));
309            } else if ("DERIVED".equals(kind)) {
310                Map<?, ?> parentUnit = get(item.get("parentUnitRef"), "sourceUnits");
311                String parent = units.get(refKey(parentUnit.get("ref")));
312                Map<?, ?> site = get(item.get("siteStatementRef"), "statements");
313                in = inputs("kind", "DERIVED", "parentUnit", parent != null ? parent : status(IdentityV1.LOCAL),
314                        "statementOrdinal", site.get("ordinal"), "pathScope", scopeOf(parentUnit),
315                        "sitePath", item.get("sitePath"), "siteOrdinal", item.get("siteOrdinal"),
316                        "variantOrdinal", item.get("variantOrdinal"));
317            } else {
318                in = inputs("kind", "LOCAL");
319            }
320            units.put(refKey(item.get("ref")), IdentityV1.compute(IdentityV1.SOURCE_UNIT, in).getId());
321        }
322        return units.get(refKey(unit.get("ref")));
323    }
324
325    private String contextId(Map<?, ?> unit) {
326        Object key = refKey(unit.get("ref"));
327        if (!contexts.containsKey(key)) {
328            Map<Object, Object> digests = new HashMap<Object, Object>();
329            for (Object digest : list(unit.get("digests"))) {
330                digests.put(map(digest).get("domain"), digest);
331            }
332            Object chosen = null;
333            for (String domain : DIGEST_PRECEDENCE) {
334                if (digests.containsKey(domain)) {
335                    chosen = digests.get(domain);
336                    break;
337                }
338            }
339            Map<?, ?> context = contextOf(unit);
340            if (unitId(unit) == null || chosen == null) {
341                contexts.put(key, null);
342            } else {
343                Map<?, ?> metadata = map(context.get("metadata"));
344                Map<?, ?> dialect = map(context.get("dialect"));
345                Map<?, ?> redaction = map(context.get("redaction"));
346                Map<?, ?> producer = map(context.get("producer"));
347                Object resolution = unit.containsKey("resolutionInputDigests") ? unit.get("resolutionInputDigests")
348                        : new ArrayList<Object>();
349                contexts.put(key, IdentityV1.compute(IdentityV1.ANALYSIS_CONTEXT, inputs(
350                        "producerName", producer.get("name"), "producerVersion", producer.get("version"),
351                        "dependencySnapshot", context.get("dependencySnapshotId"),
352                        "dialectId", dialect.get("id"), "dialectVersion", dialect.get("version"),
353                        "identifierPolicyId", map(context.get("identifierPolicy")).get("policyId"),
354                        "pathProfile", context.get("pathProfile"),
355                        "effectiveOptionsDigest", context.get("effectiveOptionsDigest"),
356                        "metadataStatus", metadata.get("status"), "metadataDigest", metadata.get("digest"),
357                        "redactionPolicyId", redaction.get("policyId"),
358                        "redactionPolicyVersion", redaction.get("policyVersion"),
359                        "capabilities", context.get("capabilities"), "decoding", unit.get("decoding"),
360                        "sourceContentDigest", chosen, "resolutionInputDigests", resolution)).getId());
361            }
362        }
363        return contexts.get(key);
364    }
365
366    private void checkContext(Map<?, ?> context, String where) {
367        Map<?, ?> metadata = map(context.get("metadata"));
368        if ("NONE".equals(metadata.get("status")) || !metadata.containsKey("digest")) {
369            return;
370        }
371        if (!metadata.containsKey("artifacts")) {
372            notes.add(where + ": metadata artifacts not listed; the metadata digest is not checked");
373            return;
374        }
375        String computed = IdentityV1.compute(IdentityV1.METADATA_DIGEST, inputs("artifacts", metadata.get("artifacts"))).getId();
376        if (!computed.equals(metadata.get("digest"))) {
377            findings.add(new Finding("ID_MISMATCH", where, "metadata digest " + metadata.get("digest") + ", artifacts compute "
378                    + computed));
379        }
380    }
381
382    // -- endpoints
383
384    private List<Object> segmentInputs(Map<?, ?> endpoint) {
385        List<Object> out = new ArrayList<Object>();
386        for (Object item : list(map(endpoint.get("name")).get("segments"))) {
387            Map<?, ?> seg = map(item);
388            Object role = seg.get("role");
389            if ("PRODUCER_DEFAULT".equals(seg.get("kind"))) {
390                out.add(inputs("role", role, "kind", "PRODUCER_DEFAULT"));
391            } else if ("AUTHORITY".equals(role) || "LOCATION".equals(role)) {
392                out.add(inputs("role", role, "kind", "NAMED", "canonicalKey", seg.get("text")));
393            } else if (seg.containsKey("keyUnavailable")) {
394                out.add(inputs("role", role, "kind", "NAMED", "keyUnavailable", seg.get("keyUnavailable")));
395            } else if (seg.containsKey("canonicalKey")) {
396                out.add(inputs("role", role, "kind", "NAMED", "canonicalKey", seg.get("canonicalKey")));
397            } else {
398                return null;
399            }
400        }
401        return out;
402    }
403
404    /** (top-level owner, full field path) of a FIELD endpoint; the path is null when a key is missing. */
405    private Object[] fieldPath(Map<?, ?> endpoint) {
406        List<Map<?, ?>> chain = new ArrayList<Map<?, ?>>();
407        Set<Map<?, ?>> visited = Collections.newSetFromMap(new java.util.IdentityHashMap<Map<?, ?>, Boolean>());
408        Map<?, ?> current = endpoint;
409        Map<?, ?> owner = get(current.get("ownerRef"), "endpoints");
410        while ("FIELD".equals(owner.get("kind"))) {
411            chain.add(current);
412            if (!visited.add(current)) {
413                return new Object[] {owner, null};
414            }
415            current = owner;
416            owner = get(current.get("ownerRef"), "endpoints");
417        }
418        // current is the top-level field; owner its relation (or ROW_SET, ...).
419        List<?> segments = list(map(current.get("name")).get("segments"));
420        if (segments.size() != 1) {
421            return new Object[] {owner, null};
422        }
423        Map<?, ?> seg = map(segments.get(0));
424        List<Object> path = new ArrayList<Object>();
425        if (seg.containsKey("keyUnavailable")) {
426            path.add(inputs("kind", "FIELD", "keyUnavailable", seg.get("keyUnavailable")));
427        } else if (seg.containsKey("canonicalKey")) {
428            path.add(inputs("kind", "FIELD", "canonicalKey", seg.get("canonicalKey")));
429        } else {
430            return new Object[] {owner, null};
431        }
432        for (int i = chain.size() - 1; i >= 0; i--) {
433            for (Object item : list(chain.get(i).get("pathSegments"))) {
434                Map<?, ?> segment = map(item);
435                Map<String, Object> copy = new LinkedHashMap<String, Object>();
436                for (String name : new String[] {"kind", "canonicalKey", "index", "keyUnavailable"}) {
437                    if (segment.containsKey(name)) {
438                        copy.put(name, segment.get(name));
439                    }
440                }
441                path.add(copy);
442            }
443        }
444        return new Object[] {owner, path};
445    }
446
447    /**
448     * (status, id) recomputed for an endpoint; null when the document lacks an input. A FIELD or ROW_SET needs
449     * its owner's result first: the owner chain is followed with an explicit list and computed from the far
450     * end back, in the order the reference's recursion computes it.
451     */
452    private String[] endpointResult(Map<?, ?> endpoint) {
453        List<Object[]> pending = new ArrayList<Object[]>(); // {endpoint, path} awaiting their owner's result
454        Map<?, ?> current = endpoint;
455        String[] result;
456        while (true) {
457            Object key = refKey(current.get("ref"));
458            if (endpoints.containsKey(key)) {
459                result = endpoints.get(key);
460                break;
461            }
462            endpoints.put(key, null); // guards against owner cycles
463            Object kind = current.get("kind");
464            Object declared = map(current.get("identity")).get("status");
465            String identityClass = kind instanceof String ? IdentityV1.IDENTITY_CLASS.get(kind) : null;
466            if (IdentityV1.OPAQUE_PRODUCER_LOCAL.equals(declared)) {
467                result = new String[] {IdentityV1.OPAQUE_PRODUCER_LOCAL, null};
468            } else if (identityClass == null) {
469                result = new String[] {IdentityV1.LOCAL, null};
470            } else if (IdentityV1.LOCAL.equals(declared) && "ROUTINE".equals(identityClass)
471                    && !current.containsKey("signature")) {
472                // without a signature the overload is not proven: a routine is LOCAL whatever its qualification
473                // (spec P1-3; EndpointIdV1 blocks it), so nothing is left unchecked
474                result = new String[] {IdentityV1.LOCAL, null};
475            } else if (IdentityV1.LOCAL.equals(declared)
476                    && ("RELATION".equals(identityClass) || "ROUTINE".equals(identityClass))) {
477                notes.add("endpoint ref " + current.get("ref") + ": declared LOCAL; the qualification policy is not checked");
478                result = new String[] {IdentityV1.LOCAL, null};
479            } else if ("RELATION".equals(identityClass) || "ROUTINE".equals(identityClass)) {
480                result = null;
481                List<Object> segments = segmentInputs(current);
482                if (segments == null) {
483                    notes.add("endpoint ref " + current.get("ref") + ": a NAMED segment has no canonicalKey; not recomputed");
484                } else {
485                    Map<String, Object> in = inputs("endpointKind", kind, "segments", segments);
486                    if ("ROUTINE".equals(kind)) {
487                        in.put("routineNamespace", current.get("routineNamespace"));
488                        in.put("routineKind", current.get("routineKind"));
489                        if (current.containsKey("signature")) {
490                            in.put("signature", current.get("signature"));
491                        }
492                    }
493                    IdentityV1.IdResult computed = IdentityV1.compute(IdentityV1.ENDPOINT, in);
494                    result = new String[] {computed.getStatus(), computed.getId()};
495                }
496            } else {
497                Map<?, ?> owner;
498                Object path;
499                if ("FIELD".equals(kind)) {
500                    Object[] found = fieldPath(current);
501                    owner = (Map<?, ?>) found[0];
502                    path = found[1];
503                } else {
504                    owner = get(current.get("ownerRef"), "endpoints");
505                    path = new ArrayList<Object>();
506                }
507                pending.add(new Object[] {current, path});
508                current = owner;
509                continue;
510            }
511            endpoints.put(key, result);
512            break;
513        }
514        for (int i = pending.size() - 1; i >= 0; i--) {
515            Map<?, ?> item = (Map<?, ?>) pending.get(i)[0];
516            Object path = pending.get(i)[1];
517            String[] ownerResult = result;
518            if (ownerResult != null && path == null && ownerResult[1] == null
519                    && (IdentityV1.LOCAL.equals(ownerResult[0])
520                    || IdentityV1.OPAQUE_PRODUCER_LOCAL.equals(ownerResult[0]))) {
521                // a FIELD of a LOCAL owner (a routine's unnamed return field) is LOCAL whatever its path: its status
522                // is the worst of the owner's and the path's, and no path status ranks above LOCAL
523                result = new String[] {ownerResult[0], null};
524            } else if (ownerResult == null || path == null) {
525                notes.add("endpoint ref " + item.get("ref") + ": owner or field key not available; not recomputed");
526                result = null;
527            } else {
528                Object ownerInput = ownerResult[1] != null ? ownerResult[1] : status(ownerResult[0]);
529                Map<String, Object> in = inputs("endpointKind", item.get("kind"), "owner", ownerInput);
530                if ("FIELD".equals(item.get("kind"))) {
531                    in.put("path", path);
532                }
533                IdentityV1.IdResult computed = IdentityV1.compute(IdentityV1.ENDPOINT, in);
534                result = new String[] {computed.getStatus(), computed.getId()};
535            }
536            endpoints.put(refKey(item.get("ref")), result);
537        }
538        return result;
539    }
540
541    private Object endpointInput(Object ref) {
542        String[] result = endpointResult(get(ref, "endpoints"));
543        if (result == null) {
544            return null;
545        }
546        return result[1] != null ? result[1] : status(result[0]);
547    }
548
549    /** ("OK", id or null) or ("UNCHECKED", null). */
550    private String[] factId(Map<?, ?> fact) {
551        Object key = refKey(fact.get("ref"));
552        if (!facts.containsKey(key)) {
553            Object source = endpointInput(fact.get("sourceRef"));
554            Object target = endpointInput(fact.get("targetRef"));
555            if (source == null || target == null) {
556                facts.put(key, new String[] {"UNCHECKED", null});
557            } else {
558                Map<String, Object> in = inputs("kind", fact.get("kind"), "source", source, "target", target);
559                if (fact.containsKey("role")) {
560                    in.put("role", fact.get("role"));
561                }
562                facts.put(key, new String[] {"OK", IdentityV1.compute(IdentityV1.FACT, in).getId()});
563            }
564        }
565        return facts.get(key);
566    }
567
568    private Map<String, Object> sideInput(Map<?, ?> end) {
569        if (end.containsKey("endpointRef")) {
570            Object value = endpointInput(end.get("endpointRef"));
571            return value == null ? null : inputs("endpoint", value);
572        }
573        Map<?, ?> set = map(end.get("candidateSet"));
574        List<Object> members = new ArrayList<Object>();
575        for (Object member : list(set.get("candidateRefs"))) {
576            members.add(endpointInput(member));
577        }
578        if (members.contains(null)) {
579            return null;
580        }
581        return inputs("candidates", members, "membership", set.get("membership"));
582    }
583
584    private Map<String, Object> subjectInput(Map<?, ?> occurrence) {
585        if (occurrence.containsKey("factRef")) {
586            String[] fact = factId(get(occurrence.get("factRef"), "facts"));
587            if ("UNCHECKED".equals(fact[0])) {
588                return null;
589            }
590            return inputs("kind", "FACT", "fact", fact[1] != null ? fact[1] : status(IdentityV1.LOCAL));
591        }
592        Map<?, ?> candidate = get(occurrence.get("factCandidateRef"), "factCandidates");
593        Map<String, Object> source = sideInput(map(candidate.get("source")));
594        Map<String, Object> target = sideInput(map(candidate.get("target")));
595        if (source == null || target == null) {
596            return null;
597        }
598        Map<String, Object> subject = inputs("kind", "CANDIDATE", "factKind", candidate.get("kind"), "source", source,
599                "target", target);
600        if (candidate.containsKey("role")) {
601            subject.put("role", candidate.get("role"));
602        }
603        return subject;
604    }
605
606    // -- the whole document
607
608    private interface Check {
609        void run();
610    }
611
612    private void guard(String where, Check check) {
613        try {
614            check.run();
615        } catch (SqllrException e) {
616            findings.add(new Finding(e.getCode(), where, e.getDetail()));
617        }
618    }
619
620    private void run() {
621        for (Object item : list(doc.get("analysisContexts"))) {
622            final Map<?, ?> context = map(item);
623            final String where = "analysisContexts ref " + context.get("ref");
624            guard(where, new Check() {
625                @Override
626                public void run() {
627                    checkContext(context, where);
628                }
629            });
630        }
631        for (Object item : list(doc.get("sourceUnits"))) {
632            final Map<?, ?> unit = map(item);
633            final String where = "sourceUnits ref " + unit.get("ref");
634            guard(where, new Check() {
635                @Override
636                public void run() {
637                    checkUnit(unit, where);
638                }
639            });
640        }
641        for (Object item : list(doc.get("endpoints"))) {
642            final Map<?, ?> endpoint = map(item);
643            final String where = "endpoints ref " + endpoint.get("ref");
644            guard(where, new Check() {
645                @Override
646                public void run() {
647                    checkEndpoint(endpoint, where);
648                }
649            });
650        }
651        for (Object item : list(doc.get("facts"))) {
652            final Map<?, ?> fact = map(item);
653            final String where = "facts ref " + fact.get("ref");
654            guard(where, new Check() {
655                @Override
656                public void run() {
657                    checkFact(fact, where);
658                }
659            });
660        }
661        for (Object item : list(doc.get("occurrences"))) {
662            final Map<?, ?> occurrence = map(item);
663            final String where = "occurrences ref " + occurrence.get("ref");
664            guard(where, new Check() {
665                @Override
666                public void run() {
667                    checkOccurrence(occurrence, where);
668                }
669            });
670        }
671        for (Object item : list(map(doc.get("explainGraph")).get("nodes"))) {
672            final Map<?, ?> node = map(item);
673            final String where = "explainGraph.nodes ref " + node.get("ref");
674            guard(where, new Check() {
675                @Override
676                public void run() {
677                    checkNode(node, where);
678                }
679            });
680        }
681    }
682
683    @SuppressWarnings("unchecked")
684    private void settle(Map<?, ?> obj, String member, String where, String computed, String what) {
685        if (fill) {
686            Map<String, Object> target = (Map<String, Object>) obj;
687            if (computed == null) {
688                target.remove(member);
689            } else {
690                target.put(member, computed);
691            }
692        } else {
693            compare(where, obj.get(member), computed, what);
694        }
695    }
696
697    private void checkUnit(Map<?, ?> unit, String where) {
698        settle(unit, "unitId", where, unitId(unit), "unitId");
699        settle(unit, "analysisContextId", where, contextId(unit), "analysisContextId");
700        Object mode = header.get("sourceEmbedding");
701        if (unit.containsKey("text") && ("FULL".equals(mode) || "REDACTED".equals(mode))) {
702            String domain = "FULL".equals(mode) ? "DECODED_TEXT_SHA256" : "REDACTED_TEXT_SHA256";
703            List<Object> declared = new ArrayList<Object>();
704            for (Object digest : list(unit.get("digests"))) {
705                if (domain.equals(map(digest).get("domain"))) {
706                    declared.add(map(digest).get("value"));
707                }
708            }
709            Object text = unit.get("text");
710            String actual = text instanceof String ? Framing.hex(IdentityV1.sha256(Framing.utf8((String) text))) : null;
711            if (actual == null || !declared.equals(Arrays.<Object>asList(actual))) {
712                findings.add(new Finding("ID_SOURCE_DIGEST_MISMATCH", where, "embedded text does not match " + domain));
713            }
714        }
715    }
716
717    @SuppressWarnings("unchecked")
718    private void checkEndpoint(Map<?, ?> endpoint, String where) {
719        String[] result = endpointResult(endpoint);
720        if (result == null) {
721            return;
722        }
723        String status = result[0];
724        String id = result[1];
725        Map<?, ?> identity;
726        if (fill) {
727            Map<String, Object> target = (Map<String, Object>) endpoint;
728            if (!(target.get("identity") instanceof Map<?, ?>)) {
729                target.put("identity", new LinkedHashMap<String, Object>());
730            }
731            identity = (Map<?, ?>) target.get("identity");
732            ((Map<String, Object>) identity).put("status", status);
733        } else {
734            identity = map(endpoint.get("identity"));
735            if (!status.equals(identity.get("status"))) {
736                findings.add(new Finding("ID_MISMATCH", where, "declared status " + identity.get("status") + ", computed "
737                        + status));
738            }
739        }
740        settle(identity, "id", where, id, "id");
741        if (fill) {
742            settle(identity, "numericKey", where, id == null ? null : IdentityV1.numericKey(id), "numericKey");
743        } else {
744            compareNumeric(where, identity.get("numericKey"), id);
745        }
746    }
747
748    private void checkFact(Map<?, ?> fact, String where) {
749        String[] result = factId(fact);
750        if ("UNCHECKED".equals(result[0])) {
751            notes.add(where + ": an endpoint was not recomputed; the Fact id is not checked");
752            return;
753        }
754        settle(fact, "id", where, result[1], "id");
755        if (fill) {
756            settle(fact, "numericKey", where, result[1] == null ? null : IdentityV1.numericKey(result[1]), "numericKey");
757        } else {
758            compareNumeric(where, fact.get("numericKey"), result[1]);
759        }
760    }
761
762    private void checkOccurrence(Map<?, ?> occurrence, String where) {
763        Map<?, ?> statement = get(occurrence.get("statementRef"), "statements");
764        Map<?, ?> unit = get(statement.get("sourceUnitRef"), "sourceUnits");
765        Map<String, Object> subject = subjectInput(occurrence);
766        if (subject == null) {
767            notes.add(where + ": an endpoint was not recomputed; the key is not checked");
768            return;
769        }
770        String unitId = unitId(unit);
771        String key = IdentityV1.compute(IdentityV1.OCCURRENCE_KEY, inputs(
772                "sourceUnit", unitId != null ? unitId : status(IdentityV1.LOCAL),
773                "statementOrdinal", statement.get("ordinal"), "subject", subject, "pathScope", scopeOf(unit),
774                "path", occurrence.get("path"), "ordinal", occurrence.get("ordinal"))).getId();
775        String context = contextId(unit);
776        String record = key != null && context != null
777                ? IdentityV1.compute(IdentityV1.OCCURRENCE_RECORD, inputs("occurrenceKey", key, "analysisContext", context))
778                .getId() : null;
779        settle(occurrence, "key", where, key, "key");
780        settle(occurrence, "recordId", where, record, "recordId");
781    }
782
783    private void checkNode(Map<?, ?> node, String where) {
784        Map<?, ?> statement = get(node.get("statementRef"), "statements");
785        Map<?, ?> unit = get(statement.get("sourceUnitRef"), "sourceUnits");
786        String unitId = unitId(unit);
787        String key = IdentityV1.compute(IdentityV1.EXPLAIN_NODE_KEY, inputs(
788                "sourceUnit", unitId != null ? unitId : status(IdentityV1.LOCAL),
789                "statementOrdinal", statement.get("ordinal"), "pathScope", scopeOf(unit), "path", node.get("path"),
790                "nodeKind", node.get("kind"), "ordinal", node.get("ordinal"))).getId();
791        String context = contextId(unit);
792        String record = key != null && context != null
793                ? IdentityV1.compute(IdentityV1.EXPLAIN_NODE_RECORD, inputs("nodeKey", key, "analysisContext", context))
794                .getId() : null;
795        settle(node, "key", where, key, "key");
796        settle(node, "recordId", where, record, "recordId");
797    }
798
799    /** A deep copy with mutable objects and arrays, for {@link #fill(Object)}. */
800    private static Object mutableCopy(Object value) {
801        return CanonicalJson.parse(CanonicalJson.jcsBytes(value));
802    }
803}