Initial commit: ABAP indexing pipeline (ingest, parser, summarize, index, query, wiki)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
"""사람 교정 보존 병합 (계획서 v4 §5.7).
|
||||
|
||||
대상은 git 에 커밋되는 program/package/concept 문서. units/ 는 재생성 산출물이라
|
||||
merge 없이 항상 덮어쓴다(호출부에서 결정).
|
||||
|
||||
사람 교정 여부는 OKF v0.2 표준 필드로 판별한다 (docs/okf-version.md):
|
||||
verified: { by: "human:<id>", at: <ISO-8601> } ← 사람이 검토·수정 후 저장할 때 추가
|
||||
(구 계획서의 x-human-edited: true 규약도 하위호환으로 인식한다.)
|
||||
|
||||
규칙:
|
||||
- verified(human) 없는 문서 → 새 내용으로 덮어쓴다.
|
||||
- verified(human) 있는 문서 → 본문을 보존하고 관리 대상 사실 필드만 갱신한다.
|
||||
코드 해시가 바뀐 경우에는 새 요약을 "## (자동 생성) 변경 후 요약" 섹션으로 덧붙이고
|
||||
status: draft 로 내린 뒤 wiki/_review.md 에 재검토 항목을 올린다.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
FM_RE = re.compile(r"\A---\r?\n(.*?)\r?\n---\r?\n?", re.S)
|
||||
AUTO_SECTION = "## (자동 생성) 변경 후 요약"
|
||||
|
||||
# 배치가 항상 갱신하는 사실 필드 (모두 한 줄로 emit 됨 — okf_writer 참조)
|
||||
MANAGED_KEYS = (
|
||||
"resource", "tags", "generated",
|
||||
"x-lines", "x-tables-read", "x-tables-write", "x-calls", "x-outputs",
|
||||
"x-code-hash", "x-prompt-version", "x-unit-count", "x-chunk-count",
|
||||
)
|
||||
|
||||
|
||||
def split_frontmatter(text: str) -> tuple[str | None, str]:
|
||||
"""(frontmatter 내용, 본문) — frontmatter 가 없으면 (None, 전체)."""
|
||||
m = FM_RE.match(text)
|
||||
if not m:
|
||||
return None, text
|
||||
return m.group(1), text[m.end():]
|
||||
|
||||
|
||||
def is_human_verified(frontmatter: str | None) -> bool:
|
||||
if not frontmatter:
|
||||
return False
|
||||
if re.search(r"""by:\s*['"]?human:""", frontmatter):
|
||||
return True
|
||||
return bool(re.search(r"^x-human-edited:\s*true\s*$", frontmatter, re.M)) # 하위호환
|
||||
|
||||
|
||||
def get_scalar(frontmatter: str, key: str) -> str | None:
|
||||
m = re.search(rf"""^{re.escape(key)}:\s*['"]?(.*?)['"]?\s*$""", frontmatter, re.M)
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
def _replace_or_append_line(fm: str, key: str, line: str) -> str:
|
||||
pattern = re.compile(rf"^{re.escape(key)}:.*$", re.M)
|
||||
if pattern.search(fm):
|
||||
return pattern.sub(line.replace("\\", "\\\\"), fm, count=1)
|
||||
return fm.rstrip("\n") + "\n" + line
|
||||
|
||||
|
||||
def _strip_auto_section(body: str) -> str:
|
||||
idx = body.find(AUTO_SECTION)
|
||||
return body if idx < 0 else body[:idx].rstrip("\n") + "\n"
|
||||
|
||||
|
||||
def add_review_entry(wiki_dir: Path, doc_rel: str, reason: str) -> None:
|
||||
review = wiki_dir / "_review.md"
|
||||
lines = review.read_text(encoding="utf-8").splitlines() if review.exists() else [
|
||||
"# 재검토 필요 문서", "",
|
||||
"코드가 바뀌어 사람 교정본과 어긋날 수 있는 문서 목록 (배치가 자동 추가).", "",
|
||||
]
|
||||
entry = f"- [ ] {doc_rel} — {reason} ({date.today().isoformat()})"
|
||||
if not any(doc_rel in ln for ln in lines):
|
||||
lines.append(entry)
|
||||
review.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def merge_write(path: Path, new_text: str, *, code_hash: str | None,
|
||||
wiki_dir: Path, doc_rel: str) -> str:
|
||||
"""새로 생성한 문서(new_text)를 사람 교정 보존 규칙에 따라 path 에 쓴다.
|
||||
|
||||
반환: created | overwritten | facts_updated | draft_appended
|
||||
"""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not path.exists():
|
||||
path.write_text(new_text, encoding="utf-8")
|
||||
return "created"
|
||||
|
||||
old = path.read_text(encoding="utf-8")
|
||||
fm_old, body_old = split_frontmatter(old)
|
||||
if not is_human_verified(fm_old):
|
||||
path.write_text(new_text, encoding="utf-8")
|
||||
return "overwritten"
|
||||
|
||||
# --- 사람 검토 문서: 본문 보존, 사실 필드만 갱신 ---
|
||||
fm_new, body_new = split_frontmatter(new_text)
|
||||
assert fm_new is not None
|
||||
fm = fm_old or ""
|
||||
for key in MANAGED_KEYS:
|
||||
m = re.search(rf"^{re.escape(key)}:.*$", fm_new, re.M)
|
||||
if m:
|
||||
fm = _replace_or_append_line(fm, key, m.group(0))
|
||||
|
||||
old_hash = get_scalar(fm_old or "", "x-code-hash")
|
||||
changed = bool(code_hash and old_hash and old_hash != code_hash)
|
||||
if changed:
|
||||
fm = _replace_or_append_line(fm, "status", "status: draft")
|
||||
body = (_strip_auto_section(body_old).rstrip("\n")
|
||||
+ f"\n\n{AUTO_SECTION}\n\n"
|
||||
+ "> 코드가 변경되어 아래 요약을 새로 생성했습니다. 위 본문(사람 검토됨)과 대조해 반영해 주세요.\n\n"
|
||||
+ body_new.lstrip("\n"))
|
||||
result = "draft_appended"
|
||||
add_review_entry(wiki_dir, doc_rel, "코드 변경 — 사람 교정본과 대조 필요")
|
||||
else:
|
||||
body = body_old
|
||||
result = "facts_updated"
|
||||
|
||||
path.write_text(f"---\n{fm.strip()}\n---\n{body}", encoding="utf-8")
|
||||
return result
|
||||
Reference in New Issue
Block a user