axmap-cli 1.2.0 → 1.2.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/governance/GOVERNANCE.md +18 -0
- package/governance/vote.mjs +85 -12
- package/package.json +1 -1
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/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.1",
|
|
5
5
|
"//private": "🔴 여기 있던 \"private\": true 를 2026-08-28 에 지웠다. 그 줄이 있는 동안 npm publish 는 거부됐다. 되돌리려면 다시 넣으면 되지만, 이미 올라간 버전은 그래도 안 사라진다 — npm 은 같은 번호를 덮어쓰지 못하고 삭제도 72시간 안에만 된다. 즉 이 줄을 되살리는 것은 '앞으로 안 올린다' 는 뜻이지 '올린 것을 없앤다' 는 뜻이 아니다.",
|
|
6
6
|
"//publishConfig": "axmap-cli 는 스코프가 없어서 기본이 이미 공개다. 즉 지금은 없어도 된다. 그래도 남겨 두는 이유: 나중에 @조직/axmap 처럼 스코프를 붙이는 날 이 줄이 없으면 publish 가 '유료 플랜이 필요하다'는 엉뚱한 말로 실패한다.",
|
|
7
7
|
"publishConfig": {
|