Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ee563e7
docs: CONVENTIONS.md 갱신 — frontmatter schema + 정책 정비
DanMeon Apr 29, 2026
a0e2235
docs: AGENTS.md 정본화 + CLAUDE.md stub
DanMeon Apr 29, 2026
e1ba5c6
docs: 24개 spec frontmatter 마이그 (Frozen 면제 조항 적용)
DanMeon Apr 29, 2026
67c79d2
docs: 상류 issue #390 RESOLVED — find_control_text_positions in-place F…
DanMeon Apr 29, 2026
539b346
docs: CLAUDE.md → AGENTS.md symlink 전환 (stub 제거)
DanMeon Apr 29, 2026
e156373
chore: spec lint 확장 — frontmatter / supersede chain / kebab-case 검증
DanMeon Apr 29, 2026
b58f244
docs: AGENTS.md "Global rules inherited" 섹션 제거
DanMeon Apr 29, 2026
d6a4006
chore: lint_docs.py argparse → typer (전역 컨벤션 정합)
DanMeon Apr 29, 2026
a3ed251
docs: design 파일 명명 정합 — -design-research → -research (rename + 링크 정정)
DanMeon Apr 29, 2026
baafe82
chore: upstream-pins.yaml SSOT + 자동 추출 스크립트
DanMeon Apr 29, 2026
514c9e7
test: pytest.mark.spec marker + 자동 trace report
DanMeon Apr 29, 2026
b4f474c
feat: /new-spec Claude Code skill — 새 version spec 스캐폴드 자동화
DanMeon Apr 29, 2026
cacdf80
chore: last_updated 자동 갱신 hook (Claude Code PostToolUse)
DanMeon Apr 29, 2026
97319c8
docs: CHANGELOG [Unreleased] + spec-system-overhaul 명세서 이주
DanMeon Apr 29, 2026
e28d0cb
ci(docs): uv pip install --system → uv run --with (PEP 668 회피)
DanMeon Apr 29, 2026
3bb68ef
ci(docs): uv run --no-project — pyproject maturin build skip
DanMeon Apr 29, 2026
8df7be7
fix(lint): external/ 디렉토리 link 검증 skip — submodule 의존성 외화
DanMeon Apr 29, 2026
8bc5172
fix(lint+scripts): code-reviewer 7개 이슈 일괄 fix + .gitignore
DanMeon Apr 29, 2026
ea4870b
docs(skill): SKILL.md 본문 한국어 → 영문 (LLM-facing 정책 정합)
DanMeon Apr 29, 2026
57f8be3
docs(contributing): path-based onboarding 추가
DanMeon Apr 29, 2026
34e9e6b
docs(contributing): 한글 primary + CONTRIBUTING_EN 분리 (README 패턴 정합)
DanMeon Apr 29, 2026
ea2377f
revert: docs/upstream-pins.yaml + update_upstream_pin.py 제거 (YAGNI)
DanMeon Apr 29, 2026
189457c
feat(spec-trace): EARS strict 완화 + 기존 테스트 soft retrofit
DanMeon Apr 29, 2026
921704a
docs(conventions): phase 정책 정리
DanMeon Apr 29, 2026
1e70bba
docs(roadmap): phase-{3,4}.md 폐기 + README 미착수 narrative 흡수
DanMeon Apr 29, 2026
71b6082
docs: Frozen body 의 phase link 정리
DanMeon Apr 29, 2026
eb84c6d
ci: codeql + dependabot-uv-sync 에 concurrency group 추가
DanMeon Apr 29, 2026
cee3c1e
chore: sync rhwp upstream
DanMeon Apr 29, 2026
69150a5
ci: paths-filter Pattern A 적용 (paths-ignore 함정 회피)
DanMeon Apr 29, 2026
e7496ba
feat(skill): /new-spec auto invocation 허용 + description 강화
DanMeon Apr 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 14 additions & 95 deletions .claude/hooks/docs-lint.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,24 @@
#!/usr/bin/env python3
"""docs/*.md 편집 후 자동 검증 — CONVENTIONS.md 정책 enforcement.
"""docs/*.md PostToolUse lint — single file mode.

PostToolUse hook 으로 Edit / Write / MultiEdit 후 실행. stdin 으로 받은 hook
event 의 ``tool_input.file_path`` 가 ``docs/*.md`` 면 검증,외는 즉시 종료.
stdin 으로 받은 hook event 의 ``tool_input.file_path`` 가 ``docs/*.md`` 면
공통 lib (``scripts/_doc_lint.py``) 의 룰 일괄 적용. 즉시 종료.

검증 항목 (CONVENTIONS.md 의 hard rule 4 종):
위반 시 exit 2 + stderr — Claude Code 가 stderr 를 LLM 컨텍스트에 주입하여
모델이 위반 사항을 인지. exit 1 은 non-blocking 이라 사용 금지.

1. **Status 헤더** — Living 외 모든 spec 은 ``**Status**: ...`` 메타 라인 보유
2. **업스트림 monorepo 잔재 키워드** — 분사 리포 컨벤션 위배 (``사용자 Fork`` /
``rhwp 본체`` / ``pyo3-sandbox`` 등). v0.1.0 historical Frozen 본문은 예외
3. **같은 vX.Y.Z 디렉토리 내 spec ↔ spec 직접 link** — pair 페어
(``<topic>.md`` ↔ ``<topic>-research.md``) 만 예외
4. **깨진 .md 링크** — relative path 가 실제 파일을 가리키는지

위반 발견 시 exit 2 + stderr — Claude Code 가 stderr 를 LLM 컨텍스트에
주입하여 모델이 위반 사항을 인지하고 후속 조치 결정. exit 1 은 non-blocking
이라 LLM 에 노출되지 않으므로 사용 금지 (hooks 명세).
룰 일람 / 정책 SSOT: ``scripts/_doc_lint.py`` docstring + ``docs/CONVENTIONS.md``.
"""

import json
import re
import sys
from pathlib import Path

# * stdin 에서 hook event 파싱
REPO = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(REPO / "scripts"))

from _doc_lint import lint_file # noqa: E402

try:
event = json.loads(sys.stdin.read() or "{}")
except json.JSONDecodeError:
Expand All @@ -34,94 +29,18 @@
if not file_path:
sys.exit(0)

repo = Path(__file__).resolve().parents[2]
try:
rel = Path(file_path).resolve().relative_to(repo)
rel = Path(file_path).resolve().relative_to(REPO)
except ValueError:
sys.exit(0)

rel_str = str(rel).replace("\\", "/")
if not (rel_str.startswith("docs/") and rel.suffix == ".md"):
sys.exit(0)

target = repo / rel
if not target.is_file():
sys.exit(0)

text = target.read_text(encoding="utf-8")
errors: list[str] = []


# * 1. Status header (required outside Living docs)
LIVING_FILES = {"docs/CONVENTIONS.md", "docs/roadmap/README.md"}
if rel_str not in LIVING_FILES:
if not re.search(r"^\*\*Status\*\*:", text, re.MULTILINE):
errors.append(
"missing Status header — add '**Status**: "
"<Active|Draft|Frozen|Superseded by [link]> · "
"**GA|Target**: vX.Y.Z · **Last updated**: YYYY-MM-DD' "
"(CONVENTIONS § Status header format)"
)


# * 2. Upstream monorepo residue keywords (v0.1.0 Frozen historical exempted)
HISTORICAL_FROZEN = ("docs/implementation/v0.1.0/",)
if not any(rel_str.startswith(p) for p in HISTORICAL_FROZEN):
forbidden = [
"사용자 Fork",
"rhwp 본체",
"pyo3-sandbox",
"/Cargo.toml (루트)",
"pyo3-bindings.md",
]
for kw in forbidden:
if kw in text:
errors.append(
f"upstream monorepo residue keyword {kw!r} — "
"this is a spinoff binding repo, not the source-of-truth repo"
)


# * 3. Same-version spec ↔ spec direct link (pair files exempted)
# ^ SemVer 정확 매칭 (vMAJOR.MINOR.PATCH) — 이전의 [\d.]+ 기반은 catastrophic
# backtracking 위험 (CodeQL py/redos). v0.3.0 / v0.3.1 등 모두 cover.
m = re.match(r"docs/(roadmap|design)/(v\d+\.\d+\.\d+)/(.+)\.md$", rel_str)
if m:
base = m.group(3)
pair_topic = base.removesuffix("-research")
# ^ pair: <topic>.md ↔ <topic>-research.md (the only allowed direct link)
if base.endswith("-research"):
allowed_link = f"{pair_topic}.md"
else:
allowed_link = f"{base}-research.md"
self_link = f"{base}.md"
for link in re.findall(r"\]\(([^)]+\.md)[^)]*\)", text):
link_target = link.split("#")[0]
# only same-directory .md candidates qualify
if "/" in link_target:
continue
if link_target in (allowed_link, self_link):
continue
errors.append(
f"same-version spec direct link {link!r} — "
"route through phase-N.md or roadmap/README.md "
"(CONVENTIONS § Cross-link direction rule)"
)


# * 4. Broken .md link
dir_path = target.parent
for link in re.findall(r"\]\(([^)]+\.md)[^)]*\)", text):
link_target = link.split("#")[0].split("?")[0]
if not link_target or link_target.startswith("http"):
continue
resolved = (dir_path / link_target).resolve()
if not resolved.exists():
errors.append(f"broken .md link {link!r} (resolved: {resolved})")


errors = lint_file(rel_str, REPO)
if errors:
sys.stderr.write(f"\ndocs-lint: {rel_str} — {len(errors)} violation(s)\n")
sys.stderr.write(f"\ndocs-lint: {len(errors)} violation(s)\n")
for i, e in enumerate(errors, 1):
sys.stderr.write(f" {i}. {e}\n")
sys.stderr.write("policy: docs/CONVENTIONS.md\n")
Expand Down
77 changes: 77 additions & 0 deletions .claude/hooks/update-last-updated.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""docs/*.md 편집 시 frontmatter 의 last_updated 를 오늘 날짜로 자동 갱신.

PostToolUse hook 으로 Edit / Write / MultiEdit 후 실행. stdin 으로 받은 hook
event 의 ``tool_input.file_path`` 가 ``docs/*.md`` 면서 frontmatter 가 있으면
``last_updated:`` 라인을 오늘 날짜로 in-place 교체.

skip 조건:
- ``docs/`` 외부 파일
- frontmatter 없는 파일 (Living: CONVENTIONS / roadmap/README / traces/coverage)
- last_updated 가 이미 오늘 날짜
- frontmatter 의 status 가 Frozen 또는 Superseded — 본문 의미 변경 금지가
원칙. 이런 파일을 편집한 경우는 (a) Frozen 면제 조항 활용 일괄 마이그
(PR 단위 수기 처리) 또는 (b) 오타·링크 fix (last_updated 갱신 적절) 둘
다 가능 → 자동 처리는 위험하니 hook 은 skip, 사용자가 명시 결정.

본 hook 은 silent (exit 0) — 갱신 결과는 git diff 로 확인.
"""

import json
import re
import sys
from datetime import date
from pathlib import Path

REPO = Path(__file__).resolve().parents[2]

try:
event = json.loads(sys.stdin.read() or "{}")
except json.JSONDecodeError:
sys.exit(0)

tool_input = event.get("tool_input") or {}
file_path = tool_input.get("file_path") or ""
if not file_path:
sys.exit(0)

try:
rel = Path(file_path).resolve().relative_to(REPO)
except ValueError:
sys.exit(0)

rel_str = str(rel).replace("\\", "/")
if not (rel_str.startswith("docs/") and rel.suffix == ".md"):
sys.exit(0)

target = REPO / rel
if not target.is_file():
sys.exit(0)

text = target.read_text(encoding="utf-8")
if not text.startswith("---\n"):
sys.exit(0)
end = text.find("\n---\n", 4)
if end < 0:
sys.exit(0)

block = text[4:end]
status_match = re.search(r"^status:\s*(\S+)", block, re.MULTILINE)
if status_match and status_match.group(1) in ("Frozen", "Superseded"):
# ^ Frozen / Superseded 자동 갱신 금지 — 사용자 명시 결정 필요
sys.exit(0)

today = date.today().isoformat()
new_block, n = re.subn(
r"^last_updated:\s*\S+",
f"last_updated: {today}",
block,
count=1,
flags=re.MULTILINE,
)
if n == 0 or new_block == block:
sys.exit(0)

new_text = "---\n" + new_block + text[end:]
target.write_text(new_text, encoding="utf-8")
sys.exit(0)
4 changes: 4 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "python3 ${CLAUDE_PROJECT_DIR}/.claude/hooks/update-last-updated.py"
},
{
"type": "command",
"command": "python3 ${CLAUDE_PROJECT_DIR}/.claude/hooks/docs-lint.py"
Expand Down
125 changes: 125 additions & 0 deletions .claude/skills/new-spec/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
---
name: new-spec
description: Scaffold a new version spec and paired ADR following docs/CONVENTIONS.md. Invoke when the user wants to start a new version spec (e.g. "v0.4.0 view 렌더러 시작", "phase 3 첫 spec"). Idempotent — aborts if spec already exists. State the version + topic before invoking and wait for user confirmation.
argument-hint: <version> <topic>
arguments:
- version
- topic
---

# /new-spec — scaffold a new version spec

Given `<version>` (e.g. `v0.4.0`) and `<topic>` (e.g. `view-renderer`), create a new per-version spec, its paired ADR (design research), and the index entry in one shot.

## Outputs

1. `docs/roadmap/<version>/<topic>.md` — the spec body (frontmatter `status: Draft`, `target: <version>`)
2. `docs/design/<version>/<topic>-research.md` — the paired ADR (same frontmatter)
3. `docs/roadmap/README.md` — append a row to the active spec index table
4. EARS notation placeholder section inside the spec body

## Procedure

When this skill is invoked, execute the following steps in order:

1. **Validate arguments**
- `version` must match the SemVer pattern `vX.Y.Z`
- `topic` must be kebab-case (`[a-z0-9]+(-[a-z0-9]+)*`)
- Abort if `docs/roadmap/<version>/<topic>.md` already exists — never overwrite an existing spec

2. **Re-read `docs/CONVENTIONS.md`** before writing — apply the current frontmatter schema, naming rules, cross-link direction, and EARS notation section. Conventions may have evolved since the last skill invocation.

3. **Create directories** if missing:
- `docs/roadmap/<version>/`
- `docs/design/<version>/`

4. **Write `docs/roadmap/<version>/<topic>.md`** using this template (placeholders in `<...>` must be filled by Claude based on user intent; section headers and Korean prose stay as-is — they become Korean docs):

```markdown
---
status: Draft
target: <version>
last_updated: <today YYYY-MM-DD>
---

# <version> — <Korean summary title for the topic>

<One paragraph in Korean — what this spec introduces and why>.

주요 결정의 근거·대안·실패 시나리오는 짝 페어: [<topic>-research.md](../../design/<version>/<topic>-research.md).

## 결정 사항

| 항목 | 값 | 근거 |
|---|---|---|
| 1 | (placeholder) | (placeholder) |

## 인수조건

<!-- Assign AC-N IDs; each maps 1:1 to `pytest.mark.spec("<version>/<topic>#AC-N")`.
Format is free — testable + clear is the bar. EARS notation
(`THE ... SHALL`, `WHEN ..., THE ... SHALL`, etc.) optional for
ambiguity-prone statements. -->

- **AC-1** — <testable statement>
- **AC-2** — <testable statement>

## 영구 비목표

- <items explicitly out of scope for this spec>

## 참조

- 짝 페어 (ADR): [<topic>-research.md](../../design/<version>/<topic>-research.md)
```

5. **Write `docs/design/<version>/<topic>-research.md`** using this template:

```markdown
---
status: Draft
target: <version>
last_updated: <today YYYY-MM-DD>
---

# <version> <topic> — 설계 의사결정 리서치 요약

[<version>/<topic>.md](../../roadmap/<version>/<topic>.md) §결정 사항 중 외부 독자가 "왜?" 를 던질 만한 N건의 업계 선례·대안·실패 시나리오를 기록한다. <topic>.md 본문이 최종 결정을 기술하고, 본 문서는 그 결정의 근거를 담는다.

## 결정 매트릭스

| # | 항목 | 옵션 비교 | 채택 | 1차 근거 |
|---|---|---|---|---|
| 1 | (placeholder) | A: ... / B: ... / C: ... | (?) | (?) |

## 1. <first decision item>

### 팩트
### 검증자 반박
### 최종 결정
### 1차 소스

## 참조

- [roadmap/<version>/<topic>.md](../../roadmap/<version>/<topic>.md) — 본 리서치의 결정 요약
```

6. **Append a row to `docs/roadmap/README.md`** in the active spec index table (find `## 활성 spec 인덱스` section, add at the end of the table):

```markdown
| <version> (<topic>) | Draft | [<version>/<topic>.md](<version>/<topic>.md) | [design/<version>/<topic>-research.md](../design/<version>/<topic>-research.md) |
```

7. **Run integrity check**: `uv run --no-project --with "typer>=0.12" python scripts/lint_docs.py docs/`. If violations are reported, surface them to the user and propose corrections — do not silently fix.

## Rules (must comply with `docs/CONVENTIONS.md`)

- Frontmatter schema: `status` enum, `target` SemVer, `last_updated` `YYYY-MM-DD`
- Spec ↔ research pair files may link directly; **all other spec ↔ spec direct links are forbidden** — route through index pages
- Filenames must be kebab-case; directories use `vX.Y.Z` SemVer
- Relative paths are implicit (`foo.md`, `subdir/foo.md`); no `./` prefix; external resources use fully-qualified URLs

## Limits

- Spec body and decision matrix are placeholders — actual decisions / acceptance criteria / non-goals must be filled by the user
- This skill automates **structural consistency** only (frontmatter / pair / index / EARS placeholder). Content judgment is the user's.
Loading
Loading