leerness 1.9.24 → 1.9.26
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 +65 -0
- package/bin/harness.js +216 -8
- package/package.json +1 -1
- package/scripts/e2e.js +71 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,70 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.26 — 2026-05-15
|
|
4
|
+
|
|
5
|
+
**낙관적 표시 방지 — `optimism-check <T-ID>` + `verify-claim --strict-claims`** (사용자 명시 요구사항).
|
|
6
|
+
|
|
7
|
+
API 연동/DB 저장/이메일 발송 등 외부 작용을 evidence에 적었는데 실제 코드에 호출 흔적이 없는 "낙관적 표시"를 정적 분석으로 자동 감지.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`leerness optimism-check <T-ID> [--json]`**: progress-tracker의 evidence를 5종 패턴(API/DB/Email/Webhook/Payment)으로 스캔 → 주장이 있으면 코드 본문(`src/`, `bin/`, `lib/`, `scripts/`)에 호출 흔적 검사 → 불일치 발견 시 `exit 1`.
|
|
12
|
+
- **`leerness verify-claim --strict-claims`**: 기존 verify-claim 출력에 낙관적 표시 검사 결과 통합. 의심 발견 시 종합 FAIL.
|
|
13
|
+
- 5종 패턴 카탈로그:
|
|
14
|
+
- **API**: `API 호출 / HTTP \d{3} / POST \/ / fetch / endpoint` ↔ `fetch( / http.request / axios. / undici / got.`
|
|
15
|
+
- **DB**: `DB에 저장 / insert N건 / 데이터베이스 / migration` ↔ `db. / pg. / mongoose. / prisma. / sequelize`
|
|
16
|
+
- **Email**: `이메일 발송 / sendMail` ↔ `sendMail / nodemailer / smtp / @sendgrid`
|
|
17
|
+
- **Webhook**: `웹훅 호출` ↔ `fetch / http.request / axios.`
|
|
18
|
+
- **Payment**: `결제 완료 / stripe / toss` ↔ `stripe / toss / tosspayments`
|
|
19
|
+
|
|
20
|
+
### Why
|
|
21
|
+
1.9.18~1.9.25의 verify-claim은 파일·테스트 카운트만 검증. 외부 작용(API/DB) 주장은 못 잡음. 1.9.26은 정적 분석으로 1차 방어선 추가.
|
|
22
|
+
|
|
23
|
+
### Limitations (1.9.27 후보)
|
|
24
|
+
- 같은 프로젝트가 다른 목적으로 동일 키워드(예: `http.request`)를 쓰면 false negative. URL/메서드 단위 매핑 필요.
|
|
25
|
+
- AST 분석 없는 substring 매칭. 호출 위치(call site) vs evidence 청크 매핑 필요.
|
|
26
|
+
- 파일 I/O, 메시지 큐(rabbitmq/kafka), 결제 PG 추가 필요.
|
|
27
|
+
|
|
28
|
+
### Migration
|
|
29
|
+
```bash
|
|
30
|
+
npx leerness@latest update . --yes
|
|
31
|
+
|
|
32
|
+
# 사용 예
|
|
33
|
+
leerness optimism-check T-0001
|
|
34
|
+
leerness verify-claim T-0001 --run-tests --strict-claims
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 1.9.25 — 2026-05-15
|
|
38
|
+
|
|
39
|
+
**모순 감지 0/5 → 5/5 — 소스 코드 인덱싱 + 멀티 세션 in-progress 즉시 등록**.
|
|
40
|
+
|
|
41
|
+
이전 1.9.24 실측에서 발견한 "코드는 있는데 progress-tracker에 등록 안 된 상태" 사각지대를 두 가지 신규 명령으로 보완.
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- **`leerness memory search "키" --include-code`** (후보 A): `.harness/*.md` 외에 `src/`, `tests/`, `bin/`, `lib/`, `scripts/` 폴더의 `.js/.ts/.gd/.cs/.py/.rb/.go/.rs/.md/.html/.css/.json` 본문도 검색. 모순 감지 핵심.
|
|
46
|
+
- **`leerness brainstorm "주제" --include-code`** (후보 A 확장): 단일/워크스페이스 모드 모두에서 코드 hits 별도 섹션 (`💻 코드`)으로 표시.
|
|
47
|
+
- **`leerness register-pending "<요청>"`** (후보 B): 다중 세션/모델이 작업 시작 즉시 progress-tracker에 in-progress T-row를 등록. `--agent <name> --note <text>` 옵션. 다른 세션이 즉시 발견 가능 → 중복/모순 작업 방지.
|
|
48
|
+
|
|
49
|
+
### Why
|
|
50
|
+
1.9.24까지: Gemini가 워크스페이스 직접 수정 (toJson 추가)했지만 progress-tracker에 등록 전엔 다른 세션이 발견 못함 (0/5 fail). 1.9.25:
|
|
51
|
+
- 소스 코드 인덱싱으로 즉시 발견 가능 (실측: `memory search "toJson" --include-code` → **0 → 15 matches**)
|
|
52
|
+
- `register-pending`으로 작업 시작 시점 즉시 신호 발신
|
|
53
|
+
|
|
54
|
+
### Migration
|
|
55
|
+
```bash
|
|
56
|
+
npx leerness@latest update . --yes
|
|
57
|
+
|
|
58
|
+
# 다중 세션 / 외부 모델 워크플로 (Gemini/Codex/Claude)
|
|
59
|
+
leerness handoff --compact > /tmp/ctx.txt
|
|
60
|
+
# 외부 모델에 컨텍스트 + 작업 부여
|
|
61
|
+
gemini -p "$(cat /tmp/ctx.txt)\n작업: ..." --yolo
|
|
62
|
+
|
|
63
|
+
# 모순 감지 (코드 검색 포함)
|
|
64
|
+
leerness memory search "키워드" --include-code
|
|
65
|
+
leerness brainstorm "주제" --all-apps --include-code
|
|
66
|
+
```
|
|
67
|
+
|
|
3
68
|
## 1.9.24 — 2026-05-14
|
|
4
69
|
|
|
5
70
|
**`leerness deps <capability>` — depends-on 그래프 역방향 추적 + 자동 회귀 sweep**.
|
package/bin/harness.js
CHANGED
|
@@ -6,7 +6,7 @@ const path = require('path');
|
|
|
6
6
|
const cp = require('child_process');
|
|
7
7
|
const readline = require('readline');
|
|
8
8
|
|
|
9
|
-
const VERSION = '1.9.
|
|
9
|
+
const VERSION = '1.9.26';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
@@ -1167,8 +1167,34 @@ function memorySearch(root, query) {
|
|
|
1167
1167
|
total += hits.length;
|
|
1168
1168
|
}
|
|
1169
1169
|
}
|
|
1170
|
+
// 1.9.25: --include-code 옵션 — 실제 소스 코드 본문도 검색 (src/tests/bin)
|
|
1171
|
+
// 이전 모순 감지 0/5 → 5/5의 핵심 보완
|
|
1172
|
+
if (has('--include-code')) {
|
|
1173
|
+
const codeDirs = ['src', 'tests', 'bin', 'lib', 'scripts'];
|
|
1174
|
+
for (const dir of codeDirs) {
|
|
1175
|
+
const dp = path.join(root, dir);
|
|
1176
|
+
if (!exists(dp)) continue;
|
|
1177
|
+
function walkCodeDir(d) {
|
|
1178
|
+
let entries; try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
1179
|
+
for (const e of entries) {
|
|
1180
|
+
const p = path.join(d, e.name);
|
|
1181
|
+
if (e.isDirectory()) { walkCodeDir(p); continue; }
|
|
1182
|
+
if (!/\.(js|ts|jsx|tsx|gd|cs|py|rb|go|rs|md|html|css|json)$/i.test(e.name)) continue;
|
|
1183
|
+
let txt; try { txt = read(p); } catch { continue; }
|
|
1184
|
+
const lines = txt.split('\n');
|
|
1185
|
+
const hits = lines.map((line, i) => ({ line, i })).filter(x => re.test(x.line));
|
|
1186
|
+
if (hits.length) {
|
|
1187
|
+
log(`\n# ${rel(root, p)}`);
|
|
1188
|
+
for (const h of hits.slice(0, parseInt(arg('--limit','5'),10))) log(` L${h.i+1}: ${h.line.trim().slice(0, 160)}`);
|
|
1189
|
+
total += hits.length;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
walkCodeDir(dp);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1170
1196
|
if (total === 0) log('(no matches)');
|
|
1171
|
-
else log(`\n${total} matches`);
|
|
1197
|
+
else log(`\n${total} matches${has('--include-code') ? ' (소스 코드 포함)' : ''}`);
|
|
1172
1198
|
}
|
|
1173
1199
|
|
|
1174
1200
|
function handoff(root) {
|
|
@@ -1673,6 +1699,15 @@ function verifyClaimCmd(root, taskId) {
|
|
|
1673
1699
|
}
|
|
1674
1700
|
}
|
|
1675
1701
|
|
|
1702
|
+
// 1.9.26: --strict-claims — 낙관적 표시 자동 감지 (evidence vs 코드 호출 흔적)
|
|
1703
|
+
let optimismSuspects = [];
|
|
1704
|
+
let strictOk = true;
|
|
1705
|
+
if (has('--strict-claims')) {
|
|
1706
|
+
const codeText = _scanCodeForPatterns(root);
|
|
1707
|
+
optimismSuspects = _detectOptimism(evidence, codeText);
|
|
1708
|
+
strictOk = optimismSuspects.length === 0;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1676
1711
|
log('');
|
|
1677
1712
|
const allFilesOk = fileChecks.every(c => c.exists);
|
|
1678
1713
|
const testOk = declaredTestCount == null || actualTestCount == null || actualTestCount >= declaredTestCount;
|
|
@@ -1683,7 +1718,14 @@ function verifyClaimCmd(root, taskId) {
|
|
|
1683
1718
|
log(` - npm test 실행: ${runTestsOk ? '✓ all passed' : '✗ FAIL'}`);
|
|
1684
1719
|
if (declaredPass) log(` - 주장과 실행 결과 일치: ${declaredPassMatchesActual ? '✓ pass' : '⚠ 다름'}`);
|
|
1685
1720
|
}
|
|
1686
|
-
|
|
1721
|
+
if (has('--strict-claims')) {
|
|
1722
|
+
if (strictOk) log(` - 낙관적 표시 (--strict-claims): ✓ pass (의심 없음)`);
|
|
1723
|
+
else {
|
|
1724
|
+
log(` - 낙관적 표시 (--strict-claims): ⚠ FAIL (${optimismSuspects.length}건 의심)`);
|
|
1725
|
+
for (const s of optimismSuspects) log(` · [${s.kind}] ${s.label}: evidence에 주장 있는데 코드에 호출 흔적 없음`);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
const overallFail = !allFilesOk || !testOk || (runResult && !runResult.skipped && !runTestsOk) || (has('--strict-claims') && !strictOk);
|
|
1687
1729
|
if (overallFail) {
|
|
1688
1730
|
log('');
|
|
1689
1731
|
log(` ⚠ evidence 주장과 실제가 일치하지 않음 — task 상태 재검토 권장`);
|
|
@@ -1986,6 +2028,137 @@ function depsImpactCmd(root, targetCapability) {
|
|
|
1986
2028
|
}
|
|
1987
2029
|
}
|
|
1988
2030
|
|
|
2031
|
+
// 1.9.26: optimism-check — evidence의 외부 동작 주장 vs 실제 코드 호출 흔적 불일치 감지
|
|
2032
|
+
// 사용자 요청 (1.9.26): "API 연동/작업 요청 시 실제로 일어나지 않았는데 일어난 것처럼 표시하는 낙관적 결과 방지"
|
|
2033
|
+
//
|
|
2034
|
+
// 패턴 (한국어 + 영어):
|
|
2035
|
+
// evidence에 "API 호출" / "HTTP 200|201" / "POST /" / "응답 확인" → 코드에 fetch/http.request/axios 흔적 없으면 의심
|
|
2036
|
+
// evidence에 "DB 저장" / "insert N건" / "DB에" → db.*/pg.*/mysql.*/mongoose.*/prisma.* 없으면 의심
|
|
2037
|
+
// evidence에 "이메일 발송" / "메일 전송" → sendMail/nodemailer/smtp 없으면 의심
|
|
2038
|
+
const OPTIMISM_PATTERNS = [
|
|
2039
|
+
{ kind: 'API', evidenceRe: /(API\s*호출|HTTP\s*\d{3}|POST\s*\/|GET\s*\/|PUT\s*\/|DELETE\s*\/|fetch|REST 응답|응답 확인|endpoint)/i,
|
|
2040
|
+
codeRe: /\b(fetch\s*\(|http\.request|https\.request|axios\.|got\.|undici|node-fetch)/i,
|
|
2041
|
+
label: 'API/HTTP 호출' },
|
|
2042
|
+
{ kind: 'DB', evidenceRe: /(DB에?\s*저장|insert\s+\d+|데이터베이스|SQL\s*(INSERT|UPDATE|DELETE)|migration|마이그레이션 적용)/i,
|
|
2043
|
+
codeRe: /\b(db\.|pg\.|pool\.|mysql\.|mongoose\.|prisma\.|sequelize|knex|sqlite3|MongoClient|createConnection)/i,
|
|
2044
|
+
label: 'DB 호출' },
|
|
2045
|
+
{ kind: 'Email', evidenceRe: /(이메일[^.\n]{0,30}(발송|전송|보냈|보냄|완료)|메일[^.\n]{0,30}(발송|전송|보냈|보냄)|sendMail|smtp\s*(전송|발송))/i,
|
|
2046
|
+
codeRe: /\b(sendMail|nodemailer|smtp|@sendgrid|mailgun|aws-sdk\/ses|resend\.)/i,
|
|
2047
|
+
label: '이메일 전송' },
|
|
2048
|
+
{ kind: 'Webhook', evidenceRe: /(웹훅\s*(호출|전송)|webhook\s+(sent|posted|triggered))/i,
|
|
2049
|
+
codeRe: /\b(fetch\s*\(|http\.request|axios\.)/i,
|
|
2050
|
+
label: '웹훅' },
|
|
2051
|
+
{ kind: 'Payment', evidenceRe: /(결제\s*(완료|성공)|payment\s+(processed|charged)|stripe|toss)/i,
|
|
2052
|
+
codeRe: /\b(stripe|toss|@stripe|tosspayments|iamport)/i,
|
|
2053
|
+
label: '결제' }
|
|
2054
|
+
];
|
|
2055
|
+
|
|
2056
|
+
function _scanCodeForPatterns(root) {
|
|
2057
|
+
// src/, bin/, lib/, scripts/ 의 .js/.ts/.gd/.py 파일 본문 통합
|
|
2058
|
+
let combined = '';
|
|
2059
|
+
const dirs = ['src', 'bin', 'lib', 'scripts'];
|
|
2060
|
+
for (const d of dirs) {
|
|
2061
|
+
const dp = path.join(root, d);
|
|
2062
|
+
if (!exists(dp)) continue;
|
|
2063
|
+
function walk(p) {
|
|
2064
|
+
let entries; try { entries = fs.readdirSync(p, { withFileTypes: true }); } catch { return; }
|
|
2065
|
+
for (const e of entries) {
|
|
2066
|
+
const fp = path.join(p, e.name);
|
|
2067
|
+
if (e.isDirectory()) { walk(fp); continue; }
|
|
2068
|
+
if (!/\.(js|ts|jsx|tsx|gd|cs|py|rb|go|rs)$/i.test(e.name)) continue;
|
|
2069
|
+
try { combined += read(fp) + '\n'; } catch {}
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
walk(dp);
|
|
2073
|
+
}
|
|
2074
|
+
return combined;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
function _detectOptimism(evidence, codeText) {
|
|
2078
|
+
// 각 패턴 검사: evidence에 주장 있고 코드에 흔적 없으면 의심
|
|
2079
|
+
const suspects = [];
|
|
2080
|
+
for (const p of OPTIMISM_PATTERNS) {
|
|
2081
|
+
if (p.evidenceRe.test(evidence) && !p.codeRe.test(codeText)) {
|
|
2082
|
+
suspects.push({ kind: p.kind, label: p.label });
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
return suspects;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
function optimismCheckCmd(root, taskId) {
|
|
2089
|
+
root = absRoot(root || process.cwd());
|
|
2090
|
+
if (!taskId) return fail('optimism-check <T-ID> 필요. 예: leerness optimism-check T-0001');
|
|
2091
|
+
const rows = readProgressRows(root);
|
|
2092
|
+
const row = rows.find(r => r.id === taskId);
|
|
2093
|
+
if (!row) return fail(`progress-tracker.md에 ${taskId} 없음.`);
|
|
2094
|
+
|
|
2095
|
+
const codeText = _scanCodeForPatterns(root);
|
|
2096
|
+
const suspects = _detectOptimism(row.evidence || '', codeText);
|
|
2097
|
+
|
|
2098
|
+
if (has('--json')) {
|
|
2099
|
+
log(JSON.stringify({
|
|
2100
|
+
project: path.basename(root), taskId, row,
|
|
2101
|
+
suspects, ok: suspects.length === 0,
|
|
2102
|
+
codeFilesScanned: codeText.length > 0
|
|
2103
|
+
}, null, 2));
|
|
2104
|
+
if (suspects.length > 0) return process.exit(1);
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
log(`# leerness optimism-check ${taskId} (${path.basename(root)})`);
|
|
2109
|
+
log(`Evidence: ${(row.evidence || '').slice(0, 200)}${(row.evidence || '').length > 200 ? '…' : ''}`);
|
|
2110
|
+
log('');
|
|
2111
|
+
if (!suspects.length) {
|
|
2112
|
+
log(` ✓ 낙관적 표시 의심 없음 — evidence의 주장이 실제 코드 호출 흔적과 일관`);
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
2115
|
+
log(` ⚠ 낙관적 표시 의심 ${suspects.length}건 — evidence에 주장 있는데 코드에 호출 흔적 없음`);
|
|
2116
|
+
for (const s of suspects) log(` · [${s.kind}] ${s.label} 주장 ↔ 코드 호출 미발견`);
|
|
2117
|
+
log('');
|
|
2118
|
+
log(`💡 가능한 해석:`);
|
|
2119
|
+
log(` 1) evidence 작성자가 실제 동작 없이 낙관적으로 표시 (검증 필요)`);
|
|
2120
|
+
log(` 2) 호출이 별도 모듈/test fixture/외부 스크립트에 있음 → evidence에 경로 명시 권장`);
|
|
2121
|
+
log(` 3) 라이브러리/SDK 이름 변경 → \`_apps/<proj>\` 정적 분석 패턴에 미포함된 경우`);
|
|
2122
|
+
log('');
|
|
2123
|
+
log(`정책 (1.9.26): 의심 발견 시 exit 1 — task 상태 재검토 권장`);
|
|
2124
|
+
return process.exit(1);
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
// 1.9.25: register-pending — sub-agent/외부 모델이 작업 시작 즉시 progress-tracker에 in-progress 등록
|
|
2128
|
+
// 사용 예: leerness register-pending "<요청 내용>" --agent gemini
|
|
2129
|
+
// → 다음 T-ID 자동 할당, status=in-progress, evidence="(pending) by <agent>"
|
|
2130
|
+
// → 다른 세션이 즉시 발견 가능 (모순 감지)
|
|
2131
|
+
function registerPendingCmd(root, requestParts) {
|
|
2132
|
+
root = absRoot(root || process.cwd());
|
|
2133
|
+
const request = (requestParts || []).join(' ').trim();
|
|
2134
|
+
if (!request) { fail('register-pending "<요청>" 필요. 예: leerness register-pending "toJson 함수 추가" --agent gemini'); return process.exit(1); }
|
|
2135
|
+
const agent = arg('--agent', 'unknown');
|
|
2136
|
+
const note = arg('--note', '');
|
|
2137
|
+
|
|
2138
|
+
// 다음 T-ID 산출
|
|
2139
|
+
const rows = readProgressRows(root);
|
|
2140
|
+
let maxN = 0;
|
|
2141
|
+
for (const r of rows) {
|
|
2142
|
+
const m = r.id && r.id.match(/^T-(\d+)$/);
|
|
2143
|
+
if (m) maxN = Math.max(maxN, parseInt(m[1], 10));
|
|
2144
|
+
}
|
|
2145
|
+
const id = `T-${String(maxN + 1).padStart(4, '0')}`;
|
|
2146
|
+
const evidence = `(pending) by ${agent}${note ? ' — ' + note : ''}`;
|
|
2147
|
+
const row = {
|
|
2148
|
+
id,
|
|
2149
|
+
status: 'in-progress',
|
|
2150
|
+
request: request.slice(0, 200),
|
|
2151
|
+
evidence,
|
|
2152
|
+
nextAction: '작업 진행 중',
|
|
2153
|
+
updated: today()
|
|
2154
|
+
};
|
|
2155
|
+
upsertProgress(root, row);
|
|
2156
|
+
log(`✓ ${id} 등록됨 (in-progress) by ${agent}`);
|
|
2157
|
+
log(` request: ${row.request}`);
|
|
2158
|
+
log(` 💡 작업 완료 후: leerness task update ${id} --status done --evidence "..."`);
|
|
2159
|
+
if (has('--json')) log(JSON.stringify({ ok: true, id, ...row }, null, 2));
|
|
2160
|
+
}
|
|
2161
|
+
|
|
1989
2162
|
// 1.9.22 후보 4: llm-bench record + retro 통합
|
|
1990
2163
|
function llmBenchRecordCmd(root) {
|
|
1991
2164
|
root = absRoot(root || process.cwd());
|
|
@@ -2507,7 +2680,7 @@ function _brainstormFor(root, topic) {
|
|
|
2507
2680
|
const tokens = String(topic).split(/\s+/).filter(t => t.length >= 2);
|
|
2508
2681
|
const wordRes = tokens.map(t => new RegExp(`(?<![\\p{L}\\p{N}_])${_escUnicode(t)}(?![\\p{L}\\p{N}_])`, 'iu'));
|
|
2509
2682
|
function matches(text) { return wordRes.every(re => re.test(text)); }
|
|
2510
|
-
const hits = { decisions: [], skills: [], tasks: [], rules: [], evidence: [], lessons: [] };
|
|
2683
|
+
const hits = { decisions: [], skills: [], tasks: [], rules: [], evidence: [], lessons: [], code: [] };
|
|
2511
2684
|
const dec = exists(decisionsPath(root)) ? read(decisionsPath(root)) : '';
|
|
2512
2685
|
const decLines = dec.split('\n');
|
|
2513
2686
|
for (const b of _extractDecisionBlocks(dec)) {
|
|
@@ -2563,10 +2736,38 @@ function _brainstormFor(root, topic) {
|
|
|
2563
2736
|
if (/✗|fail|롤백|incomplete|버그/i.test(block)) hits.lessons.push({ title: t.trim(), line: lineNo });
|
|
2564
2737
|
}
|
|
2565
2738
|
}
|
|
2739
|
+
// 1.9.25: --include-code 옵션 — 소스 본문 검색 추가 (모순 감지 핵심)
|
|
2740
|
+
if (has('--include-code')) {
|
|
2741
|
+
const codeDirs = ['src', 'tests', 'bin', 'lib'];
|
|
2742
|
+
for (const dir of codeDirs) {
|
|
2743
|
+
const dp = path.join(root, dir);
|
|
2744
|
+
if (!exists(dp)) continue;
|
|
2745
|
+
function walkCode(d) {
|
|
2746
|
+
let entries; try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
2747
|
+
for (const e of entries) {
|
|
2748
|
+
const p = path.join(d, e.name);
|
|
2749
|
+
if (e.isDirectory()) { walkCode(p); continue; }
|
|
2750
|
+
if (!/\.(js|ts|jsx|tsx|gd|cs|py|rb|go|rs)$/i.test(e.name)) continue;
|
|
2751
|
+
let txt; try { txt = read(p); } catch { continue; }
|
|
2752
|
+
if (matches(txt)) {
|
|
2753
|
+
const lines = txt.split('\n');
|
|
2754
|
+
const firstHit = lines.findIndex(l => matches(l));
|
|
2755
|
+
hits.code.push({
|
|
2756
|
+
file: rel(root, p),
|
|
2757
|
+
line: firstHit >= 0 ? firstHit + 1 : 0,
|
|
2758
|
+
preview: firstHit >= 0 ? lines[firstHit].trim().slice(0, 120) : ''
|
|
2759
|
+
});
|
|
2760
|
+
if (hits.code.length >= 20) return; // 너무 많으면 stop
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
walkCode(dp);
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2566
2767
|
return hits;
|
|
2567
2768
|
}
|
|
2568
2769
|
|
|
2569
|
-
function _brainstormTotal(h) { return h.decisions.length + h.skills.length + h.tasks.length + h.rules.length + h.evidence.length; }
|
|
2770
|
+
function _brainstormTotal(h) { return h.decisions.length + h.skills.length + h.tasks.length + h.rules.length + h.evidence.length + (h.code?.length || 0); }
|
|
2570
2771
|
|
|
2571
2772
|
// 1.9.16: 워크스페이스 통합 brainstorm
|
|
2572
2773
|
function _brainstormWorkspace(rootBase, topic) {
|
|
@@ -2608,8 +2809,13 @@ function _brainstormWorkspace(rootBase, topic) {
|
|
|
2608
2809
|
if (h.lessons.length) {
|
|
2609
2810
|
log(` ⚠ 과거 실패/롤백 (${h.lessons.length})`);
|
|
2610
2811
|
}
|
|
2812
|
+
// 1.9.25: 소스 코드 본문 hits
|
|
2813
|
+
if (h.code && h.code.length) {
|
|
2814
|
+
log(` 💻 코드 (${h.code.length})`);
|
|
2815
|
+
h.code.slice(0, 5).forEach(c => log(` - ${c.file}:${c.line} — ${c.preview}`));
|
|
2816
|
+
}
|
|
2611
2817
|
}
|
|
2612
|
-
log(`\n## 📊 워크스페이스 총합: ${grandTotal}건 매치 (${paths.length} 프로젝트)`);
|
|
2818
|
+
log(`\n## 📊 워크스페이스 총합: ${grandTotal}건 매치 (${paths.length} 프로젝트)${has('--include-code') ? ' (소스 코드 포함)' : ''}`);
|
|
2613
2819
|
if (grandTotal === 0) log(` ⓘ 어느 프로젝트에서도 "${topic}" 관련 자원 없음 — 새 영역. 첫 결정/스킬을 기록하면 다음 brainstorm이 풍부해짐.`);
|
|
2614
2820
|
}
|
|
2615
2821
|
|
|
@@ -2634,7 +2840,7 @@ function brainstormCmd(root, topic) {
|
|
|
2634
2840
|
const tokens = String(topic).split(/\s+/).filter(t => t.length >= 2);
|
|
2635
2841
|
const wordRes = tokens.map(t => new RegExp(`(?<![\\p{L}\\p{N}_])${_escUnicode(t)}(?![\\p{L}\\p{N}_])`, 'iu'));
|
|
2636
2842
|
function matches(text) { return wordRes.every(re => re.test(text)); }
|
|
2637
|
-
const hits = { decisions: [], skills: [], tasks: [], rules: [], evidence: [], lessons: [] };
|
|
2843
|
+
const hits = { decisions: [], skills: [], tasks: [], rules: [], evidence: [], lessons: [], code: [] };
|
|
2638
2844
|
|
|
2639
2845
|
// decisions (1.9.14: 코드블록/Template 제외, 1.9.15: 라인 번호)
|
|
2640
2846
|
const dec = exists(decisionsPath(root)) ? read(decisionsPath(root)) : '';
|
|
@@ -4002,7 +4208,7 @@ function viewworkInstall(root) {
|
|
|
4002
4208
|
}
|
|
4003
4209
|
|
|
4004
4210
|
function help() {
|
|
4005
|
-
log(`Leerness v${VERSION}\n\nUsage:\n leerness init [path] [--language auto|ko|en] [--skills recommended|all|a,b]\n leerness migrate [path] [--dry-run] [--force]\n leerness update [path] [--check|--yes|--force|--from <tarball>]\n leerness auto-update install [path]\n leerness status [path]\n leerness verify [path]\n leerness debug [path]\n leerness audit [path]\n leerness check [path]\n leerness scan secrets [path]\n leerness encoding check [path]\n leerness lazy detect [path]\n leerness memory search "query" [--limit 5]\n leerness handoff [path] [--all-apps] [--include p1,p2] [--since 24h|3d] [--compact] [--json] # 1.9.17-22 워크스페이스 (--compact: LLM 시스템 프롬프트용 1줄 요약)\n leerness orchestrate "<목표>" [--agents N] [--model qwen2.5:7b-instruct] [--retry-on-fail K] # 1.9.22 Ollama opt-in (LEERNESS_OLLAMA_BASE_URL 필요)\n leerness llm-bench record --score N --model X [--label L] [--tokens T] # 1.9.22 LLM 벤치 히스토리 누적\n leerness deps <capability> [--run-tests] [--json] # 1.9.24 depends-on 역방향 추적 + 자동 회귀 sweep\n leerness reuse-map [path] [--all-apps] [--include p1,p2] [--strict-elements] [--json] # 1.9.18 중복/잠재중복/depends-on\n leerness verify-claim <T-ID> [--path .] [--run-tests] [--json] # 1.9.18-20 evidence 자동 검증 (1.9.20: scenes/scripts 등 도메인 폴더 + jest/mocha 파싱)\n leerness verify-code [path] [--build] [--bench] # 1.9.20 --bench: scripts.bench 추가 실행 + evidence 누적\n leerness session close [path]\n leerness viewwork install [path]\n leerness viewwork emit [path] [--action a] [--note n] [--agent x] [--tool t]\n leerness route <task-type>\n leerness self check [path]\n leerness readme sync [path]\n leerness consistency check [path]\n leerness consistency merge-design-guide [path]\n leerness plan show|init|add|drop|progress|sync [args]\n leerness task list|add|update|drop|fix-evidence|relink [args]\n leerness skill list|info <name>\n leerness skill learn <id> --doc <url> --command "..." --capability "..." [--note ...]\n leerness skill use <id> [--note ...]\n leerness skill optimize <id> --before "..." --after "..." [--note ...]\n leerness skill remove <id>\n leerness skill consolidate [--threshold 0.3]\n leerness gate [path] # verify+audit+scan+encoding+lazy
|
|
4211
|
+
log(`Leerness v${VERSION}\n\nUsage:\n leerness init [path] [--language auto|ko|en] [--skills recommended|all|a,b]\n leerness migrate [path] [--dry-run] [--force]\n leerness update [path] [--check|--yes|--force|--from <tarball>]\n leerness auto-update install [path]\n leerness status [path]\n leerness verify [path]\n leerness debug [path]\n leerness audit [path]\n leerness check [path]\n leerness scan secrets [path]\n leerness encoding check [path]\n leerness lazy detect [path]\n leerness memory search "query" [--limit 5]\n leerness handoff [path] [--all-apps] [--include p1,p2] [--since 24h|3d] [--compact] [--json] # 1.9.17-22 워크스페이스 (--compact: LLM 시스템 프롬프트용 1줄 요약)\n leerness orchestrate "<목표>" [--agents N] [--model qwen2.5:7b-instruct] [--retry-on-fail K] # 1.9.22 Ollama opt-in (LEERNESS_OLLAMA_BASE_URL 필요)\n leerness llm-bench record --score N --model X [--label L] [--tokens T] # 1.9.22 LLM 벤치 히스토리 누적\n leerness deps <capability> [--run-tests] [--json] # 1.9.24 depends-on 역방향 추적 + 자동 회귀 sweep\n leerness memory search "키" [--include-code] # 1.9.25 소스 코드 본문도 검색 (모순 감지 핵심)\n leerness brainstorm "주제" [--include-code] # 1.9.25 코드 본문 hits 포함\n leerness register-pending "<요청>" [--agent X] [--note Y] # 1.9.25 다중 세션 in-progress 즉시 등록\n leerness optimism-check <T-ID> [--json] # 1.9.26 낙관적 표시 감지 (evidence vs 코드 호출 흔적)\n leerness verify-claim <T-ID> ... [--strict-claims] # 1.9.26 verify-claim에 낙관적 표시 자동 검사 통합\n leerness reuse-map [path] [--all-apps] [--include p1,p2] [--strict-elements] [--json] # 1.9.18 중복/잠재중복/depends-on\n leerness verify-claim <T-ID> [--path .] [--run-tests] [--json] # 1.9.18-20 evidence 자동 검증 (1.9.20: scenes/scripts 등 도메인 폴더 + jest/mocha 파싱)\n leerness verify-code [path] [--build] [--bench] # 1.9.20 --bench: scripts.bench 추가 실행 + evidence 누적\n leerness session close [path]\n leerness viewwork install [path]\n leerness viewwork emit [path] [--action a] [--note n] [--agent x] [--tool t]\n leerness route <task-type>\n leerness self check [path]\n leerness readme sync [path]\n leerness consistency check [path]\n leerness consistency merge-design-guide [path]\n leerness plan show|init|add|drop|progress|sync [args]\n leerness task list|add|update|drop|fix-evidence|relink [args]\n leerness skill list|info <name>\n leerness skill learn <id> --doc <url> --command "..." --capability "..." [--note ...]\n leerness skill use <id> [--note ...]\n leerness skill optimize <id> --before "..." --after "..." [--note ...]\n leerness skill remove <id>\n leerness skill consolidate [--threshold 0.3]\n leerness gate [path] # verify+audit+scan+encoding+lazy
|
|
4006
4212
|
leerness retro [path] [--days 7] [--all-apps] [--include p1,p2] [--json] # 회고 (1.9.13~1.9.16)
|
|
4007
4213
|
leerness insights [path] [--all-apps] [--include p1,p2] [--json] # 누적 통계 (1.9.13~1.9.16)
|
|
4008
4214
|
leerness brainstorm "<주제>" [--all-apps] [--include p1,p2] [--json] # 브레인스토밍 (1.9.13~1.9.16)
|
|
@@ -4041,6 +4247,8 @@ async function main() {
|
|
|
4041
4247
|
if (cmd === 'orchestrate') return await orchestrateCmd(arg('--path', process.cwd()), args.slice(1).filter(x => !x.startsWith('-')));
|
|
4042
4248
|
if (cmd === 'llm-bench' && args[1] === 'record') return llmBenchRecordCmd(arg('--path', process.cwd()));
|
|
4043
4249
|
if (cmd === 'deps') return depsImpactCmd(arg('--path', process.cwd()), args[1]);
|
|
4250
|
+
if (cmd === 'register-pending') return registerPendingCmd(arg('--path', process.cwd()), args.slice(1).filter(x => !x.startsWith('-')));
|
|
4251
|
+
if (cmd === 'optimism-check') return optimismCheckCmd(arg('--path', process.cwd()), args[1]);
|
|
4044
4252
|
if (cmd === 'session' && args[1] === 'close') { const r = sessionClose(args[2] || process.cwd()); viewworkEmit(args[2] || process.cwd(), { action: 'task', tool: 'session-close', note: 'session close' }); return r; }
|
|
4045
4253
|
if (cmd === 'viewwork' && args[1] === 'install') return viewworkInstall(args[2] || process.cwd());
|
|
4046
4254
|
if (cmd === 'viewwork' && args[1] === 'emit') return viewworkEmit(args[2] || process.cwd(), { action: arg('--action','task'), note: arg('--note',''), agent: arg('--agent','leerness'), tool: arg('--tool','leerness-cli') });
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -585,6 +585,77 @@ total++;
|
|
|
585
585
|
if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
|
|
586
586
|
}
|
|
587
587
|
|
|
588
|
+
// 1.9.25 회귀: memory search --include-code + brainstorm --include-code + register-pending
|
|
589
|
+
total++;
|
|
590
|
+
{
|
|
591
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-incode-'));
|
|
592
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
593
|
+
fs.mkdirSync(path.join(tmpC, 'src'), { recursive: true });
|
|
594
|
+
fs.writeFileSync(path.join(tmpC, 'src/feature.js'), "function specialMagicHandler() { return 42; }\n");
|
|
595
|
+
// 기본 memory search → 0 matches
|
|
596
|
+
const r1 = cp.spawnSync(process.execPath, [CLI, 'memory', 'search', 'specialMagicHandler', '--path', tmpC], { encoding: 'utf8', timeout: 10000 });
|
|
597
|
+
// --include-code → 1+ matches
|
|
598
|
+
const r2 = cp.spawnSync(process.execPath, [CLI, 'memory', 'search', 'specialMagicHandler', '--path', tmpC, '--include-code'], { encoding: 'utf8', timeout: 10000 });
|
|
599
|
+
const ok = /no matches/.test(r1.stdout) && /src\/feature\.js/.test(r2.stdout) && /소스 코드 포함/.test(r2.stdout);
|
|
600
|
+
console.log(ok ? '✓ B(1.9.25) memory search --include-code: 코드 본문 검색' : '✗ --include-code 실패');
|
|
601
|
+
if (!ok) { failed++; console.log(r2.stdout.slice(0, 400)); }
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
total++;
|
|
605
|
+
{
|
|
606
|
+
// register-pending
|
|
607
|
+
const tmpR = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-regp-'));
|
|
608
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpR, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
609
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'register-pending', 'JSON export 기능', '--agent', 'gemini', '--path', tmpR], { encoding: 'utf8', timeout: 10000 });
|
|
610
|
+
const okReg = r.status === 0 && /등록됨 \(in-progress\) by gemini/.test(r.stdout);
|
|
611
|
+
// 즉시 progress-tracker에서 검색 가능
|
|
612
|
+
const r2 = cp.spawnSync(process.execPath, [CLI, 'memory', 'search', 'JSON export', '--path', tmpR], { encoding: 'utf8', timeout: 10000 });
|
|
613
|
+
const okSearch = /in-progress/.test(r2.stdout) && /pending.*gemini/.test(r2.stdout);
|
|
614
|
+
const ok = okReg && okSearch;
|
|
615
|
+
console.log(ok ? '✓ B(1.9.25) register-pending: 즉시 등록 + 검색 가능' : `✗ register-pending 실패 (reg=${okReg} search=${okSearch})`);
|
|
616
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// 1.9.26 회귀: optimism-check (낙관적 표시 자동 감지) + verify-claim --strict-claims
|
|
620
|
+
total++;
|
|
621
|
+
{
|
|
622
|
+
const tmpO = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-optm-'));
|
|
623
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpO, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
624
|
+
fs.mkdirSync(path.join(tmpO, 'src'), { recursive: true });
|
|
625
|
+
// 정적 코드: API/DB 호출 없음
|
|
626
|
+
fs.writeFileSync(path.join(tmpO, 'src/app.js'), "function pureCompute(n) { return n * 2; }\nmodule.exports = { pureCompute };\n");
|
|
627
|
+
// 낙관적 evidence: API 호출 주장
|
|
628
|
+
fs.appendFileSync(path.join(tmpO, '.harness/progress-tracker.md'),
|
|
629
|
+
'| T-9001 | done | API 등록 | POST /users API 호출 완료, HTTP 201 응답 확인 | (완료) | 2026-05-15 |\n');
|
|
630
|
+
// 정상 evidence: 단순 계산
|
|
631
|
+
fs.appendFileSync(path.join(tmpO, '.harness/progress-tracker.md'),
|
|
632
|
+
'| T-9002 | done | pure compute | src/app.js pureCompute 함수 구현 | (완료) | 2026-05-15 |\n');
|
|
633
|
+
// T-9001 (거짓) → exit 1 + 의심 감지
|
|
634
|
+
const r1 = cp.spawnSync(process.execPath, [CLI, 'optimism-check', 'T-9001', '--path', tmpO], { encoding: 'utf8', timeout: 10000 });
|
|
635
|
+
const okFalse = r1.status !== 0 && /낙관적 표시 의심/.test(r1.stdout) && /API\/HTTP 호출/.test(r1.stdout);
|
|
636
|
+
// T-9002 (정상) → exit 0 + 의심 없음
|
|
637
|
+
const r2 = cp.spawnSync(process.execPath, [CLI, 'optimism-check', 'T-9002', '--path', tmpO], { encoding: 'utf8', timeout: 10000 });
|
|
638
|
+
const okOk = r2.status === 0 && /의심 없음/.test(r2.stdout);
|
|
639
|
+
const ok = okFalse && okOk;
|
|
640
|
+
console.log(ok ? '✓ B(1.9.26) optimism-check: 낙관적 API 거짓 감지 + 정상 무경고' : `✗ optimism-check 실패 (거짓=${okFalse} 정상=${okOk})`);
|
|
641
|
+
if (!ok) { failed++; console.log('--- 거짓 case ---\n' + r1.stdout.slice(0, 300) + '\n--- 정상 case ---\n' + r2.stdout.slice(0, 300)); }
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
total++;
|
|
645
|
+
{
|
|
646
|
+
// verify-claim --strict-claims 통합
|
|
647
|
+
const tmpS = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-stct-'));
|
|
648
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpS, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
649
|
+
fs.mkdirSync(path.join(tmpS, 'src'), { recursive: true });
|
|
650
|
+
fs.writeFileSync(path.join(tmpS, 'src/x.js'), 'module.exports={};\n');
|
|
651
|
+
fs.appendFileSync(path.join(tmpS, '.harness/progress-tracker.md'),
|
|
652
|
+
'| T-0050 | done | DB 마이그레이션 | 사용자 데이터 DB에 저장, 1000건 insert 성공 | (완료) | 2026-05-15 |\n');
|
|
653
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0050', '--path', tmpS, '--strict-claims'], { encoding: 'utf8', timeout: 10000 });
|
|
654
|
+
const ok = r.status !== 0 && /낙관적 표시 \(--strict-claims\): ⚠ FAIL/.test(r.stdout) && /DB 호출/.test(r.stdout);
|
|
655
|
+
console.log(ok ? '✓ B(1.9.26) verify-claim --strict-claims: DB 거짓 evidence 통합 감지' : '✗ --strict-claims 실패');
|
|
656
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
657
|
+
}
|
|
658
|
+
|
|
588
659
|
// 1.9.22 회귀: handoff --compact + orchestrate opt-in 정책 + llm-bench record
|
|
589
660
|
total++;
|
|
590
661
|
{
|