multivendor-cli-configurator / scripts /fix_coverage_gaps.py
gesh75's picture
Upload folder using huggingface_hub
1bd64c1 verified
Raw History Blame Contribute Delete
6.1 kB
#!/usr/bin/env python3
"""
fix_coverage_gaps.py — corpus repair pass for identified coverage gaps.
Idempotent transforms on commands.json:
1. Retag NX-OS / IOS-XE rows mislabeled as ios
2. Promote VXLAN / EVPN keyword hits into dedicated categories
3. Collapse duplicate taxonomy (STP→Spanning-Tree, VRRP→HA)
4. Unescape mangled placeholders (<name\\> → <name>)
5. Shorten over-long titles derived from cmd when title is a dump
Writes commands.json in-place (compact). Prints a change summary.
"""
from __future__ import annotations
import json
import pathlib
import re
import sys
ROOT = pathlib.Path(__file__).resolve().parent.parent
OUT = ROOT / "commands.json"
NXOS_HINT = re.compile(
r"\b(?:"
r"feature\s+(?:nxapi|bash-shell|nv\s+overlay|vn-segment|vn-segment-vlan-based|interface-vlan|lacp|vpc|bgp|ospf|pim|nv\s+overlay\s+evpn)|"
r"nve\s+\d+|vlan\s+vn-segment|evpn\s+multisite|vpc\s+domain|vdc\s+|fabric\s+forwarding|"
r"member\s+vni|peer-keepalive|peer-gateway|auto-recovery|"
r"nv\s+overlay\s+evpn|route-target\s+both\s+auto\s+evpn"
r")\b",
re.I,
)
IOSXE_HINT = re.compile(
r"\b(?:"
r"guestshell|netconf-yang|restconf|telemetry\s+ietf|"
r"platform\s+software|sdwan|iosxe|ios-xe|"
r"yang-push|encode-kvgpb|gnxi"
r")\b",
re.I,
)
VXLAN_HINT = re.compile(
r"\b(?:"
r"vxlan|nve\b|vni\b|vn-segment|overlay\s+interface|"
r"interface\s+nve|source-interface\s+nve|member\s+vni|"
r"vxlan\s+flood|vxlan\s+vlan|vxlan\s+vni"
r")\b",
re.I,
)
EVPN_HINT = re.compile(
r"\b(?:"
r"evpn\b|l2vpn\s+evpn|address-family\s+l2vpn\s+evpn|"
r"route-type\s+[25]\b|type-5|type-2|"
r"rd\s+auto|route-target\s+both\s+auto\s+evpn|"
r"advertise\s+l2vpn\s+evpn|retain\s+route-target|"
r"mac-vrf|ip-vrf|vtep"
r")\b",
re.I,
)
# Promote from generic buckets only — avoid stealing BGP/OSPF/etc.
GENERIC_CATS = {
"Interfaces",
"System",
"Protocols",
"VLAN",
"Routing",
"Troubleshooting",
"Misc",
"Security",
}
STP_HINT = re.compile(
r"\b(?:"
r"spanning[- ]?tree|rstp|mstp|pvst|rapid-pvst|"
r"stp\s+(?:enable|mode|priority|root)|"
r"bridge\s+stp|enable\s+stpd"
r")\b",
re.I,
)
BFD_HINT = re.compile(
r"\b(?:"
r"bfd\s+(?:enable|interval|multiplier|neighbor|echo)|"
r"ip\s+ospf\s+bfd|isis\s+bfd|bgp\s+neighbor\s+\S+\s+fall-over\s+bfd|"
r"protocol\s+bfd|bfd\s+all-interfaces"
r")\b",
re.I,
)
LACP_HINT = re.compile(
r"\b(?:"
r"lacp\b|port[- ]?channel|etherchannel|"
r"channel-group\s+\d+|lag\s+\d+|share-group|"
r"interface\s+port-channel|bonding\s+bond|"
r"aggregate-id|vpc\s+\d+"
r")\b",
re.I,
)
ESC_PH = re.compile(r"<([^<>\\]+)\\>")
def norm_title_from_cmd(cmd: str, limit: int = 72) -> str:
first = (cmd or "").strip().split("\n", 1)[0].strip()
first = re.sub(r"\s+", " ", first)
if len(first) <= limit:
return first
return first[: limit - 1].rstrip() + "…"
def blob(d: dict) -> str:
return " ".join(
[
d.get("cmd", ""),
d.get("title", ""),
d.get("desc", ""),
d.get("cat", ""),
]
)
def main() -> None:
data = json.load(open(OUT))
stats = {
"nxos_retag": 0,
"iosxe_retag": 0,
"vxlan_cat": 0,
"evpn_cat": 0,
"stp_cat": 0,
"bfd_cat": 0,
"lacp_cat": 0,
"stp_collapse": 0,
"vrrp_collapse": 0,
"placeholder_fix": 0,
"title_shorten": 0,
}
for d in data:
# 1) OS retag (Cisco only)
if d.get("vendor") == "Cisco" and d.get("os") == "ios":
text = blob(d)
if NXOS_HINT.search(text):
d["os"] = "nxos"
if d.get("role") == "router":
d["role"] = "switch"
stats["nxos_retag"] += 1
elif IOSXE_HINT.search(text):
d["os"] = "iosxe"
stats["iosxe_retag"] += 1
# 2) Overlay + L2/HA category promotion
cat = d.get("cat") or ""
text = blob(d)
if cat not in ("VXLAN", "EVPN"):
if EVPN_HINT.search(text):
d["cat"] = "EVPN"
stats["evpn_cat"] += 1
elif VXLAN_HINT.search(text):
d["cat"] = "VXLAN"
stats["vxlan_cat"] += 1
# Promote STP / BFD / LACP out of generic buckets only
cat = d.get("cat") or ""
if cat in GENERIC_CATS:
if STP_HINT.search(text):
d["cat"] = "Spanning-Tree"
stats["stp_cat"] += 1
elif BFD_HINT.search(text):
d["cat"] = "BFD"
stats["bfd_cat"] += 1
elif LACP_HINT.search(text):
d["cat"] = "EtherChannel"
stats["lacp_cat"] += 1
# 3) Taxonomy collapse
if cat == "STP" or d.get("cat") == "STP":
d["cat"] = "Spanning-Tree"
stats["stp_collapse"] += 1
if d.get("cat") == "VRRP":
d["cat"] = "HA"
stats["vrrp_collapse"] += 1
# 4) Placeholder unescape in cmd/title/desc
for field in ("cmd", "title", "desc"):
val = d.get(field)
if not val or "\\" not in val:
continue
fixed = ESC_PH.sub(r"<\1>", val)
# also strip stray backslash before >
fixed2 = fixed.replace("\\>", ">")
if fixed2 != val:
d[field] = fixed2
stats["placeholder_fix"] += 1
# 5) Shorten pathological titles (>90) using first cmd line
title = d.get("title") or ""
if len(title) > 90:
d["title"] = norm_title_from_cmd(d.get("cmd") or title)
stats["title_shorten"] += 1
if "--dry-run" in sys.argv:
print("[dry-run]", stats, "total", len(data))
return
with open(OUT, "w") as f:
json.dump(data, f, separators=(",", ":"))
print("fixed:", stats, "total:", len(data))
if __name__ == "__main__":
main()