leerness 1.9.302 → 1.9.304
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 +42 -0
- package/README.md +5 -5
- package/bin/harness.js +91 -91
- package/lib/analyzers.js +68 -0
- package/package.json +1 -1
- package/scripts/e2e.js +52 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.304 — 2026-06-04 — UR-0025 (6단계): 순수 분석/검증 함수 → lib/analyzers.js 모듈 분리
|
|
4
|
+
|
|
5
|
+
**🧩 모놀리스 분리 계속 — 순수 분석/검증 함수 4종을 비파괴 분리. lib 6모듈 체제(630줄 외부화).**
|
|
6
|
+
|
|
7
|
+
### 배경
|
|
8
|
+
외부 리뷰 핵심(UR-0038~0043) 완료 후, 두 리뷰 공통 지적인 단일 대형 파일을 계속 점진 분리. 데이터 카탈로그(1~5단계) 다음으로 **순수 함수**(부작용 0, 입력→출력) 클러스터를 분리.
|
|
9
|
+
|
|
10
|
+
### 구현 (UR-0025 6단계)
|
|
11
|
+
1. **`lib/analyzers.js` 신설** — 순수 분석/검증 함수 4종(62줄):
|
|
12
|
+
- `_evidenceQuality`(evidence 완전성: 파일/테스트/로그 근거)
|
|
13
|
+
- `_parseEvidenceStats`(review-evidence pass/fail 집계)
|
|
14
|
+
- `_shellGuardAnalyze`(셸 호환성 6규칙 정적 분석)
|
|
15
|
+
- `_claimFileInGit`(verify-claim git 교차검증 매칭)
|
|
16
|
+
2. **비파괴 require-based 분리** — verify-claim/shell-guard/review-evidence 소비처는 동일 바인딩. 모두 순수(내부 의존 0) 검증.
|
|
17
|
+
3. **harness.js 21686→21630줄**. lib 6모듈: pure-utils+agent-registry+role-catalog+catalogs+mcp-tools+analyzers = 630줄 외부화.
|
|
18
|
+
4. selftest 51→52 (4함수 동일참조 단일출처 + 인라인 제거) · e2e 248→249 (모듈 standalone + shell-guard 동작 회귀).
|
|
19
|
+
|
|
20
|
+
### 검증
|
|
21
|
+
- **selftest 52/52 PASS** · **E2E 249/249 PASS** (회귀 0).
|
|
22
|
+
- 기존 selftest(_evidenceQuality/_parseEvidenceStats/_shellGuardAnalyze ×2/_claimFileInGit)가 추출 정합성 즉시 검증.
|
|
23
|
+
- `shell-guard "a && b" --json` 정상 동작(모듈 분리 후 회귀 없음).
|
|
24
|
+
|
|
25
|
+
## 1.9.303 — 2026-06-04 — UR-0043: 상태 파일 lost-update 락 (외부 AI 리뷰 — 멀티에이전트 안전 완성)
|
|
26
|
+
|
|
27
|
+
**🔐 원자적 쓰기(UR-0038)가 막지 못한 동시 read-modify-write lost-update를 advisory 락으로 차단. Codex/Opus가 지적한 "상태 쓰기에 락 0건" 해소 — 멀티에이전트 동시 쓰기 안전성 완성.**
|
|
28
|
+
|
|
29
|
+
### 배경 (Codex / Opus A-3)
|
|
30
|
+
원자적 쓰기는 부분쓰기 손상은 막으나, 두 에이전트가 동시에 `task add`(또는 `state record`)를 호출하면: A 읽기 → B 읽기 → A 쓰기 → B 쓰기(A 변경 누락) = **lost-update**. 특히 `nextId`(다음 ID 계산)가 쓰기와 분리돼 **동일 ID 충돌**(둘 다 같은 T-XXXX → 덮어쓰기)까지 발생. grep 결과 `flock`/`O_EXCL` 0건(Opus).
|
|
31
|
+
|
|
32
|
+
### 구현 (UR-0043)
|
|
33
|
+
1. **`_withLock(targetPath, fn)`** — `O_EXCL`(wx) 원자적 lock 파일로 상호배제. 점증 backoff 재시도, stale(crash) 30s 초과 시 탈취, 타임아웃 5s 시 락 없이 진행(원자쓰기로 손상은 이미 방지). **프로세스 내 재진입**(`_heldLocks`)으로 중첩 호출 데드락 방지.
|
|
34
|
+
2. **`_updateRun(root, id, mutator)`** — run 레코드 RMW를 락으로 캡슐화.
|
|
35
|
+
3. **적용** — `upsertProgress`(task/plan write), `taskAdd`/`planAdd`(nextId+upsert를 **하나의 락**으로 → ID 충돌 차단), `state record/verify/handoff`(_updateRun). `_sleepSyncMs`(Atomics.wait) 헬퍼.
|
|
36
|
+
4. selftest 50→51 · e2e 247→248.
|
|
37
|
+
|
|
38
|
+
### 검증
|
|
39
|
+
- **selftest 51/51 PASS** · **E2E 248/248 PASS** (회귀 0).
|
|
40
|
+
- **실측: 6개 `task add` 병렬 실행 → 6개 모두 보존 + ID 충돌 0 + 구분자 1줄**. (락 전: 3/6 보존, ID 충돌 발생 — 동일 ID 덮어쓰기 확인 후 수정.)
|
|
41
|
+
|
|
42
|
+
### 🎉 외부 AI 리뷰 신뢰성/보안 핵심 권고 완수
|
|
43
|
+
- UR-0038 원자쓰기 · UR-0039 시크릿차단 · UR-0040 셸주입 · UR-0041 정책 메타데이터 · UR-0042 verify 시맨틱 · **UR-0043 lost-update 락** — 세 모델(Codex/Sonnet/Opus) 공통 high + 전략 항목 전부 코드화. 남은 UR-0044(handler 통합)는 low.
|
|
44
|
+
|
|
3
45
|
## 1.9.302 — 2026-06-04 — UR-0042: verify-claim git diff 시맨틱 교차검증 (외부 AI 리뷰 R3, Opus G-1)
|
|
4
46
|
|
|
5
47
|
**🔍 Opus가 "가장 전략적 약점"으로 꼽은 거짓완료 검증의 실질화 — "파일 존재 + N passed" 문자열매칭에 git diff 교차검증 추가: 주장한 파일이 실제로 변경됐는가를 git working tree + 직전 커밋으로 대조.**
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> **AI 코딩 에이전트의 거짓 완료·중복·망각·충돌을 막아주는 검수·기억·협업 CLI 하네스.**
|
|
4
4
|
> **A CLI harness that stops AI coding agents from faking completion, duplicating work, forgetting context, and colliding.**
|
|
5
5
|
|
|
6
|
-
[](https://www.npmjs.com/package/leerness) [](https://www.npmjs.com/package/leerness) []() []() []() []() []() []()
|
|
7
7
|
|
|
8
8
|
```
|
|
9
9
|
╔══════════════════════════════════════════════════════════════╗
|
|
@@ -471,7 +471,7 @@ MIT — © leerness contributors
|
|
|
471
471
|
<!-- leerness:project-readme:start -->
|
|
472
472
|
## Leerness Project Harness
|
|
473
473
|
|
|
474
|
-
이 프로젝트는 Leerness v1.9.
|
|
474
|
+
이 프로젝트는 Leerness v1.9.304 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
475
475
|
|
|
476
476
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
477
477
|
|
|
@@ -525,7 +525,7 @@ leerness memory restore decision <date|title>
|
|
|
525
525
|
|
|
526
526
|
### MCP server (외부 AI 통합)
|
|
527
527
|
|
|
528
|
-
Leerness v1.9.
|
|
528
|
+
Leerness v1.9.304는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **81개 도구**를 노출:
|
|
529
529
|
|
|
530
530
|
```jsonc
|
|
531
531
|
// 카테고리별
|
|
@@ -546,7 +546,7 @@ Leerness v1.9.302는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
546
546
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
547
547
|
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) 다음 라운드 예약.
|
|
548
548
|
|
|
549
|
-
현재 누적: **70 라운드 (1.9.40 → 1.9.
|
|
549
|
+
현재 누적: **70 라운드 (1.9.40 → 1.9.304)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
550
550
|
|
|
551
551
|
### 성능 가이드 (1.9.140 측정)
|
|
552
552
|
|
|
@@ -584,6 +584,6 @@ leerness release pack --close --auto-main-push
|
|
|
584
584
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
585
585
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
586
586
|
|
|
587
|
-
Last synced by Leerness v1.9.
|
|
587
|
+
Last synced by Leerness v1.9.304: 2026-06-04
|
|
588
588
|
<!-- leerness:project-readme:end -->
|
|
589
589
|
|
package/bin/harness.js
CHANGED
|
@@ -9,10 +9,12 @@ const readline = require('readline');
|
|
|
9
9
|
// 1.9.274 (UR-0025 1단계): 순수 유틸 함수 모듈 분리 (require-based, 비파괴). selftest 7종이 동작 검증.
|
|
10
10
|
const { _isSecretKey, compareVer, parseHarnessVersion, _classifyCJK, _riskLabel, _detectSystemLang, _parseSlashFromHelp,
|
|
11
11
|
PERMISSION_TIERS, _tierRank, _requiredTier, _policyAllows, _resolveNpmTag, _mcpJsonContent, _newRunRecord } = require('../lib/pure-utils');
|
|
12
|
+
// 1.9.304 (UR-0025): 순수 분석/검증 함수 모듈 분리.
|
|
13
|
+
const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInGit } = require('../lib/analyzers');
|
|
12
14
|
// 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
|
|
13
15
|
const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST } = require('../lib/catalogs');
|
|
14
16
|
|
|
15
|
-
const VERSION = '1.9.
|
|
17
|
+
const VERSION = '1.9.304';
|
|
16
18
|
|
|
17
19
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
18
20
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -189,6 +191,35 @@ function writeUtf8(p, s) {
|
|
|
189
191
|
}
|
|
190
192
|
}
|
|
191
193
|
function append(p, s) { mkdirp(path.dirname(p)); fs.appendFileSync(p, s, 'utf8'); }
|
|
194
|
+
// 1.9.303 (UR-0043, 외부리뷰): 동기 sleep (Atomics.wait, 실패 시 busy-wait 폴백).
|
|
195
|
+
function _sleepSyncMs(ms) {
|
|
196
|
+
try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.max(1, ms | 0)); }
|
|
197
|
+
catch { const end = Date.now() + ms; while (Date.now() < end) {} }
|
|
198
|
+
}
|
|
199
|
+
// 1.9.303 (UR-0043, 외부리뷰 Codex/Opus): read-modify-write lost-update 방지 advisory 락.
|
|
200
|
+
// O_EXCL(wx) 원자적 생성으로 상호배제. 다른 프로세스가 보유 중이면 짧게 대기 후 재시도.
|
|
201
|
+
// stale(보유 프로세스 crash) 은 staleMs 초과 시 탈취. 타임아웃(maxWaitMs) 시 락 없이 진행(원자쓰기로 손상은 이미 방지, lost-update 만 rare 노출).
|
|
202
|
+
const _heldLocks = new Set(); // 프로세스 내 재진입 추적 (중첩 _withLock 데드락 방지)
|
|
203
|
+
function _withLock(targetPath, fn, opts = {}) {
|
|
204
|
+
const lockPath = targetPath + '.lock';
|
|
205
|
+
if (_heldLocks.has(lockPath)) return fn(); // 이미 이 프로세스가 보유 → 재진입(중첩 호출 안전)
|
|
206
|
+
const maxWaitMs = opts.maxWaitMs || 5000;
|
|
207
|
+
const staleMs = opts.staleMs || 30000;
|
|
208
|
+
try { mkdirp(path.dirname(lockPath)); } catch {}
|
|
209
|
+
const start = Date.now();
|
|
210
|
+
let held = false;
|
|
211
|
+
while (Date.now() - start <= maxWaitMs) {
|
|
212
|
+
try { const fd = fs.openSync(lockPath, 'wx'); fs.closeSync(fd); held = true; break; } // 원자적 배타 생성
|
|
213
|
+
catch (e) {
|
|
214
|
+
if (e.code !== 'EEXIST') break; // 예기치 못한 오류 → 락 없이 진행
|
|
215
|
+
try { const st = fs.statSync(lockPath); if (Date.now() - st.mtimeMs > staleMs) { fs.unlinkSync(lockPath); continue; } } catch {}
|
|
216
|
+
_sleepSyncMs(15 + Math.floor((Date.now() - start) / 50)); // 점증 backoff
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (held) _heldLocks.add(lockPath);
|
|
220
|
+
try { return fn(); }
|
|
221
|
+
finally { if (held) { _heldLocks.delete(lockPath); try { fs.unlinkSync(lockPath); } catch {} } }
|
|
222
|
+
}
|
|
192
223
|
function rel(root, p) { return path.relative(root, p).replace(/\\/g, '/') || '.'; }
|
|
193
224
|
function today() { return new Date().toISOString().slice(0, 10); }
|
|
194
225
|
function now() { return new Date().toISOString(); }
|
|
@@ -3038,6 +3069,8 @@ function _selfTestCases() {
|
|
|
3038
3069
|
{ name: 'shell 주입 표면 제거: fetchNpmLatest execFile+pkg검증 + runCommandSafe argList 인용 (UR-0040 외부리뷰 1.9.300)', run: () => { const src = read(__filename); const npmFix = /cp\.execFile\('npm', \['view', pkg, 'version'\]/.test(src) && !/cp\.exec\(.npm view \$\{pkg\}/.test(src) && /패키지명 charset/.test(src); const argFix = /argList\.map\(_shellQuoteArg\)\.join/.test(src); return npmFix && argFix && typeof _shellQuoteArg === 'function'; } },
|
|
3039
3070
|
{ name: 'MCP requiredTier 메타데이터 + 정책 minTier 게이트 (UR-0041 외부리뷰 1.9.301)', run: () => { const T = require('../lib/mcp-tools'); const allValid = T.length >= 81 && T.every(t => PERMISSION_TIERS.includes(t.requiredTier)); const get = n => (T.find(t => t.name === n) || {}).requiredTier; const classOk = get('leerness_state_record') === 'safe-write' && get('leerness_provider_add') === 'safe-write' && get('leerness_web') === 'network' && get('leerness_handoff') === 'read-only' && get('leerness_audit') === 'read-only'; const src = read(__filename); const gateOk = /_tierRank\(minTier\) > _tierRank\(required\)/.test(src) && /_policyEnforce\(targetPath, cliArgs\.join\(' '\), _toolDef/.test(src); return allValid && classOk && gateOk; } },
|
|
3040
3071
|
{ name: 'verify-claim git diff 시맨틱 교차검증: _gitChangedFiles/_claimFileInGit + strict FAIL 통합 (UR-0042 외부리뷰 1.9.302)', run: () => { const fnOk = typeof _gitChangedFiles === 'function' && typeof _claimFileInGit === 'function'; const matchOk = _claimFileInGit('src/api.js', new Set(['src/api.js'])) === true && _claimFileInGit('./src/api.js', new Set(['src/api.js'])) === true && _claimFileInGit('other.js', new Set(['src/api.js'])) === false && _claimFileInGit('x', null) === null; const src = read(__filename); const wired = /git diff 교차검증/.test(src) && /\|\| !gitClaimOk/.test(src) && /_gitChangedFiles\(root\)/.test(src); return fnOk && matchOk && wired; } },
|
|
3072
|
+
{ name: '_withLock/_updateRun: lost-update 락(O_EXCL+재진입) + 적용 (UR-0043 외부리뷰 1.9.303)', run: () => { const src = read(__filename); const fnOk = typeof _withLock === 'function' && typeof _sleepSyncMs === 'function' && typeof _updateRun === 'function'; const reentrant = /if \(_heldLocks\.has\(lockPath\)\) return fn\(\)/.test(src); const excl = /fs\.openSync\(lockPath, 'wx'\)/.test(src); const applied = /const id = _withLock\(progressPath\(root\)/.test(src) && /_updateRun\(root, curId/.test(src); return fnOk && reentrant && excl && applied; } },
|
|
3073
|
+
{ name: 'lib/analyzers: 분석/검증 함수 4종 모듈 단일출처 분리 (UR-0025 1.9.304)', run: () => { const m = require('../lib/analyzers'); return m._evidenceQuality === _evidenceQuality && m._shellGuardAnalyze === _shellGuardAnalyze && m._parseEvidenceStats === _parseEvidenceStats && m._claimFileInGit === _claimFileInGit && !/function _evidenceQuality\(evidence\) \{/.test(read(__filename)) && !/function _shellGuardAnalyze\(cmd, ctx\) \{/.test(read(__filename)); } },
|
|
3041
3074
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
|
|
3042
3075
|
];
|
|
3043
3076
|
}
|
|
@@ -3081,38 +3114,7 @@ function selfTestCmd(opts = {}) {
|
|
|
3081
3114
|
// leerness 는 셸을 직접 가로챌 수 없으므로(0-dep Node CLI) → 실행 전 정적 분석 + 실패 기록/회수로 대응.
|
|
3082
3115
|
// ctx: { shell: 'powershell'|'cmd'|'bash'|'sh', psVersion: '5'|'7'|null }
|
|
3083
3116
|
// 반환: { shell, psVersion, issues: [{ rule, severity, detail, suggestion }] } — 순수 함수 (단위 테스트 가능)
|
|
3084
|
-
|
|
3085
|
-
const c = String(cmd || '');
|
|
3086
|
-
const shell = (ctx && ctx.shell) || 'unknown';
|
|
3087
|
-
const psVer = ctx && ctx.psVersion != null ? parseInt(ctx.psVersion, 10) : null;
|
|
3088
|
-
const issues = [];
|
|
3089
|
-
const isWinPowerShell = shell === 'powershell' && psVer != null && psVer < 6; // 5.1 = Windows PowerShell
|
|
3090
|
-
// 규칙 1: PowerShell 5.1 에서 && / || 체이닝 미지원 (pwsh 7+ 부터 지원)
|
|
3091
|
-
if (isWinPowerShell && /\s&&\s|\s\|\|\s/.test(c)) {
|
|
3092
|
-
issues.push({ rule: 'ps5-chain', severity: 'error', detail: 'Windows PowerShell 5.1 은 && / || 연산자를 지원하지 않습니다 (PowerShell 7+ 부터 지원).', suggestion: 'A; if ($?) { B } (조건부) 또는 A; B (무조건) 로 분리. 또는 pwsh 7 설치.' });
|
|
3093
|
-
}
|
|
3094
|
-
// 규칙 2: PowerShell 에서 2>/dev/null → 2>$null
|
|
3095
|
-
if (shell === 'powershell' && /2>\s*\/dev\/null/.test(c)) {
|
|
3096
|
-
issues.push({ rule: 'ps-devnull', severity: 'error', detail: 'PowerShell 은 /dev/null 경로가 없습니다.', suggestion: '2>$null 사용 (PowerShell 리다이렉트).' });
|
|
3097
|
-
}
|
|
3098
|
-
// 규칙 3: PowerShell 에서 inline env (VAR=val cmd) 미지원
|
|
3099
|
-
if (shell === 'powershell' && /^[A-Z_][A-Z0-9_]*=[^\s]+\s+\S/.test(c.trim())) {
|
|
3100
|
-
issues.push({ rule: 'ps-inline-env', severity: 'error', detail: 'PowerShell 은 VAR=val cmd 형식의 inline 환경변수를 지원하지 않습니다.', suggestion: "$env:VAR='val'; cmd 로 분리." });
|
|
3101
|
-
}
|
|
3102
|
-
// 규칙 4: PowerShell 에서 Unix 전용 명령 (rm -rf / ls -la 등) — 별칭은 되나 플래그 오류 가능
|
|
3103
|
-
if (shell === 'powershell' && /\brm\s+-rf\b/.test(c)) {
|
|
3104
|
-
issues.push({ rule: 'ps-rm-rf', severity: 'warn', detail: 'PowerShell 에서 rm -rf 는 -rf 플래그 파싱 오류 가능 (rm 은 Remove-Item 별칭).', suggestion: 'Remove-Item -Recurse -Force <path> 사용.' });
|
|
3105
|
-
}
|
|
3106
|
-
// 규칙 5: CMD 에서 ; 는 명령 구분자가 아님 (한 줄로 실행됨)
|
|
3107
|
-
if (shell === 'cmd' && /;/.test(c) && !/&&|\|\|/.test(c)) {
|
|
3108
|
-
issues.push({ rule: 'cmd-semicolon', severity: 'warn', detail: 'CMD 는 ; 를 명령 구분자로 처리하지 않습니다 (인자로 전달됨).', suggestion: 'A && B (조건부) 또는 A & B (무조건) 사용.' });
|
|
3109
|
-
}
|
|
3110
|
-
// 규칙 6: PowerShell 에서 && 가 있으나 버전 미상 — 정보성
|
|
3111
|
-
if (shell === 'powershell' && psVer == null && /\s&&\s/.test(c)) {
|
|
3112
|
-
issues.push({ rule: 'ps-version-unknown', severity: 'info', detail: 'PowerShell 버전 미상 — 5.1 이면 && 미지원, 7+ 이면 지원.', suggestion: '$PSVersionTable.PSVersion 확인. 안전하게 A; if ($?) { B } 권장.' });
|
|
3113
|
-
}
|
|
3114
|
-
return { shell, psVersion: psVer, issues };
|
|
3115
|
-
}
|
|
3117
|
+
// _shellGuardAnalyze → lib/analyzers.js (1.9.304 UR-0025)
|
|
3116
3118
|
function _shellFailuresPath(root) { return path.join(absRoot(root), '.harness', 'shell-failures.json'); }
|
|
3117
3119
|
function _loadShellFailures(root) {
|
|
3118
3120
|
try { const f = _shellFailuresPath(root); if (!exists(f)) return { failures: [] }; const j = JSON.parse(read(f)); return j && Array.isArray(j.failures) ? j : { failures: [] }; } catch { return { failures: [] }; }
|
|
@@ -6110,12 +6112,15 @@ function writeProgressRows(root, header, rows) {
|
|
|
6110
6112
|
writeUtf8(progressPath(root), composed);
|
|
6111
6113
|
}
|
|
6112
6114
|
function upsertProgress(root, row) {
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6115
|
+
// 1.9.303 (UR-0043): read-modify-write 전체를 락으로 직렬화 — 동시 task 쓰기 lost-update 방지.
|
|
6116
|
+
return _withLock(progressPath(root), () => {
|
|
6117
|
+
const header = progressHeader(root);
|
|
6118
|
+
const rows = readProgressRows(root);
|
|
6119
|
+
const i = rows.findIndex(r => r.id === row.id);
|
|
6120
|
+
if (i >= 0) rows[i] = { ...rows[i], ...row, updated: today() };
|
|
6121
|
+
else rows.push({ ...row, updated: today() });
|
|
6122
|
+
writeProgressRows(root, header, rows);
|
|
6123
|
+
});
|
|
6119
6124
|
}
|
|
6120
6125
|
|
|
6121
6126
|
function planShow(root) { const p = planPath(root); log(exists(p) ? read(p) : 'plan.md not found'); }
|
|
@@ -6176,11 +6181,15 @@ function planListCmd(root, opts = {}) {
|
|
|
6176
6181
|
}
|
|
6177
6182
|
|
|
6178
6183
|
function planAdd(root, text) {
|
|
6179
|
-
const
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
+
const status = arg('--status','planned'), progress = arg('--progress','0'), nextAction = arg('--next', '다음 액션 작성');
|
|
6185
|
+
// 1.9.303 (UR-0043): M-id append + T-id upsert 를 하나의 락으로 — 동시 plan add ID 충돌 방지.
|
|
6186
|
+
const { id, tid } = _withLock(progressPath(root), () => {
|
|
6187
|
+
const id = nextId(root, 'M');
|
|
6188
|
+
append(planPath(root), `\n### ${id}. ${text}\nStatus: ${status}\nProgress: ${progress}%\n\nTasks:\n- [ ] ${text}\n`);
|
|
6189
|
+
const tid = nextId(root, 'T');
|
|
6190
|
+
upsertProgress(root, { id: tid, status, request: text, evidence: `plan:${id}`, nextAction });
|
|
6191
|
+
return { id, tid };
|
|
6192
|
+
});
|
|
6184
6193
|
ok(`plan added: ${id} → progress: ${tid}`);
|
|
6185
6194
|
_autoRoadmap(absRoot(root), 'data-change');
|
|
6186
6195
|
}
|
|
@@ -6302,8 +6311,12 @@ function taskAdd(root, text) {
|
|
|
6302
6311
|
}
|
|
6303
6312
|
} catch {}
|
|
6304
6313
|
}
|
|
6305
|
-
|
|
6306
|
-
|
|
6314
|
+
// 1.9.303 (UR-0043): ID 할당 + write 를 하나의 락으로 — 동시 task add 의 ID 충돌(lost-update) 방지.
|
|
6315
|
+
const id = _withLock(progressPath(root), () => {
|
|
6316
|
+
const newId = nextId(root, 'T');
|
|
6317
|
+
upsertProgress(root, { id: newId, status: arg('--status','requested'), request: text, evidence: arg('--evidence','user-request'), nextAction: arg('--next','다음 액션 작성') });
|
|
6318
|
+
return newId;
|
|
6319
|
+
});
|
|
6307
6320
|
ok(`task added: ${id}`);
|
|
6308
6321
|
_autoRoadmap(absRoot(root), 'data-change');
|
|
6309
6322
|
// 1.9.177: task add 자동 review-request trigger (사용자 명시 1.9.176 자동화).
|
|
@@ -9394,17 +9407,7 @@ function reuseMapCmd(root) {
|
|
|
9394
9407
|
// "src/foo.js + 5개 테스트 (54/54 통과)" 같은 주장을 파싱해 실제 파일/카운트 확인
|
|
9395
9408
|
// 1.9.287 (Codex 리뷰 수렴): evidence 완전성 평가 — "테스트 통과만으로 done" 차단.
|
|
9396
9409
|
// done 주장은 (a) 수정 파일 경로 (b) 테스트명/개수 가 evidence 에 있어야 신뢰 가능. 순수 함수(selftest).
|
|
9397
|
-
|
|
9398
|
-
const e = String(evidence || '');
|
|
9399
|
-
const hasFile = /(?:[A-Za-z][\w-]*[\/\\])?[A-Za-z][\w./\\-]*\.(?:js|ts|tsx|jsx|mjs|cjs|py|go|rs|rb|kt|cs|gd|java|php|swift|c|cpp|h|html|css|scss|vue|svelte|json|yaml|yml|toml|md|sql|sh)\b/i.test(e);
|
|
9400
|
-
const hasTest = /(\d+)\s*(?:\/\s*\d+\s*)?(?:통과|passed|passing|개\s*테스트)|\btests?\b\s*[:=]?\s*\d|Tests?:\s*\d|\b\d+\s*tests?\b/i.test(e);
|
|
9401
|
-
const hasLog = /Exit\s*[:=]|exit\s*code|Command\s*[:=]|npm\s+(?:test|run)|pytest|cargo\s+test|go\s+test/i.test(e);
|
|
9402
|
-
const missing = [];
|
|
9403
|
-
if (!hasFile) missing.push('수정 파일 경로');
|
|
9404
|
-
if (!hasTest) missing.push('테스트명/개수');
|
|
9405
|
-
if (!hasLog) missing.push('실행 로그(Command/Exit)');
|
|
9406
|
-
return { hasFile, hasTest, hasLog, ok: hasFile && hasTest, missing };
|
|
9407
|
-
}
|
|
9410
|
+
// _evidenceQuality → lib/analyzers.js (1.9.304 UR-0025)
|
|
9408
9411
|
// 1.9.302 (UR-0042, 외부리뷰 Opus G-1): git 변경 파일 집합 — verify-claim 시맨틱 교차검증용.
|
|
9409
9412
|
// working tree(staged/unstaged/untracked) + 직전 커밋(HEAD~1..HEAD) 변경을 합쳐, "주장한 파일이 실제로 변경됐는가" 판정.
|
|
9410
9413
|
// git repo 아니거나 git 미설치면 null(검증 불가 → 페널티 X). 경로는 root-relative forward-slash.
|
|
@@ -9426,12 +9429,7 @@ function _gitChangedFiles(root) {
|
|
|
9426
9429
|
} catch { return null; }
|
|
9427
9430
|
}
|
|
9428
9431
|
// 주장 파일이 git 변경 집합에 있는지(상대경로 prefix 차이 허용).
|
|
9429
|
-
|
|
9430
|
-
if (!gitSet) return null;
|
|
9431
|
-
const c = String(claimed).replace(/\\/g, '/').replace(/^\.\//, '');
|
|
9432
|
-
for (const g of gitSet) { if (g === c || g.endsWith('/' + c) || c.endsWith('/' + g)) return true; }
|
|
9433
|
-
return false;
|
|
9434
|
-
}
|
|
9432
|
+
// _claimFileInGit → lib/analyzers.js (1.9.304 UR-0025)
|
|
9435
9433
|
function verifyClaimCmd(root, taskId) {
|
|
9436
9434
|
root = absRoot(root);
|
|
9437
9435
|
if (!taskId) return fail('verify-claim <T-ID> 필요. 예: leerness verify-claim T-0008');
|
|
@@ -18117,6 +18115,16 @@ function _saveLeernessState(root, state) {
|
|
|
18117
18115
|
function _runFile(root, id) { return path.join(_leernessStateDir(root), 'runs', `${id}.json`); }
|
|
18118
18116
|
function _loadRun(root, id) { const f = _runFile(root, id); if (!exists(f)) return null; try { return JSON.parse(read(f)); } catch { return null; } }
|
|
18119
18117
|
function _saveRun(root, rec) { const f = _runFile(root, rec.run_id); mkdirp(path.dirname(f)); writeUtf8(f, JSON.stringify(rec, null, 2) + '\n'); }
|
|
18118
|
+
// 1.9.303 (UR-0043): run 레코드 read-modify-write 를 락으로 직렬화 — 동시 state record/verify/handoff lost-update 방지.
|
|
18119
|
+
function _updateRun(root, id, mutator) {
|
|
18120
|
+
return _withLock(_runFile(root, id), () => {
|
|
18121
|
+
const rec = _loadRun(root, id);
|
|
18122
|
+
if (!rec) return null;
|
|
18123
|
+
mutator(rec);
|
|
18124
|
+
_saveRun(root, rec);
|
|
18125
|
+
return rec;
|
|
18126
|
+
});
|
|
18127
|
+
}
|
|
18120
18128
|
function _splitList(v) { return String(v || '').split(',').map(s => s.trim()).filter(Boolean); }
|
|
18121
18129
|
|
|
18122
18130
|
// leerness state <show|start|record|verify|handoff>
|
|
@@ -18274,36 +18282,40 @@ function stateCmd(root, sub, ...args) {
|
|
|
18274
18282
|
}
|
|
18275
18283
|
|
|
18276
18284
|
if (sub === 'record') {
|
|
18277
|
-
const rec =
|
|
18278
|
-
|
|
18279
|
-
|
|
18280
|
-
|
|
18281
|
-
|
|
18285
|
+
const rec = _updateRun(root, curId, (rec) => { // 1.9.303 (UR-0043): 락 직렬화 RMW
|
|
18286
|
+
for (const [flag, key] of [['--files-read', 'files_read'], ['--files-changed', 'files_changed'], ['--commands', 'commands_run'], ['--tests', 'tests_run'], ['--errors', 'errors'], ['--decision', 'decisions']]) {
|
|
18287
|
+
const v = arg(flag, null); if (v) rec[key] = Array.from(new Set([...(rec[key] || []), ..._splitList(v)]));
|
|
18288
|
+
}
|
|
18289
|
+
});
|
|
18290
|
+
if (!rec) return fail(`run 없음: ${curId}`);
|
|
18282
18291
|
if (json) { log(JSON.stringify({ recorded: curId, run: rec }, null, 2)); return; }
|
|
18283
18292
|
ok(`기록: ${curId} (files_changed ${rec.files_changed.length} · commands ${rec.commands_run.length} · tests ${rec.tests_run.length} · decisions ${rec.decisions.length})`);
|
|
18284
18293
|
return;
|
|
18285
18294
|
}
|
|
18286
18295
|
|
|
18287
18296
|
if (sub === 'verify') {
|
|
18288
|
-
const rec = _loadRun(root, curId); if (!rec) return fail(`run 없음: ${curId}`);
|
|
18289
18297
|
const result = (arg('--result', '') || (args.find(a => a && !a.startsWith('-')) || '')).toLowerCase();
|
|
18290
18298
|
if (result !== 'pass' && result !== 'fail') return fail('--result pass|fail 필요');
|
|
18291
|
-
|
|
18292
|
-
rec
|
|
18293
|
-
|
|
18294
|
-
|
|
18299
|
+
const note = arg('--note', null);
|
|
18300
|
+
const rec = _updateRun(root, curId, (rec) => { // 1.9.303 (UR-0043): 락 직렬화 RMW
|
|
18301
|
+
rec.verification_result = result;
|
|
18302
|
+
rec.status = result === 'pass' ? 'verified' : 'in-progress';
|
|
18303
|
+
if (note) rec.decisions = [...(rec.decisions || []), `verify(${result}): ${note}`];
|
|
18304
|
+
});
|
|
18305
|
+
if (!rec) return fail(`run 없음: ${curId}`);
|
|
18295
18306
|
if (json) { log(JSON.stringify({ verified: curId, result, run: rec }, null, 2)); return; }
|
|
18296
18307
|
(result === 'pass' ? ok : warn)(`검증 결과: ${curId} → ${result}`);
|
|
18297
18308
|
return;
|
|
18298
18309
|
}
|
|
18299
18310
|
|
|
18300
18311
|
if (sub === 'handoff') {
|
|
18301
|
-
const rec = _loadRun(root, curId); if (!rec) return fail(`run 없음: ${curId}`);
|
|
18302
18312
|
const summary = (args.find(a => a && !a.startsWith('-')) || arg('--summary', '')).trim();
|
|
18303
|
-
rec
|
|
18304
|
-
|
|
18305
|
-
|
|
18306
|
-
|
|
18313
|
+
const rec = _updateRun(root, curId, (rec) => { // 1.9.303 (UR-0043): 락 직렬화 RMW
|
|
18314
|
+
rec.handoff_summary = summary || rec.handoff_summary || '(요약 없음)';
|
|
18315
|
+
rec.ended_at = new Date().toISOString();
|
|
18316
|
+
rec.status = 'handed-off';
|
|
18317
|
+
});
|
|
18318
|
+
if (!rec) return fail(`run 없음: ${curId}`);
|
|
18307
18319
|
// 다음 에이전트가 읽을 latest handoff (json + md)
|
|
18308
18320
|
const hdir = path.join(_leernessStateDir(root), 'handoff'); mkdirp(hdir);
|
|
18309
18321
|
writeUtf8(path.join(hdir, 'latest.json'), JSON.stringify(rec, null, 2) + '\n');
|
|
@@ -18442,19 +18454,7 @@ function reuseCheckCmd(root, feature, opts = {}) {
|
|
|
18442
18454
|
// "설치한 스킬이 코딩 성능/정확도에 긍정 영향?" — 무거운 벤치 harness 가 아니라, 기존 데이터
|
|
18443
18455
|
// (skill 사용 빈도 + review-evidence 검증 통과율)를 상관(인과 아님)으로 advisory 제시. offline.
|
|
18444
18456
|
// review-evidence.md 파싱 (순수) — 검증 항목 수 + pass/fail 추정 (Exit 0 / 'PASS' vs 'fail'/Exit !=0).
|
|
18445
|
-
|
|
18446
|
-
const t = String(text || '');
|
|
18447
|
-
const blocks = t.split(/\n(?=## )/).filter(b => /Command:|Exit:|verify|test/i.test(b));
|
|
18448
|
-
let pass = 0, fail = 0;
|
|
18449
|
-
for (const b of blocks) {
|
|
18450
|
-
const exitM = b.match(/Exit:\s*(-?\d+)/i);
|
|
18451
|
-
if (exitM) { (parseInt(exitM[1], 10) === 0 ? pass++ : fail++); continue; }
|
|
18452
|
-
if (/\bPASS\b|통과|성공|✓/i.test(b)) pass++;
|
|
18453
|
-
else if (/\bFAIL\b|실패|오류|error|✗/i.test(b)) fail++;
|
|
18454
|
-
}
|
|
18455
|
-
const entries = blocks.length;
|
|
18456
|
-
return { entries, pass, fail, rate: (pass + fail) ? Math.round(pass / (pass + fail) * 100) : null };
|
|
18457
|
-
}
|
|
18457
|
+
// _parseEvidenceStats → lib/analyzers.js (1.9.304 UR-0025)
|
|
18458
18458
|
function skillImpactCmd(root) {
|
|
18459
18459
|
root = absRoot(root || process.cwd());
|
|
18460
18460
|
const json = has('--json');
|
package/lib/analyzers.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// lib/analyzers.js — 순수 분석/검증 함수 (부작용 0, 입력→출력).
|
|
2
|
+
// 1.9.304 (UR-0025): bin/harness.js 에서 비파괴 분리. selftest(evidenceQuality/parseEvidenceStats/shellGuardAnalyze/claimFileInGit)가 동작 검증.
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
function _shellGuardAnalyze(cmd, ctx) {
|
|
6
|
+
const c = String(cmd || '');
|
|
7
|
+
const shell = (ctx && ctx.shell) || 'unknown';
|
|
8
|
+
const psVer = ctx && ctx.psVersion != null ? parseInt(ctx.psVersion, 10) : null;
|
|
9
|
+
const issues = [];
|
|
10
|
+
const isWinPowerShell = shell === 'powershell' && psVer != null && psVer < 6; // 5.1 = Windows PowerShell
|
|
11
|
+
// 규칙 1: PowerShell 5.1 에서 && / || 체이닝 미지원 (pwsh 7+ 부터 지원)
|
|
12
|
+
if (isWinPowerShell && /\s&&\s|\s\|\|\s/.test(c)) {
|
|
13
|
+
issues.push({ rule: 'ps5-chain', severity: 'error', detail: 'Windows PowerShell 5.1 은 && / || 연산자를 지원하지 않습니다 (PowerShell 7+ 부터 지원).', suggestion: 'A; if ($?) { B } (조건부) 또는 A; B (무조건) 로 분리. 또는 pwsh 7 설치.' });
|
|
14
|
+
}
|
|
15
|
+
// 규칙 2: PowerShell 에서 2>/dev/null → 2>$null
|
|
16
|
+
if (shell === 'powershell' && /2>\s*\/dev\/null/.test(c)) {
|
|
17
|
+
issues.push({ rule: 'ps-devnull', severity: 'error', detail: 'PowerShell 은 /dev/null 경로가 없습니다.', suggestion: '2>$null 사용 (PowerShell 리다이렉트).' });
|
|
18
|
+
}
|
|
19
|
+
// 규칙 3: PowerShell 에서 inline env (VAR=val cmd) 미지원
|
|
20
|
+
if (shell === 'powershell' && /^[A-Z_][A-Z0-9_]*=[^\s]+\s+\S/.test(c.trim())) {
|
|
21
|
+
issues.push({ rule: 'ps-inline-env', severity: 'error', detail: 'PowerShell 은 VAR=val cmd 형식의 inline 환경변수를 지원하지 않습니다.', suggestion: "$env:VAR='val'; cmd 로 분리." });
|
|
22
|
+
}
|
|
23
|
+
// 규칙 4: PowerShell 에서 Unix 전용 명령 (rm -rf / ls -la 등) — 별칭은 되나 플래그 오류 가능
|
|
24
|
+
if (shell === 'powershell' && /\brm\s+-rf\b/.test(c)) {
|
|
25
|
+
issues.push({ rule: 'ps-rm-rf', severity: 'warn', detail: 'PowerShell 에서 rm -rf 는 -rf 플래그 파싱 오류 가능 (rm 은 Remove-Item 별칭).', suggestion: 'Remove-Item -Recurse -Force <path> 사용.' });
|
|
26
|
+
}
|
|
27
|
+
// 규칙 5: CMD 에서 ; 는 명령 구분자가 아님 (한 줄로 실행됨)
|
|
28
|
+
if (shell === 'cmd' && /;/.test(c) && !/&&|\|\|/.test(c)) {
|
|
29
|
+
issues.push({ rule: 'cmd-semicolon', severity: 'warn', detail: 'CMD 는 ; 를 명령 구분자로 처리하지 않습니다 (인자로 전달됨).', suggestion: 'A && B (조건부) 또는 A & B (무조건) 사용.' });
|
|
30
|
+
}
|
|
31
|
+
// 규칙 6: PowerShell 에서 && 가 있으나 버전 미상 — 정보성
|
|
32
|
+
if (shell === 'powershell' && psVer == null && /\s&&\s/.test(c)) {
|
|
33
|
+
issues.push({ rule: 'ps-version-unknown', severity: 'info', detail: 'PowerShell 버전 미상 — 5.1 이면 && 미지원, 7+ 이면 지원.', suggestion: '$PSVersionTable.PSVersion 확인. 안전하게 A; if ($?) { B } 권장.' });
|
|
34
|
+
}
|
|
35
|
+
return { shell, psVersion: psVer, issues };
|
|
36
|
+
}
|
|
37
|
+
function _evidenceQuality(evidence) {
|
|
38
|
+
const e = String(evidence || '');
|
|
39
|
+
const hasFile = /(?:[A-Za-z][\w-]*[\/\\])?[A-Za-z][\w./\\-]*\.(?:js|ts|tsx|jsx|mjs|cjs|py|go|rs|rb|kt|cs|gd|java|php|swift|c|cpp|h|html|css|scss|vue|svelte|json|yaml|yml|toml|md|sql|sh)\b/i.test(e);
|
|
40
|
+
const hasTest = /(\d+)\s*(?:\/\s*\d+\s*)?(?:통과|passed|passing|개\s*테스트)|\btests?\b\s*[:=]?\s*\d|Tests?:\s*\d|\b\d+\s*tests?\b/i.test(e);
|
|
41
|
+
const hasLog = /Exit\s*[:=]|exit\s*code|Command\s*[:=]|npm\s+(?:test|run)|pytest|cargo\s+test|go\s+test/i.test(e);
|
|
42
|
+
const missing = [];
|
|
43
|
+
if (!hasFile) missing.push('수정 파일 경로');
|
|
44
|
+
if (!hasTest) missing.push('테스트명/개수');
|
|
45
|
+
if (!hasLog) missing.push('실행 로그(Command/Exit)');
|
|
46
|
+
return { hasFile, hasTest, hasLog, ok: hasFile && hasTest, missing };
|
|
47
|
+
}
|
|
48
|
+
function _claimFileInGit(claimed, gitSet) {
|
|
49
|
+
if (!gitSet) return null;
|
|
50
|
+
const c = String(claimed).replace(/\\/g, '/').replace(/^\.\//, '');
|
|
51
|
+
for (const g of gitSet) { if (g === c || g.endsWith('/' + c) || c.endsWith('/' + g)) return true; }
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
function _parseEvidenceStats(text) {
|
|
55
|
+
const t = String(text || '');
|
|
56
|
+
const blocks = t.split(/\n(?=## )/).filter(b => /Command:|Exit:|verify|test/i.test(b));
|
|
57
|
+
let pass = 0, fail = 0;
|
|
58
|
+
for (const b of blocks) {
|
|
59
|
+
const exitM = b.match(/Exit:\s*(-?\d+)/i);
|
|
60
|
+
if (exitM) { (parseInt(exitM[1], 10) === 0 ? pass++ : fail++); continue; }
|
|
61
|
+
if (/\bPASS\b|통과|성공|✓/i.test(b)) pass++;
|
|
62
|
+
else if (/\bFAIL\b|실패|오류|error|✗/i.test(b)) fail++;
|
|
63
|
+
}
|
|
64
|
+
const entries = blocks.length;
|
|
65
|
+
return { entries, pass, fail, rate: (pass + fail) ? Math.round(pass / (pass + fail) * 100) : null };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInGit };
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -3402,5 +3402,57 @@ total++;
|
|
|
3402
3402
|
if (!ok) failed++;
|
|
3403
3403
|
}
|
|
3404
3404
|
|
|
3405
|
+
// 1.9.303 회귀 (UR-0043 외부리뷰): 동시 task add lost-update 락 — 6개 병렬 추가 시 모두 보존 + ID 충돌 0
|
|
3406
|
+
total++;
|
|
3407
|
+
{
|
|
3408
|
+
let ok = false;
|
|
3409
|
+
try {
|
|
3410
|
+
const lDir = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-lock-'));
|
|
3411
|
+
cp.spawnSync(process.execPath, [CLI, 'init', lDir, '--yes', '--language', 'ko', '--skills', 'recommended'], { encoding: 'utf8', timeout: 30000 });
|
|
3412
|
+
const N = 6;
|
|
3413
|
+
const procs = [];
|
|
3414
|
+
for (let i = 0; i < N; i++) procs.push(cp.spawn(process.execPath, [CLI, 'task', 'add', 'LOCKTEST-' + i, '--path', lDir], { stdio: 'ignore' }));
|
|
3415
|
+
const ptPath = path.join(lDir, '.harness', 'progress-tracker.md');
|
|
3416
|
+
// 자식들이 OS 프로세스로 독립 진행 → 부모는 파일을 sync 폴링(원자쓰기라 부분읽기 없음)
|
|
3417
|
+
const start = Date.now(); let found = 0;
|
|
3418
|
+
while (Date.now() - start < 25000) {
|
|
3419
|
+
try { const pt = fs.readFileSync(ptPath, 'utf8'); found = Array.from({ length: N }, (_, i) => i).filter(i => pt.includes('LOCKTEST-' + i)).length; if (found === N) break; } catch {}
|
|
3420
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200);
|
|
3421
|
+
}
|
|
3422
|
+
try { procs.forEach(p => { try { p.kill(); } catch {} }); } catch {}
|
|
3423
|
+
const pt = fs.readFileSync(ptPath, 'utf8');
|
|
3424
|
+
const allFound = Array.from({ length: N }, (_, i) => i).every(i => pt.includes('LOCKTEST-' + i));
|
|
3425
|
+
const ids = (pt.match(/^\| (T-\d{4}) \|/gm) || []).map(s => s.match(/T-\d{4}/)[0]);
|
|
3426
|
+
const noDupId = ids.length === new Set(ids).size; // ID 충돌 0
|
|
3427
|
+
const oneSep = pt.split('\n').filter(l => /^\|---\|/.test(l)).length === 1;
|
|
3428
|
+
ok = allFound && noDupId && oneSep;
|
|
3429
|
+
fs.rmSync(lDir, { recursive: true, force: true });
|
|
3430
|
+
} catch {}
|
|
3431
|
+
console.log(ok ? '✓ B(1.9.303) 동시 task add lost-update 락: 6 병렬 모두 보존 + ID 충돌 0 (UR-0043)' : '✗ lost-update 락 실패');
|
|
3432
|
+
if (!ok) failed++;
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3435
|
+
// 1.9.304 회귀 (UR-0025): lib/analyzers.js 모듈 분리 — 단일출처 + 부작용 0 + 소비 명령 회귀
|
|
3436
|
+
total++;
|
|
3437
|
+
{
|
|
3438
|
+
let ok = false;
|
|
3439
|
+
try {
|
|
3440
|
+
const a = require(path.resolve(__dirname, '..', 'lib', 'analyzers.js'));
|
|
3441
|
+
const dataOk = typeof a._evidenceQuality === 'function' && typeof a._parseEvidenceStats === 'function' &&
|
|
3442
|
+
typeof a._shellGuardAnalyze === 'function' && typeof a._claimFileInGit === 'function' &&
|
|
3443
|
+
a._evidenceQuality('src/api.js 수정, 12/12 통과 (Exit: 0)').ok === true &&
|
|
3444
|
+
a._shellGuardAnalyze('a && b', { shell: 'powershell', psVersion: '5' }).issues.some(i => i.rule === 'ps5-chain') &&
|
|
3445
|
+
a._claimFileInGit('src/api.js', new Set(['src/api.js'])) === true;
|
|
3446
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
|
|
3447
|
+
const movedOut = !/function _evidenceQuality\(evidence\) \{/.test(harnessSrc) && !/function _shellGuardAnalyze\(cmd, ctx\) \{/.test(harnessSrc) && /require\('\.\.\/lib\/analyzers'\)/.test(harnessSrc);
|
|
3448
|
+
// 소비 명령 회귀: shell-guard (_shellGuardAnalyze 사용)
|
|
3449
|
+
const sg = cp.spawnSync(process.execPath, [CLI, 'shell-guard', 'a && b', '--json'], { cwd: tmp, encoding: 'utf8', timeout: 20000 });
|
|
3450
|
+
const cmdOk = sg.status === 0 && /"shell"/.test(sg.stdout || '');
|
|
3451
|
+
ok = dataOk && movedOut && cmdOk;
|
|
3452
|
+
} catch {}
|
|
3453
|
+
console.log(ok ? '✓ B(1.9.304) lib/analyzers 모듈 분리: 단일출처 + 인라인 제거 + shell-guard 동작 (UR-0025)' : '✗ analyzers 모듈 분리 실패');
|
|
3454
|
+
if (!ok) failed++;
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3405
3457
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
3406
3458
|
if (failed > 0) process.exit(1);
|