Download scripts/build_temporal_pilot.py from haidang2405/telen: direct link, hf CLI and curl.
- Browser
- Download file 31.5 kB
-
https://huggingface.co/haidang2405/telen/resolve/main/scripts/build_temporal_pilot.py
- Command line
-
hf download hf://haidang2405/telen/scripts/build_temporal_pilot.py
-
curl -L -o build_temporal_pilot.py https://huggingface.co/haidang2405/telen/resolve/main/scripts/build_temporal_pilot.py
31.5 kB
| #!/usr/bin/env python3 | |
| """Build an auditable temporal-event pilot package from discovery candidates. | |
| The generated records are annotation inputs, never gold labels. They retain | |
| the source snapshot hashes, exact event spans, automated rejection reasons, | |
| and blank official/manual verification fields. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import hashlib | |
| import json | |
| import sys | |
| from collections import Counter | |
| from datetime import timedelta | |
| from pathlib import Path | |
| import pandas as pd | |
| import pyarrow.parquet as pq | |
| REPO_ROOT = Path(__file__).resolve().parents[1] | |
| sys.path.insert(0, str(REPO_ROOT / "src")) | |
| from telen.bootstrap import ( # noqa: E402 | |
| contains_document_number, | |
| extract_articles, | |
| extract_event_mentions, | |
| html_to_legal_text, | |
| ) | |
| ANNOTATION_PROTOCOL_VERSION = "pilot-1.0" | |
| QREL_TEMPLATE_COLUMNS = [ | |
| "protocol_version", | |
| "event_group_id", | |
| "query_group_id", | |
| "query_id", | |
| "snapshot_phase", | |
| "snapshot_date", | |
| "candidate_version_id", | |
| "display_candidate_id", | |
| "candidate_pool_id", | |
| "candidate_pool_status", | |
| "question_status", | |
| "query_text", | |
| "candidate_text", | |
| "candidate_official_url", | |
| "candidate_text_sha256", | |
| "semantic_support_annotator_1", | |
| "semantic_support_annotator_2", | |
| "temporal_status_annotator_1", | |
| "temporal_status_annotator_2", | |
| "relevance_grade_annotator_1", | |
| "relevance_grade_annotator_2", | |
| "official_source_span_annotator_1", | |
| "official_source_span_annotator_2", | |
| "answer_rationale_annotator_1", | |
| "answer_rationale_annotator_2", | |
| "rationale_annotator_1", | |
| "rationale_annotator_2", | |
| "reviewed_at_annotator_1", | |
| "reviewed_at_annotator_2", | |
| "semantic_support_adjudicated", | |
| "temporal_status_adjudicated", | |
| "relevance_grade_adjudicated", | |
| "official_source_span", | |
| "answer_rationale", | |
| "annotator_1", | |
| "annotator_2", | |
| "adjudicator", | |
| "judgment_status", | |
| ] | |
| def sha256_text(value: str) -> str: | |
| return hashlib.sha256(value.encode("utf-8")).hexdigest() | |
| def sha256_file(path: Path) -> str: | |
| digest = hashlib.sha256() | |
| with path.open("rb") as handle: | |
| for chunk in iter(lambda: handle.read(1024 * 1024), b""): | |
| digest.update(chunk) | |
| return digest.hexdigest() | |
| def parse_event_date(row: pd.Series) -> pd.Timestamp | None: | |
| for field in ("event_ngay_co_hieu_luc", "event_ngay_ban_hanh"): | |
| parsed = pd.to_datetime(row.get(field), format="%d/%m/%Y", errors="coerce") | |
| if not pd.isna(parsed): | |
| return parsed | |
| return None | |
| def _target_reference_spans(text: str, document_number: str) -> list[str]: | |
| """Return short source lines that explicitly name the target instrument.""" | |
| spans = [] | |
| for line in text.splitlines(): | |
| if contains_document_number(line, document_number): | |
| spans.append(line.strip()) | |
| return spans[:10] | |
| def _write_jsonl(path: Path, records: list[dict]) -> None: | |
| with path.open("w", encoding="utf-8") as handle: | |
| for record in records: | |
| handle.write(json.dumps(record, ensure_ascii=False) + "\n") | |
| def _load_official_prefill( | |
| registry_path: Path | None, | |
| audit_path: Path | None, | |
| ) -> dict[str, dict[str, str]]: | |
| """Join registry URLs to machine-fetched hashes without creating gold labels.""" | |
| if registry_path is None or audit_path is None: | |
| return {} | |
| if not registry_path.exists() or not audit_path.exists(): | |
| return {} | |
| registry = pd.read_csv(registry_path, dtype=str).fillna("") | |
| audit = pd.read_csv(audit_path, dtype=str).fillna("") | |
| required_registry = { | |
| "candidate_id", | |
| "official_event_page_url", | |
| "official_event_pdf_url", | |
| "official_affected_page_url", | |
| "official_affected_pdf_url", | |
| "relation_evidence_class", | |
| "expected_effective_date", | |
| } | |
| required_audit = { | |
| "candidate_id", | |
| "event_page_fetch", | |
| "event_page_sha256", | |
| "event_pdf_magic_valid", | |
| "event_pdf_sha256", | |
| "affected_page_fetch", | |
| "affected_page_sha256", | |
| "affected_pdf_magic_valid", | |
| "affected_pdf_sha256", | |
| "machine_verification_status", | |
| } | |
| missing_registry = required_registry.difference(registry.columns) | |
| missing_audit = required_audit.difference(audit.columns) | |
| if missing_registry: | |
| raise ValueError( | |
| f"official registry is missing columns: {sorted(missing_registry)}" | |
| ) | |
| if missing_audit: | |
| raise ValueError( | |
| f"official audit is missing columns: {sorted(missing_audit)}" | |
| ) | |
| if registry["candidate_id"].duplicated().any(): | |
| raise ValueError("official registry contains duplicate candidate_id values") | |
| if audit["candidate_id"].duplicated().any(): | |
| raise ValueError("official audit contains duplicate candidate_id values") | |
| joined = registry.merge( | |
| audit, | |
| on="candidate_id", | |
| how="inner", | |
| validate="one_to_one", | |
| suffixes=("_registry", "_audit"), | |
| ) | |
| prefill: dict[str, dict[str, str]] = {} | |
| for _, row in joined.iterrows(): | |
| event_pdf_valid = str(row["event_pdf_magic_valid"]).casefold() == "true" | |
| affected_pdf_valid = ( | |
| str(row["affected_pdf_magic_valid"]).casefold() == "true" | |
| ) | |
| event_page_valid = row["event_page_fetch"] == "fetched" | |
| affected_page_valid = row["affected_page_fetch"] == "fetched" | |
| prefill[str(row["candidate_id"])] = { | |
| "official_event_url": ( | |
| row["official_event_pdf_url"] | |
| if event_pdf_valid | |
| else row["official_event_page_url"] if event_page_valid else "" | |
| ), | |
| "official_event_artifact_sha256": ( | |
| row["event_pdf_sha256"] | |
| if event_pdf_valid | |
| else row["event_page_sha256"] if event_page_valid else "" | |
| ), | |
| "official_affected_url": ( | |
| row["official_affected_pdf_url"] | |
| if affected_pdf_valid | |
| else row["official_affected_page_url"] | |
| if affected_page_valid | |
| else "" | |
| ), | |
| "official_affected_artifact_sha256": ( | |
| row["affected_pdf_sha256"] | |
| if affected_pdf_valid | |
| else row["affected_page_sha256"] if affected_page_valid else "" | |
| ), | |
| "machine_verification_status": row["machine_verification_status"], | |
| "relation_evidence_class": row["relation_evidence_class_registry"], | |
| "expected_effective_date": row["expected_effective_date"], | |
| } | |
| return prefill | |
| def build(args: argparse.Namespace) -> dict: | |
| candidates = pd.read_csv(args.candidates, dtype=str).fillna("") | |
| required = { | |
| "candidate_id", | |
| "relation", | |
| "event_doc_id", | |
| "event_so_ky_hieu", | |
| "affected_doc_id", | |
| "affected_so_ky_hieu", | |
| "verification_status", | |
| } | |
| missing = required.difference(candidates.columns) | |
| if missing: | |
| raise ValueError(f"candidate manifest is missing columns: {sorted(missing)}") | |
| document_ids = sorted( | |
| set(candidates["event_doc_id"]) | set(candidates["affected_doc_id"]) | |
| ) | |
| contents = ( | |
| pq.read_table(args.content, filters=[("id", "in", document_ids)]) | |
| .to_pandas() | |
| .set_index("id")["content_html"] | |
| .to_dict() | |
| ) | |
| official_registry = getattr(args, "official_registry", None) | |
| official_audit = getattr(args, "official_audit", None) | |
| official_prefill = _load_official_prefill(official_registry, official_audit) | |
| audit_rows: list[dict] = [] | |
| provision_pairs: list[dict] = [] | |
| whole_document_events: list[dict] = [] | |
| verification_rows: list[dict] = [] | |
| annotation_rows: list[dict] = [] | |
| for _, row in candidates.iterrows(): | |
| source_prefill = official_prefill.get(row["candidate_id"], {}) | |
| event_group_id = f"event:{row['event_doc_id']}" | |
| event_html = str(contents.get(row["event_doc_id"], "")) | |
| affected_html = str(contents.get(row["affected_doc_id"], "")) | |
| event_text = html_to_legal_text(event_html) | |
| affected_text = html_to_legal_text(affected_html) | |
| event_articles = extract_articles(event_html) | |
| affected_articles = extract_articles(affected_html) | |
| affected_by_number: dict[str, list] = {} | |
| for article in affected_articles: | |
| affected_by_number.setdefault(article.article_number.casefold(), []).append( | |
| article | |
| ) | |
| event_by_number: dict[str, list] = {} | |
| for article in event_articles: | |
| event_by_number.setdefault(article.article_number.casefold(), []).append( | |
| article | |
| ) | |
| target_reference_found = contains_document_number( | |
| event_text, row["affected_so_ky_hieu"] | |
| ) | |
| mentions = extract_event_mentions( | |
| event_html, row["affected_so_ky_hieu"] | |
| ) | |
| referenced_articles = [] | |
| matched_articles = [] | |
| unmatched_articles = [] | |
| for mention in mentions: | |
| for number in mention.target_article_numbers: | |
| if number not in referenced_articles: | |
| referenced_articles.append(number) | |
| if number.casefold() in affected_by_number: | |
| if number not in matched_articles: | |
| matched_articles.append(number) | |
| elif number not in unmatched_articles: | |
| unmatched_articles.append(number) | |
| rejection_reasons = [] | |
| if not target_reference_found: | |
| rejection_reasons.append("target_document_not_named_in_event_content") | |
| if len(event_text) < args.minimum_text_chars: | |
| rejection_reasons.append("event_content_too_short") | |
| if len(affected_text) < args.minimum_text_chars: | |
| rejection_reasons.append("affected_content_too_short") | |
| if not event_articles: | |
| rejection_reasons.append("event_articles_not_parseable") | |
| if not affected_articles: | |
| rejection_reasons.append("affected_articles_not_parseable") | |
| if row["relation"] == "AMENDS": | |
| if not mentions: | |
| rejection_reasons.append("no_amendment_clause_extracted") | |
| if not matched_articles: | |
| rejection_reasons.append("no_target_article_resolved") | |
| auto_status = ( | |
| "eligible_for_manual_verification" | |
| if not rejection_reasons | |
| else "rejected_by_automated_checks" | |
| ) | |
| audit_rows.append( | |
| { | |
| "candidate_id": row["candidate_id"], | |
| "relation": row["relation"], | |
| "year_band": row.get("year_band", ""), | |
| "event_doc_id": row["event_doc_id"], | |
| "affected_doc_id": row["affected_doc_id"], | |
| "target_reference_found": target_reference_found, | |
| "event_text_chars": len(event_text), | |
| "affected_text_chars": len(affected_text), | |
| "event_article_count": len(event_articles), | |
| "affected_article_count": len(affected_articles), | |
| "event_mention_count": len(mentions), | |
| "referenced_target_articles": " | ".join(referenced_articles), | |
| "resolved_target_articles": " | ".join(matched_articles), | |
| "unresolved_target_articles": " | ".join(unmatched_articles), | |
| "event_content_sha256": sha256_text(event_html), | |
| "affected_content_sha256": sha256_text(affected_html), | |
| "auto_status": auto_status, | |
| "rejection_reasons": " | ".join(rejection_reasons), | |
| } | |
| ) | |
| verification_rows.append( | |
| { | |
| "protocol_version": ANNOTATION_PROTOCOL_VERSION, | |
| "event_group_id": event_group_id, | |
| "candidate_id": row["candidate_id"], | |
| "relation_discovery": row["relation"], | |
| "event_so_ky_hieu": row["event_so_ky_hieu"], | |
| "affected_so_ky_hieu": row["affected_so_ky_hieu"], | |
| "event_source_url_discovery": row.get("event_source_url", ""), | |
| "affected_source_url_discovery": row.get( | |
| "affected_source_url", "" | |
| ), | |
| "auto_status": auto_status, | |
| "machine_verification_status": source_prefill.get( | |
| "machine_verification_status", "not_machine_audited" | |
| ), | |
| "relation_evidence_class": source_prefill.get( | |
| "relation_evidence_class", "unregistered" | |
| ), | |
| "expected_effective_date_official": source_prefill.get( | |
| "expected_effective_date", "" | |
| ), | |
| "official_event_url": source_prefill.get( | |
| "official_event_url", "" | |
| ), | |
| "official_affected_url": source_prefill.get( | |
| "official_affected_url", "" | |
| ), | |
| "official_checked_at": "", | |
| "official_event_artifact_sha256": source_prefill.get( | |
| "official_event_artifact_sha256", "" | |
| ), | |
| "official_affected_artifact_sha256": source_prefill.get( | |
| "official_affected_artifact_sha256", "" | |
| ), | |
| "relation_class_annotator_1": "", | |
| "relation_class_annotator_2": "", | |
| "relation_class_adjudicated": "", | |
| "effect_scope_annotator_1": "", | |
| "effect_scope_annotator_2": "", | |
| "effect_scope_adjudicated": "", | |
| "valid_from_annotator_1": "", | |
| "valid_from_annotator_2": "", | |
| "valid_to_annotator_1": "", | |
| "valid_to_annotator_2": "", | |
| "valid_from_adjudicated": "", | |
| "valid_to_adjudicated": "", | |
| "effective_date_basis_annotator_1": "", | |
| "effective_date_basis_annotator_2": "", | |
| "effective_date_basis": "", | |
| "intermediate_instruments_annotator_1": "", | |
| "intermediate_instruments_annotator_2": "", | |
| "intermediate_instruments": "", | |
| "target_articles_annotator_1": "", | |
| "target_articles_annotator_2": "", | |
| "target_articles_adjudicated": "", | |
| "provision_mapping_annotator_1": "", | |
| "provision_mapping_annotator_2": "", | |
| "rationale_annotator_1": "", | |
| "rationale_annotator_2": "", | |
| "reviewed_at_annotator_1": "", | |
| "reviewed_at_annotator_2": "", | |
| "provision_mapping_adjudicated": "", | |
| "annotator_1": "", | |
| "annotator_2": "", | |
| "adjudicator": "", | |
| "adjudication_rationale": "", | |
| "verification_status": "pending_manual_verification", | |
| } | |
| ) | |
| if auto_status != "eligible_for_manual_verification": | |
| continue | |
| official_effective_date = source_prefill.get("expected_effective_date", "") | |
| event_date = ( | |
| pd.Timestamp(official_effective_date) | |
| if official_effective_date | |
| else parse_event_date(row) | |
| ) | |
| event_date_iso = event_date.date().isoformat() if event_date is not None else "" | |
| pre_date_iso = ( | |
| (event_date.date() - timedelta(days=1)).isoformat() | |
| if event_date is not None | |
| else "" | |
| ) | |
| if row["relation"] in {"REPLACES", "REPEALS"}: | |
| pair_id = f"{row['candidate_id']}:document" | |
| whole_document_events.append( | |
| { | |
| "schema_version": 1, | |
| "pair_id": pair_id, | |
| "candidate_id": row["candidate_id"], | |
| "discovery_relation": row["relation"], | |
| "event_effective_date": event_date_iso, | |
| "pre_snapshot_date": pre_date_iso, | |
| "post_snapshot_date": event_date_iso, | |
| "event_document_id": row["event_doc_id"], | |
| "event_document_number": row["event_so_ky_hieu"], | |
| "event_document_title": row.get("event_title", ""), | |
| "affected_document_id": row["affected_doc_id"], | |
| "affected_document_number": row["affected_so_ky_hieu"], | |
| "affected_document_title": row.get("affected_title", ""), | |
| "event_source_url_discovery": row.get("event_source_url", ""), | |
| "affected_source_url_discovery": row.get( | |
| "affected_source_url", "" | |
| ), | |
| "event_content_sha256": sha256_text(event_html), | |
| "affected_content_sha256": sha256_text(affected_html), | |
| "event_article_count": len(event_articles), | |
| "affected_article_count": len(affected_articles), | |
| "target_reference_spans": _target_reference_spans( | |
| event_text, row["affected_so_ky_hieu"] | |
| ), | |
| "post_semantics_candidate": ( | |
| "replacement_instrument_requires_provision_mapping" | |
| if row["relation"] == "REPLACES" | |
| else "repealed_without_successor_requires_abstention_review" | |
| ), | |
| "label_status": "automated_candidate_not_gold", | |
| } | |
| ) | |
| annotation_rows.append( | |
| { | |
| "protocol_version": ANNOTATION_PROTOCOL_VERSION, | |
| "event_group_id": event_group_id, | |
| "query_group_id": f"qg:{pair_id}", | |
| "pair_id": pair_id, | |
| "candidate_id": row["candidate_id"], | |
| "event_scope_candidate": "whole_document", | |
| "legal_issue_summary": "", | |
| "affected_question_1": "", | |
| "affected_question_2": "", | |
| "affected_question_3": "", | |
| "matched_unaffected_question": "", | |
| "matched_unaffected_expected_answer": "", | |
| "matched_unaffected_relevant_provision_ids": "", | |
| "pre_snapshot_date": pre_date_iso, | |
| "post_snapshot_date": event_date_iso, | |
| "expected_answer_pre": "", | |
| "expected_answer_post": "", | |
| "answer_source_spans": "", | |
| "pre_relevant_provision_ids": "", | |
| "post_relevant_provision_ids": "", | |
| "pre_status": "", | |
| "post_status": "", | |
| "question_quality_annotator_1": "", | |
| "question_quality_annotator_2": "", | |
| "question_quality_adjudicated": "", | |
| "rationale_annotator_1": "", | |
| "rationale_annotator_2": "", | |
| "reviewed_at_annotator_1": "", | |
| "reviewed_at_annotator_2": "", | |
| "annotator_1": "", | |
| "annotator_2": "", | |
| "adjudicator": "", | |
| "query_author": "", | |
| "adjudication_rationale": "", | |
| "annotation_status": "not_started", | |
| } | |
| ) | |
| for mention_index, mention in enumerate(mentions, start=1): | |
| source_articles = event_by_number.get( | |
| mention.source_article_number.casefold(), [] | |
| ) | |
| if len(source_articles) != 1: | |
| continue | |
| source_article = source_articles[0] | |
| for target_number in mention.target_article_numbers: | |
| target_articles = affected_by_number.get(target_number.casefold(), []) | |
| if len(target_articles) != 1: | |
| continue | |
| target_article = target_articles[0] | |
| pair_id = ( | |
| f"{row['candidate_id']}:s{mention.source_article_number}:" | |
| f"t{target_number}:m{mention_index}" | |
| ) | |
| record = { | |
| "schema_version": 1, | |
| "pair_id": pair_id, | |
| "candidate_id": row["candidate_id"], | |
| "discovery_relation": row["relation"], | |
| "extracted_action": mention.action, | |
| "event_effective_date": event_date_iso, | |
| "pre_snapshot_date": pre_date_iso, | |
| "post_snapshot_date": event_date_iso, | |
| "event_document_id": row["event_doc_id"], | |
| "event_document_number": row["event_so_ky_hieu"], | |
| "event_source_article_number": mention.source_article_number, | |
| "event_source_article_text": source_article.text, | |
| "affected_document_id": row["affected_doc_id"], | |
| "affected_document_number": row["affected_so_ky_hieu"], | |
| "affected_article_number": target_number, | |
| "old_provision_text": target_article.text, | |
| "extraction_evidence": mention.evidence_text, | |
| "event_source_url_discovery": row.get("event_source_url", ""), | |
| "affected_source_url_discovery": row.get( | |
| "affected_source_url", "" | |
| ), | |
| "event_content_sha256": sha256_text(event_html), | |
| "affected_content_sha256": sha256_text(affected_html), | |
| "label_status": "automated_candidate_not_gold", | |
| } | |
| provision_pairs.append(record) | |
| annotation_rows.append( | |
| { | |
| "protocol_version": ANNOTATION_PROTOCOL_VERSION, | |
| "event_group_id": event_group_id, | |
| "query_group_id": f"qg:{pair_id}", | |
| "pair_id": pair_id, | |
| "candidate_id": row["candidate_id"], | |
| "event_scope_candidate": "provision", | |
| "legal_issue_summary": "", | |
| "affected_question_1": "", | |
| "affected_question_2": "", | |
| "affected_question_3": "", | |
| "matched_unaffected_question": "", | |
| "matched_unaffected_expected_answer": "", | |
| "matched_unaffected_relevant_provision_ids": "", | |
| "pre_snapshot_date": pre_date_iso, | |
| "post_snapshot_date": event_date_iso, | |
| "expected_answer_pre": "", | |
| "expected_answer_post": "", | |
| "answer_source_spans": "", | |
| "pre_relevant_provision_ids": "", | |
| "post_relevant_provision_ids": "", | |
| "pre_status": "", | |
| "post_status": "", | |
| "question_quality_annotator_1": "", | |
| "question_quality_annotator_2": "", | |
| "question_quality_adjudicated": "", | |
| "rationale_annotator_1": "", | |
| "rationale_annotator_2": "", | |
| "reviewed_at_annotator_1": "", | |
| "reviewed_at_annotator_2": "", | |
| "annotator_1": "", | |
| "annotator_2": "", | |
| "adjudicator": "", | |
| "query_author": "", | |
| "adjudication_rationale": "", | |
| "annotation_status": "not_started", | |
| } | |
| ) | |
| args.output_dir.mkdir(parents=True, exist_ok=True) | |
| audit = pd.DataFrame(audit_rows) | |
| verification = pd.DataFrame(verification_rows) | |
| annotation = pd.DataFrame(annotation_rows) | |
| qrel_template = pd.DataFrame(columns=QREL_TEMPLATE_COLUMNS) | |
| audit_path = args.output_dir / "candidate_content_audit.csv" | |
| verification_path = args.output_dir / "official_verification_template.csv" | |
| annotation_path = args.output_dir / "expert_annotation_template.csv" | |
| qrel_path = args.output_dir / "qrel_judgment_template.csv" | |
| pairs_path = args.output_dir / "provision_event_pairs.jsonl" | |
| whole_document_path = args.output_dir / "whole_document_events.jsonl" | |
| audit.to_csv(audit_path, index=False) | |
| verification.to_csv(verification_path, index=False) | |
| annotation.to_csv(annotation_path, index=False) | |
| qrel_template.to_csv(qrel_path, index=False) | |
| _write_jsonl(pairs_path, provision_pairs) | |
| _write_jsonl(whole_document_path, whole_document_events) | |
| eligible = audit["auto_status"].eq("eligible_for_manual_verification") | |
| manifest = { | |
| "schema_version": 1, | |
| "artifact_type": "temporal_pilot_annotation_input", | |
| "annotation_protocol_version": ANNOTATION_PROTOCOL_VERSION, | |
| "source_candidates": str(args.candidates), | |
| "source_content_snapshot": str(args.content), | |
| "source_content_file_sha256": sha256_file(args.content), | |
| "official_registry": str(official_registry) if official_registry else "", | |
| "official_audit": str(official_audit) if official_audit else "", | |
| "candidate_count": int(len(audit)), | |
| "eligible_for_manual_verification": int(eligible.sum()), | |
| "rejected_by_automated_checks": int((~eligible).sum()), | |
| "eligible_relation_counts": dict( | |
| Counter(audit.loc[eligible, "relation"].astype(str)) | |
| ), | |
| "rejection_reason_counts": dict( | |
| Counter( | |
| reason | |
| for reasons in audit.loc[~eligible, "rejection_reasons"] | |
| for reason in str(reasons).split(" | ") | |
| if reason | |
| ) | |
| ), | |
| "provision_event_pair_count": len(provision_pairs), | |
| "whole_document_event_count": len(whole_document_events), | |
| "annotation_group_count": len(annotation_rows), | |
| "official_source_prefill": { | |
| "registered_candidate_count": len(official_prefill), | |
| "event_artifact_count": int( | |
| verification["official_event_artifact_sha256"].ne("").sum() | |
| ), | |
| "affected_artifact_count": int( | |
| verification["official_affected_artifact_sha256"].ne("").sum() | |
| ), | |
| "complete_pair_count": int( | |
| ( | |
| verification["official_event_artifact_sha256"].ne("") | |
| & verification["official_affected_artifact_sha256"].ne("") | |
| ).sum() | |
| ), | |
| "warning": ( | |
| "Prefilled URLs and hashes prove fetched-artifact provenance only; " | |
| "all legal judgments and verification_status remain blank/pending." | |
| ), | |
| }, | |
| "semantic_status": ( | |
| "All records are discovery/annotation inputs. No relation, legal status, " | |
| "relevance judgment, or official-source verification is gold yet." | |
| ), | |
| "files": { | |
| "candidate_content_audit": str(audit_path), | |
| "official_verification_template": str(verification_path), | |
| "expert_annotation_template": str(annotation_path), | |
| "qrel_judgment_template": str(qrel_path), | |
| "provision_event_pairs": str(pairs_path), | |
| "whole_document_events": str(whole_document_path), | |
| }, | |
| } | |
| manifest_path = args.output_dir / "manifest.json" | |
| manifest_path.write_text( | |
| json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" | |
| ) | |
| receipt = { | |
| "schema_version": 1, | |
| "artifact_type": "temporal_pilot_handoff_receipt", | |
| "generated_at": str(pd.Timestamp.now(tz="UTC").date()), | |
| "annotation_protocol_version": ANNOTATION_PROTOCOL_VERSION, | |
| "source_hashes": { | |
| "discovery_candidates_sha256": sha256_file(args.candidates), | |
| "content_snapshot_sha256": sha256_file(args.content), | |
| "official_registry_sha256": sha256_file(official_registry) | |
| if official_registry and official_registry.exists() | |
| else "", | |
| "official_audit_sha256": sha256_file(official_audit) | |
| if official_audit and official_audit.exists() | |
| else "", | |
| }, | |
| "candidate_count": manifest["candidate_count"], | |
| "eligible_for_manual_verification": manifest[ | |
| "eligible_for_manual_verification" | |
| ], | |
| "eligible_relation_counts": manifest["eligible_relation_counts"], | |
| "provision_event_pair_count": manifest["provision_event_pair_count"], | |
| "whole_document_event_count": manifest["whole_document_event_count"], | |
| "annotation_group_count": manifest["annotation_group_count"], | |
| "official_source_prefill": manifest["official_source_prefill"], | |
| "handoff_file_sha256": { | |
| "candidate_content_audit": sha256_file(audit_path), | |
| "official_verification_template": sha256_file(verification_path), | |
| "expert_annotation_template": sha256_file(annotation_path), | |
| "qrel_judgment_template": sha256_file(qrel_path), | |
| "provision_event_pairs": sha256_file(pairs_path), | |
| "whole_document_events": sha256_file(whole_document_path), | |
| }, | |
| "gold_rows": 0, | |
| "learned_residual_authorized": False, | |
| "warning": ( | |
| "This receipt inventories non-gold annotation inputs. It does not " | |
| "promote machine-confirmed provenance to legal or relevance labels." | |
| ), | |
| } | |
| args.receipt.parent.mkdir(parents=True, exist_ok=True) | |
| args.receipt.write_text( | |
| json.dumps(receipt, ensure_ascii=False, indent=2) + "\n", | |
| encoding="utf-8", | |
| ) | |
| return manifest | |
| def parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument( | |
| "--candidates", | |
| type=Path, | |
| default=REPO_ROOT / "tmp/telen/bootstrap-audit/discovery_candidates.csv", | |
| ) | |
| parser.add_argument( | |
| "--content", | |
| type=Path, | |
| default=( | |
| REPO_ROOT | |
| / "tmp/deep-research/telen/vn-legal-data/data/content.parquet" | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--output-dir", | |
| type=Path, | |
| default=REPO_ROOT / "tmp/telen/temporal-pilot", | |
| ) | |
| parser.add_argument( | |
| "--official-registry", | |
| type=Path, | |
| default=( | |
| REPO_ROOT | |
| / "research/data/telen_official_source_registry_2026-08-29.csv" | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--official-audit", | |
| type=Path, | |
| default=( | |
| REPO_ROOT | |
| / "tmp/telen/official-source-audit/verification_audit.csv" | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--receipt", | |
| type=Path, | |
| default=( | |
| REPO_ROOT | |
| / "research/results/telen_temporal_pilot_handoff_2026-08-29.json" | |
| ), | |
| ) | |
| parser.add_argument("--minimum-text-chars", type=int, default=500) | |
| return parser.parse_args() | |
| def main() -> None: | |
| args = parse_args() | |
| if args.minimum_text_chars < 1: | |
| raise ValueError("--minimum-text-chars must be positive") | |
| manifest = build(args) | |
| print(json.dumps(manifest, ensure_ascii=False, indent=2)) | |
| if __name__ == "__main__": | |
| main() | |