leerness 1.26.0 → 1.28.0
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 +72 -0
- package/README.md +4 -4
- package/bin/leerness.js +37 -5
- package/lib/audit.js +5 -0
- package/lib/catalogs.js +1 -1
- package/lib/drift.js +342 -341
- package/lib/pure-utils.js +2 -1
- package/package.json +1 -1
- package/scripts/e2e.js +74 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,77 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.28.0 — 2026-06-15 — 🛡️ [안정화/Stable] 정직성 후속 + drift 영어화 안정 minor
|
|
4
|
+
|
|
5
|
+
**🛡️ 안정화(Stable) minor — 13번째 외부리뷰 정직성 수정 + drift 진단 영어화를 npm 공개.** 직전 minor(1.27.0) 이후 누적된 패치 2건(1.27.1 + 1.27.2)을 검증·통합해 배포. R-0011 정책의 19번째 stable minor. 한국어 우선 기본은 그대로.
|
|
6
|
+
|
|
7
|
+
### 이번 minor 통합 (1.27.1~1.27.2)
|
|
8
|
+
- **🔎 정직성 후속 수정 (1.27.1)**: `audit <미초기화경로>` 가 "미초기화" 선언 후 없는 하네스에 design/reuse 체크를 보고하던 모순 출력 차단(요약/JSON 직행). `verify-claim --run-tests` 가 비-테스트 `--test-cmd`(exit 0, 미파싱)를 `✓ all passed` 로 거짓표기하던 것을 `✓ ran (exit 0) — test count unconfirmed` 로 정직 표기(판정/exit 불변 → FP=0).
|
|
9
|
+
- **🌐 drift check 출력 영어화 (1.27.2)**: `drift check` 기본 출력(경로/상태/신호 표/보안 신호/권장 조치)을 영어 opt-in. `--auto-fix` 진행 로그는 Phase 10b 백로그. 내부 호출(handoff/health)은 ko 기본이라 무영향.
|
|
10
|
+
- **한국어 우선 기본 보존**: 영어는 명시 opt-in. 한국어 출력/내부 JSON 은 그대로(e2e 무회귀).
|
|
11
|
+
|
|
12
|
+
### 잔여 (백로그)
|
|
13
|
+
- drift `--auto-fix` 로그(Phase 10b) · capabilities/commands/doctor/install-safety/constraints 영어화 · init en seed 템플릿 i18n.
|
|
14
|
+
|
|
15
|
+
### 검증 (회귀 0)
|
|
16
|
+
- **selftest 248/248** · **E2E 368/368** (정직성 후속 가드 + i18n 행위가드 lens/health/drift en/ko) · 게시본 클린룸 재실증.
|
|
17
|
+
- minor(1.28.0) — npm 배포(R-0011 stable) + annotated tag(Stable) + GitHub release(latest).
|
|
18
|
+
|
|
19
|
+
## 1.27.2 — 2026-06-15 — CLI 영어화 Phase 10: drift check 출력 영어화 (UR-0010)
|
|
20
|
+
|
|
21
|
+
**🌐 drift 진단 출력을 영어로.** 고빈도 진단 `drift check` 의 **기본 출력**(경로/상태/신호 표/보안 신호/권장 조치)을 영어 opt-in 으로. `--auto-fix` 진행 로그(~25줄)는 정직하게 Phase 10b 로 분리(반쪽 주장 회피).
|
|
22
|
+
|
|
23
|
+
### 변경 (UR-0010 Phase 10)
|
|
24
|
+
- **drift check 출력 영어화 (lib/drift.js, DI uiLang)**: 경로 없음 에러, 표 헤더(`신호/임계/가중치/발화`→`signal/threshold/weight/fired`), 신호 라벨 8종(`session close 누락`→`session close missing`, `current-state 갱신 없음`, `task update 없음`, `progress-tracker 비어있음`, `task-log 갱신 없음`, `보안 위험`→`security risk`, `Feature Graph 미정리`→`unlinked`, `task 0건 sub-app`), 보안 issue 2종, `권장 조치` 블록. `t(ko,en)`, ko 인자 verbatim.
|
|
25
|
+
- **내부 호출 무영향**: handoff/health 가 drift 를 내부 spawn(LEERNESS_INTERNAL, `--language` 없음)할 땐 ko 기본 라벨 — 한국어 출력/JSON 파싱 그대로(무회귀). 라벨은 `--json` 값도 언어 따름(en 시 영어).
|
|
26
|
+
|
|
27
|
+
### 잔여 (UR-0010 Phase 10b+, 백로그)
|
|
28
|
+
- drift `--auto-fix` 진행 로그(~25줄) + capabilities/commands/doctor/install-safety/constraints + init en seed 템플릿 i18n.
|
|
29
|
+
|
|
30
|
+
### 검증 (회귀 0)
|
|
31
|
+
- **selftest 247→248** (drift 영어/한국어 보존 + uiLang 주입 소스가드) · 행위(drift `--language en` 한글 0 / ko 보존 / 내부호출 ko 유지) · **E2E 368/368** (i18n 행위가드에 drift en/ko 추가).
|
|
32
|
+
- patch(1.27.2) — npm 미배포(R-0011, GitHub/CHANGELOG 누적).
|
|
33
|
+
|
|
34
|
+
## 1.27.1 — 2026-06-15 — 13번째 외부리뷰 정직성 후속: audit 미초기화 모순출력 + verify-claim no-parse 표기
|
|
35
|
+
|
|
36
|
+
**🔎 13번째 외부리뷰의 정직성 잔여 P2/P3 2건 수정(맹신 X 양방향 재현).** 둘 다 leerness 핵심 정체성(정직한 보고)을 직접 건드리는 출력 문제.
|
|
37
|
+
|
|
38
|
+
### 변경
|
|
39
|
+
- **audit 미초기화 경로 모순 출력 차단 (#2)**: `audit <미초기화경로>` 가 "미초기화" 를 선언한 직후 design/reuse 체크를 **없는 하네스에 대해** 보고하던 모순(예: "✓ no duplicate design guide candidates")을 차단 — 미초기화 감지 시 요약/JSON 으로 직행 후 종료. exit code(1)·`--json` 페이로드(not_initialized finding)는 종전과 동일, **정상 프로젝트 audit 은 무영향**(모든 체크 계속).
|
|
40
|
+
- **verify-claim --run-tests no-parse 정직 표기 (#3)**: 비-테스트 `--test-cmd`(예: `echo hi`)가 exit 0 이면서 테스트 비율을 못 파싱한 경우 `✓ all passed` 로 거짓표기하던 것을 `✓ ran (exit 0) — test count unconfirmed`(실행됨, 테스트 수 미확인)로 정직 표기. **메시지만 변경, 판정/exit 불변** → 출력 포맷이 다른 정상 테스트러너의 통과 주장을 거부하지 않음(FP=0). 진짜 N/N 테스트는 계속 `✓ all passed`, 실패(exit≠0)는 계속 `✗ FAIL`.
|
|
41
|
+
|
|
42
|
+
### 검증 (회귀 0)
|
|
43
|
+
- **selftest 246→247** (소스가드; 기존 1.9.421 "audit body=lib" 가드와 자기참조 충돌을 코멘트 앵커로 회피 — [[lesson-selftest-self-reference-trap]] 적용) · 행위(맹신 X 양방향) · **E2E 367→368** (정직성 후속 회귀가드 1건).
|
|
44
|
+
- patch(1.27.1) — npm 미배포(R-0011, GitHub/CHANGELOG 누적). 잔여(init en seed=대형 템플릿 i18n, Phase 10 진단 영어화)는 백로그.
|
|
45
|
+
|
|
46
|
+
## 1.27.0 — 2026-06-15 — 🛡️ [안정화/Stable] 보안 수정 안정 minor (개인키 스캔 FN + placeholder FP)
|
|
47
|
+
|
|
48
|
+
**🛡️ 안정화(Stable) minor — 13번째 외부리뷰에서 확인된 보안 수정을 조기 npm 공개.** 직전 minor(1.26.0) 이후 1.26.1 패치 1건이지만, **보안 FN/FP(거짓 "보안 OK" + CI 파손)는 패치 누적을 기다리기보다 조기 공개가 합리적**이라 단독 minor 로 게시. R-0011 정책의 18번째 stable minor.
|
|
49
|
+
|
|
50
|
+
### 이번 minor 통합 (1.26.1)
|
|
51
|
+
- **🔒 개인키 파일 스캔 FN 차단**: `scan secrets` 가 `.pem`/`.key`/`.crt`/`.p8`/`.pfx` 등을 확장자 allow-list 누락으로 건너뛰어 **커밋된 개인키 미탐 + handoff 가 "보안 OK" 거짓보증**하던 문제 수정(basename 오버라이드). gitignore 된 키는 종전대로 info.
|
|
52
|
+
- **🔒 DB placeholder 오탐(FP) 차단**: `.env.example` 의 `user:password@`·`root:root` 등 교과서 placeholder 를 커밋 시크릿으로 오탐해 `gate`/CI 를 깨뜨리던 문제 수정(valueGroup + placeholder 마커). 진짜 고엔트로피 비밀번호는 계속 탐지(FN=0).
|
|
53
|
+
- **🔧 retro --json NaN 계약**: 비숫자 `--days` 가 plain text 를 `--json` 소비자에게 흘리던 문제 → 숫자 가드 + 클램프(failJson 구조화).
|
|
54
|
+
|
|
55
|
+
### 잔여 (외부리뷰 백로그)
|
|
56
|
+
- init `--language en` seed 데이터 i18n(= 전체 `.harness/` 템플릿 i18n 필요, 대형) · verify-claim `--test-cmd` no-parse 하드닝(FP 회귀 위험으로 신중) · audit 미초기화 출력 정합(cosmetic) · 진단명령 영어화 Phase 10.
|
|
57
|
+
|
|
58
|
+
### 검증 (회귀 0)
|
|
59
|
+
- **selftest 246/246** · **E2E 367/367** (개인키 스캔 FN차단 + placeholder FP차단/FN유지 + retro --json 행위 회귀가드 포함) · 게시본 클린룸 재실증.
|
|
60
|
+
- minor(1.27.0) — npm 배포(R-0011 stable) + annotated tag(Stable) + GitHub release(latest).
|
|
61
|
+
|
|
62
|
+
## 1.26.1 — 2026-06-15 — 13번째 외부리뷰 P2 수정: 개인키파일 스캔 FN + DB placeholder FP + retro --json NaN
|
|
63
|
+
|
|
64
|
+
**🔎 13번째 외부 멀티모델 리뷰(1.26.0 게시본)에서 확인된 P2 3건 수정.** 3 에이전트 클린룸 리뷰 → 맹신 X 양방향 직접 재현으로 진짜만 채택 → 보안 2건 + --json 계약 1건 수정.
|
|
65
|
+
|
|
66
|
+
### 변경 (확인된 P2 3건)
|
|
67
|
+
- **🔒 개인키 파일 스캔 FN 차단 (보안)**: `scan secrets` 가 `.pem`/`.key`/`.crt`/`.p8`/`.pfx` 등 개인키·인증서 확장자를 스캔 allow-list 누락으로 건너뛰어 **커밋된 개인키를 미탐 + handoff 가 "보안 OK" 거짓보증**하던 문제 → basename 오버라이드(env-family 패턴 미러)로 강제 스캔. (gitignore 된 키는 종전대로 info 강등.)
|
|
68
|
+
- **🔒 DB placeholder 오탐(FP) 차단 (보안/CI)**: `.env.example` 의 `postgres://user:password@`·`root:root`·`yourpassword` 같은 교과서 placeholder 가 커밋 시크릿으로 오탐돼 `gate`/CI 를 깨뜨리던 문제 → DB URI 정규식에 비밀번호 capture group(`valueGroup`) 추가 + placeholder 마커(`root`/`admin`/`user`/`yourpassword` 등 전체-값 정확 일치)로 차단. **진짜 고엔트로피 비밀번호는 계속 탐지(FN=0)**.
|
|
69
|
+
- **🔧 retro --json NaN 크래시 (계약)**: `retro --days <비숫자>` 가 `new Date(Invalid)` throw 로 `--json` 소비자에게 plain text(`✗ Invalid time value`)를 흘리던 문제 → 숫자 가드 + 음수/오버플로 클램프(`failJson` 구조화, insights/round-history 와 일관).
|
|
70
|
+
|
|
71
|
+
### 검증 (회귀 0)
|
|
72
|
+
- **selftest 245→246** (소스가드) · 행위(맹신 X 양방향: 개인키 .key 탐지 + .crt 무오탐, placeholder 스킵 + 실비번 탐지, retro --json 구조화 + 정상동작) · **E2E 367/367** (신규 행위 회귀가드 1건).
|
|
73
|
+
- patch(1.26.1) — npm 미배포(R-0011, GitHub/CHANGELOG 누적). 잔여 리뷰 발견(audit 미초기화 출력 정합 P2, verify-claim --test-cmd no-parse 하드닝 P3, init en seed 데이터 i18n, 진단명령 영어화 Phase 10)은 백로그.
|
|
74
|
+
|
|
3
75
|
## 1.26.0 — 2026-06-15 — 🛡️ [안정화/Stable] i18n 행위가드 + health 진단 영어화 안정 minor
|
|
4
76
|
|
|
5
77
|
**🛡️ 안정화(Stable) minor — i18n 레이어 견고성 검증·가드 + health 진단 영어화를 npm 공개.** 직전 minor(1.25.0) 이후 누적된 패치 2건(1.25.1 + 1.25.2)을 검증·통합해 배포. R-0011 정책의 17번째 stable minor. 한국어 우선 기본은 그대로.
|
package/README.md
CHANGED
|
@@ -104,7 +104,7 @@ MIT
|
|
|
104
104
|
<!-- leerness:project-readme:start -->
|
|
105
105
|
## Leerness Project Harness
|
|
106
106
|
|
|
107
|
-
이 프로젝트는 Leerness v1.
|
|
107
|
+
이 프로젝트는 Leerness v1.28.0 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
108
108
|
|
|
109
109
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
110
110
|
|
|
@@ -158,7 +158,7 @@ leerness memory restore decision <date|title>
|
|
|
158
158
|
|
|
159
159
|
### MCP server (외부 AI 통합)
|
|
160
160
|
|
|
161
|
-
Leerness v1.
|
|
161
|
+
Leerness v1.28.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
|
|
162
162
|
|
|
163
163
|
```jsonc
|
|
164
164
|
// 카테고리별
|
|
@@ -179,7 +179,7 @@ Leerness v1.26.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code
|
|
|
179
179
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
180
180
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) stress-v* 신규 작성 + 누적 회귀 → 4) e2e 219/219 → 5) npm pack + git tag + GitHub release → 6) main 자동 push (1.9.140+) → 7) session close → 8) 다음 라운드 예약.
|
|
181
181
|
|
|
182
|
-
현재 누적: **70 라운드 (1.9.40 → 1.
|
|
182
|
+
현재 누적: **70 라운드 (1.9.40 → 1.28.0)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
183
183
|
|
|
184
184
|
### 성능 가이드 (1.9.140 측정)
|
|
185
185
|
|
|
@@ -217,6 +217,6 @@ leerness release pack --close --auto-main-push
|
|
|
217
217
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
218
218
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
219
219
|
|
|
220
|
-
Last synced by Leerness v1.
|
|
220
|
+
Last synced by Leerness v1.28.0: 2026-06-16
|
|
221
221
|
<!-- leerness:project-readme:end -->
|
|
222
222
|
|
package/bin/leerness.js
CHANGED
|
@@ -32,7 +32,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
|
|
|
32
32
|
// 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
|
|
33
33
|
const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST, _DEFAULT_PLATFORM_CONSTRAINTS, _DEFAULT_DOMAIN_CATALOG, _TOOL_CATALOG, _LSP_LANG_PATTERNS, OPTIMISM_PATTERNS, BUILT_IN_PERSONAS, STRINGS, BUILTIN_CATALOG, ROADMAP_STATUS_LABEL, ROADMAP_STATUS_COLOR, SECRET_PATTERNS, MERGE_OVERWRITE_FILES, MINIMAL_SKIP_KEYS, REQUIRED_WORKSPACE_FILES, KEYWORD_STOPWORDS, SKILL_CATALOG_PRESETS } = require('../lib/catalogs'); // 1.9.344/368/369 (UR-0025): catalog 분리 · 1.11.4 (UR-0007): _TOOL_CATALOG
|
|
34
34
|
|
|
35
|
-
const VERSION = '1.
|
|
35
|
+
const VERSION = '1.28.0';
|
|
36
36
|
|
|
37
37
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
38
38
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -3810,6 +3810,32 @@ function _selfTestCases() {
|
|
|
3810
3810
|
const renderEn = bin.includes('quality self-question lenses (v${VERSION})') && bin.includes("t('페르소나', 'persona')");
|
|
3811
3811
|
return enFields && koVerbatim && renderEn;
|
|
3812
3812
|
} },
|
|
3813
|
+
{ name: '13번째 외부리뷰 P2 수정 (1.26.1): 개인키파일 스캔 + DB placeholder valueGroup + retro NaN 가드 (소스 가드)', run: () => {
|
|
3814
|
+
const bin = read(__filename);
|
|
3815
|
+
const cat = read(path.join(path.dirname(__filename), '..', 'lib', 'catalogs.js'));
|
|
3816
|
+
const pu = read(path.join(path.dirname(__filename), '..', 'lib', 'pure-utils.js'));
|
|
3817
|
+
const keyFile = bin.includes('const isKeyFile =') && bin.includes('!isKeyFile) continue;');
|
|
3818
|
+
const dbVg = /DB connection string[^\n]*valueGroup: 1/.test(cat);
|
|
3819
|
+
const phPlaceholder = pu.includes('root|admin|user|username|yourpassword');
|
|
3820
|
+
const retroGuard = bin.includes("failJson(has('--json'), 'invalid_arg'") && bin.includes('Math.min(days, 36500)');
|
|
3821
|
+
return keyFile && dbVg && phPlaceholder && retroGuard;
|
|
3822
|
+
} },
|
|
3823
|
+
{ name: '13번째 외부리뷰 정직성 후속 (1.27.1): audit 미초기화 early-return + verify-claim no-parse 표기 (소스 가드)', run: () => {
|
|
3824
|
+
const bin = read(__filename);
|
|
3825
|
+
const aud = read(path.join(path.dirname(__filename), '..', 'lib', 'audit.js'));
|
|
3826
|
+
// 주의: bin 에 'not_'+'initialized' 리터럴을 두면 1.9.421('body 가 lib 로 이동') 가드가 깨짐 → 코멘트 텍스트로 앵커.
|
|
3827
|
+
const auditReturn = aud.includes('// 1.27.1 (13번째 외부리뷰 #2)') && /외부리뷰 #2\)[\s\S]{0,1200}?process\.exitCode = 1;\s*\n\s*return;/.test(aud);
|
|
3828
|
+
const vcMsg = bin.includes("test count unconfirmed") && bin.includes('runResult.parsed ? ');
|
|
3829
|
+
return auditReturn && vcMsg;
|
|
3830
|
+
} },
|
|
3831
|
+
{ name: 'CLI 영어화 Phase 10 (1.27.2, UR-0010): drift check 출력 영어/한국어 보존 + uiLang 주입 (소스 가드)', run: () => {
|
|
3832
|
+
const bin = read(__filename);
|
|
3833
|
+
const dr = read(path.join(path.dirname(__filename), '..', 'lib', 'drift.js'));
|
|
3834
|
+
const injected = bin.includes('uiLang: _uiLang(root), harnessPath: __filename, readProgressRows, planPath, handoffPath, currentStatePath, taskLogPath');
|
|
3835
|
+
const en = dr.includes('| signal | age | threshold | weight | fired |') && dr.includes('session close missing') && dr.includes('recommended actions') && dr.includes('security risk:');
|
|
3836
|
+
const koPreserved = dr.includes('| 신호 | age | 임계 | 가중치 | 발화 |') && dr.includes('session close 누락') && dr.includes('권장 조치'); // ko 인자 보존(e2e ko/내부호출)
|
|
3837
|
+
return injected && en && koPreserved;
|
|
3838
|
+
} },
|
|
3813
3839
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
|
|
3814
3840
|
];
|
|
3815
3841
|
}
|
|
@@ -7843,7 +7869,9 @@ function _collectSecretFindings(root) {
|
|
|
7843
7869
|
const ext = path.extname(file).toLowerCase();
|
|
7844
7870
|
// 1.9.386 (UR-0087): env-family(.env / .env.local / .env.production …) basename 강제 포함.
|
|
7845
7871
|
const isEnvFamily = /^\.env(\.|$)/.test(path.basename(file));
|
|
7846
|
-
|
|
7872
|
+
// 1.26.1 (13번째 외부리뷰 P2): 개인키/인증서 파일(.pem/.key/.crt/.p8 …)은 확장자 allow-list 에 없어 스캔 누락 → 커밋된 개인키 미탐 + handoff 'OK' 거짓보증. basename 으로 강제 포함('Generic private key' 정규식이 실제로 돌도록).
|
|
7873
|
+
const isKeyFile = /\.(?:pem|key|crt|cer|der|p8|p12|pfx|pkcs8|ppk|asc|gpg|keystore|jks)$/i.test(path.basename(file));
|
|
7874
|
+
if (!SCAN_TEXT_EXT.has(ext) && !isEnvFamily && !isKeyFile) continue;
|
|
7847
7875
|
let text;
|
|
7848
7876
|
// 1.12.5 (15th 버그헌트 P2, UR-0019): stat-before-read — 1MB 초과 파일은 읽지 않고 건너뜀(이전엔 read 후 검사라 대형 파일 통째 로드).
|
|
7849
7877
|
try { if (fs.statSync(file).size > 1024 * 1024) continue; } catch { continue; }
|
|
@@ -10371,7 +10399,8 @@ function verifyClaimCmd(root, taskId) {
|
|
|
10371
10399
|
// 1.17.4 (UR-0047): 측정 불가는 '통과' 가 아니라 '검증 미수행' — 이전엔 실측 0 인데 ✓ pass(실측≥주장) 모순 표기.
|
|
10372
10400
|
log(` - ${t('테스트 카운트', 'test count')}: ${declaredTestCount == null ? t('⊘ (주장 없음)', '⊘ (none claimed)') : !testMeasured ? t(`⊘ 측정 불가 — 주장 ${declaredTestCount}개 검증 미수행 (pass 아님)`, `⊘ not measurable — claimed ${declaredTestCount} not verified (not a pass)`) : testOk ? t('✓ pass (실측 ≥ 주장)', '✓ pass (measured ≥ claimed)') : t('⚠ 주장보다 적음', '⚠ fewer than claimed')}`);
|
|
10373
10401
|
if (runResult && !runResult.skipped) {
|
|
10374
|
-
|
|
10402
|
+
// 1.27.1 (13번째 외부리뷰 #3): exit 0 인데 테스트 비율을 못 파싱한 경우(예: 비-테스트 --test-cmd)를 '✓ all passed' 로 거짓표기하지 않음 — '실행됨, 테스트 수 미확인' 으로 정직 표기(판정/exit 불변 → 이색 테스트러너 FP 없음).
|
|
10403
|
+
log(` - ${runResult.cmd || 'npm test'} ${t('실행', 'run')}: ${runTestsOk ? (runResult.parsed ? '✓ all passed' : t('✓ 실행됨 (exit 0) — 테스트 수 미확인', '✓ ran (exit 0) — test count unconfirmed')) : '✗ FAIL'}`);
|
|
10375
10404
|
if (declaredPass) log(` - ${t('주장과 실행 결과 일치', 'claimed matches run')}: ${declaredPassMatchesActual ? '✓ pass' : t('⚠ 다름', '⚠ differs')}`);
|
|
10376
10405
|
}
|
|
10377
10406
|
// 1.11.2 (UR-0175): optimism+정직성 — done 주장은 기본 게이팅(claimsChecked). 완화: --lenient.
|
|
@@ -12509,7 +12538,10 @@ function retroCmd(root) {
|
|
|
12509
12538
|
if (has('--all-apps') || arg('--include', null)) {
|
|
12510
12539
|
return _retroWorkspace(root);
|
|
12511
12540
|
}
|
|
12512
|
-
|
|
12541
|
+
// 1.26.1 (13번째 외부리뷰 P2): 비숫자 --days → NaN → new Date(Invalid) throw 로 --json 소비자에 plain text 누출. 숫자 가드 + 음수/오버플로 클램프(insights/round-history 와 일관).
|
|
12542
|
+
let days = parseInt(arg('--days', '7'), 10);
|
|
12543
|
+
if (!Number.isFinite(days)) { failJson(has('--json'), 'invalid_arg', _uiLang(root) === 'en' ? '--days must be a number' : '--days 는 숫자여야 합니다'); return; }
|
|
12544
|
+
days = Math.max(0, Math.min(days, 36500));
|
|
12513
12545
|
const cutoff = new Date(Date.now() - days * 86400 * 1000).toISOString().slice(0, 10);
|
|
12514
12546
|
const agg = _retroAggregate(root);
|
|
12515
12547
|
// 1.9.16: --json
|
|
@@ -14898,7 +14930,7 @@ function autoUpdateInstall(root) {
|
|
|
14898
14930
|
// 1.9.37: drift detection — 메타파일 staleness 측정으로 "leerness 점점 안 쓰는" 현상 감지
|
|
14899
14931
|
const _drift = require('../lib/drift');
|
|
14900
14932
|
// 1.9.422 (UR-0025/UR-0125 큰 핸들러 모듈화 7번째): driftCheckCmd → lib/drift.js (DI 위임, thin wrapper)
|
|
14901
|
-
function driftCheckCmd(root, opts = {}) { return _drift.driftCheckCmd(root, opts, { VERSION, has, arg, harnessPath: __filename, readProgressRows, planPath, handoffPath, currentStatePath, taskLogPath, envDiff, _usageStatsPath, _readUsageStats, _updateUserRequest, _scanShellScriptsEncoding, _readFeatureGraph, _detectDeliveredRequests, _autoFixIdempotency }); }
|
|
14933
|
+
function driftCheckCmd(root, opts = {}) { return _drift.driftCheckCmd(root, opts, { VERSION, has, arg, uiLang: _uiLang(root), harnessPath: __filename, readProgressRows, planPath, handoffPath, currentStatePath, taskLogPath, envDiff, _usageStatsPath, _readUsageStats, _updateUserRequest, _scanShellScriptsEncoding, _readFeatureGraph, _detectDeliveredRequests, _autoFixIdempotency }); }
|
|
14902
14934
|
|
|
14903
14935
|
// 1.9.69: skill-suggestions.md rolling history 인덱스 — mtime 기반 캐시
|
|
14904
14936
|
// handoff에서 같은 키워드 과거 추천 결과를 즉시 노출 (재매칭 불필요)
|
package/lib/audit.js
CHANGED
|
@@ -26,6 +26,11 @@ function audit(root, opts = {}, deps = {}) {
|
|
|
26
26
|
failures++;
|
|
27
27
|
fail(`미초기화 또는 존재하지 않는 경로: ${root} (.harness/AGENTS.md 없음 — leerness init 필요)`);
|
|
28
28
|
_finding('not_initialized', 'fail', 'uninitialized or missing path (.harness or AGENTS.md absent)', { root });
|
|
29
|
+
// 1.27.1 (13번째 외부리뷰 #2): 미초기화 시 후속 체크(design/reuse 등)를 없는 하네스에 대해 보고하던 모순 출력 차단 — 요약/JSON 으로 직행 후 종료(exit code/JSON 페이로드는 종전과 동일).
|
|
30
|
+
log(`Audit summary: warnings=${warnings} failures=${failures}`);
|
|
31
|
+
if (jsonMode) { process.stdout.write = _origWrite; process.stdout.write(JSON.stringify({ version: VERSION, root, warnings, failures, fixed, healthy: false, fixApplied: fix, strict: has('--strict'), strictThreshold: has('--strict') ? parseInt(arg('--threshold', '1'), 10) : null, summary: `warnings=${warnings} failures=${failures}`, findings }, null, 2) + '\n'); }
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
return;
|
|
29
34
|
}
|
|
30
35
|
const designCands = ['designguide.md','design-guide.md','docs/designguide.md','docs/design-guide.md','.harness/designguide.md'];
|
|
31
36
|
const dups = designCands.filter(f => exists(path.join(root,f)));
|
package/lib/catalogs.js
CHANGED
|
@@ -383,7 +383,7 @@ const SECRET_PATTERNS = [
|
|
|
383
383
|
// 1.9.350 (UR-0060 외부리뷰 3모델): 누락 패턴 보강
|
|
384
384
|
{ name: 'GitLab PAT', re: /\bglpat-[A-Za-z0-9_-]{20,}\b/g },
|
|
385
385
|
{ name: 'JWT', re: /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
|
|
386
|
-
{ name: 'DB connection string (embedded password)', re: /\b(?:postgres|postgresql|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/[^:\s/@]+:[^@\s/]
|
|
386
|
+
{ name: 'DB connection string (embedded password)', re: /\b(?:postgres|postgresql|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/[^:\s/@]+:([^@\s/]+)@/gi, valueGroup: 1 }, // 1.26.1 (13번째 외부리뷰 P2): valueGroup=비밀번호 → .env.example 의 placeholder(user:password 등) 오탐 차단
|
|
387
387
|
{ name: 'SendGrid API key', re: /\bSG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}\b/g },
|
|
388
388
|
{ name: 'AWS Secret Access Key (context)', re: /\baws[^\n]{0,40}?(?:secret_access_key|secret_key|secret)[^\n]{0,12}?["']?[A-Za-z0-9/+]{40}["']?/gi },
|
|
389
389
|
{ name: 'Hardcoded Bearer token', re: /\bBearer\s+[A-Za-z0-9_\-.=]{20,}/g },
|
package/lib/drift.js
CHANGED
|
@@ -1,341 +1,342 @@
|
|
|
1
|
-
// lib/drift.js — drift check 핸들러 (UR-0025/UR-0125 큰 핸들러 모듈화 7번째, 1.9.422)
|
|
2
|
-
// bin/leerness.js 에서 driftCheckCmd(322줄) 분리. DI: harness 고유 의존(VERSION, has, arg, harnessPath, readProgressRows, planPath, handoffPath, currentStatePath, _usageStatsPath, _readUsageStats, _updateUserRequest, _scanShellScriptsEncoding, _readFeatureGraph, _detectDeliveredRequests, _autoFixIdempotency) 주입.
|
|
3
|
-
// io 프리미티브는 ./io, cp/path 빌트인. 내부 재귀(auto-fix 후 재검사)는 deps 전달. 동작/출력 무변경.
|
|
4
|
-
'use strict';
|
|
5
|
-
const cp = require('child_process');
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const fs = require('fs');
|
|
8
|
-
const { log, ok, warn, fail, failJson, today, now, absRoot, exists, read, readBuf, mkdirp, writeUtf8, append, rel } = require('./io');
|
|
9
|
-
|
|
10
|
-
function driftCheckCmd(root, opts = {}, deps = {}) {
|
|
11
|
-
const { VERSION, has, arg, harnessPath, readProgressRows, planPath, handoffPath, currentStatePath, taskLogPath, envDiff, _usageStatsPath, _readUsageStats, _updateUserRequest, _scanShellScriptsEncoding, _readFeatureGraph, _detectDeliveredRequests, _autoFixIdempotency } = deps;
|
|
12
|
-
root = absRoot(root || process.cwd());
|
|
13
|
-
// 1.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
else if (totalScore >=
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
stats
|
|
155
|
-
stats.drift
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
// 1.9.
|
|
163
|
-
// 1.9.
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
afLog(
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
afLog(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
afLog(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
afLog(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
afLog(
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
afLog(
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
afLog(
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
stats
|
|
295
|
-
stats.drift
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
afLog(
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
log(
|
|
316
|
-
log(
|
|
317
|
-
log(
|
|
318
|
-
log(
|
|
319
|
-
log(
|
|
320
|
-
log(
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
log(
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
log(
|
|
333
|
-
log(
|
|
334
|
-
log(` -
|
|
335
|
-
log(` -
|
|
336
|
-
log(` -
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
1
|
+
// lib/drift.js — drift check 핸들러 (UR-0025/UR-0125 큰 핸들러 모듈화 7번째, 1.9.422)
|
|
2
|
+
// bin/leerness.js 에서 driftCheckCmd(322줄) 분리. DI: harness 고유 의존(VERSION, has, arg, harnessPath, readProgressRows, planPath, handoffPath, currentStatePath, _usageStatsPath, _readUsageStats, _updateUserRequest, _scanShellScriptsEncoding, _readFeatureGraph, _detectDeliveredRequests, _autoFixIdempotency) 주입.
|
|
3
|
+
// io 프리미티브는 ./io, cp/path 빌트인. 내부 재귀(auto-fix 후 재검사)는 deps 전달. 동작/출력 무변경.
|
|
4
|
+
'use strict';
|
|
5
|
+
const cp = require('child_process');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const { log, ok, warn, fail, failJson, today, now, absRoot, exists, read, readBuf, mkdirp, writeUtf8, append, rel } = require('./io');
|
|
9
|
+
|
|
10
|
+
function driftCheckCmd(root, opts = {}, deps = {}) {
|
|
11
|
+
const { VERSION, has, arg, uiLang, harnessPath, readProgressRows, planPath, handoffPath, currentStatePath, taskLogPath, envDiff, _usageStatsPath, _readUsageStats, _updateUserRequest, _scanShellScriptsEncoding, _readFeatureGraph, _detectDeliveredRequests, _autoFixIdempotency } = deps;
|
|
12
|
+
root = absRoot(root || process.cwd());
|
|
13
|
+
const t = (ko, en) => (uiLang === 'en' ? en : ko); // 1.27.2 (UR-0010 Phase 10): drift 출력 영어 opt-in (--auto-fix 진행로그는 Phase 10b)
|
|
14
|
+
// 1.9.434 (11th 외부평가 Opus P2, UR-0136): 미존재 경로는 healthy 위조 금지 — failJson + exit 1.
|
|
15
|
+
if (!exists(root)) { failJson(has('--json'), 'path_not_found', t(`경로 없음: ${root}`, `path not found: ${root}`)); return; }
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
const _ageDays = (p) => {
|
|
18
|
+
if (!exists(p)) return null;
|
|
19
|
+
return (now - fs.statSync(p).mtimeMs) / 86400000;
|
|
20
|
+
};
|
|
21
|
+
// 각 메타파일의 마지막 갱신
|
|
22
|
+
const signals = [];
|
|
23
|
+
// 1. session-handoff.md - "Last generated" 라인 우선, 없으면 mtime
|
|
24
|
+
const shPath = handoffPath(root);
|
|
25
|
+
if (exists(shPath)) {
|
|
26
|
+
const txt = read(shPath);
|
|
27
|
+
// 1.9.316 (drift 마커 버그): 최신(마지막) 'Last generated' 사용 — 구 블록 중복 시 첫(구) 매치를 읽던 오발화 방어.
|
|
28
|
+
const allGen = [...txt.matchAll(/Last generated:\s*([\d\-T:.Z]+)/g)];
|
|
29
|
+
const m = allGen.length ? allGen[allGen.length - 1] : null;
|
|
30
|
+
let ageDays;
|
|
31
|
+
if (m) {
|
|
32
|
+
ageDays = (now - new Date(m[1]).getTime()) / 86400000;
|
|
33
|
+
} else {
|
|
34
|
+
ageDays = _ageDays(shPath);
|
|
35
|
+
}
|
|
36
|
+
signals.push({ file: 'session-handoff.md', ageDays, threshold: 1, weight: 30, label: t('session close 누락', 'session close missing') });
|
|
37
|
+
}
|
|
38
|
+
// 2. current-state.md - "Updated: YYYY-MM-DD" 라인
|
|
39
|
+
const csPath = currentStatePath(root);
|
|
40
|
+
if (exists(csPath)) {
|
|
41
|
+
const m = read(csPath).match(/Updated:\s*(\d{4}-\d{2}-\d{2})/);
|
|
42
|
+
const ageDays = m ? (now - new Date(m[1]).getTime()) / 86400000 : _ageDays(csPath);
|
|
43
|
+
signals.push({ file: 'current-state.md', ageDays, threshold: 2, weight: 20, label: t('current-state 갱신 없음', 'current-state not updated') });
|
|
44
|
+
}
|
|
45
|
+
// 3. progress-tracker.md 마지막 row의 updated 컬럼
|
|
46
|
+
const rows = readProgressRows(root);
|
|
47
|
+
if (rows.length) {
|
|
48
|
+
const dates = rows.map(r => (r.updated || '').match(/\d{4}-\d{2}-\d{2}/)).filter(Boolean).map(m => m[0]);
|
|
49
|
+
if (dates.length) {
|
|
50
|
+
dates.sort();
|
|
51
|
+
const latest = dates[dates.length - 1];
|
|
52
|
+
const ageDays = (now - new Date(latest).getTime()) / 86400000;
|
|
53
|
+
signals.push({ file: 'progress-tracker.md', ageDays, threshold: 1, weight: 30, label: t('task update 없음', 'no task update') });
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
signals.push({ file: 'progress-tracker.md', ageDays: 999, threshold: 1, weight: 25, label: t('progress-tracker 비어있음', 'progress-tracker empty') });
|
|
57
|
+
}
|
|
58
|
+
// 4. task-log.md 마지막 entry "## YYYY-MM-DD"
|
|
59
|
+
const tlPath = taskLogPath(root);
|
|
60
|
+
if (exists(tlPath)) {
|
|
61
|
+
const dates = Array.from(read(tlPath).matchAll(/^## (\d{4}-\d{2}-\d{2})/gm)).map(m => m[1]);
|
|
62
|
+
if (dates.length) {
|
|
63
|
+
dates.sort();
|
|
64
|
+
const latest = dates[dates.length - 1];
|
|
65
|
+
const ageDays = (now - new Date(latest).getTime()) / 86400000;
|
|
66
|
+
signals.push({ file: 'task-log.md', ageDays, threshold: 2, weight: 20, label: t('task-log 갱신 없음', 'task-log not updated') });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// 점수 계산
|
|
70
|
+
let totalScore = 0;
|
|
71
|
+
const fired = [];
|
|
72
|
+
for (const s of signals) {
|
|
73
|
+
if (s.ageDays > s.threshold) {
|
|
74
|
+
totalScore += s.weight;
|
|
75
|
+
fired.push(s);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// 1.9.78: 보안 신호 (env / .gitignore 누락) — 5번째 신호
|
|
79
|
+
try {
|
|
80
|
+
const envPath = path.join(root, '.env');
|
|
81
|
+
if (exists(envPath)) {
|
|
82
|
+
let secScore = 0;
|
|
83
|
+
const secIssues = [];
|
|
84
|
+
// (a) .env vs .env.example 동기화
|
|
85
|
+
try {
|
|
86
|
+
const d = envDiff(root);
|
|
87
|
+
if (d.inEnvOnly.length) {
|
|
88
|
+
secIssues.push(t(`.env→.env.example 누락 ${d.inEnvOnly.length}건`, `.env→.env.example missing ${d.inEnvOnly.length}`));
|
|
89
|
+
secScore += 15;
|
|
90
|
+
}
|
|
91
|
+
} catch {}
|
|
92
|
+
// (b) .gitignore 시크릿 패턴
|
|
93
|
+
try {
|
|
94
|
+
const giText = exists(path.join(root, '.gitignore')) ? read(path.join(root, '.gitignore')) : '';
|
|
95
|
+
const giLines = giText.split('\n').map(l => l.trim());
|
|
96
|
+
const SECRET_PATTERNS = ['.env', '.env.local', '.env.production', '.env.*.local', '*.pem', 'credentials.json'];
|
|
97
|
+
const missing = SECRET_PATTERNS.filter(p => !giLines.some(l => l === p || l === '/' + p));
|
|
98
|
+
if (missing.length) {
|
|
99
|
+
secIssues.push(t(`.gitignore 시크릿 누락 ${missing.length}건`, `.gitignore missing secret patterns ${missing.length}`));
|
|
100
|
+
// 누락이 .env 자체면 최우선 위험 — 15점 가중
|
|
101
|
+
if (missing.includes('.env')) secScore += 30;
|
|
102
|
+
else secScore += Math.min(20, missing.length * 5);
|
|
103
|
+
}
|
|
104
|
+
} catch {}
|
|
105
|
+
if (secScore > 0) {
|
|
106
|
+
totalScore += secScore;
|
|
107
|
+
fired.push({ file: '.env / .gitignore', ageDays: null, threshold: 0, weight: secScore, label: t(`보안 위험 (1.9.78): ${secIssues.join(' · ')}`, `security risk: ${secIssues.join(' · ')}`) });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch {}
|
|
111
|
+
// 1.9.143: Feature Graph 미사용 신호 — 노드는 있는데 edges 비율 낮으면 인과관계 정리 미진
|
|
112
|
+
try {
|
|
113
|
+
const { nodes: fGraphNodes } = _readFeatureGraph(root);
|
|
114
|
+
if (fGraphNodes.length >= 3) {
|
|
115
|
+
const edgeCount = fGraphNodes.reduce((s, n) => s + (n.dependsOn?.length || 0) + (n.affects?.length || 0) + (n.coChangesWith?.length || 0), 0);
|
|
116
|
+
const linkedSet = new Set();
|
|
117
|
+
for (const n of fGraphNodes) {
|
|
118
|
+
for (const x of [...(n.dependsOn||[]), ...(n.affects||[]), ...(n.coChangesWith||[])]) { linkedSet.add(n.id); linkedSet.add(x); }
|
|
119
|
+
}
|
|
120
|
+
const isolatedCount = Math.max(0, fGraphNodes.length - linkedSet.size);
|
|
121
|
+
const isolatedRatio = isolatedCount / fGraphNodes.length;
|
|
122
|
+
if (edgeCount === 0 || isolatedRatio >= 0.5) {
|
|
123
|
+
const fgScore = edgeCount === 0 ? 25 : 15;
|
|
124
|
+
totalScore += fgScore;
|
|
125
|
+
fired.push({ file: '.harness/feature-graph.md', ageDays: null, threshold: 0, weight: fgScore, label: t(`Feature Graph 미정리 (1.9.143): ${fGraphNodes.length} 노드, edges=${edgeCount}, isolated=${isolatedCount}`, `Feature Graph unlinked: ${fGraphNodes.length} nodes, edges=${edgeCount}, isolated=${isolatedCount}`) });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch {}
|
|
129
|
+
// 신규 _apps/* 에서 task 0건도 신호로
|
|
130
|
+
const appsDir = path.join(root, '_apps');
|
|
131
|
+
let appsZeroTask = [];
|
|
132
|
+
if (exists(appsDir)) {
|
|
133
|
+
for (const d of fs.readdirSync(appsDir)) {
|
|
134
|
+
const sub = path.join(appsDir, d);
|
|
135
|
+
if (!exists(path.join(sub, '.harness'))) continue;
|
|
136
|
+
const subRows = readProgressRows(sub);
|
|
137
|
+
if (!subRows.length) appsZeroTask.push(d);
|
|
138
|
+
}
|
|
139
|
+
if (appsZeroTask.length) {
|
|
140
|
+
const w = Math.min(50, appsZeroTask.length * 10);
|
|
141
|
+
totalScore += w;
|
|
142
|
+
fired.push({ file: `_apps/* (${appsZeroTask.length}개)`, ageDays: null, threshold: 0, weight: w, label: t(`task 0건 sub-app: ${appsZeroTask.slice(0, 3).join(', ')}${appsZeroTask.length > 3 ? '...' : ''}`, `sub-app with 0 tasks: ${appsZeroTask.slice(0, 3).join(', ')}${appsZeroTask.length > 3 ? '...' : ''}`) });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// 레벨 판정
|
|
146
|
+
let level = '🟢 healthy';
|
|
147
|
+
if (totalScore >= 100) level = '🔴 critical';
|
|
148
|
+
else if (totalScore >= 50) level = '🟡 warning';
|
|
149
|
+
else if (totalScore >= 20) level = '🟠 attention';
|
|
150
|
+
|
|
151
|
+
// 1.9.38 (D): drift critical 등급은 누적 카운트 (학습 신호)
|
|
152
|
+
try {
|
|
153
|
+
if (level === '🔴 critical') {
|
|
154
|
+
const stats = _readUsageStats(root);
|
|
155
|
+
stats.drift = stats.drift || {};
|
|
156
|
+
stats.drift.criticalSeen = (stats.drift.criticalSeen || 0) + 1;
|
|
157
|
+
const p = _usageStatsPath(root);
|
|
158
|
+
mkdirp(path.dirname(p));
|
|
159
|
+
writeUtf8(p, JSON.stringify(stats, null, 2) + '\n');
|
|
160
|
+
}
|
|
161
|
+
} catch {}
|
|
162
|
+
// 1.9.39: --auto-fix — critical 시 session close 자동 실행
|
|
163
|
+
// 1.9.82: --auto-fix가 보안 신호도 자동 회복 (audit --fix 호출)
|
|
164
|
+
// 1.9.432 (10th 외부평가 Opus latent, UR-0131 잔여): depth 가드 — 재귀 호출(_noAutoFix)은 auto-fix 재진입 금지.
|
|
165
|
+
// 기존엔 autoFix=has('--auto-fix')가 전역 argv 재독→재귀도 auto-fix 분기 재진입, 종료는 'audit이 보안신호를 지운다'는 취약 불변식에 의존(미래 신호 타입이 비가역이면 무한재귀). 명시 1회 보장.
|
|
166
|
+
const autoFix = has('--auto-fix') && !opts._noAutoFix;
|
|
167
|
+
// 1.9.439 (10th 외부평가 Codex P1, UR-0135): --json 모드면 auto-fix 진행로그 억제(stdout 순수 JSON 보장).
|
|
168
|
+
// 재귀(_noAutoFix)는 auto-fix 블록을 건너뛰고 마지막 JSON(아래 has('--json') 블록)만 출력 → afLog 로 첫 패스 진행로그만 무음화.
|
|
169
|
+
const afLog = has('--json') ? () => {} : log;
|
|
170
|
+
// 1.9.82: 보안 신호가 fired에 있으면 우선 audit --fix 호출
|
|
171
|
+
const hasSecurityFired = fired.some(f => /보안 위험 \(1\.9\.78\)/.test(f.label));
|
|
172
|
+
if (autoFix && hasSecurityFired) {
|
|
173
|
+
afLog('');
|
|
174
|
+
afLog(`🔒 --auto-fix 활성 (1.9.82) — 보안 신호 회복: audit --fix 자동 실행 중...`);
|
|
175
|
+
try {
|
|
176
|
+
const r = cp.spawnSync(process.execPath, [harnessPath, 'audit', root, '--fix'],
|
|
177
|
+
{ encoding: 'utf8', timeout: 30000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' } });
|
|
178
|
+
if (r.status === 0) {
|
|
179
|
+
afLog(`✓ audit --fix 완료 — .gitignore + .env.example 동기화`);
|
|
180
|
+
// 재검사 (보안 신호 회복 확인)
|
|
181
|
+
afLog('');
|
|
182
|
+
afLog(`재검사 중...`);
|
|
183
|
+
return driftCheckCmd(root, { ...opts, _noAutoFix: true }, deps); // 재귀 1회 (auto-fix 없이, 1.9.432 depth 가드)
|
|
184
|
+
} else {
|
|
185
|
+
afLog(`⚠ audit --fix 실패 (exit ${r.status}) — 수동 \`leerness audit --fix\` 권장`);
|
|
186
|
+
}
|
|
187
|
+
} catch (e) {
|
|
188
|
+
afLog(`⚠ auto-fix 보안 회복 오류: ${e.message}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// 1.9.242: drift check --auto-fix 에 env encoding BOM 자동 추가 통합 (사용자 명시 UR-0014 2단계)
|
|
192
|
+
// 1.9.82 패턴 확장 — drift 회복 시 셸 스크립트 인코딩 위험도 자동 해결
|
|
193
|
+
if (autoFix) {
|
|
194
|
+
try {
|
|
195
|
+
const encScan = _scanShellScriptsEncoding(root);
|
|
196
|
+
if (encScan.atRisk && encScan.atRisk.length > 0) {
|
|
197
|
+
afLog('');
|
|
198
|
+
afLog(`🌐 --auto-fix 활성 (1.9.242) — 셸 스크립트 인코딩 위험 ${encScan.atRisk.length}건 BOM 자동 추가 중...`);
|
|
199
|
+
let ok = 0;
|
|
200
|
+
for (const r of encScan.atRisk) {
|
|
201
|
+
try {
|
|
202
|
+
const fullPath = path.join(root, r.file);
|
|
203
|
+
const orig = fs.readFileSync(fullPath);
|
|
204
|
+
const bom = Buffer.from([0xEF, 0xBB, 0xBF]);
|
|
205
|
+
const fixed = Buffer.concat([bom, orig]);
|
|
206
|
+
fs.writeFileSync(fullPath, fixed);
|
|
207
|
+
ok++;
|
|
208
|
+
} catch {}
|
|
209
|
+
}
|
|
210
|
+
afLog(`✓ UTF-8 BOM 추가 ${ok}/${encScan.atRisk.length}건 (1.9.242 UR-0014)`);
|
|
211
|
+
}
|
|
212
|
+
} catch (e) {
|
|
213
|
+
afLog(`⚠ env encoding auto-fix 오류 (1.9.242): ${e.message}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// 1.9.225: drift check --auto-fix 에 delivered 패턴 자동 적용 통합 (1.9.223/224 시스템 회수)
|
|
217
|
+
// 사용자 요청에 "구현 완료" 패턴이 누적되면 가짜 미답 신호가 drift score 를 가중시킬 수 있음 → 자동 정리.
|
|
218
|
+
// 1.9.82 audit --fix 패턴과 동일: --auto-fix 시 즉시 적용, 적용 후 재검사.
|
|
219
|
+
if (autoFix) {
|
|
220
|
+
try {
|
|
221
|
+
const delivered = _detectDeliveredRequests(root);
|
|
222
|
+
if (delivered.candidates && delivered.candidates.length > 0) {
|
|
223
|
+
afLog('');
|
|
224
|
+
afLog(`📥 --auto-fix 활성 (1.9.225) — delivered 패턴 ${delivered.candidates.length}건 자동 완료 중...`);
|
|
225
|
+
let ok = 0;
|
|
226
|
+
for (const c of delivered.candidates) {
|
|
227
|
+
const u = _updateUserRequest(root, c.id, { status: 'completed', autoCompletedAt: new Date().toISOString(), autoCompleteReason: 'drift-auto-fix-1.9.225' });
|
|
228
|
+
if (u) ok++;
|
|
229
|
+
}
|
|
230
|
+
afLog(`✓ delivered 자동 완료 ${ok}/${delivered.candidates.length}건`);
|
|
231
|
+
}
|
|
232
|
+
} catch (e) {
|
|
233
|
+
afLog(`⚠ delivered auto-apply 오류 (1.9.225): ${e.message}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// 1.9.293: drift check --auto-fix 에 idempotency task/user-request 중복 자동 정리 통합
|
|
237
|
+
// 누적 중복 task/요청이 idempotency 위반(medium)을 가중 → drift/handoff 노이즈. 안전: 완전중복 행 제거 + 동일텍스트 dropped 보존(id 유지).
|
|
238
|
+
if (autoFix) {
|
|
239
|
+
try {
|
|
240
|
+
const idemFixes = _autoFixIdempotency(root);
|
|
241
|
+
const totalFixed = idemFixes.reduce((n, f) => n + (f.removedExact || 0) + (f.droppedSameText || 0) + (f.count || 0), 0);
|
|
242
|
+
if (totalFixed > 0) {
|
|
243
|
+
afLog('');
|
|
244
|
+
afLog(`🔁 --auto-fix 활성 (1.9.293) — idempotency 중복 ${totalFixed}건 자동 정리 (task/user-request dedup)`);
|
|
245
|
+
}
|
|
246
|
+
} catch (e) {
|
|
247
|
+
afLog(`⚠ idempotency auto-fix 오류 (1.9.293): ${e.message}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// 1.9.236: drift check --auto-fix 에 release cleanup 통합 (1.9.235 회수)
|
|
251
|
+
// 누적된 50개+ release/* branches → abnormal-shutdown release-branch-pending 신호 가중
|
|
252
|
+
// 안전: keep 10 (최근 10개 유지), merged 만 삭제 (1.9.235 안전 가드)
|
|
253
|
+
// 임계: 50개 초과 시만 자동 정리 (소량 누적은 정상 운영)
|
|
254
|
+
if (autoFix) {
|
|
255
|
+
try {
|
|
256
|
+
const branchR = cp.spawnSync('git', ['branch', '--merged', 'main', '--list', 'release/*'], { cwd: root, encoding: 'utf8' });
|
|
257
|
+
if (branchR.status === 0) {
|
|
258
|
+
const merged = (branchR.stdout || '').split('\n')
|
|
259
|
+
.map(l => l.replace(/^\*?\s+/, '').trim())
|
|
260
|
+
.filter(l => l && /^release\/\d+\.\d+\.\d+$/.test(l));
|
|
261
|
+
if (merged.length > 50) {
|
|
262
|
+
afLog('');
|
|
263
|
+
afLog(`🗑 --auto-fix 활성 (1.9.236) — release/* merged ${merged.length}개 (50+) 자동 정리 (keep 10)...`);
|
|
264
|
+
// 정렬 (semver desc)
|
|
265
|
+
merged.sort((a, b) => {
|
|
266
|
+
const va = a.replace('release/', '').split('.').map(n => parseInt(n, 10) || 0);
|
|
267
|
+
const vb = b.replace('release/', '').split('.').map(n => parseInt(n, 10) || 0);
|
|
268
|
+
for (let i = 0; i < 3; i++) if (va[i] !== vb[i]) return vb[i] - va[i];
|
|
269
|
+
return 0;
|
|
270
|
+
});
|
|
271
|
+
const currentBranchR = cp.spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: root, encoding: 'utf8' });
|
|
272
|
+
const currentBranch = (currentBranchR.stdout || '').trim();
|
|
273
|
+
const toDelete = merged.slice(10).filter(b => b !== currentBranch);
|
|
274
|
+
let ok = 0;
|
|
275
|
+
for (const b of toDelete) {
|
|
276
|
+
const r = cp.spawnSync('git', ['branch', '-d', b], { cwd: root, encoding: 'utf8' });
|
|
277
|
+
if (r.status === 0) ok++;
|
|
278
|
+
}
|
|
279
|
+
afLog(`✓ release cleanup 자동 완료 ${ok}/${toDelete.length}건 (keep 10)`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
} catch (e) {
|
|
283
|
+
afLog(`⚠ release cleanup auto-fix 오류 (1.9.236): ${e.message}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (autoFix && level === '🔴 critical' && !hasSecurityFired) {
|
|
287
|
+
afLog('');
|
|
288
|
+
afLog(`🔧 --auto-fix 활성 — session close 자동 실행 중...`);
|
|
289
|
+
try {
|
|
290
|
+
const r = cp.spawnSync(process.execPath, [harnessPath, 'session', 'close', root], { encoding: 'utf8', timeout: 60000, env: { ...process.env, LEERNESS_INTERNAL: '1' } });
|
|
291
|
+
if (r.status === 0) {
|
|
292
|
+
afLog(`✓ session close 자동 완료`);
|
|
293
|
+
// autoResolved 카운트
|
|
294
|
+
const stats = _readUsageStats(root);
|
|
295
|
+
stats.drift = stats.drift || {};
|
|
296
|
+
stats.drift.autoResolved = (stats.drift.autoResolved || 0) + 1;
|
|
297
|
+
const p = _usageStatsPath(root);
|
|
298
|
+
mkdirp(path.dirname(p));
|
|
299
|
+
writeUtf8(p, JSON.stringify(stats, null, 2) + '\n');
|
|
300
|
+
// 재검사
|
|
301
|
+
afLog('');
|
|
302
|
+
afLog(`재검사 중...`);
|
|
303
|
+
return driftCheckCmd(root, { ...opts, _noAutoFix: true }, deps); // 재귀 1회 (auto-fix 없이, 1.9.432 depth 가드)
|
|
304
|
+
} else {
|
|
305
|
+
afLog(`⚠ session close 실패 (exit ${r.status}) — 수동 실행 필요`);
|
|
306
|
+
}
|
|
307
|
+
} catch (e) {
|
|
308
|
+
afLog(`⚠ auto-fix 오류: ${e.message}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (has('--json')) {
|
|
312
|
+
log(JSON.stringify({ root, score: totalScore, level, signals, fired, appsZeroTask }, null, 2));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
log(`# leerness drift check (1.9.37)`);
|
|
316
|
+
log(t(`경로: ${root}`, `path: ${root}`));
|
|
317
|
+
log('');
|
|
318
|
+
log(t(`상태: ${level} · 점수 ${totalScore}/200`, `status: ${level} · score ${totalScore}/200`));
|
|
319
|
+
log('');
|
|
320
|
+
log(t(`| 신호 | age | 임계 | 가중치 | 발화 |`, `| signal | age | threshold | weight | fired |`));
|
|
321
|
+
log(`|---|---:|---:|---:|---|`);
|
|
322
|
+
for (const s of signals) {
|
|
323
|
+
const fire = s.ageDays > s.threshold ? '🔥' : '✓';
|
|
324
|
+
const age = s.ageDays === null ? '-' : `${s.ageDays.toFixed(1)}d`;
|
|
325
|
+
log(`| ${s.label} | ${age} | ${s.threshold}d | ${s.weight} | ${fire} |`);
|
|
326
|
+
}
|
|
327
|
+
if (appsZeroTask.length) {
|
|
328
|
+
log('');
|
|
329
|
+
log(t(`task 0건 sub-app (${appsZeroTask.length}개): ${appsZeroTask.join(', ')}`, `sub-apps with 0 tasks (${appsZeroTask.length}): ${appsZeroTask.join(', ')}`));
|
|
330
|
+
}
|
|
331
|
+
if (totalScore >= 50) {
|
|
332
|
+
log('');
|
|
333
|
+
log(t(`💡 권장 조치:`, `💡 recommended actions:`));
|
|
334
|
+
log(t(` - 즉시: leerness session close . (handoff/current-state 갱신)`, ` - now: leerness session close . (refresh handoff/current-state)`));
|
|
335
|
+
log(t(` - 또는: leerness audit . --fix (자동 갱신 가능 항목 적용)`, ` - or: leerness audit . --fix (apply auto-fixable items)`));
|
|
336
|
+
log(t(` - sub-app에 task 등록: cd _apps/X && leerness task add "..."`, ` - add tasks to a sub-app: cd _apps/X && leerness task add "..."`));
|
|
337
|
+
log(t(` - 이 검사 끄기: --no-drift-check 또는 LEERNESS_NO_DRIFT_CHECK=1`, ` - disable this check: --no-drift-check or LEERNESS_NO_DRIFT_CHECK=1`));
|
|
338
|
+
}
|
|
339
|
+
if (level === '🔴 critical') process.exitCode = 1;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
module.exports = { driftCheckCmd };
|
package/lib/pure-utils.js
CHANGED
|
@@ -642,7 +642,8 @@ function _isPlaceholderSecret(value) {
|
|
|
642
642
|
let v = String(value).trim().replace(/^["']|["']$/g, '').trim().toLowerCase();
|
|
643
643
|
if (!v) return true;
|
|
644
644
|
// 전체가 placeholder 토큰
|
|
645
|
-
|
|
645
|
+
// 1.26.1 (13번째 외부리뷰 P2): DB URI 등 placeholder 자격증명(user:password / root:root / yourpassword …) 추가 — 전체-값 정확 일치만 매칭하므로 실키(길고 고엔트로피)에는 FN 영향 0.
|
|
646
|
+
if (/^(?:x{3,}|\*{3,}|\.{3,}|-+|0+|1234567890?|12345678|abc123|secret|password|passwd|pass|changeme|change[-_]me|changeit|replace[-_]?me|placeholder|example|examples?|sample|dummy|test|testing|foo|bar|baz|tbd|todo|none|null|undefined|nil|empty|redacted|hidden|value|string|here|root|admin|user|username|yourpassword|your[-_]?password|mypassword)$/.test(v)) return true;
|
|
646
647
|
// 1.9.405 (8번째 버그헌트 회귀수정, UR-0109): placeholder 단어 신호를 entropy 가드보다 먼저 검사.
|
|
647
648
|
// 1.9.401 회귀: 긴 서술형 placeholder('your-super-secret-api-key-example-value')가 고엔트로피(영숫자24+ & 고유12+)를 넘어 실키로 오탐(FP).
|
|
648
649
|
// → placeholder 마커 단어가 있으면 entropy 가드 무시하고 placeholder 로 판정. 실키 prefix(sk-/AKIA 등)는 마커보다 우선(FN 방지).
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -6230,10 +6230,82 @@ total++;
|
|
|
6230
6230
|
const hEn = out(cp.spawnSync(process.execPath, [CLI, 'health', '--language', 'en', '--path', d], { encoding: 'utf8', timeout: 20000 }));
|
|
6231
6231
|
const hKo = out(cp.spawnSync(process.execPath, [CLI, 'health', '--path', d], { encoding: 'utf8', timeout: 20000 }));
|
|
6232
6232
|
const healthOk = /## Security/.test(hEn) && !H.test(hEn) && /## 보안/.test(hKo);
|
|
6233
|
+
// ⑥ (1.27.2 Phase 10) drift check 출력: en 영어(한글 0, --auto-fix 제외) + ko 기본 한글 보존
|
|
6234
|
+
const drEn = out(cp.spawnSync(process.execPath, [CLI, 'drift', 'check', d, '--language', 'en'], { encoding: 'utf8', timeout: 20000 }));
|
|
6235
|
+
const drKo = out(cp.spawnSync(process.execPath, [CLI, 'drift', 'check', d], { encoding: 'utf8', timeout: 20000 }));
|
|
6236
|
+
const driftOk = /signal \| age \| threshold/.test(drEn) && !H.test(drEn) && /신호 \| age \| 임계/.test(drKo);
|
|
6233
6237
|
fs.rmSync(d, { recursive: true, force: true });
|
|
6234
|
-
ok = lensKoOk && lensEnOk && noLeak && stOk && healthOk;
|
|
6238
|
+
ok = lensKoOk && lensEnOk && noLeak && stOk && healthOk && driftOk;
|
|
6235
6239
|
} catch {}
|
|
6236
|
-
console.log(ok ? '✓ B(1.25.1/1.25.2) i18n 행위: --language en 런타임 영어(lens/health) + ko 기본 보존 + --language positional 무누출 + status 에러 en/ko (UR-0010)' : '✗ i18n 행위 회귀 가드 실패');
|
|
6240
|
+
console.log(ok ? '✓ B(1.25.1/1.25.2/1.27.2) i18n 행위: --language en 런타임 영어(lens/health/drift) + ko 기본 보존 + --language positional 무누출 + status 에러 en/ko (UR-0010)' : '✗ i18n 행위 회귀 가드 실패');
|
|
6241
|
+
if (!ok) failed++;
|
|
6242
|
+
}
|
|
6243
|
+
|
|
6244
|
+
// 1.26.1 (13번째 외부리뷰 P2 회귀가드): 개인키파일 스캔 FN + DB placeholder FP + retro --json NaN 행위 가드.
|
|
6245
|
+
total++;
|
|
6246
|
+
{
|
|
6247
|
+
let ok = false;
|
|
6248
|
+
try {
|
|
6249
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-rev13-'));
|
|
6250
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes'], { encoding: 'utf8', timeout: 30000 });
|
|
6251
|
+
const out = (r) => (r.stdout || '') + (r.stderr || '');
|
|
6252
|
+
// #4: 커밋된 개인키 파일(.key, gitignore 미포함)은 잡혀야 함(FN 차단)
|
|
6253
|
+
fs.writeFileSync(path.join(d, 'server.key'), '-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA1234567890abcdefghij\n-----END RSA PRIVATE KEY-----\n');
|
|
6254
|
+
const keyScan = cp.spawnSync(process.execPath, [CLI, 'scan', 'secrets', d], { encoding: 'utf8', timeout: 15000 });
|
|
6255
|
+
const keyCaught = keyScan.status === 1 && /Generic private key/.test(out(keyScan));
|
|
6256
|
+
fs.unlinkSync(path.join(d, 'server.key'));
|
|
6257
|
+
// #5: .env.example 의 placeholder DB URI 는 오탐 X (FP 차단) / 진짜 비번은 잡힘(FN 유지)
|
|
6258
|
+
fs.writeFileSync(path.join(d, '.env.example'), 'A=postgres://user:password@h:5432/db\nB=mysql://root:root@h/db\n');
|
|
6259
|
+
const phScan = cp.spawnSync(process.execPath, [CLI, 'scan', 'secrets', d], { encoding: 'utf8', timeout: 15000 });
|
|
6260
|
+
const noFp = phScan.status === 0 && !/DB connection string/.test(out(phScan));
|
|
6261
|
+
fs.unlinkSync(path.join(d, '.env.example'));
|
|
6262
|
+
fs.writeFileSync(path.join(d, 'real.env'), 'D=postgres://admin:Xk9zQ2mP7rL4wT@prod.example.com:5432/main\n');
|
|
6263
|
+
const realScan = cp.spawnSync(process.execPath, [CLI, 'scan', 'secrets', d], { encoding: 'utf8', timeout: 15000 });
|
|
6264
|
+
const realCaught = realScan.status === 1 && /DB connection string/.test(out(realScan));
|
|
6265
|
+
fs.unlinkSync(path.join(d, 'real.env'));
|
|
6266
|
+
// #1: retro --days 비숫자 --json 은 구조화 JSON(plain text 누출 X)
|
|
6267
|
+
const rj = cp.spawnSync(process.execPath, [CLI, 'retro', d, '--days', 'xyz', '--json'], { encoding: 'utf8', timeout: 15000 });
|
|
6268
|
+
let retroJsonOk = false;
|
|
6269
|
+
try { const j = JSON.parse(rj.stdout); retroJsonOk = j && (j.error || j.code) && rj.status === 1; } catch {}
|
|
6270
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
6271
|
+
ok = keyCaught && noFp && realCaught && retroJsonOk;
|
|
6272
|
+
} catch {}
|
|
6273
|
+
console.log(ok ? '✓ B(1.26.1) 13th 외부리뷰: 개인키파일 스캔(FN차단) + DB placeholder(FP차단/FN유지) + retro --json NaN 구조화' : '✗ 13th 외부리뷰 P2 회귀가드 실패');
|
|
6274
|
+
if (!ok) failed++;
|
|
6275
|
+
}
|
|
6276
|
+
|
|
6277
|
+
// 1.27.1 (13번째 외부리뷰 정직성 후속 회귀가드): audit 미초기화 모순출력 차단 + verify-claim no-parse 정직표기 (양방향 무회귀).
|
|
6278
|
+
total++;
|
|
6279
|
+
{
|
|
6280
|
+
let ok = false;
|
|
6281
|
+
try {
|
|
6282
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-rev13b-'));
|
|
6283
|
+
const out = (r) => (r.stdout || '') + (r.stderr || '');
|
|
6284
|
+
// #2 audit 미초기화: design/reuse 모순 출력 없이 요약 직행 + exit 1 + --json not_initialized
|
|
6285
|
+
fs.mkdirSync(path.join(d, 'uninit'));
|
|
6286
|
+
const au = cp.spawnSync(process.execPath, [CLI, 'audit', path.join(d, 'uninit')], { encoding: 'utf8', timeout: 15000 });
|
|
6287
|
+
const auClean = au.status === 1 && !/design guide|reuse-map/.test(out(au)) && /Audit summary/.test(out(au));
|
|
6288
|
+
const auj = cp.spawnSync(process.execPath, [CLI, 'audit', path.join(d, 'uninit'), '--json'], { encoding: 'utf8', timeout: 15000 });
|
|
6289
|
+
let aujOk = false; try { const j = JSON.parse(auj.stdout); aujOk = j.healthy === false && (j.findings || []).some(f => f.kind === ('not_' + 'initialized')); } catch {}
|
|
6290
|
+
// #2 회귀: 정상 프로젝트 audit 는 체크 계속 수행
|
|
6291
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes'], { encoding: 'utf8', timeout: 30000 });
|
|
6292
|
+
const auReal = out(cp.spawnSync(process.execPath, [CLI, 'audit', d], { encoding: 'utf8', timeout: 15000 }));
|
|
6293
|
+
const auRealOk = /Audit summary/.test(auReal) && /gitignore|design|reuse/.test(auReal);
|
|
6294
|
+
// #3 verify-claim 비-테스트 --test-cmd → 거짓 'all passed' 아님(정직 표기)
|
|
6295
|
+
fs.mkdirSync(path.join(d, 'src'), { recursive: true });
|
|
6296
|
+
fs.writeFileSync(path.join(d, 'src', 'x.js'), 'module.exports={};\n');
|
|
6297
|
+
fs.writeFileSync(path.join(d, 'x.test.js'), 'test();\n');
|
|
6298
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'add', 'x', '--path', d], { encoding: 'utf8', timeout: 15000 });
|
|
6299
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'update', 'T-0002', '--status', 'done', '--evidence', 'src/x.js implemented, x.test.js added', '--path', d], { encoding: 'utf8', timeout: 15000 });
|
|
6300
|
+
const vcNon = out(cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0002', '--run-tests', '--test-cmd', 'echo hi', '--path', d], { encoding: 'utf8', timeout: 20000 }));
|
|
6301
|
+
const vcNonOk = /미확인|unconfirmed/.test(vcNon) && !/echo hi.*all passed/.test(vcNon);
|
|
6302
|
+
// #3 회귀: 진짜 N/N 테스트 → all passed 유지
|
|
6303
|
+
const vcReal = out(cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0002', '--run-tests', '--test-cmd', 'echo Tests: 2 passed, 2 total', '--path', d], { encoding: 'utf8', timeout: 20000 }));
|
|
6304
|
+
const vcRealOk = /all passed/.test(vcReal);
|
|
6305
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
6306
|
+
ok = auClean && aujOk && auRealOk && vcNonOk && vcRealOk;
|
|
6307
|
+
} catch {}
|
|
6308
|
+
console.log(ok ? '✓ B(1.27.1) 13th 리뷰 정직성: audit 미초기화 모순출력 차단(+정상 무회귀) + verify-claim no-parse 정직표기(+진짜테스트 무회귀)' : '✗ 13th 리뷰 정직성 후속 회귀가드 실패');
|
|
6237
6309
|
if (!ok) failed++;
|
|
6238
6310
|
}
|
|
6239
6311
|
|