001package gudusoft.gsqlparser.dlineage.dynamicsql;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.dlineage.dataflow.model.xml.dataflow;
005import gudusoft.gsqlparser.dlineage.dynamicsql.RoutineSummaryEdge.Endpoint;
006import gudusoft.gsqlparser.sqlenv.CanonKey;
007import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType;
008import gudusoft.gsqlparser.util.SQLUtil;
009
010import java.util.ArrayList;
011import java.util.Collections;
012import java.util.LinkedHashSet;
013import java.util.List;
014import java.util.Set;
015
016/**
017 * B3 (design {@code routine-summary-scc-design.md} §2.6): the canonical
018 * LEAF-edge comparator between a LEGACY analysis and the SHADOW side model.
019 * Comparison is over boundary→boundary leaf edges — the same reachability
020 * projection the B2 differential proof uses — never XML equality.
021 *
022 * <p>Edge keys are STRUCTURED ({@link LeafEdgeKey}): every identifier segment
023 * is a {@link CanonKey} built with an EXPLICIT vendor, so equality carries the
024 * vendor's real identifier relation (SQL Server collation included) and never
025 * consults ambient global state; display strings are rendering only
026 * (codex-B3-r1 finding 8).
027 *
028 * <p>Classification:
029 * <ul>
030 * <li>{@code EQUAL} — leaf edge present in both runs;</li>
031 * <li>{@code INTENDED_ADDITION} — SHADOW-only edge (the promotion gate
032 *     requires every one of these to match a hand-authored golden,
033 *     bidirectionally — arch-F2 / codex-r2 N4);</li>
034 * <li>{@code MISSING_LEGACY_EDGE} — LEGACY edge absent from SHADOW. Current
035 *     publication is observation-only and cannot remove legacy edges; the
036 *     class remains useful for evaluating a future application mode;</li>
037 * <li>{@code REGRESSION} — a missing legacy edge whose exact endpoints
038 *     survive in SHADOW with a FLIPPED relation flavor (fdd↔fdr): the flow
039 *     survived but its semantics changed. Checked BEFORE CHANGED_TARGET —
040 *     the more specific pairing wins (codex-B3-r1 finding 9);</li>
041 * <li>{@code CHANGED_TARGET} — a missing legacy edge paired with a
042 *     shadow-only edge sharing (vendor, relation type, source): the flow
043 *     moved rather than vanished. Only meaningful under replacement (B6).</li>
044 * </ul>
045 */
046public final class ShadowLeafEdgeComparator {
047
048    public enum Classification {
049        EQUAL, INTENDED_ADDITION, MISSING_LEGACY_EDGE, CHANGED_TARGET, REGRESSION
050    }
051
052    /** One canonical leaf-edge key: vendor + relation type + two endpoints,
053     *  each endpoint a kind plus CanonKey'd name segments. */
054    public static final class LeafEdgeKey {
055        private final EDbVendor vendor;
056        private final String relationType;
057        private final String sourceKind;
058        private final List<CanonKey> sourceSegments;
059        private final String targetKind;
060        private final List<CanonKey> targetSegments;
061        private final String display;
062
063        private LeafEdgeKey(EDbVendor vendor, String relationType,
064                String sourceKind, List<CanonKey> sourceSegments, String sourceText,
065                String targetKind, List<CanonKey> targetSegments, String targetText) {
066            this.vendor = vendor;
067            this.relationType = relationType;
068            this.sourceKind = sourceKind;
069            this.sourceSegments = Collections.unmodifiableList(sourceSegments);
070            this.targetKind = targetKind;
071            this.targetSegments = Collections.unmodifiableList(targetSegments);
072            this.display = relationType + "|" + sourceKind + ":" + sourceText
073                    + "|" + targetKind + ":" + targetText;
074        }
075
076        static LeafEdgeKey of(EDbVendor vendor, Endpoint source, Endpoint target,
077                String relationType) {
078            return new LeafEdgeKey(vendor, relationType,
079                    source.getKind().name(), segmentsOf(vendor, source),
080                    source.getParent() + "." + source.getColumn(),
081                    target.getKind().name(), segmentsOf(vendor, target),
082                    target.getParent() + "." + target.getColumn());
083        }
084
085        /**
086         * Parse one hand-golden line: {@code type|KIND:parent.column|KIND:parent.column}.
087         * Segments go through the SAME canonKey construction as model-derived
088         * keys, so the golden's spelling case is irrelevant wherever the
089         * vendor's identifier rules say it is.
090         */
091        public static LeafEdgeKey parse(EDbVendor vendor, String line) {
092            String[] parts = line.split("\\|");
093            if (parts.length != 3) {
094                throw new IllegalArgumentException("expected type|src|tgt: " + line);
095            }
096            String[] src = splitEndpoint(parts[1], line);
097            String[] tgt = splitEndpoint(parts[2], line);
098            return new LeafEdgeKey(vendor, parts[0].trim(),
099                    src[0], segmentsOf(vendor, src[1]), src[1],
100                    tgt[0], segmentsOf(vendor, tgt[1]), tgt[1]);
101        }
102
103        private static String[] splitEndpoint(String text, String line) {
104            int colon = text.indexOf(':');
105            if (colon <= 0) {
106                throw new IllegalArgumentException("expected KIND:name in " + line);
107            }
108            return new String[] { text.substring(0, colon).trim(),
109                    text.substring(colon + 1).trim() };
110        }
111
112        private static List<CanonKey> segmentsOf(EDbVendor vendor, Endpoint e) {
113            return segmentsOf(vendor, e.getParent() + "." + e.getColumn());
114        }
115
116        private static List<CanonKey> segmentsOf(EDbVendor vendor, String qualified) {
117            List<String> parts = SQLUtil.parseNames(qualified);
118            List<CanonKey> keys = new ArrayList<CanonKey>();
119            for (int i = 0; i < parts.size(); i++) {
120                // last segment is the column; the rest are container names
121                ESQLDataObjectType type = i == parts.size() - 1
122                        ? ESQLDataObjectType.dotColumn : ESQLDataObjectType.dotTable;
123                keys.add(SQLUtil.canonKey(vendor, type, parts.get(i)));
124            }
125            return keys;
126        }
127
128        public String getRelationType() { return relationType; }
129
130        boolean sameSourceAndType(LeafEdgeKey o) {
131            // non-identifier-compare: relation type + kind tokens; segments use CanonKey equality
132            return vendor == o.vendor && relationType.equals(o.relationType)
133                    && sourceKind.equals(o.sourceKind)
134                    && sourceSegments.equals(o.sourceSegments);
135        }
136
137        boolean sameEndpointsFlippedType(LeafEdgeKey o) {
138            // non-identifier-compare: relation type + kind tokens; segments use CanonKey equality
139            return vendor == o.vendor && !relationType.equals(o.relationType)
140                    && sourceKind.equals(o.sourceKind)
141                    && sourceSegments.equals(o.sourceSegments)
142                    && targetKind.equals(o.targetKind)
143                    && targetSegments.equals(o.targetSegments);
144        }
145
146        @Override
147        public boolean equals(Object obj) {
148            if (this == obj) {
149                return true;
150            }
151            if (!(obj instanceof LeafEdgeKey)) {
152                return false;
153            }
154            LeafEdgeKey o = (LeafEdgeKey) obj;
155            // non-identifier-compare: relation type + kind tokens; segments use CanonKey equality
156            return vendor == o.vendor && relationType.equals(o.relationType)
157                    && sourceKind.equals(o.sourceKind)
158                    && sourceSegments.equals(o.sourceSegments)
159                    && targetKind.equals(o.targetKind)
160                    && targetSegments.equals(o.targetSegments);
161        }
162
163        @Override
164        public int hashCode() {
165            int h = vendor.hashCode();
166            h = h * 31 + relationType.hashCode();
167            h = h * 31 + sourceKind.hashCode();
168            h = h * 31 + sourceSegments.hashCode();
169            h = h * 31 + targetKind.hashCode();
170            h = h * 31 + targetSegments.hashCode();
171            return h;
172        }
173
174        /** Rendering only — never compare display strings. */
175        @Override
176        public String toString() {
177            return display;
178        }
179    }
180
181    /** One classified canonical leaf edge. */
182    public static final class ClassifiedEdge {
183        private final LeafEdgeKey key;
184        private final Classification classification;
185
186        ClassifiedEdge(LeafEdgeKey key, Classification classification) {
187            this.key = key;
188            this.classification = classification;
189        }
190
191        public LeafEdgeKey getKey() { return key; }
192        public Classification getClassification() { return classification; }
193
194        @Override
195        public String toString() {
196            return classification + " " + key;
197        }
198    }
199
200    /** Comparator output: classified edges + promotion-gate view. */
201    public static final class Report {
202        private final List<ClassifiedEdge> entries;
203
204        Report(List<ClassifiedEdge> entries) {
205            this.entries = Collections.unmodifiableList(entries);
206        }
207
208        public List<ClassifiedEdge> getEntries() { return entries; }
209
210        public List<ClassifiedEdge> byClassification(Classification c) {
211            List<ClassifiedEdge> result = new ArrayList<ClassifiedEdge>();
212            for (ClassifiedEdge e : entries) {
213                if (e.getClassification() == c) {
214                    result.add(e);
215                }
216            }
217            return result;
218        }
219
220        /** §2.6 gate: the report contains ONLY EQUAL + INTENDED_ADDITION. */
221        public boolean onlyEqualAndIntendedAdditions() {
222            for (ClassifiedEdge e : entries) {
223                if (e.getClassification() != Classification.EQUAL
224                        && e.getClassification() != Classification.INTENDED_ADDITION) {
225                    return false;
226                }
227            }
228            return true;
229        }
230    }
231
232    private ShadowLeafEdgeComparator() {
233    }
234
235    /**
236     * Canonical leaf-edge keys of an analyzed dataflow: its relation graph's
237     * boundary→boundary reachability projection (identical machinery to the
238     * summary extraction closure, applied to the WHOLE unit's model).
239     */
240    public static Set<LeafEdgeKey> leafKeysOf(EDbVendor vendor, dataflow df) {
241        Set<LeafEdgeKey> keys = new LinkedHashSet<LeafEdgeKey>();
242        if (df == null) {
243            return keys;
244        }
245        RoutineSummaryExtractor.RelationGraph graph =
246                RoutineSummaryExtractor.RelationGraph.of(df);
247        for (RoutineSummaryEdge edge : RoutineSummaryExtractor.close(graph)) {
248            keys.add(LeafEdgeKey.of(vendor, edge.getSource(), edge.getTarget(),
249                    edge.getRelationType()));
250        }
251        return keys;
252    }
253
254    /**
255     * Leaf key → number of DISTINCT composition occurrences producing it
256     * (caller × callee × call-site ordinal — the additions set already
257     * carries occurrence identity). This is the provenance-bearing layer of
258     * the two-layer key ruling (codex-B3-r2 findings 8/10): the golden gate
259     * checks OCCURRENCE counts, so losing one of several same-topology
260     * composition paths fails even though the leaf projection is unchanged.
261     */
262    public static java.util.Map<LeafEdgeKey, Integer> occurrenceCountsOf(
263            EDbVendor vendor, Set<ShadowSummaryApplier.ComposedEdge> additions) {
264        java.util.Map<LeafEdgeKey, Integer> counts =
265                new java.util.LinkedHashMap<LeafEdgeKey, Integer>();
266        for (ShadowSummaryApplier.ComposedEdge edge : additions) {
267            LeafEdgeKey key = LeafEdgeKey.of(vendor, edge.getSource(),
268                    edge.getTarget(), edge.getRelationType());
269            Integer prior = counts.get(key);
270            counts.put(key, prior == null ? 1 : prior + 1);
271        }
272        return counts;
273    }
274
275    /** Canonical keys of SHADOW additions (same construction as legacy keys). */
276    public static Set<LeafEdgeKey> additionKeysOf(EDbVendor vendor,
277            Set<ShadowSummaryApplier.ComposedEdge> additions) {
278        Set<LeafEdgeKey> keys = new LinkedHashSet<LeafEdgeKey>();
279        for (ShadowSummaryApplier.ComposedEdge edge : additions) {
280            keys.add(LeafEdgeKey.of(vendor, edge.getSource(), edge.getTarget(),
281                    edge.getRelationType()));
282        }
283        return keys;
284    }
285
286    /**
287     * Classify a LEGACY leaf-edge set against the SHADOW side model
288     * (B3 shape: SHADOW = LEGACY ∪ additions, so only EQUAL and
289     * INTENDED_ADDITION can appear; the remaining classes guard the
290     * comparator's reuse under B6 replacement).
291     */
292    public static Report compare(Set<LeafEdgeKey> legacyKeys,
293            Set<LeafEdgeKey> shadowKeys) {
294        List<ClassifiedEdge> entries = new ArrayList<ClassifiedEdge>();
295        List<LeafEdgeKey> missing = new ArrayList<LeafEdgeKey>();
296        List<LeafEdgeKey> shadowOnly = new ArrayList<LeafEdgeKey>();
297        for (LeafEdgeKey key : legacyKeys) {
298            if (shadowKeys.contains(key)) {
299                entries.add(new ClassifiedEdge(key, Classification.EQUAL));
300            } else {
301                missing.add(key);
302            }
303        }
304        for (LeafEdgeKey key : shadowKeys) {
305            if (!legacyKeys.contains(key)) {
306                shadowOnly.add(key);
307            }
308        }
309        for (LeafEdgeKey miss : missing) {
310            Classification cls = Classification.MISSING_LEGACY_EDGE;
311            for (LeafEdgeKey candidate : shadowOnly) {
312                if (miss.sameEndpointsFlippedType(candidate)) {
313                    cls = Classification.REGRESSION;
314                    break;
315                }
316            }
317            if (cls == Classification.MISSING_LEGACY_EDGE) {
318                for (LeafEdgeKey candidate : shadowOnly) {
319                    if (miss.sameSourceAndType(candidate)) {
320                        cls = Classification.CHANGED_TARGET;
321                        break;
322                    }
323                }
324            }
325            entries.add(new ClassifiedEdge(miss, cls));
326        }
327        for (LeafEdgeKey key : shadowOnly) {
328            entries.add(new ClassifiedEdge(key, Classification.INTENDED_ADDITION));
329        }
330        return new Report(entries);
331    }
332}