axmap-cli 0.0.1
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/commands/ax-done.md +10 -0
- package/.claude/commands/ax-setup.md +31 -0
- package/.claude/commands/ax-start.md +15 -0
- package/.claude/commands/ax-tell.md +14 -0
- package/.claude/commands/ax-update.md +19 -0
- package/.claude/commands/ax.md +13 -0
- package/CLAUDE.md +309 -0
- package/LICENSE +20 -0
- package/README.md +207 -0
- package/app/README.md +366 -0
- package/app/eval/edges.mjs +242 -0
- package/app/lib/adjacent.mjs +125 -0
- package/app/lib/agentcli.mjs +153 -0
- package/app/lib/analyze.mjs +1159 -0
- package/app/lib/cochange.mjs +421 -0
- package/app/lib/datanodes.mjs +127 -0
- package/app/lib/entry.mjs +192 -0
- package/app/lib/featuregraph.mjs +389 -0
- package/app/lib/features.mjs +645 -0
- package/app/lib/fetchrepo-run.mjs +37 -0
- package/app/lib/fetchrepo.mjs +164 -0
- package/app/lib/flow.mjs +1089 -0
- package/app/lib/ladder.mjs +387 -0
- package/app/lib/langs.mjs +630 -0
- package/app/lib/live.mjs +346 -0
- package/app/lib/llm.mjs +594 -0
- package/app/lib/newfile.mjs +126 -0
- package/app/lib/prdiff.mjs +651 -0
- package/app/lib/reveal.mjs +316 -0
- package/app/lib/roots.mjs +186 -0
- package/app/lib/scope.mjs +342 -0
- package/app/lib/session.mjs +389 -0
- package/app/lib/slots.mjs +233 -0
- package/app/lib/ssot.mjs +277 -0
- package/app/lib/teamview.mjs +962 -0
- package/app/lib/terms.ko.mjs +169 -0
- package/app/server.mjs +1959 -0
- package/app/web/shell.css +538 -0
- package/app/web/shell.html +197 -0
- package/app/web/shell.js +638 -0
- package/app/web/stage.js +347 -0
- package/app/web/words.js +85 -0
- package/bin/axmap.mjs +1918 -0
- package/governance/GOVERNANCE.md +433 -0
- package/governance/gate.mjs +526 -0
- package/governance/vote.mjs +501 -0
- package/mcp/README.md +254 -0
- package/mcp/SETUP-FOR-AI.md +186 -0
- package/mcp/install.ps1 +341 -0
- package/mcp/install.sh +339 -0
- package/mcp/server.mjs +969 -0
- package/package.json +48 -0
- package/src/closure.mjs +343 -0
- package/src/governance.mjs +839 -0
- package/src/invariants.mjs +226 -0
- package/src/mrtarget.mjs +284 -0
- package/src/promote.mjs +177 -0
- package/src/protocol.mjs +423 -0
- package/src/repotarget.mjs +81 -0
- package/src/update.mjs +177 -0
- package/src/version.mjs +186 -0
- package/tools/bus.mjs +520 -0
- package/tools/cluster-experiment.mjs +256 -0
- package/tools/cluster-sweep.mjs +226 -0
- package/tools/make-icon.mjs +108 -0
- package/tools/mcp-register.mjs +269 -0
- package/tools/mr-target.mjs +49 -0
- package/tools/persona-bench.mjs +362 -0
- package/tools/pick-repo.mjs +229 -0
- package/tools/promote.mjs +550 -0
- package/tools/reveal-demo.mjs +158 -0
- package/tools/run-tests.mjs +42 -0
- package/tools/setup.mjs +490 -0
- package/tools/shortcut.mjs +121 -0
- package/tools/smoke.mjs +166 -0
- package/tools/topicgraph.py +154 -0
- package/tools/vendor.mjs +382 -0
- package/tools/version.mjs +115 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
# 합의제 MR — 저장소 안에서 정족수를 센다
|
|
2
|
+
|
|
3
|
+
이 층이 답하는 질문은 **하나뿐**이다.
|
|
4
|
+
|
|
5
|
+
> **"이 변경은 정족수를 채웠는가?"**
|
|
6
|
+
> 정족수(定足數) — **통과에 필요한 최소 찬성 수.**
|
|
7
|
+
|
|
8
|
+
MR(**Merge Request** — 내 브랜치의 변경을 다른 브랜치로 합쳐 달라는 요청)에 붙는
|
|
9
|
+
리뷰 코멘트·담당자 배정·라벨은 GitLab 이 이미 잘 한다. 우리는 **숫자 하나**만 센다.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 🔴 먼저 — 이것은 강제 장치가 아니다
|
|
14
|
+
|
|
15
|
+
> **이 층은 강제 장치가 아니라 부인 불가능성(non-repudiation) 장치다.**
|
|
16
|
+
> 부인 불가능성 — **"나는 그런 적 없다" 고 말할 수 없게 만드는 성질.**
|
|
17
|
+
>
|
|
18
|
+
> 규칙을 우회한 머지를 **막지는 못한다.**
|
|
19
|
+
> 우회했다는 사실이 **지워지지 않는 기록으로 남게** 한다.
|
|
20
|
+
|
|
21
|
+
이 문장을 첫 화면에 두는 이유는, 숨기면 팀이 **보호받고 있다고 믿기** 때문이다.
|
|
22
|
+
믿는 순간 아무도 확인하지 않고, 확인하지 않는 보호는 없는 보호보다 나쁘다.
|
|
23
|
+
|
|
24
|
+
### 못 막는 것 셋 — 정확히 무엇인지
|
|
25
|
+
|
|
26
|
+
**1. `.gitlab-ci.yml` 에서 governance 잡을 지우는 MR**
|
|
27
|
+
|
|
28
|
+
CI(**Continuous Integration** — 코드를 올릴 때마다 자동으로 검사를 돌리는 것)의
|
|
29
|
+
파이프라인(**한 번 올릴 때 도는 검사 묶음**)은, MR 에서는 **소스 브랜치**
|
|
30
|
+
(**합쳐 달라고 내미는 쪽 브랜치**)의 `.gitlab-ci.yml` 로 돈다. 즉 잡(job —
|
|
31
|
+
파이프라인이 돌리는 작업 하나)을 지운 MR 은 **그 잡이 없는 파이프라인**을 돌린다.
|
|
32
|
+
전부 초록이 되고, 초록이니 머지 버튼이 열린다.
|
|
33
|
+
|
|
34
|
+
**어떤 CI 설계로도 이것을 사전에 막을 수 없다.** GitLab 이 소스 브랜치의 설정으로
|
|
35
|
+
파이프라인을 만들기 때문이고, 그건 우리가 바꿀 수 있는 것이 아니다.
|
|
36
|
+
|
|
37
|
+
대신 **사후 감사**로 잡기로 했다. `gate audit` 이 잠가 둔 브랜치의 머지 이력을 처음부터
|
|
38
|
+
재생하면서 "이 머지는 그때 정족수를 채웠는가" 를 다시 계산하고, 채우지 못한 머지를
|
|
39
|
+
커밋 해시와 함께 지목한다. 막지는 못하지만 **지워지지 않는다.**
|
|
40
|
+
|
|
41
|
+
> ⚠️ **`gate audit` 은 아직 안 만들었다.** 2026-08-26 기준으로 이 층에서 유일하게
|
|
42
|
+
> 비어 있는 자리다 (아래 "지금 있는 것"). 그때까지 이 문단은 **설계이지 사실이 아니다.**
|
|
43
|
+
|
|
44
|
+
**2. 표 위조**
|
|
45
|
+
|
|
46
|
+
막지 못한다. 파일을 손으로 만들면 그만이다.
|
|
47
|
+
|
|
48
|
+
다만 게이트(**판정을 실행하는 프로그램**)는 표에 적힌 `voter` 의 email 과, **그 표
|
|
49
|
+
파일을 만든 커밋의 committer**(**커밋을 실제로 기록한 git 신원**)를 대조한다.
|
|
50
|
+
어긋나면 세지 않는다. 그래서 남의 표를 위조하려면 **git 신원 자체를 바꿔야 하고**,
|
|
51
|
+
바꾼 사실이 `axmap/votes` 브랜치의 이력에 영구히 남는다.
|
|
52
|
+
|
|
53
|
+
GPG 서명(**커밋에 암호학적 서명을 붙여 작성자를 증명하는 것**)은 **선택으로 열어
|
|
54
|
+
둔다.** 필수로 걸면 키를 못 만든 사람이 투표를 못 하고, 그 순간 팀은 이 층을 끈다.
|
|
55
|
+
|
|
56
|
+
**3. 투표권자가 전멸한 저장소**
|
|
57
|
+
|
|
58
|
+
승계(→ 아래)로도 못 채우면 **막힌 채로 둔다.** 탈출구를 만들지 않았다.
|
|
59
|
+
오픈소스에서 그 상태의 정답은 fork(**저장소를 통째로 복제해 새 주인이 이어가는 것**)다.
|
|
60
|
+
탈출구를 하나 두면 그것이 곧 평시의 우회로가 된다.
|
|
61
|
+
|
|
62
|
+
### 코드로 만들 수 없는 전제 조건 셋
|
|
63
|
+
|
|
64
|
+
아래 셋은 **저장소 밖(GitLab 설정)에 있다.** 사람이 켜야 하고, 안 켜면 이 층은 장식이다.
|
|
65
|
+
|
|
66
|
+
> 🔴 **"지금" 칸은 2026-08-26 에 GitLab API**(**다른 프로그램이 GitLab 에 물어보는
|
|
67
|
+
> 창구**)**로 직접 읽은 값**이다. **이 표 밖의 GitLab 설정은 아무도 안 봤다** —
|
|
68
|
+
> 안 본 것을 본 것처럼 적으면 다음 사람이 그 줄을 믿고 확인을 건너뛴다.
|
|
69
|
+
|
|
70
|
+
| 조건 | 지금 (2026-08-26 실측) | 없으면 무슨 일이 | 왜 코드로 못 만드나 |
|
|
71
|
+
|---|---|---|---|
|
|
72
|
+
| **`Pipelines must succeed`** — *"MR 을 머지하려면 CI 검사가 전부 초록이어야 한다"* 는 설정 (`Settings → Merge requests → Merge checks`) | ❌ **꺼져 있다** | 판정이 빨개도 머지 버튼이 눌린다. 이 층 전체가 **읽어도 되고 안 읽어도 되는 참고 자료**가 된다 | 저장소 안의 파일이 아니라 GitLab 프로젝트 설정이다 |
|
|
73
|
+
| **보호 브랜치**(**아무나 직접 밀어 넣지 못하게 잠가 둔 브랜치**) **직접 push 금지** | ⚠️ **`main` 하나뿐**이다 (push `No one` · merge `Maintainers`). `front/dev` · `back/dev` · `front/main` · `back/main` 은 **보호돼 있지 않다** | MR 을 아예 안 거치고 밀어 넣으면 판정이 돌 자리가 없다 | 같은 이유. 서버가 거절해야 한다 |
|
|
74
|
+
| **Owner 최소 2명** — Owner(**프로젝트 설정 자체를 바꿀 수 있는 최고 권한**) | **확인 안 했다.** 위 두 줄과 달리 이건 읽어보지 않았다 | 한 사람이 위 두 설정을 **혼자 조용히 끄고 되돌릴 수 있다.** 그 사람이 팀을 떠나면 되돌릴 사람도 없다 | 권한은 사람의 목록이고, 목록은 가진 사람이 고친다 |
|
|
75
|
+
|
|
76
|
+
🔴 **보호 브랜치가 `main` 뿐이라는 사실의 결과 — 지금 이 층은 파트 브랜치를 못 지킨다.**
|
|
77
|
+
누구나 MR 없이 `front/dev` 같은 파트 브랜치에 **직접 push 할 수 있다.** 게이트도
|
|
78
|
+
`verify:mr-target`(**MR 이 한 칸씩만 올라가는지 보는 잡** — [docs/CI.md](../../docs/CI.md))도
|
|
79
|
+
**MR 이 있어야 도는 잡**이라, 직접 push 에는 아예 걸릴 자리가 없다. 표 한 장 없이
|
|
80
|
+
코드가 들어가고, 그 사실이 어디에도 안 남는다. 나머지 결과 하나(봇 토큰을 Protected
|
|
81
|
+
로 만들면 그 브랜치들에서 값이 비어 태그가 안 붙는 것)는
|
|
82
|
+
[docs/HANDOVER.md](../../docs/HANDOVER.md) 4.1 절에 있다.
|
|
83
|
+
|
|
84
|
+
🔴 **그리고 지금은 러너**(**CI 잡을 실제로 실행하는 프로그램**)**가 한 대도 없다.**
|
|
85
|
+
인스턴스·그룹·프로젝트 전부 0개다(같은 날 실측). 러너가 없으면 파이프라인은 실패가
|
|
86
|
+
아니라 `pending`(**받아갈 실행기가 없어 멈춰 있는 상태**) 이고, `governance` 잡은
|
|
87
|
+
**돌지도 않는다.** 위 첫 줄이 꺼져 있으니 그 상태로도 머지 버튼은 열린다.
|
|
88
|
+
**지금 이 층은 아무것도 막지 못한다** — 손으로 부르면 판정은 정확하지만(아래 실측 기록),
|
|
89
|
+
아무도 부르지 않으면 아무 일도 일어나지 않는다.
|
|
90
|
+
|
|
91
|
+
루트 [`CLAUDE.md`](../../CLAUDE.md) 3절이 같은 말을 한다 —
|
|
92
|
+
**권한은 사람의 목록이고 파이프라인은 코드의 조건이다.** 이 층은 조건 쪽에 서 있지만,
|
|
93
|
+
그 조건을 켜는 스위치는 목록 쪽에 있다.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 표는 저장소 안의 파일이다
|
|
98
|
+
|
|
99
|
+
**GitLab 의 Approve 버튼은 판정에 넣지 않는다.**
|
|
100
|
+
|
|
101
|
+
이 저장소는 **오픈소스 이식이 목표**다. 판정을 GitLab API(**다른 프로그램이 GitLab 에
|
|
102
|
+
물어보는 창구**)에 걸면, 이 저장소를 다른 곳으로 clone(**저장소를 통째로 내려받는 것**)한
|
|
103
|
+
사람에게는 **이 층이 존재하지 않는다.**
|
|
104
|
+
파일이면 `git clone` 이 곧 이관이다. 근거는 [DECISIONS.md](../docs/DECISIONS.md) 의 D18.
|
|
105
|
+
|
|
106
|
+
> 판단 당시에는 원격(**코드를 올려두는 서버 쪽 저장소**)이 셋(`origin`·`personal`·`aws`)
|
|
107
|
+
> 이었고, 2026-08-26 에 개인 저장소를 접어 **지금은 `origin` 하나**다. 근거는 그대로
|
|
108
|
+
> 유효하다 — 이유는 원격의 개수가 아니라 *"clone 에 따라오는가"* 이기 때문이다.
|
|
109
|
+
|
|
110
|
+
### 자리
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
브랜치: axmap/votes (코드 브랜치와 섞이지 않는다. 장부 브랜치와 같은 방식)
|
|
114
|
+
경로 : votes/<소스브랜치>/<voter>-<sha8>.json
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
이 브랜치는 **2026-08-26 에 원격(`origin`)에 실제로 생겼다.** 고아 브랜치
|
|
118
|
+
(**아무 이력에도 붙지 않은 브랜치**)라 코드 이력과 섞이지 않는다.
|
|
119
|
+
|
|
120
|
+
장부 브랜치 — **이 저장소가 claim(선점) 기록을 코드와 섞지 않고 따로 두는 브랜치**
|
|
121
|
+
(`axmap/claims`). 표도 같은 방식이다: 판정에 쓰는 데이터는 코드 리뷰를 오염시키지 않는
|
|
122
|
+
자리에 둔다.
|
|
123
|
+
`sha` — **커밋 하나를 가리키는 40자짜리 지문.** 파일 이름에는 앞 8자만 쓴다.
|
|
124
|
+
|
|
125
|
+
**한 사람이 한 커밋에 정확히 파일 하나.** 중복이 파일시스템 수준에서 불가능하다.
|
|
126
|
+
`tools/bus.mjs`(에이전트 쪽지함)에서 가져온 것은 이 **모양**뿐이다 — 코드가 아니다.
|
|
127
|
+
|
|
128
|
+
### 표 한 장
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"voter": "rleaderjoon",
|
|
133
|
+
"email": "rleaderjoon@gmail.com",
|
|
134
|
+
"branch": "feat/S15P21E201-144-login",
|
|
135
|
+
"sha": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0",
|
|
136
|
+
"vote": "approve",
|
|
137
|
+
"at": "2026-08-25T05:00:00Z",
|
|
138
|
+
"note": "여행 상세 화면 확인함"
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
| 칸 | |
|
|
143
|
+
|---|---|
|
|
144
|
+
| `voter` | **정책에 적힌 id 여야 한다** — `git config user.name` 이 아니다. `vote.mjs` 가 email 로 명단을 찾아 채운다. 승계로 들어온 사람은 정책에 id 가 없으므로 이 칸을 대조하지 않는다 |
|
|
145
|
+
| `email` | **유일 키다.** 사람을 가리키는 것은 id 가 아니라 email 이다 |
|
|
146
|
+
| `sha` | 이 표가 승인한 커밋. 40자 전체를 적는다 |
|
|
147
|
+
| `vote` | `approve` 또는 `reject`. 그 밖의 값은 판정 불가다 |
|
|
148
|
+
| `committerEmail` | 파일에 적지 않는다. **게이트가 git 에서 읽어 채운다** — 표에 적힌 신원과 대조하기 위한 칸이라, 표를 쓰는 사람이 적으면 대조가 아니라 자기 신고가 된다 |
|
|
149
|
+
|
|
150
|
+
`reject` 는 **정족수 계산에 들어가지 않는다.** 이 층은 "찬성이 몇인가" 만 세고,
|
|
151
|
+
거부권은 다른 제도다. 다만 출력에는 반드시 보인다 — 사람이 읽어야 하기 때문이다.
|
|
152
|
+
|
|
153
|
+
### 표를 던지는 법
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
node axmap/governance/vote.mjs --branch <소스브랜치> [--sha <커밋>] [--vote approve|reject] [--note "..."]
|
|
157
|
+
|
|
158
|
+
# 판정을 손으로 미리 돌려본다 (CI 밖에서도 된다)
|
|
159
|
+
node axmap/governance/gate.mjs --source <소스브랜치> --target <타깃브랜치>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`voter` 칸은 **정책 명단이 정한다.** `vote.mjs` 가 내 email 로 명단을 찾아 그 줄의
|
|
163
|
+
`id` 를 쓰고, `git config user.name`(**이 PC 의 git 에 설정된 내 이름**)과 다르면
|
|
164
|
+
바꾼다는 사실을 화면에 알린다. 왜 그렇게 됐는지는 이 문서 아래쪽 **"실측 기록"** 의
|
|
165
|
+
두 번째 항목에 있다 — **이 문서가 "사람을 가리키는 것은 email 이다" 라고 적어 둔 규칙을,
|
|
166
|
+
표를 쓰는 코드가 안 지키고 있었다.**
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 지키는 성질 — G1~G5
|
|
171
|
+
|
|
172
|
+
| | | 안 지키면 |
|
|
173
|
+
|---|---|---|
|
|
174
|
+
| **G1** | **자기 표는 세지 않는다** — MR 소스 브랜치 커밋의 author 인 사람의 표는 무효 | 혼자 쓰고 혼자 승인한다 |
|
|
175
|
+
| **G2** | **정책은 언제나 타깃 브랜치**(**합쳐 받는 쪽 브랜치**)**에서 읽는다** | 정족수를 1 로 낮추는 MR 이 **자기 자신의 낮춘 규칙으로** 통과한다 |
|
|
176
|
+
| **G3** | **표는 커밋(sha)에 묶인다** — 헤드가 바뀌면 효력을 잃는다 | 승인받은 뒤 내용을 갈아끼운다 |
|
|
177
|
+
| **G4** | **한 사람은 한 표** — id 유일, email 유일 | 명단에 자기를 두 줄 적으면 두 표다 |
|
|
178
|
+
| **G5** | **못 세면 통과가 아니다** — 판정 불가는 언제나 빨강 | 고장이 통과로 읽힌다 |
|
|
179
|
+
|
|
180
|
+
**G3 의 "효력을 잃는다" 는 "사라진다" 가 아니다.** 만료된 claim 과 같은 취급이다
|
|
181
|
+
([SPEC.md](../docs/SPEC.md) §5) — 레코드는 남고 효력만 잃는다. 그래서 출력에
|
|
182
|
+
`안 센 표` 로 **보여준다.** 지워버리면 "왜 갑자기 표가 없지" 를 아무도 못 푼다.
|
|
183
|
+
|
|
184
|
+
**G2 만 순수 판정 밖에 있다.** 어느 브랜치에서 정책을 읽어오는가는 git 을 만지는
|
|
185
|
+
일이라 게이트의 책임이다. `src/governance.mjs` 는 **읽어온 정책이 성립하는지**만 본다.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 종료 코드
|
|
190
|
+
|
|
191
|
+
종료 코드 — **프로그램이 끝나면서 내는 숫자. 0이면 성공, 그 외는 실패.**
|
|
192
|
+
[SPEC.md](../docs/SPEC.md) §8 의 관례를 잇는다.
|
|
193
|
+
|
|
194
|
+
| 코드 | 뜻 | 사람이 할 일 |
|
|
195
|
+
|---|---|---|
|
|
196
|
+
| **0** | 정족수 충족 | 머지 |
|
|
197
|
+
| **2** | 정족수 미달 | **정상적인 "아직 아니다".** 사람에게 투표를 요청한다 |
|
|
198
|
+
| **1** | 판정 불가 — 정책 못 읽음, 타깃 브랜치 모름, JSON 깨짐 | **환경을 고친다.** 투표로는 안 풀린다 |
|
|
199
|
+
| **4** | 정책 자체가 깨짐 — 계층 위반, email 중복, 문턱이 투표권자 수보다 큼 | 정책을 고친다 (그 MR 은 개정 문턱을 지난다) |
|
|
200
|
+
|
|
201
|
+
> 🔴 **2 와 1 을 가르는 것이 이 층의 핵심이다.**
|
|
202
|
+
> **2 는 "사람이 아직 안 눌렀다", 1 은 "내가 판정을 못 했다".**
|
|
203
|
+
> 둘을 뭉치면 AI 에이전트가 고장을 **기다리면 되는 일**로 읽고 영원히 기다린다.
|
|
204
|
+
> 이 저장소가 선점 프로토콜에서 `2`(겹침)와 `1`(환경)을 가른 것과 같은 이유다.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## 정책 — `policy.json`
|
|
209
|
+
|
|
210
|
+
### 자리 — 팀 저장소의 루트다. `axmap/` 안이 아니다
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
governance/policy.json ← 팀 저장소 루트. 여기가 정본이다
|
|
214
|
+
axmap/governance/policy.json ← 2026-08-26 까지 있던 옛 자리. 지금은 없다
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**도구는 나가고 데이터는 남는다.** axMap 은 곧 자기 저장소로 떨어져 나가고, 그때
|
|
218
|
+
팀 저장소에서 `axmap/` 폴더는 통째로 사라진다. 그런데 *"누가 투표권자인가"* 는
|
|
219
|
+
도구가 정하는 것이 아니라 **팀이 정하는 것**이다. 정책을 도구 폴더 안에 두면
|
|
220
|
+
도구가 나가는 날 팀의 합의 규칙이 같이 딸려 나가고, 그 순간 모든 MR 의 `governance`
|
|
221
|
+
잡이 정책을 못 찾아 종료 코드 1 로 죽는다 — `Pipelines must succeed`(**MR 을 머지하려면
|
|
222
|
+
CI 검사가 전부 초록이어야 한다는 설정**)가 켜져 있으면 **아무것도 머지되지 않는다.**
|
|
223
|
+
장부 브랜치(`axmap/claims`)와 표 브랜치(`axmap/votes`)를 팀 저장소에 남기는 것과
|
|
224
|
+
같은 이유다.
|
|
225
|
+
|
|
226
|
+
### 자리를 옮기려면
|
|
227
|
+
|
|
228
|
+
axMap 은 이제 범용 도구라 남의 저장소는 정책을 다른 자리에 둘 수 있다.
|
|
229
|
+
`gate.mjs` 와 `vote.mjs` 는 **이 순서**로 자리를 정한다.
|
|
230
|
+
|
|
231
|
+
| 순서 | | |
|
|
232
|
+
|---|---|---|
|
|
233
|
+
| 1 | `--policy <경로>` | 플래그가 가장 세다 (`--remote` 가 `AXMAP_REMOTE` 를 이기는 것과 같은 관례) |
|
|
234
|
+
| 2 | `AXMAP_POLICY_PATH` | 환경변수. CI 에서 한 번 정해 두기 좋다 |
|
|
235
|
+
| 3 | `governance/policy.json` | 기본값. `src/governance.mjs` 의 `DEFAULT_POLICY_PATH` |
|
|
236
|
+
|
|
237
|
+
경로는 **저장소 루트 기준**이다 — 게이트를 어느 폴더에서 돌리든 같은 파일을 읽는다.
|
|
238
|
+
|
|
239
|
+
> 🔴 **못 찾으면 다른 자리를 대신 뒤지지 않는다.** 옛 자리로 되돌아가는 폴백은
|
|
240
|
+
> 일부러 넣지 않았다. 두 자리를 다 보면 어느 것이 진짜 정책인지 아무도 모르게 되고,
|
|
241
|
+
> 이 저장소는 그것을 *"장부가 둘"* 로 한 번 아프게 배웠다. 못 찾으면 **종료 코드 1 로
|
|
242
|
+
> 멈추고 어디를 봤는지·어떻게 알려주는지를 말한다.** 그 성질은 `test/gate.test.mjs`
|
|
243
|
+
> 의 음성 테스트(**막아야 하는 것이 실제로 막히는지 보는 테스트**)로 고정돼 있다.
|
|
244
|
+
|
|
245
|
+
### 지금 들어 있는 내용
|
|
246
|
+
|
|
247
|
+
**지금 저장소에 실제로 들어 있는 파일이다.** 아래는 베낀 것이 아니라 그 내용이다 —
|
|
248
|
+
어긋나면 [`governance/policy.json`](../../governance/policy.json) 쪽이 기준이다.
|
|
249
|
+
|
|
250
|
+
```json
|
|
251
|
+
{
|
|
252
|
+
"version": 1,
|
|
253
|
+
|
|
254
|
+
"voters": [
|
|
255
|
+
{ "id": "rleaderjoon", "email": "rleaderjoon@gmail.com" },
|
|
256
|
+
{ "id": "masdf13", "email": "masdf13@naver.com" }
|
|
257
|
+
],
|
|
258
|
+
|
|
259
|
+
"amendment": {
|
|
260
|
+
"paths": ["governance", ".gitlab-ci.yml", "ci"],
|
|
261
|
+
"threshold": "majority"
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
"default": { "threshold": "majority" },
|
|
265
|
+
|
|
266
|
+
"rules": [],
|
|
267
|
+
|
|
268
|
+
"succession": { "window_days": 90, "top": 3 }
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
`amendment.paths` 에 셋이 들어 있는 이유는 전부 다르다.
|
|
273
|
+
|
|
274
|
+
| 경로 | 왜 개정 문턱인가 |
|
|
275
|
+
|---|---|
|
|
276
|
+
| `governance` | 폴더를 통째로 덮으므로 **정책 파일 자신이 덮인다.** 안 덮이면 `validatePolicy` 가 종료 코드 4 를 낸다 — 문턱을 낮추는 변경이 기본 문턱으로 통과하는 것을 막는 규칙이다 |
|
|
277
|
+
| `.gitlab-ci.yml` | **게이트 잡을 지우는 변경**이 기본 문턱으로 통과하면 안 된다 (지운 MR 자체를 막지는 못한다 — 위 "못 막는 것 셋") |
|
|
278
|
+
| `ci` | 🔴 `ci/axmap/` 은 **규칙을 실제로 집행하는 코드**다 (벤더링 — **다른 저장소의 코드를 복사해 와서 그 사본을 쓰는 것**). 사본을 고치는 것은 규칙을 고치는 것과 같다. `ci/verify-vendor.mjs`(사본이 원본과 같은지 확인하는 검사)도 같은 이유로 덮인다 |
|
|
279
|
+
|
|
280
|
+
### 문턱 — `"majority"` (과반)
|
|
281
|
+
|
|
282
|
+
`threshold` 자리에는 **1 이상의 정수**나 문자열 **`"majority"`** 를 적는다.
|
|
283
|
+
지금 정책은 기본과 개정 **둘 다 `"majority"`** 다. 계산식은 하나뿐이다.
|
|
284
|
+
|
|
285
|
+
> **문턱 = floor((정책 명단 − 이 MR 의 author) / 2) + 1**
|
|
286
|
+
|
|
287
|
+
계산은 `src/governance.mjs` 의 `majorityThreshold()` 가 하고, **코드가 정본이다.**
|
|
288
|
+
|
|
289
|
+
| | |
|
|
290
|
+
|---|---|
|
|
291
|
+
| **분모에서 작성자를 뺀다** | 자기 표는 어차피 안 세는데(G1) 분모에 남겨 두면 **투표권자가 둘일 때 과반이 2 라서 영원히 못 채운다** — 한 명은 작성자라 셀 수 없기 때문이다. 명단이 둘인 지금 팀에서는 첫날부터 막힌다 |
|
|
292
|
+
| **승계로 들어온 사람은 분모에 안 넣는다** | 넣으면 *모자라서 부른 사람*이 자기가 넘어야 할 문턱을 같이 올린다. 승계는 문턱을 **채우는 쪽**만 돕는다 |
|
|
293
|
+
| **최소 1** | 명단이 전부 작성자여도 0 으로 안 내려간다. **문턱 0 은 문턱이 아니라 이 층이 없는 것**이다. 그때는 승계로 들어온 남이 한 표를 줘야 통과한다 |
|
|
294
|
+
| **작성자를 모르면 판정 불가(1)** | 그럴듯한 숫자로 때우지 않는다. 분모가 틀린 문턱은 아무것도 지키지 않는다 (G5) |
|
|
295
|
+
| **계층 비교에서는 가장 높은 값** | 아래 계층 검사에서 `"majority"` 는 어떤 고정 숫자보다도 크게 친다. 명단이 커지면 실제로 그럴 수 있고, 검사는 fail-closed 여야 한다 |
|
|
296
|
+
|
|
297
|
+
지금 명단은 둘(`rleaderjoon` · `masdf13`)이므로, 둘 중 한 명이 쓴 MR 의 문턱은
|
|
298
|
+
**floor(1 / 2) + 1 = 1 표** — *"상대 한 명의 찬성"* 이다. 세 번째 사람이 명단에
|
|
299
|
+
들어오면 아무도 아무것도 안 고쳐도 문턱이 2 로 오른다. **고정 숫자 대신 과반을 고른
|
|
300
|
+
이유가 그것이다** (버린 대안은 [DECISIONS.md](../docs/DECISIONS.md) 의 D20).
|
|
301
|
+
|
|
302
|
+
출력에도 과반이라는 사실과 분모가 함께 찍힌다. 숫자만 보이면 왜 이 문턱인지 아무도
|
|
303
|
+
모르고, 명단이 하나 늘어난 날 그 숫자가 조용히 달라진다.
|
|
304
|
+
|
|
305
|
+
### 계층 — `개정 ≥ 기본 ≥ 파트별 규칙`
|
|
306
|
+
|
|
307
|
+
어기면 exit 4 다. 이유는 하나다.
|
|
308
|
+
|
|
309
|
+
> **기본값이 어떤 규칙보다 낮으면, 새 폴더를 만드는 것이 기존 폴더를 고치는 것보다
|
|
310
|
+
> 싸진다.** 규칙이 안 붙은 새 경로는 기본값으로 판정되기 때문이다.
|
|
311
|
+
> 그 순간 사람들은 고치지 않고 옆에 새로 만들기 시작한다.
|
|
312
|
+
|
|
313
|
+
`amendment.paths` 는 **반드시 `policy.json` 자신을 덮어야 한다.** 안 덮으면 exit 4 다.
|
|
314
|
+
안 덮으면 문턱을 1 로 낮추는 변경이 **기본 문턱**으로 통과하고, 이 층은 그날 끝난다.
|
|
315
|
+
|
|
316
|
+
규칙이 **여럿 걸리면 최댓값**을 쓴다(fail-closed — **판단이 갈리면 안전한 쪽으로
|
|
317
|
+
닫는다**). 최솟값이면 규칙을 하나 더 얹는 것이 문턱을 낮추는 수단이 된다.
|
|
318
|
+
|
|
319
|
+
경로 매칭은 새로 만들지 않았다. `src/protocol.mjs` 의 `coversPath` 를 그대로 쓴다 —
|
|
320
|
+
"규칙 경로가 이 파일을 덮는가" 는 pre-commit 훅(**커밋 직전에 자동으로 도는 검사**)이
|
|
321
|
+
"claim 이 이 파일을 덮는가" 를 묻는 것과 같은 질문이다. **한 저장소에 경로 규칙이
|
|
322
|
+
둘이면 어느 쪽이 맞는지 아무도 모르게 된다.**
|
|
323
|
+
|
|
324
|
+
### 승계 — `{ window_days: 90, top: 3 }`
|
|
325
|
+
|
|
326
|
+
살아 있는 투표권자가 필요 표보다 적으면, 최근 90일 안에 커밋한 author 상위 3명이
|
|
327
|
+
**임시 투표권**을 갖는다.
|
|
328
|
+
|
|
329
|
+
- 승계로 들어온 사람도 **자기 표는 못 센다** (G1 은 예외가 없다)
|
|
330
|
+
- 정책에 적힌 사람은 **살아 있지 않아도 투표권을 잃지 않는다.** 생사는 *승계를 켤지*만
|
|
331
|
+
정한다. 두 달 자리를 비운 사람이 돌아와 던진 표가 안 세지면 그건 fail-closed 가
|
|
332
|
+
아니라 그냥 틀린 판정이다
|
|
333
|
+
- 🔴 **승계 발동 사실은 반드시 출력에 드러난다.** 조용히 발동하는 승계는 승계가 아니라
|
|
334
|
+
우회로다. 그래서 그 줄을 지우면 테스트가 빨개지게 고정해 두었다
|
|
335
|
+
- 승계로도 못 채우면 **막힌 채로 둔다** (위 "못 막는 것 셋" 의 3번)
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## 지금 있는 것
|
|
340
|
+
|
|
341
|
+
**전부 있다.** 예전 이 자리에 있던 *"아직 없다"* 목록은 2026-08-26 에 다 채워졌다.
|
|
342
|
+
|
|
343
|
+
| | | |
|
|
344
|
+
|---|---|---|
|
|
345
|
+
| ✅ | `governance/GOVERNANCE.md` | 이 문서 |
|
|
346
|
+
| ✅ | `docs/DECISIONS.md` 의 D18 · D20 | 왜 파일에 기록하나(D18) · 왜 과반인가(D20). 버린 대안이 함께 있다 |
|
|
347
|
+
| ✅ | `src/governance.mjs` | **순수 판정** — git 도 fs 도 `Date.now()` 도 없다 |
|
|
348
|
+
| ✅ | `governance/policy.json` | 투표권자와 문턱. **팀 저장소 루트다** — `axmap/` 안이 아니다 (위 "자리"). 지금은 기본·개정 둘 다 `"majority"` |
|
|
349
|
+
| ✅ | `governance/gate.mjs` | git 에서 재료를 모아 판정을 부르는 쪽. G2(정책은 타깃 브랜치에서)가 여기 산다 |
|
|
350
|
+
| ✅ | `governance/vote.mjs` | 표를 쓰는 쪽 |
|
|
351
|
+
| ✅ | `.gitlab-ci.yml` 의 `governance` 잡 | MR 파이프라인에서만 돈다. **`GIT_DEPTH: 0` 이 필요하다** — 아래 |
|
|
352
|
+
| ✅ | 원격의 `axmap/votes` 브랜치 | 2026-08-26 에 실제로 생겼다 |
|
|
353
|
+
| ✅ | `test/governance.test.mjs` · `test/gate.test.mjs` | 앞은 순수 판정, 뒤는 게이트가 재료를 모으는 부분 |
|
|
354
|
+
|
|
355
|
+
🔴 **`GIT_DEPTH: 0`** — GitLab 은 기본으로 얕은 클론(**최근 몇 커밋만 받아오는 것**)을
|
|
356
|
+
한다. 게이트는 소스와 타깃의 **갈림점**(merge-base — **두 브랜치가 갈라진 지점**)에서
|
|
357
|
+
바뀐 파일을 뽑는데, 얕으면 그 지점이 받아온 범위 밖에 있어 **바뀐 파일 목록이 통째로
|
|
358
|
+
틀린다.** 그러면 문턱을 엉뚱한 규칙에서 고른다. 이 한 줄이 빠지면 판정이 조용히 틀린다.
|
|
359
|
+
|
|
360
|
+
**아직 없는 것은 사후 감사(`gate audit`)뿐이다** — 잠가 둔 브랜치의 머지 이력을 재생해
|
|
361
|
+
"이 머지는 그때 정족수를 채웠는가" 를 다시 계산하는 것(위 "못 막는 것 셋" 의 1번).
|
|
362
|
+
그리고 MCP(**AI 도구가 외부 기능을 붙여 쓰는 규약**)에는 아직 판정을 **읽어서 보여주는**
|
|
363
|
+
도구가 없다. 투표 도구는 앞으로도 안 만든다 — D18 의 버린 대안 2.
|
|
364
|
+
|
|
365
|
+
**판정을 순수 함수로 뺀 이유**는 이 저장소가 `src/protocol.mjs` 와 `bin/axmap.mjs` 를
|
|
366
|
+
가른 이유와 같다 — 판정을 검증하려고 저장소를 만들거나 승계 창 90일을 기다릴 수는 없다.
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## 실측 기록 — 2026-08-26
|
|
371
|
+
|
|
372
|
+
**저장소 실물과 실제 `policy.json` 로 끝까지 돌린 기록이다.** 손으로 만든 상황이 아니라
|
|
373
|
+
`feat/S15P21E201-999-quorum-test` 라는 실제 브랜치에 실제 표를 던져서 얻었다.
|
|
374
|
+
|
|
375
|
+
### ① 세 단계가 전부 예상대로 나왔다
|
|
376
|
+
|
|
377
|
+
명단은 둘, 작성자는 그중 한 명이므로 문턱은 과반으로 **1 표**다.
|
|
378
|
+
|
|
379
|
+
| 단계 | 종료 코드 | 출력 |
|
|
380
|
+
|---|---|---|
|
|
381
|
+
| 표 0장 | **2** | `합의 미달 — 유효 찬성 0 / 필요 1` |
|
|
382
|
+
| 표 1장 (작성자가 아닌 사람) | **0** | `합의 충족` |
|
|
383
|
+
| 그 뒤 커밋을 하나 더 올림 | **2** | 두 표 모두 `헤드가 바뀐 뒤라 효력 없음 (G3)` 으로 **보인 채** 안 세짐 |
|
|
384
|
+
|
|
385
|
+
세 번째 줄이 G3 의 *"효력을 잃는 것은 사라지는 것이 아니다"* 다. 표는 목록에 그대로
|
|
386
|
+
남아 있고 **왜 안 세졌는지가 화면에 적혀 있다.** 지워버렸다면 "어제 승인받았는데 왜
|
|
387
|
+
0표지" 를 아무도 못 풀었을 것이다.
|
|
388
|
+
|
|
389
|
+
실측용 표는 확인이 끝난 뒤 치웠다. `axmap/votes` 의 이력에는 던진 커밋과 치운 커밋이
|
|
390
|
+
**둘 다 남아 있다** — 표 브랜치에서 무엇을 지워도 지운 사실이 남는 것이 이 층의 요점이다.
|
|
391
|
+
|
|
392
|
+
### ② 🔴 표를 쓰는 코드가 이 문서의 규칙을 안 지키고 있었다
|
|
393
|
+
|
|
394
|
+
이 기록이 위 표보다 값지다.
|
|
395
|
+
|
|
396
|
+
`vote.mjs` 는 표의 `voter` 를 `git config user.name` 에서 가져왔다. 그런데 이 PC 의
|
|
397
|
+
`user.name` 은 **`janghyojoon`** 이고 정책 명단의 `id` 는 **`rleaderjoon`**(GitLab 계정
|
|
398
|
+
이름)이었다. **email 은 같았다.**
|
|
399
|
+
|
|
400
|
+
결과가 고약하다. 표는 **정상적으로 쓰였고** `axmap/votes` 에 push 까지 됐는데, 게이트는
|
|
401
|
+
`표의 이름과 email 이 명단과 어긋남` 으로 **그 표를 안 셌다.**
|
|
402
|
+
|
|
403
|
+
> **던진 사람은 던졌다고 믿는데, 판정에는 들어가지 않는다.**
|
|
404
|
+
> 실패가 조용하다 — 이 저장소가 락에서 가장 경계해 온 모양이다.
|
|
405
|
+
|
|
406
|
+
**고친 방식.** email 이 유일 키(G4)이므로, `vote.mjs` 가 **타깃 브랜치의 정책에서 email
|
|
407
|
+
로 명단을 찾아 그 줄의 `id` 를 쓴다.** 바꿀 때는 사람에게 알린다 —
|
|
408
|
+
`이름을 명단에 맞춥니다: janghyojoon → rleaderjoon`. 명단에 없으면(승계로 들어올 사람)
|
|
409
|
+
`user.name` 을 그대로 둔다. 게이트도 그 경우엔 `id` 를 대조하지 않기 때문이다.
|
|
410
|
+
|
|
411
|
+
**교훈은 규칙이 아니라 규칙이 사는 자리에 관한 것이다.**
|
|
412
|
+
|
|
413
|
+
> *사람을 가리키는 것은 이름이 아니라 email 이다* — 이 규칙은 이 문서에 이미 적혀
|
|
414
|
+
> 있었고(위 "표 한 장" 표의 `email` 칸), 판정 코드도 지키고 있었다.
|
|
415
|
+
> **표를 쓰는 쪽만 안 지켰다.**
|
|
416
|
+
> **규칙이 한 곳에만 적혀 있으면 다른 곳은 안 지킨다.**
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## 검증
|
|
421
|
+
|
|
422
|
+
```bash
|
|
423
|
+
npm --prefix axmap test
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
판정은 언제나 종료 코드다. **개수를 여기 적지 않는다** — 적어 두면 테스트를 늘릴
|
|
427
|
+
때마다 그 줄이 낡고, 낡은 기준은 없는 기준보다 나쁘다.
|
|
428
|
+
|
|
429
|
+
🔴 **음성 테스트**(**막아야 하는 것이 실제로 막히는지 보는 테스트**)**가 이 층의 계약이다.**
|
|
430
|
+
한 번도 실패하지 않는 검사기는 검사기가 아니다.
|
|
431
|
+
자기 표 · email 중복 · 계층 위반 · 개정이 자기를 제외 · sha 불일치 · 채울 수 없는 문턱 ·
|
|
432
|
+
과반 분모에서 작성자가 빠지는 것 · 승계 발동 — **이 목록이 빠지면 테스트를 지운 것이다.**
|
|
433
|
+
(개수를 적지 않는다. 하나 늘릴 때마다 그 숫자가 낡는다.)
|