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,
}
|