Skip to content

Commit 55e28c0

Browse files
authored
Merge pull request #5 from DanMeon/feature/v0.3.0
v0.3.0 — Document IR v1.1 + rhwp-py CLI
2 parents 67c41f8 + 1796932 commit 55e28c0

60 files changed

Lines changed: 7239 additions & 1549 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/hooks/docs-lint.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
#!/usr/bin/env python3
2+
"""docs/*.md 편집 후 자동 검증 — CONVENTIONS.md 정책 enforcement.
3+
4+
PostToolUse hook 으로 Edit / Write / MultiEdit 후 실행. stdin 으로 받은 hook
5+
event 의 ``tool_input.file_path`` 가 ``docs/*.md`` 면 검증, 그 외는 즉시 종료.
6+
7+
검증 항목 (CONVENTIONS.md 의 hard rule 4 종):
8+
9+
1. **Status 헤더** — Living 외 모든 spec 은 ``**Status**: ...`` 메타 라인 보유
10+
2. **업스트림 monorepo 잔재 키워드** — 분사 리포 컨벤션 위배 (``사용자 Fork`` /
11+
``rhwp 본체`` / ``pyo3-sandbox`` 등). v0.1.0 historical Frozen 본문은 예외
12+
3. **같은 vX.Y.Z 디렉토리 내 spec ↔ spec 직접 link** — pair 페어
13+
(``<topic>.md`` ↔ ``<topic>-research.md``) 만 예외
14+
4. **깨진 .md 링크** — relative path 가 실제 파일을 가리키는지
15+
16+
위반 발견 시 exit 2 + stderr — Claude Code 가 stderr 를 LLM 컨텍스트에
17+
주입하여 모델이 위반 사항을 인지하고 후속 조치 결정. exit 1 은 non-blocking
18+
이라 LLM 에 노출되지 않으므로 사용 금지 (hooks 명세).
19+
"""
20+
21+
import json
22+
import re
23+
import sys
24+
from pathlib import Path
25+
26+
# * stdin 에서 hook event 파싱
27+
try:
28+
event = json.loads(sys.stdin.read() or "{}")
29+
except json.JSONDecodeError:
30+
sys.exit(0)
31+
32+
tool_input = event.get("tool_input") or {}
33+
file_path = tool_input.get("file_path") or ""
34+
if not file_path:
35+
sys.exit(0)
36+
37+
repo = Path(__file__).resolve().parents[2]
38+
try:
39+
rel = Path(file_path).resolve().relative_to(repo)
40+
except ValueError:
41+
sys.exit(0)
42+
43+
rel_str = str(rel).replace("\\", "/")
44+
if not (rel_str.startswith("docs/") and rel.suffix == ".md"):
45+
sys.exit(0)
46+
47+
target = repo / rel
48+
if not target.is_file():
49+
sys.exit(0)
50+
51+
text = target.read_text(encoding="utf-8")
52+
errors: list[str] = []
53+
54+
55+
# * 1. Status header (required outside Living docs)
56+
LIVING_FILES = {"docs/CONVENTIONS.md", "docs/roadmap/README.md"}
57+
if rel_str not in LIVING_FILES:
58+
if not re.search(r"^\*\*Status\*\*:", text, re.MULTILINE):
59+
errors.append(
60+
"missing Status header — add '**Status**: "
61+
"<Active|Draft|Frozen|Superseded by [link]> · "
62+
"**GA|Target**: vX.Y.Z · **Last updated**: YYYY-MM-DD' "
63+
"(CONVENTIONS § Status header format)"
64+
)
65+
66+
67+
# * 2. Upstream monorepo residue keywords (v0.1.0 Frozen historical exempted)
68+
HISTORICAL_FROZEN = ("docs/implementation/v0.1.0/",)
69+
if not any(rel_str.startswith(p) for p in HISTORICAL_FROZEN):
70+
forbidden = [
71+
"사용자 Fork",
72+
"rhwp 본체",
73+
"pyo3-sandbox",
74+
"/Cargo.toml (루트)",
75+
"pyo3-bindings.md",
76+
]
77+
for kw in forbidden:
78+
if kw in text:
79+
errors.append(
80+
f"upstream monorepo residue keyword {kw!r} — "
81+
"this is a spinoff binding repo, not the source-of-truth repo"
82+
)
83+
84+
85+
# * 3. Same-version spec ↔ spec direct link (pair files exempted)
86+
# ^ SemVer 정확 매칭 (vMAJOR.MINOR.PATCH) — 이전의 [\d.]+ 기반은 catastrophic
87+
# backtracking 위험 (CodeQL py/redos). v0.3.0 / v0.3.1 등 모두 cover.
88+
m = re.match(r"docs/(roadmap|design)/(v\d+\.\d+\.\d+)/(.+)\.md$", rel_str)
89+
if m:
90+
base = m.group(3)
91+
pair_topic = base.removesuffix("-research")
92+
# ^ pair: <topic>.md ↔ <topic>-research.md (the only allowed direct link)
93+
if base.endswith("-research"):
94+
allowed_link = f"{pair_topic}.md"
95+
else:
96+
allowed_link = f"{base}-research.md"
97+
self_link = f"{base}.md"
98+
for link in re.findall(r"\]\(([^)]+\.md)[^)]*\)", text):
99+
link_target = link.split("#")[0]
100+
# only same-directory .md candidates qualify
101+
if "/" in link_target:
102+
continue
103+
if link_target in (allowed_link, self_link):
104+
continue
105+
errors.append(
106+
f"same-version spec direct link {link!r} — "
107+
"route through phase-N.md or roadmap/README.md "
108+
"(CONVENTIONS § Cross-link direction rule)"
109+
)
110+
111+
112+
# * 4. Broken .md link
113+
dir_path = target.parent
114+
for link in re.findall(r"\]\(([^)]+\.md)[^)]*\)", text):
115+
link_target = link.split("#")[0].split("?")[0]
116+
if not link_target or link_target.startswith("http"):
117+
continue
118+
resolved = (dir_path / link_target).resolve()
119+
if not resolved.exists():
120+
errors.append(f"broken .md link {link!r} (resolved: {resolved})")
121+
122+
123+
if errors:
124+
sys.stderr.write(f"\ndocs-lint: {rel_str}{len(errors)} violation(s)\n")
125+
for i, e in enumerate(errors, 1):
126+
sys.stderr.write(f" {i}. {e}\n")
127+
sys.stderr.write("policy: docs/CONVENTIONS.md\n")
128+
sys.exit(2)

