Spaces:
Running
Running
| import pandas as pd | |
| import re | |
| import json | |
| # Expected chapter counts for the three Acts | |
| EXPECTED_CHAPTERS = { | |
| "BNS": 20, # Chapters I to XX | |
| "BNSS": 39, # Chapters I to XXXIX (with Chapter V injected) | |
| "BSA": 12 # Chapters I to XII | |
| } | |
| def validate_datasets(page_df: pd.DataFrame, line_df: pd.DataFrame, toc_df: pd.DataFrame, schedule_df: pd.DataFrame): | |
| print("\n==================== Running Validation Checks ====================") | |
| errors = [] | |
| # 1. Basic Shape and Nullity checks | |
| print(f"DataFrames shape: page_df={page_df.shape}, line_df={line_df.shape}, toc_df={toc_df.shape}") | |
| if not schedule_df.empty: | |
| print(f"schedule_df shape={schedule_df.shape}") | |
| for name, df in [("page_df", page_df), ("line_df", line_df), ("toc_df", toc_df)]: | |
| if df.empty: | |
| errors.append(f"Error: {name} is empty.") | |
| # 2. Orphans Checks | |
| # Every section_id in line_df must exist in toc_df | |
| toc_ids = set(toc_df["section_id"].unique()) | |
| line_ids = set(line_df["section_id"].unique()) | |
| orphans = line_ids - toc_ids | |
| if orphans: | |
| errors.append(f"Error: Orphans found! The following section_ids in line_df are not in toc_df: {orphans}") | |
| else: | |
| print("Success: No orphaned lines. Every line maps to a valid structural node in toc_df.") | |
| # 3. Chapter Structure Verification | |
| for act, expected in EXPECTED_CHAPTERS.items(): | |
| act_toc = toc_df[toc_df["act_code"] == act] | |
| chapters = act_toc[act_toc["node_type"] == "chapter"] | |
| chapter_count = len(chapters) | |
| print(f"Act {act}: found {chapter_count} chapters (expected {expected}).") | |
| # Verify that chapter numbers are not duplicated | |
| chap_nos = chapters["chapter_no"].tolist() | |
| duplicates = set([x for x in chap_nos if chap_nos.count(x) > 1]) | |
| if duplicates: | |
| errors.append(f"Error: Duplicate chapters in Act {act}: {duplicates}") | |
| if chapter_count != expected: | |
| errors.append(f"Error: Chapter count mismatch for {act}. Found {chapter_count}, expected {expected}.") | |
| # 3b. Root & Hierarchy Structure Verification | |
| roots = toc_df[toc_df["node_type"] == "root"] | |
| expected_roots_count = 4 if "SOP" in toc_df["act_code"].values else 3 | |
| if len(roots) != expected_roots_count: | |
| errors.append(f"Error: Expected exactly {expected_roots_count} root nodes, found {len(roots)}: {roots['section_id'].tolist()}") | |
| else: | |
| acts_to_check = list(EXPECTED_CHAPTERS.keys()) | |
| if "SOP" in toc_df["act_code"].values: | |
| acts_to_check.append("SOP") | |
| for act in acts_to_check: | |
| act_root = roots[roots["act_code"] == act] | |
| if act_root.empty: | |
| errors.append(f"Error: Missing root node for Act {act}.") | |
| else: | |
| row = act_root.iloc[0] | |
| if row["level"] != 0 or not pd.isna(row["parent_id"]): | |
| errors.append(f"Error: Act {act} root node has invalid level ({row['level']}) or parent_id ({row['parent_id']}).") | |
| # Check that all chapters point to their Act's root | |
| acts_to_check = list(EXPECTED_CHAPTERS.keys()) | |
| if "SOP" in toc_df["act_code"].values: | |
| acts_to_check.append("SOP") | |
| for act in acts_to_check: | |
| act_toc = toc_df[toc_df["act_code"] == act] | |
| chapters = act_toc[act_toc["node_type"] == "chapter"] | |
| for _, chap in chapters.iterrows(): | |
| expected_parent = f"{act}_root" | |
| if chap["parent_id"] != expected_parent: | |
| errors.append(f"Error: Chapter {chap['section_id']} has invalid parent_id: expected {expected_parent}, got {chap['parent_id']}.") | |
| if chap["level"] != 1: | |
| errors.append(f"Error: Chapter {chap['section_id']} has invalid level: expected 1, got {chap['level']}.") | |
| # Check front_matter | |
| front = act_toc[act_toc["node_type"] == "front_matter"] | |
| if not front.empty: | |
| frow = front.iloc[0] | |
| expected_parent = f"{act}_root" | |
| if frow["parent_id"] != expected_parent: | |
| errors.append(f"Error: Front matter of {act} has invalid parent_id: expected {expected_parent}, got {frow['parent_id']}.") | |
| if frow["level"] != 1: | |
| errors.append(f"Error: Front matter of {act} has invalid level: expected 1, got {frow['level']}.") | |
| # 4. Section Numbering Sequence Contiguity Check | |
| for act in EXPECTED_CHAPTERS.keys(): | |
| act_toc = toc_df[(toc_df["act_code"] == act) & (toc_df["node_type"] == "section")] | |
| sections = act_toc["section_no"].dropna().tolist() | |
| # Parse section numbers to integers | |
| section_ints = [] | |
| for s in sections: | |
| # Strip sub-sections (a), (b), etc. | |
| match = re.match(r'^(\d+)', str(s)) | |
| if match: | |
| section_ints.append(int(match.group(1))) | |
| if not section_ints: | |
| errors.append(f"Error: No section numbers found in Act {act}.") | |
| continue | |
| max_sec = max(section_ints) | |
| min_sec = min(section_ints) | |
| # We expect sections to start at 1 | |
| if min_sec != 1: | |
| errors.append(f"Warning: Act {act} sections start at {min_sec} instead of 1.") | |
| # Find gaps in the sequence | |
| expected_seq = set(range(1, max_sec + 1)) | |
| actual_seq = set(section_ints) | |
| gaps = expected_seq - actual_seq | |
| # Find duplicates | |
| seen = set() | |
| dupes = set() | |
| for x in section_ints: | |
| if x in seen: | |
| dupes.add(x) | |
| seen.add(x) | |
| print(f"Act {act}: sections span from {min_sec} to {max_sec}.") | |
| if gaps: | |
| errors.append(f"Warning: Gaps found in section numbering for Act {act}: {sorted(list(gaps))}") | |
| if dupes: | |
| errors.append(f"Warning: Duplicate section numbers found in Act {act}: {sorted(list(dupes))}") | |
| # 5. BNSS Schedule Checks | |
| if not schedule_df.empty: | |
| # Check columns | |
| expected_cols = ["act_code", "page_no", "section", "offence", "punishment", "cognizable", "bailable", "court"] | |
| for c in expected_cols: | |
| if c not in schedule_df.columns: | |
| errors.append(f"Error: Expected column '{c}' missing in schedule_df.") | |
| # Row count sanity | |
| rows_count = len(schedule_df) | |
| print(f"BNSS First Schedule classification table has {rows_count} rows.") | |
| if rows_count < 400: | |
| errors.append(f"Error: Unusually low row count in First Schedule ({rows_count} rows).") | |
| # Check that there are no remaining merged cells | |
| long_fields = [] | |
| for idx, row in schedule_df.iterrows(): | |
| for field in ["cognizable", "bailable", "court"]: | |
| val = str(row[field]) | |
| if len(val) > 100: | |
| long_fields.append((idx, row["section"], field, val[:30] + "...")) | |
| if long_fields: | |
| errors.append(f"Warning: Found fields in schedule_df that look like unmerged text: {long_fields[:5]}") | |
| # Summary of validation | |
| errors_only = [e for e in errors if e.startswith("Error")] | |
| warnings_only = [w for w in errors if w.startswith("Warning")] | |
| if warnings_only: | |
| print("\nValidation Warnings:") | |
| for warn in warnings_only: | |
| print(f" - {warn}") | |
| if errors_only: | |
| print("\nValidation FAILED with the following errors:") | |
| for err in errors_only: | |
| print(f" - {err}") | |
| return False | |
| else: | |
| print("\nSuccess: All core validation checks passed successfully!") | |
| return True | |
| def validate_completed_trees(tree_dir="tree", output_dir="output"): | |
| import os | |
| import json | |
| import pandas as pd | |
| print("\n==================== Running Tree Validation Checks ====================") | |
| errors = [] | |
| # 1. Check index.json | |
| index_path = os.path.join(tree_dir, "index.json") | |
| if not os.path.exists(index_path): | |
| errors.append(f"Error: index.json is missing in {tree_dir}.") | |
| return False | |
| try: | |
| with open(index_path, "r", encoding="utf-8") as f: | |
| index_data = json.load(f) | |
| except Exception as e: | |
| errors.append(f"Error: Failed to parse index.json: {e}") | |
| return False | |
| acts = ["BNS", "BNSS", "BSA"] | |
| if "SOP" in index_data.get("acts", {}): | |
| acts.append("SOP") | |
| for act in acts: | |
| if act not in index_data.get("acts", {}): | |
| errors.append(f"Error: Act {act} is missing in index.json.") | |
| else: | |
| act_info = index_data["acts"][act] | |
| if not act_info.get("summary"): | |
| errors.append(f"Error: Summary for Act {act} is missing or empty in index.json.") | |
| if not act_info.get("path"): | |
| errors.append(f"Error: Path for Act {act} is missing in index.json.") | |
| # 2. Check each Act's JSON tree | |
| all_nodes_flat = [] | |
| for act in acts: | |
| act_path = os.path.join(tree_dir, f"{act}.json") | |
| if not os.path.exists(act_path): | |
| errors.append(f"Error: {act}.json is missing in {tree_dir}.") | |
| continue | |
| try: | |
| with open(act_path, "r", encoding="utf-8") as f: | |
| root_node = json.load(f) | |
| except Exception as e: | |
| errors.append(f"Error: Failed to parse {act}.json: {e}") | |
| continue | |
| # Collect all nodes recursively | |
| act_nodes = [] | |
| def traverse(node): | |
| act_nodes.append(node) | |
| for child in node.get("children", []): | |
| traverse(child) | |
| traverse(root_node) | |
| all_nodes_flat.extend(act_nodes) | |
| # Verify root properties | |
| if root_node.get("level") != 0 or root_node.get("node_type") != "root": | |
| errors.append(f"Error: Root node for {act} has invalid level ({root_node.get('level')}) or node_type ({root_node.get('node_type')}).") | |
| if not root_node.get("summary"): | |
| errors.append(f"Error: Root node for {act} has empty or missing summary.") | |
| if root_node.get("content") is not None: | |
| errors.append(f"Error: Root node for {act} has non-null content.") | |
| # Verify node-specific properties | |
| for node in act_nodes: | |
| nid = node.get("node_id") | |
| ntype = node.get("node_type") | |
| level = node.get("level") | |
| summary = node.get("summary") | |
| content = node.get("content") | |
| children = node.get("children", []) | |
| # Every node must have a non-empty summary | |
| if not summary: | |
| errors.append(f"Error: Node {nid} has an empty or missing summary.") | |
| # Leaf nodes (node_type == "section", "schedule_row", or SOP node types) | |
| if ntype in ["section", "schedule_row", "sop_procedure", "sop_form", "sop_reference", "sop_table"]: | |
| if content is None or len(content) == 0: | |
| errors.append(f"Error: Leaf node {nid} has null or empty content.") | |
| if len(children) > 0: | |
| errors.append(f"Error: Leaf node {nid} has children.") | |
| else: | |
| # Non-leaf nodes | |
| if content is not None: | |
| errors.append(f"Error: Non-leaf node {nid} ({ntype}) has non-null content.") | |
| # Check metadata fields | |
| meta = node.get("metadata", {}) | |
| if not meta: | |
| errors.append(f"Error: Node {nid} is missing metadata.") | |
| else: | |
| for field in ["act_code", "page_range", "internal_refs", "cross_act_refs", "token_estimate", "stable_hash"]: | |
| if field not in meta: | |
| errors.append(f"Error: Node {nid} metadata is missing field '{field}'.") | |
| # 3. Check duplicate node_ids across all acts | |
| node_ids = [n.get("node_id") for n in all_nodes_flat if n.get("node_id")] | |
| if len(node_ids) != len(set(node_ids)): | |
| seen = set() | |
| dupes = set() | |
| for x in node_ids: | |
| if x in seen: | |
| dupes.add(x) | |
| seen.add(x) | |
| errors.append(f"Error: Duplicate node_ids found: {dupes}") | |
| # 4. Check that combined nav scaffold fits within token budget (< 250,000 tokens) | |
| total_nav_tokens = sum(len(n.get("summary", "")) // 4 for n in all_nodes_flat) | |
| print(f"Total navigation scaffold token estimate: {total_nav_tokens} tokens (limit 250,000).") | |
| if total_nav_tokens >= 250000: | |
| errors.append(f"Error: Combined nav scaffold token count ({total_nav_tokens}) exceeds the 250,000 token limit.") | |
| # Summary of validation | |
| errors_only = [e for e in errors if e.startswith("Error")] | |
| warnings_only = [w for w in errors if w.startswith("Warning")] | |
| if warnings_only: | |
| print("\nTree Validation Warnings:") | |
| for warn in warnings_only: | |
| print(f" - {warn}") | |
| if errors_only: | |
| print("\nTree Validation FAILED with the following errors:") | |
| for err in errors_only: | |
| print(f" - {err}") | |
| return False | |
| else: | |
| print("\nSuccess: All tree validation checks passed successfully!") | |
| return True | |