init: llm-wiki-haness 하네스 설계

This commit is contained in:
DongHyeonka
2026-07-24 14:21:35 +09:00
parent 42bf3db4fd
commit 6c53ded9cb
2436 changed files with 194486 additions and 1 deletions
+193
View File
@@ -0,0 +1,193 @@
#!/usr/bin/env python3
"""Small, dependency-free parsers for the wiki's structured Markdown contracts."""
from __future__ import annotations
from dataclasses import dataclass
import re
from typing import Iterable
FM_RE = re.compile(r"^([A-Za-z_][\w-]*):\s*(.*)$")
HEADING_RE = re.compile(r"^(#{1,6})\s+(.+?)\s*$")
TABLE_SEP_RE = re.compile(r"^:?-{3,}:?$")
SECTION_ID_RE = re.compile(r"^\s*<!--\s*section-id:\s*([a-z0-9][a-z0-9-]*)\s*-->\s*$")
# 셀 전체가 하나의 코드스팬/강조일 때만 벗긴다. 예전에는 strip("`* ") 로 양끝 문자를
# 무조건 깎았는데, 그러면 코드스팬으로 *시작만* 하는 셀이 여는 백틱을 잃는다 —
# "`domain <- application` 의존 방향과 …" 가 "domain <- application` 의존 방향과 …" 가 돼
# 투영된 표 11곳(hub 자신의 생성 블록 포함)에서 코드스팬이 깨져 있었다.
SINGLE_CODE_SPAN_RE = re.compile(r"^`([^`]*)`$")
SINGLE_EMPHASIS_RE = re.compile(r"^(\*{1,2})([^*]*)\1$")
def clean(value: str) -> str:
value = value.strip()
while True:
match = SINGLE_CODE_SPAN_RE.match(value) or SINGLE_EMPHASIS_RE.match(value)
if match is None:
break
value = match.group(match.lastindex).strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in "'\"":
value = value[1:-1]
return value.strip()
def parse_frontmatter(text: str) -> dict[str, object]:
lines = text.splitlines()
if not lines or lines[0].strip() != "---":
return {}
values: dict[str, object] = {}
current: str | None = None
for line in lines[1:]:
if line.strip() == "---":
break
match = FM_RE.match(line)
if match:
current = match.group(1)
raw = match.group(2).strip()
if raw.startswith("[") and raw.endswith("]"):
values[current] = [clean(item) for item in raw[1:-1].split(",") if clean(item)]
else:
values[current] = clean(raw)
continue
item = re.match(r"^\s+-\s+(.+?)\s*$", line)
if item and current:
if not isinstance(values.get(current), list):
values[current] = []
assert isinstance(values[current], list)
values[current].append(clean(item.group(1)))
return values
def as_list(value: object) -> list[str]:
if isinstance(value, list):
return [str(item).strip() for item in value if str(item).strip()]
if isinstance(value, str) and value.strip():
return [value.strip()]
return []
def split_row(line: str) -> list[str]:
token = "\x00PIPE\x00"
return [
cell.strip().replace(token, "|")
for cell in line.strip().strip("|").replace("\\|", token).split("|")
]
def header_key(value: str) -> str:
return re.sub(r"[^0-9a-zA-Z가-힣]+", "", value).lower()
@dataclass(frozen=True)
class MarkdownTable:
headings: tuple[str, ...]
section_ids: tuple[str, ...]
headers: tuple[str, ...]
header_line: int
rows: tuple[tuple[int, dict[str, str]], ...]
def parse_tables(text: str) -> list[MarkdownTable]:
lines = text.splitlines()
headings: dict[int, str] = {}
section_ids: dict[int, str] = {}
pending_section_id = ""
tables: list[MarkdownTable] = []
index = 0
while index < len(lines):
section_id = SECTION_ID_RE.match(lines[index])
if section_id:
pending_section_id = section_id.group(1)
index += 1
continue
heading = HEADING_RE.match(lines[index])
if heading:
level = len(heading.group(1))
headings = {key: value for key, value in headings.items() if key < level}
section_ids = {key: value for key, value in section_ids.items() if key < level}
headings[level] = heading.group(2).strip()
if pending_section_id:
section_ids[level] = pending_section_id
pending_section_id = ""
index += 1
continue
if (
lines[index].lstrip().startswith("|")
and index + 1 < len(lines)
and lines[index + 1].lstrip().startswith("|")
):
headers = split_row(lines[index])
separators = split_row(lines[index + 1])
if len(headers) == len(separators) and all(TABLE_SEP_RE.fullmatch(item) for item in separators):
rows: list[tuple[int, dict[str, str]]] = []
cursor = index + 2
keys = [header_key(header) for header in headers]
while cursor < len(lines) and lines[cursor].lstrip().startswith("|"):
cells = split_row(lines[cursor])
cells += [""] * (len(headers) - len(cells))
rows.append((cursor + 1, dict(zip(keys, cells))))
cursor += 1
tables.append(
MarkdownTable(
headings=tuple(headings.values()),
section_ids=tuple(section_ids.values()),
headers=tuple(headers),
header_line=index + 1,
rows=tuple(rows),
)
)
index = cursor
continue
index += 1
return tables
def table_for(tables: Iterable[MarkdownTable], *headings: str) -> MarkdownTable | None:
"""Return the first table under any accepted localized heading.
Structured column names remain stable machine schema. Section headings are
presentation text, so readers and migration tools accept both the current
Korean title and the legacy English title during rollout.
"""
needles = tuple(heading.casefold() for heading in headings)
return next(
(
table
for table in tables
if any(
needle in item.casefold()
for needle in needles
for item in (*table.headings, *(f"section-id:{value}" for value in table.section_ids))
)
),
None,
)
def cell(row: dict[str, str], name: str) -> str:
return row.get(header_key(name), "")
def replace_table_cell(
text: str,
table: MarkdownTable,
row_line: int,
header_name: str,
value: str,
) -> str:
keys = [header_key(header) for header in table.headers]
target_key = header_key(header_name)
if target_key not in keys:
raise ValueError(f"table has no {header_name!r} column")
lines = text.splitlines(keepends=True)
original = lines[row_line - 1]
newline = "\n" if original.endswith("\n") else ""
cells = split_row(original.rstrip("\n"))
cells += [""] * (len(keys) - len(cells))
cells[keys.index(target_key)] = value
rendered = [item.replace("|", "\\|") for item in cells[: len(keys)]]
lines[row_line - 1] = "| " + " | ".join(rendered) + " |" + newline
return "".join(lines)