forked from diegoecab/oci-deal-accelerator
initial version
This commit is contained in:
649
tools/feature_matrix_cli.py
Normal file
649
tools/feature_matrix_cli.py
Normal file
@@ -0,0 +1,649 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
OCI Deal Accelerator — ADB Feature Compatibility Matrix CLI
|
||||
|
||||
Query, compare, and update the ADB feature compatibility matrix from
|
||||
the command line.
|
||||
|
||||
Usage:
|
||||
python feature_matrix_cli.py check "auto scaling" adb_s 23ai
|
||||
python feature_matrix_cli.py compare adb_s exacs 23ai
|
||||
python feature_matrix_cli.py gaps adb_s 23ai
|
||||
python feature_matrix_cli.py update "Auto Scaling (OCPU)" adb_s 23ai --status GA --notes "Updated"
|
||||
python feature_matrix_cli.py export --format markdown
|
||||
python feature_matrix_cli.py export --format csv
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
from datetime import date
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
VALID_CONFIDENCE = {"validated", "observed", "reported", "inferred"}
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Constants
|
||||
# =============================================================================
|
||||
|
||||
MATRIX_PATH = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"..", "kb", "compatibility", "adb-feature-matrix.yaml",
|
||||
)
|
||||
|
||||
VALID_STATUSES = {"GA", "GA_CAVEAT", "PREVIEW", "LIMITED", "NOT_AVAIL", "BROKEN", "UNTESTED"}
|
||||
|
||||
# ANSI color codes for terminal output
|
||||
_COLORS = {
|
||||
"GA": "\033[32m", # green
|
||||
"GA_CAVEAT": "\033[33m", # yellow
|
||||
"PREVIEW": "\033[36m", # cyan
|
||||
"LIMITED": "\033[93m", # bright yellow
|
||||
"NOT_AVAIL": "\033[31m", # red
|
||||
"BROKEN": "\033[1;31m", # red bold
|
||||
"UNTESTED": "\033[90m", # gray
|
||||
}
|
||||
_RESET = "\033[0m"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Helpers
|
||||
# =============================================================================
|
||||
|
||||
def _color(status: str) -> str:
|
||||
"""Wrap a status string with the appropriate ANSI color."""
|
||||
code = _COLORS.get(status, "")
|
||||
return f"{code}{status}{_RESET}" if code else status
|
||||
|
||||
|
||||
def _supports_color() -> bool:
|
||||
"""Return True when stdout is a tty that likely supports ANSI escapes."""
|
||||
return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
|
||||
|
||||
|
||||
def _plain(status: str) -> str:
|
||||
"""Return the status without color codes."""
|
||||
return status
|
||||
|
||||
|
||||
def _get_color_fn():
|
||||
"""Return the appropriate formatter (colored or plain)."""
|
||||
return _color if _supports_color() else _plain
|
||||
|
||||
|
||||
def load_matrix(path: str) -> Dict[str, Any]:
|
||||
"""Load the YAML matrix file and return its contents as a dict."""
|
||||
resolved = os.path.realpath(path)
|
||||
if not os.path.isfile(resolved):
|
||||
print(f"Error: Matrix file not found at {resolved}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
with open(resolved, "r", encoding="utf-8") as fh:
|
||||
try:
|
||||
data = yaml.safe_load(fh)
|
||||
except yaml.YAMLError as exc:
|
||||
print(f"Error: Failed to parse YAML — {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if not data or "features" not in data:
|
||||
print("Error: Matrix file is missing 'features' key.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return data
|
||||
|
||||
|
||||
def save_matrix(path: str, data: Dict[str, Any]) -> None:
|
||||
"""Write the matrix dict back to YAML, preserving readability."""
|
||||
resolved = os.path.realpath(path)
|
||||
with open(resolved, "w", encoding="utf-8") as fh:
|
||||
yaml.dump(
|
||||
data,
|
||||
fh,
|
||||
default_flow_style=False,
|
||||
sort_keys=False,
|
||||
allow_unicode=True,
|
||||
width=120,
|
||||
)
|
||||
|
||||
|
||||
def get_version_ids(data: Dict[str, Any]) -> List[str]:
|
||||
"""Return ordered list of version ids from the matrix."""
|
||||
return [v["id"] for v in data.get("versions", [])]
|
||||
|
||||
|
||||
def get_deployment_ids(data: Dict[str, Any]) -> List[str]:
|
||||
"""Return ordered list of deployment type ids from the matrix."""
|
||||
return [d["id"] for d in data.get("deployment_types", [])]
|
||||
|
||||
|
||||
def get_deployment_name(data: Dict[str, Any], deploy_id: str) -> str:
|
||||
"""Look up the human-readable name for a deployment id."""
|
||||
for d in data.get("deployment_types", []):
|
||||
if d["id"] == deploy_id:
|
||||
return d.get("name", deploy_id)
|
||||
return deploy_id
|
||||
|
||||
|
||||
def validate_deployment(data: Dict[str, Any], deploy_id: str) -> None:
|
||||
"""Exit with an error if the deployment id is not in the matrix."""
|
||||
valid = get_deployment_ids(data)
|
||||
if deploy_id not in valid:
|
||||
print(
|
||||
f"Error: Unknown deployment type '{deploy_id}'. "
|
||||
f"Valid types: {', '.join(valid)}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def validate_version(data: Dict[str, Any], version: str) -> None:
|
||||
"""Exit with an error if the version is not in the matrix."""
|
||||
valid = get_version_ids(data)
|
||||
if version not in valid:
|
||||
print(
|
||||
f"Error: Unknown version '{version}'. Valid versions: {', '.join(valid)}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def find_features(data: Dict[str, Any], query: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Find features whose name matches *query* (case-insensitive, partial).
|
||||
Returns a list of matching feature dicts.
|
||||
"""
|
||||
query_lower = query.lower()
|
||||
return [
|
||||
f for f in data["features"]
|
||||
if query_lower in f["name"].lower()
|
||||
]
|
||||
|
||||
|
||||
def get_cell(feature: Dict[str, Any], deploy: str, version: str) -> Optional[Dict[str, Any]]:
|
||||
"""Return the matrix cell dict for a feature/deploy/version, or None."""
|
||||
return feature.get("matrix", {}).get(deploy, {}).get(version)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Library API — importable functions for programmatic use and testing
|
||||
# =============================================================================
|
||||
|
||||
def check(feature_name: str, deployment: str, version: str, matrix_path: str = MATRIX_PATH) -> Dict[str, Any]:
|
||||
"""Look up a feature/deployment/version and return {status, notes, feature_name}."""
|
||||
data = load_matrix(matrix_path)
|
||||
matches = find_features(data, feature_name)
|
||||
if not matches:
|
||||
raise LookupError(f"No features matching '{feature_name}'")
|
||||
if len(matches) > 1:
|
||||
raise LookupError(
|
||||
f"Ambiguous feature name '{feature_name}'. Matches: "
|
||||
+ ", ".join(m["name"] for m in matches)
|
||||
)
|
||||
feature = matches[0]
|
||||
cell = get_cell(feature, deployment, version)
|
||||
if cell is None:
|
||||
return {"status": "UNTESTED", "notes": "no entry in matrix", "feature_name": feature["name"]}
|
||||
return {
|
||||
"status": cell.get("status", "UNTESTED"),
|
||||
"notes": cell.get("notes", ""),
|
||||
"feature_name": feature["name"],
|
||||
}
|
||||
|
||||
|
||||
def compare(deploy1: str, deploy2: str, version: str, matrix_path: str = MATRIX_PATH) -> List[Dict[str, Any]]:
|
||||
"""Compare two deployment types. Returns list of dicts with name, status1, status2."""
|
||||
data = load_matrix(matrix_path)
|
||||
results = []
|
||||
for feature in data["features"]:
|
||||
cell1 = get_cell(feature, deploy1, version)
|
||||
cell2 = get_cell(feature, deploy2, version)
|
||||
results.append({
|
||||
"name": feature["name"],
|
||||
"status1": cell1["status"] if cell1 else "UNTESTED",
|
||||
"status2": cell2["status"] if cell2 else "UNTESTED",
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
def gaps(deployment: str, version: str, matrix_path: str = MATRIX_PATH) -> List[Dict[str, Any]]:
|
||||
"""List features that are NOT_AVAIL, BROKEN, or LIMITED."""
|
||||
data = load_matrix(matrix_path)
|
||||
gap_statuses = {"NOT_AVAIL", "BROKEN", "LIMITED"}
|
||||
results = []
|
||||
for feature in data["features"]:
|
||||
cell = get_cell(feature, deployment, version)
|
||||
if cell is None:
|
||||
continue
|
||||
status = cell.get("status", "UNTESTED")
|
||||
if status in gap_statuses:
|
||||
results.append({
|
||||
"name": feature["name"],
|
||||
"status": status,
|
||||
"notes": cell.get("notes", ""),
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
def export_markdown(output_path: str, matrix_path: str = MATRIX_PATH) -> None:
|
||||
"""Export the full matrix as a markdown table to a file."""
|
||||
data = load_matrix(matrix_path)
|
||||
versions = get_version_ids(data)
|
||||
deployments = get_deployment_ids(data)
|
||||
columns = [(d, v) for d in deployments for v in versions]
|
||||
col_labels = [f"{d}/{v}" for d, v in columns]
|
||||
features = data["features"]
|
||||
|
||||
feat_width = max((len(f["name"]) for f in features), default=7)
|
||||
feat_width = max(feat_width, len("Feature"))
|
||||
col_widths = [max(len(lbl), 10) for lbl in col_labels]
|
||||
|
||||
lines = []
|
||||
header = f"| {'Feature':<{feat_width}} |"
|
||||
for lbl, w in zip(col_labels, col_widths):
|
||||
header += f" {lbl:<{w}} |"
|
||||
lines.append(header)
|
||||
|
||||
sep = f"|{'-' * (feat_width + 2)}|"
|
||||
for w in col_widths:
|
||||
sep += f"{'-' * (w + 2)}|"
|
||||
lines.append(sep)
|
||||
|
||||
for feature in features:
|
||||
row = f"| {feature['name']:<{feat_width}} |"
|
||||
for (d, v), w in zip(columns, col_widths):
|
||||
cell = get_cell(feature, d, v)
|
||||
status = cell["status"] if cell else "-"
|
||||
row += f" {status:<{w}} |"
|
||||
lines.append(row)
|
||||
|
||||
with open(output_path, "w", encoding="utf-8") as fh:
|
||||
fh.write("\n".join(lines) + "\n")
|
||||
|
||||
|
||||
def export_csv(output_path: str, matrix_path: str = MATRIX_PATH) -> None:
|
||||
"""Export the full matrix as CSV to a file."""
|
||||
data = load_matrix(matrix_path)
|
||||
versions = get_version_ids(data)
|
||||
deployments = get_deployment_ids(data)
|
||||
columns = [(d, v) for d in deployments for v in versions]
|
||||
col_labels = [f"{d}/{v}" for d, v in columns]
|
||||
|
||||
with open(output_path, "w", encoding="utf-8", newline="") as fh:
|
||||
writer = csv.writer(fh)
|
||||
writer.writerow(["Feature", "Category"] + col_labels)
|
||||
for feature in data["features"]:
|
||||
row = [feature["name"], feature.get("category", "")]
|
||||
for d, v in columns:
|
||||
cell = get_cell(feature, d, v)
|
||||
row.append(cell["status"] if cell else "")
|
||||
writer.writerow(row)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Subcommands (CLI handlers)
|
||||
# =============================================================================
|
||||
|
||||
def cmd_check(args: argparse.Namespace) -> None:
|
||||
"""Look up a specific feature / deployment / version combo."""
|
||||
data = load_matrix(MATRIX_PATH)
|
||||
validate_deployment(data, args.deployment)
|
||||
validate_version(data, args.version)
|
||||
|
||||
matches = find_features(data, args.feature)
|
||||
|
||||
if not matches:
|
||||
print(f"No features matching '{args.feature}'.")
|
||||
sys.exit(1)
|
||||
|
||||
if len(matches) > 1:
|
||||
print(f"Ambiguous feature name '{args.feature}'. Multiple matches:")
|
||||
for m in matches:
|
||||
print(f" - {m['name']}")
|
||||
print("\nPlease be more specific.")
|
||||
sys.exit(1)
|
||||
|
||||
feature = matches[0]
|
||||
cell = get_cell(feature, args.deployment, args.version)
|
||||
cfn = _get_color_fn()
|
||||
deploy_name = get_deployment_name(data, args.deployment)
|
||||
|
||||
print(f"Feature: {feature['name']}")
|
||||
print(f"Deployment: {deploy_name} ({args.deployment})")
|
||||
print(f"Version: {args.version}")
|
||||
print()
|
||||
|
||||
if cell is None:
|
||||
print(f"Status: {cfn('UNTESTED')} (no entry in matrix)")
|
||||
else:
|
||||
status = cell.get("status", "UNTESTED")
|
||||
print(f"Status: {cfn(status)}")
|
||||
notes = cell.get("notes")
|
||||
if notes:
|
||||
print(f"Notes: {notes}")
|
||||
|
||||
|
||||
def cmd_compare(args: argparse.Namespace) -> None:
|
||||
"""Side-by-side comparison of two deployment types for a version."""
|
||||
data = load_matrix(MATRIX_PATH)
|
||||
validate_deployment(data, args.deploy1)
|
||||
validate_deployment(data, args.deploy2)
|
||||
validate_version(data, args.version)
|
||||
|
||||
cfn = _get_color_fn()
|
||||
name1 = get_deployment_name(data, args.deploy1)
|
||||
name2 = get_deployment_name(data, args.deploy2)
|
||||
|
||||
# Determine column widths
|
||||
feature_names = [f["name"] for f in data["features"]]
|
||||
col_feat = max(len(n) for n in feature_names) + 2
|
||||
col_status = max(len(name1), len(name2), 12) + 2
|
||||
|
||||
header = (
|
||||
f"{'Feature':<{col_feat}}"
|
||||
f" {name1:<{col_status}}"
|
||||
f" {name2:<{col_status}}"
|
||||
)
|
||||
print(f"Comparison: {name1} vs {name2} (version {args.version})")
|
||||
print()
|
||||
print(header)
|
||||
print("-" * len(header))
|
||||
|
||||
for feature in data["features"]:
|
||||
cell1 = get_cell(feature, args.deploy1, args.version)
|
||||
cell2 = get_cell(feature, args.deploy2, args.version)
|
||||
s1 = cell1["status"] if cell1 else "UNTESTED"
|
||||
s2 = cell2["status"] if cell2 else "UNTESTED"
|
||||
|
||||
# For plain (non-tty) we can just print aligned; for color we need
|
||||
# to pad BEFORE wrapping with color codes so alignment is correct.
|
||||
s1_padded = f"{s1:<{col_status}}"
|
||||
s2_padded = f"{s2:<{col_status}}"
|
||||
|
||||
if _supports_color():
|
||||
s1_display = f"{_COLORS.get(s1, '')}{s1_padded}{_RESET}"
|
||||
s2_display = f"{_COLORS.get(s2, '')}{s2_padded}{_RESET}"
|
||||
else:
|
||||
s1_display = s1_padded
|
||||
s2_display = s2_padded
|
||||
|
||||
print(f"{feature['name']:<{col_feat}} {s1_display} {s2_display}")
|
||||
|
||||
|
||||
def cmd_gaps(args: argparse.Namespace) -> None:
|
||||
"""List features that are NOT_AVAIL, BROKEN, or LIMITED for a deploy/version."""
|
||||
data = load_matrix(MATRIX_PATH)
|
||||
validate_deployment(data, args.deployment)
|
||||
validate_version(data, args.version)
|
||||
|
||||
cfn = _get_color_fn()
|
||||
deploy_name = get_deployment_name(data, args.deployment)
|
||||
gap_statuses = {"NOT_AVAIL", "BROKEN", "LIMITED"}
|
||||
gaps: List[Tuple[str, str, Optional[str]]] = []
|
||||
|
||||
for feature in data["features"]:
|
||||
cell = get_cell(feature, args.deployment, args.version)
|
||||
if cell is None:
|
||||
# No entry — could be untested or implicitly not available
|
||||
continue
|
||||
status = cell.get("status", "UNTESTED")
|
||||
if status in gap_statuses:
|
||||
gaps.append((feature["name"], status, cell.get("notes")))
|
||||
|
||||
print(f"Gaps for {deploy_name} ({args.deployment}), version {args.version}:")
|
||||
print()
|
||||
|
||||
if not gaps:
|
||||
print(" No gaps found — all mapped features are GA, GA_CAVEAT, PREVIEW, or UNTESTED.")
|
||||
return
|
||||
|
||||
name_width = max(len(g[0]) for g in gaps) + 2
|
||||
for name, status, notes in gaps:
|
||||
status_display = cfn(status)
|
||||
line = f" {name:<{name_width}} {status_display}"
|
||||
if notes:
|
||||
line += f" — {notes}"
|
||||
print(line)
|
||||
|
||||
|
||||
def cmd_update(args: argparse.Namespace) -> None:
|
||||
"""Modify a specific cell in the matrix."""
|
||||
status = args.status.upper()
|
||||
if status not in VALID_STATUSES:
|
||||
print(
|
||||
f"Error: Invalid status '{args.status}'. "
|
||||
f"Valid statuses: {', '.join(sorted(VALID_STATUSES))}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
data = load_matrix(MATRIX_PATH)
|
||||
validate_deployment(data, args.deployment)
|
||||
validate_version(data, args.version)
|
||||
|
||||
matches = find_features(data, args.feature)
|
||||
|
||||
if not matches:
|
||||
print(f"Error: No features matching '{args.feature}'.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if len(matches) > 1:
|
||||
print(f"Ambiguous feature name '{args.feature}'. Multiple matches:")
|
||||
for m in matches:
|
||||
print(f" - {m['name']}")
|
||||
print("\nPlease use the exact feature name.")
|
||||
sys.exit(1)
|
||||
|
||||
feature = matches[0]
|
||||
|
||||
# Ensure nested dicts exist
|
||||
if "matrix" not in feature:
|
||||
feature["matrix"] = {}
|
||||
if args.deployment not in feature["matrix"]:
|
||||
feature["matrix"][args.deployment] = {}
|
||||
|
||||
cell: Dict[str, Any] = {"status": status}
|
||||
if args.notes:
|
||||
cell["notes"] = args.notes
|
||||
|
||||
# Add contributor attribution if provided
|
||||
contributor_name = getattr(args, "name", None)
|
||||
contributor_team = getattr(args, "team", None)
|
||||
if contributor_name and contributor_team:
|
||||
contributor = {
|
||||
"name": contributor_name,
|
||||
"team": contributor_team,
|
||||
"date": date.today().isoformat(),
|
||||
}
|
||||
confidence = getattr(args, "confidence", None)
|
||||
if confidence and confidence in VALID_CONFIDENCE:
|
||||
contributor["confidence"] = confidence
|
||||
cell["contributor"] = contributor
|
||||
|
||||
# Add field_finding_ref if provided
|
||||
field_ref = getattr(args, "field_finding_ref", None)
|
||||
if field_ref:
|
||||
cell["field_finding_ref"] = field_ref
|
||||
|
||||
feature["matrix"][args.deployment][args.version] = cell
|
||||
|
||||
# Update last_verified at root
|
||||
data["last_verified"] = date.today().isoformat()
|
||||
|
||||
save_matrix(MATRIX_PATH, data)
|
||||
|
||||
cfn = _get_color_fn()
|
||||
print(f"Updated '{feature['name']}' / {args.deployment} / {args.version}:")
|
||||
print(f" Status: {cfn(status)}")
|
||||
if args.notes:
|
||||
print(f" Notes: {args.notes}")
|
||||
print(f" last_verified set to {data['last_verified']}")
|
||||
|
||||
|
||||
def cmd_export(args: argparse.Namespace) -> None:
|
||||
"""Export the full matrix as markdown or CSV."""
|
||||
data = load_matrix(MATRIX_PATH)
|
||||
versions = get_version_ids(data)
|
||||
deployments = get_deployment_ids(data)
|
||||
|
||||
# Build column headers: each column is deploy + version
|
||||
columns: List[Tuple[str, str]] = []
|
||||
for d in deployments:
|
||||
for v in versions:
|
||||
columns.append((d, v))
|
||||
|
||||
col_labels = [f"{d}/{v}" for d, v in columns]
|
||||
|
||||
if args.format == "csv":
|
||||
_export_csv(data, columns, col_labels)
|
||||
else:
|
||||
_export_markdown(data, columns, col_labels)
|
||||
|
||||
|
||||
def _export_markdown(
|
||||
data: Dict[str, Any],
|
||||
columns: List[Tuple[str, str]],
|
||||
col_labels: List[str],
|
||||
) -> None:
|
||||
"""Print a markdown table to stdout."""
|
||||
features = data["features"]
|
||||
|
||||
# Column widths
|
||||
feat_width = max(len(f["name"]) for f in features)
|
||||
feat_width = max(feat_width, len("Feature"))
|
||||
col_widths = [max(len(lbl), 10) for lbl in col_labels]
|
||||
|
||||
# Header row
|
||||
header = f"| {'Feature':<{feat_width}} |"
|
||||
for lbl, w in zip(col_labels, col_widths):
|
||||
header += f" {lbl:<{w}} |"
|
||||
print(header)
|
||||
|
||||
# Separator
|
||||
sep = f"|{'-' * (feat_width + 2)}|"
|
||||
for w in col_widths:
|
||||
sep += f"{'-' * (w + 2)}|"
|
||||
print(sep)
|
||||
|
||||
# Data rows
|
||||
for feature in features:
|
||||
row = f"| {feature['name']:<{feat_width}} |"
|
||||
for (d, v), w in zip(columns, col_widths):
|
||||
cell = get_cell(feature, d, v)
|
||||
status = cell["status"] if cell else "-"
|
||||
row += f" {status:<{w}} |"
|
||||
print(row)
|
||||
|
||||
|
||||
def _export_csv(
|
||||
data: Dict[str, Any],
|
||||
columns: List[Tuple[str, str]],
|
||||
col_labels: List[str],
|
||||
) -> None:
|
||||
"""Print CSV to stdout."""
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
|
||||
# Header
|
||||
writer.writerow(["Feature", "Category"] + col_labels)
|
||||
|
||||
for feature in data["features"]:
|
||||
row = [feature["name"], feature.get("category", "")]
|
||||
for d, v in columns:
|
||||
cell = get_cell(feature, d, v)
|
||||
row.append(cell["status"] if cell else "")
|
||||
writer.writerow(row)
|
||||
|
||||
print(output.getvalue(), end="")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CLI entry point
|
||||
# =============================================================================
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="feature_matrix_cli",
|
||||
description="Query and update the ADB feature compatibility matrix.",
|
||||
)
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
# --- check ---
|
||||
p_check = subparsers.add_parser(
|
||||
"check",
|
||||
help="Look up a specific feature/deployment/version combo.",
|
||||
)
|
||||
p_check.add_argument("feature", help="Feature name (case-insensitive, partial match).")
|
||||
p_check.add_argument("deployment", help="Deployment type id (e.g. adb_s, exacs).")
|
||||
p_check.add_argument("version", help="Version id (e.g. 23ai, 26ai).")
|
||||
|
||||
# --- compare ---
|
||||
p_compare = subparsers.add_parser(
|
||||
"compare",
|
||||
help="Side-by-side comparison of two deployment types for a version.",
|
||||
)
|
||||
p_compare.add_argument("deploy1", help="First deployment type id.")
|
||||
p_compare.add_argument("deploy2", help="Second deployment type id.")
|
||||
p_compare.add_argument("version", help="Version id.")
|
||||
|
||||
# --- gaps ---
|
||||
p_gaps = subparsers.add_parser(
|
||||
"gaps",
|
||||
help="List features that are NOT_AVAIL, BROKEN, or LIMITED.",
|
||||
)
|
||||
p_gaps.add_argument("deployment", help="Deployment type id.")
|
||||
p_gaps.add_argument("version", help="Version id.")
|
||||
|
||||
# --- update ---
|
||||
p_update = subparsers.add_parser(
|
||||
"update",
|
||||
help="Modify a specific cell in the matrix.",
|
||||
)
|
||||
p_update.add_argument("feature", help="Feature name (case-insensitive, partial match).")
|
||||
p_update.add_argument("deployment", help="Deployment type id.")
|
||||
p_update.add_argument("version", help="Version id.")
|
||||
p_update.add_argument("--status", required=True, help="New status value.")
|
||||
p_update.add_argument("--notes", default=None, help="Optional notes for the cell.")
|
||||
p_update.add_argument("--name", default=None, help="Contributor name for attribution.")
|
||||
p_update.add_argument("--team", default=None, help="Contributor team for attribution.")
|
||||
p_update.add_argument("--confidence", default=None, help="Confidence level: validated/observed/reported/inferred.")
|
||||
p_update.add_argument("--field-finding-ref", default=None, help="Reference to field finding ID.")
|
||||
|
||||
# --- export ---
|
||||
p_export = subparsers.add_parser(
|
||||
"export",
|
||||
help="Export the full matrix as markdown or CSV.",
|
||||
)
|
||||
p_export.add_argument(
|
||||
"--format",
|
||||
required=True,
|
||||
choices=["markdown", "csv"],
|
||||
help="Output format.",
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
dispatch = {
|
||||
"check": cmd_check,
|
||||
"compare": cmd_compare,
|
||||
"gaps": cmd_gaps,
|
||||
"update": cmd_update,
|
||||
"export": cmd_export,
|
||||
}
|
||||
|
||||
handler = dispatch.get(args.command)
|
||||
if handler is None:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
handler(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user