pipeline: make tech-log-tree.json the one decomposition contract and enforce it
리뷰 두 건을 반영했다. 계약 - tech-log-tree.json 하나가 분해 계약이자 색인이다. 사람이 읽는 트리·Node Specification· 후보 대장은 없어졌고, 문서에 남아 있던 그 개념을 걷어냈다 - candidateScope — 후보를 찾는 SSOT 범위. 접어 넣은 제2부·제3부는 근거이지 후보가 아니다 - sourceRepository — 분석한 저장소의 경로·리비전·판단 근거. 리비전을 모르면 null 로 두고 지어내지 않는다. 갈래가 여럿이면 revisions - 검사기: 계약 미채택·PENDING·PROMOTE↔글감 양방향·candidateScope·sourceRepository 를 error/warn 으로 센다. 옛 스키마도 검사를 피하지 못한다. 테스트 22 → 31 기록 쓰기 - 템플릿 5종에 source·sourceRevision·topicName, Question 에 닫는 조건, 본문 없는 종류에서 assets 제거. 고정 절 개수 삭제 - check_evidence.mjs — 인용한 코드가 SSOT 에 있는지, 앵커가 SSOT 를 가리키는지, 제목이 계약과 같은지, 리비전이 저장소에 있는지. 게시된 기록에서 SSOT 와 다른 URL 을 잡았다 문체 - 문체 규칙의 정본을 ai-tells.md 로. explaining.md 의 질문체 제목·절 끝 대조 반복·그림 예고 규칙을 삭제해 충돌을 없앴다. 첫 절 「설명 뒤에 평가를 붙이지 않는다」에 지우는 사례 네 유형 - voice 스킬의 「독자 쪽을 본다」를 자료에 오독 기록이 있을 때로 좁히고, 평가만 더한 예시를 교체 - check_prose: 안내 문장을 요구하던 경고 제거, 문장이 끝나지 않은 채 문단이 끝나는 조각 검사 추가 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
73026cada6
commit
9d2a3725c5
@@ -0,0 +1,117 @@
|
||||
---
|
||||
name: writing-as-the-person-who-did-it
|
||||
description: Use when a Korean technical document is accurate, well-ordered and well-evidenced but reads like a report produced by nobody — no one chose anything, nothing surprised anyone, and the limits are an inventory instead of an admission.
|
||||
---
|
||||
|
||||
# 일한 사람이 쓴 글로 만들기
|
||||
|
||||
## 무엇을 고치는 스킬인가
|
||||
|
||||
문장도 맞고 순서도 맞고 근거도 붙어 있는데 **아무도 쓰지 않은 글처럼 읽히는** 상태를 고친다.
|
||||
증상은 셋이다.
|
||||
|
||||
- 무엇을 골랐다는 말은 있는데 **왜 그것을 골랐는지, 무엇과 견주었는지**가 없다
|
||||
- 확인하지 못한 것이 **목록**으로 있고, 그것이 걸리는 대목에는 없다
|
||||
- 어긋난 자리가 없다. 처음부터 지금 구조를 알고 있었던 것처럼 읽힌다
|
||||
|
||||
고치는 방법은 하나뿐이다. **자료에 남아 있는 사람의 흔적을 찾아서 제자리에 놓는다.**
|
||||
|
||||
## 절대 규칙 — 없는 사람을 만들지 않는다
|
||||
|
||||
이 스킬을 잘못 쓰면 지어낸 경험이 붙는다. 그것이 아무 목소리도 없는 글보다 나쁘다.
|
||||
|
||||
**쓸 수 있는 것은 자료가 기록한 사람의 행동과 판단뿐이다.** 넣은 문장마다 그것이 어느 파일,
|
||||
어느 칸, 어느 줄에서 왔는지 댈 수 있어야 한다. 못 대면 지어낸 것이다.
|
||||
|
||||
| 이런 말이 떠오르면 | 실제로 벌어지는 일 |
|
||||
|---|---|
|
||||
| 「처음에는 X를 의심했다」 | 자료에 그런 기록이 없으면 없던 수사 과정을 만든 것이다 |
|
||||
| 「놀랍게도」·「당황스럽게도」 | 감정을 지어냈다. 관측이 뜻밖이었다는 근거가 자료에 없다 |
|
||||
| 「고민 끝에 Y를 골랐다」 | 대안을 견주었다는 기록이 없으면 고민도 없었다 |
|
||||
| 「많은 시행착오를 겪었다」 | 시행착오가 자료에 있으면 그것을 적고, 없으면 이 문장도 없다 |
|
||||
| 「이 글이 도움이 되길 바란다」 | 기록에는 독자에게 인사하는 자리가 없다. 참고 글의 습관을 옮겨 온 것이다 |
|
||||
| 「~로 읽기 쉽다. 그렇지 않다」 | 자료에 그런 오독 기록이 없으면 독자를 지어낸 것이다. 관측부터 적는다 |
|
||||
|
||||
`../rewriting-technical-prose-naturally/references/protected-content.md`가 함께 걸린다. 수치·버전·
|
||||
식별자는 이 스킬에서도 한 글자도 바뀌지 않는다.
|
||||
|
||||
## 자료 어디에 사람이 남아 있나
|
||||
|
||||
기록을 열기 전에 이것부터 찾는다. 대개 **이미 문서 안에 있고 자리가 틀렸을 뿐**이다.
|
||||
|
||||
| 어디 | 무엇이 있나 |
|
||||
|---|---|
|
||||
| `확인하지 못한 것` 칸 | 재지 않은 것을 아는 사람이 적어 둔 것이다. 목록이 아니라 그 대목에서 할 말이다 |
|
||||
| `재현 조건` 칸 | 누군가 실제로 이 순서로 돌렸다. 그 순서가 왜 그 순서인지가 대개 빠져 있다 |
|
||||
| javadoc·README·주석 | 코드를 쓴 사람이 남긴 말. 그대로 옮길 수 있다 |
|
||||
| 커밋 메시지 | 왜 지웠는지·왜 바꿨는지가 한 줄로 남아 있다 |
|
||||
| `final/document.md` | 기록으로 뽑으면서 잘라낸 판단이 여기 남아 있는 경우가 많다 |
|
||||
| 결정 기록의 `판단 이유`·`영향` | 감수한 비용이 적혀 있다 |
|
||||
|
||||
찾은 것이 없으면 **이 스킬은 여기서 끝난다.** 없는 목소리를 채우지 않는다.
|
||||
|
||||
## 옮길 수 있는 다섯 가지
|
||||
|
||||
참고 여섯 편에서 뽑았고, 각 항목이 어느 글에서 왔는지는
|
||||
[references/voice-moves.md](references/voice-moves.md)에 있다. 여섯 편이 다 쓰는 것이 아니다 —
|
||||
13569 한 편에 몰려 있고 23625는 거의 쓰지 않는다. **글마다 자료가 허락하는 만큼만 쓴다.**
|
||||
|
||||
1. **고른 이유를 견준 대상과 함께 적는다** — 무엇을 놓고 무엇을 골랐고 무엇을 감수했나
|
||||
2. **확인하지 못한 것을 그 대목에서 말한다** — 끝의 목록이 아니라 그 주장 옆에서
|
||||
3. **어긋난 자리를 남긴다** — 예상과 결과가 달랐던 지점이 자료에 있으면 지우지 않는다
|
||||
4. **남의 말을 그대로 옮긴다** — javadoc·문서·커밋 메시지·사용자 문의. 요약하지 않는다
|
||||
5. **자료에 남은 오독을 그 자리에 둔다** — 누군가 실제로 틀리게 읽었다는 기록(버그·정정·문의)이 있을 때만.
|
||||
독자가 틀릴 것이라고 가정해서 만든 「~로 읽기 쉽다」는 이 항목이 아니라 지어낸 독자다
|
||||
|
||||
다섯을 다 쓰지 않는다. 한 기록에 둘이면 충분하고 **하나도 없어도 된다.** 이미 충분히 설명된 문단에
|
||||
사람의 목소리를 더하려고 문장을 붙이면 그것이 「설명 뒤의 평가」가 된다. 다섯을 채우면 그것이 또 다른 틀이 된다.
|
||||
|
||||
## 문체는 옮기지 않는다
|
||||
|
||||
참고 글은 합니다체이고 `~는데요`·`~텐데요`·`~거든요`로 말을 부드럽게 한다. 이 저장소의 기록은
|
||||
한다체다. **부드러움을 흉내 내면 그 회사 목소리가 되고, 기록의 어조가 무너진다.**
|
||||
|
||||
한다체에서 사람이 드러나는 자리는 어미가 아니라 **무엇을 말하기로 했는가**다.
|
||||
|
||||
```text
|
||||
없는 목소리 컨트롤러가 직접 내부 토큰을 확인한다.
|
||||
지어낸 목소리 컨트롤러가 직접 내부 토큰을 확인하는데요, 이게 좀 아쉬운 부분입니다.
|
||||
평가만 더한 것 컨트롤러가 직접 내부 토큰을 확인한다. 지금 구조에서 이것을 막는 것은 없다.
|
||||
있는 목소리 컨트롤러가 직접 내부 토큰을 확인한다. /edge/** 전체를 보호하는 filter 는 없어서,
|
||||
새 엔드포인트가 같은 메서드를 부르지 않으면 그 엔드포인트는 검사를 지나친다.
|
||||
```
|
||||
|
||||
네 번째가 사람이 쓴 글이다. 어미는 그대로이고, 아는 사람만 할 수 있는 말이 하나 붙었다 — 그리고 그
|
||||
말이 **사실을 더한다**(filter 가 없다, 어느 경로가 검사를 지나치는가). 세 번째처럼 판정만 더하면
|
||||
(「막는 것은 없다」) 그것은 목소리가 아니라 평가다. 그런 문장은 빼는 쪽이 맞다.
|
||||
|
||||
## 절차
|
||||
|
||||
1. **자료를 먼저 훑는다.** 위 표의 자리를 다 열고, 쓸 수 있는 흔적을 목록으로 적는다
|
||||
2. **흔적이 없으면 멈춘다.** 문서에 그렇게 보고한다
|
||||
3. **흔적마다 놓일 자리를 정한다.** 대개 그 주장이 나오는 문단 바로 뒤다
|
||||
4. **옮긴다.** 문장을 새로 짓지 말고, 자료의 말을 그 자리에 놓는 것으로 시작한다
|
||||
5. **검사한다** — `scripts/check_voice.mjs`. 이 검사기는 목소리가 **모자란지** 재지 않는다.
|
||||
**지어낸 목소리**를 잡는다
|
||||
6. `../rewriting-technical-prose-naturally`의 `check_prose.mjs`를 다시 돌린다
|
||||
|
||||
## 검사
|
||||
|
||||
```bash
|
||||
node .agents/skills/writing-as-the-person-who-did-it/scripts/check_voice.mjs <파일.md>
|
||||
```
|
||||
|
||||
세 가지를 본다.
|
||||
|
||||
- **지어낸 경험** — 자료가 뒷받침하지 않으면 error인 표현들
|
||||
- **참고 글에서 옮겨 온 인사말** — 「도움이 되길 바랍니다」 계열
|
||||
- **합니다체 부드러움을 한다체에 섞은 자리**
|
||||
|
||||
검사기가 조용하다고 목소리가 생긴 것은 아니다. **모자란 것은 사람이 읽어야 안다.**
|
||||
|
||||
## 참조
|
||||
|
||||
- [references/voice-moves.md](references/voice-moves.md) — 다섯 가지가 여섯 편에서 어떻게 쓰였나
|
||||
- `../rewriting-technical-prose-naturally/examples/` — 원문 여섯 편
|
||||
- `../rewriting-technical-prose-naturally/references/article-shape.md` — 밀도. 이 스킬보다 먼저 본다
|
||||
- `../writing-tech-log-records/references/writing-each-kind.md` — 종류마다 어느 칸이 사람의 자리인가
|
||||
@@ -0,0 +1,160 @@
|
||||
# 다섯 가지가 여섯 편에서 어떻게 쓰였나
|
||||
|
||||
원문은 `../../rewriting-technical-prose-naturally/examples/`에 있다. 인용은 그 파일에서 그대로
|
||||
옮긴 것이다.
|
||||
|
||||
## 먼저 — 여섯 편이 고르게 쓰지 않는다
|
||||
|
||||
세어 보면 이렇다.
|
||||
|
||||
| | 1인칭 | 그때 든 생각 | 예상이 빗나감 | 독자에게 말 걸기 | 모순·자조 | 겸양 닫기 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 13569 | 12 | 3 | 5 | 3 | 5 | 1 |
|
||||
| 17386 | 10 | 5 | 0 | 2 | 1 | 0 |
|
||||
| 7835 | 8 | 0 | 0 | 3 | 0 | 0 |
|
||||
| 20161 | 5 | 0 | 0 | 2 | 0 | 0 |
|
||||
| 22396 | 4 | 0 | 0 | 2 | 0 | 4 |
|
||||
| 23625 | 1 | 0 | 0 | 0 | 0 | 0 |
|
||||
|
||||
**13569 한 편에 몰려 있다.** 23625는 거의 쓰지 않는데도 읽힌다. 그러니까 이것은 채워야 할 칸이
|
||||
아니라, **자료가 그런 글일 때만 나오는 것**이다.
|
||||
|
||||
13569이 그렇게 쓸 수 있었던 이유는 그 글이 「예상보다 빨라져서 문제가 생겼다」는 사건을 실제로
|
||||
겪었기 때문이다. 겪지 않은 글이 같은 어조를 쓰면 그때부터 거짓이 된다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 고른 이유를 견준 대상과 함께 적는다
|
||||
|
||||
무엇을 골랐다가 아니라, **무엇을 놓고 무엇을 골랐고 무엇을 감수했나**를 적는다.
|
||||
|
||||
22396은 상용 내비게이션을 쓰지 않은 이유를 돈으로 적는다.
|
||||
|
||||
> 하지만 초당 2만 건에 달하는 경로 계산을 위해 API 호출마다 비용이 발생하는 상용 내비게이션을
|
||||
> 사용하는 것은 현실적으로 어렵습니다. 게다가 배차를 추천받은 라이더분이 바로 수락하는 경우,
|
||||
> 한 번 계산한 거리를 오랜 시간 재활용하기도 쉽지 않습니다.
|
||||
|
||||
그리고 고르지 **않은** 쪽을 표로 남긴다 — TTL 삭제 대 명시적 삭제. 장점 칸이 채워진 쪽을 버리고
|
||||
단점이 하나뿐인 쪽을 골랐다는 것이 표에 그대로 보인다.
|
||||
|
||||
7835는 저장소를 고르는 대목에서 반대 의견을 먼저 적는다.
|
||||
|
||||
> 이벤트는 작은 단위로 저장이 되고, 고속 처리되어야하기 때문에 RDBMS 가 아닌 다른 데이터베이스를
|
||||
> 선택해야한다고 생각할 수 있습니다.
|
||||
|
||||
그리고 왜 그럼에도 RDBMS인지로 이어 간다.
|
||||
|
||||
**한다체로 옮길 때** — 어미만 바꾸면 된다. 「~하는 것은 현실적으로 어렵다」·「~라고 생각할 수 있다」.
|
||||
|
||||
**쓸 수 없는 경우** — 자료에 대안이 없으면 이 항목은 건너뛴다. 「여러 방법을 검토했다」는 검토한
|
||||
기록이 없으면 지어낸 문장이다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 확인하지 못한 것을 그 대목에서 말한다
|
||||
|
||||
이 저장소의 기록은 `확인하지 못한 것`을 끝에 목록으로 모아 둔다. 참고 글은 **그 말이 걸리는 자리**
|
||||
에서 한다.
|
||||
|
||||
22396은 압축을 시도한 절 안에서 바로 닫는다.
|
||||
|
||||
> 압축률이 높더라도 원본 데이터가 워낙 커서 압축만으로는 대역폭 문제를 해결할 수 없었습니다.
|
||||
|
||||
13569은 배포 전 검토 항목을 늘어놓은 뒤 그 자리에서 한계를 인정한다.
|
||||
|
||||
> 문제가 없을 것으로 예상하였지만 일들이 항상 마음처럼 굴러가던가요?
|
||||
|
||||
**한다체로 옮길 때** — 끝의 목록을 지우라는 말이 아니다. 목록은 두고, **그중 하나를 그 주장이
|
||||
나오는 문단 뒤에 한 번 더** 놓는다. 읽는 사람이 그 주장을 믿을지 정하는 자리가 거기이기 때문이다.
|
||||
|
||||
```text
|
||||
전 (본문 끝) ## 확인하지 못한 것
|
||||
role 전달은 확인하지 않았다.
|
||||
|
||||
후 (그 대목) 지금 edge 응답은 사용자와 이메일만 전달한다. role을 넘기면 무엇이 달라지는지는
|
||||
확인하지 않았다.
|
||||
(본문 끝) ## 확인하지 못한 것 ← 목록은 그대로 둔다
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 어긋난 자리를 남긴다
|
||||
|
||||
**자료에 있을 때만 쓴다.** 없으면 이 항목은 통째로 건너뛴다.
|
||||
|
||||
13569의 「빨라도 문제」 절 전체가 이것이다.
|
||||
|
||||
> 근데 이거 빨라도 너무 빨라진 것 같습니다.
|
||||
> '내가 뭘 놓친 게 있나?' 아니면 '코드를 잘못 짰나?' 생각이 들었지만…
|
||||
|
||||
그리고 배포 뒤 실제로 문제가 났고, 그 대응이 스스로 모순임을 적는다.
|
||||
|
||||
> 근본적으로는 실행 속도가 너무 빨라진 것이 문제이기 때문에
|
||||
> 모순적이지만 우선 단기적인 대응 방안으로 의도적으로 지연 시간을 설정해 천천히 실행하도록
|
||||
> 수정하였습니다.
|
||||
|
||||
22396은 실패한 시도를 지우지 않는다 — 「시도 1. 데이터 압축」이 실패한 채로 남아 있고, 24MB → 3MB
|
||||
라는 성공한 압축률까지 적은 다음 그것으로도 모자란다고 닫는다.
|
||||
|
||||
**한다체로 옮길 때** — 감탄사와 혼잣말은 옮기지 않는다. 옮기는 것은 **순서**다. 그때 그렇게
|
||||
생각했다 → 실제로는 이랬다 → 그래서 이렇게 했다.
|
||||
|
||||
```text
|
||||
지어낸 것 놀랍게도 응답은 200이었다.
|
||||
있는 것 세션 자체는 유효하므로 이 요청이 200으로 처리되는 것은 정상이고, 그래서 이 테스트의
|
||||
판정 기준은 상태 코드가 아니다.
|
||||
```
|
||||
|
||||
두 번째는 읽는 사람이 200을 보고 「뚫렸다」고 짐작할 것을 알고 쓴 문장이다. 감정을 적지 않고도
|
||||
사람이 보인다.
|
||||
|
||||
---
|
||||
|
||||
## 4. 남의 말을 그대로 옮긴다
|
||||
|
||||
여섯 편이 가장 자주 쓰는 방법이고, **증거로 뒷받침되므로 지어낼 수 없다.**
|
||||
|
||||
13569은 최적화를 말하다 말고 격언 셋을 통째로 옮긴다.
|
||||
|
||||
> "우리는 최적화에 대해서 다음 두가지 규칙을 따른다. 첫째. 하지 마라. 둘째. (전문가 한정)
|
||||
> 아직은 하지 마라."
|
||||
> M. A. Jackson (1975)
|
||||
|
||||
23625는 사용자 문의를 그대로 옮긴다.
|
||||
|
||||
> 심각한 문제는 사용자가 "같은 엑셀 파일을 여러 번 받았다"는 문의였습니다.
|
||||
|
||||
20161은 Nginx 문서와 Lucene 문서를 원문 그대로 인용문으로 넣는다.
|
||||
|
||||
**이 저장소에서 쓸 수 있는 것** — javadoc이 가장 많다. 코드를 쓴 사람이 자기 코드의 위험을 적어
|
||||
둔 자리가 있으면 그것을 요약하지 말고 그대로 옮긴다.
|
||||
|
||||
> `InboxCleanupJob`의 javadoc이 스스로 적는다 — "A single unbounded DELETE over a table that has
|
||||
> been accumulating for weeks holds locks long enough to block the very reservations the inbox
|
||||
> exists to serve, so the cleanup would cause the outage it is meant to prevent."
|
||||
|
||||
이 한 줄이 있으면 「이 결함은 알려져 있었다」는 말을 따로 할 필요가 없다.
|
||||
|
||||
---
|
||||
|
||||
## 5. 한 번은 독자 쪽을 본다
|
||||
|
||||
읽는 사람이 **틀리게 짐작할 자리**에서 한 문장이다. 인사가 아니다.
|
||||
|
||||
20161은 겉보기에 멀쩡한 쿼리를 보여 주고 나서 짚는다.
|
||||
|
||||
> 겉으로 보기에는 문제가 없어 보이는 쿼리였지만, 카테고리 필터가 있을 때와 없을 때 검색 API의
|
||||
> 응답 속도 차이는 매우 큰 편이었습니다.
|
||||
|
||||
22396은 강조를 두 번 한다고 말하고 두 번 한다.
|
||||
|
||||
> 앞서 언급했지만 중요한 내용이기에 다시 한번 강조드립니다.
|
||||
> **배달료 책정이나 배민커넥트 앱상에서 활용되는 거리 계산에는 상용 내비게이션이 사용됩니다.**
|
||||
|
||||
17386은 글 첫머리에 「누가 읽으면 좋을까」를 두고 전제 지식을 못 박는다.
|
||||
|
||||
**한다체로 옮길 때** — 「~라고 생각하기 쉽다」·「겉보기에는 ~이지만」·「여기서 확인할 값은
|
||||
~가 아니다」. 한 기록에 **한 번**이다. 두 번 넘으면 독자를 끌고 다니는 글이 된다.
|
||||
|
||||
**옮기지 않는 것** — 「도움이 되길 바랍니다」·「공유드립니다」·「즐거움을 느끼고 있습니다」.
|
||||
블로그 글의 맺음말 자리에 있는 인사이고, 기록에는 그 자리가 없다.
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env node
|
||||
// 지어낸 목소리를 잡는다. 모자란 목소리는 재지 않는다 — 그것은 사람이 읽어야 안다.
|
||||
//
|
||||
// node check_voice.mjs [--블로그] <파일.md> [...]
|
||||
//
|
||||
// 왜 세지 않는가: 이 저장소에서 「종결어미 종류 수」를 세는 검사를 넣었더니, 그것을 맞추려고
|
||||
// 없던 물음표 문장과 「~해 보자」가 문서에 끼어들었다. 목소리를 개수로 재면 같은 일이 난다.
|
||||
// 그래서 이 검사기는 있어야 할 것을 요구하지 않고, 있으면 안 되는 것만 잡는다.
|
||||
//
|
||||
// 참고 여섯 편을 그냥 돌리면 떨어진다. 그것은 규칙이 과해서가 아니라 장르가 달라서다 —
|
||||
// 여섯 편은 합니다체 블로그 글이라 `~는데요`를 쓰고 맺음말에 인사를 둔다. 기록은 한다체이고
|
||||
// 인사할 자리가 없다. 여섯 편에 돌려 볼 때는 `--블로그`를 붙인다. 그러면 이 두 규칙만 꺼지고
|
||||
// 지어낸 목소리를 잡는 규칙은 그대로 돈다 — 여섯 편은 그쪽에 한 건도 걸리지 않는다.
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { basename } from "node:path";
|
||||
|
||||
const ERR = "error", WARN = "warn";
|
||||
|
||||
const RULES = [
|
||||
// 1. 수사 과정을 말하는 문구.
|
||||
//
|
||||
// 이것만으로는 지어냈는지 알 수 없다. 22396 의 「여러 시행착오를 겪었습니다」는 바로 뒤에
|
||||
// 실패한 시도 1 을 통째로 싣고, 7835 의 「고민 끝에」는 글 전체가 그 고민이다. 둘 다 정당하다.
|
||||
// 나쁜 것은 문장만 있고 과정이 없는 경우인데, 그건 문서를 읽어야 안다. 그래서 판단 항목이다.
|
||||
{ id: "process-claimed", sev: WARN,
|
||||
re: /(처음에는[^.\n]{0,30}(의심|생각|짐작)|한참[^.\n]{0,15}(헤매|찾|고민)|여러[^.\n]{0,10}(시행착오|삽질)|고민\s*끝에|우여곡절|검토\s*끝에)/g,
|
||||
msg: "겪은 과정을 말했습니다. 이 문서가 그 과정을 실제로 보여 주면 두고, 문장만 있으면 지우세요." },
|
||||
|
||||
{ id: "invented-emotion", sev: ERR,
|
||||
re: /(놀랍게도|당황스럽|의외로|뜻밖에도|아쉽게도|다행히도|기쁘게도|흥미롭게도|충격적)/g,
|
||||
msg: "감정을 지어냈습니다. 관측이 뜻밖이었다는 근거가 자료에 있어야 쓸 수 있습니다." },
|
||||
|
||||
// 2. 참고 글의 맺음말을 옮겨 온 자리. 기록에는 독자에게 인사하는 칸이 없다.
|
||||
{ id: "borrowed-greeting", sev: ERR,
|
||||
re: /(도움이\s*되(길|기를)|되었으면\s*좋겠|공유(드립니다|하고자|합니다)|읽어\s*주셔서|감사합니다|즐거움을\s*느끼|노력하겠습니다|기대합니다)/g,
|
||||
msg: "블로그 맺음말의 인사입니다. 기록에는 그 자리가 없습니다. 남은 일이나 감수한 것으로 닫으세요." },
|
||||
|
||||
// 3. 합니다체의 부드러움을 한다체 문서에 섞은 자리
|
||||
{ id: "register-mix", sev: ERR,
|
||||
re: /(는데요|거든요|텐데요|인데요|한데요|잖아요|네요)/g,
|
||||
msg: "합니다체의 부드러운 어미를 섞었습니다. 참고 글의 어조이지 이 기록의 어조가 아닙니다." },
|
||||
|
||||
// 4. 겪지 않은 1인칭. 기록의 주어는 대개 코드와 요청이다.
|
||||
// 앞 글자가 한글이면 낱말 안이다 — 「브라우저는」의 「저는」을 잡지 않는다
|
||||
{ id: "unsupported-first-person", sev: WARN,
|
||||
re: /(?<![가-힣])(저는|저희(는|가|의|도)|제가|우리는)\s/g,
|
||||
msg: "1인칭입니다. 자료가 그 사람의 행동을 기록했으면 두고, 아니면 무엇이 그렇게 했는지로 바꾸세요." },
|
||||
|
||||
// 5. 독자를 끌고 다니는 문장이 여러 번 나오는 것은 참고 글도 하지 않는다 (개수는 아래에서 본다)
|
||||
{ id: "steering", sev: WARN,
|
||||
re: /(라고\s*생각하기\s*쉽|겉보기에는|여기서\s*확인할\s*값은|짐작하기\s*쉽|헷갈리기\s*쉽)/g,
|
||||
msg: "독자 쪽을 보는 문장입니다. 한 기록에 한 번이면 충분합니다." },
|
||||
];
|
||||
|
||||
function strip(src) {
|
||||
return src
|
||||
.replace(/```[\s\S]*?```/g, (m) => m.replace(/[^\n]/g, " "))
|
||||
.replace(/`[^`\n]*`/g, (m) => " ".repeat(m.length))
|
||||
.replace(/^---\n[\s\S]*?\n---\n/, (m) => m.replace(/[^\n]/g, " "));
|
||||
}
|
||||
|
||||
function lineOf(text, index) {
|
||||
return text.slice(0, index).split("\n").length;
|
||||
}
|
||||
|
||||
let failed = 0;
|
||||
const argv = process.argv.slice(2);
|
||||
// 합니다체 블로그 글에 돌릴 때는 어조·인사 규칙을 끈다. 장르가 다르지 글이 나빠서가 아니다.
|
||||
const blogMode = argv.includes("--블로그");
|
||||
const GENRE = new Set(["register-mix", "borrowed-greeting"]);
|
||||
const files = argv.filter((a) => !a.startsWith("--"));
|
||||
if (!files.length) {
|
||||
console.error("쓰는 법: node check_voice.mjs [--블로그] <파일.md>");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const raw = readFileSync(file, "utf8");
|
||||
const text = strip(raw);
|
||||
const found = [];
|
||||
for (const r of RULES) {
|
||||
if (blogMode && GENRE.has(r.id)) continue;
|
||||
for (const m of text.matchAll(r.re)) {
|
||||
found.push({ ...r, line: lineOf(text, m.index), hit: m[0].trim() });
|
||||
}
|
||||
}
|
||||
// 독자 쪽을 보는 문장은 하나까지가 정상이다
|
||||
const steering = found.filter((f) => f.id === "steering");
|
||||
const rest = found.filter((f) => f.id !== "steering");
|
||||
const shown = steering.length > 1 ? rest.concat(steering) : rest;
|
||||
|
||||
const errors = shown.filter((f) => f.sev === ERR).length;
|
||||
const warns = shown.length - errors;
|
||||
if (errors) failed = 1;
|
||||
|
||||
console.log(
|
||||
errors
|
||||
? `FAIL ${basename(file)} — error ${errors}건${warns ? ` · 경고 ${warns}건` : ""}`
|
||||
: `OK ${basename(file)}${warns ? ` (경고 ${warns}건)` : ""}`
|
||||
);
|
||||
for (const f of shown) {
|
||||
const mark = f.sev === ERR ? " " : "·";
|
||||
console.log(` ${mark} ${basename(file)}:${f.line} [${f.id}] "${f.hit}"`);
|
||||
console.log(` ${f.msg}`);
|
||||
}
|
||||
if (steering.length > 1) {
|
||||
console.log(` · [steering] 독자 쪽을 보는 문장이 ${steering.length}개입니다 — 하나만 남기세요.`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
"\n검사기가 조용해도 목소리가 생긴 것은 아닙니다. 모자란 것은 사람이 읽어야 압니다."
|
||||
);
|
||||
process.exit(failed);
|
||||
Reference in New Issue
Block a user