diff --git a/.github/scripts/filter_core_ids_by_standard.py b/.github/scripts/filter_core_ids_by_standard.py new file mode 100644 index 000000000..8d2a961f5 --- /dev/null +++ b/.github/scripts/filter_core_ids_by_standard.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +"""Filter Published CORE rule IDs by Authorities.Standards.Name.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import yaml + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Return space-separated CORE IDs for Published rules matching standards." + ) + parser.add_argument( + "--rules-root", + required=True, + help="Repository root containing Published/", + ) + parser.add_argument( + "--standards", + nargs="+", + required=True, + help="Standards to match from Authorities[].Standards[].Name", + ) + parser.add_argument( + "--core-ids", + default="", + help="Optional space-separated CORE IDs to intersect with the standard filter.", + ) + return parser.parse_args() + + +def iter_published_rule_files(rules_root: Path) -> list[Path]: + published_dir = rules_root / "Published" + files: list[Path] = [] + for pattern in ("**/rule.yml", "**/rule.yaml"): + files.extend(published_dir.glob(pattern)) + return sorted(set(files)) + + +def rule_matches_standard(rule: dict, target_standards: set[str]) -> bool: + for authority in rule.get("Authorities") or []: + if not isinstance(authority, dict): + continue + for standard in authority.get("Standards") or []: + if not isinstance(standard, dict): + continue + standard_name = str(standard.get("Name") or "").strip().upper() + if standard_name in target_standards: + return True + return False + + +def collect_filtered_core_ids(rules_root: Path, standards: list[str]) -> list[str]: + target_standards = {name.strip().upper() for name in standards if name.strip()} + core_ids: list[str] = [] + + for rule_file in iter_published_rule_files(rules_root): + with rule_file.open("r", encoding="utf-8") as handle: + rule = yaml.safe_load(handle) or {} + if not isinstance(rule, dict): + continue + if not rule_matches_standard(rule, target_standards): + continue + + core = rule.get("Core") or {} + if not isinstance(core, dict): + continue + core_id = str(core.get("Id") or "").strip() + if core_id: + core_ids.append(core_id) + return core_ids + + +def intersect_with_requested(core_ids: list[str], requested_core_ids: str) -> list[str]: + requested = requested_core_ids.split() + if not requested: + return core_ids + + allowed = set(core_ids) + return [core_id for core_id in requested if core_id in allowed] + + +def main() -> int: + args = parse_args() + rules_root = Path(args.rules_root) + if not rules_root.is_dir(): + print(f"rules-root does not exist: {rules_root}", file=sys.stderr) + return 1 + + filtered_core_ids = collect_filtered_core_ids(rules_root, args.standards) + output_core_ids = intersect_with_requested(filtered_core_ids, args.core_ids) + print(" ".join(output_core_ids)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/workflows/update-published-results.yml b/.github/workflows/update-published-results.yml index ba0c5e14b..fc229a858 100644 --- a/.github/workflows/update-published-results.yml +++ b/.github/workflows/update-published-results.yml @@ -79,20 +79,38 @@ jobs: run: venv/bin/pip install tabulate jmespath pyyaml ruamel.yaml # ----------------------------------------------------------------------- - # 4. Run the engine for every Published rule, writing actual results.csv - # directly into each case's results/ directory (overwriting the old baseline) + # 4. Run the engine for Published rules filtered by Authorities.Standards.Name, + # writing actual results.csv directly into each case's results/ directory + # (overwriting the old baseline) # ----------------------------------------------------------------------- - name: Run engine and write results id: run_engine continue-on-error: true run: | chmod +x .github/scripts/run_validation.sh + FILTER_STANDARDS=("USDM" "TIG") + mapfile -t FILTER_RESULT < <(venv/bin/python .github/scripts/filter_core_ids_by_standard.py \ + --rules-root "$(pwd)" \ + --standards "${FILTER_STANDARDS[@]}" \ + --core-ids "${{ inputs.core_ids }}") + FILTERED_CORE_IDS="${FILTER_RESULT[0]}" + CORE_IDS_TO_RUN="${FILTER_RESULT[1]}" + + if [ -z "$FILTERED_CORE_IDS" ]; then + echo "No Published rules found for standards: ${FILTER_STANDARDS[*]}" + exit 0 + fi - CORE_IDS_ARG="" - if [ -n "${{ inputs.core_ids }}" ]; then - CORE_IDS_ARG="--core-ids ${{ inputs.core_ids }}" + if [ -z "$CORE_IDS_TO_RUN" ]; then + echo "No requested core_ids matched Published rules for standards: ${FILTER_STANDARDS[*]}" + exit 0 fi + echo "Running validation for standards: ${FILTER_STANDARDS[*]}" + echo "Rule count: $(echo "$CORE_IDS_TO_RUN" | wc -w | tr -d ' ')" + + CORE_IDS_ARG="--core-ids $CORE_IDS_TO_RUN" + ENGINE_DIR_OVERRIDE="$(pwd)/engine" \ venv/bin/python engine/scripts/validate_published_rules.py \ --rules-root "$(pwd)" \