001package gudusoft.gsqlparser.lineage.v2.fold;
002
003import gudusoft.gsqlparser.lineage.v2.Finding;
004import gudusoft.gsqlparser.lineage.v2.validation.DocumentIndex;
005import gudusoft.gsqlparser.lineage.v2.validation.SemanticStage;
006
007import java.util.ArrayList;
008import java.util.Arrays;
009import java.util.Collections;
010import java.util.HashMap;
011import java.util.HashSet;
012import java.util.LinkedHashMap;
013import java.util.LinkedHashSet;
014import java.util.List;
015import java.util.Map;
016import java.util.Set;
017import java.util.TreeMap;
018import java.util.TreeSet;
019
020import static gudusoft.gsqlparser.lineage.v2.validation.DocumentIndex.intRef;
021import static gudusoft.gsqlparser.lineage.v2.validation.DocumentIndex.items;
022
023/**
024 * The {@code PROJECTOR} checks of {@code algorithms/fold-v1.md}, Section 11: folds each statement with
025 * {@link FoldKernel} and compares the result with the Fact layer of the same document, at stage
026 * {@code SEMANTIC}.
027 *
028 * <p>The comparison is a port of the reference runner's ({@code conformance/run.py}, class {@code Projector}),
029 * so a document gets the same diagnostic codes from both, with one declared difference: the reference leaves out
030 * the derived-unit rules of Section 7.3, and this check applies them, so a statement of a derived unit whose text
031 * is only {@code OBSERVED} folds to {@code ASSUMED} Occurrences and writes bounded by {@code PARTIAL}.</p>
032 *
033 * @since 4.2.17
034 */
035public final class Projector implements SemanticStage.FoldCheck {
036
037    /**
038     * The check to give {@link gudusoft.gsqlparser.lineage.v2.validation.LineageValidator#standard}, with the
039     * kernel's default limits.
040     */
041    public static final SemanticStage.FoldCheck CHECK = new Projector(FoldKernel.DEFAULT_MAX_STATES,
042            FoldKernel.DEFAULT_MAX_WORLDS);
043
044    private final int maxStates;
045    private final int maxWorlds;
046
047    private Projector(int maxStates, int maxWorlds) {
048        this.maxStates = maxStates;
049        this.maxWorlds = maxWorlds;
050    }
051
052    /** The check with other limits on the work of folding one statement (see {@link FoldKernel}). */
053    public static SemanticStage.FoldCheck withLimits(int maxStates, int maxWorlds) {
054        return new Projector(maxStates, maxWorlds);
055    }
056
057    @Override
058    public void check(DocumentIndex index, List<Finding> findings, List<String> notes) {
059        new Run(new FoldKernel(index, maxStates, maxWorlds), findings).run();
060    }
061
062    /** One comparison: the document, the kernel and the findings. */
063    private static final class Run {
064        private final FoldKernel kernel;
065        private final DocumentIndex index;
066        private final List<Finding> findings;
067        private final Map<List<Object>, List<Long>> factsByProposition = new HashMap<List<Object>, List<Long>>();
068
069        Run(FoldKernel kernel, List<Finding> findings) {
070            this.kernel = kernel;
071            this.index = kernel.getIndex();
072            this.findings = findings;
073            for (Map<?, ?> fact : index.all("fact")) {
074                List<Object> key = Arrays.<Object>asList(FoldKernel.norm(fact.get("sourceRef")),
075                        FoldKernel.norm(fact.get("targetRef")), labelOfFact(fact));
076                List<Long> refs = factsByProposition.get(key);
077                if (refs == null) {
078                    refs = new ArrayList<Long>();
079                    factsByProposition.put(key, refs);
080                }
081                refs.add(intRef(fact.get("ref")));
082            }
083        }
084
085        void run() {
086            findings.addAll(kernel.malformedEdges());
087            for (Long statement : kernel.getStatements()) {
088                compare(statement, kernel.fold(statement));
089            }
090        }
091
092        private static FoldLabel labelOfFact(Map<?, ?> obj) {
093            return FoldLabel.of(obj.get("kind"), obj.get("role"));
094        }
095
096        /** (source, target, label, path, ordinal): the key of a Fact Occurrence. */
097        private static List<Object> factKey(Object source, Object target, FoldLabel label, Object path, Object ordinal) {
098            return Arrays.<Object>asList(FoldKernel.norm(source), FoldKernel.norm(target), label,
099                    new FoldKernel.PathKey(path), FoldKernel.norm(ordinal));
100        }
101
102        private void compare(long statement, StatementFold folded) {
103            boolean complete = "COMPLETE".equals(kernel.completeness(statement));
104            List<Map<?, ?>> occurrences = new ArrayList<Map<?, ?>>();
105            for (Map<?, ?> occurrence : index.all("occurrence")) {
106                if (FoldKernel.same(occurrence.get("statementRef"), statement)) {
107                    occurrences.add(occurrence);
108                }
109            }
110            String where = "statement ref " + statement;
111
112            LinkedHashMap<List<Object>, StatementFold.FactOccurrence> expectedFacts =
113                    new LinkedHashMap<List<Object>, StatementFold.FactOccurrence>();
114            for (StatementFold.FactOccurrence fact : folded.getFacts()) {
115                expectedFacts.put(factKey(fact.getSourceRef(), fact.getTargetRef(), fact.getLabel(), fact.getPath(),
116                        (long) fact.getOrdinal()), fact);
117            }
118            Set<List<Object>> matchedFactKeys = new LinkedHashSet<List<Object>>();
119            for (Map<?, ?> occurrence : occurrences) {
120                if (!occurrence.containsKey("factRef")) {
121                    continue;
122                }
123                Map<?, ?> fact = index.get("fact", occurrence.get("factRef"));
124                if (fact == null) {
125                    continue;
126                }
127                List<Object> key = factKey(fact.get("sourceRef"), fact.get("targetRef"), labelOfFact(fact),
128                        occurrence.get("path"), occurrence.get("ordinal"));
129                StatementFold.FactOccurrence expected = expectedFacts.get(key);
130                if (expected != null) {
131                    matchedFactKeys.add(key);
132                    checkOccurrence(occurrence, expected.getProofStatus(), expected.getSupport(), complete, expected, null);
133                    continue;
134                }
135                if (candidateHolds(folded, labelOfFact(fact), fact.get("sourceRef"), fact.get("targetRef"))) {
136                    findings.add(new Finding("SEMANTIC_FOLD_CANDIDATE_AS_FACT", "occurrences ref " + occurrence.get("ref"),
137                            "the fold yields only a FactCandidate for this proposition"));
138                } else if (complete) {
139                    findings.add(new Finding("SEMANTIC_FOLD_UNSUPPORTED", "occurrences ref " + occurrence.get("ref"),
140                            "no fold walk yields this Fact Occurrence"));
141                }
142            }
143            for (Map.Entry<List<Object>, StatementFold.FactOccurrence> entry : expectedFacts.entrySet()) {
144                if (!matchedFactKeys.contains(entry.getKey())) {
145                    findings.add(new Finding("SEMANTIC_FOLD_MISSING_FACT", where,
146                            "no Occurrence for fold result " + entry.getValue()));
147                }
148            }
149
150            // Candidate results: exact matches first, then the near misses on the same path and kind.
151            List<Object[]> declared = new ArrayList<Object[]>();
152            for (Map<?, ?> occurrence : occurrences) {
153                Map<?, ?> candidate = index.get("factCandidate", occurrence.get("factCandidateRef"));
154                if (candidate != null) {
155                    declared.add(new Object[] {occurrence, candidate});
156                }
157            }
158            LinkedHashMap<Object, StatementFold.Candidate> candidateMatches =
159                    new LinkedHashMap<Object, StatementFold.Candidate>();
160            List<StatementFold.Candidate> unmatched = new ArrayList<StatementFold.Candidate>();
161            for (StatementFold.Candidate result : folded.getCandidates()) {
162                Map<?, ?> hit = null;
163                for (Object[] pair : declared) {
164                    Map<?, ?> occurrence = (Map<?, ?>) pair[0];
165                    if (!candidateMatches.containsKey(FoldKernel.norm(occurrence.get("ref")))
166                            && sameCandidate(occurrence, (Map<?, ?>) pair[1], result)) {
167                        hit = occurrence;
168                        break;
169                    }
170                }
171                if (hit == null) {
172                    unmatched.add(result);
173                } else {
174                    candidateMatches.put(FoldKernel.norm(hit.get("ref")), result);
175                    checkOccurrence(hit, result.getProofStatus(), result.getSupport(), complete, null, result);
176                }
177            }
178            List<Object[]> leftover = new ArrayList<Object[]>();
179            for (Object[] pair : declared) {
180                if (!candidateMatches.containsKey(FoldKernel.norm(((Map<?, ?>) pair[0]).get("ref")))) {
181                    leftover.add(pair);
182                }
183            }
184            for (StatementFold.Candidate result : unmatched) {
185                Object[] near = null;
186                for (Object[] pair : leftover) {
187                    Map<?, ?> occurrence = (Map<?, ?>) pair[0];
188                    if (FoldKernel.same(occurrence.get("path"), result.getPath())
189                            && labelOfFact((Map<?, ?>) pair[1]).equals(result.getLabel())) {
190                        near = pair;
191                        break;
192                    }
193                }
194                if (near != null) {
195                    leftover.remove(near);
196                    findings.add(new Finding("SEMANTIC_FOLD_CANDIDATE_MISMATCH",
197                            "occurrences ref " + ((Map<?, ?>) near[0]).get("ref"), "the fold yields " + result));
198                } else {
199                    findings.add(new Finding("SEMANTIC_FOLD_MISSING_FACT", where, "no FactCandidate for fold result "
200                            + result));
201                }
202            }
203            if (complete) {
204                for (Object[] pair : leftover) {
205                    findings.add(new Finding("SEMANTIC_FOLD_UNSUPPORTED", "occurrences ref " + ((Map<?, ?>) pair[0]).get("ref"),
206                            "no fold walk yields this FactCandidate"));
207                }
208            }
209            checkWrites(statement, matchedFactKeys, candidateMatches);
210        }
211
212        private static boolean candidateHolds(StatementFold folded, FoldLabel label, Object source, Object target) {
213            StatementFold.Pair pair = new StatementFold.Pair(source, target);
214            for (StatementFold.Candidate candidate : folded.getCandidates()) {
215                if (candidate.getLabel().equals(label)) {
216                    for (Set<StatementFold.Pair> outcome : candidate.getEffectiveOutcomes()) {
217                        if (outcome.contains(pair)) {
218                            return true;
219                        }
220                    }
221                }
222            }
223            return false;
224        }
225
226        /**
227         * The decision of a published FactCandidate end (fold-v1 Section 7.1, item 5): the deciding set it names, a set of
228         * the document's candidateSets, compared by its members and membership; null without decidedBy.
229         */
230        private StatementFold.Decision declaredDecision(Map<?, ?> cset) {
231            if (!(cset.get("decidedBy") instanceof Map<?, ?>)) {
232                return null;
233            }
234            Map<?, ?> decided = (Map<?, ?>) cset.get("decidedBy");
235            Map<?, ?> deciding = null;
236            for (Map<?, ?> root : items(index.document().get("candidateSets"))) {
237                if (FoldKernel.same(FoldKernel.norm(root.get("ref")), FoldKernel.norm(decided.get("candidateSetRef")))) {
238                    deciding = root;
239                }
240            }
241            List<Object> alternatives = new ArrayList<Object>();
242            if (deciding != null && deciding.get("candidateRefs") instanceof List<?>) {
243                for (Object member : (List<?>) deciding.get("candidateRefs")) {
244                    alternatives.add(FoldKernel.norm(member));
245                }
246            }
247            List<StatementFold.Pair> alignment = new ArrayList<StatementFold.Pair>();
248            for (Map<?, ?> pair : items(decided.get("alignment"))) {
249                alignment.add(new StatementFold.Pair(FoldKernel.norm(pair.get("alternativeRef")),
250                        FoldKernel.norm(pair.get("candidateRef"))));
251            }
252            return new StatementFold.Decision(decided.get("candidateSetRef"), alternatives,
253                    deciding == null ? null : deciding.get("membership"), alignment);
254        }
255
256        /** (source end, target end, explicit outcomes or null, effective outcomes) of a FactCandidate record. */
257        private Object[] declaredCandidate(Map<?, ?> candidate) {
258            StatementFold.End[] ends = new StatementFold.End[2];
259            String[] sides = {"source", "target"};
260            for (int i = 0; i < 2; i++) {
261                Map<?, ?> end = candidate.get(sides[i]) instanceof Map<?, ?> ? (Map<?, ?>) candidate.get(sides[i])
262                        : Collections.emptyMap();
263                if (end.containsKey("endpointRef")) {
264                    ends[i] = StatementFold.End.fixed(FoldKernel.norm(end.get("endpointRef")));
265                } else {
266                    Map<?, ?> cset = end.get("candidateSet") instanceof Map<?, ?> ? (Map<?, ?>) end.get("candidateSet")
267                            : Collections.emptyMap();
268                    List<Object> list = new ArrayList<Object>();
269                    if (cset.get("candidateRefs") instanceof List<?>) {
270                        for (Object member : (List<?>) cset.get("candidateRefs")) {
271                            if (intRef(member) != null) {
272                                list.add(intRef(member));
273                            }
274                        }
275                    }
276                    // sorted with duplicates kept, as a sorted tuple keeps them
277                    Collections.sort(list, FoldKernel.REF_ORDER);
278                    ends[i] = StatementFold.End.set(list, cset.get("membership"), declaredDecision(cset));
279                }
280            }
281            Set<Set<StatementFold.Pair>> explicit = null;
282            if (candidate.get("outcomes") instanceof List<?>) {
283                explicit = new LinkedHashSet<Set<StatementFold.Pair>>();
284                for (Map<?, ?> outcome : items(candidate.get("outcomes"))) {
285                    Set<StatementFold.Pair> pairs = new LinkedHashSet<StatementFold.Pair>();
286                    for (Map<?, ?> pair : items(outcome.get("pairs"))) {
287                        pairs.add(new StatementFold.Pair(FoldKernel.norm(pair.get("sourceRef")),
288                                FoldKernel.norm(pair.get("targetRef"))));
289                    }
290                    explicit.add(pairs);
291                }
292            }
293            return new Object[] {ends[0], ends[1], explicit,
294                    explicit != null ? explicit : FoldKernel.impliedOutcomes(ends[0], ends[1])};
295        }
296
297        /**
298         * The reference matches a FactCandidate Occurrence by kind, path, ends and outcomes; this check also matches
299         * the ordinal (identity-v1, Section 5.4), so that of two FactCandidates with one subject on one path each
300         * Occurrence is compared with its own result (a declared difference from the reference).
301         */
302        private boolean sameCandidate(Map<?, ?> occurrence, Map<?, ?> candidate, StatementFold.Candidate result) {
303            if (!labelOfFact(candidate).equals(result.getLabel()) || !FoldKernel.same(occurrence.get("path"), result.getPath())
304                    || !FoldKernel.same(occurrence.get("ordinal"), (long) result.getOrdinal())) {
305                return false;
306            }
307            Object[] ends = declaredCandidate(candidate);
308            return ends[0].equals(result.getSource()) && ends[1].equals(result.getTarget())
309                    && (ends[2] == null) == (result.getOutcomes() == null) && ends[3].equals(result.getEffectiveOutcomes());
310        }
311
312        private void checkOccurrence(Map<?, ?> occurrence, String folded, Set<Long> support, boolean complete,
313                                     StatementFold.FactOccurrence fact, StatementFold.Candidate candidate) {
314            String where = "occurrences ref " + occurrence.get("ref");
315            Object declared = occurrence.get("proofStatus");
316            if ("ASSUMED".equals(declared) && "PROVEN".equals(folded)) {
317                findings.add(new Finding("SEMANTIC_FOLD_PROOF_MISMATCH", where, "ASSUMED, the fold proves it"));
318            }
319            if ("PROVEN".equals(declared) && "ASSUMED".equals(folded) && complete) {
320                findings.add(new Finding("SEMANTIC_FOLD_PROOF_MISMATCH", where, "PROVEN, the fold yields ASSUMED"));
321            }
322            Set<Object> listed = new HashSet<Object>();
323            Object pathRefs = occurrence.get("supportPathRefs");
324            if (pathRefs instanceof List<?>) {
325                for (Object pathRef : (List<?>) pathRefs) {
326                    Map<?, ?> path = index.get("supportPath", pathRef);
327                    Object refs = path == null ? null : path.get("microEdgeRefs");
328                    List<?> list = refs instanceof List<?> ? (List<?>) refs : Collections.emptyList();
329                    if (!yields(list, occurrence, fact, candidate)) {
330                        findings.add(new Finding("SEMANTIC_FOLD_BAD_SUPPORT_PATH", where,
331                                "support path " + pathRef + " is not a fold walk yielding this record"));
332                    }
333                    for (Object r : list) {
334                        listed.add(FoldKernel.norm(r));
335                    }
336                }
337            }
338            if (complete) {
339                Set<Long> missing = new TreeSet<Long>();
340                for (Long edge : support) {
341                    if (!listed.contains(edge)) {
342                        missing.add(edge);
343                    }
344                }
345                if (!missing.isEmpty()) {
346                    findings.add(new Finding("SEMANTIC_FOLD_SUPPORT_NOT_COVERED", where,
347                            "micro-edges " + missing + " are not covered"));
348                }
349            }
350        }
351
352        /**
353         * True when the listed micro-edges form a fold walk yielding the record of the Occurrence. Beyond the
354         * reference's test, the walk must start at the Occurrence's own reference (its position, for a Fact) or take
355         * only the references of the FactCandidate's group (fold-v1, Section 8.2, item 3; a declared difference).
356         */
357        private boolean yields(List<?> refs, Map<?, ?> occurrence, StatementFold.FactOccurrence expectedFact,
358                               StatementFold.Candidate expectedCandidate) {
359            List<Map<?, ?>> edges = new ArrayList<Map<?, ?>>();
360            for (Object r : refs) {
361                Map<?, ?> edge = index.get("microEdge", r);
362                if (edge == null) {
363                    return false;
364                }
365                edges.add(edge);
366            }
367            if (edges.isEmpty()) {
368                return false;
369            }
370            Object statement = occurrence.get("statementRef");
371            for (Map<?, ?> edge : edges) {
372                if (!FoldKernel.same(edge.get("statementRef"), statement)) {
373                    return false;
374                }
375            }
376            Map<?, ?> first = edges.get(0);
377            Map<?, ?> last = edges.get(edges.size() - 1);
378            if (!(kernel.kept(first.get("sourceRef")) || kernel.graphCset(first.get("sourceRef")))) {
379                return false;
380            }
381            if (!kernel.kept(last.get("targetRef"))) {
382                return false;
383            }
384            FoldLabel label = FoldKernel.edgeLabel(first);
385            FoldKernel.Sigma sigma = FoldKernel.Sigma.EMPTY;
386            for (int i = 0; i < edges.size(); i++) {
387                Map<?, ?> edge = edges.get(i);
388                if (i > 0 && !FoldKernel.same(edge.get("sourceRef"), edges.get(i - 1).get("targetRef"))) {
389                    return false;
390                }
391                if (i < edges.size() - 1 && !kernel.folded(edge.get("targetRef"))) {
392                    return false;
393                }
394                if (i > 0) {
395                    label = label.compose(FoldKernel.edgeLabel(edge));
396                }
397                sigma = sigma.extend(kernel.choices(edge));
398                if (sigma == null) {
399                    return false;
400                }
401            }
402            Map<?, ?> fact = index.get("fact", occurrence.get("factRef"));
403            if (fact != null) {
404                return label.equals(labelOfFact(fact)) && FoldKernel.same(first.get("sourceRef"), fact.get("sourceRef"))
405                        && FoldKernel.same(last.get("targetRef"), fact.get("targetRef"))
406                        && FoldKernel.same(first.get("referencePath"), occurrence.get("path"))
407                        && (expectedFact == null
408                        || FoldKernel.same(FoldKernel.offsetOf(first.get("referenceStart")), expectedFact.getOffset()));
409            }
410            if (expectedCandidate != null) {
411                for (Object ref : sigma.refs) {
412                    if (!FoldKernel.containsRef(expectedCandidate.getReferences(), ref)) {
413                        return false;
414                    }
415                }
416            }
417            Map<?, ?> candidate = index.get("factCandidate", occurrence.get("factCandidateRef"));
418            if (candidate == null || sigma.isEmpty() || !label.equals(labelOfFact(candidate))) {
419                return false;
420            }
421            Object[] ends = declaredCandidate(candidate);
422            StatementFold.End targetEnd = (StatementFold.End) ends[1];
423            if (kernel.graphCset(first.get("sourceRef"))) {
424                List<Object> targets = targetEnd.isFixed() ? Collections.singletonList(targetEnd.getEndpointRef())
425                        : targetEnd.getMembers();
426                return FoldKernel.containsRef(targets, last.get("targetRef"));
427            }
428            StatementFold.Pair pair = new StatementFold.Pair(first.get("sourceRef"), last.get("targetRef"));
429            @SuppressWarnings("unchecked")
430            Set<Set<StatementFold.Pair>> effective = (Set<Set<StatementFold.Pair>>) ends[3];
431            for (Set<StatementFold.Pair> outcome : effective) {
432                if (outcome.contains(pair)) {
433                    return true;
434                }
435            }
436            return false;
437        }
438
439        private static final class Need {
440            final Set<Object> facts = new LinkedHashSet<Object>();
441            final Set<Object> candidates = new LinkedHashSet<Object>();
442            final Set<StatementFold.TerminalInput> terminals = new LinkedHashSet<StatementFold.TerminalInput>();
443            String bound = "COMPLETE";
444            final Set<Long> diagnostics = new TreeSet<Long>();
445        }
446
447        /** Sections 7.4 and 7.5, as the reference checks them against the write records of the statement. */
448        private void checkWrites(long statement, Set<List<Object>> matchedFactKeys,
449                                 Map<Object, StatementFold.Candidate> candidateMatches) {
450            Map<Object, List<Map<?, ?>>> out = kernel.outOf(statement);
451            TreeMap<Object, Need> required = new TreeMap<Object, Need>(FoldKernel.REF_ORDER);
452            for (Map<?, ?> edge : kernel.edgesOf(statement)) {
453                if (kernel.isField(edge.get("targetRef"))) {
454                    need(required, edge.get("targetRef"));
455                }
456            }
457            List<Map<?, ?>> writes = new ArrayList<Map<?, ?>>();
458            for (Map<?, ?> write : index.all("tfo")) {
459                if (FoldKernel.same(write.get("statementRef"), statement)) {
460                    writes.add(write);
461                    if (kernel.isField(write.get("targetRef"))) {
462                        need(required, write.get("targetRef"));
463                    }
464                }
465            }
466            for (List<Object> key : matchedFactKeys) {
467                List<Long> facts = factsByProposition.get(key.subList(0, 3));
468                if (kernel.isField(key.get(1))) {
469                    Need need = need(required, key.get(1));
470                    if (facts != null) {
471                        need.facts.addAll(facts);
472                    }
473                }
474            }
475            for (Map.Entry<Object, StatementFold.Candidate> entry : candidateMatches.entrySet()) {
476                Map<?, ?> occurrence = index.get("occurrence", entry.getKey());
477                StatementFold.End target = entry.getValue().getTarget();
478                List<Object> targets = target.isFixed() ? Collections.singletonList(target.getEndpointRef())
479                        : target.getMembers();
480                for (Object t : targets) {
481                    if (kernel.isField(t)) {
482                        Need need = need(required, t);
483                        need.candidates.add(FoldKernel.norm(occurrence.get("factCandidateRef")));
484                        need.bound = "PARTIAL";
485                    }
486                }
487            }
488            for (Map.Entry<Object, Set<StatementFold.TerminalInput>> entry : kernel.terminalInputs(statement, out).entrySet()) {
489                need(required, entry.getKey()).terminals.addAll(entry.getValue());
490            }
491            // the reference also lowers through micro-edges that take two alternatives of one reference; fold-v1
492            // counts walks only (a declared difference)
493            Map<Object, Object[]> lowered = kernel.lowering(statement, out);
494            boolean observed = kernel.observed(statement);
495            for (Map.Entry<Object, Need> entry : required.entrySet()) {
496                Need need = entry.getValue();
497                if (observed) {
498                    need.bound = FoldKernel.lower(need.bound, "PARTIAL");
499                }
500                if (kernel.enteredByChoice(statement, entry.getKey())) {
501                    need.bound = FoldKernel.lower(need.bound, "PARTIAL"); // written only in some worlds (Section 7.4)
502                }
503                Object[] lowering = lowered.get(entry.getKey());
504                if (lowering != null) {
505                    need.bound = FoldKernel.lower(need.bound, lowering[0]);
506                    @SuppressWarnings("unchecked")
507                    Set<Long> diagnostics = (Set<Long>) lowering[1];
508                    need.diagnostics.addAll(diagnostics);
509                }
510            }
511            for (Map.Entry<Object, Need> entry : required.entrySet()) {
512                Need need = entry.getValue();
513                List<Map<?, ?>> mine = new ArrayList<Map<?, ?>>();
514                for (Map<?, ?> write : writes) {
515                    if (FoldKernel.same(write.get("targetRef"), entry.getKey())) {
516                        mine.add(write);
517                    }
518                }
519                if (mine.isEmpty()) {
520                    findings.add(new Finding("SEMANTIC_FOLD_INPUT_MISSING", "statement ref " + statement,
521                            "field ref " + entry.getKey() + " is written but has no TargetFieldOccurrence"));
522                    continue;
523                }
524                for (Map<?, ?> write : mine) {
525                    String where = "targetFieldOccurrences ref " + write.get("ref");
526                    Set<Object> haveFacts = new HashSet<Object>();
527                    Set<Object> haveCandidates = new HashSet<Object>();
528                    Set<StatementFold.TerminalInput> haveTerminals = new HashSet<StatementFold.TerminalInput>();
529                    for (Map<?, ?> input : items(write.get("inputs"))) {
530                        if (input.containsKey("factRef")) {
531                            haveFacts.add(FoldKernel.norm(input.get("factRef")));
532                        }
533                        if (input.containsKey("factCandidateRef")) {
534                            haveCandidates.add(FoldKernel.norm(input.get("factCandidateRef")));
535                        }
536                        if (!"FIELD_REF".equals(input.get("kind"))) {
537                            haveTerminals.add(FoldKernel.terminalInput(input.get("kind"), input));
538                        }
539                    }
540                    List<Object> missing = new ArrayList<Object>();
541                    for (Object fact : need.facts) {
542                        if (!haveFacts.contains(fact)) {
543                            missing.add(fact);
544                        }
545                    }
546                    for (Object candidate : need.candidates) {
547                        if (!haveCandidates.contains(candidate)) {
548                            missing.add(candidate);
549                        }
550                    }
551                    for (StatementFold.TerminalInput terminal : need.terminals) {
552                        if (!haveTerminals.contains(terminal)) {
553                            missing.add(terminal);
554                        }
555                    }
556                    if (!missing.isEmpty()) {
557                        findings.add(new Finding("SEMANTIC_FOLD_INPUT_MISSING", where, "missing inputs " + missing));
558                    }
559                    Set<Long> linked = new HashSet<Long>();
560                    FoldKernel.addRefs(linked, write.get("diagnosticRefs"));
561                    Set<Long> lowering = new TreeSet<Long>(need.diagnostics);
562                    lowering.removeAll(linked);
563                    if (FoldKernel.rank(write.get("upstreamCompleteness")) > FoldKernel.rank(need.bound)
564                            || !lowering.isEmpty()) {
565                        findings.add(new Finding("SEMANTIC_FOLD_COMPLETENESS_EXCEEDS_BOUND", where, "declared "
566                                + write.get("upstreamCompleteness") + ", bound " + need.bound + ", unlinked diagnostics "
567                                + lowering));
568                    }
569                }
570            }
571        }
572
573        private static Need need(Map<Object, Need> required, Object target) {
574            Object key = FoldKernel.norm(target);
575            Need need = required.get(key);
576            if (need == null) {
577                need = new Need();
578                required.put(key, need);
579            }
580            return need;
581        }
582    }
583}