leerness 1.9.419 → 1.9.420
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 +19 -0
- package/README.md +5 -5
- package/bin/leerness.js +25 -278
- package/lib/review-request.js +288 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.420 — 2026-06-07 — 무거움 점진 해소: reviewRequestCmd → lib/review-request.js 모듈화 (UR-0025/UR-0125)
|
|
4
|
+
|
|
5
|
+
**🪶 `bin/leerness.js`(21k줄/1.3MB) 무거움 점진 해소 — `review-request` 핸들러(277줄)를 lib/ 로 DI 분리.**
|
|
6
|
+
|
|
7
|
+
### 배경
|
|
8
|
+
실현가능성 조사(3-에이전트)에서 무거움=MODERATE, 큰 핸들러 점진 추출 권장. 후보별 외부 의존 footprint 측정 결과 **reviewRequestCmd(285줄, 의존 최소)**가 최저 위험 → 첫 추출 대상으로 선정(안정성 우선).
|
|
9
|
+
|
|
10
|
+
### 변경
|
|
11
|
+
- `lib/review-request.js` 신설(288줄): `reviewRequestCmd(root, request, deps)` — io 프리미티브는 `./io`, `cp`/`path` 빌트인, harness 고유 의존(`has`·`harnessPath`·`_checkRequestConstraints`·`_recordRun`)은 **DI 주입**(기존 doctor/team/migrate/feature 패턴 동일).
|
|
12
|
+
- `bin/leerness.js`: 277줄 함수 → **3줄 thin wrapper** 위임. **21,177 → 20,903줄(−274)**.
|
|
13
|
+
- 동작/출력 **무변경**(텍스트·--json·빈입력 가드 동일).
|
|
14
|
+
|
|
15
|
+
### 검증 (회귀 0)
|
|
16
|
+
- **selftest 165→166 PASS** (모듈 존재 + DI 위임 + 인라인 제거 + estimatedType/recommendedSteps 동작).
|
|
17
|
+
- **E2E 419→420 PASS**.
|
|
18
|
+
|
|
19
|
+
### 무거움 진행 (UR-0125, 다음 라운드)
|
|
20
|
+
다음 추출 후보(의존 footprint 순): audit(~9) → driftCheck(~13) → healthCmd(~24) → handoff(1434줄, 최대/최고결합). 라운드마다 1개씩 안전 추출.
|
|
21
|
+
|
|
3
22
|
## 1.9.419 — 2026-06-07 — 엔트리 파일명 변경: bin/harness.js → bin/leerness.js (네이밍 일관성, UR-0126)
|
|
4
23
|
|
|
5
24
|
**📛 패키지 엔트리 파일명을 `bin/harness.js` → `bin/leerness.js` 로 변경 — 명령어(`leerness`)와 파일명 일치.**
|
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.420 하네스를 사용합니다. 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.420는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
|
|
529
529
|
|
|
530
530
|
```jsonc
|
|
531
531
|
// 카테고리별
|
|
@@ -546,7 +546,7 @@ Leerness v1.9.419는 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.420)** · 매 라운드 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.420: 2026-06-07
|
|
588
588
|
<!-- leerness:project-readme:end -->
|
|
589
589
|
|
package/bin/leerness.js
CHANGED
|
@@ -31,7 +31,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
|
|
|
31
31
|
// 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
|
|
32
32
|
const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST, _DEFAULT_PLATFORM_CONSTRAINTS, _DEFAULT_DOMAIN_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 분리 (MERGE_OVERWRITE_FILES/MINIMAL_SKIP_KEYS 포함)
|
|
33
33
|
|
|
34
|
-
const VERSION = '1.9.
|
|
34
|
+
const VERSION = '1.9.420';
|
|
35
35
|
|
|
36
36
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
37
37
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -3073,6 +3073,27 @@ function _selfTestCases() {
|
|
|
3073
3073
|
} },
|
|
3074
3074
|
{ name: '9라운드 (UR-0119/0120): team review(메인 검수) — _composeTeamPlan reviewStep + handoff 검수필요 + team add 와이어 (1.9.414)', run: () => { const m = require('../lib/pure-utils'); const on = m._composeTeamPlan({ id: 't', members: ['a', 'b'], personas: ['security'] }, '점검'); const off = m._composeTeamPlan({ id: 't', members: ['a'], review: false }, '점검'); const planOk = on.review === true && !!on.reviewStep && on.reviewStep.suggestedCommand.includes('verify-claim') && off.review === false && !off.reviewStep; const rem = m._teamHandoffReminders([{ id: 'r', schedule: 'every-session', status: 'active', members: ['a'], review: true }]); const remOk = rem.length === 1 && rem[0].includes('검수필요'); const teamSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'team.js')); const wired = teamSrc.includes("review: !has('--no-review')") && teamSrc.includes('메인 검수 (필수)'); return planOk && remOk && wired; } },
|
|
3075
3075
|
{ name: '파일명 변경 (UR-0126): bin 파일=leerness.js + package.json bin/main 일치 (1.9.419)', run: () => { const okName = path.basename(__filename) === 'leerness.js'; let pkg; try { pkg = require('../package.json'); } catch { return false; } const okBin = pkg && pkg.bin && pkg.bin.leerness === 'bin/leerness.js' && pkg.main === 'bin/leerness.js'; return okName && okBin; } },
|
|
3076
|
+
{ name: 'UR-0025 큰핸들러 모듈화 5번째: reviewRequestCmd → lib/review-request.js + DI 위임 + 동작 (1.9.420)', run: () => {
|
|
3077
|
+
const m = require('../lib/review-request');
|
|
3078
|
+
const expOk = typeof m.reviewRequestCmd === 'function';
|
|
3079
|
+
const src = read(__filename);
|
|
3080
|
+
const delegated = src.includes("require('../lib/review-request')") && src.includes('_reviewRequest.reviewRequestCmd(root, request,');
|
|
3081
|
+
const modSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'review-request.js'));
|
|
3082
|
+
// thin wrapper 는 같은 시그니처를 유지하므로(시그니처 부재로 판정 X), 본문 고유 마커(routeKw)가 lib 로 이동했는지로 검증.
|
|
3083
|
+
const bodyMarker = 'const route' + 'Kw = {'; // split-literal: 자기참조(이 케이스 코드가 src 에 포함) 회피
|
|
3084
|
+
const movedToLib = modSrc.includes("require('./io')") && modSrc.includes('estimatedType') && modSrc.includes('harnessPath') && modSrc.includes(bodyMarker) && !src.includes(bodyMarker);
|
|
3085
|
+
let behavOk = false;
|
|
3086
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_rr_'));
|
|
3087
|
+
const _w = process.stdout.write; let out = '';
|
|
3088
|
+
try {
|
|
3089
|
+
fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true });
|
|
3090
|
+
process.stdout.write = s => { out += s; return true; };
|
|
3091
|
+
// harnessPath 를 존재하지 않는 경로로 → 내부 brainstorm spawn 즉시 실패(무부작용). estimatedType/steps 는 spawn 이전 계산이라 검증 가능.
|
|
3092
|
+
m.reviewRequestCmd(tmp, '결제 기능 추가 구현', { has: () => true, harnessPath: path.join(tmp, '__nope.js'), _checkRequestConstraints: () => ({ matched: [], suggestions: [] }), _recordRun: () => {} });
|
|
3093
|
+
} catch {} finally { process.stdout.write = _w; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
3094
|
+
try { const j = JSON.parse(out); behavOk = j.estimatedType === 'feature' && Array.isArray(j.recommendedSteps) && j.recommendedSteps.length === 5; } catch {}
|
|
3095
|
+
return expOk && delegated && movedToLib && behavOk;
|
|
3096
|
+
} },
|
|
3076
3097
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
|
|
3077
3098
|
];
|
|
3078
3099
|
}
|
|
@@ -20418,283 +20439,9 @@ function lspCmd(root, sub, ...args) {
|
|
|
20418
20439
|
//
|
|
20419
20440
|
// REPL: :review <request> (1.9.175 slash 패턴)
|
|
20420
20441
|
// MCP : leerness_review_request (외부 AI 직접 호출)
|
|
20421
|
-
|
|
20422
|
-
|
|
20423
|
-
|
|
20424
|
-
return fail('leerness review-request "<request>" — 사용자 요청 텍스트 필요');
|
|
20425
|
-
}
|
|
20426
|
-
const t0 = Date.now();
|
|
20427
|
-
const text = String(request).trim();
|
|
20428
|
-
|
|
20429
|
-
// 1) 작업 유형 추정 (route 기반 키워드 매핑)
|
|
20430
|
-
const lower = text.toLowerCase();
|
|
20431
|
-
const routeKw = {
|
|
20432
|
-
bugfix: ['버그', '오류', '에러', '수정', '고쳐', '실패', 'fix', 'bug', 'error'],
|
|
20433
|
-
refactor: ['리팩토', '재구성', '정리', '개선', 'refactor', 'cleanup'],
|
|
20434
|
-
feature: ['추가', '구현', '만들', '새', '기능', 'add', 'implement', 'feature', 'create', 'new'],
|
|
20435
|
-
research: ['조사', '분석', '비교', '검토', '연구', 'research', 'analyze', 'compare', 'investigate'],
|
|
20436
|
-
planning: ['계획', '설계', '로드맵', 'plan', 'design', 'architecture', 'roadmap'],
|
|
20437
|
-
release: ['배포', '릴리즈', '버전', 'release', 'deploy', 'publish'],
|
|
20438
|
-
consistency: ['일관성', '통합', '동기화', '맞춰', 'consistency', 'sync', 'align']
|
|
20439
|
-
};
|
|
20440
|
-
let estimatedType = 'feature'; // default
|
|
20441
|
-
let maxScore = 0;
|
|
20442
|
-
for (const [type, kws] of Object.entries(routeKw)) {
|
|
20443
|
-
const score = kws.filter(k => lower.includes(k)).length;
|
|
20444
|
-
if (score > maxScore) { maxScore = score; estimatedType = type; }
|
|
20445
|
-
}
|
|
20446
|
-
|
|
20447
|
-
// 2) 기존 자원 회수 — brainstorm spawn (모든 surface 통합 회수)
|
|
20448
|
-
const conflictHints = []; // ⚠ 같은 키워드 + 실패/오류 패턴
|
|
20449
|
-
const reuseCandidates = []; // 🔁 기존 skill / reuse-map / decision 후보
|
|
20450
|
-
const lessonsRecall = []; // 🧠 과거 lesson
|
|
20451
|
-
const planConflicts = []; // 📋 진행 중 milestone과 충돌 가능
|
|
20452
|
-
|
|
20453
|
-
// brainstorm 호출 (1.9.13~) — JSON 결과 회수
|
|
20454
|
-
try {
|
|
20455
|
-
const r = cp.spawnSync(process.execPath, [__filename, 'brainstorm', text, '--path', root, '--json'], {
|
|
20456
|
-
encoding: 'utf8', timeout: 12000,
|
|
20457
|
-
env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_BANNER: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' }
|
|
20458
|
-
});
|
|
20459
|
-
if (r.stdout) {
|
|
20460
|
-
const j = JSON.parse(r.stdout);
|
|
20461
|
-
const hits = j.hits || {};
|
|
20462
|
-
// decisions — 과거 결정 후보
|
|
20463
|
-
(hits.decisions || []).slice(0, 5).forEach(d => {
|
|
20464
|
-
lessonsRecall.push({ kind: 'decision', title: d.title, line: d.line, preview: (d.preview || '').slice(0, 100) });
|
|
20465
|
-
});
|
|
20466
|
-
// lessons — 과거 교훈 (특히 실패 키워드)
|
|
20467
|
-
(hits.lessons || []).slice(0, 5).forEach(l => {
|
|
20468
|
-
const preview = (l.text || l.preview || '').slice(0, 100);
|
|
20469
|
-
const isFailure = /실패|오류|에러|fail|error|bug|문제|warning/i.test(preview);
|
|
20470
|
-
if (isFailure) {
|
|
20471
|
-
conflictHints.push({ kind: 'lesson-failure', preview, tags: l.tags });
|
|
20472
|
-
} else {
|
|
20473
|
-
lessonsRecall.push({ kind: 'lesson', preview, tags: l.tags });
|
|
20474
|
-
}
|
|
20475
|
-
});
|
|
20476
|
-
// skills — 기존 skill 후보
|
|
20477
|
-
(hits.skills || []).slice(0, 3).forEach(s => {
|
|
20478
|
-
reuseCandidates.push({ kind: 'skill', id: s.id, displayNameKo: s.displayNameKo, capabilities: s.capabilities });
|
|
20479
|
-
});
|
|
20480
|
-
// tasks — 진행 중 task 충돌
|
|
20481
|
-
(hits.tasks || []).slice(0, 3).forEach(tsk => {
|
|
20482
|
-
if (tsk.status && /in-progress|진행/.test(String(tsk.status))) {
|
|
20483
|
-
conflictHints.push({ kind: 'task-in-progress', id: tsk.id, title: tsk.title });
|
|
20484
|
-
}
|
|
20485
|
-
});
|
|
20486
|
-
// plan milestones — 진행 중 milestone
|
|
20487
|
-
(hits.planMilestones || []).slice(0, 3).forEach(m => {
|
|
20488
|
-
if (m.status && /in-progress|진행/.test(String(m.status))) {
|
|
20489
|
-
planConflicts.push({ kind: 'milestone-in-progress', id: m.id, title: m.title });
|
|
20490
|
-
}
|
|
20491
|
-
});
|
|
20492
|
-
// taskLogFails — 과거 같은 키워드 실패 흔적
|
|
20493
|
-
(hits.taskLogFails || []).slice(0, 3).forEach(f => {
|
|
20494
|
-
conflictHints.push({ kind: 'task-log-failure', preview: (f.preview || f.text || '').slice(0, 100) });
|
|
20495
|
-
});
|
|
20496
|
-
}
|
|
20497
|
-
} catch {}
|
|
20498
|
-
|
|
20499
|
-
// 3) reuse-map 매칭 — 기존 capability 등록 후보
|
|
20500
|
-
try {
|
|
20501
|
-
const reusePath = path.join(root, '.harness/reuse-map.md');
|
|
20502
|
-
if (exists(reusePath)) {
|
|
20503
|
-
const reuseLines = read(reusePath).split('\n');
|
|
20504
|
-
const tokens = lower.split(/\s+/).filter(t => t.length >= 3);
|
|
20505
|
-
for (const line of reuseLines) {
|
|
20506
|
-
if (!/^\| /.test(line)) continue; // 테이블 row만
|
|
20507
|
-
const ll = line.toLowerCase();
|
|
20508
|
-
const matched = tokens.filter(t => ll.includes(t)).length;
|
|
20509
|
-
if (matched > 0) {
|
|
20510
|
-
const cols = line.split('|').map(s => s.trim());
|
|
20511
|
-
if (cols[1]) {
|
|
20512
|
-
reuseCandidates.push({ kind: 'reuse-map', capability: cols[1], where: cols[2] || '', note: cols[3] || '' });
|
|
20513
|
-
}
|
|
20514
|
-
}
|
|
20515
|
-
}
|
|
20516
|
-
}
|
|
20517
|
-
} catch {}
|
|
20518
|
-
|
|
20519
|
-
// 4) feature_graph — 같은 영역 변경 가능성
|
|
20520
|
-
const featureConflicts = [];
|
|
20521
|
-
try {
|
|
20522
|
-
const fgPath = path.join(root, '.harness/feature_graph.md');
|
|
20523
|
-
if (exists(fgPath)) {
|
|
20524
|
-
const fg = read(fgPath);
|
|
20525
|
-
const tokens = lower.split(/\s+/).filter(t => t.length >= 4);
|
|
20526
|
-
// F-XXXX 노드 라인 추출
|
|
20527
|
-
const nodeBlocks = fg.split(/\n### /);
|
|
20528
|
-
for (const blk of nodeBlocks.slice(1)) {
|
|
20529
|
-
const bl = blk.toLowerCase();
|
|
20530
|
-
const matched = tokens.filter(t => bl.includes(t)).length;
|
|
20531
|
-
if (matched > 0) {
|
|
20532
|
-
const titleMatch = blk.match(/^([^\n]+)/);
|
|
20533
|
-
const idMatch = blk.match(/F-\d+/);
|
|
20534
|
-
if (titleMatch && idMatch) {
|
|
20535
|
-
featureConflicts.push({ kind: 'feature', id: idMatch[0], title: titleMatch[1].trim() });
|
|
20536
|
-
}
|
|
20537
|
-
}
|
|
20538
|
-
}
|
|
20539
|
-
}
|
|
20540
|
-
} catch {}
|
|
20541
|
-
|
|
20542
|
-
// 5) 권장 단계 (작업 유형별)
|
|
20543
|
-
const recommendedSteps = {
|
|
20544
|
-
feature: [
|
|
20545
|
-
'1) leerness reuse-check "<기능>" — 외부 OSS 빌드 vs 재사용 판단 (1.9.285)',
|
|
20546
|
-
'2) leerness reuse find "<핵심 capability>" — 내부 중복 구현 사전 차단',
|
|
20547
|
-
'3) leerness plan add "<milestone>" — 진행 추적',
|
|
20548
|
-
'4) leerness contract verify SPEC.md src/<mod>.js — 사양 ↔ 구현 일치 검증',
|
|
20549
|
-
'5) verify-claim --run-tests 로 evidence 의무화'
|
|
20550
|
-
],
|
|
20551
|
-
bugfix: [
|
|
20552
|
-
'1) leerness brainstorm "<버그 키워드>" — 과거 같은 영역 lesson 회수',
|
|
20553
|
-
'2) leerness verify-claim T-XXX --strict-claims — 낙관적 표시 사전 감지',
|
|
20554
|
-
'3) verify-code --run-tests — 재현 + fix 검증',
|
|
20555
|
-
'4) leerness lesson save "<root cause>" — 같은 실수 재발 차단'
|
|
20556
|
-
],
|
|
20557
|
-
refactor: [
|
|
20558
|
-
'1) leerness reuse-map — 영향 범위 파악',
|
|
20559
|
-
'2) leerness impact <file> — 강한/약한 참조 분리',
|
|
20560
|
-
'3) leerness contract verify — 외부 인터페이스 보존 확인',
|
|
20561
|
-
'4) verify-code --run-tests + 회귀 테스트'
|
|
20562
|
-
],
|
|
20563
|
-
research: [
|
|
20564
|
-
'1) leerness brainstorm "<주제>" — 누적 컨텍스트 회수',
|
|
20565
|
-
'2) leerness lessons --query "<주제>" — 과거 같은 영역 결정',
|
|
20566
|
-
'3) leerness review <file> --persona research — 깊이 검토',
|
|
20567
|
-
'4) leerness decision add "<결론>" — 회수 가능하게 영구화'
|
|
20568
|
-
],
|
|
20569
|
-
planning: [
|
|
20570
|
-
'1) leerness plan add "<milestone>" — 분해 시작',
|
|
20571
|
-
'2) leerness reuse-map — 기존 자원 인벤토리',
|
|
20572
|
-
'3) leerness agents recommend planning — sub-agent 분배',
|
|
20573
|
-
'4) leerness session close — 결정 영구화'
|
|
20574
|
-
],
|
|
20575
|
-
release: [
|
|
20576
|
-
'1) leerness health — production-ready 확인',
|
|
20577
|
-
'2) leerness audit + verify-code — 보안 + 검수',
|
|
20578
|
-
'3) leerness release bump + note + publish'
|
|
20579
|
-
],
|
|
20580
|
-
consistency: [
|
|
20581
|
-
'1) leerness audit — design/reuse/handoff 일관성 검사',
|
|
20582
|
-
'2) leerness consistency check — 잠재 일관성 위반',
|
|
20583
|
-
'3) leerness drift check --auto-fix — 자동 회복'
|
|
20584
|
-
]
|
|
20585
|
-
}[estimatedType] || [];
|
|
20586
|
-
|
|
20587
|
-
// 6) 효율 제안 (적용 가능한 sub-agent + skill)
|
|
20588
|
-
const efficiencyHints = [];
|
|
20589
|
-
if (reuseCandidates.length > 0) {
|
|
20590
|
-
efficiencyHints.push(`🔁 기존 자원 ${reuseCandidates.length}건 발견 — 신규 구현 전 재사용 검토 권장`);
|
|
20591
|
-
}
|
|
20592
|
-
if (conflictHints.length > 0) {
|
|
20593
|
-
efficiencyHints.push(`⚠ 충돌 신호 ${conflictHints.length}건 — 과거 실패 lesson / 진행 중 task 확인 필요`);
|
|
20594
|
-
}
|
|
20595
|
-
if (planConflicts.length > 0) {
|
|
20596
|
-
efficiencyHints.push(`📋 진행 중 milestone ${planConflicts.length}건과 영역 겹침 가능 — plan 정렬 권장`);
|
|
20597
|
-
}
|
|
20598
|
-
if (featureConflicts.length > 0) {
|
|
20599
|
-
efficiencyHints.push(`🕸 Feature Graph ${featureConflicts.length}건 영역 겹침 — 의존성 사전 확인`);
|
|
20600
|
-
}
|
|
20601
|
-
// 다중 에이전트 분배 추천
|
|
20602
|
-
if (estimatedType === 'feature' || estimatedType === 'planning') {
|
|
20603
|
-
efficiencyHints.push(`👥 leerness agents recommend ${estimatedType} — 작업 유형별 sub-agent 매핑 활용 가능`);
|
|
20604
|
-
}
|
|
20605
|
-
if (efficiencyHints.length === 0) {
|
|
20606
|
-
efficiencyHints.push('✨ 충돌 신호 없음 — 즉시 진행 안전');
|
|
20607
|
-
}
|
|
20608
|
-
|
|
20609
|
-
// 6.5) 1.9.208: 플랫폼/API 제약 사전 체크 — 사용자 명시 ("호출속도 초당 5회" 같은 규정 사전 확인)
|
|
20610
|
-
let constraintsCheck = { matched: [], suggestions: [] };
|
|
20611
|
-
try {
|
|
20612
|
-
constraintsCheck = _checkRequestConstraints(root, text);
|
|
20613
|
-
if (constraintsCheck.matched.length > 0) {
|
|
20614
|
-
efficiencyHints.push(`⚠ 플랫폼 제약 ${constraintsCheck.matched.length}건 — leerness constraints check 로 상세 확인`);
|
|
20615
|
-
}
|
|
20616
|
-
} catch {}
|
|
20617
|
-
|
|
20618
|
-
// 7) proceed 권장 (충돌 critical 시 false)
|
|
20619
|
-
const proceed = conflictHints.length < 3 && planConflicts.length === 0;
|
|
20620
|
-
|
|
20621
|
-
const dt = Date.now() - t0;
|
|
20622
|
-
const out = {
|
|
20623
|
-
request: text,
|
|
20624
|
-
estimatedType,
|
|
20625
|
-
conflicts: conflictHints,
|
|
20626
|
-
reuseCandidates,
|
|
20627
|
-
lessonsRecall,
|
|
20628
|
-
planConflicts,
|
|
20629
|
-
featureConflicts,
|
|
20630
|
-
recommendedSteps,
|
|
20631
|
-
efficiencyHints,
|
|
20632
|
-
platformConstraints: constraintsCheck.matched,
|
|
20633
|
-
constraintSuggestions: constraintsCheck.suggestions,
|
|
20634
|
-
proceed,
|
|
20635
|
-
proceedReason: proceed ? '안전 — 충돌 신호 < 3 + plan 충돌 0' : '⚠ 충돌 critical — 사용자 확인 후 진행',
|
|
20636
|
-
durationMs: dt
|
|
20637
|
-
};
|
|
20638
|
-
|
|
20639
|
-
try { _recordRun(root, { kind: 'review_request', estimatedType, conflicts: conflictHints.length, reuse: reuseCandidates.length, durationMs: dt, ok: true }); } catch {}
|
|
20640
|
-
|
|
20641
|
-
if (has('--json')) {
|
|
20642
|
-
log(JSON.stringify(out, null, 2));
|
|
20643
|
-
return;
|
|
20644
|
-
}
|
|
20645
|
-
|
|
20646
|
-
log(`# leerness review-request (1.9.176 사전 검토)`);
|
|
20647
|
-
log(`요청: "${text.slice(0, 200)}${text.length > 200 ? '…' : ''}"`);
|
|
20648
|
-
log(`추정 작업 유형: ${estimatedType}`);
|
|
20649
|
-
log('');
|
|
20650
|
-
if (conflictHints.length) {
|
|
20651
|
-
log(`## ⚠ 충돌 신호 (${conflictHints.length})`);
|
|
20652
|
-
conflictHints.slice(0, 5).forEach(c => log(` - [${c.kind}] ${c.title || c.id || ''} ${c.preview || ''}`.trim()));
|
|
20653
|
-
log('');
|
|
20654
|
-
}
|
|
20655
|
-
if (reuseCandidates.length) {
|
|
20656
|
-
log(`## 🔁 재사용 후보 (${reuseCandidates.length}) — 신규 구현 전 검토`);
|
|
20657
|
-
reuseCandidates.slice(0, 5).forEach(r => {
|
|
20658
|
-
if (r.kind === 'skill') log(` - [skill] ${r.id}${r.displayNameKo ? ' · ' + r.displayNameKo : ''}`);
|
|
20659
|
-
else if (r.kind === 'reuse-map') log(` - [reuse] ${r.capability} @ ${r.where}`);
|
|
20660
|
-
});
|
|
20661
|
-
log('');
|
|
20662
|
-
}
|
|
20663
|
-
if (lessonsRecall.length) {
|
|
20664
|
-
log(`## 🧠 과거 컨텍스트 (${lessonsRecall.length}) — 관련 결정/교훈`);
|
|
20665
|
-
lessonsRecall.slice(0, 3).forEach(l => log(` - [${l.kind}] ${l.title || l.preview}`));
|
|
20666
|
-
log('');
|
|
20667
|
-
}
|
|
20668
|
-
if (planConflicts.length || featureConflicts.length) {
|
|
20669
|
-
log(`## 📋 진행 중 영역 (${planConflicts.length + featureConflicts.length})`);
|
|
20670
|
-
planConflicts.forEach(m => log(` - [milestone] ${m.id}: ${m.title}`));
|
|
20671
|
-
featureConflicts.slice(0, 5).forEach(f => log(` - [feature] ${f.id}: ${f.title}`));
|
|
20672
|
-
log('');
|
|
20673
|
-
}
|
|
20674
|
-
log(`## 💡 효율 제안`);
|
|
20675
|
-
efficiencyHints.forEach(h => log(` ${h}`));
|
|
20676
|
-
log('');
|
|
20677
|
-
// 1.9.208: 플랫폼/API 제약 사전 노출 (사용자 명시)
|
|
20678
|
-
if (constraintsCheck.matched.length > 0) {
|
|
20679
|
-
log(`## 🚦 플랫폼/API 제약 사전 체크 (${constraintsCheck.matched.length})`);
|
|
20680
|
-
for (const m of constraintsCheck.matched) {
|
|
20681
|
-
log(` - 📦 ${m.platform} (docs: ${m.docs || '-'})`);
|
|
20682
|
-
for (const c of (m.constraints || []).slice(0, 3)) {
|
|
20683
|
-
log(` • [${c.kind}] ${c.detail}`);
|
|
20684
|
-
}
|
|
20685
|
-
}
|
|
20686
|
-
log(` → leerness constraints check "${text.slice(0, 40)}…" 로 상세 확인`);
|
|
20687
|
-
log('');
|
|
20688
|
-
}
|
|
20689
|
-
if (recommendedSteps.length) {
|
|
20690
|
-
log(`## 📍 권장 단계 (${estimatedType})`);
|
|
20691
|
-
recommendedSteps.forEach(s => log(` ${s}`));
|
|
20692
|
-
log('');
|
|
20693
|
-
}
|
|
20694
|
-
log(`## ▶ 진행 권장: ${proceed ? '✓ 진행 안전' : '⚠ 사용자 확인 필요'}`);
|
|
20695
|
-
log(` 사유: ${out.proceedReason}`);
|
|
20696
|
-
log(` 분석 소요: ${dt}ms`);
|
|
20697
|
-
}
|
|
20442
|
+
const _reviewRequest = require('../lib/review-request');
|
|
20443
|
+
// 1.9.420 (UR-0125 큰 핸들러 모듈화 5번째): reviewRequestCmd → lib/review-request.js (DI 위임, thin wrapper)
|
|
20444
|
+
function reviewRequestCmd(root, request) { return _reviewRequest.reviewRequestCmd(root, request, { has, harnessPath: __filename, _checkRequestConstraints, _recordRun }); }
|
|
20698
20445
|
|
|
20699
20446
|
// 1.9.164: leerness which — 진단 도구 (구버전 충돌 / npx 캐시 / PATH 충돌 해결)
|
|
20700
20447
|
// 사용자가 "최신 버전 작동 안 함" 의심 시: 실제 실행 중인 leerness 의 경로 / 버전 / npm 캐시 / PATH 후보 표시.
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// lib/review-request.js — review-request 핸들러 (UR-0125 큰 핸들러 모듈화, 1.9.420)
|
|
2
|
+
// bin/leerness.js 에서 reviewRequestCmd(277줄) 분리. DI: harness 고유 의존(has · harnessPath · _checkRequestConstraints · _recordRun) 주입.
|
|
3
|
+
// io 프리미티브는 ./io, cp/path 는 빌트인. 동작/출력 무변경(thin wrapper 위임).
|
|
4
|
+
'use strict';
|
|
5
|
+
const cp = require('child_process');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { absRoot, exists, read, log, fail } = require('./io');
|
|
8
|
+
|
|
9
|
+
function reviewRequestCmd(root, request, deps = {}) {
|
|
10
|
+
const { has, harnessPath, _checkRequestConstraints, _recordRun } = deps;
|
|
11
|
+
root = absRoot(root || process.cwd());
|
|
12
|
+
if (!request || !String(request).trim()) {
|
|
13
|
+
return fail('leerness review-request "<request>" — 사용자 요청 텍스트 필요');
|
|
14
|
+
}
|
|
15
|
+
const t0 = Date.now();
|
|
16
|
+
const text = String(request).trim();
|
|
17
|
+
|
|
18
|
+
// 1) 작업 유형 추정 (route 기반 키워드 매핑)
|
|
19
|
+
const lower = text.toLowerCase();
|
|
20
|
+
const routeKw = {
|
|
21
|
+
bugfix: ['버그', '오류', '에러', '수정', '고쳐', '실패', 'fix', 'bug', 'error'],
|
|
22
|
+
refactor: ['리팩토', '재구성', '정리', '개선', 'refactor', 'cleanup'],
|
|
23
|
+
feature: ['추가', '구현', '만들', '새', '기능', 'add', 'implement', 'feature', 'create', 'new'],
|
|
24
|
+
research: ['조사', '분석', '비교', '검토', '연구', 'research', 'analyze', 'compare', 'investigate'],
|
|
25
|
+
planning: ['계획', '설계', '로드맵', 'plan', 'design', 'architecture', 'roadmap'],
|
|
26
|
+
release: ['배포', '릴리즈', '버전', 'release', 'deploy', 'publish'],
|
|
27
|
+
consistency: ['일관성', '통합', '동기화', '맞춰', 'consistency', 'sync', 'align']
|
|
28
|
+
};
|
|
29
|
+
let estimatedType = 'feature'; // default
|
|
30
|
+
let maxScore = 0;
|
|
31
|
+
for (const [type, kws] of Object.entries(routeKw)) {
|
|
32
|
+
const score = kws.filter(k => lower.includes(k)).length;
|
|
33
|
+
if (score > maxScore) { maxScore = score; estimatedType = type; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 2) 기존 자원 회수 — brainstorm spawn (모든 surface 통합 회수)
|
|
37
|
+
const conflictHints = []; // ⚠ 같은 키워드 + 실패/오류 패턴
|
|
38
|
+
const reuseCandidates = []; // 🔁 기존 skill / reuse-map / decision 후보
|
|
39
|
+
const lessonsRecall = []; // 🧠 과거 lesson
|
|
40
|
+
const planConflicts = []; // 📋 진행 중 milestone과 충돌 가능
|
|
41
|
+
|
|
42
|
+
// brainstorm 호출 (1.9.13~) — JSON 결과 회수
|
|
43
|
+
try {
|
|
44
|
+
const r = cp.spawnSync(process.execPath, [harnessPath, 'brainstorm', text, '--path', root, '--json'], {
|
|
45
|
+
encoding: 'utf8', timeout: 12000,
|
|
46
|
+
env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_BANNER: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' }
|
|
47
|
+
});
|
|
48
|
+
if (r.stdout) {
|
|
49
|
+
const j = JSON.parse(r.stdout);
|
|
50
|
+
const hits = j.hits || {};
|
|
51
|
+
// decisions — 과거 결정 후보
|
|
52
|
+
(hits.decisions || []).slice(0, 5).forEach(d => {
|
|
53
|
+
lessonsRecall.push({ kind: 'decision', title: d.title, line: d.line, preview: (d.preview || '').slice(0, 100) });
|
|
54
|
+
});
|
|
55
|
+
// lessons — 과거 교훈 (특히 실패 키워드)
|
|
56
|
+
(hits.lessons || []).slice(0, 5).forEach(l => {
|
|
57
|
+
const preview = (l.text || l.preview || '').slice(0, 100);
|
|
58
|
+
const isFailure = /실패|오류|에러|fail|error|bug|문제|warning/i.test(preview);
|
|
59
|
+
if (isFailure) {
|
|
60
|
+
conflictHints.push({ kind: 'lesson-failure', preview, tags: l.tags });
|
|
61
|
+
} else {
|
|
62
|
+
lessonsRecall.push({ kind: 'lesson', preview, tags: l.tags });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
// skills — 기존 skill 후보
|
|
66
|
+
(hits.skills || []).slice(0, 3).forEach(s => {
|
|
67
|
+
reuseCandidates.push({ kind: 'skill', id: s.id, displayNameKo: s.displayNameKo, capabilities: s.capabilities });
|
|
68
|
+
});
|
|
69
|
+
// tasks — 진행 중 task 충돌
|
|
70
|
+
(hits.tasks || []).slice(0, 3).forEach(tsk => {
|
|
71
|
+
if (tsk.status && /in-progress|진행/.test(String(tsk.status))) {
|
|
72
|
+
conflictHints.push({ kind: 'task-in-progress', id: tsk.id, title: tsk.title });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
// plan milestones — 진행 중 milestone
|
|
76
|
+
(hits.planMilestones || []).slice(0, 3).forEach(m => {
|
|
77
|
+
if (m.status && /in-progress|진행/.test(String(m.status))) {
|
|
78
|
+
planConflicts.push({ kind: 'milestone-in-progress', id: m.id, title: m.title });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
// taskLogFails — 과거 같은 키워드 실패 흔적
|
|
82
|
+
(hits.taskLogFails || []).slice(0, 3).forEach(f => {
|
|
83
|
+
conflictHints.push({ kind: 'task-log-failure', preview: (f.preview || f.text || '').slice(0, 100) });
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
} catch {}
|
|
87
|
+
|
|
88
|
+
// 3) reuse-map 매칭 — 기존 capability 등록 후보
|
|
89
|
+
try {
|
|
90
|
+
const reusePath = path.join(root, '.harness/reuse-map.md');
|
|
91
|
+
if (exists(reusePath)) {
|
|
92
|
+
const reuseLines = read(reusePath).split('\n');
|
|
93
|
+
const tokens = lower.split(/\s+/).filter(t => t.length >= 3);
|
|
94
|
+
for (const line of reuseLines) {
|
|
95
|
+
if (!/^\| /.test(line)) continue; // 테이블 row만
|
|
96
|
+
const ll = line.toLowerCase();
|
|
97
|
+
const matched = tokens.filter(t => ll.includes(t)).length;
|
|
98
|
+
if (matched > 0) {
|
|
99
|
+
const cols = line.split('|').map(s => s.trim());
|
|
100
|
+
if (cols[1]) {
|
|
101
|
+
reuseCandidates.push({ kind: 'reuse-map', capability: cols[1], where: cols[2] || '', note: cols[3] || '' });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch {}
|
|
107
|
+
|
|
108
|
+
// 4) feature_graph — 같은 영역 변경 가능성
|
|
109
|
+
const featureConflicts = [];
|
|
110
|
+
try {
|
|
111
|
+
const fgPath = path.join(root, '.harness/feature_graph.md');
|
|
112
|
+
if (exists(fgPath)) {
|
|
113
|
+
const fg = read(fgPath);
|
|
114
|
+
const tokens = lower.split(/\s+/).filter(t => t.length >= 4);
|
|
115
|
+
// F-XXXX 노드 라인 추출
|
|
116
|
+
const nodeBlocks = fg.split(/\n### /);
|
|
117
|
+
for (const blk of nodeBlocks.slice(1)) {
|
|
118
|
+
const bl = blk.toLowerCase();
|
|
119
|
+
const matched = tokens.filter(t => bl.includes(t)).length;
|
|
120
|
+
if (matched > 0) {
|
|
121
|
+
const titleMatch = blk.match(/^([^\n]+)/);
|
|
122
|
+
const idMatch = blk.match(/F-\d+/);
|
|
123
|
+
if (titleMatch && idMatch) {
|
|
124
|
+
featureConflicts.push({ kind: 'feature', id: idMatch[0], title: titleMatch[1].trim() });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
} catch {}
|
|
130
|
+
|
|
131
|
+
// 5) 권장 단계 (작업 유형별)
|
|
132
|
+
const recommendedSteps = {
|
|
133
|
+
feature: [
|
|
134
|
+
'1) leerness reuse-check "<기능>" — 외부 OSS 빌드 vs 재사용 판단 (1.9.285)',
|
|
135
|
+
'2) leerness reuse find "<핵심 capability>" — 내부 중복 구현 사전 차단',
|
|
136
|
+
'3) leerness plan add "<milestone>" — 진행 추적',
|
|
137
|
+
'4) leerness contract verify SPEC.md src/<mod>.js — 사양 ↔ 구현 일치 검증',
|
|
138
|
+
'5) verify-claim --run-tests 로 evidence 의무화'
|
|
139
|
+
],
|
|
140
|
+
bugfix: [
|
|
141
|
+
'1) leerness brainstorm "<버그 키워드>" — 과거 같은 영역 lesson 회수',
|
|
142
|
+
'2) leerness verify-claim T-XXX --strict-claims — 낙관적 표시 사전 감지',
|
|
143
|
+
'3) verify-code --run-tests — 재현 + fix 검증',
|
|
144
|
+
'4) leerness lesson save "<root cause>" — 같은 실수 재발 차단'
|
|
145
|
+
],
|
|
146
|
+
refactor: [
|
|
147
|
+
'1) leerness reuse-map — 영향 범위 파악',
|
|
148
|
+
'2) leerness impact <file> — 강한/약한 참조 분리',
|
|
149
|
+
'3) leerness contract verify — 외부 인터페이스 보존 확인',
|
|
150
|
+
'4) verify-code --run-tests + 회귀 테스트'
|
|
151
|
+
],
|
|
152
|
+
research: [
|
|
153
|
+
'1) leerness brainstorm "<주제>" — 누적 컨텍스트 회수',
|
|
154
|
+
'2) leerness lessons --query "<주제>" — 과거 같은 영역 결정',
|
|
155
|
+
'3) leerness review <file> --persona research — 깊이 검토',
|
|
156
|
+
'4) leerness decision add "<결론>" — 회수 가능하게 영구화'
|
|
157
|
+
],
|
|
158
|
+
planning: [
|
|
159
|
+
'1) leerness plan add "<milestone>" — 분해 시작',
|
|
160
|
+
'2) leerness reuse-map — 기존 자원 인벤토리',
|
|
161
|
+
'3) leerness agents recommend planning — sub-agent 분배',
|
|
162
|
+
'4) leerness session close — 결정 영구화'
|
|
163
|
+
],
|
|
164
|
+
release: [
|
|
165
|
+
'1) leerness health — production-ready 확인',
|
|
166
|
+
'2) leerness audit + verify-code — 보안 + 검수',
|
|
167
|
+
'3) leerness release bump + note + publish'
|
|
168
|
+
],
|
|
169
|
+
consistency: [
|
|
170
|
+
'1) leerness audit — design/reuse/handoff 일관성 검사',
|
|
171
|
+
'2) leerness consistency check — 잠재 일관성 위반',
|
|
172
|
+
'3) leerness drift check --auto-fix — 자동 회복'
|
|
173
|
+
]
|
|
174
|
+
}[estimatedType] || [];
|
|
175
|
+
|
|
176
|
+
// 6) 효율 제안 (적용 가능한 sub-agent + skill)
|
|
177
|
+
const efficiencyHints = [];
|
|
178
|
+
if (reuseCandidates.length > 0) {
|
|
179
|
+
efficiencyHints.push(`🔁 기존 자원 ${reuseCandidates.length}건 발견 — 신규 구현 전 재사용 검토 권장`);
|
|
180
|
+
}
|
|
181
|
+
if (conflictHints.length > 0) {
|
|
182
|
+
efficiencyHints.push(`⚠ 충돌 신호 ${conflictHints.length}건 — 과거 실패 lesson / 진행 중 task 확인 필요`);
|
|
183
|
+
}
|
|
184
|
+
if (planConflicts.length > 0) {
|
|
185
|
+
efficiencyHints.push(`📋 진행 중 milestone ${planConflicts.length}건과 영역 겹침 가능 — plan 정렬 권장`);
|
|
186
|
+
}
|
|
187
|
+
if (featureConflicts.length > 0) {
|
|
188
|
+
efficiencyHints.push(`🕸 Feature Graph ${featureConflicts.length}건 영역 겹침 — 의존성 사전 확인`);
|
|
189
|
+
}
|
|
190
|
+
// 다중 에이전트 분배 추천
|
|
191
|
+
if (estimatedType === 'feature' || estimatedType === 'planning') {
|
|
192
|
+
efficiencyHints.push(`👥 leerness agents recommend ${estimatedType} — 작업 유형별 sub-agent 매핑 활용 가능`);
|
|
193
|
+
}
|
|
194
|
+
if (efficiencyHints.length === 0) {
|
|
195
|
+
efficiencyHints.push('✨ 충돌 신호 없음 — 즉시 진행 안전');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 6.5) 1.9.208: 플랫폼/API 제약 사전 체크 — 사용자 명시 ("호출속도 초당 5회" 같은 규정 사전 확인)
|
|
199
|
+
let constraintsCheck = { matched: [], suggestions: [] };
|
|
200
|
+
try {
|
|
201
|
+
constraintsCheck = _checkRequestConstraints(root, text);
|
|
202
|
+
if (constraintsCheck.matched.length > 0) {
|
|
203
|
+
efficiencyHints.push(`⚠ 플랫폼 제약 ${constraintsCheck.matched.length}건 — leerness constraints check 로 상세 확인`);
|
|
204
|
+
}
|
|
205
|
+
} catch {}
|
|
206
|
+
|
|
207
|
+
// 7) proceed 권장 (충돌 critical 시 false)
|
|
208
|
+
const proceed = conflictHints.length < 3 && planConflicts.length === 0;
|
|
209
|
+
|
|
210
|
+
const dt = Date.now() - t0;
|
|
211
|
+
const out = {
|
|
212
|
+
request: text,
|
|
213
|
+
estimatedType,
|
|
214
|
+
conflicts: conflictHints,
|
|
215
|
+
reuseCandidates,
|
|
216
|
+
lessonsRecall,
|
|
217
|
+
planConflicts,
|
|
218
|
+
featureConflicts,
|
|
219
|
+
recommendedSteps,
|
|
220
|
+
efficiencyHints,
|
|
221
|
+
platformConstraints: constraintsCheck.matched,
|
|
222
|
+
constraintSuggestions: constraintsCheck.suggestions,
|
|
223
|
+
proceed,
|
|
224
|
+
proceedReason: proceed ? '안전 — 충돌 신호 < 3 + plan 충돌 0' : '⚠ 충돌 critical — 사용자 확인 후 진행',
|
|
225
|
+
durationMs: dt
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
try { _recordRun(root, { kind: 'review_request', estimatedType, conflicts: conflictHints.length, reuse: reuseCandidates.length, durationMs: dt, ok: true }); } catch {}
|
|
229
|
+
|
|
230
|
+
if (has('--json')) {
|
|
231
|
+
log(JSON.stringify(out, null, 2));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
log(`# leerness review-request (1.9.176 사전 검토)`);
|
|
236
|
+
log(`요청: "${text.slice(0, 200)}${text.length > 200 ? '…' : ''}"`);
|
|
237
|
+
log(`추정 작업 유형: ${estimatedType}`);
|
|
238
|
+
log('');
|
|
239
|
+
if (conflictHints.length) {
|
|
240
|
+
log(`## ⚠ 충돌 신호 (${conflictHints.length})`);
|
|
241
|
+
conflictHints.slice(0, 5).forEach(c => log(` - [${c.kind}] ${c.title || c.id || ''} ${c.preview || ''}`.trim()));
|
|
242
|
+
log('');
|
|
243
|
+
}
|
|
244
|
+
if (reuseCandidates.length) {
|
|
245
|
+
log(`## 🔁 재사용 후보 (${reuseCandidates.length}) — 신규 구현 전 검토`);
|
|
246
|
+
reuseCandidates.slice(0, 5).forEach(r => {
|
|
247
|
+
if (r.kind === 'skill') log(` - [skill] ${r.id}${r.displayNameKo ? ' · ' + r.displayNameKo : ''}`);
|
|
248
|
+
else if (r.kind === 'reuse-map') log(` - [reuse] ${r.capability} @ ${r.where}`);
|
|
249
|
+
});
|
|
250
|
+
log('');
|
|
251
|
+
}
|
|
252
|
+
if (lessonsRecall.length) {
|
|
253
|
+
log(`## 🧠 과거 컨텍스트 (${lessonsRecall.length}) — 관련 결정/교훈`);
|
|
254
|
+
lessonsRecall.slice(0, 3).forEach(l => log(` - [${l.kind}] ${l.title || l.preview}`));
|
|
255
|
+
log('');
|
|
256
|
+
}
|
|
257
|
+
if (planConflicts.length || featureConflicts.length) {
|
|
258
|
+
log(`## 📋 진행 중 영역 (${planConflicts.length + featureConflicts.length})`);
|
|
259
|
+
planConflicts.forEach(m => log(` - [milestone] ${m.id}: ${m.title}`));
|
|
260
|
+
featureConflicts.slice(0, 5).forEach(f => log(` - [feature] ${f.id}: ${f.title}`));
|
|
261
|
+
log('');
|
|
262
|
+
}
|
|
263
|
+
log(`## 💡 효율 제안`);
|
|
264
|
+
efficiencyHints.forEach(h => log(` ${h}`));
|
|
265
|
+
log('');
|
|
266
|
+
// 1.9.208: 플랫폼/API 제약 사전 노출 (사용자 명시)
|
|
267
|
+
if (constraintsCheck.matched.length > 0) {
|
|
268
|
+
log(`## 🚦 플랫폼/API 제약 사전 체크 (${constraintsCheck.matched.length})`);
|
|
269
|
+
for (const m of constraintsCheck.matched) {
|
|
270
|
+
log(` - 📦 ${m.platform} (docs: ${m.docs || '-'})`);
|
|
271
|
+
for (const c of (m.constraints || []).slice(0, 3)) {
|
|
272
|
+
log(` • [${c.kind}] ${c.detail}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
log(` → leerness constraints check "${text.slice(0, 40)}…" 로 상세 확인`);
|
|
276
|
+
log('');
|
|
277
|
+
}
|
|
278
|
+
if (recommendedSteps.length) {
|
|
279
|
+
log(`## 📍 권장 단계 (${estimatedType})`);
|
|
280
|
+
recommendedSteps.forEach(s => log(` ${s}`));
|
|
281
|
+
log('');
|
|
282
|
+
}
|
|
283
|
+
log(`## ▶ 진행 권장: ${proceed ? '✓ 진행 안전' : '⚠ 사용자 확인 필요'}`);
|
|
284
|
+
log(` 사유: ${out.proceedReason}`);
|
|
285
|
+
log(` 분석 소요: ${dt}ms`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
module.exports = { reviewRequestCmd };
|