File size: 5,123 Bytes
300df0f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
"""
Data models for the segmentation pipeline.

All dataclasses are pure Python β€” no BeautifulSoup or Neo4j dependency.
This lets cross_reference/ and the application layer import them freely.

UID conventions (must match Neo4j schema from T1.4 / Người A):
  Article.uid  = "doc_{doc_id}_dieu_{index}"
  Clause.uid   = "doc_{doc_id}_dieu_{dieu_idx}_khoan_{idx}"
  Point.uid    = "doc_{doc_id}_dieu_{dieu_idx}_khoan_{khoan_idx}_diem_{letter}"
  Chapter has no uid β€” identified by (doc_id, roman_index)
"""
from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum
from typing import Optional


# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------

class HierarchyType(str, Enum):
    """Structural level of a parsed segment."""
    PHAN    = "PhαΊ§n"       # Part  β€” above ChΖ°Ζ‘ng, used in Bα»™ luαΊ­t (gap in spec)
    CHUONG  = "ChΖ°Ζ‘ng"     # Chapter
    MUC     = "Mα»₯c"        # Section β€” between ChΖ°Ζ‘ng and Điều
    DIEU    = "Điều"       # Article
    KHOAN   = "KhoαΊ£n"      # Clause
    DIEM    = "Điểm"       # Point
    UNKNOWN = "unknown"


class ConfidenceLevel(str, Enum):
    HIGH   = "high"    # β‰₯ 0.9
    MEDIUM = "medium"  # 0.6 – 0.9
    LOW    = "low"     # < 0.6


# ---------------------------------------------------------------------------
# Core segment dataclass
# ---------------------------------------------------------------------------

@dataclass
class Segment:
    """
    One structural node produced by the parser.

    This is the primary output unit of LegalDocumentParser and the
    primary input unit of SegmentWriter and ArticleEmbedder.

    Interface note for Người A:
      - doc_id must match Document.id already in Neo4j (from T1.4 / T1.5)
    Interface note for cross_reference/ (Người B, Phase 2):
      - article_uid, clause_uid, point_uid are the stable IDs used in
        InternalRef.source_article_uid / target_article_uid
    """
    # Identity
    doc_id: str
    hierarchy_type: HierarchyType
    index: any                          # position or label (int or str)

    # Hierarchy path β€” human-readable, e.g. "ChΖ°Ζ‘ng I / Điều 5 / KhoαΊ£n 2"
    path: str = ""

    # Content
    text_content: str = ""              # raw HTML of this segment
    clean_text: str = ""                # plain text, no HTML tags

    # Parent linkage
    parent_uid: Optional[str] = None    # UID of parent segment; None for top-level

    # Computed UIDs (set after parsing, before Neo4j write)
    uid: Optional[str] = None           # own stable UID

    # Chapter-specific
    roman_index: Optional[str] = None   # "I", "II", "III"...
    title: Optional[str] = None         # heading text (ChΖ°Ζ‘ng/Điều/Mα»₯c title)
    section: Optional[str] = None       # section context (Mα»₯c), stored as metadata instead of separate node

    # Embedding (set by ArticleEmbedder, only for DIEU level)
    embedding: Optional[list[float]] = None  # 1024-dim vector

    # Parser metadata
    parse_notes: list[str] = field(default_factory=list)  # warnings / edge cases

    # ── Computed properties ────────────────────────────────────────────────

    @property
    def article_uid(self) -> Optional[str]:
        """Convenience accessor used by cross_reference/."""
        return self.uid if self.hierarchy_type == HierarchyType.DIEU else None


@dataclass
class ParseResult:
    """
    Full output for one document from LegalDocumentParser.

    Consumed by:
      - SegmentWriter  (T1.5) β†’ Neo4j ingest
      - ArticleEmbedder (T1.6) β†’ embedding generation
      - ConfidenceScorer (T1.2) β†’ quality check
      - cross_reference/extractor.py (T2.1-T2.3) β†’ reference extraction
    """
    doc_id: str
    segments: list[Segment] = field(default_factory=list)

    # Confidence (set by ConfidenceScorer after parsing)
    confidence_score: float = 0.0
    confidence_level: ConfidenceLevel = ConfidenceLevel.LOW
    confidence_notes: list[str] = field(default_factory=list)

    # Parse statistics
    chapter_count: int = 0
    article_count: int = 0
    clause_count: int = 0
    point_count: int = 0
    parse_errors: list[str] = field(default_factory=list)

    # ── Convenience accessors ──────────────────────────────────────────────

    def articles(self) -> list[Segment]:
        return [s for s in self.segments if s.hierarchy_type == HierarchyType.DIEU]

    def clauses_of(self, article_uid: str) -> list[Segment]:
        return [s for s in self.segments
                if s.hierarchy_type == HierarchyType.KHOAN
                and s.parent_uid == article_uid]

    def points_of(self, clause_uid: str) -> list[Segment]:
        return [s for s in self.segments
                if s.hierarchy_type == HierarchyType.DIEM
                and s.parent_uid == clause_uid]