crumbs-spec 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.ko.md +139 -101
  2. package/README.md +159 -121
  3. package/dist/src/check.js +7 -5
  4. package/dist/src/check.js.map +1 -1
  5. package/dist/src/cli.js +2 -0
  6. package/dist/src/cli.js.map +1 -1
  7. package/dist/src/config.js +16 -1
  8. package/dist/src/config.js.map +1 -1
  9. package/dist/src/dates.js +7 -0
  10. package/dist/src/dates.js.map +1 -1
  11. package/dist/src/graph.js +81 -14
  12. package/dist/src/graph.js.map +1 -1
  13. package/dist/src/init.js +24 -8
  14. package/dist/src/init.js.map +1 -1
  15. package/dist/src/markdown.js +155 -25
  16. package/dist/src/markdown.js.map +1 -1
  17. package/dist/src/messages.js +40 -15
  18. package/dist/src/messages.js.map +1 -1
  19. package/dist/src/model.js +2 -3
  20. package/dist/src/model.js.map +1 -1
  21. package/dist/src/report.js +1 -1
  22. package/dist/src/report.js.map +1 -1
  23. package/dist/src/rules/broken-link.js +4 -1
  24. package/dist/src/rules/broken-link.js.map +1 -1
  25. package/dist/src/rules/duplicate-policy.js +11 -2
  26. package/dist/src/rules/duplicate-policy.js.map +1 -1
  27. package/dist/src/rules/log-body.js +3 -3
  28. package/dist/src/rules/log-body.js.map +1 -1
  29. package/dist/src/rules/log-format.js +6 -0
  30. package/dist/src/rules/log-format.js.map +1 -1
  31. package/dist/src/rules/missing-backlink.js +4 -1
  32. package/dist/src/rules/missing-backlink.js.map +1 -1
  33. package/dist/src/rules/missing-section.js +14 -2
  34. package/dist/src/rules/missing-section.js.map +1 -1
  35. package/dist/src/rules/unreachable.js +1 -1
  36. package/dist/src/rules/unreachable.js.map +1 -1
  37. package/dist/src/rules/unterminated-comment.js +13 -0
  38. package/dist/src/rules/unterminated-comment.js.map +1 -0
  39. package/dist/src/skills.js +12 -1
  40. package/dist/src/skills.js.map +1 -1
  41. package/dist/src/templates/agents.js +2 -2
  42. package/dist/src/templates/agents.js.map +1 -1
  43. package/dist/src/templates/en.js +5 -5
  44. package/dist/src/templates/en.js.map +1 -1
  45. package/dist/src/templates/ko.js +1 -1
  46. package/dist/src/templates/ko.js.map +1 -1
  47. package/dist/src/templates/skills-en.js +16 -15
  48. package/dist/src/templates/skills-en.js.map +1 -1
  49. package/dist/src/templates/skills-ko.js +10 -9
  50. package/dist/src/templates/skills-ko.js.map +1 -1
  51. package/dist/src/version.js +7 -0
  52. package/dist/src/version.js.map +1 -0
  53. package/package.json +3 -2
package/README.ko.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  **한국어** · [English](README.md)
4
4
 
