File size: 8,012 Bytes
f770448
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
"""Math Ink 0.6 ๋ฐ์ดํ„ฐ ์ถœ์ฒ˜์˜ ๋ฐœ๊ฒฌยท๊ถŒ๋ฆฌยท์ค‘๋ณตยทํ•™์Šต ์ค€๋น„ ๋‹จ๊ณ„๋ฅผ ๊ฒ€์ฆํ•œ๋‹ค."""

from __future__ import annotations

from dataclasses import dataclass
import json
from pathlib import Path
from typing import Sequence


SOURCE_STAGES = ("discovered", "rights_review", "deduplicated", "approved")
TRAINING_ROLES = (
    "supervised_symbol", "geometry_pretrain", "raster_verifier", "raster_pseudo_stroke", "evaluation_only",
)
VOCABULARY_POLICIES = (
    "exact_378", "intersection_378", "digits_latin_intersection", "geometry_only", "evaluation_only",
)


@dataclass(frozen=True, slots=True)
class SourceRegistryEntry06:
    """ํ•„์š” ๋ณ€์ˆ˜: ์ถœ์ฒ˜ ๊ถŒ๋ฆฌยท์ค‘๋ณตยท๋กœ์ปฌ ์ƒํƒœ. ์ž‘๋™ ์›๋ฆฌ: ๋ฐฐํฌ checkpoint ์œ ์ž… ์—ฌ๋ถ€๋ฅผ ๋ช…์‹œ์ ์ธ ํ•œ ํ–‰์œผ๋กœ ํ‘œํ˜„ํ•œ๋‹ค."""

    source_id: str
    stage: str
    official_url: str
    license_id: str | None
    commercial_allowed: bool | None
    allowed_tracks: tuple[str, ...]
    independent_source_group: str | None
    deployment_role: str | None
    vocabulary_policy: str | None
    local_materialized: bool
    notes: str


