commitgate 0.9.5 → 0.9.8
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/CHANGELOG.md +30 -0
- package/README.en.md +6 -0
- package/README.md +6 -0
- package/bin/init.ts +3 -2
- package/bin/sync.ts +143 -18
- package/package.json +1 -1
- package/scripts/req/lib/evidence-ports.ts +116 -0
- package/scripts/req/lib/evidence.ts +658 -0
- package/scripts/req/req-commit.ts +623 -806
- package/scripts/req/req-doctor.ts +76 -15
- package/scripts/req/req-new.ts +255 -252
- package/scripts/req/req-next.ts +813 -775
- package/scripts/req/review-codex.ts +2146 -2065
- package/skills/ATTRIBUTION.md +3 -1
- package/skills/commitgate-quality/SKILL.md +126 -0
- package/templates/CLAUDE.template.md +1 -0
- package/templates/workflow.gitignore +4 -0
|
@@ -12,7 +12,10 @@ import { resolve, join, relative } from 'node:path'
|
|
|
12
12
|
import { pathToFileURL } from 'node:url'
|
|
13
13
|
import { createHash } from 'node:crypto'
|
|
14
14
|
import { parseStatusZ, entryPaths, formatStatusEntry, STATUS_Z_ARGS, type StatusEntry } from './lib/porcelain'
|
|
15
|
-
import {
|
|
15
|
+
import { isAllowedResponsesScratch, reviewScratchPaths } from './lib/scratch'
|
|
16
|
+
// REQ-2026-048 phase-1: confinement 술어는 leaf `lib/evidence.ts`가 정본 — 여기서 재수출(기존 경로 보존).
|
|
17
|
+
import { isConfinedArchivePath } from './lib/evidence'
|
|
18
|
+
export { isConfinedArchivePath } from './lib/evidence'
|
|
16
19
|
import {
|
|
17
20
|
loadState,
|
|
18
21
|
validateVerdict,
|
|
@@ -20,6 +23,7 @@ import {
|
|
|
20
23
|
findUnstagedOrUntracked,
|
|
21
24
|
captureDesignBinding,
|
|
22
25
|
designDocPaths,
|
|
26
|
+
REVIEW_CALL_LOG_REL,
|
|
23
27
|
type WorkflowState,
|
|
24
28
|
type Verdict,
|
|
25
29
|
type ApprovalEvidence,
|
|
@@ -98,6 +102,13 @@ export interface DoctorInputs {
|
|
|
98
102
|
// undefined = 미계산(2-arg/legacy) → OK. [] = 없음/최신/대상없음 → OK. 비어있지 않음 → WARN.
|
|
99
103
|
// dev/dogfood(packageRootDiffers===false)면 D20처럼 skip. optional이어야 테스트 base 리터럴이 안 깨진다.
|
|
100
104
|
quickstartMissing?: string[]
|
|
105
|
+
// D22(REQ-2026-047): repo-root 런타임 스크래치 경로 중 **ignore도 tracked도 아닌** 것(= 다음 review가 만들면 D10이 막는 것).
|
|
106
|
+
// 런타임이 소비 repo 루트에 만드는 스크래치(현재 `workflow/.review-calls.jsonl`)는 티켓 밖이라 `/REQ-*/` 앵커에
|
|
107
|
+
// 걸리지 않고 `reviewScratchPaths` 허용목록에도 없다 → **gitignore가 유일한 방어**다. 0.9.6 이하 설치본은 배포
|
|
108
|
+
// 템플릿에 그 규칙이 없어, 첫 리뷰 뒤 D10 FAIL로 커밋이 전부 막혔다.
|
|
109
|
+
// undefined = 미계산(2-arg/legacy) → OK. [] = 전부 보호됨 → OK. 비어있지 않음 → WARN.
|
|
110
|
+
// dev/dogfood(packageRootDiffers===false)면 D20/D21처럼 skip. optional이어야 테스트 base 리터럴이 안 깨진다.
|
|
111
|
+
repoRootScratchUnprotected?: string[]
|
|
101
112
|
}
|
|
102
113
|
|
|
103
114
|
/**
|
|
@@ -190,20 +201,9 @@ export interface ArchiveCheck {
|
|
|
190
201
|
structureOk: boolean
|
|
191
202
|
}
|
|
192
203
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
*/
|
|
197
|
-
export function isConfinedArchivePath(p: string, ticketRel: string | undefined): boolean {
|
|
198
|
-
if (!ticketRel || typeof p !== 'string' || !p) return false
|
|
199
|
-
const norm = p.replace(/\\/g, '/')
|
|
200
|
-
if (norm.includes('..') || norm.startsWith('/') || /^[a-zA-Z]:\//.test(norm)) return false
|
|
201
|
-
const prefix = `${ticketRel.replace(/\\/g, '/').replace(/\/+$/, '')}/responses/`
|
|
202
|
-
if (!norm.startsWith(prefix)) return false
|
|
203
|
-
const name = norm.slice(prefix.length)
|
|
204
|
-
if (!name || name.includes('/')) return false
|
|
205
|
-
return isArchiveFileName(name)
|
|
206
|
-
}
|
|
204
|
+
// `isConfinedArchivePath`는 REQ-2026-048 phase-1에서 `lib/evidence.ts`로 이동했다 — 매니페스트 검증이
|
|
205
|
+
// 같은 술어를 쓰는데 여기 두면 `lib/evidence → req-doctor` 런타임 간선이 생겨 순환이 된다. 아래 re-export로
|
|
206
|
+
// 기존 import 경로(`from './req-doctor'`)를 보존한다.
|
|
207
207
|
|
|
208
208
|
/**
|
|
209
209
|
* 승인 증거(evidence)와 그 아카이브 파일의 정합 문제 목록(순수, A2/D-016-5).
|
|
@@ -482,6 +482,32 @@ export function runChecks(inp: DoctorInputs): Check[] {
|
|
|
482
482
|
})
|
|
483
483
|
}
|
|
484
484
|
|
|
485
|
+
// D22(REQ-2026-047): repo-root 런타임 스크래치가 ignore도 tracked도 아님 → 다음 review 뒤 D10이 커밋을 막는다.
|
|
486
|
+
//
|
|
487
|
+
// 🔴 **level 상한은 WARN — 절대 FAIL이 아니다**(D19:425-428·D20:443-447·D21과 동일 근거). `req:commit`이 이 doctor를
|
|
488
|
+
// exit≠0에 throw하는 하드 게이트로 spawn하므로, FAIL이면 백필 전까지 소비자의 모든 커밋이 벽돌이 된다.
|
|
489
|
+
// 더구나 이 드리프트는 **이미** D10 FAIL로 발현한다 — 신규 진단이 차단을 만드는 것이 아니라, 불투명한
|
|
490
|
+
// `D10: unstaged/untracked workflow/.review-calls.jsonl`을 **행동 가능한 안내로 번역**하는 것이 역할이다.
|
|
491
|
+
//
|
|
492
|
+
// tracked인 경우는 여기서 경고하지 않는다(이미 커밋된 상태 = 다른 문제). 그 복구는 `git rm --cached` 절차로
|
|
493
|
+
// troubleshooting 문서가 다룬다 — ignore 규칙만 넣어서는 tracked 파일이 빠지지 않기 때문이다.
|
|
494
|
+
if (inp.packageRootDiffers === false) {
|
|
495
|
+
c.push({ id: 'D22', level: 'OK', msg: 'repo-root 스크래치 보호 점검 불요(dev repo/dogfood — packageRoot === config root)' })
|
|
496
|
+
} else if (inp.repoRootScratchUnprotected === undefined) {
|
|
497
|
+
c.push({ id: 'D22', level: 'OK', msg: 'repo-root 스크래치 보호 점검 불요(2-arg/미계산)' })
|
|
498
|
+
} else if (inp.repoRootScratchUnprotected.length === 0) {
|
|
499
|
+
c.push({ id: 'D22', level: 'OK', msg: 'repo-root 런타임 스크래치가 모두 ignore(또는 tracked)됨' })
|
|
500
|
+
} else {
|
|
501
|
+
c.push({
|
|
502
|
+
id: 'D22',
|
|
503
|
+
level: 'WARN',
|
|
504
|
+
msg:
|
|
505
|
+
`${inp.repoRootScratchUnprotected.join(', ')} 이(가) gitignore로 무시되지 않습니다 — ` +
|
|
506
|
+
'다음 review가 이 파일을 만들면 **D10이 FAIL하여 커밋이 막힙니다**. ' +
|
|
507
|
+
'`commitgate sync --gitignore --apply` 로 배포 템플릿의 누락 규칙을 보강하세요(기존 행은 변경하지 않습니다, REQ-2026-047).',
|
|
508
|
+
})
|
|
509
|
+
}
|
|
510
|
+
|
|
485
511
|
return c
|
|
486
512
|
}
|
|
487
513
|
|
|
@@ -583,6 +609,38 @@ export function safeSha256(abs: string): string | null {
|
|
|
583
609
|
}
|
|
584
610
|
}
|
|
585
611
|
|
|
612
|
+
/**
|
|
613
|
+
* D22(REQ-2026-047): repo-root 런타임 스크래치 중 **ignore도 tracked도 아닌** 경로.
|
|
614
|
+
*
|
|
615
|
+
* 판정은 **로컬 git 상태 그대로**다(전역 excludes 포함) — D10이 보는 `git status`와 같은 기준이어야
|
|
616
|
+
* "다음 review 뒤 D10이 막는다"는 예측이 맞는다. 파일이 아직 없어도 `check-ignore`는 패턴 매칭이라
|
|
617
|
+
* 동작한다(그래서 **첫 리뷰 전에 미리** 경고할 수 있다 — 이 검사의 존재 이유).
|
|
618
|
+
*
|
|
619
|
+
* 읽기 전용 advisory라 어떤 오류도 삼킨다(조회 실패 = 보호됨으로 간주 → WARN 안 냄. fail-safe: 게이트를 막지 않는다).
|
|
620
|
+
*/
|
|
621
|
+
export function unprotectedRepoRootScratch(paths: readonly string[], gitFn: (a: string[]) => string): string[] {
|
|
622
|
+
const out: string[] = []
|
|
623
|
+
for (const p of paths) {
|
|
624
|
+
let ignored = false
|
|
625
|
+
let tracked = false
|
|
626
|
+
try {
|
|
627
|
+
gitFn(['check-ignore', '-q', '--', p])
|
|
628
|
+
ignored = true
|
|
629
|
+
} catch {
|
|
630
|
+
ignored = false
|
|
631
|
+
}
|
|
632
|
+
if (!ignored) {
|
|
633
|
+
try {
|
|
634
|
+
tracked = gitFn(['ls-files', '--', p]).trim() !== ''
|
|
635
|
+
} catch {
|
|
636
|
+
tracked = true // 조회 불가 → 보호됨으로 간주(경고하지 않음)
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (!ignored && !tracked) out.push(p)
|
|
640
|
+
}
|
|
641
|
+
return out
|
|
642
|
+
}
|
|
643
|
+
|
|
586
644
|
/** package.json의 version 문자열. 부재·파손 시 null(D20 WARN 메시지용 — 없어도 무해). */
|
|
587
645
|
function safeReadVersion(pkgAbs: string): string | null {
|
|
588
646
|
try {
|
|
@@ -699,6 +757,9 @@ export function main(argv: string[] = process.argv.slice(2)): void {
|
|
|
699
757
|
schemaPathIsDefault: cfg.schemaPathAbs === resolve(cfg.root, DEFAULTS.schemaPath), // 정규화 절대경로 비교(동치 상대경로 포함)
|
|
700
758
|
installedVersion: safeReadVersion(join(packageRoot(), 'package.json')),
|
|
701
759
|
quickstartMissing: missingQuickstartFiles(cfg.root),
|
|
760
|
+
// D22(REQ-2026-047): 현재 repo-root 런타임 스크래치 축은 review-call 측정 로그 1건.
|
|
761
|
+
// 새 축이 생기면 이 배열에 추가하고 packed-consumer smoke 단언도 함께 늘린다(docs 인벤토리 표의 유지 규칙).
|
|
762
|
+
repoRootScratchUnprotected: unprotectedRepoRootScratch([REVIEW_CALL_LOG_REL], git),
|
|
702
763
|
}
|
|
703
764
|
|
|
704
765
|
const checks = runChecks(inp)
|