.claude/settings.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"hooks": {
3+
"PostToolUse": [
4+
{
5+
"matcher": "Edit|Write|MultiEdit",
6+
"hooks": [
7+
{
8+
"type": "command",
9+
"command": "python3 ${CLAUDE_PROJECT_DIR}/.claude/hooks/docs-lint.py"
10+
}
11+
]
12+
}
13+
]
14+
}
15+
}

.github/codeql-config.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
paths-ignore:
22
- tests
3+
- external
4+
- examples
5+
- benches

.github/workflows/ci.yml

Lines changed: 96 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -27,36 +27,60 @@ concurrency:
2727
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
2828

2929
jobs:
30-
# * 메인 테스트 + 린트 + 타입체크
31-
# abi3-py310 wheel 로 빌드는 한 번이지만 런타임 동작은 버전별 검증 필요 → Linux × 전 버전.
32-
# macOS / Windows 는 OS 레이어 스모크이므로 py3.12 하나만.
30+
# * Linux abi3 wheel 1회 빌드 → 모든 Linux 잡(test×4 / slow / core-only)이 공유
31+
# abi3-py310 이라 py3.10/3.11/3.12/3.13 가 동일 wheel 재사용 가능.
32+
# macOS/Windows 는 단일 잡이라 빌드/테스트 분리 이득이 없어 그대로 매번 빌드.
33+
build-linux-wheel:
34+
name: Build Linux abi3 wheel
35+
runs-on: ubuntu-latest
36+
steps:
37+
- uses: actions/checkout@v6
38+
with:
39+
submodules: recursive
40+
- uses: dtolnay/rust-toolchain@stable
41+
- uses: Swatinem/rust-cache@v2
42+
with:
43+
save-if: ${{ github.ref == 'refs/heads/main' }}
44+
- uses: astral-sh/setup-uv@v8.1.0
45+
with:
46+
python-version: "3.12"
47+
- run: uv sync --no-install-project --group all
48+
- run: uv run maturin build --release --out dist
49+
- uses: actions/upload-artifact@v7
50+
with:
51+
name: rhwp-python-linux-wheel
52+
path: dist/*.whl
53+
retention-days: 1
54+
55+
# * 메인 테스트 + 린트 + 타입체크 (Linux × 전 Python 버전 — wheel 공유)
3356
test:
34-
name: Test (${{ matrix.os }} / py${{ matrix.python }})
35-
runs-on: ${{ matrix.os }}
57+
name: Test (Linux / py${{ matrix.python }})
58+
needs: build-linux-wheel
59+
runs-on: ubuntu-latest
3660
strategy:
3761
fail-fast: false
3862
matrix:
3963
include:
40-
- { os: ubuntu-latest, python: "3.10", lint: true }
41-
- { os: ubuntu-latest, python: "3.11" }
42-
- { os: ubuntu-latest, python: "3.12" }
43-
- { os: ubuntu-latest, python: "3.13" }
44-
- { os: macos-latest, python: "3.12" }
45-
- { os: windows-latest, python: "3.12" }
64+
- { python: "3.10", lint: true }
65+
- { python: "3.11" }
66+
- { python: "3.12" }
67+
- { python: "3.13" }
4668
defaults:
4769
run:
4870
shell: bash
4971
steps:
5072
- uses: actions/checkout@v6
5173
with:
5274
submodules: recursive
53-
- uses: dtolnay/rust-toolchain@stable
54-
- uses: Swatinem/rust-cache@v2
5575
- uses: astral-sh/setup-uv@v8.1.0
5676
with:
5777
python-version: ${{ matrix.python }}
5878
- run: uv sync --no-install-project --group all
59-
- run: uv run maturin develop --release
79+
- uses: actions/download-artifact@v8
80+
with:
81+
name: rhwp-python-linux-wheel
82+
path: dist/
83+
- run: uv pip install --reinstall dist/*.whl
6084
- name: Run pytest (not slow) with coverage
6185
run: uv run pytest tests/ -m "not slow" --cov=rhwp --cov-report=term-missing -v
6286
- name: Run pyright (normal)
@@ -68,6 +92,11 @@ jobs:
6892
tests/test_langchain_loader.py tests/test_langchain_loader_ir.py \
6993
tests/test_ir_schema.py tests/test_ir_roundtrip.py tests/test_ir_tables.py \
7094
tests/test_ir_iter_blocks.py tests/test_ir_schema_export.py \
95+
tests/test_ir_picture.py tests/test_ir_furniture.py \
96+
tests/test_ir_formula.py tests/test_ir_footnote.py \
97+
tests/test_ir_list.py tests/test_ir_caption.py \
98+
tests/test_ir_toc.py tests/test_ir_field.py \
99+
tests/test_cli.py \
71100
tests/conftest.py tests/type_check_samples.py
72101
- name: Run pyright (intentional errors — expect 4)
73102
if: matrix.lint
@@ -81,9 +110,36 @@ jobs:
81110
exit 1
82111
fi
83112
84-
# * PDF 렌더링 — 느려서 별도 잡
113+
# * macOS / Windows 스모크 — 단일 잡이라 wheel 분리 이득 없음 → 직접 maturin develop
114+
test-other-os:
115+
name: Test (${{ matrix.os }} / py3.12)
116+
runs-on: ${{ matrix.os }}
117+
strategy:
118+
fail-fast: false
119+
matrix:
120+
os: [macos-latest, windows-latest]
121+
defaults:
122+
run:
123+
shell: bash
124+
steps:
125+
- uses: actions/checkout@v6
126+
with:
127+
submodules: recursive
128+
- uses: dtolnay/rust-toolchain@stable
129+
- uses: Swatinem/rust-cache@v2
130+
with:
131+
save-if: ${{ github.ref == 'refs/heads/main' }}
132+
- uses: astral-sh/setup-uv@v8.1.0
133+
with:
134+
python-version: "3.12"
135+
- run: uv sync --no-install-project --group all
136+
- run: uv run maturin develop --release
137+
- run: uv run pytest tests/ -m "not slow" -v
138+
139+
# * PDF 렌더링 — 느려서 별도 잡, Linux wheel 재사용
85140
test-slow:
86141
name: Test slow (Linux / py3.12 — PDF)
142+
needs: build-linux-wheel
87143
runs-on: ubuntu-latest
88144
defaults:
89145
run:
@@ -92,18 +148,21 @@ jobs:
92148
- uses: actions/checkout@v6
93149
with:
94150
submodules: recursive
95-
- uses: dtolnay/rust-toolchain@stable
96-
- uses: Swatinem/rust-cache@v2
97151
- uses: astral-sh/setup-uv@v8.1.0
98152
with:
99153
python-version: "3.12"
100154
- run: uv sync --no-install-project --group testing
101-
- run: uv run maturin develop --release
155+
- uses: actions/download-artifact@v8
156+
with:
157+
name: rhwp-python-linux-wheel
158+
path: dist/
159+
- run: uv pip install --reinstall dist/*.whl
102160
- run: uv run pytest tests/ -m slow -v
103161

104162
# * extras 미설치 시 langchain 테스트가 importorskip 로 auto-skip 되는지 검증
105163
test-core-only:
106164
name: Test without extras (importorskip auto-skip)
165+
needs: build-linux-wheel
107166
runs-on: ubuntu-latest
108167
defaults:
109168
run:
@@ -112,24 +171,36 @@ jobs:
112171
- uses: actions/checkout@v6
113172
with:
114173
submodules: recursive
115-
- uses: dtolnay/rust-toolchain@stable
116-
- uses: Swatinem/rust-cache@v2
117174
- uses: astral-sh/setup-uv@v8.1.0
118175
with:
119176
python-version: "3.12"
120177
- name: Install pytest only (no langchain extras — intentional)
121178
run: |
122179
uv venv
123180
uv pip install pytest
124-
- run: uv run maturin develop --release
181+
- uses: actions/download-artifact@v8
182+
with:
183+
name: rhwp-python-linux-wheel
184+
path: dist/
185+
- run: uv pip install dist/*.whl
125186
- name: Run pytest — extras-gated tests must auto-skip via importorskip
126187
# ^ 파일-레벨 importorskip 은 해당 파일 전체를 skip 1개로 카운트.
127-
# 현재 gated 파일: test_langchain_loader.py + test_langchain_loader_ir.py
128-
# (langchain-core), test_ir_schema_export.py (jsonschema),
129-
# test_async.py (aiofiles) → 총 4 파일
188+
# v0.3.0 기준 gated 파일: test_langchain_loader.py + test_langchain_loader_ir.py
189+
# (langchain-core), test_ir_schema_export.py (jsonschema), test_cli.py (typer)
190+
# → 총 4 파일. test_async.py 는 v0.3.0 부터 stdlib 만 사용 (aiofiles 의존성 제거)
130191
run: |
131192
uv run pytest tests/ -m "not slow" -v | tee pytest-output.txt
132193
if ! grep -qE '(^|[^0-9])4 skipped([^0-9]|$)' pytest-output.txt; then
133-
echo "::error::expected 4 extras-gated files to auto-skip via importorskip (langchain×2, jsonschema, aiofiles)"
194+
echo "::error::expected 4 extras-gated files to auto-skip via importorskip (langchain×2, jsonschema, typer)"
134195
exit 1
135196
fi
197+
198+
all-tests-passed:
199+
name: All tests passed
200+
if: always()
201+
runs-on: ubuntu-latest
202+
needs: [build-linux-wheel, test, test-other-os, test-slow, test-core-only]
203+
steps:
204+
- uses: re-actors/alls-green@release/v1
205+
with:
206+
jobs: ${{ toJSON(needs) }}

.github/workflows/publish-schema.yml

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ on:
88
push:
99
branches: [main]
1010
paths:
11-
- 'python/rhwp/ir/schema/hwp_ir_v1.json'
11+
# ^ glob — v2 도입 시 hwp_ir_v2.json 만 변경되어도 자동 트리거
12+
- 'python/rhwp/ir/schema/hwp_ir_v*.json'
1213
- 'python/rhwp/ir/nodes.py'
1314
- 'python/rhwp/ir/schema.py'
1415
- '.github/workflows/publish-schema.yml'
@@ -54,11 +55,16 @@ jobs:
5455
runs-on: ubuntu-latest
5556
steps:
5657
- uses: actions/checkout@v6
57-
- name: Prepare pages directory — copy every versioned schema
58+
- name: Prepare pages directory — copy versioned schema + content-addressed alias
5859
# ^ 불변 경로 정책: repo 의 hwp_ir_v*.json 을 모두 각 버전 URL 로 배포.
5960
# v2 도입 시 python/rhwp/ir/schema/hwp_ir_v2.json 을 추가하기만 하면
6061
# 이 루프가 자동으로 v1/v2 양쪽 모두를 pages 아티팩트에 포함한다.
6162
# `actions/deploy-pages@v4` 의 replace-all 동작으로 v1 이 누락되는 것을 원천 차단.
63+
#
64+
# Content-addressed alias (v0.3.0 S4 추가, ir-expansion.md § 스키마 버저닝):
65+
# 같은 v1 URL 안에서 minor bump (1.0 → 1.1) 가 발생할 때마다 hash-tagged
66+
# immutable alias 를 alongside 발행. 구 hash 는 영구 보존되어 SchemaStore /
67+
# 외부 도구가 정확한 스냅샷을 reproducible 하게 참조 가능.
6268
run: |
6369
set -euo pipefail
6470
mkdir -p pages/schema/hwp_ir
@@ -69,7 +75,10 @@ jobs:
6975
ver="${name#hwp_ir_}" # v1, v2, ...
7076
mkdir -p "pages/schema/hwp_ir/$ver"
7177
cp "$f" "pages/schema/hwp_ir/$ver/schema.json"
72-
echo "Published $f -> pages/schema/hwp_ir/$ver/schema.json"
78+
sha=$(shasum -a 256 "$f" | awk '{print $1}')
79+
alias="pages/schema/hwp_ir/${name}-sha256-${sha}.json"
80+
cp "$f" "$alias"
81+
echo "Published $f -> $ver/schema.json + alias ${name}-sha256-${sha}.json"
7382
copied=$((copied + 1))
7483
done
7584
if [ "$copied" -eq 0 ]; then

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,8 @@ coverage.xml
3838
# * MCP
3939
.serena/
4040

41+
# * Claude Code — settings.json 은 팀 공유 (hooks 등록), local override 만 ignore
42+
.claude/settings.local.json
43+
4144
# * Examples 산출물
4245
render_output/

0 commit comments

Comments
 (0)