@mitre/hdf-diff 3.1.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { validateComparison as validateComparison$1 } from "@mitre/hdf-validators";
2
+ import { parseCvssVector } from "@mitre/hdf-utilities";
2
3
  //#region src/status.ts
3
4
  /** Status severity ranking — higher index = worse */
4
5
  const STATUS_SEVERITY = [
@@ -398,7 +399,7 @@ function createMappedIdStrategy(mapping) {
398
399
  * Extract CCI identifiers from a requirement's tags.
399
400
  * Looks for `tags.cci` as an array of strings.
400
401
  */
401
- function extractCcis(req) {
402
+ function extractCcis$1(req) {
402
403
  const tags = req["tags"];
403
404
  if (!tags) return [];
404
405
  const cci = tags["cci"];
@@ -406,6 +407,65 @@ function extractCcis(req) {
406
407
  return cci.filter((c) => typeof c === "string");
407
408
  }
408
409
  /**
410
+ * Extract CWE identifiers from a requirement.
411
+ *
412
+ * Prefers the structured `req.cwe[]` field (introduced in Evaluated_Requirement
413
+ * Wave 1) when present and non-empty. Falls back to `tags.cwe` for compatibility
414
+ * with HDF files emitted before the structured field existed. `tags.cwe` may be
415
+ * either an array of strings or a single string.
416
+ *
417
+ * Exported for reuse by sibling matchers (e.g. srg-cci-tiebreak) and to make
418
+ * the preference order directly testable.
419
+ */
420
+ function extractCwes$1(req) {
421
+ const cwe = req["cwe"];
422
+ if (Array.isArray(cwe)) {
423
+ const filtered = cwe.filter((c) => typeof c === "string");
424
+ if (filtered.length > 0) return filtered;
425
+ }
426
+ const tags = req["tags"];
427
+ if (!tags) return [];
428
+ const tagCwe = tags["cwe"];
429
+ if (Array.isArray(tagCwe)) return tagCwe.filter((c) => typeof c === "string");
430
+ if (typeof tagCwe === "string") return [tagCwe];
431
+ return [];
432
+ }
433
+ /**
434
+ * Build a key → [requirement-indices] map for unambiguous-pair lookup.
435
+ */
436
+ function indexByKey(reqs, keys) {
437
+ const map = /* @__PURE__ */ new Map();
438
+ for (let i = 0; i < reqs.length; i++) for (const key of keys(reqs[i])) {
439
+ const list = map.get(key);
440
+ if (list) list.push(i);
441
+ else map.set(key, [i]);
442
+ }
443
+ return map;
444
+ }
445
+ /**
446
+ * Pair indices by an unambiguous shared key (exactly 1 old + 1 new per key),
447
+ * skipping any indices already claimed in `matchedOld` / `matchedNew`.
448
+ */
449
+ function pairUnambiguous(oldMap, newMap, matchedOld, matchedNew) {
450
+ const pairs = [];
451
+ const allKeys = new Set([...oldMap.keys(), ...newMap.keys()]);
452
+ for (const key of allKeys) {
453
+ const oldIndices = oldMap.get(key) ?? [];
454
+ const newIndices = newMap.get(key) ?? [];
455
+ if (oldIndices.length !== 1 || newIndices.length !== 1) continue;
456
+ const oldIdx = oldIndices[0];
457
+ const newIdx = newIndices[0];
458
+ if (matchedOld.has(oldIdx) || matchedNew.has(newIdx)) continue;
459
+ pairs.push({
460
+ oldIdx,
461
+ newIdx
462
+ });
463
+ matchedOld.add(oldIdx);
464
+ matchedNew.add(newIdx);
465
+ }
466
+ return pairs;
467
+ }
468
+ /**
409
469
  * Create a CCI-based matching strategy.
410
470
  *
411
471
  * Matches requirements that share the same CCI identifier in `tags.cci`.
@@ -414,51 +474,44 @@ function extractCcis(req) {
414
474
  * by multiple old or multiple new requirements) are skipped, and those
415
475
  * requirements are left unmatched.
416
476
  *
417
- * Confidence is 0.8 for unambiguous matches.
477
+ * As a fallback, also pairs unambiguous CWE matches (preferring the
478
+ * structured `req.cwe[]` field over the legacy `tags.cwe`). This catches
479
+ * CVE-ecosystem findings where CCI is absent but CWE is present.
480
+ *
481
+ * Confidence is 0.8 for CCI matches and 0.6 for CWE-fallback matches.
418
482
  */
419
483
  function createCciMatchStrategy() {
420
484
  return {
421
485
  name: "cciMatch",
422
486
  match(oldReqs, newReqs) {
423
- const result = {
424
- matched: [],
425
- unmatchedOld: [],
426
- unmatchedNew: []
427
- };
428
- const oldCciMap = /* @__PURE__ */ new Map();
429
- for (let i = 0; i < oldReqs.length; i++) for (const cci of extractCcis(oldReqs[i])) {
430
- const list = oldCciMap.get(cci);
431
- if (list) list.push(i);
432
- else oldCciMap.set(cci, [i]);
433
- }
434
- const newCciMap = /* @__PURE__ */ new Map();
435
- for (let i = 0; i < newReqs.length; i++) for (const cci of extractCcis(newReqs[i])) {
436
- const list = newCciMap.get(cci);
437
- if (list) list.push(i);
438
- else newCciMap.set(cci, [i]);
439
- }
440
487
  const matchedOldIndices = /* @__PURE__ */ new Set();
441
488
  const matchedNewIndices = /* @__PURE__ */ new Set();
442
- const allCcis = new Set([...oldCciMap.keys(), ...newCciMap.keys()]);
443
- for (const cci of allCcis) {
444
- const oldIndices = oldCciMap.get(cci) ?? [];
445
- const newIndices = newCciMap.get(cci) ?? [];
446
- if (oldIndices.length !== 1 || newIndices.length !== 1) continue;
447
- const oldIdx = oldIndices[0];
448
- const newIdx = newIndices[0];
449
- if (matchedOldIndices.has(oldIdx) || matchedNewIndices.has(newIdx)) continue;
450
- result.matched.push({
451
- oldReq: oldReqs[oldIdx],
452
- newReq: newReqs[newIdx],
453
- strategy: "cciMatch",
454
- confidence: .8
455
- });
456
- matchedOldIndices.add(oldIdx);
457
- matchedNewIndices.add(newIdx);
458
- }
459
- for (let i = 0; i < oldReqs.length; i++) if (!matchedOldIndices.has(i)) result.unmatchedOld.push(oldReqs[i]);
460
- for (let i = 0; i < newReqs.length; i++) if (!matchedNewIndices.has(i)) result.unmatchedNew.push(newReqs[i]);
461
- return result;
489
+ const matched = [];
490
+ const oldCciMap = indexByKey(oldReqs, extractCcis$1);
491
+ const newCciMap = indexByKey(newReqs, extractCcis$1);
492
+ for (const { oldIdx, newIdx } of pairUnambiguous(oldCciMap, newCciMap, matchedOldIndices, matchedNewIndices)) matched.push({
493
+ oldReq: oldReqs[oldIdx],
494
+ newReq: newReqs[newIdx],
495
+ strategy: "cciMatch",
496
+ confidence: .8
497
+ });
498
+ const oldCweMap = indexByKey(oldReqs, (r) => extractCwes$1(r));
499
+ const newCweMap = indexByKey(newReqs, (r) => extractCwes$1(r));
500
+ for (const { oldIdx, newIdx } of pairUnambiguous(oldCweMap, newCweMap, matchedOldIndices, matchedNewIndices)) matched.push({
501
+ oldReq: oldReqs[oldIdx],
502
+ newReq: newReqs[newIdx],
503
+ strategy: "cciMatch",
504
+ confidence: .6
505
+ });
506
+ const unmatchedOld = [];
507
+ for (let i = 0; i < oldReqs.length; i++) if (!matchedOldIndices.has(i)) unmatchedOld.push(oldReqs[i]);
508
+ const unmatchedNew = [];
509
+ for (let i = 0; i < newReqs.length; i++) if (!matchedNewIndices.has(i)) unmatchedNew.push(newReqs[i]);
510
+ return {
511
+ matched,
512
+ unmatchedOld,
513
+ unmatchedNew
514
+ };
462
515
  }
463
516
  };
464
517
  }
@@ -648,6 +701,437 @@ function createFuzzyTitleStrategy(minConfidence) {
648
701
  };
649
702
  }
650
703
  //#endregion
704
+ //#region src/matching/srg-deterministic.ts
705
+ /**
706
+ * Extract the SRG-OS ID from a requirement's tags.gtitle field.
707
+ * Returns null if missing or not a string.
708
+ */
709
+ function extractGtitle$1(req) {
710
+ const tags = req["tags"];
711
+ if (!tags) return null;
712
+ const gtitle = tags["gtitle"];
713
+ if (typeof gtitle !== "string" || gtitle === "") return null;
714
+ return gtitle;
715
+ }
716
+ /**
717
+ * Create a deterministic SRG matching strategy (Tier 1 of the delta pipeline).
718
+ *
719
+ * Matches requirements by exact `tags.gtitle` (SRG-OS requirement ID).
720
+ *
721
+ * For each gtitle shared between old and new:
722
+ * - 1 old + 1 new → single MatchPair (confidence 1.0, relationship "primary")
723
+ * - 1 new + N old → N MatchPairs (first old "primary", rest "related")
724
+ * - N new + 1 old → N MatchPairs (first new "primary", rest "related")
725
+ * - N:M (both > 1) → skip, leave for fallback strategies
726
+ *
727
+ * Requirements without a gtitle pass through as unmatched.
728
+ */
729
+ function createSrgDeterministicStrategy() {
730
+ return {
731
+ name: "srgDeterministic",
732
+ match(oldReqs, newReqs) {
733
+ const oldGtitleMap = /* @__PURE__ */ new Map();
734
+ for (let i = 0; i < oldReqs.length; i++) {
735
+ const g = extractGtitle$1(oldReqs[i]);
736
+ if (g) {
737
+ const list = oldGtitleMap.get(g);
738
+ if (list) list.push(i);
739
+ else oldGtitleMap.set(g, [i]);
740
+ }
741
+ }
742
+ const newGtitleMap = /* @__PURE__ */ new Map();
743
+ for (let i = 0; i < newReqs.length; i++) {
744
+ const g = extractGtitle$1(newReqs[i]);
745
+ if (g) {
746
+ const list = newGtitleMap.get(g);
747
+ if (list) list.push(i);
748
+ else newGtitleMap.set(g, [i]);
749
+ }
750
+ }
751
+ const matched = [];
752
+ const matchedOldIndices = /* @__PURE__ */ new Set();
753
+ const matchedNewIndices = /* @__PURE__ */ new Set();
754
+ const claimedOldIndices = /* @__PURE__ */ new Set();
755
+ for (const [gtitle, newIdxList] of newGtitleMap) {
756
+ const oldIdxList = oldGtitleMap.get(gtitle);
757
+ if (!oldIdxList || oldIdxList.length === 0) continue;
758
+ const nc = newIdxList.length;
759
+ const oc = oldIdxList.length;
760
+ if (nc > 1 && oc > 1) continue;
761
+ for (const ni of newIdxList) {
762
+ let primaryOldSet = false;
763
+ for (const oi of oldIdxList) {
764
+ let relationship;
765
+ if (oc === 1) {
766
+ relationship = claimedOldIndices.has(oi) ? "related" : "primary";
767
+ if (relationship === "primary") claimedOldIndices.add(oi);
768
+ } else {
769
+ relationship = primaryOldSet ? "related" : "primary";
770
+ primaryOldSet = true;
771
+ }
772
+ matched.push({
773
+ oldReq: oldReqs[oi],
774
+ newReq: newReqs[ni],
775
+ strategy: "srgDeterministic",
776
+ confidence: 1,
777
+ relationship
778
+ });
779
+ matchedOldIndices.add(oi);
780
+ }
781
+ matchedNewIndices.add(ni);
782
+ }
783
+ }
784
+ const unmatchedOld = [];
785
+ for (let i = 0; i < oldReqs.length; i++) if (!matchedOldIndices.has(i)) unmatchedOld.push(oldReqs[i]);
786
+ const unmatchedNew = [];
787
+ for (let i = 0; i < newReqs.length; i++) if (!matchedNewIndices.has(i)) unmatchedNew.push(newReqs[i]);
788
+ return {
789
+ matched,
790
+ unmatchedOld,
791
+ unmatchedNew
792
+ };
793
+ }
794
+ };
795
+ }
796
+ //#endregion
797
+ //#region src/matching/srg-cci-tiebreak.ts
798
+ /** Composite score weights matching SAF CLI's Tier 2. */
799
+ const CCI_WEIGHT = .7;
800
+ const TITLE_WEIGHT = .3;
801
+ /**
802
+ * Extract CCI identifiers from a requirement's tags.cci.
803
+ */
804
+ function extractCcis(req) {
805
+ const tags = req["tags"];
806
+ if (!tags) return /* @__PURE__ */ new Set();
807
+ const cci = tags["cci"];
808
+ if (!Array.isArray(cci)) return /* @__PURE__ */ new Set();
809
+ return new Set(cci.filter((c) => typeof c === "string"));
810
+ }
811
+ /**
812
+ * Extract CWE identifiers from a requirement.
813
+ *
814
+ * Prefers the structured `req.cwe[]` field (introduced in Evaluated_Requirement
815
+ * Wave 1) when present and non-empty. Falls back to `tags.cwe` (array or string)
816
+ * for compatibility with HDF files emitted before the structured field existed.
817
+ *
818
+ * Exported so callers and tests can verify the preference order directly.
819
+ */
820
+ function extractCwes(req) {
821
+ const cwe = req["cwe"];
822
+ if (Array.isArray(cwe)) {
823
+ const filtered = cwe.filter((c) => typeof c === "string");
824
+ if (filtered.length > 0) return new Set(filtered);
825
+ }
826
+ const tags = req["tags"];
827
+ if (!tags) return /* @__PURE__ */ new Set();
828
+ const tagCwe = tags["cwe"];
829
+ if (Array.isArray(tagCwe)) return new Set(tagCwe.filter((c) => typeof c === "string"));
830
+ if (typeof tagCwe === "string") return new Set([tagCwe]);
831
+ return /* @__PURE__ */ new Set();
832
+ }
833
+ /**
834
+ * Extract tags.gtitle from a requirement.
835
+ */
836
+ function extractGtitle(req) {
837
+ const tags = req["tags"];
838
+ if (!tags) return null;
839
+ const gtitle = tags["gtitle"];
840
+ if (typeof gtitle !== "string" || gtitle === "") return null;
841
+ return gtitle;
842
+ }
843
+ /**
844
+ * Extract title from a requirement.
845
+ */
846
+ function safeTitle$1(req) {
847
+ const title = req["title"];
848
+ return typeof title === "string" ? title : "";
849
+ }
850
+ /**
851
+ * Jaccard similarity between two sets.
852
+ */
853
+ function cciJaccard(a, b) {
854
+ if (a.size === 0 && b.size === 0) return 0;
855
+ let intersectionSize = 0;
856
+ for (const x of a) if (b.has(x)) intersectionSize++;
857
+ const unionSize = a.size + b.size - intersectionSize;
858
+ return intersectionSize / unionSize;
859
+ }
860
+ /**
861
+ * Simple whitespace-split token Jaccard similarity.
862
+ */
863
+ function tokenJaccard(a, b) {
864
+ const ta = new Set(a.toLowerCase().split(/\s+/).filter((t) => t.length > 0));
865
+ const tb = new Set(b.toLowerCase().split(/\s+/).filter((t) => t.length > 0));
866
+ if (ta.size === 0 || tb.size === 0) return 0;
867
+ let intersectionSize = 0;
868
+ for (const x of ta) if (tb.has(x)) intersectionSize++;
869
+ const unionSize = ta.size + tb.size - intersectionSize;
870
+ return intersectionSize / unionSize;
871
+ }
872
+ /**
873
+ * Create an SRG CCI Tiebreak matching strategy (Tier 2 of the delta pipeline).
874
+ *
875
+ * Handles ambiguous SRG matches where multiple old or new requirements share
876
+ * the same tags.gtitle. Uses a composite score of CCI Jaccard (70%) and
877
+ * token Jaccard on titles (30%) to pick the best match.
878
+ *
879
+ * Only activates for gtitle groups with multiple candidates (>1 old or >1 new).
880
+ * 1:1 matches are left for srgDeterministic (Tier 1).
881
+ */
882
+ function createSrgCciTiebreakStrategy() {
883
+ return {
884
+ name: "srgCciTiebreak",
885
+ match(oldReqs, newReqs) {
886
+ const oldGtitleMap = /* @__PURE__ */ new Map();
887
+ for (let i = 0; i < oldReqs.length; i++) {
888
+ const g = extractGtitle(oldReqs[i]);
889
+ if (g) {
890
+ const list = oldGtitleMap.get(g);
891
+ if (list) list.push(i);
892
+ else oldGtitleMap.set(g, [i]);
893
+ }
894
+ }
895
+ const newGtitleMap = /* @__PURE__ */ new Map();
896
+ for (let i = 0; i < newReqs.length; i++) {
897
+ const g = extractGtitle(newReqs[i]);
898
+ if (g) {
899
+ const list = newGtitleMap.get(g);
900
+ if (list) list.push(i);
901
+ else newGtitleMap.set(g, [i]);
902
+ }
903
+ }
904
+ const matched = [];
905
+ const matchedOldIndices = /* @__PURE__ */ new Set();
906
+ const matchedNewIndices = /* @__PURE__ */ new Set();
907
+ for (const [gtitle, newIdxList] of newGtitleMap) {
908
+ const oldIdxList = oldGtitleMap.get(gtitle);
909
+ if (!oldIdxList || oldIdxList.length === 0) continue;
910
+ const nc = newIdxList.length;
911
+ const oc = oldIdxList.length;
912
+ if (nc === 1 && oc === 1) continue;
913
+ for (const ni of newIdxList) {
914
+ if (matchedNewIndices.has(ni)) continue;
915
+ const newCcis = extractCcis(newReqs[ni]);
916
+ const newTitle = safeTitle$1(newReqs[ni]);
917
+ const newCwes = extractCwes(newReqs[ni]);
918
+ let bestIdx = -1;
919
+ let bestComposite = -1;
920
+ let bestCci = 0;
921
+ let bestCwe = -1;
922
+ let bestIsUnclaimed = false;
923
+ for (const oi of oldIdxList) {
924
+ const oldCcis = extractCcis(oldReqs[oi]);
925
+ const oldTitle = safeTitle$1(oldReqs[oi]);
926
+ const oldCwes = extractCwes(oldReqs[oi]);
927
+ const cci = cciJaccard(newCcis, oldCcis);
928
+ const title = tokenJaccard(newTitle, oldTitle);
929
+ const cwe = cciJaccard(newCwes, oldCwes);
930
+ const composite = CCI_WEIGHT * cci + TITLE_WEIGHT * title;
931
+ const isUnclaimed = !matchedOldIndices.has(oi);
932
+ if (bestIdx === -1 || isUnclaimed && !bestIsUnclaimed || isUnclaimed === bestIsUnclaimed && composite > bestComposite || isUnclaimed === bestIsUnclaimed && composite === bestComposite && cwe > bestCwe) {
933
+ bestIdx = oi;
934
+ bestComposite = composite;
935
+ bestCci = cci;
936
+ bestCwe = cwe;
937
+ bestIsUnclaimed = isUnclaimed;
938
+ }
939
+ }
940
+ if (bestIdx >= 0) {
941
+ const relationship = matchedOldIndices.has(bestIdx) ? "related" : "primary";
942
+ matched.push({
943
+ oldReq: oldReqs[bestIdx],
944
+ newReq: newReqs[ni],
945
+ strategy: "srgCciTiebreak",
946
+ confidence: Math.max(bestCci, 0),
947
+ relationship
948
+ });
949
+ matchedOldIndices.add(bestIdx);
950
+ matchedNewIndices.add(ni);
951
+ }
952
+ }
953
+ }
954
+ const unmatchedOld = [];
955
+ for (let i = 0; i < oldReqs.length; i++) if (!matchedOldIndices.has(i)) unmatchedOld.push(oldReqs[i]);
956
+ const unmatchedNew = [];
957
+ for (let i = 0; i < newReqs.length; i++) if (!matchedNewIndices.has(i)) unmatchedNew.push(newReqs[i]);
958
+ return {
959
+ matched,
960
+ unmatchedOld,
961
+ unmatchedNew
962
+ };
963
+ }
964
+ };
965
+ }
966
+ //#endregion
967
+ //#region src/matching/vendor-fuzzy-title.ts
968
+ /** Default threshold for accepting a Levenshtein match. */
969
+ const DEFAULT_ACCEPT_THRESHOLD = .45;
970
+ /** Modal verbs that mark the start of compliance statements. */
971
+ const COMPLIANCE_MODALS = new Set([
972
+ "must",
973
+ "will",
974
+ "shall",
975
+ "should",
976
+ "may",
977
+ "needs"
978
+ ]);
979
+ /**
980
+ * Compute the Levenshtein edit distance between two strings.
981
+ */
982
+ function levenshteinDistance(a, b) {
983
+ const m = a.length;
984
+ const n = b.length;
985
+ if (m === 0) return n;
986
+ if (n === 0) return m;
987
+ let prev = new Array(n + 1);
988
+ let curr = new Array(n + 1);
989
+ for (let j = 0; j <= n; j++) prev[j] = j;
990
+ for (let i = 1; i <= m; i++) {
991
+ curr[0] = i;
992
+ for (let j = 1; j <= n; j++) {
993
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
994
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
995
+ }
996
+ [prev, curr] = [curr, prev];
997
+ }
998
+ return prev[n];
999
+ }
1000
+ /**
1001
+ * Normalized Levenshtein distance (0.0 = identical, 1.0 = completely different).
1002
+ */
1003
+ function normalizedLevenshtein(a, b) {
1004
+ const maxLen = Math.max(a.length, b.length);
1005
+ if (maxLen === 0) return 0;
1006
+ return levenshteinDistance(a, b) / maxLen;
1007
+ }
1008
+ /**
1009
+ * Extract tokens before the first modal verb in a title.
1010
+ */
1011
+ function tokensBeforeModal(title) {
1012
+ const tokens = title.split(/\s+/).filter((t) => t.length > 0);
1013
+ const modalIdx = tokens.findIndex((t) => COMPLIANCE_MODALS.has(t.toLowerCase()));
1014
+ return modalIdx === -1 ? tokens : tokens.slice(0, modalIdx);
1015
+ }
1016
+ /**
1017
+ * Auto-detect the dominant vendor prefix in a corpus of titles.
1018
+ *
1019
+ * Tries progressively shorter leading-token prefixes. Returns the prefix
1020
+ * that appears in > 50% of titles, stopping before modal verbs.
1021
+ * Returns '' when no prefix reaches the threshold.
1022
+ */
1023
+ function autoDetectPrefix(titles, threshold = .5) {
1024
+ if (titles.length === 0) return "";
1025
+ const leading = titles.map((t) => tokensBeforeModal(t));
1026
+ const maxLen = Math.max(...leading.map((l) => l.length));
1027
+ for (let n = maxLen; n > 0; n--) {
1028
+ const counts = /* @__PURE__ */ new Map();
1029
+ for (const l of leading) if (l.length >= n) {
1030
+ const key = l.slice(0, n).join(" ");
1031
+ counts.set(key, (counts.get(key) ?? 0) + 1);
1032
+ }
1033
+ let bestKey = "";
1034
+ let bestCount = 0;
1035
+ for (const [key, count] of counts) if (count > bestCount) {
1036
+ bestKey = key;
1037
+ bestCount = count;
1038
+ }
1039
+ if (bestCount / titles.length > threshold) return bestKey;
1040
+ }
1041
+ return "";
1042
+ }
1043
+ /**
1044
+ * Strip a detected vendor prefix from a title.
1045
+ */
1046
+ function normalizeTitle(title, prefix) {
1047
+ if (!prefix) return title;
1048
+ if (title === prefix) return "";
1049
+ if (title.startsWith(prefix + " ")) return title.slice(prefix.length + 1);
1050
+ return title;
1051
+ }
1052
+ /**
1053
+ * Extract title string from a requirement.
1054
+ */
1055
+ function safeTitle(req) {
1056
+ const title = req["title"];
1057
+ return typeof title === "string" ? title : "";
1058
+ }
1059
+ /**
1060
+ * Create a vendor fuzzy title matching strategy (Tier 3 of the delta pipeline).
1061
+ *
1062
+ * Cross-vendor matching with auto-detected vendor prefix stripping.
1063
+ * Uses normalized Levenshtein distance to compare titles after removing
1064
+ * the dominant vendor prefix (e.g., "RHEL 9" or "Amazon Linux 2023").
1065
+ *
1066
+ * Accepts matches below threshold 0.45 (confidence = 1.0 - distance).
1067
+ * Greedy best-match.
1068
+ *
1069
+ * @param acceptThreshold Max normalized Levenshtein distance to accept (default: 0.45)
1070
+ */
1071
+ function createVendorFuzzyTitleStrategy(acceptThreshold) {
1072
+ const threshold = acceptThreshold ?? DEFAULT_ACCEPT_THRESHOLD;
1073
+ return {
1074
+ name: "vendorFuzzyTitle",
1075
+ match(oldReqs, newReqs) {
1076
+ const oldTitles = oldReqs.map(safeTitle).filter((t) => t.length > 0);
1077
+ const newTitles = newReqs.map(safeTitle).filter((t) => t.length > 0);
1078
+ const oldPrefix = autoDetectPrefix(oldTitles);
1079
+ const newPrefix = autoDetectPrefix(newTitles);
1080
+ const oldNorm = [];
1081
+ for (let i = 0; i < oldReqs.length; i++) {
1082
+ const t = safeTitle(oldReqs[i]);
1083
+ if (t) oldNorm.push({
1084
+ idx: i,
1085
+ normalized: normalizeTitle(t, oldPrefix)
1086
+ });
1087
+ }
1088
+ const newNorm = [];
1089
+ for (let i = 0; i < newReqs.length; i++) {
1090
+ const t = safeTitle(newReqs[i]);
1091
+ if (t) newNorm.push({
1092
+ idx: i,
1093
+ normalized: normalizeTitle(t, newPrefix)
1094
+ });
1095
+ }
1096
+ const candidates = [];
1097
+ for (const old of oldNorm) for (const nw of newNorm) {
1098
+ if (!old.normalized || !nw.normalized) continue;
1099
+ const dist = normalizedLevenshtein(old.normalized, nw.normalized);
1100
+ if (dist < threshold) candidates.push({
1101
+ oldIdx: old.idx,
1102
+ newIdx: nw.idx,
1103
+ distance: dist
1104
+ });
1105
+ }
1106
+ candidates.sort((a, b) => a.distance - b.distance);
1107
+ const matched = [];
1108
+ const matchedOldIndices = /* @__PURE__ */ new Set();
1109
+ const matchedNewIndices = /* @__PURE__ */ new Set();
1110
+ for (const c of candidates) {
1111
+ if (matchedOldIndices.has(c.oldIdx) || matchedNewIndices.has(c.newIdx)) continue;
1112
+ matched.push({
1113
+ oldReq: oldReqs[c.oldIdx],
1114
+ newReq: newReqs[c.newIdx],
1115
+ strategy: "vendorFuzzyTitle",
1116
+ confidence: 1 - c.distance,
1117
+ relationship: "primary"
1118
+ });
1119
+ matchedOldIndices.add(c.oldIdx);
1120
+ matchedNewIndices.add(c.newIdx);
1121
+ }
1122
+ const unmatchedOld = [];
1123
+ for (let i = 0; i < oldReqs.length; i++) if (!matchedOldIndices.has(i)) unmatchedOld.push(oldReqs[i]);
1124
+ const unmatchedNew = [];
1125
+ for (let i = 0; i < newReqs.length; i++) if (!matchedNewIndices.has(i)) unmatchedNew.push(newReqs[i]);
1126
+ return {
1127
+ matched,
1128
+ unmatchedOld,
1129
+ unmatchedNew
1130
+ };
1131
+ }
1132
+ };
1133
+ }
1134
+ //#endregion
651
1135
  //#region src/matching/index.ts
652
1136
  /**
653
1137
  * Create a strategy instance by name, using the provided options for
@@ -659,6 +1143,9 @@ function createStrategy(name, options) {
659
1143
  case "mappedId": return createMappedIdStrategy(options.mappingTable ?? {});
660
1144
  case "cciMatch": return createCciMatchStrategy();
661
1145
  case "fuzzyTitle": return createFuzzyTitleStrategy(options.minConfidence);
1146
+ case "srgDeterministic": return createSrgDeterministicStrategy();
1147
+ case "srgCciTiebreak": return createSrgCciTiebreakStrategy();
1148
+ case "vendorFuzzyTitle": return createVendorFuzzyTitleStrategy(options.minConfidence !== void 0 && options.minConfidence > 0 ? 1 - options.minConfidence : void 0);
662
1149
  default: throw new Error(`Unknown matching strategy: '${name}'`);
663
1150
  }
664
1151
  }
@@ -1168,13 +1655,26 @@ function comparePair(oldNormalized, newNormalized, oldTimestamp, newTimestamp, t
1168
1655
  requirementDiffs
1169
1656
  };
1170
1657
  }
1658
+ /** Field names handled by dedicated CVE-ecosystem diff handlers (not generic JSON compare). */
1659
+ const CVE_ECOSYSTEM_FIELDS = new Set([
1660
+ "cvss",
1661
+ "epss",
1662
+ "kev",
1663
+ "cwe",
1664
+ "affectedPackages"
1665
+ ]);
1171
1666
  /**
1172
1667
  * Compute field-level changes for tracked fields between two requirements.
1173
1668
  * Uses JSON Patch-like op/path format.
1669
+ *
1670
+ * Always runs the CVE-ecosystem handlers (cvss, epss, kev, cwe, affectedPackages)
1671
+ * in addition to whatever scalar fields are in trackedFields, since these
1672
+ * structured types need richer diff than generic JSON comparison provides.
1174
1673
  */
1175
1674
  function computeFieldChanges(oldReq, newReq, trackedFields) {
1176
1675
  const changes = [];
1177
1676
  for (const field of trackedFields) {
1677
+ if (CVE_ECOSYSTEM_FIELDS.has(field)) continue;
1178
1678
  const oldVal = oldReq[field];
1179
1679
  const newVal = newReq[field];
1180
1680
  if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) if (oldVal === void 0 && newVal !== void 0) changes.push({
@@ -1194,6 +1694,367 @@ function computeFieldChanges(oldReq, newReq, trackedFields) {
1194
1694
  newValue: newVal
1195
1695
  });
1196
1696
  }
1697
+ changes.push(...diffCvssArray(oldReq["cvss"], newReq["cvss"]));
1698
+ changes.push(...diffEpss(oldReq["epss"], newReq["epss"]));
1699
+ changes.push(...diffKev(oldReq["kev"], newReq["kev"]));
1700
+ changes.push(...diffCweSet(oldReq["cwe"], newReq["cwe"]));
1701
+ changes.push(...diffAffectedPackages(oldReq["affectedPackages"], newReq["affectedPackages"]));
1702
+ return changes;
1703
+ }
1704
+ const CVSS_VECTOR_FIELDS = [
1705
+ "baseVector",
1706
+ "threatVector",
1707
+ "environmentalVector",
1708
+ "supplementalVector"
1709
+ ];
1710
+ /**
1711
+ * Parse a CVSS vector string into a map of metric-code → value.
1712
+ *
1713
+ * Accepts both v3.x ("CVSS:3.1/AV:N/AC:L/...") and v4.0 ("CVSS:4.0/AV:N/...")
1714
+ * shapes, plus partial vectors that omit the version prefix (e.g., the
1715
+ * `threatVector` field is conventionally just the threat metrics).
1716
+ *
1717
+ * Returns `null` for inputs that don't look like CVSS vectors so callers
1718
+ * can fall back to a scalar string compare.
1719
+ *
1720
+ * Delegates to `parseCvssVector` from `@mitre/hdf-utilities` but adapts the
1721
+ * return shape: returns null for empty/unparseable inputs so call sites can
1722
+ * fall back to scalar string compare.
1723
+ */
1724
+ function parseCvssVector$1(vec) {
1725
+ if (typeof vec !== "string" || vec.length === 0) return null;
1726
+ const parsed = parseCvssVector(vec);
1727
+ if (parsed.metrics.size === 0) return null;
1728
+ return {
1729
+ version: parsed.version === "unknown" ? void 0 : parsed.version,
1730
+ metrics: parsed.metrics
1731
+ };
1732
+ }
1733
+ /**
1734
+ * Diff a single pair of cvss entries (same source). Emits per-field changes
1735
+ * including decomposed per-metric vector diffs.
1736
+ */
1737
+ function diffCvssEntry(source, oldEntry, newEntry) {
1738
+ const changes = [];
1739
+ const allKeys = new Set([...Object.keys(oldEntry), ...Object.keys(newEntry)]);
1740
+ for (const key of allKeys) {
1741
+ if (key === "source") continue;
1742
+ const oldVal = oldEntry[key];
1743
+ const newVal = newEntry[key];
1744
+ if (JSON.stringify(oldVal) === JSON.stringify(newVal)) continue;
1745
+ if (CVSS_VECTOR_FIELDS.includes(key)) {
1746
+ if (typeof oldVal === "string" && typeof newVal === "string") {
1747
+ const oldParsed = parseCvssVector$1(oldVal);
1748
+ const newParsed = parseCvssVector$1(newVal);
1749
+ if (oldParsed && newParsed) {
1750
+ const metricKeys = new Set([...oldParsed.metrics.keys(), ...newParsed.metrics.keys()]);
1751
+ for (const m of metricKeys) {
1752
+ const o = oldParsed.metrics.get(m);
1753
+ const n = newParsed.metrics.get(m);
1754
+ if (o === n) continue;
1755
+ if (o === void 0 && n !== void 0) changes.push({
1756
+ op: "add",
1757
+ path: `cvss[${source}].${key}.${m}`,
1758
+ newValue: n
1759
+ });
1760
+ else if (o !== void 0 && n === void 0) changes.push({
1761
+ op: "remove",
1762
+ path: `cvss[${source}].${key}.${m}`,
1763
+ oldValue: o
1764
+ });
1765
+ else changes.push({
1766
+ op: "replace",
1767
+ path: `cvss[${source}].${key}.${m}`,
1768
+ oldValue: o,
1769
+ newValue: n
1770
+ });
1771
+ }
1772
+ continue;
1773
+ }
1774
+ } else if (oldVal === void 0 && typeof newVal === "string") {
1775
+ const parsed = parseCvssVector$1(newVal);
1776
+ if (parsed) {
1777
+ for (const [m, v] of parsed.metrics) changes.push({
1778
+ op: "add",
1779
+ path: `cvss[${source}].${key}.${m}`,
1780
+ newValue: v
1781
+ });
1782
+ continue;
1783
+ }
1784
+ } else if (typeof oldVal === "string" && newVal === void 0) {
1785
+ const parsed = parseCvssVector$1(oldVal);
1786
+ if (parsed) {
1787
+ for (const [m, v] of parsed.metrics) changes.push({
1788
+ op: "remove",
1789
+ path: `cvss[${source}].${key}.${m}`,
1790
+ oldValue: v
1791
+ });
1792
+ continue;
1793
+ }
1794
+ }
1795
+ }
1796
+ if (oldVal === void 0) changes.push({
1797
+ op: "add",
1798
+ path: `cvss[${source}].${key}`,
1799
+ newValue: newVal
1800
+ });
1801
+ else if (newVal === void 0) changes.push({
1802
+ op: "remove",
1803
+ path: `cvss[${source}].${key}`,
1804
+ oldValue: oldVal
1805
+ });
1806
+ else changes.push({
1807
+ op: "replace",
1808
+ path: `cvss[${source}].${key}`,
1809
+ oldValue: oldVal,
1810
+ newValue: newVal
1811
+ });
1812
+ }
1813
+ return changes;
1814
+ }
1815
+ /**
1816
+ * Diff old vs new cvss[] arrays. Entries are matched by `source` (CVE ID
1817
+ * or scoring authority); added/removed entries emit whole-entry add/remove
1818
+ * records; matched pairs are decomposed via diffCvssEntry.
1819
+ */
1820
+ function diffCvssArray(oldField, newField) {
1821
+ const oldArr = Array.isArray(oldField) ? oldField : [];
1822
+ const newArr = Array.isArray(newField) ? newField : [];
1823
+ if (oldArr.length === 0 && newArr.length === 0) return [];
1824
+ const oldBySource = /* @__PURE__ */ new Map();
1825
+ for (const e of oldArr) if (typeof e?.source === "string") oldBySource.set(e.source, e);
1826
+ const newBySource = /* @__PURE__ */ new Map();
1827
+ for (const e of newArr) if (typeof e?.source === "string") newBySource.set(e.source, e);
1828
+ const changes = [];
1829
+ const allSources = new Set([...oldBySource.keys(), ...newBySource.keys()]);
1830
+ for (const source of allSources) {
1831
+ const o = oldBySource.get(source);
1832
+ const n = newBySource.get(source);
1833
+ if (o && !n) changes.push({
1834
+ op: "remove",
1835
+ path: `cvss[${source}]`,
1836
+ oldValue: o
1837
+ });
1838
+ else if (!o && n) changes.push({
1839
+ op: "add",
1840
+ path: `cvss[${source}]`,
1841
+ newValue: n
1842
+ });
1843
+ else if (o && n) changes.push(...diffCvssEntry(source, o, n));
1844
+ }
1845
+ return changes;
1846
+ }
1847
+ /** Diff the optional `epss` object (score, percentile, date). */
1848
+ function diffEpss(oldField, newField) {
1849
+ if (oldField === void 0 && newField === void 0) return [];
1850
+ if (oldField === void 0) return [{
1851
+ op: "add",
1852
+ path: "epss",
1853
+ newValue: newField
1854
+ }];
1855
+ if (newField === void 0) return [{
1856
+ op: "remove",
1857
+ path: "epss",
1858
+ oldValue: oldField
1859
+ }];
1860
+ if (typeof oldField !== "object" || typeof newField !== "object" || oldField === null || newField === null) {
1861
+ if (JSON.stringify(oldField) === JSON.stringify(newField)) return [];
1862
+ return [{
1863
+ op: "replace",
1864
+ path: "epss",
1865
+ oldValue: oldField,
1866
+ newValue: newField
1867
+ }];
1868
+ }
1869
+ const oldObj = oldField;
1870
+ const newObj = newField;
1871
+ const changes = [];
1872
+ const allKeys = new Set([...Object.keys(oldObj), ...Object.keys(newObj)]);
1873
+ for (const key of allKeys) {
1874
+ const o = oldObj[key];
1875
+ const n = newObj[key];
1876
+ if (JSON.stringify(o) === JSON.stringify(n)) continue;
1877
+ if (o === void 0) changes.push({
1878
+ op: "add",
1879
+ path: `epss.${key}`,
1880
+ newValue: n
1881
+ });
1882
+ else if (n === void 0) changes.push({
1883
+ op: "remove",
1884
+ path: `epss.${key}`,
1885
+ oldValue: o
1886
+ });
1887
+ else changes.push({
1888
+ op: "replace",
1889
+ path: `epss.${key}`,
1890
+ oldValue: o,
1891
+ newValue: n
1892
+ });
1893
+ }
1894
+ return changes;
1895
+ }
1896
+ /**
1897
+ * Diff the optional `kev` object. Flips of `inKev` from false→true (or
1898
+ * fresh add with `inKev: true`) get an annotated `message` field calling
1899
+ * out the CISA KEV catalog inclusion.
1900
+ */
1901
+ function diffKev(oldField, newField) {
1902
+ if (oldField === void 0 && newField === void 0) return [];
1903
+ if (oldField === void 0) {
1904
+ const change = {
1905
+ op: "add",
1906
+ path: "kev",
1907
+ newValue: newField
1908
+ };
1909
+ const nObj = newField;
1910
+ if (nObj && nObj["inKev"] === true) change.message = `Newly added to CISA KEV catalog as of ${typeof nObj["dateAdded"] === "string" ? nObj["dateAdded"] : "unknown date"}`;
1911
+ return [change];
1912
+ }
1913
+ if (newField === void 0) return [{
1914
+ op: "remove",
1915
+ path: "kev",
1916
+ oldValue: oldField
1917
+ }];
1918
+ if (typeof oldField !== "object" || typeof newField !== "object" || oldField === null || newField === null) {
1919
+ if (JSON.stringify(oldField) === JSON.stringify(newField)) return [];
1920
+ return [{
1921
+ op: "replace",
1922
+ path: "kev",
1923
+ oldValue: oldField,
1924
+ newValue: newField
1925
+ }];
1926
+ }
1927
+ const oldObj = oldField;
1928
+ const newObj = newField;
1929
+ const changes = [];
1930
+ const allKeys = new Set([...Object.keys(oldObj), ...Object.keys(newObj)]);
1931
+ for (const key of allKeys) {
1932
+ const o = oldObj[key];
1933
+ const n = newObj[key];
1934
+ if (JSON.stringify(o) === JSON.stringify(n)) continue;
1935
+ let change;
1936
+ if (o === void 0) change = {
1937
+ op: "add",
1938
+ path: `kev.${key}`,
1939
+ newValue: n
1940
+ };
1941
+ else if (n === void 0) change = {
1942
+ op: "remove",
1943
+ path: `kev.${key}`,
1944
+ oldValue: o
1945
+ };
1946
+ else change = {
1947
+ op: "replace",
1948
+ path: `kev.${key}`,
1949
+ oldValue: o,
1950
+ newValue: n
1951
+ };
1952
+ if (key === "inKev" && o === false && n === true) {
1953
+ const dateAdded = typeof newObj["dateAdded"] === "string" ? newObj["dateAdded"] : "unknown date";
1954
+ change.message = `Newly added to CISA KEV catalog as of ${dateAdded}`;
1955
+ }
1956
+ changes.push(change);
1957
+ }
1958
+ return changes;
1959
+ }
1960
+ /** Diff the `cwe[]` field as a set — order does not matter. */
1961
+ function diffCweSet(oldField, newField) {
1962
+ const toSet = (v) => {
1963
+ if (!Array.isArray(v)) return /* @__PURE__ */ new Set();
1964
+ return new Set(v.filter((x) => typeof x === "string"));
1965
+ };
1966
+ const oldSet = toSet(oldField);
1967
+ const newSet = toSet(newField);
1968
+ if (oldSet.size === 0 && newSet.size === 0) return [];
1969
+ const changes = [];
1970
+ for (const c of newSet) if (!oldSet.has(c)) changes.push({
1971
+ op: "add",
1972
+ path: "cwe",
1973
+ newValue: c
1974
+ });
1975
+ for (const c of oldSet) if (!newSet.has(c)) changes.push({
1976
+ op: "remove",
1977
+ path: "cwe",
1978
+ oldValue: c
1979
+ });
1980
+ return changes;
1981
+ }
1982
+ /** Build the natural key for an AffectedPackage: `name@version`. */
1983
+ function packageKey(pkg) {
1984
+ const name = pkg?.name;
1985
+ const version = pkg?.version;
1986
+ if (typeof name !== "string" || name.length === 0) return null;
1987
+ if (typeof version !== "string" || version.length === 0) return null;
1988
+ return `${name}@${version}`;
1989
+ }
1990
+ /**
1991
+ * Diff a single pair of affectedPackages entries (matched by name+version).
1992
+ * Emits per-field changes for cpe / purl / fixedInVersion and any other
1993
+ * non-key fields that differ.
1994
+ */
1995
+ function diffAffectedPackageEntry(key, oldPkg, newPkg) {
1996
+ const changes = [];
1997
+ const allKeys = new Set([...Object.keys(oldPkg), ...Object.keys(newPkg)]);
1998
+ for (const k of allKeys) {
1999
+ if (k === "name" || k === "version") continue;
2000
+ const o = oldPkg[k];
2001
+ const n = newPkg[k];
2002
+ if (JSON.stringify(o) === JSON.stringify(n)) continue;
2003
+ if (o === void 0) changes.push({
2004
+ op: "add",
2005
+ path: `affectedPackages[${key}].${k}`,
2006
+ newValue: n
2007
+ });
2008
+ else if (n === void 0) changes.push({
2009
+ op: "remove",
2010
+ path: `affectedPackages[${key}].${k}`,
2011
+ oldValue: o
2012
+ });
2013
+ else changes.push({
2014
+ op: "replace",
2015
+ path: `affectedPackages[${key}].${k}`,
2016
+ oldValue: o,
2017
+ newValue: n
2018
+ });
2019
+ }
2020
+ return changes;
2021
+ }
2022
+ /**
2023
+ * Diff old vs new affectedPackages[] arrays. Entries are matched by the
2024
+ * `name@version` tuple; added/removed packages emit whole-entry records;
2025
+ * matched pairs are decomposed via diffAffectedPackageEntry.
2026
+ */
2027
+ function diffAffectedPackages(oldField, newField) {
2028
+ const oldArr = Array.isArray(oldField) ? oldField : [];
2029
+ const newArr = Array.isArray(newField) ? newField : [];
2030
+ if (oldArr.length === 0 && newArr.length === 0) return [];
2031
+ const oldByKey = /* @__PURE__ */ new Map();
2032
+ for (const p of oldArr) {
2033
+ const k = packageKey(p);
2034
+ if (k) oldByKey.set(k, p);
2035
+ }
2036
+ const newByKey = /* @__PURE__ */ new Map();
2037
+ for (const p of newArr) {
2038
+ const k = packageKey(p);
2039
+ if (k) newByKey.set(k, p);
2040
+ }
2041
+ const changes = [];
2042
+ const allKeys = new Set([...oldByKey.keys(), ...newByKey.keys()]);
2043
+ for (const key of allKeys) {
2044
+ const o = oldByKey.get(key);
2045
+ const n = newByKey.get(key);
2046
+ if (o && !n) changes.push({
2047
+ op: "remove",
2048
+ path: `affectedPackages[${key}]`,
2049
+ oldValue: o
2050
+ });
2051
+ else if (!o && n) changes.push({
2052
+ op: "add",
2053
+ path: `affectedPackages[${key}]`,
2054
+ newValue: n
2055
+ });
2056
+ else if (o && n) changes.push(...diffAffectedPackageEntry(key, o, n));
2057
+ }
1197
2058
  return changes;
1198
2059
  }
1199
2060
  /**
@@ -1670,7 +2531,7 @@ function stripSnapshots(req) {
1670
2531
  return rest;
1671
2532
  }
1672
2533
  /**
1673
- * Render an HdfComparison as a JSON string.
2534
+ * Render an HDFComparison as a JSON string.
1674
2535
  *
1675
2536
  * - `detail: 'summary'` -- only `{ formatVersion, comparisonMode, summary }`
1676
2537
  * - `detail: 'control'` -- full document but `before`/`after` stripped from requirementDiffs
@@ -1784,7 +2645,7 @@ function renderStateSection(state, diffs, detail) {
1784
2645
  return lines.join("\n");
1785
2646
  }
1786
2647
  /**
1787
- * Render an HdfComparison as a Markdown string.
2648
+ * Render an HDFComparison as a Markdown string.
1788
2649
  *
1789
2650
  * - `detail: 'summary'` -- summary table only
1790
2651
  * - `detail: 'control'` -- summary + per-requirement tables by state
@@ -1920,7 +2781,7 @@ function buildHeaderLine(comparison, useColor) {
1920
2781
  return header;
1921
2782
  }
1922
2783
  /**
1923
- * Render an HdfComparison for terminal display with optional ANSI colors.
2784
+ * Render an HDFComparison for terminal display with optional ANSI colors.
1924
2785
  *
1925
2786
  * - `detail: 'summary'` -- just the summary line
1926
2787
  * - `detail: 'control'` -- requirement list + summary (excludes unchanged)
@@ -1973,7 +2834,7 @@ function formatFieldChanges(req) {
1973
2834
  }).join("; ");
1974
2835
  }
1975
2836
  /**
1976
- * Render an HdfComparison as a CSV string.
2837
+ * Render an HDFComparison as a CSV string.
1977
2838
  *
1978
2839
  * One row per requirement. Columns:
1979
2840
  * - ID, Title, State, Old Status, New Status, Impact (Old), Impact (New), Change Reasons
@@ -2019,7 +2880,7 @@ function renderCsv(comparison, options) {
2019
2880
  /**
2020
2881
  * Convenience function to render a comparison in any supported format.
2021
2882
  *
2022
- * @param comparison - The HdfComparison document to render
2883
+ * @param comparison - The HDFComparison document to render
2023
2884
  * @param format - Output format: 'json', 'markdown', 'terminal', or 'csv'
2024
2885
  * @param options - Rendering options (detail level, filters, color)
2025
2886
  * @returns The rendered string
@@ -2033,6 +2894,6 @@ function render(comparison, format, options) {
2033
2894
  }
2034
2895
  }
2035
2896
  //#endregion
2036
- export { EXIT_DETAILED_BASELINE_CHANGED, EXIT_DETAILED_DRIFT_ONLY, EXIT_DETAILED_ERROR, EXIT_DETAILED_FIXES_ONLY, EXIT_DETAILED_IDENTICAL, EXIT_DETAILED_MIXED, EXIT_DETAILED_REGRESSIONS_ONLY, EXIT_DIFFERENCES, EXIT_ERROR, EXIT_IDENTICAL, classifyChangeReasons, classifyDiffStatus, computeDetailedExitCode, computeEffectiveStatus, computeExitCode, computeSummary, createCciMatchStrategy, createExactIdStrategy, createFuzzyTitleStrategy, createMappedIdStrategy, diffBaselines, diffHdf, diffSboms, diffSystems, isV1Format, jaccardSimilarity, matchRequirements, normalizeToV2, render, renderCsv, renderJson, renderMarkdown, renderTerminal, tokenize, validateComparison };
2897
+ export { EXIT_DETAILED_BASELINE_CHANGED, EXIT_DETAILED_DRIFT_ONLY, EXIT_DETAILED_ERROR, EXIT_DETAILED_FIXES_ONLY, EXIT_DETAILED_IDENTICAL, EXIT_DETAILED_MIXED, EXIT_DETAILED_REGRESSIONS_ONLY, EXIT_DIFFERENCES, EXIT_ERROR, EXIT_IDENTICAL, classifyChangeReasons, classifyDiffStatus, computeDetailedExitCode, computeEffectiveStatus, computeExitCode, computeSummary, createCciMatchStrategy, createExactIdStrategy, createFuzzyTitleStrategy, createMappedIdStrategy, createSrgCciTiebreakStrategy, createSrgDeterministicStrategy, createVendorFuzzyTitleStrategy, diffBaselines, diffHdf, diffSboms, diffSystems, isV1Format, jaccardSimilarity, levenshteinDistance, matchRequirements, normalizeToV2, normalizedLevenshtein, render, renderCsv, renderJson, renderMarkdown, renderTerminal, tokenize, validateComparison };
2037
2898
 
2038
2899
  //# sourceMappingURL=index.js.map