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}