axmap-cli 1.2.0 → 1.2.2
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.
- package/CLAUDE.md +82 -335
- package/bin/axmap.mjs +94 -0
- package/governance/GOVERNANCE.md +18 -0
- package/governance/vote.mjs +85 -12
- package/mcp/server.mjs +23 -3
- package/package.json +1 -1
- package/src/overlap.mjs +192 -0
package/CLAUDE.md
CHANGED
|
@@ -1,335 +1,82 @@
|
|
|
1
|
-
# axMap 작업 규칙 —
|
|
2
|
-
|
|
3
|
-
이 문서는 **axMap 자체를 고칠 때** 지키는
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
`
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
>
|
|
84
|
-
> 같은 PC 에서 AI 도구 세션 두 개가 같은 저장소를 만졌다. 둘 다 이름을 안 주어
|
|
85
|
-
> `git config user.name` 으로 떨어졌고, 장부에서 **한 사람**이 되었다.
|
|
86
|
-
> 뒤에 온 claim 이 그냥 통과했고 — 레코드는 이름당 파일 하나다 — 앞 세션의
|
|
87
|
-
> 작업·의도가 덮여 `status` 에 1건만 남았다. 두 세션이 같은 파일을 동시에 밀었다.
|
|
88
|
-
>
|
|
89
|
-
> 이제 claim 레코드에 **세션 id** 가 함께 적히고(`--session` · `AXMAP_SESSION` ·
|
|
90
|
-
> `CLAUDE_CODE_SESSION_ID` 순), **레코드의 임자가 이름 하나가 아니라 (이름, 세션)
|
|
91
|
-
> 짝**이다. 창을 둘 띄우면 장부에 두 줄이 서고 서로 덮지 않는다.
|
|
92
|
-
> `release` 는 **그 세션이 잡은 것만** 푼다 — 다른 창이 두고 간 것까지 풀려면
|
|
93
|
-
> `release --all-sessions` 를 붙인다.
|
|
94
|
-
>
|
|
95
|
-
> 처음에는 세션이 다르면 아예 **거부**했다(1.0.3). 증상은 멎었지만 같은 사람이
|
|
96
|
-
> 창 두 개로 겹치지도 않는 다른 일을 하는 것까지 막혀서, 1.0.4 에서 자리를 나누는
|
|
97
|
-
> 쪽으로 바꿨다.
|
|
98
|
-
>
|
|
99
|
-
> 그래도 제대로 된 해법은 여전히 위 한 줄이다. **세션마다 `AXMAP_AGENT` 를 다르게
|
|
100
|
-
> 준다.** 겹침 검사는 자기 이름의 claim 을 겹침으로 보지 않으므로, 이름이 같으면
|
|
101
|
-
> 두 창이 **같은 파일을 함께 잡을 수 있다** (그때 claim 이 겹친다고 찍어준다).
|
|
102
|
-
|
|
103
|
-
> 🔴 **링크된 worktree**(`git worktree add` 로 옆에 펼친 폴더)**에서도 그대로 돈다.**
|
|
104
|
-
> 장부는 메인 체크아웃에 **하나뿐**이고 링크된 worktree 는 그것을 본다
|
|
105
|
-
> (`git rev-parse --git-common-dir` 로 찾는다). 그 안에 `.axmap/ledger` 를 또 만들지
|
|
106
|
-
> 않는다 — 같은 저장소에 장부가 둘이면 락이 아니다.
|
|
107
|
-
|
|
108
|
-
**어떤 파일이든 수정하기 전에 claim 이 먼저다.** 읽기만 할 때는 필요 없다.
|
|
109
|
-
|
|
110
|
-
작업 범위가 확실치 않으면 **넓게 잡지 말고 좁게 시작해서 추가로 claim** 한다.
|
|
111
|
-
`claim` 은 누적되므로 나중에 경로를 더할 수 있다. 넓게 잡으면 남의 작업을 헛되이 막는다.
|
|
112
|
-
|
|
113
|
-
## 2. 거부당하면 재시도하지 말고 방향을 바꾼다
|
|
114
|
-
|
|
115
|
-
| 종료 코드 | 의미 | 할 일 |
|
|
116
|
-
|---|---|---|
|
|
117
|
-
| 2 | 경로가 겹침 **또는 같은 이름·다른 세션** | **다른 작업으로 전환.** 재시도해도 결과는 같다 |
|
|
118
|
-
| 3 | claim 없이 커밋 시도 | 해당 경로를 claim 하거나 스테이징에서 뺀다 |
|
|
119
|
-
| 1 | 환경 오류 / 경합 과다 | 메시지를 읽고 고친다 |
|
|
120
|
-
| 5 | `release` 가 **아무것도 반납 못 함** | 잡을 때와 이름이 다르다. `AXMAP_AGENT` 를 확인한다 |
|
|
121
|
-
|
|
122
|
-
**5 를 무시하지 않는다.** 반납했다고 믿는데 락이 남아 있다는 뜻이고,
|
|
123
|
-
그동안 다른 에이전트는 최대 30분을 헛되이 기다린다.
|
|
124
|
-
두 대로 협업하다 실제로 물렸다 — claim 은 `AXMAP_AGENT=X` 로 했는데
|
|
125
|
-
release 를 그 변수가 없는 셸에서 불러 `git config user.name` 으로 떨어졌다.
|
|
126
|
-
메시지가 **이름이 어디서 왔는지** 알려주므로 그것부터 읽는다.
|
|
127
|
-
|
|
128
|
-
종료 코드 2 의 메시지에는 점유자, 작업 ID, 남은 TTL 이 들어 있다.
|
|
129
|
-
그것을 읽고 **비어 있는 다른 영역**을 찾는다. `axmap status --json` 으로 전체를 볼 수 있다.
|
|
130
|
-
|
|
131
|
-
사람의 판단이 필요하면 기다리지 말고 물어본다. TTL 만료를 기다리며 놀지 않는다.
|
|
132
|
-
|
|
133
|
-
## 3. 오래 걸리면 갱신하고, 끝나면 반납한다
|
|
134
|
-
|
|
135
|
-
```bash
|
|
136
|
-
axmap renew # 기본 TTL 30분. 넘어갈 것 같으면 미리
|
|
137
|
-
axmap release # 작업이 끝나면 즉시
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
반납하지 않아도 TTL 이 지나면 자동으로 풀린다. 그래도 **끝나면 바로 반납한다.**
|
|
141
|
-
다른 에이전트가 30분을 헛되이 기다리게 하지 않기 위해서다.
|
|
142
|
-
|
|
143
|
-
## 4. 훅을 설치한다
|
|
144
|
-
|
|
145
|
-
```bash
|
|
146
|
-
axmap hook install
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
claim 하지 않은 파일의 커밋을 막는다. 이것이 없으면 프로토콜은 권고 사항에 불과하다.
|
|
150
|
-
|
|
151
|
-
---
|
|
152
|
-
|
|
153
|
-
## 5. 일하는 방식 — 코드를 쓰기 전에 지키는 것
|
|
154
|
-
|
|
155
|
-
> 🔴 **이 절은 원래 `~/.claude/CLAUDE.md`(그 PC 의 전역 설정)에만 있었다.**
|
|
156
|
-
> 그래서 **다른 컴퓨터에서 이어받으면 조용히 사라졌다** — 없어도 에러가 안 나고,
|
|
157
|
-
> 그냥 에이전트가 다르게 일한다. 저장소에 없는 것이 다른 곳에서 다르게 동작하는
|
|
158
|
-
> 것은 이 프로젝트가 반복해서 겪은 사고 모양이라(`.env` · `data/` · 러너),
|
|
159
|
-
> 여기로 옮긴다. 2026-08-27.
|
|
160
|
-
>
|
|
161
|
-
> 전역 파일에도 같은 내용이 남아 있다. **이 저장소에서는 이쪽이 기준이고**,
|
|
162
|
-
> 둘이 갈라지면 이쪽을 따른다.
|
|
163
|
-
|
|
164
|
-
### 5.1. 코딩 전에 먼저 생각한다
|
|
165
|
-
|
|
166
|
-
- **가정하지 않는다.** 불확실한 부분은 숨기지 말고 표면으로 드러낸다
|
|
167
|
-
- **해석이 여러 가지면** 각각을 명시하고 어느 쪽인지 확인한다
|
|
168
|
-
- **혼란스러우면 묻는다.** 잘못된 가정으로 구현한 뒤 되돌리는 것보다 먼저 묻는 게 싸다
|
|
169
|
-
- **더 나은 방법이 있으면 제안한다.** 요청이 최선이 아닐 때는 트레이드오프를 설명한다
|
|
170
|
-
|
|
171
|
-
### 5.2. 단순함이 먼저다
|
|
172
|
-
|
|
173
|
-
문제를 해결하는 **최소한의 코드**만 쓴다. 금지: 요청하지 않은 기능 · 미래를 위한
|
|
174
|
-
추상화 · 발생 불가능한 에러 핸들링 · 불필요한 유연성.
|
|
175
|
-
|
|
176
|
-
자가 점검은 한 문장이다 — *"시니어 엔지니어가 이걸 보고 과하다고 할까?"*
|
|
177
|
-
|
|
178
|
-
### 5.3. 외과적으로 바꾼다
|
|
179
|
-
|
|
180
|
-
**반드시 바꿔야 할 것만** 건드린다. 내가 만든 mess 만 정리하고, 주변 코드 스타일
|
|
181
|
-
개선이나 고장나지 않은 코드의 리팩토링은 하지 않는다.
|
|
182
|
-
**변경된 모든 줄은 사용자 요청으로 직접 추적 가능해야 한다.**
|
|
183
|
-
|
|
184
|
-
> 이 규칙이 이 저장소에서 특히 중요한 이유가 있다. `checkOverlap` 주변을 "겸사겸사"
|
|
185
|
-
> 정리하다 판정이 한 칸 넓어지면, 그 순간부터 **두 에이전트가 같은 파일을 잡고도
|
|
186
|
-
> 아무 경고가 안 난다.** 락 시스템에서 조용한 통과가 최악인 것과 같은 이유다.
|
|
187
|
-
|
|
188
|
-
### 5.4. 목표를 검증 가능한 것으로 바꾼다
|
|
189
|
-
|
|
190
|
-
추상적인 태스크를 **검증 가능한 체크포인트**로 만든다.
|
|
191
|
-
**성공 기준을 먼저 정의하고, 검증 방법 없이 구현하지 않는다.**
|
|
192
|
-
|
|
193
|
-
이 저장소에서 그 기준은 언제나 **종료 코드**다. 개수도 문장도 아니다.
|
|
194
|
-
|
|
195
|
-
### 5.5. 확인은 긴 글이 아니라 선택지로
|
|
196
|
-
|
|
197
|
-
사용자는 **긴 프롬프트를 건너뛸 수 있다.** 확인·결정이 필요하면 장문 설명 대신
|
|
198
|
-
**선택지(`AskUserQuestion`)** 로 제시한다.
|
|
199
|
-
|
|
200
|
-
**새로 무언가를 만들어 남기기 전에는**(설계 문서 · 티켓 · 산출물) 무엇을 만들지
|
|
201
|
-
선택지로 컨펌받고 진행한다. 설명이 꼭 필요하면 짧게, 결정 포인트는 항상 선택지로.
|
|
202
|
-
|
|
203
|
-
### 5.6. 용어는 쓰기 전에 뜻을 붙인다
|
|
204
|
-
|
|
205
|
-
**처음 쓰는 용어에는 괄호로 한 줄 뜻을 붙인다.**
|
|
206
|
-
`claim(파일 예약 — 건드리기 전에 자리를 잡아두는 것)`
|
|
207
|
-
|
|
208
|
-
사용자가 **"그게 뭐야" 를 묻게 만들면 실패다.** 직접 찾아보는 것이 대화로 듣는 것보다
|
|
209
|
-
훨씬 힘들다. **설명 비용은 말하는 쪽이 낸다.**
|
|
210
|
-
|
|
211
|
-
🔴 **저장소 문서(SPEC · DECISIONS · README)에 있는 용어라도 예외 없다.**
|
|
212
|
-
문서에 적혀 있다는 것이 상대가 안다는 뜻이 아니다.
|
|
213
|
-
뜻 없이 쓴 용어가 한 답변에 셋 이상이면 답변 끝에 용어 표를 붙인다.
|
|
214
|
-
이미 사용자가 쓴 적 있는 단어는 그대로 쓴다 — 아는 말까지 설명하면 그것도 소음이다.
|
|
215
|
-
|
|
216
|
-
> 이 규칙은 [docs/WORDS.md](docs/WORDS.md) 원칙 B(*"전문 용어는 지우지 말고 괄호로
|
|
217
|
-
> 뒤에 붙인다 — 어휘를 뺏지 않고 가르친다"*)와 같은 것이다.
|
|
218
|
-
> **제품 화면에 적용하기로 한 규칙을 대화에도 똑같이 적용한다.**
|
|
219
|
-
> 팀 저장소는 이것을 자기 `CLAUDE.md` 0절로 가져갔다.
|
|
220
|
-
|
|
221
|
-
---
|
|
222
|
-
|
|
223
|
-
### 5.7. 한 번에 두 가지만 설명하고, 따라오는지 확인한다
|
|
224
|
-
|
|
225
|
-
🔴 **한 답변에 설명은 최대 두 가지.** 셋째부터는 꺼내지 않는다.
|
|
226
|
-
끝에 **"다음 것도 설명할까요?"** 를 붙인다. 선택지로 물어도 된다.
|
|
227
|
-
|
|
228
|
-
**왜 이 규칙이 생겼나.** 길게 쓰면서 동시에 압축하려 하면 **둘 다 실패한다.**
|
|
229
|
-
밀도만 올라가고 읽는 사람은 따라갈 수가 없다. 사용자의 말 그대로다 —
|
|
230
|
-
*"요약을 엄청 길게 해주면서 어느 정도 축약을 하려고 하다 보니 이해하기가 더 힘들다."*
|
|
231
|
-
|
|
232
|
-
**작업 보고에도 똑같이 적용한다.** 한 일이 열 개여도 **제일 중요한 둘**만 쓰고
|
|
233
|
-
나머지는 물어본다. 다 적어야 성실한 것이 아니다 — 안 읽히면 안 적은 것과 같다.
|
|
234
|
-
|
|
235
|
-
> 5.6(용어에 뜻을 붙인다)과 짝이다. 저쪽은 **모르는 단어**를 막고
|
|
236
|
-
> 이쪽은 **한꺼번에 너무 많은 것**을 막는다. 둘 다 *"읽는 사람이 검색하러
|
|
237
|
-
> 나가거나 포기하는 순간 그 글은 실패"* 라는 같은 판정 기준에서 나왔다.
|
|
238
|
-
|
|
239
|
-
---
|
|
240
|
-
|
|
241
|
-
## 코드 규칙
|
|
242
|
-
|
|
243
|
-
### 의존성을 추가하지 않는다
|
|
244
|
-
|
|
245
|
-
Node 20+ 와 git 만으로 돌아간다. 테스트도 `node:test` 내장 러너를 쓴다.
|
|
246
|
-
새 의존성이 필요하다고 판단되면 **추가하기 전에 사람에게 묻는다.**
|
|
247
|
-
|
|
248
|
-
`package.json` 은 claim 없이 건드리지 않는다. 모든 에이전트의 공유 경로다.
|
|
249
|
-
|
|
250
|
-
### 순수 로직과 부수효과를 섞지 않는다
|
|
251
|
-
|
|
252
|
-
| 파일 | 넣는 것 | 넣지 않는 것 |
|
|
253
|
-
|---|---|---|
|
|
254
|
-
| `axmap/src/protocol.mjs` | 판정, 계산, 메시지 생성 | git 호출, 파일 I/O, `Date.now()` |
|
|
255
|
-
| `axmap/bin/axmap.mjs` | git, fs, 시계, 인자 파싱 | 판정 로직 |
|
|
256
|
-
|
|
257
|
-
시각은 항상 인자로 받는다. `checkOverlap({..., now})` 처럼.
|
|
258
|
-
그래야 TTL 동작을 30분 기다리지 않고 검증할 수 있다.
|
|
259
|
-
|
|
260
|
-
### 애매하면 거부한다 (fail-closed)
|
|
261
|
-
|
|
262
|
-
락 시스템에서 최악의 실패는 거부해야 할 것을 조용히 통과시키는 것이다.
|
|
263
|
-
잘못 막으면 에이전트가 메시지를 읽고 방향을 틀면 그만이지만,
|
|
264
|
-
못 막으면 두 에이전트가 같은 코드를 고치고 아무도 에러를 보지 못한다.
|
|
265
|
-
|
|
266
|
-
- 장부 레코드를 파싱할 수 없으면 **건너뛰지 말고 전체를 중단**한다.
|
|
267
|
-
- 이상한 입력을 **안전한 값으로 치환하지 말고 거부**한다.
|
|
268
|
-
치환은 서로 다른 두 입력을 같은 것으로 만들고, 그것이 곧 소유권 충돌이다.
|
|
269
|
-
- 만료된 레코드를 **효력이 있는 것처럼 다루지 않는다.**
|
|
270
|
-
만료는 레코드가 사라지는 것이 아니라 효력만 잃는 것이다.
|
|
271
|
-
|
|
272
|
-
`checkOverlap` 주변을 고칠 때는 "이 변경으로 통과가 늘어나는가"를 먼저 확인한다.
|
|
273
|
-
늘어난다면 그것이 의도한 것인지 근거를 남긴다.
|
|
274
|
-
|
|
275
|
-
### 프로토콜 로직을 바꾸면 테스트를 함께 바꾼다
|
|
276
|
-
|
|
277
|
-
`axmap/test/protocol.test.mjs` 의 다음 테스트는 **설계 근거 자체**다. 이유 없이 지우지 않는다.
|
|
278
|
-
|
|
279
|
-
> `Case 1 과 Case 2 는 반드시 같은 답이어야 한다`
|
|
280
|
-
|
|
281
|
-
무관한 제3자의 데이터가 판정을 바꾸지 못하도록 고정하는 테스트다.
|
|
282
|
-
git 의 텍스트 충돌 대신 파서를 고른 이유 전체가 여기 들어 있다.
|
|
283
|
-
배경은 [docs/EXPERIMENT.md](docs/EXPERIMENT.md).
|
|
284
|
-
|
|
285
|
-
### 명령을 추가하거나 claim 의미론을 바꾸면 SPEC 을 먼저 고친다
|
|
286
|
-
|
|
287
|
-
[docs/SPEC.md](docs/SPEC.md) 가 규격이고 코드가 구현이다. 순서를 지킨다.
|
|
288
|
-
여섯 명이 병렬로 붙는 상황에서 규격이 코드를 따라가면 아무도 진실을 모르게 된다.
|
|
289
|
-
|
|
290
|
-
### 주석은 "왜"를 쓴다
|
|
291
|
-
|
|
292
|
-
"무엇"은 코드가 이미 말한다. 주석에는 그 선택을 한 이유를 남긴다.
|
|
293
|
-
특히 git 의 비직관적인 동작(worktree 별 `FETCH_HEAD`, 훅의 `GIT_*` 환경변수 등)은
|
|
294
|
-
반드시 이유를 적는다. 다음 사람이 반드시 다시 밟는다.
|
|
295
|
-
|
|
296
|
-
---
|
|
297
|
-
|
|
298
|
-
## 검증
|
|
299
|
-
|
|
300
|
-
작업을 끝내기 전에 전부 통과해야 한다.
|
|
301
|
-
|
|
302
|
-
```bash
|
|
303
|
-
npm --prefix axmap test # 회귀 테스트 + 모델 검증기 - 전부 통과 (실패 0)
|
|
304
|
-
npm --prefix axmap run demo # 에이전트 9명 동시 작업 - 8개 섹션 전부 정상
|
|
305
|
-
npm --prefix axmap run demo:compare # git 판정 vs 파서 판정 - 파서가 3/3
|
|
306
|
-
npm --prefix axmap run demo:chaos # 카오스 테스트 - I1·I3·I4·I5 전부 통과, exit 0
|
|
307
|
-
npm --prefix axmap run smoke # 화면이 실제로 뜨는지 - 헤드리스 크롬, exit 0
|
|
308
|
-
npm --prefix axmap run desktop:smoke # 데스크톱 셸 - 창이 뜨고 정지 후 초당 그리기 25회 이하, exit 0
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
> 개수를 여기에 적지 않는다. 적어 두면 테스트를 늘릴 때마다 이 줄이 낡고,
|
|
312
|
-
> 낡은 기준은 **없는 기준보다 나쁘다** — 다음 사람이 그것을 믿고 통과했다고 여긴다.
|
|
313
|
-
> 판정은 언제나 종료 코드다.
|
|
314
|
-
|
|
315
|
-
**`axmap/app/web/` 를 건드렸으면 `npm --prefix axmap run smoke` 를 반드시 돌린다.**
|
|
316
|
-
`npm --prefix axmap test` 는 브라우저 코드를 실행하지 않으므로 **화면이 죽어도 초록이다.**
|
|
317
|
-
하룻밤에 두 번 겪었다 — 주석 안의 `*/` 가 파일을 닫았고, `let` 선언이 첫
|
|
318
|
-
사용처보다 아래에 있어 TDZ 참조 에러로 부트스트랩이 통째로 죽었다.
|
|
319
|
-
둘 다 문법 검사와 단위 테스트를 전부 통과했다.
|
|
320
|
-
|
|
321
|
-
데모는 실제 git 저장소를 만들어 돌린다. 통과 여부를 눈으로 확인한다.
|
|
322
|
-
|
|
323
|
-
**데모가 초록이라고 안심하지 않는다.** 데모는 사람이 고른 순서로 각 기능이 한 번씩
|
|
324
|
-
동작한다는 것만 보여준다. 실제로 발견된 버그는 전부 데모가 초록인 상태에서 살아 있었다.
|
|
325
|
-
프로토콜 로직을 바꿨으면 `npm --prefix axmap test`(무작위 순서)와 `npm --prefix axmap run demo:chaos`(진짜 경합)를
|
|
326
|
-
반드시 함께 돌린다.
|
|
327
|
-
|
|
328
|
-
### 불변식을 건드리면
|
|
329
|
-
|
|
330
|
-
[docs/INVARIANTS.md](docs/INVARIANTS.md) 의 I1~I7 이 이 프로젝트가 지키기로 한 성질이다.
|
|
331
|
-
`axmap/src/invariants.mjs` 는 모델 검증기·카오스 테스트·`axmap audit` 이 **공유하는** 검사기다.
|
|
332
|
-
여기를 고치면 세 도구의 판정이 한꺼번에 바뀐다.
|
|
333
|
-
|
|
334
|
-
불변식을 추가할 때는 **그 검사가 실제로 실패하는 케이스**를 함께 넣는다.
|
|
335
|
-
한 번도 실패하지 않는 검사기는 검사기가 아니다.
|
|
1
|
+
# axMap 작업 규칙 — 색인 한 장
|
|
2
|
+
|
|
3
|
+
이 문서는 **axMap 자체를 고칠 때** 지키는 규칙의 **입구**다. 사람도 AI 에이전트도 읽는다.
|
|
4
|
+
규칙 본문은 **[`rule/`](rule/) 폴더**에 갈래별로 나뉘어 있다. **필요할 때 그 파일을 연다.**
|
|
5
|
+
|
|
6
|
+
> 🔴 **2026-09-11 에 이렇게 바뀌었다.** 전에는 이 파일 하나에 전부(764줄) 들어 있었고,
|
|
7
|
+
> Claude Code 는 **세션을 켤 때마다 그걸 통째로 읽는다.** 한 번도 안 쓰는 규칙까지
|
|
8
|
+
> 매번 값을 치렀다. 지금은 이 색인만 항상 읽히고, 본문은 **찾아갈 때만** 읽는다.
|
|
9
|
+
> 내용은 지우지 않았다 — 자리만 옮겼다.
|
|
10
|
+
|
|
11
|
+
**이 문서가 기준이 되게 하려면 `claude` 를 이 폴더에서 켠다.** Claude Code 는 **켠 폴더**를
|
|
12
|
+
기준으로 `CLAUDE.md` 를 읽는다. 팀 저장소 폴더에서 켜고 axMap 파일만 고치면, 읽히는 것은
|
|
13
|
+
팀 규칙이지 이 문서가 아니다.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cd C:/Users/SSAFY/Desktop/axMap
|
|
17
|
+
claude
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
axMap 은 **AI Experience(ax) 의 지도**다 — 에이전트가 코드베이스에서 길을 찾고 서로를
|
|
21
|
+
피해 가는 경험을 만든다. 팀에서는 여행 서비스를 만드는 **도구**로 쓰인다.
|
|
22
|
+
|
|
23
|
+
> **산문에서는 `axMap`, 식별자·명령·경로·패키지 이름에서는 `axmap`.**
|
|
24
|
+
> 다만 **MCP 도구 이름만 `ax_` 접두사**를 쓴다 (`ax_claim`, `ax_status` …).
|
|
25
|
+
> 환경변수 접두사는 `AXMAP_`, 장부 브랜치는 `axmap/claims` 다.
|
|
26
|
+
|
|
27
|
+
> 🔴 **팀 규칙은 여기 없다.** axMap 은 독립 저장소이고 팀은 npm 꾸러미 `axmap-cli` 로
|
|
28
|
+
> 받아 쓴다. 팀 규칙은 그 저장소의 `CONTRIBUTING.md` 에 있다 —
|
|
29
|
+
> `https://lab.ssafy.com/s15-bigdata-dist-sub1/S15P21E201`.
|
|
30
|
+
> 상대경로(`../CLAUDE.md`)로 걸지 않는다. 폴더가 옮겨지면 조용히 거짓말이 된다.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 시작하기 전에 세 줄
|
|
35
|
+
|
|
36
|
+
| | |
|
|
37
|
+
|---|---|
|
|
38
|
+
| **개발선** | `team-sync` 다. `main` 이 아니다 → [rule/01](rule/01-이-저장소의-함정.md) |
|
|
39
|
+
| **고치기 전에** | `node bin/axmap.mjs claim <경로> --task … --intent …` → [rule/02](rule/02-선점-작업-반납.md) |
|
|
40
|
+
| **고친 코드를 돌리려면** | `node bin/axmap.mjs …`. 그냥 `axmap` 은 **다른 판**이 돈다 → [rule/01](rule/01-이-저장소의-함정.md) |
|
|
41
|
+
| **싸게 물어볼 일** | `agy --model gemini-3.8-flash-low -p='…'`. 🔴 `-p=` 는 **등호로 붙여** 쓴다 → [rule/08](rule/08-agy-외부-모델.md) |
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 🔴 하지 말 것 — 한눈에
|
|
46
|
+
|
|
47
|
+
이 표만은 항상 읽힌다. 나머지는 찾아본다.
|
|
48
|
+
|
|
49
|
+
| | 왜 |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `git commit --no-verify` | 장부에 없는 변경이 생기고, `audit` 이 나중에 불변식 위반으로 잡는다 |
|
|
52
|
+
| 아무 생각 없이 `git push` | 대상이 `origin`(팀 저장소)일 수 있다. axMap 은 **`personal team-sync`** |
|
|
53
|
+
| `git merge` 로 다른 갈래 가져오기 | 공통 조상이 없어 거부당한다. `git checkout <브랜치> -- <경로>` |
|
|
54
|
+
| `git merge-base` 결과를 파이프로 잇기 | 실패해도 조용하다. 종료 코드를 따로 본다 |
|
|
55
|
+
| 테스트 **개수**를 문서에 적기 | 늘릴 때마다 낡는다. 판정은 종료 코드다 |
|
|
56
|
+
| 실측 안 한 숫자 적기 | 확실하지 않으면 **"확인 못 함"** 이라고 쓴다 |
|
|
57
|
+
| 낡은 실측 지우기 | 정정한 날짜와 함께 **둘 다** 남긴다 |
|
|
58
|
+
| 선점이 방금 풀린 자리에 바로 들어가기 | 선점은 "지금" 만 막는다. 남은 결과물은 안 막는다 |
|
|
59
|
+
| 새 의존성 추가 | 추가하기 전에 **사람에게 묻는다** |
|
|
60
|
+
| 세기·고르기를 모델에게 시키기 | 스크립트가 토큰 0으로 **더 정확히** 한다 → [rule/08](rule/08-agy-외부-모델.md) |
|
|
61
|
+
| `setx` 로 PATH 고치기 | 1024자에서 **자른다.** 잘린 줄은 경고 없이 사라진다 → [rule/08](rule/08-agy-외부-모델.md) |
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 규칙 본문 — 이럴 때 이 파일을 연다
|
|
66
|
+
|
|
67
|
+
문서 안에서 **"2절"** 이라고 하면 **`rule/02-…`** 를 말한다. 번호는 그대로 남겼다.
|
|
68
|
+
|
|
69
|
+
| 파일 | 언제 여나 |
|
|
70
|
+
|---|---|
|
|
71
|
+
| [00 — 백그라운드 에이전트](rule/00-백그라운드-에이전트.md) | 일을 대화에서 직접 할지 넘길지 가를 때. 프롬프트에 무엇을 박아야 하나 |
|
|
72
|
+
| [01 — 이 저장소의 함정](rule/01-이-저장소의-함정.md) | git 이 이상하게 굴 때. remote 이름·개발선·무관한 역사·`partOf()`·어느 `axmap` 이 도나 |
|
|
73
|
+
| [02 — 선점 → 작업 → 반납](rule/02-선점-작업-반납.md) | 파일을 고치기 전. 거부당했을 때. 커밋 훅이 막을 때. 쪽지 주고받기 |
|
|
74
|
+
| [03 — 검증](rule/03-검증.md) | 끝내기 전. 무엇을 돌려야 끝난 것인가. 화면 코드를 건드렸을 때 |
|
|
75
|
+
| [04 — 코드 규칙](rule/04-코드-규칙.md) | 코드를 쓸 때. 의존성·순수 로직·fail-closed·SPEC·불변식·버전 |
|
|
76
|
+
| [05 — 코드를 쓰기 전에](rule/05-코드를-쓰기-전에.md) | 무엇을 만들지 아직 안 정했을 때. 단순함·외과적 변경·선택지로 확인받기 |
|
|
77
|
+
| [06 — 답변과 문서](rule/06-답변과-문서.md) | 사람에게 답할 때. 터미널에 전부·두 가지까지·기호로만 부르지 않기·용어 풀기 |
|
|
78
|
+
| [07 — NotebookLM 자료 폴더](rule/07-notebooklm-자료폴더.md) | *"LM 에 넣게 자료 만들어줘"* 라는 말을 들었을 때 |
|
|
79
|
+
| [08 — `agy` 로 바깥 모델 부르기](rule/08-agy-외부-모델.md) | 토큰을 아끼고 싶을 때. **부르기 전에 "모델이 정말 필요한가" 부터** |
|
|
80
|
+
|
|
81
|
+
고칠 것이 있으면 **`rule/` 안의 그 파일**을 고친다. 여기에 본문을 되돌려 놓지 않는다 —
|
|
82
|
+
두 벌이 되는 순간 어느 쪽이 진짜인지 아무도 모르게 된다.
|
package/bin/axmap.mjs
CHANGED
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
registryUrl,
|
|
45
45
|
} from '../src/update.mjs'
|
|
46
46
|
import { homeRegistrations } from '../src/mcpstate.mjs'
|
|
47
|
+
import { compareBranchFor, formatOverlap } from '../src/overlap.mjs'
|
|
47
48
|
|
|
48
49
|
/** 이 CLI 가 들어 있는 axMap 폴더. 갱신 확인과 setup 이 자기 위치를 알아야 한다. */
|
|
49
50
|
const SELF_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
@@ -524,6 +525,98 @@ function busReadHint(me) {
|
|
|
524
525
|
return `node ${use} list --to ${me}`
|
|
525
526
|
}
|
|
526
527
|
|
|
528
|
+
/**
|
|
529
|
+
* 겹칠지도 모르는 작업을 찾아서 claim 끝에 붙인다. 없으면 아무것도 안 찍는다.
|
|
530
|
+
*
|
|
531
|
+
* 판정과 문장은 `src/overlap.mjs` 에 있다. 여기는 git 을 부르는 부분만 있다.
|
|
532
|
+
*
|
|
533
|
+
* ── 🔴 명령을 경로마다 띄우지 않는다 ────────────────────────────────────────
|
|
534
|
+
*
|
|
535
|
+
* 처음 만들 때는 원격 브랜치를 하나씩 돌며 `git` 을 100번 띄웠다. **4,475ms** 가
|
|
536
|
+
* 나왔고 "느리니까 최근 2주만 보자" 는 **틀린 결론**까지 냈다. `--all --not` 으로
|
|
537
|
+
* **한 번에** 물으면 같은 답이 **89ms** 다 — 50배. 느린 것은 검사가 아니라
|
|
538
|
+
* 프로세스를 띄우는 일이었다. (기간으로 자르는 것은 쓸모도 없었다 — 원격 브랜치
|
|
539
|
+
* 100개 중 91개가 최근 2주 안에 움직였다.)
|
|
540
|
+
*
|
|
541
|
+
* ── 못 재면 어떻게 하나 ────────────────────────────────────────────────────
|
|
542
|
+
*
|
|
543
|
+
* 🔴 **조용히 넘어간다.** 이 저장소는 관문에서는 "못 재면 막는다" 를 지키지만
|
|
544
|
+
* (`mrtarget.mjs` 가 이름을 못 받으면 거부하는 것처럼), 이것은 **관문이 아니라
|
|
545
|
+
* 조언**이다. 견줄 브랜치가 없는 저장소도 있고, 그건 고장이 아니라 정상이다.
|
|
546
|
+
* 조언이 못 떴다고 선점을 막으면 도구가 쓸모없어진다.
|
|
547
|
+
*/
|
|
548
|
+
function printOverlap(root, paths) {
|
|
549
|
+
try {
|
|
550
|
+
const branch = git(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: root }).out
|
|
551
|
+
|
|
552
|
+
// 🔴 사다리(`<파트>/dev`)가 없는 저장소는 견줄 곳을 **직접 적어 둔다.**
|
|
553
|
+
// `git config axmap.comparebranch team-sync`. 없으면 빈 문자열이 되고,
|
|
554
|
+
// 그러면 판정 쪽이 지금까지처럼 파트 칸만 본다. 왜 새 브랜치를 만드는
|
|
555
|
+
// 대신 설정값으로 했는지는 `src/overlap.mjs` 의 주석에 있다.
|
|
556
|
+
const cfg = git(['config', '--get', 'axmap.comparebranch'], { cwd: root })
|
|
557
|
+
const rel = compareBranchFor(branch, cfg.code === 0 ? cfg.out : null)
|
|
558
|
+
if (!rel) return
|
|
559
|
+
|
|
560
|
+
// 원격 것을 먼저 본다. 남이 올린 것을 보려는 검사라 로컬 사본은 늘 낡아 있다.
|
|
561
|
+
let cmp = `origin/${rel}`
|
|
562
|
+
if (git(['rev-parse', '--verify', '--quiet', cmp], { cwd: root }).code !== 0) {
|
|
563
|
+
cmp = rel
|
|
564
|
+
if (git(['rev-parse', '--verify', '--quiet', cmp], { cwd: root }).code !== 0) return
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const FMT = '%h%x09%an%x09%s'
|
|
568
|
+
const sep = ['--'].concat(paths)
|
|
569
|
+
|
|
570
|
+
// [2] 낡은 바탕 — 저쪽엔 이미 있는데 내 바탕엔 없는 것. 경로로 자른다.
|
|
571
|
+
// `--no-merges` — 머지 커밋은 "누가 무엇을 했나" 를 안 알려 준다. 실제로
|
|
572
|
+
// 재 보니 [2] 에 머지 한 줄이 끼어 세 칸 중 하나를 잡아먹고 있었다.
|
|
573
|
+
const staleR = git(
|
|
574
|
+
['log', '-3', '--no-merges', `--format=${FMT}`, `HEAD..${cmp}`].concat(sep),
|
|
575
|
+
{ cwd: root },
|
|
576
|
+
)
|
|
577
|
+
const stale = staleR.code !== 0 ? [] : splitLog(staleR.out, 3)
|
|
578
|
+
|
|
579
|
+
// [1] 보이지 않는 창 — 어느 브랜치엔 있는데 저쪽에도 내 쪽에도 아직 없는 것.
|
|
580
|
+
// 장부 브랜치는 코드 트리를 안 건드리지만, 훑는 대상에서 아예 빼 둔다.
|
|
581
|
+
const unmergedR = git(
|
|
582
|
+
[
|
|
583
|
+
'log', '-5', '--no-merges', '--source', `--format=%h%x09%S%x09%an%x09%s`,
|
|
584
|
+
'--exclude=refs/heads/axmap/*', '--exclude=refs/remotes/*/axmap/*',
|
|
585
|
+
'--all', '--not', cmp, 'HEAD',
|
|
586
|
+
].concat(sep),
|
|
587
|
+
{ cwd: root },
|
|
588
|
+
)
|
|
589
|
+
const unmerged =
|
|
590
|
+
unmergedR.code !== 0
|
|
591
|
+
? []
|
|
592
|
+
: unmergedR.out
|
|
593
|
+
.split('\n')
|
|
594
|
+
.filter(Boolean)
|
|
595
|
+
.slice(0, 5)
|
|
596
|
+
.map((l) => {
|
|
597
|
+
const [hash, ref, who, ...rest] = l.split('\t')
|
|
598
|
+
return { hash, ref: String(ref).replace(/^refs\/(remotes|heads)\//, ''), who, subject: rest.join('\t') }
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
const msg = formatOverlap({ compareBranch: cmp, stale, unmerged })
|
|
602
|
+
if (msg) console.log(msg)
|
|
603
|
+
} catch {
|
|
604
|
+
// 조언이 claim 을 죽이면 안 된다 — printUnread 와 같은 이유다.
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** `%h\t%an\t%s` 줄들을 레코드로. */
|
|
609
|
+
function splitLog(out, limit) {
|
|
610
|
+
return String(out)
|
|
611
|
+
.split('\n')
|
|
612
|
+
.filter(Boolean)
|
|
613
|
+
.slice(0, limit)
|
|
614
|
+
.map((l) => {
|
|
615
|
+
const [hash, who, ...rest] = l.split('\t')
|
|
616
|
+
return { hash, who, subject: rest.join('\t') }
|
|
617
|
+
})
|
|
618
|
+
}
|
|
619
|
+
|
|
527
620
|
/** claim·status 끝에 붙이는 알림. 없으면 아무것도 안 찍는다. */
|
|
528
621
|
function printUnread(root, me) {
|
|
529
622
|
let notes = []
|
|
@@ -1514,6 +1607,7 @@ function cmdClaim(positional, flags) {
|
|
|
1514
1607
|
console.log(`claim 성공 - ${me}`)
|
|
1515
1608
|
for (const p of res.record.paths) console.log(` + ${p}`)
|
|
1516
1609
|
console.log(` TTL ${humanDuration(ttlMs)} (만료 ${new Date(t + ttlMs).toISOString()})`)
|
|
1610
|
+
printOverlap(root, res.record.paths)
|
|
1517
1611
|
printUnread(root, me)
|
|
1518
1612
|
return
|
|
1519
1613
|
}
|
package/governance/GOVERNANCE.md
CHANGED
|
@@ -272,6 +272,24 @@ node axmap/governance/gate.mjs --source <소스브랜치> --target <타깃브랜
|
|
|
272
272
|
해제 덕분에 세어진 사람의 이름까지 남는다. 기계가 읽을 표식은 `tally` 에 있다 —
|
|
273
273
|
`g1Waived` · `eligibleCount` · `threshold` · `selfVoted`. `gate --json` 이 그대로 낸다.
|
|
274
274
|
|
|
275
|
+
🔴 **표를 쓰는 쪽도 같은 판단을 한다.** `governance/vote.mjs` 는 자기 표를 만나면
|
|
276
|
+
게이트를 `--json` 으로 한 번 돌려 그 판정의 `tally.g1Waived` 를 읽는다. 해제가
|
|
277
|
+
걸리면 **`--force` 없이 그냥 써 주고** 왜 걸렸는지(유효 투표권자 N명 < 정족수 M)를
|
|
278
|
+
같이 찍는다. 안 걸리면 지금까지대로 경고하고 `--force` 를 요구한다.
|
|
279
|
+
|
|
280
|
+
> **왜 이 줄이 여기 있나** — 1.2.0 에서 이 예외를 세는 쪽에만 넣었더니, 쓰는 쪽이
|
|
281
|
+
> 그것을 모른 채 *"이 표는 자기 표라 세지지 않습니다"* 라며 막았다. **세어질 표를
|
|
282
|
+
> 막은 것이다.** 사람은 던져 보지도 않고 교착이라고 믿었다 (실측: 팀 저장소
|
|
283
|
+
> S15P21E201 의 `front/dev → front/main`, 2026-09-07 — `--force` 로 밀어 넣은
|
|
284
|
+
> 자기 표 둘이 판정문에서 "해제 덕분에 센 표" 로 세어져 종료 코드 0 이 났다).
|
|
285
|
+
>
|
|
286
|
+
> 그래서 쓰는 쪽은 조건을 **다시 세지 않는다.** 판정이 내놓은 표식을 읽을 뿐이다.
|
|
287
|
+
> 같은 규칙을 두 곳에 적으면 한쪽만 고치는 날 같은 어긋남이 또 난다 —
|
|
288
|
+
> `vote_note` 를 두 곳에서 같은 함수 하나로 보는 것과 같은 이유다.
|
|
289
|
+
>
|
|
290
|
+
> **fail-closed 는 쓰는 쪽에도 그대로다.** 게이트를 못 돌렸거나 판정이 `count`
|
|
291
|
+
> 단계까지 못 갔으면 해제하지 않는다. 즉 **모르면 여전히 `--force` 가 필요하다.**
|
|
292
|
+
|
|
275
293
|
**`self_vote` 정책(아래)과는 다른 층이다.** `self_vote` 는 *"누구까지를 자기로
|
|
276
294
|
보는가"* 를 **사람이 미리 정하는 것**이고, 이 예외는 *"그렇게 정했더니 아무도 못
|
|
277
295
|
던지게 됐을 때"* **코드가 그 판정에서만 푸는 것**이다. `self_vote: "off"` 면
|
package/governance/vote.mjs
CHANGED
|
@@ -50,6 +50,11 @@
|
|
|
50
50
|
* 언제나 게이트다. 여기서 미리 거르면 규칙이 두 군데로 갈라지고, 그때부터
|
|
51
51
|
* 한 쪽만 고쳐지는 날이 온다.
|
|
52
52
|
*
|
|
53
|
+
* 🔴 그래서 **자기 표를 막을지 말지도 여기서 판단하지 않는다.** 게이트를
|
|
54
|
+
* `--json` 으로 한 번 돌려 그 판정의 `tally.g1Waived` 를 그대로 읽는다
|
|
55
|
+
* (`g1WaiverFor`). 1.2.0 에서 세는 쪽이 얻은 G1 해제를 이 파일이 몰라
|
|
56
|
+
* **세어질 표를 "안 세어집니다" 라며 막던** 버그가 바로 그 갈라짐이었다.
|
|
57
|
+
*
|
|
53
58
|
* ── 장부와 같은 방식 ─────────────────────────────────────────────────────
|
|
54
59
|
*
|
|
55
60
|
* 표 브랜치를 다루는 방법은 `bin/axmap.mjs` 의 장부(`axmap/claims`)와 같다:
|
|
@@ -222,6 +227,8 @@ const HELP = `표를 던진다 — 저장소 안의 파일 하나로
|
|
|
222
227
|
--target 자기 표(G1) 경고를 위해 비교할 브랜치. 생략하면 main.
|
|
223
228
|
변경량(agent.shown)을 재는 기준이기도 하다
|
|
224
229
|
--force 자기 표라는 경고를 무릅쓰고 그래도 쓴다.
|
|
230
|
+
🔴 던질 수 있는 사람이 정족수보다 적어 G1 이 해제되는 판정에서는
|
|
231
|
+
필요 없다 — 그때는 자기 표도 세어지므로 그냥 써 준다
|
|
225
232
|
🔴 이유(--note) 요구는 --force 로 못 넘긴다
|
|
226
233
|
--remote 원격 이름. 없으면 git config axmap.remote → 유일한 원격 → origin
|
|
227
234
|
--policy 정책 파일의 자리 (저장소 루트 기준). 없으면 AXMAP_POLICY_PATH
|
|
@@ -352,7 +359,38 @@ function selfVoteRange(branch, targetName) {
|
|
|
352
359
|
return { args: ['-n', '200', branch], label: `${branch} 의 최근 200 커밋` }
|
|
353
360
|
}
|
|
354
361
|
|
|
355
|
-
|
|
362
|
+
/**
|
|
363
|
+
* **이 판정에서 G1 이 해제되는가 — 세는 쪽에 물어본다.**
|
|
364
|
+
*
|
|
365
|
+
* 🔴 왜 여기서 계산하지 않는가 — **이 함수가 없어서 버그가 났다.** 1.2.0 에서
|
|
366
|
+
* 세는 쪽(`src/governance.mjs` 의 `countVotes`)은 *"자기 표를 빼고 나면 던질
|
|
367
|
+
* 수 있는 사람이 정족수보다 적을 때"* 그 판정 하나에 한해 G1 을 푸는 예외를
|
|
368
|
+
* 얻었는데, **쓰는 쪽인 이 파일은 그 사실을 몰랐다.** 그래서 세어질 표를
|
|
369
|
+
* "안 세어집니다" 라며 막았고, 팀은 반나절을 교착이라고 믿었다
|
|
370
|
+
* (실측: 팀 저장소 S15P21E201 의 `front/dev → front/main`, 2026-09-07).
|
|
371
|
+
*
|
|
372
|
+
* 같은 규칙을 두 곳에 적으면 그 어긋남이 그대로 다시 난다. 그래서 여기서는
|
|
373
|
+
* 조건을 세지 않고 **판정이 내놓은 표식**(`tally.g1Waived` · `eligibleCount`
|
|
374
|
+
* · `threshold`)을 그대로 읽는다. `vote_note` 를 두 곳에서 같은 함수 하나로
|
|
375
|
+
* 보는 것과 같은 태도다.
|
|
376
|
+
*
|
|
377
|
+
* 🔴 **fail-closed.** 게이트를 못 돌렸거나 판정이 `count` 단계까지 못 갔으면
|
|
378
|
+
* `null` 을 돌려 **해제하지 않는다.** 모를 때 풀어 주면 G1 은 성질이 아니라
|
|
379
|
+
* 켜고 끄는 스위치가 된다 (GOVERNANCE.md 의 "G1 의 예외 하나").
|
|
380
|
+
*
|
|
381
|
+
* @returns {{eligibleCount:number, threshold:number}|null} 해제될 때만 숫자들
|
|
382
|
+
*/
|
|
383
|
+
function g1WaiverFor(branch, targetName, policyFlag) {
|
|
384
|
+
const v = runGate(branch, targetName, policyFlag)
|
|
385
|
+
if (!v || v.stage !== 'count' || !v.tally) return null
|
|
386
|
+
const { g1Waived, eligibleCount, threshold } = v.tally
|
|
387
|
+
if (g1Waived !== true) return null
|
|
388
|
+
// 표식이 반쪽만 온 판정은 못 읽은 것으로 친다 — 숫자를 지어내서 안내하지 않는다.
|
|
389
|
+
if (!Number.isInteger(eligibleCount) || !Number.isInteger(threshold)) return null
|
|
390
|
+
return { eligibleCount, threshold }
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function warnIfSelfVote(branch, targetName, email, force, policyFlag) {
|
|
356
394
|
const range = selfVoteRange(branch, targetName)
|
|
357
395
|
const r = git(['log', '--format=%aE', ...range.args])
|
|
358
396
|
if (r.code !== 0) {
|
|
@@ -362,6 +400,18 @@ function warnIfSelfVote(branch, targetName, email, force) {
|
|
|
362
400
|
const authors = new Set(r.out.split('\n').map((s) => s.trim().toLowerCase()).filter(Boolean))
|
|
363
401
|
if (!authors.has(email.toLowerCase())) return
|
|
364
402
|
|
|
403
|
+
// 자기 표는 맞다. 그런데 **이 판정에서는 세어지는가?** 세는 쪽에 물어본다.
|
|
404
|
+
const waiver = g1WaiverFor(branch, targetName, policyFlag)
|
|
405
|
+
if (waiver) {
|
|
406
|
+
console.error(
|
|
407
|
+
'이 표는 자기 표지만 **이 판정에서는 세어집니다** (G1 의 예외 — 해제).\n'
|
|
408
|
+
+ ` ${targetName} 기준 유효 투표권자 ${waiver.eligibleCount}명 < 정족수 ${waiver.threshold} —\n`
|
|
409
|
+
+ ' 자기 표를 빼고 나면 던질 수 있는 사람이 정족수보다 적어, 이 판정 하나에 한해 G1 이 풀립니다.\n'
|
|
410
|
+
+ ' --force 없이 그대로 씁니다. 판정문에는 "해제 덕분에 센 표" 로 이름까지 남습니다.\n',
|
|
411
|
+
)
|
|
412
|
+
return
|
|
413
|
+
}
|
|
414
|
+
|
|
365
415
|
console.error(
|
|
366
416
|
`이 표는 자기 표라 세지지 않습니다 (G1 — 자기가 쓴 코드에 자기가 찬성할 수 없습니다).\n`
|
|
367
417
|
+ ` ${email} 이(가) ${range.label} 의 author 목록에 있습니다.\n\n`
|
|
@@ -478,16 +528,21 @@ function measureDiff(targetName, branch) {
|
|
|
478
528
|
}
|
|
479
529
|
|
|
480
530
|
/**
|
|
481
|
-
*
|
|
531
|
+
* **세는 쪽에게 직접 물어본다.** 게이트를 `--json` 으로 한 번 돌리고 그 판정을
|
|
532
|
+
* 그대로 기억한다. 못 돌렸으면 `null`.
|
|
482
533
|
*
|
|
483
|
-
* 🔴
|
|
484
|
-
*
|
|
534
|
+
* 🔴 **이 프로그램은 판정을 흉내내지 않는다.** "자기 표가 세어지는가" 를 여기서
|
|
535
|
+
* 다시 계산하면 규칙이 두 벌이 되고, 한쪽만 고치는 날 **세는 쪽은 세는데
|
|
536
|
+
* 쓰는 쪽은 막는** 어긋남이 난다. 그게 실제로 났다 (`g1WaiverFor` 주석).
|
|
485
537
|
*
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
538
|
+
* 🔴 한 번만 돌린다. 이 결과를 쓰는 자리가 둘이기 때문이다 — 자기 표를 막을지
|
|
539
|
+
* 말지(`g1WaiverFor`)와 표에 적히는 `agent.shown.gate`. 두 번 돌리면 게이트가
|
|
540
|
+
* 두 번 fetch 하고, 그 사이에 남이 표를 던지면 **같은 실행 안에서 두 판정이
|
|
541
|
+
* 갈린다.**
|
|
489
542
|
*/
|
|
490
|
-
|
|
543
|
+
let gateOnce // undefined = 아직 안 돌림 · null = 못 돌림
|
|
544
|
+
function runGate(branch, targetName, policyFlag) {
|
|
545
|
+
if (gateOnce !== undefined) return gateOnce
|
|
491
546
|
const r = spawnSync(process.execPath, [
|
|
492
547
|
path.join(HERE, 'gate.mjs'),
|
|
493
548
|
'--source', branch,
|
|
@@ -502,10 +557,28 @@ function measureGate(branch, targetName, policyFlag) {
|
|
|
502
557
|
maxBuffer: 64 * 1024 * 1024,
|
|
503
558
|
timeout: GATE_TIMEOUT_MS,
|
|
504
559
|
})
|
|
505
|
-
|
|
560
|
+
gateOnce = null
|
|
561
|
+
if (r.error || r.status === null) return gateOnce
|
|
506
562
|
let verdict = null
|
|
507
|
-
try { verdict = JSON.parse(r.stdout ?? '') } catch { return
|
|
508
|
-
if (!verdict || typeof verdict !== 'object') return
|
|
563
|
+
try { verdict = JSON.parse(r.stdout ?? '') } catch { return gateOnce }
|
|
564
|
+
if (!verdict || typeof verdict !== 'object') return gateOnce
|
|
565
|
+
gateOnce = verdict
|
|
566
|
+
return gateOnce
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* **던지기 직전의 판정** 한 줄. 위에서 받아 둔 판정을 요약할 뿐이다.
|
|
571
|
+
*
|
|
572
|
+
* 🔴 이 표 자신은 아직 없다. 즉 `"미달 1/2"` 는 *"내가 던지기 전에 1표였다"* 는
|
|
573
|
+
* 뜻이다. 나중에 이 표가 몇 번째였는지를 이력만으로 알 수 있게 하는 값이다.
|
|
574
|
+
*
|
|
575
|
+
* 판정문을 다시 해석하지 않고 `--json` 의 `exit`·숫자만 쓴다. 문구를 다듬는
|
|
576
|
+
* 커밋이 이 칸의 뜻을 바꾸면 안 되기 때문이다 (gate.mjs 의 관례 그대로).
|
|
577
|
+
* 못 돌리면 `"잴 수 없음"` — 표는 그래도 던져진다.
|
|
578
|
+
*/
|
|
579
|
+
function measureGate(branch, targetName, policyFlag) {
|
|
580
|
+
const verdict = runGate(branch, targetName, policyFlag)
|
|
581
|
+
if (!verdict) return '잴 수 없음'
|
|
509
582
|
if (verdict.stage === 'policy') return '정책 깨짐'
|
|
510
583
|
if (verdict.stage === 'undecidable') return '판정 불가'
|
|
511
584
|
if (!Number.isInteger(verdict.approvals) || !Number.isInteger(verdict.threshold)) return '잴 수 없음'
|
|
@@ -645,7 +718,7 @@ const nameErr = agentNameError(voter)
|
|
|
645
718
|
if (nameErr) die(`${nameErr}\n git config user.name 을 고치세요.`)
|
|
646
719
|
|
|
647
720
|
warnIfNotAVoter(roster, targetName, email)
|
|
648
|
-
warnIfSelfVote(branch, targetName, email, flags.force === true)
|
|
721
|
+
warnIfSelfVote(branch, targetName, email, flags.force === true, str(flags.policy))
|
|
649
722
|
|
|
650
723
|
// ── 이유 없는 표는 여기서 거부한다 ─────────────────────────────────────────
|
|
651
724
|
//
|
package/mcp/server.mjs
CHANGED
|
@@ -652,11 +652,31 @@ function brief() {
|
|
|
652
652
|
*
|
|
653
653
|
* scripts 에는 검사(`test` · `smoke` · `demo:*`)와 실행(`app` · `desktop`)이
|
|
654
654
|
* 섞여 있다. 전부 나열하면 새 에이전트가 앱을 띄워 놓고 "검증했다" 고 여긴다.
|
|
655
|
-
* **무엇이 통과해야 하는지를 정한 곳은
|
|
655
|
+
* **무엇이 통과해야 하는지를 정한 곳은 규칙 문서의 「검증」 절**이므로
|
|
656
656
|
* 거기서 읽어 온다. 규격이 진실이고 목록은 그 사본이다.
|
|
657
|
+
*
|
|
658
|
+
* 🔴 **제목 글자에 그대로 묶지 않는다 (2026-09-11).**
|
|
659
|
+
* 예전에는 `'## 검증'` 이라는 **문자열을 그대로** 찾았다. 그런데 규칙 문서가
|
|
660
|
+
* 번호 절(`# 3. 검증`)로 바뀌자 이 줄이 아무것도 못 찾았고, 브리핑에서 검증
|
|
661
|
+
* 목록이 **조용히 사라졌다.** 문서 모양을 바꿀 때마다 코드가 깨지면 둘 중
|
|
662
|
+
* 하나는 안 고쳐진다 — 그래서 제목 단계와 번호를 안 따지게 한다.
|
|
663
|
+
*
|
|
664
|
+
* 🔴 **자리가 옮겨갈 수 있다.** `CLAUDE.md` 가 색인 한 장이 되면서 본문은
|
|
665
|
+
* `rule/` 로 나갔다. 그래서 색인에 없으면 규칙 본문에서 이어 찾는다.
|
|
666
|
+
* 못 찾으면 **빈 목록**이 되고, 빈 목록은 위 블록이 통째로 안 나간다 —
|
|
667
|
+
* 지어낸 명령을 내보내는 것보다 아무 말도 안 하는 편이 낫다.
|
|
657
668
|
*/
|
|
658
|
-
const
|
|
659
|
-
const
|
|
669
|
+
const SECTION = /^#{1,6}\s*[0-9.]*\s*검증/m
|
|
670
|
+
const pickFence = (text) => {
|
|
671
|
+
if (!text) return null
|
|
672
|
+
const m = SECTION.exec(text)
|
|
673
|
+
if (!m) return null
|
|
674
|
+
return text.slice(m.index).match(/```[a-z]*\n([\s\S]*?)```/)
|
|
675
|
+
}
|
|
676
|
+
const fence =
|
|
677
|
+
pickFence(read('CLAUDE.md')) ??
|
|
678
|
+
pickFence(read('rule/03-검증.md')) ??
|
|
679
|
+
null
|
|
660
680
|
const cmds = fence ? fence[1].split('\n').map((l) => l.trim()).filter((l) => l.startsWith('npm ')) : []
|
|
661
681
|
if (cmds.length) {
|
|
662
682
|
out.push('## 끝내기 전에 전부 통과해야 하는 것')
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//name": "🔴 npm 의 'axmap' 은 2022년부터 남이 쓰고 있는 다른 꾸러미다(leozin_ 의 Map 라이브러리, 1.1.2). 그래서 `npx axmap` 은 우리 것이 아니고, 누구에게도 그렇게 안내하면 안 된다. 2026-08-28 에 axmap-cli 로 정했다 — 스코프(@사용자/이름)를 쓰지 않는 이유는 스코프가 npm 계정 이름에 묶여서 나중에 팀이나 조직으로 옮길 때 이름이 통째로 바뀌기 때문이다. 사람이 치는 **명령** 이름은 그대로 axmap 이다 (아래 bin) — 꾸러미 이름과 명령 이름은 달라도 된다.",
|
|
3
3
|
"name": "axmap-cli",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.2",
|
|
5
5
|
"//private": "🔴 여기 있던 \"private\": true 를 2026-08-28 에 지웠다. 그 줄이 있는 동안 npm publish 는 거부됐다. 되돌리려면 다시 넣으면 되지만, 이미 올라간 버전은 그래도 안 사라진다 — npm 은 같은 번호를 덮어쓰지 못하고 삭제도 72시간 안에만 된다. 즉 이 줄을 되살리는 것은 '앞으로 안 올린다' 는 뜻이지 '올린 것을 없앤다' 는 뜻이 아니다.",
|
|
6
6
|
"//publishConfig": "axmap-cli 는 스코프가 없어서 기본이 이미 공개다. 즉 지금은 없어도 된다. 그래도 남겨 두는 이유: 나중에 @조직/axmap 처럼 스코프를 붙이는 날 이 줄이 없으면 publish 가 '유료 플랜이 필요하다'는 엉뚱한 말로 실패한다.",
|
|
7
7
|
"publishConfig": {
|
package/src/overlap.mjs
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 겹침 경고 — **선점이 못 막는 종류의 중복**을 선점할 때 함께 알린다.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 이것이 따로 필요한가 ─────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* 선점(claim)은 **"지금 이 순간 둘이 같은 파일을 열고 있다"** 를 막는다.
|
|
7
|
+
* 그런데 2026-09-10 에 같은 일(사진첩 권한 문구 한 줄)을 세 사람이 다섯 시간
|
|
8
|
+
* 안에 각각 만들었고, **셋 다 규칙을 어기지 않았다.** 각자 잡고 각자 반납해서
|
|
9
|
+
* 겹친 순간이 한 번도 없었다. 둘은 버려졌다.
|
|
10
|
+
*
|
|
11
|
+
* 즉 선점은 **동시성**을 막는 장치지 **중복**을 막는 장치가 아니다.
|
|
12
|
+
* 이 둘을 같은 것으로 여긴 것이 빈틈의 시작이었다.
|
|
13
|
+
*
|
|
14
|
+
* ── 구멍이 둘이고, 검사도 둘이어야 한다 ─────────────────────────────────────
|
|
15
|
+
*
|
|
16
|
+
* 그날의 두 중복은 **서로 다른 이유로** 났다. 하나로 묶으면 대책이 반만 듣는다.
|
|
17
|
+
*
|
|
18
|
+
* 가) **보이지 않는 창** — 누가 커밋한 때부터 그것이 파트 dev 에 합쳐질 때까지,
|
|
19
|
+
* 그 일은 git 어디에도 없다. 그날은 55분이었다 (19:46 커밋 → 20:41 머지).
|
|
20
|
+
* 그 사이에 시작한 사람은 **무엇을 해도 알 수 없다.** 파트 dev 를 최신으로
|
|
21
|
+
* 받아도 없고, 최상위 main 에서 갈라져 나와도 없다 — 거기엔 더 없다.
|
|
22
|
+
* → `unmergedTouching` 이 잡는다.
|
|
23
|
+
*
|
|
24
|
+
* 나) **낡은 바탕** — 내 브랜치가 갈라져 나온 시점이 오래돼서, **이미 합쳐진**
|
|
25
|
+
* 남의 작업조차 나에게는 안 보이는 상태.
|
|
26
|
+
* → `staleTouching` 이 잡는다.
|
|
27
|
+
*
|
|
28
|
+
* 실측으로 확인했다. 두 검사는 **정확히 엇갈린다** —
|
|
29
|
+
* 뒤처짐 검사는 20:15 건을 못 잡고, 안 합쳐진 브랜치 검사는 22:31 건을 못 잡는다.
|
|
30
|
+
* 그래서 둘 다 있어야 한다. (근거: axMap `docs/26.09.11/04-무엇으로-막나.md`)
|
|
31
|
+
*
|
|
32
|
+
* ── 🔴 왜 숫자를 경로로 자르는가 ────────────────────────────────────────────
|
|
33
|
+
*
|
|
34
|
+
* "내 바탕이 몇 커밋 뒤처졌나" 를 **쌩숫자로 쓰면 거짓말을 한다.**
|
|
35
|
+
*
|
|
36
|
+
* | 그날 상황 | 쌩숫자 | 경로로 자른 숫자 | 진실 |
|
|
37
|
+
* |---------------|------------|----------------|------------------------|
|
|
38
|
+
* | 20:13 (미리) | 4 뒤처짐 | 0 | 위험이 저장소 **밖**에 있었다 |
|
|
39
|
+
* | 22:28 (예승) | 32 뒤처짐 | 1 | **그 1개가 전부**였다 |
|
|
40
|
+
*
|
|
41
|
+
* 32 중 31 개는 내 파일과 상관없는 남의 화면 작업이었다. 쓸모 있는 신호는
|
|
42
|
+
* **"내가 잡으려는 경로를 건드린 커밋이 몇 개인가"** 하나뿐이다.
|
|
43
|
+
*
|
|
44
|
+
* ── 🔴 왜 막지 않고 경고만 하는가 ───────────────────────────────────────────
|
|
45
|
+
*
|
|
46
|
+
* 이것은 **"겹쳤다" 가 아니라 "겹쳤을지도 모른다"** 이다. 같은 파일을 건드렸다고
|
|
47
|
+
* 같은 일인 것은 아니다. 막으면 멀쩡한 작업이 자주 막히고, **자주 막히는 검사는
|
|
48
|
+
* 사람이 끈다.** 그러면 0 이 된다 — 지켜지지 않는 규칙은 없는 규칙보다 나쁘다.
|
|
49
|
+
*
|
|
50
|
+
* 그리고 이 선택이 **판 번호를 정한다.** 어제 통과하던 것을 오늘 실패로 바꾸면
|
|
51
|
+
* 맨 앞자리(major)를 올리고 먼저 알려야 한다 (팀 규칙 0.3). 경고는 그렇지 않다.
|
|
52
|
+
*
|
|
53
|
+
* ── 순수하게 둔다 ──────────────────────────────────────────────────────────
|
|
54
|
+
*
|
|
55
|
+
* 이 파일에는 git 호출도 파일 접근도 없다. 판정과 문장만 있다.
|
|
56
|
+
* 실제로 git 을 부르는 것은 `bin/axmap.mjs` 의 `collectOverlap` 이다.
|
|
57
|
+
* 이 저장소가 `mrtarget.mjs` · `governance.mjs` 에서 지켜 온 경계와 같다.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
import { levelOf } from './version.mjs'
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 이 브랜치에서 일할 때, **무엇과 견주어야 하는가.**
|
|
64
|
+
*
|
|
65
|
+
* 기능 브랜치(`feat/front/S15P21E201-241-…`)의 가운데 칸이 파트다.
|
|
66
|
+
* 그 파트의 개발 브랜치(`front/dev`)가 "이미 된 일" 이 모이는 자리다.
|
|
67
|
+
*
|
|
68
|
+
* 🔴 **단계 판정은 `version.mjs` 의 `levelOf` 를 가져다 쓴다.** 여기서 다시
|
|
69
|
+
* 만들면 한 저장소에 브랜치 단계 판정이 둘이 되고, `back/main` 이 최상위인지
|
|
70
|
+
* 파트인지를 두 곳이 서로 다르게 답할 수 있다. `mrtarget.mjs` 가 같은 이유로
|
|
71
|
+
* 같은 선택을 했다.
|
|
72
|
+
*
|
|
73
|
+
* 🔴 **그런데 파트 이름은 `partOf` 로 못 구한다.** `partOf` 는 *마지막* 슬래시
|
|
74
|
+
* 앞을 준다 — `front/dev` → `front` 는 맞지만
|
|
75
|
+
* `feat/front/S15P21E201-241-photo` → `feat/front` 가 된다. 기능 브랜치의
|
|
76
|
+
* 파트는 **가운데 칸**이다 (`[접두사]/[파트]/[Jira키]-[작업명]`, 팀 규칙 2절).
|
|
77
|
+
* 그래서 여기서만 가운데 칸을 직접 읽는다. 이 차이를 모르고 `partOf` 를 쓰면
|
|
78
|
+
* `feat/front/dev` 라는 **있지도 않은 브랜치**와 견주게 되고, 그러면 검사는
|
|
79
|
+
* 늘 조용해진다 — 가장 나쁜 고장이다(아무 말도 안 하니 아무도 모른다).
|
|
80
|
+
*
|
|
81
|
+
* 가운데 칸이 파트가 아닌 경우(팀 규칙이 경고하는 `feat/feat/…` 같은 오타)에는
|
|
82
|
+
* 그 이름의 dev 브랜치가 **없어서** 부르는 쪽이 조용히 넘어간다. 억지로 고쳐
|
|
83
|
+
* 맞히지 않는다 — 틀린 것과 견주느니 안 견주는 편이 낫다.
|
|
84
|
+
*
|
|
85
|
+
* ── 🔴 사다리가 없는 저장소도 있다 — 그래서 설정값을 받는다 ────────────────
|
|
86
|
+
*
|
|
87
|
+
* 이 검사를 만든 axMap 저장소 **자신이 첫 피해자였다.** axMap 의 개발선은
|
|
88
|
+
* `team-sync` 한 줄이라 브랜치 이름에 **파트 칸이 아예 없다.** 그래서 위 규칙은
|
|
89
|
+
* 늘 `null` 을 냈고, 검사는 **한 번도 돈 적이 없다.** 그러면서 아무 말도 안 하니
|
|
90
|
+
* 아무도 그 사실을 몰랐다 — 이 파일이 계속 경계하는 그 고장이다.
|
|
91
|
+
*
|
|
92
|
+
* 고치는 길이 둘이었다.
|
|
93
|
+
*
|
|
94
|
+
* 가) **`core/dev` 같은 파트 브랜치를 새로 만든다.** 안 골랐다. 그러려면 개발선
|
|
95
|
+
* 자체를 옮겨야 하고, 이미 나가 있는 브랜치·MR·CI 가 전부 따라와야 한다.
|
|
96
|
+
* **되돌리기 어려운 변경으로 검사 하나를 살리는 것은 값이 맞지 않는다.**
|
|
97
|
+
* 나) ✅ **견줄 브랜치를 저장소가 직접 적게 한다** (git 설정값
|
|
98
|
+
* `axmap.comparebranch`). 브랜치 구조를 한 글자도 안 건드리고, 사다리가
|
|
99
|
+
* 없는 **어느 저장소에서도** 이 검사가 돈다. 틀리면 설정값 한 줄을
|
|
100
|
+
* 지우면 끝이라 **되돌리기도 공짜**다.
|
|
101
|
+
*
|
|
102
|
+
* 우선순위는 **파트 칸이 먼저, 설정값이 나중**이다. 팀 저장소처럼 사다리가 있는
|
|
103
|
+
* 곳에서는 지금 동작이 그대로 남아야 하기 때문이다 — 설정값을 앞세우면 한 줄
|
|
104
|
+
* 설정이 파트별 판정을 통째로 덮어써서, `feat/front/…` 도 `feat/back/…` 도
|
|
105
|
+
* 같은 한 곳과 견주게 된다.
|
|
106
|
+
*
|
|
107
|
+
* 🔴 **지금 서 있는 브랜치가 곧 견줄 브랜치이면 `null` 이다.** 자기 자신과 견주면
|
|
108
|
+
* `HEAD..HEAD` 도 `--all --not HEAD HEAD` 도 언제나 비어서, 검사가 **도는
|
|
109
|
+
* 것처럼 보이면서 아무것도 안 잡는다.** 안 도는 것보다 나쁘다 — 안 도는 줄
|
|
110
|
+
* 알면 고치기라도 한다.
|
|
111
|
+
*
|
|
112
|
+
* 🔴 **설정값을 읽는 것은 여기가 아니다.** 이 파일에는 git 호출도 파일 접근도
|
|
113
|
+
* 없다(머리말). `bin/axmap.mjs` 가 `git config --get axmap.comparebranch` 로
|
|
114
|
+
* 읽어 **인자로 넘긴다.** `mrtarget.mjs` · `governance.mjs` 가 지켜 온 경계다.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} branch 지금 체크아웃된 브랜치 이름
|
|
117
|
+
* @param {string|null} [configured] 저장소가 적어 둔 견줄 브랜치
|
|
118
|
+
* (git 설정값 `axmap.comparebranch`). 없으면 null
|
|
119
|
+
* @returns {string|null} 견줄 브랜치 이름(`front/dev`). 견줄 것이 없으면 null
|
|
120
|
+
*/
|
|
121
|
+
export function compareBranchFor(branch, configured = null) {
|
|
122
|
+
const name = String(branch ?? '').trim()
|
|
123
|
+
if (!name) return null
|
|
124
|
+
|
|
125
|
+
// 이미 파트 dev 위에 있거나, 파트 브랜치·최상위에 있으면 견줄 위쪽이 없다.
|
|
126
|
+
// "위쪽이 없다" 는 고장이 아니라 정상이므로 조용히 넘어간다.
|
|
127
|
+
// 🔴 설정값이 있어도 여기서는 안 쓴다 — 이 자리들은 "못 읽었다" 가 아니라
|
|
128
|
+
// "위쪽이 없는 것이 맞다" 이고, 둘을 섞으면 파트 dev 가 자기 위의 아무 데나
|
|
129
|
+
// 견주게 된다.
|
|
130
|
+
const level = levelOf(name)
|
|
131
|
+
if (level === 'dev' || level === 'func' || level === 'main') return null
|
|
132
|
+
|
|
133
|
+
// ① `접두사/파트/작업명` 세 칸이 있으면 가운데 칸이 파트다.
|
|
134
|
+
// `hotfix/S15P21E201-33-…` 처럼 두 칸이면 파트가 적혀 있지 않다 — ② 로 간다.
|
|
135
|
+
const segs = name.split('/').filter(Boolean)
|
|
136
|
+
const part = segs.length >= 3 ? segs[1] : null
|
|
137
|
+
|
|
138
|
+
// ② 파트를 못 읽었으면 저장소가 적어 둔 것을 쓴다. ③ 그것도 없으면 null.
|
|
139
|
+
const cmp = part ? `${part}/dev` : String(configured ?? '').trim() || null
|
|
140
|
+
if (!cmp) return null
|
|
141
|
+
|
|
142
|
+
// 🔴 자기 자신과는 안 견준다. 견줘 봐야 늘 비어서, 있으나 마나 한 검사가 된다.
|
|
143
|
+
if (cmp === name) return null
|
|
144
|
+
|
|
145
|
+
return cmp
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 두 검사 결과를 사람이 읽는 경고로 바꾼다.
|
|
150
|
+
*
|
|
151
|
+
* 🔴 **겹치는 것이 없으면 아무것도 내지 않는다** (null 을 준다).
|
|
152
|
+
* 선점은 하루에도 수십 번 도는 자리라, 평소에 조용한 것이 이 검사의 조건이다.
|
|
153
|
+
*
|
|
154
|
+
* @param {object} o
|
|
155
|
+
* @param {string} o.compareBranch 견준 브랜치 (`origin/front/dev`)
|
|
156
|
+
* @param {Array<{hash:string, who:string, subject:string}>} o.stale
|
|
157
|
+
* 내 바탕 뒤로 이미 합쳐진 것 중, 내가 잡은 경로를 건드린 커밋
|
|
158
|
+
* @param {Array<{hash:string, ref:string, who:string, subject:string}>} o.unmerged
|
|
159
|
+
* 내가 잡은 경로를 건드렸지만 아직 그 브랜치에 안 합쳐진 커밋
|
|
160
|
+
* @returns {string|null}
|
|
161
|
+
*/
|
|
162
|
+
export function formatOverlap({ compareBranch, stale = [], unmerged = [] }) {
|
|
163
|
+
if (!stale.length && !unmerged.length) return null
|
|
164
|
+
|
|
165
|
+
const L = []
|
|
166
|
+
L.push('')
|
|
167
|
+
L.push(' ── 겹칠지도 모릅니다 (막지는 않습니다) ─────────────────────────────')
|
|
168
|
+
|
|
169
|
+
if (unmerged.length) {
|
|
170
|
+
L.push('')
|
|
171
|
+
L.push(` [1] 이 경로를 이미 건드렸지만 ${compareBranch} 에 아직 없는 작업이 있습니다.`)
|
|
172
|
+
L.push(' 아직 합쳐지지 않아 최신으로 받아도 보이지 않습니다.')
|
|
173
|
+
for (const c of unmerged) {
|
|
174
|
+
L.push(` · ${c.hash} ${c.who} ${c.subject}`)
|
|
175
|
+
L.push(` ↳ ${c.ref}`)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (stale.length) {
|
|
180
|
+
L.push('')
|
|
181
|
+
L.push(` [2] ${compareBranch} 에는 이미 합쳐졌는데 내 바탕에는 없는 작업이 있습니다.`)
|
|
182
|
+
L.push(' 내 브랜치가 갈라져 나온 시점이 그보다 앞섭니다.')
|
|
183
|
+
for (const c of stale) {
|
|
184
|
+
L.push(` · ${c.hash} ${c.who} ${c.subject}`)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
L.push('')
|
|
189
|
+
L.push(' 같은 일인지 먼저 보십시오. 같은 일이면 다시 만들지 말고 그것을 가져오십시오.')
|
|
190
|
+
L.push(' ────────────────────────────────────────────────────────────────')
|
|
191
|
+
return L.join('\n')
|
|
192
|
+
}
|