#!/usr/bin/env python3 """ Check change percentage between original and transformed text. Flags if >40% of words changed (may indicate over-editing). Pure reordering is exempt: change_percentage still reflects the raw delete+insert diff, but excessive_change stays false when the transformed text is a word-level permutation of the original (moved sentences are not over-editing). SKILL.md's 40% guidance refers to the excessive_change flag. Usage: python diff_check.py original.txt transformed.txt """ from __future__ import annotations import sys import json import re from collections import Counter from difflib import SequenceMatcher from typing import TypedDict class DiffResult(TypedDict): original_word_count: int transformed_word_count: int similarity_ratio: float change_percentage: float words_added: int words_removed: int words_changed: int excessive_change: bool flags: list[str] def split_words(text: str) -> list[str]: """Split text into comparison tokens. Punctuation is tokenized separately (not stripped) so that structural edits like repunctuation register as change instead of reading as identical. """ return re.findall(r'\w+|[^\w\s]', text.lower()) def calculate_diff(original: str, transformed: str) -> DiffResult: """Calculate difference metrics between two texts.""" original_words = split_words(original) transformed_words = split_words(transformed) # Use SequenceMatcher for word-level diff matcher = SequenceMatcher(None, original_words, transformed_words) similarity = matcher.ratio() # Count operations words_added = 0 words_removed = 0 words_changed = 0 for tag, i1, i2, j1, j2 in matcher.get_opcodes(): if tag == 'replace': words_changed += max(i2 - i1, j2 - j1) elif tag == 'delete': words_removed += i2 - i1 elif tag == 'insert': words_added += j2 - j1 # Change percentage (relative to original) total_changes = words_added + words_removed + words_changed change_percentage = ( (total_changes / len(original_words) * 100) if original_words else 0 ) shared_tokens = sum((Counter(original_words) & Counter(transformed_words)).values()) token_overlap = shared_tokens / len(original_words) if original_words else 0 # Flags flags: list[str] = [] excessive = False if change_percentage > 40: if token_overlap >= 0.9: flags.append(f"Mostly reordered ({token_overlap:.0%} tokens shared)") else: flags.append(f"Excessive change ({change_percentage:.1f}% > 40% threshold)") excessive = True if len(transformed_words) < len(original_words) * 0.3: flags.append("Transformed text is less than 30% of original length") excessive = True if len(transformed_words) > len(original_words) * 1.5: flags.append("Transformed text is 50%+ longer than original") # Length change ratio length_ratio = len(transformed_words) / len(original_words) if original_words else 0 if length_ratio < 0.5: flags.append(f"Significant condensation ({length_ratio:.0%} of original)") elif length_ratio > 1.2: flags.append(f"Text expanded ({length_ratio:.0%} of original)") return { "original_word_count": len(original_words), "transformed_word_count": len(transformed_words), "similarity_ratio": round(similarity, 3), "change_percentage": round(change_percentage, 1), "words_added": words_added, "words_removed": words_removed, "words_changed": words_changed, "excessive_change": excessive, "flags": flags } def main() -> None: if len(sys.argv) < 3: print("Usage: diff_check.py ") sys.exit(1) # Read inputs try: with open(sys.argv[1], 'r') as f: original = f.read() with open(sys.argv[2], 'r') as f: transformed = f.read() except OSError as e: print(json.dumps({"error": f"Could not read input: {e}"})) sys.exit(2) result = calculate_diff(original, transformed) print(json.dumps(result, indent=2)) # Exit with 1 if excessive change sys.exit(1 if result["excessive_change"] else 0) if __name__ == "__main__": main()