5
+ [![CI](https://github.com/Sienna94/crumbs-spec/actions/workflows/ci.yml/badge.svg)](https://github.com/Sienna94/crumbs-spec/actions/workflows/ci.yml) [![npm](https://img.shields.io/npm/v/crumbs-spec)](https://www.npmjs.com/package/crumbs-spec)
6
+
5
7
  에이전트가 길을 잃지 않게 문서마다 빵부스러기(링크)를 남기고, 인덱스에서 상세 문서까지 한 길로 이어지는 **살아 있는 스펙**을 만들어요. 저장소가 *지금* 어떻게 동작하는지를 책임 단위 문서로 두고, 그 문서들이 잘 이어져 있는지 명령 하나로 검사하고, 에이전트가 문서를 쓰고·찾고·묻고·반영하는 절차를 스킬로 고정한 도구예요.
6
8
 
7
9
  ## 왜 만들었나요
@@ -22,48 +24,55 @@ Spec Kit은 구현 전 계획을, LLM Wiki는 자료를 쌓는 위키를 잘 다
22
24
  |---|---|---|---|---|
23
25
  | 문서 단위 | 변경(기능) 하나: `spec.md`·`plan.md`·`tasks.md` | 현재 상태 스펙 + 변경 델타 | 엔티티·개념·원본 요약 페이지 | **책임(사용자 흐름) 하나**. 정책은 문서 하나가 소유 |
24
26
  | 시점 | 구현 전 | 구현 전·후 | 자료 반영 시점 | **구현 뒤 현재 동작** |
25
- | 기존 코드 | "추론하지 않는다"고 스스로 밝힘 | 온보딩은 있으나 코드→스펙 초안은 없음 | 원본 자료가 대상, 코드 아님 | **코드·테스트에서 초안을 만들고 모르는 것은 `[확인 필요]` 질문으로** |
27
+ | 기존 코드 | "추론하지 않는다"고 스스로 밝힘 | 온보딩은 있으나 코드→스펙 초안은 없음 | 원본 자료가 대상, 코드 아님 | **crumbs-writer 스킬이 코드·테스트에서 초안을 만들고, 모르는 것은 `[확인 필요]` 질문으로 남겨요** |
26
28
  | 탐색 | 폴더와 파일 이름 | 폴더와 파일 이름 | 인덱스 우선 | **인덱스 → 개요 → 상세 링크만**. ID·front matter 없음 |
27
- | 기계 검사 | spec·plan·tasks 일관성 | 델타 병합 | 고아 페이지·끊긴 링크 lint(LLM 판단 포함) | **결정론적 15개 규칙**: 링크·앵커·도달 가능·역링크·정책 중복·근거 경로 실존·절 순서·흐름 도식·로그 형식 |
28
- | 구현 후 동기화 | 팀이 유지 방식을 고름 | `archive`/`sync`로 변경분만 | 질의 결과를 위키에 환류 | **에이전트 규칙("요청 없어도 같은 변경에서 문서 갱신") + 결정 로그 + 예정된 `drift`** |
29
+ | 기계 검사 | spec·plan·tasks 일관성 | `openspec validate`(구조) 검사와 아카이브 시 델타 병합 | 고아 페이지·끊긴 링크 lint(LLM 판단 포함) | **결정론적 16개 구조 규칙**: 링크·앵커·도달 가능·역링크·정책 이름 중복·근거 경로·절 순서·흐름 도식·로그 형식·닫히지 않은 주석 |
30
+ | 구현 후 동기화 | 팀이 유지 방식을 고름 | `archive`/`sync`로 변경분만 | 질의 결과를 위키에 환류 | **에이전트 규칙("요청 없어도 같은 변경에서 문서 갱신")과 결정 로그** |
29
31
  | 사람 가독성 | 산문 | 산문 | 산문 | **흐름 도식(Mermaid)과 분기 표를 템플릿에 포함** |
30
- | 측정 | 없음 | 없음 | 없음 | **같은 질문으로 두 문서 체계의 탐색 비용 비교** |
32
+
33
+ Kiro 스펙과 BMAD는 Spec Kit과 비슷하게 기능 단위이고 구현 전에 쓰이므로 따로 싣지 않았어요. 한국어 기획 문서가 다섯 가지를 모두 비교해요.
31
34
 
32
35
  LLM Wiki에서는 원본/위키/스키마 분리, 인덱스 우선, 추가 전용 로그, 사람이 감독하는 ingest를 그대로 가져왔어요. 일부러 다르게 한 건 셋이에요. 엔티티 페이지 대신 책임 단위로 조직하고(여러 흐름이 분기 축으로 쓰는 개념만 `concepts/`에 둬요), 문서는 에이전트가 유지하되 사람이 소유하고, lint는 LLM 판단 없이 결정론적으로만 해요. 더 자세한 비교는 [docs/IMPLEMENTATION_PLAN.md](docs/IMPLEMENTATION_PLAN.md) 1절에 있어요.
33
36
 
34
37
  ## 중요하게 생각한 것
35
38
 
36
39
  - **현재 상태의 단일 진실.** 변경 디렉터리를 만들지 않아요. 문서는 "무엇이 바뀌었나"가 아니라 "지금 어떻게 동작하나"를 설명해요.
37
- - **추측 금지.** 코드·테스트·승인된 자료로 확인되지 않은 규칙은 "지금은 이렇게 동작한다" 서술과 `[확인 필요]` 질문으로만 남겨요. 질문 수는 `check`가 세어 줘요.
40
+ - **추측 금지.** 코드·테스트·승인된 자료로 확인되지 않은 규칙은 "지금은 이렇게 동작한다" 서술과 `[확인 필요]` 질문으로만 남겨요. `check`는 질문이 남은 문서마다 `open-question` 경고를 하나씩 내요. 어떤 서술이 추측인지는 스킬 규칙과 리뷰어의 몫이고, `check`는 판별하지 못해요.
41
+ - **정책 하나에 주인 하나, 이름 단위로 검사.** `check`는 같은 정책 이름이 두 문서에 있거나 한 문서에 두 번 있으면 거부해요. 다른 이름으로 같은 말을 하는 두 정책은 찾지 못하고, 그 이름 규칙은 writer 스킬이 맡아요.
38
42
  - **사람 우선의 본문.** ID, 상태 표, 문법 강제가 없어요. 탐색 정보는 링크와 제목에만 둬요.
39
43
  - **검사는 결정론적으로, 판단은 사람과 스킬이.** `check`는 구조만 봐요. 문체·내용·노후 판정은 스킬 지침과 사람이 맡아요.
40
44
  - **결정은 한 곳에.** 질문이 답을 얻으면 로그에 `decision` 항목을 남기고 문서는 날짜 링크 하나로 그 결정을 가리켜요.
41
45
  - **두 언어.** `crumbs.json`의 `lang`이 저장소 전체의 템플릿, 스킬, 메시지, AGENTS 블록 언어를 정해요. 기본값은 영어(`en`)이고 `ko`를 쓰면 한국어가 돼요.
42
46
 
43
- ## 실제로 써 보니
47
+ ## 처음 보는 코드에서도 맞게 쓸까요? (RealWorld 실험)
44
48
 
45
- 실제 앱 저장소 세 영역(코드 약 4만 줄)을 기존 스펙 문서를 전혀 보여 주지 않고 crumbs-writer 스킬로 다시 썼어요. 그다음 기존 스펙 체계와 crumbs 체계에 같은 질문 18개를 던져 에이전트가 문서만 읽고 답하게 했어요.
49
+ [RealWorld](https://github.com/realworld-apps/realworld) "Conduit" API를 구현한 공개 백엔드 두 개에 writer 스킬을 돌리고, 그 프로젝트의 공식 API 테스트로 문서를 채점했어요. 생성된 문서, 프롬프트, 정답지, 채점 행은 모두 [crumbs-spec-conduit](https://github.com/Sienna94/crumbs-spec-conduit)에 공개했어요.
46
50
 
47
- **제로 베이스에서 쓰는 데 든 비용** (에이전트 1개가 도메인 하나를 맡음, 병렬 3개)
51
+ - **준비.** 같은 API를 구현한 두 저장소를 커밋으로 고정했어요. [Nitro + Prisma](https://github.com/realworld-apps/nitro-prisma-zod-realworld-example-app)(TypeScript)와 [Django Ninja](https://github.com/c4ffein/realworld-django-ninja)(Python)예요. 구현체마다 Opus 5.5 에이전트 하나가 코드와 테스트만 보고 문서를 썼어요. 쓰는 동안 실험 저장소에는 공식 스펙 저장소가 없었고, 나중 커밋에서 추가했어요.
52
+ - **정답지.** 두 구현체가 고정해 둔 버전의 공식 Hurl 테스트에서 요청 하나를 한 행으로 삼았어요. 준비·정리 요청을 빼면 130행이에요.
53
+ - **채점.** 다른 모델(Sonnet 5.5)이 모든 행을 채점했어요. `correct`는 상태 코드와 검증하는 값을 문서가 모두 적은 경우, `partial`은 하나라도 빠진 경우, `wrong`은 문서가 반대로 적은 경우, `missing`은 다룬 문서가 없는 경우예요. 정책마다 코드와도 대조했어요.
48
54
 
49
- | 도메인 | 코드 범위 | 만든 문서 | 정책 | 테스트로 확정 / 질문 | 에이전트 토큰 | 작성 시간 |
50
- |---|---|---|---|---|---|---|
51
- | 결제 | 약 2만 줄 | 19개 | 100 | 5 / 95 | 57만 | 1시간 7분 |
52
- | 뷰어 | 약 2만 3천 줄 | 40개 | 194 | 29 / 165 | 85만 | 1시간 7분 |
53
- | 인증 | 약 1만 줄 | 35개 | 195 | 64 / 131 | 79만 | 1시간 13분 |
55
+ | | Nitro + Prisma | Django Ninja |
56
+ |---|---|---|
57
+ | 저장소의 코드 / 테스트 | 약 1,400줄 / 단위 테스트 파일 2개 | 약 2,300줄 / 앱마다 엔드포인트 테스트 |
58
+ | 문서 / 정책 | 28 / 51 | 26 / 91 |
59
+ | 테스트로 확정된 정책 / 질문으로 남긴 정책 | 6 / 45 | 50 / 41 |
60
+ | 공식 테스트 행 정답 | **125 / 130** | **116 / 130** |
61
+ | 부분 / 오답 / 누락 | 5 / 0 / 0 | 14 / 0 / 0 |
62
+ | 지어낸 정책(코드에 없는 것) | 0 | 0 |
63
+ | 일부를 과장한 정책 | 1 | 6 |
64
+ | writer 토큰 / 걸린 시간 | 16.5만 / 2시간 40분 | 20.2만 / 2시간 47분 |
54
65
 
55
- 작성 시간은 에이전트 전사의 타임스탬프에서 10분 넘게 비어 있던 대기 시간을 뺀 실제 작업 시간이에요. 세 도메인을 병렬로 돌려 1시간 15분쯤에 모두 끝났어요. 지어낸 정책은 0개였고, 기존 스펙 문서와 코드가 달랐던 6곳을 모두 코드대로 적었어요. 그 뒤 사용자 흐름 단위로 다시 묶어 38개 문서가 됐어요.
66
+ - **공식 테스트와 어긋난 문장도, 지어낸 정책도 없었어요.** 놓친 것은 모두 세부가 빠진 경우예요. Nitro의 부분 정답 5개는 문서가 다루지 않은 `null` 입력과 태그 순서예요. Django의 14개 중 12개는 질문 하나에서 나왔어요. 앱의 오류 처리기와 테스트가 401 응답 본문을 다르게 보여서 writer가 둘 다 적고 질문으로 남겼어요.
67
+ - **질문이 붙은 서술도 대부분 맞았어요.** 아직 `[needs confirmation]`이 붙은 서술만으로 정답 판정을 받은 행이 Nitro 12개, Django 32개예요. "지금은 이렇게 동작해요"에 질문을 붙이는 방식이 테스트가 증명하는 범위를 넘지 않으면서도 문서를 쓸모 있게 했어요.
68
+ - **테스트가 적으면 질문이 많아져요.** Nitro는 단위 테스트 파일이 2개뿐이라 정책 51개 중 45개가 질문으로 남았어요. 그중 29개는 공식 테스트가 답해 주는데, 관리자가 먼저 확정할 목록이 바로 이거예요.
69
+ - **공식 테스트가 다루지 않는 문제도 질문으로 올렸어요.** Django의 글 목록은 토큰을 읽지 않고 세션 사용자를 보고, `createUser` 명령은 마이그레이션에서 이름이 바뀐 필드로 사용자를 찾아요. Nitro 문서는 이메일 형식을 검사하지 않는다고도 그대로 적었는데, 공식 테스트의 더 새 버전은 이걸 검사해요.
56
70
 
57
- **같은 질문 18개로 비교한 탐색 비용** (에이전트는 문서만 읽고 코드는 금지)
71
+ 한계도 있어요. 구현체마다 한 번씩, writer 모델 하나로 돌렸어요. 채점자도 모델이라 작성자가 인용된 앵커가 모두 있는지 확인하고 일부 행을 직접 대조했어요. RealWorld는 널리 알려진 프로젝트라 저장소에 스펙이 없어도 writer가 학습 중에 봤을 수 있어요. 공식 테스트는 문서 내용의 일부만 검증해서, Nitro 정책 20개와 Django 정책 41개는 코드와만 대조했어요. Django 채점자는 의존성 버전을 고정하지 않고 코드를 실행했기 때문에 "일부 과장" 판정 중 몇 개는 라이브러리 버전에 따라 달라질 수 있어요.
58
72
 
59
- | | 기존 스펙 체계 | crumbs-spec |
60
- |---|---|---|
61
- | 정답 | 17 / 18 (부분 1) | 15 / 18 (부분 3) |
62
- | 읽은 파일 수 합계 | 70 (평균 3.9, 최대 8) | 64 (평균 3.6, 최대 6) |
63
- | 검색(grep)으로 떨어진 횟수 | 3 | 1 |
64
- | 토큰 합계 | 232만 | 189만 (−18%) |
73
+ ### 앞서 비공개 앱에서 돌린 결과 (일화)
65
74
 
66
- 기존 체계의 부분 정답은 소유 문서를 못 찾은 경우였고, crumbs의 부분 정답 3개는 모두 문서엔 3~5파일로 도달했는데 writer가 화면 수치·채널 분기 같은 세부를 버린 경우였어요. 그 원인은 스킬 규칙("조건이 붙은 값은 버리지 않는다")으로 고쳤어요. 질문이 기존 스펙에서 나와 기존 체계에 유리한 편향이 있다는 점은 감안해 주세요.
75
+ 이 실험 전에 비공개 앱의 세 영역(코드 약 4만 줄)을 다시 쓰고, 기존 스펙 체계와 crumbs에 같은 질문 18개를 문서만 보고 답하게 했어요. crumbs는 17개 대 15개로 정답이 적었지만 읽은 파일은 70개 대 64개, 토큰은 18% 적었어요. crumbs의 부분 정답 3개는 writer가 세부를 버린 경우여서 "조건이 붙은 값은 버리지 않는다" 규칙을 스킬에 넣었어요. 작성자가 채점했고, 질문은 기존 스펙에서 나왔고, 데이터는 공개할 수 없어서 일화로 읽어 주세요. 탐색 비용 비교는 아직 공개 코드에서 다시 돌리지 않았어요.
67
76
 
68
77
  ## 설치와 빠른 시작
69
78
 
@@ -77,7 +86,7 @@ npx crumbs-spec check # 링크 그래프와 구조 검사
77
86
 
78
87
  한국어 저장소는 `--lang ko`를 붙여요.
79
88
 
80
- `init`은 다음 일곱 파일을 만들고, 이미 있는 파일은 절대 덮어쓰지 않아요.
89
+ `init`은 다음 파일을 만들고, 이미 있는 파일은 절대 덮어쓰지 않아요. `--skills all`이면 SKILL.md 여덟 개도 함께 써요. AGENTS.md에서는 `<!-- crumbs:start -->`와 `<!-- crumbs:end -->` 사이의 자기 블록만 바꿔요.
81
90
 
82
91
  ```
83
92
  crumbs.json 설정
@@ -89,7 +98,9 @@ docs/raw/README.md 원본 자료 디렉터리(읽기 전용)
89
98
  AGENTS.md crumbs 블록(에이전트 규칙)
90
99
  ```
91
100
 
92
- npm에 공개하기 전에는 저장소에서 `npm run build` 뒤 `node dist/src/cli.js <명령>`으로 실행해요.
101
+ `crumbs.json`이 이미 있으면 `init`은 그 설정을 그대로 두고 `--spec-dir`와 `--lang`은 무시해요(그렇다고 알려 줘요). `init`의 마지막 줄은 다음에 실행할 명령이에요: `npx crumbs-spec check`.
102
+
103
+ npm에 [`crumbs-spec`](https://www.npmjs.com/package/crumbs-spec)으로 올라가 있어요. 저장소를 클론해서 쓰려면 `npm run build` 뒤 `node dist/src/cli.js <명령>`으로 실행해요.
93
104
 
94
105
  ## 기존 프로젝트에 추가하기
95
106
 
@@ -101,7 +112,7 @@ npm에 공개하기 전에는 저장소에서 `npm run build` 뒤 `node dist/src
101
112
  npx crumbs-spec init --spec-dir docs/crumbs --skills claude
102
113
  ```
103
114
 
104
- 2. **에이전트가 규칙을 읽게 해요.** Claude Code는 `CLAUDE.md`에 `@AGENTS.md` 한 줄이 있어야 crumbs 블록을 읽어요. 스킬은 `.claude/skills/`(Claude Code)와 `.agents/skills/`(Codex)에서 자동으로 찾아요.
115
+ 2. **에이전트가 규칙을 읽게 해요.** Codex와 Claude Code(v2.1.277 이상)는 `AGENTS.md`를 바로 읽어요. 다만 `CLAUDE.md`(또는 `.claude/CLAUDE.md`, `CLAUDE.local.md`)가 있는 저장소에서는 Claude Code가 그 파일만 읽으니, 거기에 `@AGENTS.md` 한 줄을 넣어 crumbs 블록을 가져와요. 이때 `init`이 안내를 출력해요. 스킬은 `.claude/skills/`(Claude Code)와 `.agents/skills/`(Codex)에서 자동으로 찾아요.
105
116
 
106
117
  3. **기존 문서는 옮기지 않아요.** writer는 코드와 테스트만 원본으로 삼고, 기존 위키·설계 메모·README는 사용자가 원본 자료로 지목하지 않는 한 읽지 않아요. 지금 코드와 맞는지 확인할 수 없기 때문이에요. 기존 문서 중 근거로 쓸 것(기획서, 계약서, 정책 결정 기록)은 `docs/raw/`에 복사해 두고 `crumbs-ingest`로 한 건씩 반영해요.
107
118
 
@@ -112,11 +123,11 @@ npm에 공개하기 전에는 저장소에서 `npm run build` 뒤 `node dist/src
112
123
  도메인 디렉터리는 docs/crumbs/billing/이다. 확인하지 못한 정책은 질문으로 남기고 끝까지 해 줘.
113
124
  ```
114
125
 
115
- 5. **`check`를 CI에 걸어요.** 오류(끊어진 링크·앵커, 도달 불가 문서, 빠진 역링크, 없는 코드 경로, 정책 이름 중복)만 실패시키고 경고는 둬요. 아직 crumbs로 다루지 않는 디렉터리가 스펙 루트 안에 있으면 `crumbs.json`의 `exclude`에 넣어요.
126
+ 5. **`check`를 CI에 걸어요.** 오류(아래 검사 규칙 표에서 error인 규칙)만 실패시키고 경고는 무시해요. 스펙 루트 안의 Markdown 가운데 아직 crumbs 문서가 아닌 것은 `crumbs.json`의 `exclude`에 넣어요. 항목은 스펙 루트 기준 상대 경로이고, `/`로 끝나면 그 디렉터리를 제외해요. 글롭 패턴은 없어요. 필수 절이 하나도 없는 문서는 `check`가 알려 줘요.
116
127
 
117
128
  6. **코드를 바꿀 때 문서를 같이 바꿔요.** AGENTS 블록이 "요청이 없어도 같은 변경에서 소유 문서를 갱신한다"를 에이전트에게 요구하니까, PR마다 `check`와 로그 항목이 함께 들어와요. 도메인이 하나씩 늘어나면서 인덱스가 채워져요.
118
129
 
119
- 기존 스펙 체계가 있다면 두 체계에 같은 질문을 던져 비교해 보세요. 읽은 파일 수와 정답률로 어느 쪽이 더 잘 찾히는지 바로 보여요(5단계 벤치마크 러너가 이걸 자동화할 예정이에요).
130
+ 기존 스펙 체계가 있다면 두 체계에 같은 질문을 던져 비교해 보세요. 읽은 파일 수와 정답률로 어느 쪽이 더 잘 찾히는지 바로 보여요.
120
131
 
121
132
  ## 스펙이 만들어지는 순서
122
133
 
@@ -162,51 +173,46 @@ docs/raw/ 계약서·기획 메모 같은 불변 원본
162
173
  | 현재 동작 | 사용자가 겪는 대로, 현재형으로. 단계가 셋 이상이면 `###` 소제목 |
163
174
  | 흐름 | 두 단계 이상이면 Mermaid 도식, 비교 축이 둘 이상이면 표 |
164
175
  | 정책 | `### 정책 이름`(저장소 전체에서 유일)과 규칙. 미확정이면 지금 동작 서술 뒤에 `[확인 필요]` 질문 |
165
- | 근거 | 백틱 코드 경로와 설명. `check`가 경로의 존재를 확인해요 |
176
+ | 근거 | `/`가 들어간 백틱 경로와 설명. `check`가 경로의 존재를 확인해요 |
166
177
  | 확인 필요 사항 | `- [확인 필요] [정책 이름](#앵커) — 질문` |
167
178
  | 관련 문서 | 개요로 가는 링크와 이웃 문서. 역링크는 상호여야 해요 |
168
179
 
169
- 예시(`docs/specs/shopping/장바구니.md`):
180
+ 예시: [RealWorld 실험](#처음-보는-코드에서도-맞게-쓸까요-realworld-실험)에서 writer가 Nitro 코드로 만든 상세 문서를 고치지 않고 그대로 실었어요([원본](https://github.com/Sienna94/crumbs-spec-conduit/blob/main/nitro/docs/specs/api/articles/tag-list.md)). 이 저장소는 `lang: en`이라 제목이 영어예요. 아래 사용 예시 3에서 이 문서의 질문에 답하고 흐름도가 추가돼요.
170
181
 
171
182
  ````markdown
172
- # 장바구니
173
- <!-- ../index.md를 먼저 읽는다. 이 문서를 바꾼 뒤 ../log.md에 항목을 추가한다 -->
174
-
175
- ## 목적
176
- 사용자가 상품을 담고 수량을 바꾸고 비울 때까지의 흐름을 맡는다. 쿠폰 적용은 [쿠폰 할인](쿠폰-할인.md)이 맡는다.
177
-
178
- ## 현재 동작
179
- - 사용자가 같은 상품을 다시 담으면 앱은 수량을 더한다.
180
- - 수량이 99를 넘으면 앱은 "최대 99개까지 담을 수 있습니다." 안내를 보이고 담지 않는다.
181
-
182
- ## 흐름
183
- ```mermaid
184
- flowchart TD
185
- A["상품 담기"] --> B{"같은 상품이 있나"}
186
- B -->|예| C["수량 합산"]
187
- B -->|아니오| D["새 줄 추가"]
188
- C --> E{"99 초과"}
189
- E -->|예| F["안내 후 담지 않음"]
190
- ```
191
-
192
- ## 정책
193
- ### 수량 상한
194
- 장바구니 한 줄의 수량은 99 이하다.
195
-
196
- ### 최소 주문 금액
197
- 지금은 합계가 10,000원 미만이면 결제 버튼을 비활성화한다.
198
- [확인 필요] 10,000원이라는 기준의 근거는 무엇인가?
199
-
200
- ## 근거
201
- - `src/cart/cart.ts:12-48` — 담기와 수량 합산
202
- - `test/cart/cart.test.ts` — 수량 상한
203
-
204
- ## 확인 필요 사항
205
- - [확인 필요] [최소 주문 금액](#최소-주문-금액) — 10,000원이라는 기준의 근거는 무엇인가?
206
-
207
- ## 관련 문서
208
- - [쇼핑 개요](overview.md)
209
- - [쿠폰 할인](쿠폰-할인.md)
183
+ # Tag list
184
+ <!-- Read ../../index.md first. After changing this document, add an entry to ../../log.md -->
185
+
186
+ ## Purpose
187
+ This document owns GET /api/tags, which returns the most used tag names. How tags are attached to articles is owned by [Article creation](article-create.md#article-tag-name-handling). The endpoint allows anonymous viewers as described in [Authentication token](../authentication-token.md), and the list is the same for anonymous and signed-in viewers.
188
+
189
+ ## Current behavior
190
+ - The server considers only tags that at least one article uses.
191
+ - The server sorts those tags by the number of articles that use them, most used first.
192
+ - The server returns at most 10 tag names.
193
+ - The server responds with status 200 and {"tags": ["<name>"…]}.
194
+ - If no article uses a tag, the list is empty.
195
+
196
+ ## Flow
197
+ None
198
+
199
+ ## Policies
200
+ ### Popular tag list
201
+ The tag list holds at most 10 names of tags used by at least one article, sorted by the number of articles that use each tag, most used first. The order of tags used by the same number of articles is not defined, and the list has no pagination. No test covers this rule.
202
+ [needs confirmation] What is the basis of the 10-tag limit, and what should order tags with the same usage count?
203
+
204
+ ## Evidence
205
+ - `server/routes/api/tags/index.get.ts` — selects tags used by at least one article, sorts them by usage and returns at most 10 names
206
+ - `prisma/schema.prisma` — unique tag names and the relation between tags and articles
207
+
208
+ ## Open questions
209
+ - [needs confirmation] [Popular tag list](#popular-tag-list) — What is the basis of the 10-tag limit, and what should order tags with the same usage count?
210
+
211
+ ## Related
212
+ - [Articles API overview](overview.md)
213
+ - [Article creation](article-create.md)
214
+ - [Article update](article-update.md)
215
+ - [Article deletion](article-delete.md)
210
216
  ````
211
217
 
212
218
  문서 본문은 간결한 `-한다`체로 써요. 개요의 `## 용어`는 별칭을 소유 정책으로 잇는 링크 목록이에요(`- 정액권(정액제) — [정액권 이용 중 판정](정액권.md#정액권-이용-중-판정)`). 정의만 있는 용어는 여기서 끝나고, 여러 흐름이 분기 축으로 쓰는 개념(채널, 재화, 회원 상태)만 `concepts/`에 소유 문서를 둬요.
@@ -224,26 +230,32 @@ npx crumbs-spec log add --kind <ingest|query|lint|drift|change|decision> "<제
224
230
  `check`의 출력은 `파일:줄 심각도 규칙: 메시지` 한 줄씩이고 마지막 줄이 요약이에요.
225
231
 
226
232
  ```
227
- docs/specs/shopping/장바구니.md:34 error broken-anchor: 앵커가 없음: 쿠폰-할인.md#적용-순서
228
- docs/specs/shopping/장바구니.md:20 warning open-question: 미해결 질문 표시 2개
229
-
230
- crumbs check: 문서 4개, 오류 1개, 경고 1개
233
+ docs/specs/api/articles/tag-list.md:31 error broken-link: link target not found: ../log.md#2026-11-03-decision--popular-tag-list-most-used-first-stays-ties-are-undefined-10-tags-are-cut-after-sorting-and-every-viewer-gets-the-same-unpaginated-list
234
+ docs/specs/api/articles/article-delete.md:44 warning open-question: 1 open question(s)
235
+ …
236
+ crumbs check: 28 documents, 1 error, 20 warnings
231
237
  ```
232
238
 
239
+ 사용 예시 3에서 writer가 결정 링크를 고치기 전에 나온 출력이고, 나머지 경고 19개는 생략했어요. 이 저장소는 `lang: en`이라 메시지가 영어예요. `lang: ko`에서는 `링크 대상이 없음`, `미해결 질문 1개`, `crumbs check: 문서 28개, 오류 1개, 경고 20개`처럼 나와요.
240
+
241
+ **지원하는 Markdown.** 제목은 ATX(`#`부터 `######`까지)만 인식하고 setext 제목(밑줄식)은 인식하지 않아요. 인라인 링크와 참조 링크(`[텍스트][라벨]`, `[라벨][]`와 `[라벨]: 경로` 정의)를 검사해요. `[라벨]`만 쓴 축약 참조는 링크가 아니에요. 제목 앵커는 github-slugger 방식이에요. 공백마다 `-`가 되고, `-`와 `_`를 뺀 문장부호는 지우며, 연속된 `-`는 합치지 않아요. 예를 들어 `### 수량 & 한도`는 `#수량--한도`가 돼요. 스펙 루트 아래의 심볼릭 링크 파일과 디렉터리도 따라가요. 근거 경로는 `codeRoot`(기본값은 저장소 루트) 기준으로 해석하고, `rawDir` 아래 경로만 언제나 저장소 루트 기준으로 해석해요. 근거 경로에는 `/`가 있어야 하며 저장소 루트의 파일은 `./package.json`처럼 써요. `:12`, `:12-48`, `:12:5`, `:12-48,60-70` 같은 줄 표기를 받아요. `exclude` 항목은 스펙 루트 기준 상대 경로이고, `/`로 끝나면 그 디렉터리를 제외해요. JSON 출력의 모든 항목에는 `line`이 있어요(더 나은 위치가 없으면 1).
242
+
233
243
  ### 검사 규칙
234
244
 
235
245
  | 규칙 | 심각도 | 잡는 것 |
236
246
  |---|---|---|
237
247
  | `index-missing` | 오류 | 인덱스 문서가 없어요(다른 규칙은 돌지 않아요) |
238
248
  | `missing-title` | 오류 | 첫 제목이 H1이 아니에요 |
239
- | `broken-link` / `broken-anchor` | 오류 | 링크 대상 파일·제목이 없어요 |
249
+ | `broken-link` | 오류 | 링크 대상 파일이 없거나, 철자(대소문자·유니코드 정규화)가 디스크의 파일과 달라요 |
250
+ | `broken-anchor` | 오류 | 링크가 가리키는 제목(앵커)이 없어요 |
240
251
  | `unreachable` | 오류 | 인덱스에서 링크로 닿을 수 없는 문서예요 |
241
252
  | `missing-section` | 오류 | 역할별 필수 절이 없어요 |
242
253
  | `missing-backlink` | 오류 | 부모(개요·인덱스)로 돌아가는 링크가 없거나, 상세 A→B 링크에 B→A가 없어요 |
243
254
  | `missing-code-path` | 오류 | 근거의 코드 경로가 존재하지 않아요 |
244
- | `duplicate-policy` | 오류 | 같은 정책 이름이 두 문서에 있어요 |
245
- | `log-format` | 오류 | 로그 항목이 `## [YYYY-MM-DD] kind \| 제목` 형식이 아니에요 |
246
- | `open-question` | 경고 | 문서의 `[확인 필요]` 수예요 |
255
+ | `duplicate-policy` | 오류 | 같은 정책 이름이 두 문서에 있거나 한 문서에 두 번 있어요 |
256
+ | `log-format` | 오류 | 로그 항목이 `## [YYYY-MM-DD] kind \| 제목` 형식이 아니거나, 날짜가 하루를 넘어 미래예요 |
257
+ | `unterminated-comment` | 오류 | HTML 주석이 열린 뒤 닫히지 않아, 그 뒤의 문서 전체가 가려져요 |
258
+ | `open-question` | 경고 | 질문이 남은 문서마다 경고 하나예요. 개수는 미해결 질문 수예요 |
247
259
  | `log-order` | 경고 | 로그 날짜가 역순이에요 |
248
260
  | `missing-flow` | 경고 | 현재 동작이 6단계 이상인데 도식도 표도 없어요 |
249
261
  | `section-order` | 경고 | 절 순서가 템플릿과 달라요 |
@@ -251,30 +263,39 @@ crumbs check: 문서 4개, 오류 1개, 경고 1개
251
263
 
252
264
  ## 로그
253
265
 
254
- 로그는 한 곳(`docs/specs/log.md`)에 시간순으로 덧붙여요. 제목은 작업명이 아니라 **사용자 결과 한 문장**이고, 본문은 세 줄이에요.
266
+ 로그는 한 곳(`docs/specs/log.md`)에 시간순으로 덧붙여요. 제목은 작업명이 아니라 **사용자 결과 한 문장**이고, 본문은 세 줄이에요. 항목은 `log add`로 쓰고, 제목이나 본문에 `<!--`가 있으면 거부해요.
255
267
 
256
268
  ```bash
257
- npx crumbs-spec log add --kind change "장바구니 수량 상한을 99로 올림" \
258
- --body "결과: 한 줄에 99개까지 담을 수 있다. 이유: 요청 SHOP-42. 문서: [수량 상한](shopping/장바구니.md#수량-상한)"
269
+ npx crumbs-spec log add --kind change "<사용자 결과 한 문장>" \
270
+ --body "결과: <바뀐 것>. 이유: <까닭>. 문서: [<정책>](<도메인>/<문서>.md#<앵커>)"
271
+ ```
272
+
273
+ 사용 예시 5에서 남긴 항목이에요. 이 저장소는 `lang: en`이라 본문 머리가 `Result:`, `Reason:`, `Document:`예요.
274
+
275
+ ```markdown
276
+ ## [2026-11-03] change | A malformed email on registration, login and account update is rejected with 422 and is invalid
277
+ Result: a malformed email on registration, login and account update gets status 422 with {"errors": {"email": ["is invalid"]}}; 3 api documents updated and 1 open question remains about accepted address forms. Reason: code change in server/schemas/user.schema.ts, reported by the user. Document: [Email format](api/users/registration.md#email-format)
259
278
  ```
260
279
 
261
- `[확인 필요]` 질문이 답을 얻으면 `decision` 항목을 남기고, 확정한 정책 본문 끝에 결정 링크 하나를 둬요. 문서 안에 변경 이력을 두지 않는 대신 결정은 로그에 쌓이고 문서는 링크로만 가리켜요.
280
+ `[확인 필요]` 질문이 답을 얻으면 `decision` 항목을 남기고, 확정한 정책 본문 끝에 결정 링크 하나를 둬요. 문서 안에 변경 이력을 두지 않는 대신 결정은 로그에 쌓이고 문서는 링크로만 가리켜요. 슬러그는 대괄호·`|`·쉼표·콜론을 빼므로, `|`가 있던 자리는 `--`가 돼요. 사용 예시 3에서 남긴 결정이에요.
262
281
 
263
282
  ```markdown
264
- ## [2026-11-02] decision | 최소 주문 금액: 10,000원은 배송비 정책에서 온 값이다
265
- 결과: 합계 10,000원 미만이면 결제 버튼이 비활성이다. 이유: 2026-11-02 기획 확인. 문서: [최소 주문 금액](shopping/장바구니.md#최소-주문-금액)
283
+ ## [2026-11-03] decision | Popular tag list: most used first stays, ties are undefined, 10 tags are cut after sorting, and every viewer gets the same unpaginated list
284
+ Result: GET /api/tags keeps the most-used-first order, leaves the order of equally used tags undefined, applies the 10-tag limit after sorting, and returns the same list without pagination to every viewer. Reason: answers from the maintainer; existing clients may rely on most-used-first and the limit keeps the tag sidebar short. Document: [Popular tag list](api/articles/tag-list.md#popular-tag-list)
266
285
  ```
267
286
 
268
287
  ```markdown
269
- ### 최소 주문 금액
270
- 합계가 10,000원 미만이면 결제 버튼을 비활성화한다. (결정: [2026-11-02](../log.md#2026-11-02-decision-최소-주문-금액-10000원은-배송비-정책에서-온-값이다))
288
+ ### Popular tag list
289
+ The tag list holds at most 10 names of tags used by at least one article, sorted by the number of articles that use each tag, most used first, and the server applies the 10-tag limit after sorting. … (decision: [2026-11-03](../../log.md#2026-11-03-decision--popular-tag-list-most-used-first-stays-ties-are-undefined-10-tags-are-cut-after-sorting-and-every-viewer-gets-the-same-unpaginated-list))
271
290
  ```
272
291
 
273
292
  kind는 `ingest`(원본 자료 반영), `query`(답을 찾아 환류), `lint`, `drift`, `change`(문서 변경), `decision`(질문 확정)이에요.
274
293
 
275
294
  ## 스킬
276
295
 
277
- `init --skills`는 에이전트가 crumbs 문서를 다루는 절차를 `.claude/skills/<이름>/SKILL.md`(Claude Code)와 `.agents/skills/<이름>/SKILL.md`(Codex)에 설치해요. 기존 `SKILL.md`는 덮어쓰지 않으니 새 버전으로 바꾸려면 지우고 다시 설치해요. Claude Code는 `CLAUDE.md`에 `@AGENTS.md`가 있어야 crumbs 블록의 규칙을 읽어요.
296
+ `init --skills`는 에이전트가 crumbs 문서를 다루는 절차를 `.claude/skills/<이름>/SKILL.md`(Claude Code)와 `.agents/skills/<이름>/SKILL.md`(Codex)에 설치해요. 기존 `SKILL.md`는 덮어쓰지 않으니 새 버전으로 바꾸려면 지우고 다시 설치해요. Claude Code가 crumbs 블록을 언제 읽는지는 [기존 프로젝트에 추가하기](#기존-프로젝트에-추가하기) 2단계에 있어요.
297
+
298
+ 생성된 SKILL.md는 frontmatter 바로 뒤에 그것을 만든 crumbs-spec 버전을 적은 주석 한 줄을 둬요. 스킬과 템플릿은 패키지 버전을 따라요. 마이너 릴리스는 문구를 바꿀 수 있고 패치 릴리스는 바꾸지 않아요. 새 버전으로 올리려면 파일을 지우고 `init --skills`를 다시 실행해요.
278
299
 
279
300
  | 스킬 | 하는 일 |
280
301
  |---|---|
@@ -285,57 +306,70 @@ kind는 `ingest`(원본 자료 반영), `query`(답을 찾아 환류), `lint`, `
285
306
 
286
307
  ## 사용 예시
287
308
 
309
+ 아래 예시는 모두 실험의 Nitro 백엔드로 crumbs-spec-conduit의 [`usage/`](https://github.com/Sienna94/crumbs-spec-conduit/tree/main/usage) 작업공간에서 실제로 돌린 결과예요. 예시 1은 실험의 Opus 5.5 writer예요. 예시 2~5는 요청문만 받은 Claude Sonnet 5.5 에이전트가 `AGENTS.md`를 읽고 스킬을 스스로 골랐고, 에이전트가 물은 것은 작성자가 관리자 입장에서 답했어요. 요청문은 실제로는 영어로 보냈고, 여기서는 요청문과 `check` 출력을 한국어로 옮겼어요. 작업공간이 `lang: en`이라 실제 문서에는 질문 표시와 절 이름이 영어로 들어가 있어요. 파일을 바꾼 예시는 하나씩 커밋으로 남겨 링크했어요.
310
+
288
311
  ### 1. 기존 코드를 처음 문서화해요
289
312
 
290
313
  ```
291
- src/cart/ 아래 코드를 crumbs-writer의 "기존 코드 문서화" 모드로 문서화해 줘.
292
- 도메인 디렉터리는 docs/specs/shopping/이고, 확인하지 못한 정책은 질문으로 남기고 끝까지 해 줘.
314
+ ../vendor/nitro 아래 코드를 crumbs-writer의 "기존 코드 문서화" 모드로 문서화해 줘.
315
+ 코드와 테스트만 근거로 삼고, 확인하지 못한 정책은 질문으로 남기고 끝까지 해 줘.
293
316
  ```
294
317
 
295
- 에이전트는 인덱스와 템플릿을 읽고, 진입점부터 코드와 테스트를 따라가며 `shopping/overview.md`와 상세 문서를 만들어요. 테스트가 확정하는 규칙은 정책으로, 코드에만 있는 규칙은 "지금은 …" 서술과 `[확인 필요]` 질문으로 적어요. 끝에 `check`를 돌려 오류를 없애고 로그를 남겨요.
318
+ 실험의 [writer 프롬프트](https://github.com/Sienna94/crumbs-spec-conduit/blob/main/eval/prompts/writer.md)를 줄인 형태예요. writer는 라우트 처리기부터 코드를 따라가며 API를 하위 디렉터리 네 개로 나누고, 상세 문서 21개와 개요를 만들었어요. 저장소에 단위 테스트 파일이 2개뿐이라 테스트로 확정된 정책은 6개, `[확인 필요]` 질문은 45개였어요. 마지막에 `check`를 돌리고 로그를 남겼어요([커밋](https://github.com/Sienna94/crumbs-spec-conduit/commit/572b0db)).
296
319
 
297
320
  ```
298
- crumbs check: 문서 5개, 오류 0개, 경고 3개
321
+ crumbs check: 문서 28개, 오류 0개, 경고 21개
299
322
  ```
300
323
 
301
- 경고는 남긴 질문의 수예요. `확인 필요 사항` 절만 훑어도 무엇을 결정해야 하는지 알 수 있어요.
324
+ 경고 21개는 아직 질문이 남은 상세 문서 21개예요. 각 문서의 확인 필요 사항 절이 관리자가 정해야 할 목록이에요.
302
325
 
303
- ### 2. 코드 변경을 문서에 반영해요
326
+ ### 2. 답을 찾아요
304
327
 
305
328
  ```
306
- 장바구니 수량 상한을 99에서 199로 올렸어. 문서도 맞춰 줘.
329
+ GET /api/tags는 태그를 몇 개, 어떤 순서로 돌려줘?
307
330
  ```
308
331
 
309
- writer는 `grep -rl 'src/cart/cart.ts' docs/specs`로 근거에 그 경로를 적은 소유 문서를 찾고, "현재 동작"과 `### 수량 상한` 본문을 고치고, 흐름 도식의 값을 맞추고, 로그에 `change | 장바구니 수량 상한을 199로 올림`을 남겨요. 소유 문서가 둘로 보이면 멈추고 어느 쪽이 소유자인지 물어요.
332
+ finder는 색인 → API 개요 → 글 개요 → [tag-list.md](https://github.com/Sienna94/crumbs-spec-conduit/blob/main/nitro/docs/specs/api/articles/tag-list.md) 순서로 스펙 문서 4개만 읽고, 코드는 보지 않은 채 답했어요. 태그는 최대 10개이고, 많이 쓰인 순서이며, 글 하나 이상이 쓰는 태그만 나오고 페이지 나눔은 없다고요. 같은 개수인 태그의 순서와 10개 제한의 근거는 아직 `[확인 필요]` 질문이라는 점도 함께 알렸어요. 바뀐 파일은 없어요.
310
333
 
311
334
  ### 3. 모르는 정책은 묻고, 답은 결정으로 남겨요
312
335
 
313
336
  ```
314
- 쿠폰 중복 적용 규칙을 바꾸려고 해. 먼저 뭘 확인해야 하지?
337
+ 태그 목록의 정렬 방식을 바꾸고 싶어. 먼저 무엇을 확인해야 해?
315
338
  ```
316
339
 
317
- interview가 요청이 건드리는 정책을 나열하고, 코드·테스트·원본으로 확인된 것과 미확인인 것을 가른 뒤 미확인 정책을 질문으로 내요. 답하시면 writer가 정책을 확정 서술로 바꾸고 `decision` 로그와 결정 링크를 남겨요.
340
+ 질문 스킬은 요청이 건드리는 정책 8개를 찾아 코드로 확정된 3개를 표시하고, 나머지를 질문 7개로 만들었어요. "태그 목록이 GET /api/tags인지 글 안의 tagList인지"부터 "기존 클라이언트가 지금 순서에 기대는지"까지예요. 문서는 하나도 고치지 않고 멈췄어요. 답은 이랬어요. 대상은 GET /api/tags뿐이고, 많이 쓰인 순서를 유지하고, 같은 개수의 순서는 일부러 정하지 않으며, 10개 제한은 정렬 뒤에 그대로 적용해요. writer가 정책을 확정하고 `decision` 항목과 결정 링크를 남겼어요([로그](#로그) 참고). 그 과정에서 `check`가 두 가지를 잡았어요. 결정 링크가 `../../log.md`가 아니라 `../log.md`를 가리킨 것(`broken-link`), 그리고 현재 동작이 일곱 단계로 늘었는데 흐름도가 없는 것(`missing-flow`)이에요. 그래서 writer가 흐름도를 추가했어요([커밋](https://github.com/Sienna94/crumbs-spec-conduit/commit/7b86052)).
318
341
 
319
342
  ### 4. 원본 자료를 반영해요
320
343
 
321
344
  ```
322
- docs/raw/coupon-policy-v3.pdf를 반영해 줘.
345
+ 상위 API 스펙이 바뀌었어. docs/raw/realworld-b8a37f1-validate-email-format.diff를 반영해 줘.
323
346
  ```
324
347
 
325
- ingest가 자료를 읽고 영향을 받는 문서를 인덱스·역링크·근거로 찾아 "바꿀 내용 목록"을 먼저 보여 줘요. 확인을 받은 뒤에만 소유 문서를 고치고 `ingest` 로그를 남겨요. 자동 병합은 하지 않아요.
348
+ 자료는 RealWorld 스펙에 이메일 형식 검사를 추가한 실제 상위 커밋이에요. 반영 스킬은 자료를 읽고 색인을 따라 회원가입·로그인·정보 수정 문서를 찾은 뒤, 자료의 주장 4개를 정리했어요. 그리고 충돌에서 멈췄어요. 자료는 형식이 틀린 이메일에 422 "is invalid"를 기대하는데, 문서와 코드는 형식을 검사하지 않는다고 해요. 자료와 코드 중 무엇을 따를지 물어서, 문서는 현재 동작을 설명하므로 코드를 따르기로 했어요. 스킬은 회원가입 문서에 이메일 형식 정책 하나를 두고 현재 동작과 상위 스펙을 인용한 질문을 적었어요. 로그인·정보 수정 문서에는 그 정책으로 가는 문장 하나씩만 넣고 `ingest` 항목을 남겼어요([커밋](https://github.com/Sienna94/crumbs-spec-conduit/commit/130946a)). 변경 목록을 확인받기 전에는 아무것도 고치지 않았어요.
326
349
 
327
- ### 5. 답을 찾아요
350
+ ### 5. 코드 변경을 문서에 반영해요
328
351
 
329
352
  ```
330
- 장바구니에 몇 개까지 담을 수 있어?
353
+ 회원가입·로그인·정보 수정에 이메일 형식 검사를 추가했어
354
+ (server/schemas/user.schema.ts, 아직 커밋 전). 이제 형식이 틀린 이메일은
355
+ 422와 {"errors":{"email":["is invalid"]}}를 받아. 문서를 갱신해 줘.
331
356
  ```
332
357
 
333
- finder는 인덱스 → 쇼핑 개요 → 장바구니 문서 순으로 내려가 `### 수량 상한`을 출처로 답해요. 용어·개념 질문이면 `concepts/overview.md`의 용어 절을 먼저 봐요. 문서에 답이 없으면 "문서에 없음"과 가장 가까이 간 문서를 말해요.
358
+ writer는 `grep -rl 'user.schema' docs/specs`로 소유 문서를 찾았어요. 세 문서 모두 근거 절에 이 파일을 적어 두었기 때문이에요. 이메일 형식 정책을 확정 정책으로 고쳐 예시 4의 질문을 닫고, 로그인·정보 수정의 연결 문장과 흐름도 두 개를 고쳤어요. 코드만으로 정해지지 않는 것, 즉 어떤 주소를 유효로 볼지와 빈 이메일이 "is invalid"도 함께 보고하는지는 추측하지 않고 새 질문으로 남겼어요. 실제로 빈 이메일에는 두 메시지가 모두 나와요. 코드와 문서는 한 커밋이에요([커밋](https://github.com/Sienna94/crumbs-spec-conduit/commit/32fd2f3)).
334
359
 
335
360
  ### 6. CI에서 검사해요
336
361
 
362
+ 오류가 있으면 실패하는 GitHub Actions 작업의 예시예요.
363
+
337
364
  ```yaml
338
- - run: npx crumbs-spec check --format json > crumbs-report.json
365
+ jobs:
366
+ specs:
367
+ runs-on: ubuntu-latest
368
+ steps:
369
+ - uses: actions/checkout@v4
370
+ - uses: actions/setup-node@v4
371
+ with: { node-version: 20 }
372
+ - run: npx crumbs-spec check --format json > crumbs-report.json
339
373
  ```
340
374
 
341
375
  종료 코드 1이면 오류가 있어요. 경고(`open-question`, `missing-flow` 등)는 실패시키지 않아요.
@@ -355,6 +389,10 @@ finder는 인덱스 → 쇼핑 개요 → 장바구니 문서 순으로 내려
355
389
  - 정의만 있는 용어 페이지. 개요의 용어 절로 충분해요.
356
390
  - `check`의 문체·내용 판정. 문체는 스킬 지침과 [docs/lint/](docs/lint/README.md)의 markdownlint 설정 샘플이 맡아요.
357
391
 
392
+ ## 테스트와 호환성
393
+
394
+ `npm test`는 CI에서 Linux와 macOS 위에서 node:test 테스트를 돌려요(`.github/workflows/ci.yml` 참고). 변경 내역은 [CHANGELOG.md](CHANGELOG.md)에 있어요. 1.0 전에는 마이너 버전에서 검사 규칙, 템플릿, 스킬 문구가 바뀔 수 있고, 패치 버전은 버그만 고쳐요.
395
+
358
396
  ## 로드맵
359
397
 
360
398
  | 단계 | 내용 | 상태 |
@@ -363,4 +401,4 @@ finder는 인덱스 → 쇼핑 개요 → 장바구니 문서 순으로 내려
363
401
  | 2 | 스킬 4종, `log add`, 흐름 절, 책임 단위 기준, 용어·개념 페이지, 결정 로그 | 완료 |
364
402
  | 3 | `crumbs reverse <path>` — 코드에서 초안 생성 명령(지금은 writer 스킬이 대신해요) | 예정 |
365
403
  | 4 | `crumbs drift --base <ref>` — diff로 근거가 바뀐 문서를 찾아요 | 예정 |
366
- | 5 | 예제 앱과 탐색 비용 벤치마크 러너 | 예정 |
404
+ | 5 | 공개 예제([crumbs-spec-conduit](https://github.com/Sienna94/crumbs-spec-conduit))와 탐색 비용 벤치마크 러너 | 예제 완료, 러너 예정 |