parsa-hke's picture
Negative-Space Reader demo: ZeroGPU bundle (app/space_bundle.py, project commit fc6e4a9)
7103d1c verified
Raw History Blame Contribute Delete
4.33 kB
"""Document / Section / Sentence / Assertion records and jsonl I/O (plan §2.3).
Every corpus is converted into these records by its loader (data/loaders/), so nothing downstream knows
which corpus a document came from. Field meanings:
Document id globally unique: "<source>:<native id>[:<version>]"
genre pack name ("privacy" | "papers"); the engine only ever compares within a genre
source corpus name ("privacy_pl" | "opp115" | "unarxive")
entity_id what versions belong to: website domain for policies, arXiv id for papers
version position in the entity's chain (policies: snapshot label; papers: "v1", "v2", ...)
date ISO date (YYYY-MM-DD) of this version
category website / app category or arXiv primary category; None when unknown
title document title if the corpus has one
meta corpus-specific extras, kept verbatim for provenance (never read by the engine)
Section heading is None when the source has no heading; char_span is (start, end) into the
document's concatenated section text (sections joined by "\\n\\n").
Assertion produced from Phase 3 on; prob is the calibrated P(present).
"""
from __future__ import annotations
import gzip
import json
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any, Iterable, Iterator, TypeVar
SECTION_JOINER = "\n\n"
@dataclass
class Document:
id: str
genre: str
source: str
entity_id: str
version: str | None = None
date: str | None = None
category: str | None = None
title: str | None = None
meta: dict[str, Any] = field(default_factory=dict)
@dataclass
class Section:
doc_id: str
idx: int
heading: str | None
text: str
char_span: tuple[int, int] = (0, 0)
@dataclass
class Sentence:
doc_id: str
section_idx: int
idx: int
text: str
@dataclass
class Assertion:
doc_id: str
section_idx: int
type_id: str
prob: float
evidence_sentence_ids: list[int] = field(default_factory=list)
@dataclass
class RevisionPair:
"""Two consecutive, materially different versions of one entity (plan §2.6b, OmitBench-revision)."""
entity_id: str
genre: str
old_doc_id: str
new_doc_id: str
old_date: str | None
new_date: str | None
change_ratio: float # 1 - similarity of the two versions' normalised text
@dataclass
class ParsedDocument:
"""What a loader yields: one document with its sections, before sentence splitting."""
document: Document
sections: list[Section]
def text(self) -> str:
return SECTION_JOINER.join(s.text for s in self.sections)
def with_char_spans(sections: list[Section]) -> list[Section]:
"""Fill char_span offsets for sections joined with SECTION_JOINER; renumbers idx from 0."""
out, pos = [], 0
for i, s in enumerate(sections):
out.append(Section(doc_id=s.doc_id, idx=i, heading=s.heading, text=s.text,
char_span=(pos, pos + len(s.text))))
pos += len(s.text) + len(SECTION_JOINER)
return out
# ------------------------------------------------------------------------------------ jsonl
R = TypeVar("R", Document, Section, Sentence, Assertion, RevisionPair)
def _open(path: Path, mode: str):
path = Path(path)
if path.suffix == ".gz":
return gzip.open(path, mode + "t", encoding="utf-8")
return open(path, mode, encoding="utf-8")
def write_jsonl(path: str | Path, records: Iterable[Any]) -> int:
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
n = 0
with _open(path, "w") as f:
for r in records:
f.write(json.dumps(asdict(r) if hasattr(r, "__dataclass_fields__") else r, ensure_ascii=False) + "\n")
n += 1
return n
def read_jsonl(path: str | Path, cls: type[R] | None = None) -> Iterator[R | dict[str, Any]]:
with _open(Path(path), "r") as f:
for line in f:
if not line.strip():
continue
row = json.loads(line)
if cls is Section and "char_span" in row:
row["char_span"] = tuple(row["char_span"])
yield cls(**row) if cls is not None else row