commitgate 0.9.8 → 0.9.10

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.
@@ -28,7 +28,7 @@ import {
28
28
  type Verdict,
29
29
  type ApprovalEvidence,
30
30
  } from './review-codex'
31
- import { loadConfig, packageRoot, stripBom, DEFAULTS, type ResolvedConfig } from './lib/config'
31
+ import { loadConfig, packageRoot, stripBom, DEFAULTS, type ResolvedConfig, type PackageManager } from './lib/config'
32
32
  import { createGitAdapter, type GitAdapter } from './lib/adapters'
33
33
  import { missingQuickstartFiles } from '../../bin/quickstart'
34
34
 
@@ -109,6 +109,11 @@ export interface DoctorInputs {
109
109
  // undefined = 미계산(2-arg/legacy) → OK. [] = 전부 보호됨 → OK. 비어있지 않음 → WARN.
110
110
  // dev/dogfood(packageRootDiffers===false)면 D20/D21처럼 skip. optional이어야 테스트 base 리터럴이 안 깨진다.
111
111
  repoRootScratchUnprotected?: string[]
112
+ // D23(REQ-2026-056): frozen-lockfile 위생. 감지된 PM의 lockfile이 없거나 untracked면 재현 가능한 설치
113
+ // (`<pm> ci`/`--frozen-lockfile`)가 불가하다. undefined = 미계산(2-arg/legacy) → OK.
114
+ // 'no-package-json'/'ok' → OK. 'missing'/'untracked' → **WARN**(FAIL 아님 — D19~D22 근거 동일).
115
+ // optional이어야 테스트 base 리터럴이 안 깨진다.
116
+ lockfileStatus?: 'ok' | 'missing' | 'untracked' | 'no-package-json'
112
117
  }
113
118
 
114
119
  /**
@@ -508,6 +513,26 @@ export function runChecks(inp: DoctorInputs): Check[] {
508
513
  })
509
514
  }
510
515
 
516
+ // D23(REQ-2026-056): frozen-lockfile 위생 진단.
517
+ //
518
+ // 🔴 **level 상한은 WARN — 절대 FAIL이 아니다**(D19~D22와 동일 근거). `req:commit`이 doctor를 하드 게이트로
519
+ // spawn하므로 FAIL이면 lockfile 없는 프로젝트의 모든 커밋이 벽돌이 된다. lockfile ↔ package.json 동기
520
+ // 여부는 검사하지 않는다(PM 실행 없이 신뢰 불가) — 존재·tracked 위생만.
521
+ if (inp.lockfileStatus === undefined || inp.lockfileStatus === 'ok') {
522
+ c.push({ id: 'D23', level: 'OK', msg: inp.lockfileStatus === undefined ? 'lockfile 위생 점검 불요(미계산)' : 'lockfile 존재·git-tracked — 재현 가능한 설치(--frozen-lockfile) 가능' })
523
+ } else if (inp.lockfileStatus === 'no-package-json') {
524
+ c.push({ id: 'D23', level: 'OK', msg: 'lockfile 위생 점검 불요(package.json 없음)' })
525
+ } else {
526
+ c.push({
527
+ id: 'D23',
528
+ level: 'WARN',
529
+ msg:
530
+ inp.lockfileStatus === 'missing'
531
+ ? '감지된 패키지 매니저의 lockfile이 없습니다 — 재현 가능한 설치(`<pm> ci` / `--frozen-lockfile`)가 불가합니다. lockfile을 생성·커밋하세요.'
532
+ : 'lockfile이 git-tracked가 아닙니다(untracked) — 재현 가능한 설치가 불가합니다. lockfile을 커밋하세요.',
533
+ })
534
+ }
535
+
511
536
  return c
512
537
  }
513
538
 
@@ -651,6 +676,29 @@ function safeReadVersion(pkgAbs: string): string | null {
651
676
  }
652
677
  }
653
678
 
679
+ /** PM별 기대 lockfile 후보(npm은 둘 다 유효). */
680
+ const LOCKFILES_FOR_PM: Record<PackageManager, string[]> = {
681
+ npm: ['package-lock.json', 'npm-shrinkwrap.json'],
682
+ pnpm: ['pnpm-lock.yaml'],
683
+ yarn: ['yarn.lock'],
684
+ }
685
+
686
+ /**
687
+ * frozen-lockfile 위생 판정(D23·REQ-2026-056). package.json 없으면 점검 불요. PM 기대 lockfile이 하나라도
688
+ * 존재+tracked면 ok, 존재하나 전부 untracked면 untracked, 하나도 없으면 missing.
689
+ * 🔴 lockfile↔package.json **동기 검사는 안 한다**(PM 실행 없이 신뢰 불가) — 존재·tracked 위생만.
690
+ */
691
+ export function lockfileHygiene(
692
+ root: string,
693
+ pm: PackageManager,
694
+ isTracked: (rel: string) => boolean,
695
+ ): 'ok' | 'missing' | 'untracked' | 'no-package-json' {
696
+ if (!existsSync(join(root, 'package.json'))) return 'no-package-json'
697
+ const present = LOCKFILES_FOR_PM[pm].filter((f) => existsSync(join(root, f)))
698
+ if (present.length === 0) return 'missing'
699
+ return present.some((f) => isTracked(f)) ? 'ok' : 'untracked'
700
+ }
701
+
654
702
  export function main(argv: string[] = process.argv.slice(2)): void {
655
703
  const opts = parseArgs(argv)
656
704
  const cfg = loadConfig({ root: opts.root })
@@ -760,6 +808,15 @@ export function main(argv: string[] = process.argv.slice(2)): void {
760
808
  // D22(REQ-2026-047): 현재 repo-root 런타임 스크래치 축은 review-call 측정 로그 1건.
761
809
  // 새 축이 생기면 이 배열에 추가하고 packed-consumer smoke 단언도 함께 늘린다(docs 인벤토리 표의 유지 규칙).
762
810
  repoRootScratchUnprotected: unprotectedRepoRootScratch([REVIEW_CALL_LOG_REL], git),
811
+ // D23(REQ-2026-056): frozen-lockfile 위생(존재·tracked). tracked 판정은 read-only `git ls-files`.
812
+ lockfileStatus: lockfileHygiene(cfg.root, cfg.packageManager, (rel) => {
813
+ try {
814
+ git(['ls-files', '--error-unmatch', '--', rel])
815
+ return true
816
+ } catch {
817
+ return false
818
+ }
819
+ }),
763
820
  }
764
821
 
765
822
  const checks = runChecks(inp)