Legal-Vectorless-RAG-HF / src /validation.py
ayanshuDS
Deploy to HF without binaries
685cc60
Raw
History Blame Contribute Delete
13.6 kB
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