leerness 1.9.418 → 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 +44 -0
- package/README.md +5 -5
- package/bin/{harness.js → leerness.js} +27 -279
- package/docs/PUBLISH_PRECHECK.md +93 -93
- package/lib/review-request.js +288 -0
- package/package.json +6 -6
- package/scripts/e2e.js +35 -35
- package/scripts/smoke.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
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
|
+
|
|
22
|
+
## 1.9.419 — 2026-06-07 — 엔트리 파일명 변경: bin/harness.js → bin/leerness.js (네이밍 일관성, UR-0126)
|
|
23
|
+
|
|
24
|
+
**📛 패키지 엔트리 파일명을 `bin/harness.js` → `bin/leerness.js` 로 변경 — 명령어(`leerness`)와 파일명 일치.**
|
|
25
|
+
|
|
26
|
+
### 배경
|
|
27
|
+
파일명 `harness.js` 는 초기 잔재로, 명령어/패키지명(`leerness`)과 불일치했습니다. 3-에이전트 실현가능성 조사 결과 **EASY/안전**(명령어는 이미 `leerness`, 내부 참조는 `__filename`/`path.resolve` 기반이라 자동 대응)으로 판정되어 실행.
|
|
28
|
+
|
|
29
|
+
### 변경
|
|
30
|
+
- `git mv bin/harness.js bin/leerness.js` (히스토리 보존).
|
|
31
|
+
- `package.json`: `bin.leerness` / `main` / npm scripts(test/test:fast/prepack) → `bin/leerness.js`.
|
|
32
|
+
- `scripts/e2e.js`(35건)·`scripts/smoke.js`·`.github/workflows/ci.yml`·`docs/PUBLISH_PRECHECK.md` 경로 참조 갱신.
|
|
33
|
+
- 내부 self-invocation/진단(`harnessPath: __filename`)·selftest(`read(__filename)`)는 런타임 자동 — 무변경.
|
|
34
|
+
- next-action 제안 문자열 1건 `node ./bin/harness.js whats-new` → `leerness whats-new`.
|
|
35
|
+
|
|
36
|
+
### 사용자 영향
|
|
37
|
+
**없음** — 설치/실행은 `leerness` 명령(불변). 파일을 직접 호출하지 않음. 기존 워크스페이스(.harness)·상태도 불변.
|
|
38
|
+
|
|
39
|
+
### 검증 (회귀 0)
|
|
40
|
+
- **selftest 164→165 PASS** (신규 구조 가드: bin 파일=leerness.js + package.json bin/main 일치).
|
|
41
|
+
- **E2E 418→419 PASS** (CLI 경로 변경 후 전체 스모크).
|
|
42
|
+
|
|
43
|
+
### 참고 (조사 결과 — 다음 라운드)
|
|
44
|
+
- harness.js 무거움(21k줄) → UR-0125 점진 모듈화(handoff 등 DI 추출).
|
|
45
|
+
- `.harness`→`.leerness` 폴더명 → UR-0127 **직접 변경 비권장**(기존 .leerness 상태 substrate 충돌) — 상태기판 격리(대안 A) 권장.
|
|
46
|
+
|
|
3
47
|
## 1.9.418 — 2026-06-07 — health 보안 정직화 + status "healthy" 의미 명시 (9번째 외부평가 Codex P2, UR-0121 잔여)
|
|
4
48
|
|
|
5
49
|
**🩺 `health`/`status` 의 "healthy" 가 프로젝트 안전을 뜻하는 듯 오해되던 것(Codex P2) 보강 — 정직성 일관 적용.**
|
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.418는 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
|
|
|
@@ -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') 시 호스트 프로세스 오염.
|
|
@@ -3072,6 +3072,28 @@ function _selfTestCases() {
|
|
|
3072
3072
|
return handoffWired && scanJson && encJson && contractExit && behav;
|
|
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
|
+
{ 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
|
+
} },
|
|
3075
3097
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
|
|
3076
3098
|
];
|
|
3077
3099
|
}
|
|
@@ -5709,7 +5731,7 @@ function _suggestNextActions(root, latestRow, keyword) {
|
|
|
5709
5731
|
// 6) CHANGELOG / README 버전 일치 확인 (1.9.X bump 후 자주 누락되는 단계)
|
|
5710
5732
|
try {
|
|
5711
5733
|
if (/(\d\.\d\.\d+)/.test(taskText) || /(bump|version|release)/i.test(taskText)) {
|
|
5712
|
-
actions.push({ icon: '📝', title: `버전 변동 task — CHANGELOG.md / README.md 갱신 확인`, command: `
|
|
5734
|
+
actions.push({ icon: '📝', title: `버전 변동 task — CHANGELOG.md / README.md 갱신 확인`, command: `leerness whats-new .` });
|
|
5713
5735
|
}
|
|
5714
5736
|
} catch {}
|
|
5715
5737
|
|
|
@@ -20417,283 +20439,9 @@ function lspCmd(root, sub, ...args) {
|
|
|
20417
20439
|
//
|
|
20418
20440
|
// REPL: :review <request> (1.9.175 slash 패턴)
|
|
20419
20441
|
// MCP : leerness_review_request (외부 AI 직접 호출)
|
|
20420
|
-
|
|
20421
|
-
|
|
20422
|
-
|
|
20423
|
-
return fail('leerness review-request "<request>" — 사용자 요청 텍스트 필요');
|
|
20424
|
-
}
|
|
20425
|
-
const t0 = Date.now();
|
|
20426
|
-
const text = String(request).trim();
|
|
20427
|
-
|
|
20428
|
-
// 1) 작업 유형 추정 (route 기반 키워드 매핑)
|
|
20429
|
-
const lower = text.toLowerCase();
|
|
20430
|
-
const routeKw = {
|
|
20431
|
-
bugfix: ['버그', '오류', '에러', '수정', '고쳐', '실패', 'fix', 'bug', 'error'],
|
|
20432
|
-
refactor: ['리팩토', '재구성', '정리', '개선', 'refactor', 'cleanup'],
|
|
20433
|
-
feature: ['추가', '구현', '만들', '새', '기능', 'add', 'implement', 'feature', 'create', 'new'],
|
|
20434
|
-
research: ['조사', '분석', '비교', '검토', '연구', 'research', 'analyze', 'compare', 'investigate'],
|
|
20435
|
-
planning: ['계획', '설계', '로드맵', 'plan', 'design', 'architecture', 'roadmap'],
|
|
20436
|
-
release: ['배포', '릴리즈', '버전', 'release', 'deploy', 'publish'],
|
|
20437
|
-
consistency: ['일관성', '통합', '동기화', '맞춰', 'consistency', 'sync', 'align']
|
|
20438
|
-
};
|
|
20439
|
-
let estimatedType = 'feature'; // default
|
|
20440
|
-
let maxScore = 0;
|
|
20441
|
-
for (const [type, kws] of Object.entries(routeKw)) {
|
|
20442
|
-
const score = kws.filter(k => lower.includes(k)).length;
|
|
20443
|
-
if (score > maxScore) { maxScore = score; estimatedType = type; }
|
|
20444
|
-
}
|
|
20445
|
-
|
|
20446
|
-
// 2) 기존 자원 회수 — brainstorm spawn (모든 surface 통합 회수)
|
|
20447
|
-
const conflictHints = []; // ⚠ 같은 키워드 + 실패/오류 패턴
|
|
20448
|
-
const reuseCandidates = []; // 🔁 기존 skill / reuse-map / decision 후보
|
|
20449
|
-
const lessonsRecall = []; // 🧠 과거 lesson
|
|
20450
|
-
const planConflicts = []; // 📋 진행 중 milestone과 충돌 가능
|
|
20451
|
-
|
|
20452
|
-
// brainstorm 호출 (1.9.13~) — JSON 결과 회수
|
|
20453
|
-
try {
|
|
20454
|
-
const r = cp.spawnSync(process.execPath, [__filename, 'brainstorm', text, '--path', root, '--json'], {
|
|
20455
|
-
encoding: 'utf8', timeout: 12000,
|
|
20456
|
-
env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_BANNER: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' }
|
|
20457
|
-
});
|
|
20458
|
-
if (r.stdout) {
|
|
20459
|
-
const j = JSON.parse(r.stdout);
|
|
20460
|
-
const hits = j.hits || {};
|
|
20461
|
-
// decisions — 과거 결정 후보
|
|
20462
|
-
(hits.decisions || []).slice(0, 5).forEach(d => {
|
|
20463
|
-
lessonsRecall.push({ kind: 'decision', title: d.title, line: d.line, preview: (d.preview || '').slice(0, 100) });
|
|
20464
|
-
});
|
|
20465
|
-
// lessons — 과거 교훈 (특히 실패 키워드)
|
|
20466
|
-
(hits.lessons || []).slice(0, 5).forEach(l => {
|
|
20467
|
-
const preview = (l.text || l.preview || '').slice(0, 100);
|
|
20468
|
-
const isFailure = /실패|오류|에러|fail|error|bug|문제|warning/i.test(preview);
|
|
20469
|
-
if (isFailure) {
|
|
20470
|
-
conflictHints.push({ kind: 'lesson-failure', preview, tags: l.tags });
|
|
20471
|
-
} else {
|
|
20472
|
-
lessonsRecall.push({ kind: 'lesson', preview, tags: l.tags });
|
|
20473
|
-
}
|
|
20474
|
-
});
|
|
20475
|
-
// skills — 기존 skill 후보
|
|
20476
|
-
(hits.skills || []).slice(0, 3).forEach(s => {
|
|
20477
|
-
reuseCandidates.push({ kind: 'skill', id: s.id, displayNameKo: s.displayNameKo, capabilities: s.capabilities });
|
|
20478
|
-
});
|
|
20479
|
-
// tasks — 진행 중 task 충돌
|
|
20480
|
-
(hits.tasks || []).slice(0, 3).forEach(tsk => {
|
|
20481
|
-
if (tsk.status && /in-progress|진행/.test(String(tsk.status))) {
|
|
20482
|
-
conflictHints.push({ kind: 'task-in-progress', id: tsk.id, title: tsk.title });
|
|
20483
|
-
}
|
|
20484
|
-
});
|
|
20485
|
-
// plan milestones — 진행 중 milestone
|
|
20486
|
-
(hits.planMilestones || []).slice(0, 3).forEach(m => {
|
|
20487
|
-
if (m.status && /in-progress|진행/.test(String(m.status))) {
|
|
20488
|
-
planConflicts.push({ kind: 'milestone-in-progress', id: m.id, title: m.title });
|
|
20489
|
-
}
|
|
20490
|
-
});
|
|
20491
|
-
// taskLogFails — 과거 같은 키워드 실패 흔적
|
|
20492
|
-
(hits.taskLogFails || []).slice(0, 3).forEach(f => {
|
|
20493
|
-
conflictHints.push({ kind: 'task-log-failure', preview: (f.preview || f.text || '').slice(0, 100) });
|
|
20494
|
-
});
|
|
20495
|
-
}
|
|
20496
|
-
} catch {}
|
|
20497
|
-
|
|
20498
|
-
// 3) reuse-map 매칭 — 기존 capability 등록 후보
|
|
20499
|
-
try {
|
|
20500
|
-
const reusePath = path.join(root, '.harness/reuse-map.md');
|
|
20501
|
-
if (exists(reusePath)) {
|
|
20502
|
-
const reuseLines = read(reusePath).split('\n');
|
|
20503
|
-
const tokens = lower.split(/\s+/).filter(t => t.length >= 3);
|
|
20504
|
-
for (const line of reuseLines) {
|
|
20505
|
-
if (!/^\| /.test(line)) continue; // 테이블 row만
|
|
20506
|
-
const ll = line.toLowerCase();
|
|
20507
|
-
const matched = tokens.filter(t => ll.includes(t)).length;
|
|
20508
|
-
if (matched > 0) {
|
|
20509
|
-
const cols = line.split('|').map(s => s.trim());
|
|
20510
|
-
if (cols[1]) {
|
|
20511
|
-
reuseCandidates.push({ kind: 'reuse-map', capability: cols[1], where: cols[2] || '', note: cols[3] || '' });
|
|
20512
|
-
}
|
|
20513
|
-
}
|
|
20514
|
-
}
|
|
20515
|
-
}
|
|
20516
|
-
} catch {}
|
|
20517
|
-
|
|
20518
|
-
// 4) feature_graph — 같은 영역 변경 가능성
|
|
20519
|
-
const featureConflicts = [];
|
|
20520
|
-
try {
|
|
20521
|
-
const fgPath = path.join(root, '.harness/feature_graph.md');
|
|
20522
|
-
if (exists(fgPath)) {
|
|
20523
|
-
const fg = read(fgPath);
|
|
20524
|
-
const tokens = lower.split(/\s+/).filter(t => t.length >= 4);
|
|
20525
|
-
// F-XXXX 노드 라인 추출
|
|
20526
|
-
const nodeBlocks = fg.split(/\n### /);
|
|
20527
|
-
for (const blk of nodeBlocks.slice(1)) {
|
|
20528
|
-
const bl = blk.toLowerCase();
|
|
20529
|
-
const matched = tokens.filter(t => bl.includes(t)).length;
|
|
20530
|
-
if (matched > 0) {
|
|
20531
|
-
const titleMatch = blk.match(/^([^\n]+)/);
|
|
20532
|
-
const idMatch = blk.match(/F-\d+/);
|
|
20533
|
-
if (titleMatch && idMatch) {
|
|
20534
|
-
featureConflicts.push({ kind: 'feature', id: idMatch[0], title: titleMatch[1].trim() });
|
|
20535
|
-
}
|
|
20536
|
-
}
|
|
20537
|
-
}
|
|
20538
|
-
}
|
|
20539
|
-
} catch {}
|
|
20540
|
-
|
|
20541
|
-
// 5) 권장 단계 (작업 유형별)
|
|
20542
|
-
const recommendedSteps = {
|
|
20543
|
-
feature: [
|
|
20544
|
-
'1) leerness reuse-check "<기능>" — 외부 OSS 빌드 vs 재사용 판단 (1.9.285)',
|
|
20545
|
-
'2) leerness reuse find "<핵심 capability>" — 내부 중복 구현 사전 차단',
|
|
20546
|
-
'3) leerness plan add "<milestone>" — 진행 추적',
|
|
20547
|
-
'4) leerness contract verify SPEC.md src/<mod>.js — 사양 ↔ 구현 일치 검증',
|
|
20548
|
-
'5) verify-claim --run-tests 로 evidence 의무화'
|
|
20549
|
-
],
|
|
20550
|
-
bugfix: [
|
|
20551
|
-
'1) leerness brainstorm "<버그 키워드>" — 과거 같은 영역 lesson 회수',
|
|
20552
|
-
'2) leerness verify-claim T-XXX --strict-claims — 낙관적 표시 사전 감지',
|
|
20553
|
-
'3) verify-code --run-tests — 재현 + fix 검증',
|
|
20554
|
-
'4) leerness lesson save "<root cause>" — 같은 실수 재발 차단'
|
|
20555
|
-
],
|
|
20556
|
-
refactor: [
|
|
20557
|
-
'1) leerness reuse-map — 영향 범위 파악',
|
|
20558
|
-
'2) leerness impact <file> — 강한/약한 참조 분리',
|
|
20559
|
-
'3) leerness contract verify — 외부 인터페이스 보존 확인',
|
|
20560
|
-
'4) verify-code --run-tests + 회귀 테스트'
|
|
20561
|
-
],
|
|
20562
|
-
research: [
|
|
20563
|
-
'1) leerness brainstorm "<주제>" — 누적 컨텍스트 회수',
|
|
20564
|
-
'2) leerness lessons --query "<주제>" — 과거 같은 영역 결정',
|
|
20565
|
-
'3) leerness review <file> --persona research — 깊이 검토',
|
|
20566
|
-
'4) leerness decision add "<결론>" — 회수 가능하게 영구화'
|
|
20567
|
-
],
|
|
20568
|
-
planning: [
|
|
20569
|
-
'1) leerness plan add "<milestone>" — 분해 시작',
|
|
20570
|
-
'2) leerness reuse-map — 기존 자원 인벤토리',
|
|
20571
|
-
'3) leerness agents recommend planning — sub-agent 분배',
|
|
20572
|
-
'4) leerness session close — 결정 영구화'
|
|
20573
|
-
],
|
|
20574
|
-
release: [
|
|
20575
|
-
'1) leerness health — production-ready 확인',
|
|
20576
|
-
'2) leerness audit + verify-code — 보안 + 검수',
|
|
20577
|
-
'3) leerness release bump + note + publish'
|
|
20578
|
-
],
|
|
20579
|
-
consistency: [
|
|
20580
|
-
'1) leerness audit — design/reuse/handoff 일관성 검사',
|
|
20581
|
-
'2) leerness consistency check — 잠재 일관성 위반',
|
|
20582
|
-
'3) leerness drift check --auto-fix — 자동 회복'
|
|
20583
|
-
]
|
|
20584
|
-
}[estimatedType] || [];
|
|
20585
|
-
|
|
20586
|
-
// 6) 효율 제안 (적용 가능한 sub-agent + skill)
|
|
20587
|
-
const efficiencyHints = [];
|
|
20588
|
-
if (reuseCandidates.length > 0) {
|
|
20589
|
-
efficiencyHints.push(`🔁 기존 자원 ${reuseCandidates.length}건 발견 — 신규 구현 전 재사용 검토 권장`);
|
|
20590
|
-
}
|
|
20591
|
-
if (conflictHints.length > 0) {
|
|
20592
|
-
efficiencyHints.push(`⚠ 충돌 신호 ${conflictHints.length}건 — 과거 실패 lesson / 진행 중 task 확인 필요`);
|
|
20593
|
-
}
|
|
20594
|
-
if (planConflicts.length > 0) {
|
|
20595
|
-
efficiencyHints.push(`📋 진행 중 milestone ${planConflicts.length}건과 영역 겹침 가능 — plan 정렬 권장`);
|
|
20596
|
-
}
|
|
20597
|
-
if (featureConflicts.length > 0) {
|
|
20598
|
-
efficiencyHints.push(`🕸 Feature Graph ${featureConflicts.length}건 영역 겹침 — 의존성 사전 확인`);
|
|
20599
|
-
}
|
|
20600
|
-
// 다중 에이전트 분배 추천
|
|
20601
|
-
if (estimatedType === 'feature' || estimatedType === 'planning') {
|
|
20602
|
-
efficiencyHints.push(`👥 leerness agents recommend ${estimatedType} — 작업 유형별 sub-agent 매핑 활용 가능`);
|
|
20603
|
-
}
|
|
20604
|
-
if (efficiencyHints.length === 0) {
|
|
20605
|
-
efficiencyHints.push('✨ 충돌 신호 없음 — 즉시 진행 안전');
|
|
20606
|
-
}
|
|
20607
|
-
|
|
20608
|
-
// 6.5) 1.9.208: 플랫폼/API 제약 사전 체크 — 사용자 명시 ("호출속도 초당 5회" 같은 규정 사전 확인)
|
|
20609
|
-
let constraintsCheck = { matched: [], suggestions: [] };
|
|
20610
|
-
try {
|
|
20611
|
-
constraintsCheck = _checkRequestConstraints(root, text);
|
|
20612
|
-
if (constraintsCheck.matched.length > 0) {
|
|
20613
|
-
efficiencyHints.push(`⚠ 플랫폼 제약 ${constraintsCheck.matched.length}건 — leerness constraints check 로 상세 확인`);
|
|
20614
|
-
}
|
|
20615
|
-
} catch {}
|
|
20616
|
-
|
|
20617
|
-
// 7) proceed 권장 (충돌 critical 시 false)
|
|
20618
|
-
const proceed = conflictHints.length < 3 && planConflicts.length === 0;
|
|
20619
|
-
|
|
20620
|
-
const dt = Date.now() - t0;
|
|
20621
|
-
const out = {
|
|
20622
|
-
request: text,
|
|
20623
|
-
estimatedType,
|
|
20624
|
-
conflicts: conflictHints,
|
|
20625
|
-
reuseCandidates,
|
|
20626
|
-
lessonsRecall,
|
|
20627
|
-
planConflicts,
|
|
20628
|
-
featureConflicts,
|
|
20629
|
-
recommendedSteps,
|
|
20630
|
-
efficiencyHints,
|
|
20631
|
-
platformConstraints: constraintsCheck.matched,
|
|
20632
|
-
constraintSuggestions: constraintsCheck.suggestions,
|
|
20633
|
-
proceed,
|
|
20634
|
-
proceedReason: proceed ? '안전 — 충돌 신호 < 3 + plan 충돌 0' : '⚠ 충돌 critical — 사용자 확인 후 진행',
|
|
20635
|
-
durationMs: dt
|
|
20636
|
-
};
|
|
20637
|
-
|
|
20638
|
-
try { _recordRun(root, { kind: 'review_request', estimatedType, conflicts: conflictHints.length, reuse: reuseCandidates.length, durationMs: dt, ok: true }); } catch {}
|
|
20639
|
-
|
|
20640
|
-
if (has('--json')) {
|
|
20641
|
-
log(JSON.stringify(out, null, 2));
|
|
20642
|
-
return;
|
|
20643
|
-
}
|
|
20644
|
-
|
|
20645
|
-
log(`# leerness review-request (1.9.176 사전 검토)`);
|
|
20646
|
-
log(`요청: "${text.slice(0, 200)}${text.length > 200 ? '…' : ''}"`);
|
|
20647
|
-
log(`추정 작업 유형: ${estimatedType}`);
|
|
20648
|
-
log('');
|
|
20649
|
-
if (conflictHints.length) {
|
|
20650
|
-
log(`## ⚠ 충돌 신호 (${conflictHints.length})`);
|
|
20651
|
-
conflictHints.slice(0, 5).forEach(c => log(` - [${c.kind}] ${c.title || c.id || ''} ${c.preview || ''}`.trim()));
|
|
20652
|
-
log('');
|
|
20653
|
-
}
|
|
20654
|
-
if (reuseCandidates.length) {
|
|
20655
|
-
log(`## 🔁 재사용 후보 (${reuseCandidates.length}) — 신규 구현 전 검토`);
|
|
20656
|
-
reuseCandidates.slice(0, 5).forEach(r => {
|
|
20657
|
-
if (r.kind === 'skill') log(` - [skill] ${r.id}${r.displayNameKo ? ' · ' + r.displayNameKo : ''}`);
|
|
20658
|
-
else if (r.kind === 'reuse-map') log(` - [reuse] ${r.capability} @ ${r.where}`);
|
|
20659
|
-
});
|
|
20660
|
-
log('');
|
|
20661
|
-
}
|
|
20662
|
-
if (lessonsRecall.length) {
|
|
20663
|
-
log(`## 🧠 과거 컨텍스트 (${lessonsRecall.length}) — 관련 결정/교훈`);
|
|
20664
|
-
lessonsRecall.slice(0, 3).forEach(l => log(` - [${l.kind}] ${l.title || l.preview}`));
|
|
20665
|
-
log('');
|
|
20666
|
-
}
|
|
20667
|
-
if (planConflicts.length || featureConflicts.length) {
|
|
20668
|
-
log(`## 📋 진행 중 영역 (${planConflicts.length + featureConflicts.length})`);
|
|
20669
|
-
planConflicts.forEach(m => log(` - [milestone] ${m.id}: ${m.title}`));
|
|
20670
|
-
featureConflicts.slice(0, 5).forEach(f => log(` - [feature] ${f.id}: ${f.title}`));
|
|
20671
|
-
log('');
|
|
20672
|
-
}
|
|
20673
|
-
log(`## 💡 효율 제안`);
|
|
20674
|
-
efficiencyHints.forEach(h => log(` ${h}`));
|
|
20675
|
-
log('');
|
|
20676
|
-
// 1.9.208: 플랫폼/API 제약 사전 노출 (사용자 명시)
|
|
20677
|
-
if (constraintsCheck.matched.length > 0) {
|
|
20678
|
-
log(`## 🚦 플랫폼/API 제약 사전 체크 (${constraintsCheck.matched.length})`);
|
|
20679
|
-
for (const m of constraintsCheck.matched) {
|
|
20680
|
-
log(` - 📦 ${m.platform} (docs: ${m.docs || '-'})`);
|
|
20681
|
-
for (const c of (m.constraints || []).slice(0, 3)) {
|
|
20682
|
-
log(` • [${c.kind}] ${c.detail}`);
|
|
20683
|
-
}
|
|
20684
|
-
}
|
|
20685
|
-
log(` → leerness constraints check "${text.slice(0, 40)}…" 로 상세 확인`);
|
|
20686
|
-
log('');
|
|
20687
|
-
}
|
|
20688
|
-
if (recommendedSteps.length) {
|
|
20689
|
-
log(`## 📍 권장 단계 (${estimatedType})`);
|
|
20690
|
-
recommendedSteps.forEach(s => log(` ${s}`));
|
|
20691
|
-
log('');
|
|
20692
|
-
}
|
|
20693
|
-
log(`## ▶ 진행 권장: ${proceed ? '✓ 진행 안전' : '⚠ 사용자 확인 필요'}`);
|
|
20694
|
-
log(` 사유: ${out.proceedReason}`);
|
|
20695
|
-
log(` 분석 소요: ${dt}ms`);
|
|
20696
|
-
}
|
|
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 }); }
|
|
20697
20445
|
|
|
20698
20446
|
// 1.9.164: leerness which — 진단 도구 (구버전 충돌 / npx 캐시 / PATH 충돌 해결)
|
|
20699
20447
|
// 사용자가 "최신 버전 작동 안 함" 의심 시: 실제 실행 중인 leerness 의 경로 / 버전 / npm 캐시 / PATH 후보 표시.
|
package/docs/PUBLISH_PRECHECK.md
CHANGED
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
# Publish Pre-check (leerness 1.9.0)
|
|
2
|
-
|
|
3
|
-
> ⚠ **Owner 권한 필수**: npm `leerness`의 메인테이너는 `gytlrgpfl <gytlrgpfl96@gmail.com>` 입니다. 이 계정으로 로그인되어 있거나 collaborator로 등록되어 있어야 publish가 통과합니다. 권한이 없으면 `E403 Forbidden`이 떨어집니다.
|
|
4
|
-
|
|
5
|
-
## 1. 메타데이터 보강 (선택)
|
|
6
|
-
|
|
7
|
-
`package.json`의 다음 필드는 비어 있거나 일반적입니다. 사용자 정보로 채우는 것을 권장합니다.
|
|
8
|
-
|
|
9
|
-
```json
|
|
10
|
-
{
|
|
11
|
-
"author": "leerness contributors",
|
|
12
|
-
"repository": { /* 비어있음 */ },
|
|
13
|
-
"bugs": { /* 비어있음 */ },
|
|
14
|
-
"homepage": ""
|
|
15
|
-
}
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
예:
|
|
19
|
-
|
|
20
|
-
```json
|
|
21
|
-
"author": "Your Name <you@example.com>",
|
|
22
|
-
"repository": { "type": "git", "url": "git+https://github.com/<user>/leerness.git" },
|
|
23
|
-
"bugs": { "url": "https://github.com/<user>/leerness/issues" },
|
|
24
|
-
"homepage": "https://github.com/<user>/leerness#readme"
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
publish 필수는 아니지만, npm 페이지 신뢰도와 사용자 경험을 위해 채워두면 좋습니다.
|
|
28
|
-
|
|
29
|
-
## 2. 권한 확인
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
npm whoami
|
|
33
|
-
# → 응답이 leerness의 owner인지 확인
|
|
34
|
-
npm owner ls leerness
|
|
35
|
-
# → 자신의 계정이 목록에 있는지 확인
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
권한이 없으면 collaborator 추가를 owner에게 요청해야 합니다.
|
|
39
|
-
|
|
40
|
-
## 3. 로컬 검증
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
node ./bin/
|
|
44
|
-
npm pack --dry-run # 패키지 내용 미리보기
|
|
45
|
-
node ./scripts/e2e.js # 30+개 시나리오 통과
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## 4. 1.8.0 → 1.9.0 자동 업그레이드 시연 (선택)
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
mkdir /tmp/lr-old; cd /tmp/lr-old
|
|
52
|
-
npx -y leerness@1.8.0 init . --language ko --skills recommended
|
|
53
|
-
LEERNESS_OFFLINE=1 npx -y -p /path/to/leerness-1.9.0.tgz leerness update . --yes
|
|
54
|
-
cat .harness/HARNESS_VERSION # → 1.9.0
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
## 5. publish dry-run
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
npm publish --dry-run
|
|
61
|
-
# 또는
|
|
62
|
-
npm publish leerness-1.9.0.tgz --dry-run
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
확인:
|
|
66
|
-
- `package.json#files`와 실제 tarball 내용 일치
|
|
67
|
-
- 총 크기 (≈30~40 kB)
|
|
68
|
-
- bin →
|
|
69
|
-
|
|
70
|
-
## 6. 실 publish
|
|
71
|
-
|
|
72
|
-
```bash
|
|
73
|
-
npm login # 권한 있는 계정으로
|
|
74
|
-
npm publish --access public
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
## 7. publish 후 검증
|
|
78
|
-
|
|
79
|
-
```bash
|
|
80
|
-
npm view leerness version # → 1.9.0
|
|
81
|
-
npx -y leerness@latest --version # → 1.9.0
|
|
82
|
-
mkdir test-install && cd test-install
|
|
83
|
-
npx -y leerness@latest init . --yes --language ko --skills recommended
|
|
84
|
-
npx -y leerness@latest verify .
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
## 8. 롤백
|
|
88
|
-
|
|
89
|
-
24시간 이내라면 `npm unpublish leerness@1.9.0` 가능. 이후엔 1.9.1 패치 또는 deprecate.
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
npm deprecate leerness@1.9.0 "Use 1.9.1+"
|
|
93
|
-
```
|
|
1
|
+
# Publish Pre-check (leerness 1.9.0)
|
|
2
|
+
|
|
3
|
+
> ⚠ **Owner 권한 필수**: npm `leerness`의 메인테이너는 `gytlrgpfl <gytlrgpfl96@gmail.com>` 입니다. 이 계정으로 로그인되어 있거나 collaborator로 등록되어 있어야 publish가 통과합니다. 권한이 없으면 `E403 Forbidden`이 떨어집니다.
|
|
4
|
+
|
|
5
|
+
## 1. 메타데이터 보강 (선택)
|
|
6
|
+
|
|
7
|
+
`package.json`의 다음 필드는 비어 있거나 일반적입니다. 사용자 정보로 채우는 것을 권장합니다.
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"author": "leerness contributors",
|
|
12
|
+
"repository": { /* 비어있음 */ },
|
|
13
|
+
"bugs": { /* 비어있음 */ },
|
|
14
|
+
"homepage": ""
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
예:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
"author": "Your Name <you@example.com>",
|
|
22
|
+
"repository": { "type": "git", "url": "git+https://github.com/<user>/leerness.git" },
|
|
23
|
+
"bugs": { "url": "https://github.com/<user>/leerness/issues" },
|
|
24
|
+
"homepage": "https://github.com/<user>/leerness#readme"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
publish 필수는 아니지만, npm 페이지 신뢰도와 사용자 경험을 위해 채워두면 좋습니다.
|
|
28
|
+
|
|
29
|
+
## 2. 권한 확인
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm whoami
|
|
33
|
+
# → 응답이 leerness의 owner인지 확인
|
|
34
|
+
npm owner ls leerness
|
|
35
|
+
# → 자신의 계정이 목록에 있는지 확인
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
권한이 없으면 collaborator 추가를 owner에게 요청해야 합니다.
|
|
39
|
+
|
|
40
|
+
## 3. 로컬 검증
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
node ./bin/leerness.js --version # → 1.9.0
|
|
44
|
+
npm pack --dry-run # 패키지 내용 미리보기
|
|
45
|
+
node ./scripts/e2e.js # 30+개 시나리오 통과
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 4. 1.8.0 → 1.9.0 자동 업그레이드 시연 (선택)
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
mkdir /tmp/lr-old; cd /tmp/lr-old
|
|
52
|
+
npx -y leerness@1.8.0 init . --language ko --skills recommended
|
|
53
|
+
LEERNESS_OFFLINE=1 npx -y -p /path/to/leerness-1.9.0.tgz leerness update . --yes
|
|
54
|
+
cat .harness/HARNESS_VERSION # → 1.9.0
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## 5. publish dry-run
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm publish --dry-run
|
|
61
|
+
# 또는
|
|
62
|
+
npm publish leerness-1.9.0.tgz --dry-run
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
확인:
|
|
66
|
+
- `package.json#files`와 실제 tarball 내용 일치
|
|
67
|
+
- 총 크기 (≈30~40 kB)
|
|
68
|
+
- bin → leerness.js 매핑
|
|
69
|
+
|
|
70
|
+
## 6. 실 publish
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npm login # 권한 있는 계정으로
|
|
74
|
+
npm publish --access public
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## 7. publish 후 검증
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npm view leerness version # → 1.9.0
|
|
81
|
+
npx -y leerness@latest --version # → 1.9.0
|
|
82
|
+
mkdir test-install && cd test-install
|
|
83
|
+
npx -y leerness@latest init . --yes --language ko --skills recommended
|
|
84
|
+
npx -y leerness@latest verify .
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 8. 롤백
|
|
88
|
+
|
|
89
|
+
24시간 이내라면 `npm unpublish leerness@1.9.0` 가능. 이후엔 1.9.1 패치 또는 deprecate.
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npm deprecate leerness@1.9.0 "Use 1.9.1+"
|
|
93
|
+
```
|
|
@@ -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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.420",
|
|
4
4
|
"description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"leerness",
|
|
@@ -28,13 +28,13 @@
|
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"author": "leerness contributors",
|
|
30
30
|
"type": "commonjs",
|
|
31
|
-
"main": "bin/
|
|
31
|
+
"main": "bin/leerness.js",
|
|
32
32
|
"preferGlobal": true,
|
|
33
33
|
"engines": {
|
|
34
34
|
"node": ">=18"
|
|
35
35
|
},
|
|
36
36
|
"bin": {
|
|
37
|
-
"leerness": "bin/
|
|
37
|
+
"leerness": "bin/leerness.js"
|
|
38
38
|
},
|
|
39
39
|
"files": [
|
|
40
40
|
"bin",
|
|
@@ -47,10 +47,10 @@
|
|
|
47
47
|
"LICENSE"
|
|
48
48
|
],
|
|
49
49
|
"scripts": {
|
|
50
|
-
"test": "node ./bin/
|
|
51
|
-
"test:fast": "node ./bin/
|
|
50
|
+
"test": "node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e.js",
|
|
51
|
+
"test:fast": "node ./bin/leerness.js selftest && node ./scripts/smoke.js",
|
|
52
52
|
"test:smoke": "node ./scripts/e2e.js",
|
|
53
|
-
"prepack": "node ./bin/
|
|
53
|
+
"prepack": "node ./bin/leerness.js readme sync . && node ./bin/leerness.js --version"
|
|
54
54
|
},
|
|
55
55
|
"publishConfig": {
|
|
56
56
|
"access": "public"
|
package/scripts/e2e.js
CHANGED
|
@@ -11,7 +11,7 @@ process.env.LEERNESS_OFFLINE = process.env.LEERNESS_OFFLINE || '1';
|
|
|
11
11
|
// 1.9.284 (UR-0029): e2e 속도 — 기본 roadmap.html(70KB HTML) 자동 생성 OFF (roadmap 전용 테스트 블록만 일시 ON).
|
|
12
12
|
// 대부분의 init/session 테스트는 roadmap 을 검증하지 않으므로 생성 비용 제거 → 5분 내 완료.
|
|
13
13
|
process.env.LEERNESS_NO_AUTO_ROADMAP = '1';
|
|
14
|
-
const CLI = path.resolve(__dirname, '..', 'bin', '
|
|
14
|
+
const CLI = path.resolve(__dirname, '..', 'bin', 'leerness.js');
|
|
15
15
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-e2e-'));
|
|
16
16
|
let failed = 0; let total = 0;
|
|
17
17
|
const _e2eStart = Date.now(); // 1.9.284 (UR-0029): 총 소요시간 투명성
|
|
@@ -3081,7 +3081,7 @@ total++;
|
|
|
3081
3081
|
{
|
|
3082
3082
|
let ok = false;
|
|
3083
3083
|
try {
|
|
3084
|
-
const h = require(path.resolve(__dirname, '..', 'bin', '
|
|
3084
|
+
const h = require(path.resolve(__dirname, '..', 'bin', 'leerness.js'));
|
|
3085
3085
|
const q = h._shellQuoteArg('a; rm -rf / && echo $(whoami)');
|
|
3086
3086
|
const win = process.platform === 'win32';
|
|
3087
3087
|
// 따옴표로 감싸 메타문자가 단일 리터럴 인자가 됨 (POSIX 단일/Windows 이중)
|
|
@@ -3104,7 +3104,7 @@ total++;
|
|
|
3104
3104
|
"const survived=process.listeners('warning').includes(L);" +
|
|
3105
3105
|
"const polluted=(process.env.NODE_OPTIONS||'')!==o;" +
|
|
3106
3106
|
"process.exit(survived&&!polluted?0:1);";
|
|
3107
|
-
const harnessPath = path.resolve(__dirname, '..', 'bin', '
|
|
3107
|
+
const harnessPath = path.resolve(__dirname, '..', 'bin', 'leerness.js');
|
|
3108
3108
|
const r = cp.spawnSync(process.execPath, ['-e', probe, harnessPath], { encoding: 'utf8', timeout: 20000 });
|
|
3109
3109
|
ok = r.status === 0;
|
|
3110
3110
|
} catch {}
|
|
@@ -3118,14 +3118,14 @@ total++;
|
|
|
3118
3118
|
let ok = false;
|
|
3119
3119
|
try {
|
|
3120
3120
|
const reg = require(path.resolve(__dirname, '..', 'lib', 'agent-registry.js'));
|
|
3121
|
-
const h = require(path.resolve(__dirname, '..', 'bin', '
|
|
3121
|
+
const h = require(path.resolve(__dirname, '..', 'bin', 'leerness.js'));
|
|
3122
3122
|
const dataOk = Array.isArray(reg.EXTERNAL_AGENTS) && reg.EXTERNAL_AGENTS.length === 10 &&
|
|
3123
3123
|
reg.EXTERNAL_AGENTS.every(a => a.id && a.bin && a.envFlag) &&
|
|
3124
3124
|
reg.AGENT_SLASH_COMMANDS && Object.keys(reg.AGENT_SLASH_COMMANDS).length === 9;
|
|
3125
3125
|
// harness 가 모듈을 단일출처로 사용 (같은 객체 참조)
|
|
3126
3126
|
const singleSource = h.AGENT_SLASH_COMMANDS === reg.AGENT_SLASH_COMMANDS;
|
|
3127
3127
|
// harness.js 소스에 인라인 정의가 더 이상 없음 (모듈로 이동 완료)
|
|
3128
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
3128
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
3129
3129
|
const movedOut = !/const EXTERNAL_AGENTS = \[/.test(harnessSrc) && /require\('\.\.\/lib\/agent-registry'\)/.test(harnessSrc);
|
|
3130
3130
|
ok = dataOk && singleSource && movedOut;
|
|
3131
3131
|
} catch {}
|
|
@@ -3143,7 +3143,7 @@ total++;
|
|
|
3143
3143
|
const structOk = r.status === 0 && j.schemaVersion === 1 && !!j.version && !!j.project && ('currentTask' in j) &&
|
|
3144
3144
|
j.openRequests && typeof j.openRequests.count === 'number' && Array.isArray(j.recentDecisions) &&
|
|
3145
3145
|
Array.isArray(j.activeRules) && Array.isArray(j.nextActions) && j.memory && typeof j.memory.rulesActive === 'number';
|
|
3146
|
-
const h = require(path.resolve(__dirname, '..', 'bin', '
|
|
3146
|
+
const h = require(path.resolve(__dirname, '..', 'bin', 'leerness.js'));
|
|
3147
3147
|
const mcpOk = h._mcpToolCount && h._mcpToolCount() >= 80;
|
|
3148
3148
|
ok = structOk && mcpOk;
|
|
3149
3149
|
} catch {}
|
|
@@ -3188,7 +3188,7 @@ total++;
|
|
|
3188
3188
|
reg._PROVIDER_MODEL_CATALOG && Object.keys(reg._PROVIDER_MODEL_CATALOG).length === 10 &&
|
|
3189
3189
|
reg._ROLE_ALIASES && Object.keys(reg._ROLE_ALIASES).length >= 14 &&
|
|
3190
3190
|
reg._AGENT_ROLE_PROMPTS && reg._AGENT_ROLE_PROMPTS.actor;
|
|
3191
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
3191
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
3192
3192
|
const movedOut = !/const ROLE_CATALOG = \{/.test(harnessSrc) && /require\('\.\.\/lib\/role-catalog'\)/.test(harnessSrc);
|
|
3193
3193
|
// roles 명령이 여전히 동작 (모듈 require 후) — 회귀 방지
|
|
3194
3194
|
const rr = cp.spawnSync(process.execPath, [CLI, 'roles', 'list', '--path', tmp, '--json'], { cwd: tmp, encoding: 'utf8', timeout: 20000 });
|
|
@@ -3210,7 +3210,7 @@ total++;
|
|
|
3210
3210
|
reg.ADAPTERS && Object.keys(reg.ADAPTERS).length === 9 &&
|
|
3211
3211
|
Array.isArray(reg.REUSE_CATEGORIES) && reg.REUSE_CATEGORIES.length === 15 &&
|
|
3212
3212
|
Array.isArray(reg.REUSE_CHECKLIST) && reg.REUSE_CHECKLIST.length === 6;
|
|
3213
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
3213
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
3214
3214
|
const movedOut = !/const CAPABILITY_SURFACE = \{/.test(harnessSrc) && !/const ADAPTERS = \{/.test(harnessSrc) && /require\('\.\.\/lib\/catalogs'\)/.test(harnessSrc);
|
|
3215
3215
|
// 소비 명령 회귀: capabilities + reuse-check (카탈로그 require 후 동작)
|
|
3216
3216
|
const cap = cp.spawnSync(process.execPath, [CLI, 'capabilities', '--json'], { cwd: tmp, encoding: 'utf8', timeout: 20000 });
|
|
@@ -3253,12 +3253,12 @@ total++;
|
|
|
3253
3253
|
try {
|
|
3254
3254
|
const T = require(path.resolve(__dirname, '..', 'lib', 'mcp-tools.js'));
|
|
3255
3255
|
const dataOk = Array.isArray(T) && T.length >= 81 && T.every(t => t.name && t.description && t.inputSchema) && T[0].name === 'leerness_handoff';
|
|
3256
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
3256
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
3257
3257
|
const movedOut = !/const TOOLS = \[/.test(harnessSrc) && /require\('\.\.\/lib\/mcp-tools'\)/.test(harnessSrc);
|
|
3258
3258
|
// tools/list(라이브 MCP) == 모듈 length (단일출처 일치)
|
|
3259
3259
|
const ml = cp.spawnSync(process.execPath, [CLI, 'mcp', 'serve'], { cwd: tmp, encoding: 'utf8', timeout: 15000, input: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }) + '\n' });
|
|
3260
3260
|
const live = JSON.parse(ml.stdout.split('\n').filter(Boolean)[0]).result.tools.length;
|
|
3261
|
-
const h = require(path.resolve(__dirname, '..', 'bin', '
|
|
3261
|
+
const h = require(path.resolve(__dirname, '..', 'bin', 'leerness.js'));
|
|
3262
3262
|
const singleSource = live === T.length && h._mcpToolCount() === T.length;
|
|
3263
3263
|
ok = dataOk && movedOut && singleSource;
|
|
3264
3264
|
} catch {}
|
|
@@ -3325,7 +3325,7 @@ total++;
|
|
|
3325
3325
|
{
|
|
3326
3326
|
let ok = false;
|
|
3327
3327
|
try {
|
|
3328
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
3328
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
3329
3329
|
// (1) 소스: cp.exec 템플릿 제거 + execFile args 배열(view pkg version) + argList 인용
|
|
3330
3330
|
const srcOk = /'view', pkg, 'version'/.test(harnessSrc) && // 1.9.360(CV-2/UR-0077): cmd.exe /d /s /c npm view (args 배열) 형태
|
|
3331
3331
|
!/cp\.exec\(.npm view \$\{pkg\}/.test(harnessSrc) &&
|
|
@@ -3442,7 +3442,7 @@ total++;
|
|
|
3442
3442
|
a._evidenceQuality('src/api.js 수정, 12/12 통과 (Exit: 0)').ok === true &&
|
|
3443
3443
|
a._shellGuardAnalyze('a && b', { shell: 'powershell', psVersion: '5' }).issues.some(i => i.rule === 'ps5-chain') &&
|
|
3444
3444
|
a._claimFileInGit('src/api.js', new Set(['src/api.js'])) === true;
|
|
3445
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
3445
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
3446
3446
|
const movedOut = !/function _evidenceQuality\(evidence\) \{/.test(harnessSrc) && !/function _shellGuardAnalyze\(cmd, ctx\) \{/.test(harnessSrc) && /require\('\.\.\/lib\/analyzers'\)/.test(harnessSrc);
|
|
3447
3447
|
// 소비 명령 회귀: shell-guard (_shellGuardAnalyze 사용)
|
|
3448
3448
|
const sg = cp.spawnSync(process.execPath, [CLI, 'shell-guard', 'a && b', '--json'], { cwd: tmp, encoding: 'utf8', timeout: 20000 });
|
|
@@ -3787,7 +3787,7 @@ total++;
|
|
|
3787
3787
|
const work = m._htmlToText('<p>a <b>b</b></p>') === 'a b'
|
|
3788
3788
|
&& m._extractTitle('<title>T & U</title>') === 'T & U'
|
|
3789
3789
|
&& m._extractLinks('<a href="/x">x</a><a href="https://o.com/y">y</a>', 'https://h.com/').length === 1; // same-domain only
|
|
3790
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
3790
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
3791
3791
|
const movedOut = !/function _htmlToText\(html\) \{/.test(harnessSrc) && harnessSrc.includes('_htmlToText, _extractTitle, _extractLinks') && /require\('\.\.\/lib\/pure-utils'\)/.test(harnessSrc); // 1.9.324: import 순서 비의존(이후 import 추가 허용)
|
|
3792
3792
|
const r = cp.spawnSync(process.execPath, [CLI, 'api-skill'], { encoding: 'utf8', timeout: 15000 }); // 소비 명령 로드
|
|
3793
3793
|
const cmdOk = /api-skill/.test(r.stdout || '');
|
|
@@ -3900,7 +3900,7 @@ total++;
|
|
|
3900
3900
|
const fnOk = typeof m._countDatedBlocks === 'function' && typeof m._extractDecisionBlocks === 'function';
|
|
3901
3901
|
const work = m._countDatedBlocks('```md\n### 2026-01-01 — T\n```\n### 2026-06-05 — R\n') === 1
|
|
3902
3902
|
&& m._extractDecisionBlocks('### 2026-06-05 — A\n- Decision: x\n').length === 1;
|
|
3903
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
3903
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
3904
3904
|
// 1.9.325: import 순서 비의존 — pure-utils 구조분해 블록을 추출해 이름 포함 확인(이후 import 추가 허용)
|
|
3905
3905
|
const _puImport = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
|
|
3906
3906
|
const movedOut = !/function _countDatedBlocks\(/.test(harnessSrc) && !/function _compareSemver\(/.test(harnessSrc)
|
|
@@ -3928,7 +3928,7 @@ total++;
|
|
|
3928
3928
|
const work = m._classifyIntent('정확히 그것만').intent === 'precise'
|
|
3929
3929
|
&& m._classifyIntent('전체 다양한 기능').intent === 'broad'
|
|
3930
3930
|
&& m._classifyIntent('로그인 구현').intent === 'default';
|
|
3931
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
3931
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
3932
3932
|
// import 순서 비의존: pure-utils 구조분해 블록 추출 후 이름 포함 확인
|
|
3933
3933
|
const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
|
|
3934
3934
|
const movedOut = !/function _classifyIntent\(/.test(harnessSrc) && _puImp.includes('_classifyIntent');
|
|
@@ -3955,7 +3955,7 @@ total++;
|
|
|
3955
3955
|
&& m._detectPwshFromEnv({ POWERSHELL_DISTRIBUTION_CHANNEL: 'X' }).version === '7'
|
|
3956
3956
|
&& m._detectPwshFromEnv({}).isPowerShell === false
|
|
3957
3957
|
&& /^['"]/.test(m._shellQuoteArg('a b'));
|
|
3958
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
3958
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
3959
3959
|
const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0]; // import 순서 비의존
|
|
3960
3960
|
const movedOut = !/function _sanitizeFences\(/.test(harnessSrc) && !/function _shellQuoteArg\(/.test(harnessSrc) && !/function _detectPwshFromEnv\(/.test(harnessSrc)
|
|
3961
3961
|
&& _puImp.includes('_sanitizeFences') && _puImp.includes('_shellQuoteArg') && _puImp.includes('_detectPwshFromEnv');
|
|
@@ -3978,7 +3978,7 @@ total++;
|
|
|
3978
3978
|
const work = m._formatLocal('2026-06-05T01:13:00.000Z', { tz: 'Asia/Seoul' }) === '2026-06-05 10:13 KST' // UTC→KST +9h
|
|
3979
3979
|
&& m._formatLocal('2026-06-05T01:13:00.000Z', { tz: 'Asia/Seoul', dateOnly: true }) === '2026-06-05'
|
|
3980
3980
|
&& m._formatLocal('') === '?' && typeof m._getLocalTz() === 'string';
|
|
3981
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
3981
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
3982
3982
|
const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0]; // import 순서 비의존
|
|
3983
3983
|
const movedOut = !/function _formatLocal\(/.test(harnessSrc) && !/function _getLocalTz\(/.test(harnessSrc)
|
|
3984
3984
|
&& _puImp.includes('_getLocalTz') && _puImp.includes('_formatLocal');
|
|
@@ -3997,7 +3997,7 @@ total++;
|
|
|
3997
3997
|
const work = typeof m._truncate === 'function' && typeof m._splitList === 'function'
|
|
3998
3998
|
&& m._truncate('hello world', 8) === 'hello w…' && m._truncate('hi', 8) === 'hi'
|
|
3999
3999
|
&& JSON.stringify(m._splitList('a, b ,c,')) === '["a","b","c"]';
|
|
4000
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
4000
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
4001
4001
|
const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0]; // import 순서 비의존
|
|
4002
4002
|
const movedOut = !/function _truncate\(/.test(harnessSrc) && !/function _splitList\(/.test(harnessSrc)
|
|
4003
4003
|
&& _puImp.includes('_truncate') && _puImp.includes('_splitList');
|
|
@@ -4017,7 +4017,7 @@ total++;
|
|
|
4017
4017
|
&& m._roadmapMapStatus('REQUESTED') === 'planned' && m._roadmapMapStatus('done') === 'done'
|
|
4018
4018
|
&& m._roadmapParseMilestones('### M-0001. 로그인\nStatus: in-progress\nProgress: 40%')[0].progress === 40
|
|
4019
4019
|
&& m._roadmapParseTokens('| color | #fff |').color === '#fff';
|
|
4020
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
4020
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
4021
4021
|
const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0]; // import 순서 비의존
|
|
4022
4022
|
const movedOut = !/function _roadmapMapStatus\(/.test(harnessSrc) && !/function _roadmapParseMilestones\(/.test(harnessSrc) && !/function _roadmapParseTokens\(/.test(harnessSrc)
|
|
4023
4023
|
&& _puImp.includes('_roadmapMapStatus') && _puImp.includes('_roadmapParseMilestones') && _puImp.includes('_roadmapParseTokens');
|
|
@@ -4035,7 +4035,7 @@ total++;
|
|
|
4035
4035
|
const m = require(path.resolve(__dirname, '..', 'lib', 'pure-utils.js'));
|
|
4036
4036
|
const cfgOk = Array.isArray(m._BRIEF_FIELDS) && m._BRIEF_FIELDS.length === 10 && m._BRIEF_FIELDS[0].key === 'intro';
|
|
4037
4037
|
const work = m._briefFilled({ intro: 'x', features: ['a'] }) === 2 && m._briefFilled({}) === 0;
|
|
4038
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
4038
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
4039
4039
|
const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0]; // import 순서 비의존
|
|
4040
4040
|
const movedOut = !/const _BRIEF_FIELDS = \[/.test(harnessSrc) && !/function _briefFilled\(/.test(harnessSrc)
|
|
4041
4041
|
&& _puImp.includes('_BRIEF_FIELDS') && _puImp.includes('_briefFilled');
|
|
@@ -4062,7 +4062,7 @@ total++;
|
|
|
4062
4062
|
const work = typeof m._briefReadmeBlock === 'function' && typeof m._briefBlueprint === 'function'
|
|
4063
4063
|
&& m._briefReadmeBlock(b).includes(m.BRIEF_START) && /f1/.test(m._briefReadmeBlock(b))
|
|
4064
4064
|
&& /Blueprint/.test(m._briefBlueprint(b, '9.9.9')) && /leerness v9\.9\.9/.test(m._briefBlueprint(b, '9.9.9')); // VERSION 주입
|
|
4065
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
4065
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
4066
4066
|
const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0]; // import 순서 비의존
|
|
4067
4067
|
const movedOut = !/function _briefReadmeBlock\(/.test(harnessSrc) && !/function _briefBlueprint\(/.test(harnessSrc) && !/^const BRIEF_START =/m.test(harnessSrc)
|
|
4068
4068
|
&& _puImp.includes('_briefReadmeBlock') && _puImp.includes('_briefBlueprint') && _puImp.includes('BRIEF_START');
|
|
@@ -4088,7 +4088,7 @@ total++;
|
|
|
4088
4088
|
const m = require(path.resolve(__dirname, '..', 'lib', 'pure-utils.js'));
|
|
4089
4089
|
const r = m._parseLessonEntries('### 2026-06-05\n- Lesson: A\n- Tag: t\n\n### 2026-06-04\n- Lesson: B');
|
|
4090
4090
|
const work = typeof m._parseLessonEntries === 'function' && r.length === 2 && r[0].text === 'A' && r[0].tag === 't' && r[1].tag === null;
|
|
4091
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
4091
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
4092
4092
|
const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0]; // import 순서 비의존
|
|
4093
4093
|
const movedOut = !/function _parseLessonEntries\(/.test(harnessSrc) && _puImp.includes('_parseLessonEntries');
|
|
4094
4094
|
// 소비 명령 회귀: lesson save + list --json
|
|
@@ -4114,7 +4114,7 @@ total++;
|
|
|
4114
4114
|
const catOk = c._DEFAULT_PLATFORM_CONSTRAINTS && Object.keys(c._DEFAULT_PLATFORM_CONSTRAINTS.platforms).length === 6;
|
|
4115
4115
|
const r = m._matchConstraints(c._DEFAULT_PLATFORM_CONSTRAINTS, 'stripe 결제');
|
|
4116
4116
|
const work = catOk && r.matched.length === 1 && r.matched[0].platform === 'stripe' && r.totalPlatforms === 6 && m._matchConstraints(null, 'x').matched.length === 0;
|
|
4117
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
4117
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
4118
4118
|
// 1.9.334: catalogs import 블록 추출 후 이름 포함 확인(순서/추가 비의존 — 이후 import 추가 허용)
|
|
4119
4119
|
const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0];
|
|
4120
4120
|
const movedOut = !/const _DEFAULT_PLATFORM_CONSTRAINTS = \{/.test(harnessSrc) && harnessSrc.includes('_matchConstraints(_loadPlatformConstraints(root), text)')
|
|
@@ -4141,7 +4141,7 @@ total++;
|
|
|
4141
4141
|
const catOk = c._DEFAULT_DOMAIN_CATALOG && Object.keys(c._DEFAULT_DOMAIN_CATALOG.domains).length === 5;
|
|
4142
4142
|
const r = m._matchDomain(c._DEFAULT_DOMAIN_CATALOG, 'unity 게임');
|
|
4143
4143
|
const work = catOk && typeof m._matchDomain === 'function' && r.domain === 'game' && Array.isArray(r.components) && m._matchDomain(null, 'x').domain === null;
|
|
4144
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
4144
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
4145
4145
|
const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
|
|
4146
4146
|
const movedOut = !/const _DEFAULT_DOMAIN_CATALOG = \{/.test(harnessSrc) && harnessSrc.includes('_matchDomain(_loadDomainCatalog(root), text)')
|
|
4147
4147
|
&& _catImp.includes('_DEFAULT_DOMAIN_CATALOG');
|
|
@@ -4168,7 +4168,7 @@ total++;
|
|
|
4168
4168
|
const langOk = m._detectLspLang('x.py') === 'python' && m._detectLspLang('y.rs') === 'rust' && m._detectLspLang('z.txt') === 'javascript';
|
|
4169
4169
|
const sy = m._matchLspSymbols(c._LSP_LANG_PATTERNS, 'def foo():\n pass\nclass Bar:\n pass', 'python');
|
|
4170
4170
|
const work = catOk && langOk && typeof m._matchLspSymbols === 'function' && sy.length === 2 && sy[0].name === 'foo' && sy[0].kind === 'function' && sy[1].name === 'Bar' && m._matchLspSymbols(null, 'x', 'javascript').length === 0;
|
|
4171
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
4171
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
4172
4172
|
const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
|
|
4173
4173
|
const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
|
|
4174
4174
|
const movedOut = !/const _LSP_LANG_PATTERNS = \{/.test(harnessSrc) && !/function _detectLspLang\(/.test(harnessSrc)
|
|
@@ -4199,7 +4199,7 @@ total++;
|
|
|
4199
4199
|
const work = catOk && typeof m._detectOptimism === 'function' && sus.some(s => s.kind === 'API' && s.severity === 'high') && conf < 0.5
|
|
4200
4200
|
&& m._computeConfidence(c.OPTIMISM_PATTERNS, '그냥 정리함', 'x') === 1 && m._detectOptimism(null, ev, 'x').length === 0
|
|
4201
4201
|
&& m._extractUrlClaims('POST /a/b').length === 1 && m._verifyUrlClaim({ path: '/a/b' }, 'has /a/b') === true;
|
|
4202
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
4202
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
4203
4203
|
const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
|
|
4204
4204
|
const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
|
|
4205
4205
|
const movedOut = !/const OPTIMISM_PATTERNS = \[/.test(harnessSrc) && !/function _extractUrlClaims\(/.test(harnessSrc)
|
|
@@ -4228,7 +4228,7 @@ total++;
|
|
|
4228
4228
|
const catOk = c.BUILT_IN_PERSONAS && Object.keys(c.BUILT_IN_PERSONAS).length === 5 && c.BUILT_IN_PERSONAS.security && typeof c.BUILT_IN_PERSONAS.security.body === 'string';
|
|
4229
4229
|
const sm = m._personaSummaries(c.BUILT_IN_PERSONAS);
|
|
4230
4230
|
const work = catOk && typeof m._personaSummaries === 'function' && Array.isArray(sm) && sm.length === 5 && sm[0].id === 'security' && sm[0].body === undefined && m._personaSummaries(null).length === 0;
|
|
4231
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
4231
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
4232
4232
|
const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
|
|
4233
4233
|
const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
|
|
4234
4234
|
const movedOut = !/const BUILT_IN_PERSONAS = \{/.test(harnessSrc) && _catImp.includes('BUILT_IN_PERSONAS') && _puImp.includes('_personaSummaries');
|
|
@@ -4262,7 +4262,7 @@ total++;
|
|
|
4262
4262
|
&& m._translate(c.STRINGS, 'no.such.key', 'en') === 'no.such.key'
|
|
4263
4263
|
&& m._translate(null, 'x', 'ko') === 'x'
|
|
4264
4264
|
&& m._translate({ k: { ko: '케이' } }, 'k', 'en') === '케이';
|
|
4265
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
4265
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
4266
4266
|
const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
|
|
4267
4267
|
const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
|
|
4268
4268
|
// _t 박막: 인라인 STRINGS 정의 제거 + import + _translate(STRINGS,..) 호출
|
|
@@ -4378,7 +4378,7 @@ total++;
|
|
|
4378
4378
|
const work = catOk && typeof m._withBuiltinSource === 'function' && Object.keys(out).length === 9
|
|
4379
4379
|
&& Object.values(out).every(v => v._source === 'builtin') && Array.isArray(out.office.capabilities)
|
|
4380
4380
|
&& Object.keys(m._withBuiltinSource(null)).length === 0;
|
|
4381
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
4381
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
4382
4382
|
const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
|
|
4383
4383
|
const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
|
|
4384
4384
|
const movedOut = !/const BUILTIN_CATALOG = \{/.test(harnessSrc) && _catImp.includes('BUILTIN_CATALOG') && _puImp.includes('_withBuiltinSource')
|
|
@@ -4405,7 +4405,7 @@ total++;
|
|
|
4405
4405
|
const mapsOk = c.ROADMAP_STATUS_LABEL && c.ROADMAP_STATUS_COLOR
|
|
4406
4406
|
&& Object.keys(c.ROADMAP_STATUS_LABEL).length === 11 && Object.keys(c.ROADMAP_STATUS_COLOR).length === 11
|
|
4407
4407
|
&& c.ROADMAP_STATUS_LABEL.done === '완료' && c.ROADMAP_STATUS_COLOR.done === '#16a34a' && c.ROADMAP_STATUS_COLOR.skill === '#8b5cf6';
|
|
4408
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
4408
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
4409
4409
|
const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
|
|
4410
4410
|
const movedOut = !/const ROADMAP_STATUS_LABEL = \{/.test(harnessSrc) && !/const ROADMAP_STATUS_COLOR = \{/.test(harnessSrc)
|
|
4411
4411
|
&& _catImp.includes('ROADMAP_STATUS_LABEL') && _catImp.includes('ROADMAP_STATUS_COLOR');
|
|
@@ -4433,7 +4433,7 @@ total++;
|
|
|
4433
4433
|
const hit = (s) => c.SECRET_PATTERNS.some(p => { p.re.lastIndex = 0; return p.re.test(s); });
|
|
4434
4434
|
const catOk = Array.isArray(c.SECRET_PATTERNS) && c.SECRET_PATTERNS.length === 20
|
|
4435
4435
|
&& hit('AKIA' + 'ABCD1234EFGH5678') && hit('sk-' + 'ant-api03-' + A + '_' + A) && !hit('const u = "john' + '_doe_2024";');
|
|
4436
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
4436
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
4437
4437
|
const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
|
|
4438
4438
|
// 모듈 레벨 정의 제거(블록 지역 .env 배열은 보존) + import
|
|
4439
4439
|
const movedOut = !/const SECRET_PATTERNS = \[\r?\n\s*\{ name:/.test(harnessSrc) && _catImp.includes('SECRET_PATTERNS')
|
|
@@ -4467,7 +4467,7 @@ total++;
|
|
|
4467
4467
|
const catOk = c.SKILL_CATALOG_PRESETS && Object.keys(c.SKILL_CATALOG_PRESETS).length === 2
|
|
4468
4468
|
&& c.SKILL_CATALOG_PRESETS.vercel && c.SKILL_CATALOG_PRESETS.vercel.owner === 'vercel-labs'
|
|
4469
4469
|
&& c.SKILL_CATALOG_PRESETS.anthropic && c.SKILL_CATALOG_PRESETS.anthropic.repo === 'skills';
|
|
4470
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
4470
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
4471
4471
|
const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
|
|
4472
4472
|
const movedOut = !/const SKILL_CATALOG_PRESETS = \{/.test(harnessSrc) && _catImp.includes('SKILL_CATALOG_PRESETS');
|
|
4473
4473
|
// 소비 회귀: skill discover 가 preset 목록을 catalog 에서 노출 (네트워크 없이 unknown preset → 사용가능 목록)
|
|
@@ -4493,7 +4493,7 @@ total++;
|
|
|
4493
4493
|
&& m._esc('&<>"\'') === '&<>"''
|
|
4494
4494
|
&& m._esc('<script>x</script>') === '<script>x</script>'
|
|
4495
4495
|
&& m._esc(null) === '' && m._esc(undefined) === '' && m._esc(42) === '42';
|
|
4496
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
4496
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
4497
4497
|
const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
|
|
4498
4498
|
const movedOut = !/function _esc\(/.test(harnessSrc) && _puImp.includes('_esc');
|
|
4499
4499
|
// 소비 회귀: roadmap.html 이 악성 task 제목을 이스케이프 (인젝션 방지)
|
|
@@ -4521,7 +4521,7 @@ total++;
|
|
|
4521
4521
|
const pureOk = typeof m._roadmapTokenStyles === 'function' && out.startsWith(':root {')
|
|
4522
4522
|
&& out.includes('--lr-primary: #2563eb') && out.includes('--lr-surface: #fff') && out.includes('--lr-custom: #abc')
|
|
4523
4523
|
&& out.includes('--lr-card-bg') && out.includes('--lr-page-bg') && m._roadmapTokenStyles(null, null).startsWith(':root {');
|
|
4524
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
4524
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
4525
4525
|
const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
|
|
4526
4526
|
const movedOut = !/function _roadmapTokenStyles\(/.test(harnessSrc) && _puImp.includes('_roadmapTokenStyles');
|
|
4527
4527
|
// 소비 회귀: roadmap.html 이 :root CSS 변수 주입
|
|
@@ -4548,7 +4548,7 @@ total++;
|
|
|
4548
4548
|
const pureOk = typeof m._parseSkillMd === 'function' && r.meta.name === 's1' && r.meta.description === 'd1' && r.body === 'body'
|
|
4549
4549
|
&& m._parseSkillMd('---\nname: b\n---\nx').meta.name === 'b'
|
|
4550
4550
|
&& Object.keys(m._parseSkillMd('plain').meta).length === 0 && m._parseSkillMd(null).body === '';
|
|
4551
|
-
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', '
|
|
4551
|
+
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
4552
4552
|
const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
|
|
4553
4553
|
const movedOut = !/function _parseSkillMd\(/.test(harnessSrc) && _puImp.includes('_parseSkillMd');
|
|
4554
4554
|
// 소비 회귀: skill install 이 BOM 포함 SKILL.md 를 정상 설치 (frontmatter name 파싱)
|
package/scripts/smoke.js
CHANGED
|
@@ -12,7 +12,7 @@ const path = require('path');
|
|
|
12
12
|
const cp = require('child_process');
|
|
13
13
|
|
|
14
14
|
process.env.LEERNESS_OFFLINE = process.env.LEERNESS_OFFLINE || '1';
|
|
15
|
-
const CLI = path.resolve(__dirname, '..', 'bin', '
|
|
15
|
+
const CLI = path.resolve(__dirname, '..', 'bin', 'leerness.js');
|
|
16
16
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-smoke-'));
|
|
17
17
|
let failed = 0, total = 0;
|
|
18
18
|
const t0 = Date.now();
|