def load_source_registry06(path: Path) -> tuple[SourceRegistryEntry06, ...]:
    """ํ•„์š” ๋ณ€์ˆ˜: UTF-8 source registry ๊ฒฝ๋กœ. ์ž‘๋™ ์›๋ฆฌ: fail-closed schema ๊ฒ€์ฆ ํ›„ ๋ถˆ๋ณ€ entry ๋ฌถ์Œ์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค."""

    payload = json.loads(path.read_text(encoding="utf-8"))
    if payload.get("schema_version") != "0.6":
        raise ValueError("Math Ink source registry schema_version์€ 0.6์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.")
    raw_sources = payload.get("sources")
    if not isinstance(raw_sources, list):
        raise ValueError("Math Ink source registry sources๊ฐ€ ๋ฐฐ์—ด์ด ์•„๋‹™๋‹ˆ๋‹ค.")
    entries: list[SourceRegistryEntry06] = []
    seen: set[str] = set()
    for raw in raw_sources:
        source_id = str(raw.get("source_id") or "").strip()
        if not source_id or source_id in seen:
            raise ValueError(f"source_id๊ฐ€ ๋น„์—ˆ๊ฑฐ๋‚˜ ์ค‘๋ณต์ž…๋‹ˆ๋‹ค: {source_id!r}")
        seen.add(source_id)
        stage = str(raw.get("stage") or "")
        if stage not in SOURCE_STAGES:
            raise ValueError(f"{source_id}์˜ stage๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค: {stage}")
        official_url = str(raw.get("official_url") or "")
        if not official_url.startswith("https://"):
            raise ValueError(f"{source_id}์˜ ๊ณต์‹ HTTPS URL์ด ์—†์Šต๋‹ˆ๋‹ค.")
        license_id = raw.get("license_id")
        commercial_allowed = raw.get("commercial_allowed")
        group = raw.get("independent_source_group")
        role = raw.get("deployment_role")
        vocabulary_policy = raw.get("vocabulary_policy")
        if SOURCE_STAGES.index(stage) >= SOURCE_STAGES.index("rights_review"):
            if not license_id or not isinstance(commercial_allowed, bool):
                raise ValueError(f"{source_id}์˜ ๊ถŒ๋ฆฌ ๊ฒ€ํ†  ๊ฒฐ๊ณผ๊ฐ€ ๋ถˆ์™„์ „ํ•ฉ๋‹ˆ๋‹ค.")
        if SOURCE_STAGES.index(stage) >= SOURCE_STAGES.index("deduplicated") and not group:
            raise ValueError(f"{source_id}์˜ independent_source_group์ด ์—†์Šต๋‹ˆ๋‹ค.")
        if stage == "approved":
            if commercial_allowed is not True or "P" not in raw.get("allowed_tracks", []):
                raise ValueError(f"{source_id}๋Š” ์ƒ์šฉ P-track ์Šน์ธ ์กฐ๊ฑด์„ ์ถฉ์กฑํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.")
            if role not in TRAINING_ROLES or vocabulary_policy not in VOCABULARY_POLICIES:
                raise ValueError(f"{source_id}์˜ ๋ฐฐํฌ ์—ญํ•  ๋˜๋Š” vocabulary ์ •์ฑ…์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")
            if not bool(raw.get("local_materialized", False)):
                raise ValueError(f"{source_id}๋Š” content dedup ์ „์ด๋ผ approved๊ฐ€ ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
        entries.append(SourceRegistryEntry06(
            source_id=source_id, stage=stage, official_url=official_url,
            license_id=str(license_id) if license_id else None,
            commercial_allowed=commercial_allowed if isinstance(commercial_allowed, bool) else None,
            allowed_tracks=tuple(str(value) for value in raw.get("allowed_tracks", [])),
            independent_source_group=str(group) if group else None,
            deployment_role=str(role) if role else None,
            vocabulary_policy=str(vocabulary_policy) if vocabulary_policy else None,
            local_materialized=bool(raw.get("local_materialized", False)), notes=str(raw.get("notes") or ""),
        ))
    return tuple(entries)


def approved_training_source_ids06(
    entries: Sequence[SourceRegistryEntry06], *, role: str = "supervised_symbol",
) -> tuple[str, ...]:
    """ํ•„์š” ๋ณ€์ˆ˜: ๊ฒ€์ฆ๋œ entryยทํ•™์Šต ์—ญํ• . ์ž‘๋™ ์›๋ฆฌ: ์Šน์ธยท์ƒ์šฉยท๋กœ์ปฌ ์ค€๋น„ ์กฐ๊ฑด์„ ๋ชจ๋‘ ๋งŒ์กฑํ•œ ์ถœ์ฒ˜๋งŒ ๋ฐ˜ํ™˜ํ•œ๋‹ค."""

    if role not in TRAINING_ROLES:
        raise ValueError(f"์ง€์›ํ•˜์ง€ ์•Š๋Š” deployment role์ž…๋‹ˆ๋‹ค: {role}")
    return tuple(sorted(
        entry.source_id for entry in entries
        if entry.stage == "approved" and entry.commercial_allowed is True and "P" in entry.allowed_tracks
        and entry.local_materialized and entry.deployment_role == role
    ))


def source_registry_audit06(
    entries: Sequence[SourceRegistryEntry06],
    *,
    required_discovered_sources: int = 200,
    required_approved_groups: int = 30,
) -> dict:
    """ํ•„์š” ๋ณ€์ˆ˜: ๊ฒ€์ฆ๋œ ์ถœ์ฒ˜ยท๋ชฉํ‘œ ์ˆ˜. ์ž‘๋™ ์›๋ฆฌ: ์กฐ์‚ฌ ์ˆ˜์™€ ๋ฏธ๋Ÿฌ ์ œ๊ฑฐ ๋…๋ฆฝ ๋ฐฐํฌ ๊ทธ๋ฃน์„ ๋ณ„๋„ hard gate๋กœ ๊ณ„์‚ฐํ•œ๋‹ค."""

    approved = [entry for entry in entries if entry.stage == "approved"]
    approved_deployment = [
        entry for entry in approved
        if entry.deployment_role != "evaluation_only"
    ]
    rights_cleared = [
        entry for entry in entries
        if entry.commercial_allowed is True and entry.license_id is not None
    ]
    groups = {entry.independent_source_group for entry in approved if entry.independent_source_group}
    deployment_groups = {
        entry.independent_source_group
        for entry in approved_deployment
        if entry.independent_source_group
    }
    group_members: dict[str, list[str]] = {}
    for entry in entries:
        if entry.independent_source_group:
            group_members.setdefault(entry.independent_source_group, []).append(entry.source_id)
    ready = approved_training_source_ids06(entries)
    materialized_by_role = {
        role: sorted(entry.source_id for entry in approved if entry.local_materialized and entry.deployment_role == role)
        for role in TRAINING_ROLES
    }
    return {
        "discovered_sources": len(entries),
        "required_discovered_sources": required_discovered_sources,
        "discovery_gate_passed": len(entries) >= required_discovered_sources,
        "stage_counts": {stage: sum(entry.stage == stage for entry in entries) for stage in SOURCE_STAGES},
        "approved_sources": len(approved),
        "approved_independent_groups": len(groups),
        "approved_deployment_sources": len(approved_deployment),
        "approved_deployment_independent_groups": len(deployment_groups),
        "required_approved_independent_groups": required_approved_groups,
        "approved_group_gate_passed": len(deployment_groups) >= required_approved_groups,
        "source_release_gate_passed": (
            len(entries) >= required_discovered_sources
            and len(deployment_groups) >= required_approved_groups
        ),
        "shared_independent_groups": {
            group: sorted(members)
            for group, members in sorted(group_members.items())
            if len(members) > 1
        },
        "rights_cleared_sources": sorted(entry.source_id for entry in rights_cleared),
        "rights_cleared_source_count": len(rights_cleared),
        "rights_cleared_pending_materialization": sorted(
            entry.source_id for entry in rights_cleared if entry.stage != "approved"
        ),
        "materialized_supervised_sources": list(ready),
        "materialized_supervised_source_count": len(ready),
        "materialized_by_role": materialized_by_role,
    }