leerness 1.9.419 → 1.9.421
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 +38 -0
- package/README.md +5 -5
- package/bin/leerness.js +48 -588
- package/lib/audit.js +322 -0
- package/lib/review-request.js +288 -0
- package/package.json +1 -1
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.421';
|
|
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,47 @@ 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 큰핸들러 모듈화 6번째: audit → lib/audit.js + DI 위임 + 동작 (1.9.421)', run: () => {
|
|
3077
|
+
const m = require('../lib/audit');
|
|
3078
|
+
const expOk = typeof m.audit === 'function';
|
|
3079
|
+
const src = read(__filename);
|
|
3080
|
+
const delegated = src.includes("require('../lib/audit')") && src.includes('_audit.audit(root, opts,');
|
|
3081
|
+
const modSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'audit.js'));
|
|
3082
|
+
const bodyMarker = 'not_' + 'initialized'; // audit 본문 고유 finding kind(split-literal 자기참조 회피)
|
|
3083
|
+
const movedToLib = modSrc.includes("require('./io')") && modSrc.includes("require('./catalogs')") && modSrc.includes(bodyMarker) && !src.includes(bodyMarker);
|
|
3084
|
+
let behavOk = false;
|
|
3085
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_audit_'));
|
|
3086
|
+
const _w = process.stdout.write; let out = '';
|
|
3087
|
+
try {
|
|
3088
|
+
fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true });
|
|
3089
|
+
fs.writeFileSync(path.join(tmp, 'AGENTS.md'), '# x');
|
|
3090
|
+
process.stdout.write = s => { out += s; return true; };
|
|
3091
|
+
m.audit(tmp, { json: true }, { VERSION, arg: (k, d) => d, has: f => f === '--json', planPath, readProgressRows, currentStatePath, handoffPath, envDiff, _readFeatureGraph, _matchAPISkills, _listAPISkills });
|
|
3092
|
+
} catch (e) { out = 'ERR:' + e.message; } finally { process.stdout.write = _w; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
3093
|
+
try { const j = JSON.parse(out); behavOk = typeof j.healthy === 'boolean' && Array.isArray(j.findings); } catch {}
|
|
3094
|
+
return expOk && delegated && movedToLib && behavOk;
|
|
3095
|
+
} },
|
|
3096
|
+
{ name: 'UR-0025 큰핸들러 모듈화 5번째: reviewRequestCmd → lib/review-request.js + DI 위임 + 동작 (1.9.420)', run: () => {
|
|
3097
|
+
const m = require('../lib/review-request');
|
|
3098
|
+
const expOk = typeof m.reviewRequestCmd === 'function';
|
|
3099
|
+
const src = read(__filename);
|
|
3100
|
+
const delegated = src.includes("require('../lib/review-request')") && src.includes('_reviewRequest.reviewRequestCmd(root, request,');
|
|
3101
|
+
const modSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'review-request.js'));
|
|
3102
|
+
// thin wrapper 는 같은 시그니처를 유지하므로(시그니처 부재로 판정 X), 본문 고유 마커(routeKw)가 lib 로 이동했는지로 검증.
|
|
3103
|
+
const bodyMarker = 'const route' + 'Kw = {'; // split-literal: 자기참조(이 케이스 코드가 src 에 포함) 회피
|
|
3104
|
+
const movedToLib = modSrc.includes("require('./io')") && modSrc.includes('estimatedType') && modSrc.includes('harnessPath') && modSrc.includes(bodyMarker) && !src.includes(bodyMarker);
|
|
3105
|
+
let behavOk = false;
|
|
3106
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_rr_'));
|
|
3107
|
+
const _w = process.stdout.write; let out = '';
|
|
3108
|
+
try {
|
|
3109
|
+
fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true });
|
|
3110
|
+
process.stdout.write = s => { out += s; return true; };
|
|
3111
|
+
// harnessPath 를 존재하지 않는 경로로 → 내부 brainstorm spawn 즉시 실패(무부작용). estimatedType/steps 는 spawn 이전 계산이라 검증 가능.
|
|
3112
|
+
m.reviewRequestCmd(tmp, '결제 기능 추가 구현', { has: () => true, harnessPath: path.join(tmp, '__nope.js'), _checkRequestConstraints: () => ({ matched: [], suggestions: [] }), _recordRun: () => {} });
|
|
3113
|
+
} catch {} finally { process.stdout.write = _w; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
3114
|
+
try { const j = JSON.parse(out); behavOk = j.estimatedType === 'feature' && Array.isArray(j.recommendedSteps) && j.recommendedSteps.length === 5; } catch {}
|
|
3115
|
+
return expOk && delegated && movedToLib && behavOk;
|
|
3116
|
+
} },
|
|
3076
3117
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
|
|
3077
3118
|
];
|
|
3078
3119
|
}
|
|
@@ -6830,316 +6871,9 @@ function debug(root) {
|
|
|
6830
6871
|
if (failures) process.exitCode = 1;
|
|
6831
6872
|
}
|
|
6832
6873
|
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
// 1.9.35 개선 #5: --fix 옵션 — 자동 수정 가능한 항목 적용
|
|
6837
|
-
const fix = has('--fix');
|
|
6838
|
-
let fixed = 0;
|
|
6839
|
-
// 1.9.102: --json 모드 — stdout 억제 후 구조화 출력
|
|
6840
|
-
const jsonMode = !!opts.json || has('--json');
|
|
6841
|
-
const findings = [];
|
|
6842
|
-
const _finding = (kind, severity, message, details = {}) => findings.push({ kind, severity, message, ...details });
|
|
6843
|
-
const _origWrite = process.stdout.write.bind(process.stdout);
|
|
6844
|
-
if (jsonMode) process.stdout.write = () => true;
|
|
6845
|
-
try {
|
|
6846
|
-
// 외부리뷰 CV-3/UR-0078: 미초기화/존재하지 않는 경로를 healthy 로 오판하던 것 수정 — 필수 마커 부재 시 failure 승격(verify 와 일관).
|
|
6847
|
-
if (!exists(root) || !exists(path.join(root, '.harness')) || !exists(path.join(root, 'AGENTS.md'))) {
|
|
6848
|
-
failures++;
|
|
6849
|
-
fail(`미초기화 또는 존재하지 않는 경로: ${root} (.harness/AGENTS.md 없음 — leerness init 필요)`);
|
|
6850
|
-
_finding('not_initialized', 'fail', 'uninitialized or missing path (.harness or AGENTS.md absent)', { root });
|
|
6851
|
-
}
|
|
6852
|
-
const designCands = ['designguide.md','design-guide.md','docs/designguide.md','docs/design-guide.md','.harness/designguide.md'];
|
|
6853
|
-
const dups = designCands.filter(f => exists(path.join(root,f)));
|
|
6854
|
-
if (dups.length) { warnings++; warn(`design guide duplicates outside canonical: ${dups.join(', ')} (run: leerness consistency merge-design-guide)`); _finding('design_dup', 'warn', 'design guide duplicates outside canonical', { duplicates: dups }); }
|
|
6855
|
-
else ok('no duplicate design guide candidates');
|
|
6856
|
-
// 1.9.1 P4: <!-- leerness:na --> 마커가 있는 파일은 placeholder 경고 스킵.
|
|
6857
|
-
const naMarker = '<!-- leerness:na';
|
|
6858
|
-
const ds = exists(path.join(root,'.harness/design-system.md')) ? read(path.join(root,'.harness/design-system.md')) : '';
|
|
6859
|
-
if (ds.includes(naMarker)) ok('design-system.md marked NA (skipped)');
|
|
6860
|
-
else if (!/\| color\.primary \|/.test(ds) || /\(실제 값으로 업데이트\)/.test(ds)) { warnings++; warn('design-system.md tokens not customized'); _finding('design_system_default', 'warn', 'design-system.md tokens not customized'); }
|
|
6861
|
-
else ok('design-system tokens populated');
|
|
6862
|
-
const reuse = exists(path.join(root,'.harness/reuse-map.md')) ? read(path.join(root,'.harness/reuse-map.md')) : '';
|
|
6863
|
-
const reuseLines = reuse.split('\n').filter(l => l.startsWith('|') && !/Capability|---/.test(l)).length;
|
|
6864
|
-
if (reuse.includes(naMarker)) ok('reuse-map.md marked NA (skipped)');
|
|
6865
|
-
else if (reuseLines === 0) { warnings++; warn('reuse-map.md is empty (consider populating known reusable elements)'); _finding('reuse_map_empty', 'warn', 'reuse-map.md is empty'); }
|
|
6866
|
-
else ok(`reuse-map.md has ${reuseLines} entries`);
|
|
6867
|
-
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
6868
|
-
const milestoneIds = Array.from(planText.matchAll(/^### (M-\d{4})\./gm)).map(m => m[1]);
|
|
6869
|
-
const rows = readProgressRows(root);
|
|
6870
|
-
// 1.9.6 수정: 한 row에 여러 plan:M-XXXX 링크가 있어도 모두 인식 (matchAll로 전부 추출)
|
|
6871
|
-
const linkedMs = new Set(
|
|
6872
|
-
rows.flatMap(r => Array.from(String(r.evidence || '').matchAll(/M-\d{4}/g), m => m[0]))
|
|
6873
|
-
);
|
|
6874
|
-
const missingFromProgress = milestoneIds.filter(m => !linkedMs.has(m));
|
|
6875
|
-
if (missingFromProgress.length) {
|
|
6876
|
-
warnings++;
|
|
6877
|
-
warn(`milestones without progress entry: ${missingFromProgress.join(', ')}`);
|
|
6878
|
-
_finding('milestone_unlinked', 'warn', 'milestones without progress entry', { milestones: missingFromProgress });
|
|
6879
|
-
log(` → 자동 매칭 제안: leerness task relink`);
|
|
6880
|
-
log(` → 자동 적용: leerness task relink --apply`);
|
|
6881
|
-
}
|
|
6882
|
-
else if (milestoneIds.length) ok('all milestones linked in progress-tracker');
|
|
6883
|
-
const handoff = exists(handoffPath(root)) ? read(handoffPath(root)) : '';
|
|
6884
|
-
if (handoff.includes('Last generated: (자동)')) {
|
|
6885
|
-
warnings++; warn('session-handoff.md never auto-generated (run: leerness session close .)');
|
|
6886
|
-
_finding('handoff_not_generated', 'warn', 'session-handoff.md never auto-generated');
|
|
6887
|
-
// 1.9.35 #5: --fix → session-handoff.md 자동 생성 마커 갱신
|
|
6888
|
-
if (fix) {
|
|
6889
|
-
const stamped = handoff.replace('Last generated: (자동)', `Last generated: ${today()} (leerness audit --fix)`);
|
|
6890
|
-
writeUtf8(handoffPath(root), stamped);
|
|
6891
|
-
ok(' ↳ fixed: session-handoff.md timestamp 갱신');
|
|
6892
|
-
fixed++;
|
|
6893
|
-
}
|
|
6894
|
-
}
|
|
6895
|
-
else if (handoff.includes('Last generated:')) ok('session-handoff.md auto-generated previously');
|
|
6896
|
-
const cur = exists(currentStatePath(root)) ? read(currentStatePath(root)) : '';
|
|
6897
|
-
const updMatch = cur.match(/Updated: (\d{4}-\d{2}-\d{2})/);
|
|
6898
|
-
if (updMatch) {
|
|
6899
|
-
const dDays = (Date.now() - new Date(updMatch[1]).getTime()) / 86400000;
|
|
6900
|
-
if (dDays > 7) {
|
|
6901
|
-
warnings++; warn(`current-state.md stale (${Math.round(dDays)} days)`);
|
|
6902
|
-
_finding('current_state_stale', 'warn', 'current-state.md stale', { days: Math.round(dDays) });
|
|
6903
|
-
// 1.9.35 #5: --fix → current-state.md Updated 라인 갱신
|
|
6904
|
-
if (fix) {
|
|
6905
|
-
const stamped = cur.replace(/Updated: \d{4}-\d{2}-\d{2}/, `Updated: ${today()}`);
|
|
6906
|
-
writeUtf8(currentStatePath(root), stamped);
|
|
6907
|
-
ok(' ↳ fixed: current-state.md Updated 갱신');
|
|
6908
|
-
fixed++;
|
|
6909
|
-
}
|
|
6910
|
-
}
|
|
6911
|
-
else ok('current-state.md fresh');
|
|
6912
|
-
}
|
|
6913
|
-
// 1.9.40: README의 version 배지 ↔ package.json#version mismatch 감지 (도구 만드는 자가 자기 도구 stale하는 dogfooding gap 차단)
|
|
6914
|
-
try {
|
|
6915
|
-
const readmePath = path.join(root, 'README.md');
|
|
6916
|
-
const pkgPath = path.join(root, 'package.json');
|
|
6917
|
-
if (exists(readmePath) && exists(pkgPath)) {
|
|
6918
|
-
const readmeText = read(readmePath);
|
|
6919
|
-
const pkg = JSON.parse(read(pkgPath));
|
|
6920
|
-
const m = readmeText.match(/badge\/version-(\d+\.\d+\.\d+)/);
|
|
6921
|
-
if (pkg.version && m && m[1] !== pkg.version) {
|
|
6922
|
-
warnings++;
|
|
6923
|
-
warn(`README.md version badge mismatch: README=${m[1]} vs package.json=${pkg.version} (run: leerness readme sync)`);
|
|
6924
|
-
_finding('readme_version_mismatch', 'warn', 'README.md version badge mismatch', { readme: m[1], pkg: pkg.version });
|
|
6925
|
-
if (fix) {
|
|
6926
|
-
const updated = readmeText.replace(/badge\/version-[\d.]+-(green|blue|red)/g, `badge/version-${pkg.version}-green`);
|
|
6927
|
-
writeUtf8(readmePath, updated);
|
|
6928
|
-
ok(' ↳ fixed: README.md version 배지 갱신');
|
|
6929
|
-
fixed++;
|
|
6930
|
-
}
|
|
6931
|
-
}
|
|
6932
|
-
}
|
|
6933
|
-
} catch {}
|
|
6934
|
-
// 1.9.62: package.json 있으면 npm audit --json 자동 호출 → CVE 보고 (opt-out: --no-npm-audit)
|
|
6935
|
-
// 정책: leerness가 외부 호출하지만 사용자 컨텍스트에 이미 npm 설치되어 있음을 가정 (offline 시 자동 스킵)
|
|
6936
|
-
if (exists(path.join(root, 'package.json')) && !has('--no-npm-audit') && process.env.LEERNESS_OFFLINE !== '1') {
|
|
6937
|
-
try {
|
|
6938
|
-
const r = cp.spawnSync('npm', ['audit', '--json'], {
|
|
6939
|
-
cwd: root, encoding: 'utf8', shell: true, timeout: 30000
|
|
6940
|
-
});
|
|
6941
|
-
if (r.stdout) {
|
|
6942
|
-
let j = null;
|
|
6943
|
-
try { j = JSON.parse(r.stdout); } catch {}
|
|
6944
|
-
if (j && j.metadata && j.metadata.vulnerabilities) {
|
|
6945
|
-
const v = j.metadata.vulnerabilities;
|
|
6946
|
-
const total = (v.critical || 0) + (v.high || 0) + (v.moderate || 0) + (v.low || 0);
|
|
6947
|
-
if (total > 0) {
|
|
6948
|
-
warnings++;
|
|
6949
|
-
warn(`npm CVE: ${total}건 (critical=${v.critical||0}, high=${v.high||0}, moderate=${v.moderate||0}, low=${v.low||0})`);
|
|
6950
|
-
_finding('npm_cve', 'warn', `npm CVE: ${total}건`, { vulnerabilities: v });
|
|
6951
|
-
log(` → 수정: npm audit fix · 상세: npm audit`);
|
|
6952
|
-
if (v.critical || v.high) {
|
|
6953
|
-
warnings++; // critical/high는 추가 가중
|
|
6954
|
-
warn(` ⚠ critical/high CVE 즉시 대응 권장`);
|
|
6955
|
-
_finding('npm_cve_critical', 'warn', 'critical/high CVE 즉시 대응 권장', { critical: v.critical, high: v.high });
|
|
6956
|
-
}
|
|
6957
|
-
} else {
|
|
6958
|
-
ok('npm CVE: 0건');
|
|
6959
|
-
}
|
|
6960
|
-
}
|
|
6961
|
-
}
|
|
6962
|
-
} catch {}
|
|
6963
|
-
}
|
|
6964
|
-
// 1.9.75: .gitignore 보안 검증 — .env / 시크릿 파일이 .gitignore에 포함되는지 (--no-gitignore-check로 끄기)
|
|
6965
|
-
if (!has('--no-gitignore-check')) {
|
|
6966
|
-
try {
|
|
6967
|
-
const gi = path.join(root, '.gitignore');
|
|
6968
|
-
const envPath = path.join(root, '.env');
|
|
6969
|
-
if (exists(envPath)) {
|
|
6970
|
-
// .env가 존재하면 .gitignore가 반드시 있어야 하고, .env가 포함되어야 함
|
|
6971
|
-
const giText = exists(gi) ? read(gi) : '';
|
|
6972
|
-
const giLines = giText.split('\n').map(l => l.trim());
|
|
6973
|
-
// 필수 보안 패턴 (글로벌 룰 .gitignore 보안 체크리스트)
|
|
6974
|
-
const SECRET_PATTERNS = ['.env', '.env.local', '.env.production', '.env.*.local', '*.pem', 'credentials.json'];
|
|
6975
|
-
const missing = SECRET_PATTERNS.filter(p => !giLines.some(l => l === p || l === '/' + p));
|
|
6976
|
-
if (missing.length) {
|
|
6977
|
-
warnings++;
|
|
6978
|
-
warn(`.gitignore에 시크릿 패턴 ${missing.length}건 누락: ${missing.slice(0, 4).join(', ')}${missing.length > 4 ? ' …' : ''}`);
|
|
6979
|
-
_finding('gitignore_missing_secrets', 'warn', '.gitignore에 시크릿 패턴 누락', { missing });
|
|
6980
|
-
if (fix) {
|
|
6981
|
-
// 자동 추가
|
|
6982
|
-
let newGi = giText;
|
|
6983
|
-
if (newGi && !newGi.endsWith('\n')) newGi += '\n';
|
|
6984
|
-
newGi += `\n# 1.9.75 audit --fix: 시크릿 파일 보안 패턴 자동 추가 (사용자 글로벌 룰)\n`;
|
|
6985
|
-
for (const p of missing) newGi += `${p}\n`;
|
|
6986
|
-
writeUtf8(gi, newGi);
|
|
6987
|
-
ok(` ↳ fixed: .gitignore에 ${missing.length}건 자동 추가 (시크릿 보안 1.9.75)`);
|
|
6988
|
-
fixed++;
|
|
6989
|
-
} else {
|
|
6990
|
-
log(` → 자동 추가: leerness audit --fix`);
|
|
6991
|
-
}
|
|
6992
|
-
} else {
|
|
6993
|
-
ok('.gitignore 시크릿 패턴 OK (1.9.75)');
|
|
6994
|
-
}
|
|
6995
|
-
}
|
|
6996
|
-
} catch {}
|
|
6997
|
-
}
|
|
6998
|
-
// 1.9.71: .env / .env.example 동기화 감사 (--no-env-check로 끄기)
|
|
6999
|
-
if (!has('--no-env-check')) {
|
|
7000
|
-
try {
|
|
7001
|
-
const d = envDiff(root);
|
|
7002
|
-
if (exists(d.envPath) && exists(d.examplePath)) {
|
|
7003
|
-
if (d.inEnvOnly.length) {
|
|
7004
|
-
warnings++;
|
|
7005
|
-
warn(`.env에 있는 키 ${d.inEnvOnly.length}건이 .env.example에 누락: ${d.inEnvOnly.slice(0, 4).join(', ')}${d.inEnvOnly.length > 4 ? ' …' : ''}`);
|
|
7006
|
-
_finding('env_keys_missing', 'warn', '.env 키가 .env.example에 누락', { keys: d.inEnvOnly });
|
|
7007
|
-
if (fix) {
|
|
7008
|
-
// 자동 동기화: 누락 키만 .env.example 끝에 append (값 비움)
|
|
7009
|
-
let example = read(d.examplePath);
|
|
7010
|
-
if (!example.endsWith('\n')) example += '\n';
|
|
7011
|
-
example += `\n# 1.9.71 audit --fix: 누락 키 자동 추가 (값은 빈 문자열, 보안 정책)\n`;
|
|
7012
|
-
for (const k of d.inEnvOnly) example += `${k}=\n`;
|
|
7013
|
-
writeUtf8(d.examplePath, example);
|
|
7014
|
-
ok(` ↳ fixed: .env.example에 ${d.inEnvOnly.length}건 자동 추가 (값은 빈 문자열, 1.9.71)`);
|
|
7015
|
-
fixed++;
|
|
7016
|
-
} else {
|
|
7017
|
-
log(` → 자동 동기화: leerness env sync 또는 leerness audit --fix`);
|
|
7018
|
-
}
|
|
7019
|
-
} else {
|
|
7020
|
-
ok('.env ↔ .env.example 동기화됨 (1.9.71)');
|
|
7021
|
-
}
|
|
7022
|
-
}
|
|
7023
|
-
} catch {}
|
|
7024
|
-
}
|
|
7025
|
-
// 1.9.142: Feature Graph 무결성 검증 — orphan/cycle 자동 감지 (--no-feature-check로 끄기)
|
|
7026
|
-
if (!has('--no-feature-check')) {
|
|
7027
|
-
try {
|
|
7028
|
-
const { nodes: fNodes } = _readFeatureGraph(root);
|
|
7029
|
-
if (fNodes.length > 0) {
|
|
7030
|
-
const ids = new Set(fNodes.map(n => n.id));
|
|
7031
|
-
// (1) orphan: 다른 노드가 참조하는데 정의가 없는 ID
|
|
7032
|
-
const orphans = [];
|
|
7033
|
-
for (const n of fNodes) {
|
|
7034
|
-
for (const ref of [...(n.dependsOn || []), ...(n.affects || []), ...(n.coChangesWith || [])]) {
|
|
7035
|
-
if (!ids.has(ref)) orphans.push({ from: n.id, missingRef: ref });
|
|
7036
|
-
}
|
|
7037
|
-
}
|
|
7038
|
-
if (orphans.length) {
|
|
7039
|
-
warnings++;
|
|
7040
|
-
warn(`Feature Graph: orphan 참조 ${orphans.length}건 — ${orphans.slice(0, 3).map(o => `${o.from}→${o.missingRef}`).join(', ')}${orphans.length > 3 ? ' …' : ''}`);
|
|
7041
|
-
_finding('feature_graph_orphan', 'warn', 'Feature Graph 에 정의되지 않은 ID 참조', { count: orphans.length, orphans: orphans.slice(0, 10) });
|
|
7042
|
-
log(` → 수정: leerness feature add 또는 link 제거`);
|
|
7043
|
-
}
|
|
7044
|
-
// (2) cycle: affects 그래프에서 순환 의존성 감지 (DFS)
|
|
7045
|
-
const cycles = [];
|
|
7046
|
-
const WHITE = 0, GRAY = 1, BLACK = 2;
|
|
7047
|
-
const color = new Map();
|
|
7048
|
-
for (const n of fNodes) color.set(n.id, WHITE);
|
|
7049
|
-
const byId = new Map(fNodes.map(n => [n.id, n]));
|
|
7050
|
-
const dfs = (nodeId, path) => {
|
|
7051
|
-
color.set(nodeId, GRAY);
|
|
7052
|
-
const node = byId.get(nodeId);
|
|
7053
|
-
if (!node) { color.set(nodeId, BLACK); return; }
|
|
7054
|
-
for (const next of [...(node.affects || []), ...(node.dependsOn || [])]) {
|
|
7055
|
-
if (!byId.has(next)) continue;
|
|
7056
|
-
const c = color.get(next);
|
|
7057
|
-
if (c === GRAY) {
|
|
7058
|
-
// 순환 발견 — path 에 next 까지 자르기
|
|
7059
|
-
const idx = path.indexOf(next);
|
|
7060
|
-
const cyc = idx >= 0 ? path.slice(idx).concat([next]) : [...path, next];
|
|
7061
|
-
if (!cycles.some(existing => existing.join() === cyc.join())) cycles.push(cyc);
|
|
7062
|
-
} else if (c === WHITE) {
|
|
7063
|
-
dfs(next, [...path, next]);
|
|
7064
|
-
}
|
|
7065
|
-
}
|
|
7066
|
-
color.set(nodeId, BLACK);
|
|
7067
|
-
};
|
|
7068
|
-
for (const n of fNodes) if (color.get(n.id) === WHITE) dfs(n.id, [n.id]);
|
|
7069
|
-
if (cycles.length) {
|
|
7070
|
-
warnings++;
|
|
7071
|
-
warn(`Feature Graph: 순환 의존 ${cycles.length}건 — ${cycles[0].join(' → ')}${cycles.length > 1 ? ` (외 ${cycles.length-1}건)` : ''}`);
|
|
7072
|
-
_finding('feature_graph_cycle', 'warn', 'Feature Graph 에 순환 의존', { count: cycles.length, cycles: cycles.slice(0, 5) });
|
|
7073
|
-
log(` → 수정: feature link 재구성 (affects/depends-on 방향 정리)`);
|
|
7074
|
-
}
|
|
7075
|
-
if (!orphans.length && !cycles.length) {
|
|
7076
|
-
ok(`Feature Graph OK (${fNodes.length} 노드, orphan/cycle 없음, 1.9.142)`);
|
|
7077
|
-
}
|
|
7078
|
-
}
|
|
7079
|
-
} catch {}
|
|
7080
|
-
}
|
|
7081
|
-
// 1.9.247 (UR-0015 2단계): api-skill 참조 audit — API 관련 task 인데 .harness/api-skills/ 미참조 시 경고
|
|
7082
|
-
// 사용자 명시 (UR-0015): "AI가 정리해둔 파일이 참조되는지 확인"
|
|
7083
|
-
// 현재 in-progress task 의 request/nextAction 에 API 키워드 (URL, "API", "endpoint", "REST", "GraphQL", "OAuth", "webhook") 있는데
|
|
7084
|
-
// _matchAPISkills() 결과가 0 이면 → 경고 + leerness api-skill add <url> 안내
|
|
7085
|
-
try {
|
|
7086
|
-
const rows = readProgressRows(root);
|
|
7087
|
-
const ip = rows.find(r => r.status === 'in-progress');
|
|
7088
|
-
if (ip) {
|
|
7089
|
-
const taskText = (ip.request || '') + ' ' + (ip.nextAction || '') + ' ' + (ip.evidence || '');
|
|
7090
|
-
const apiKeywords = /\bAPI\b|endpoint|REST|GraphQL|OAuth|webhook|https?:\/\/[^\s]+/i;
|
|
7091
|
-
if (apiKeywords.test(taskText)) {
|
|
7092
|
-
const matched = _matchAPISkills(root, taskText);
|
|
7093
|
-
const allSkills = _listAPISkills(root);
|
|
7094
|
-
if (matched.length === 0) {
|
|
7095
|
-
warnings++;
|
|
7096
|
-
warn(`API 관련 task 감지 (현재: "${(ip.request || '').slice(0, 60)}") — .harness/api-skills/ 매칭 0건 (저장 ${allSkills.length})`);
|
|
7097
|
-
warn(` → leerness api-skill add <url> --direction "구현 방향" 으로 정리 권장 (1.9.245 UR-0015 / 1.9.247 audit)`);
|
|
7098
|
-
_finding('api_skill_missing', 'warn', 'API 관련 task 인데 .harness/api-skills/ 매칭 없음', {
|
|
7099
|
-
taskRequest: (ip.request || '').slice(0, 100),
|
|
7100
|
-
apiSkillsTotal: allSkills.length,
|
|
7101
|
-
matched: 0,
|
|
7102
|
-
hint: 'leerness api-skill add <url> --direction "..."'
|
|
7103
|
-
});
|
|
7104
|
-
} else {
|
|
7105
|
-
ok(`API skill 매칭 OK (현재 task → ${matched.length}건 매칭 in .harness/api-skills/, 1.9.247 UR-0015 2단계)`);
|
|
7106
|
-
}
|
|
7107
|
-
}
|
|
7108
|
-
}
|
|
7109
|
-
} catch {}
|
|
7110
|
-
// 1.9.63: --strict — warnings ≥ threshold 시 failures로 승격 (CI 친화)
|
|
7111
|
-
if (has('--strict')) {
|
|
7112
|
-
const threshold = parseInt(arg('--threshold', '1'), 10);
|
|
7113
|
-
if (warnings >= threshold) {
|
|
7114
|
-
failures++;
|
|
7115
|
-
warn(`--strict 활성: warnings ${warnings} ≥ threshold ${threshold} → failures 승격`);
|
|
7116
|
-
_finding('strict_promoted', 'fail', `warnings ${warnings} ≥ threshold ${threshold} → failures 승격`, { warnings, threshold });
|
|
7117
|
-
}
|
|
7118
|
-
}
|
|
7119
|
-
log(`Audit summary: warnings=${warnings} failures=${failures}${fix ? ` fixed=${fixed}` : ''}${has('--strict') ? ` strict-threshold=${arg('--threshold', '1')}` : ''}`);
|
|
7120
|
-
} finally {
|
|
7121
|
-
// 1.9.102: stdout 복원
|
|
7122
|
-
if (jsonMode) process.stdout.write = _origWrite;
|
|
7123
|
-
}
|
|
7124
|
-
// 1.9.102: JSON 모드 — 구조화 출력
|
|
7125
|
-
if (jsonMode) {
|
|
7126
|
-
const payload = {
|
|
7127
|
-
version: VERSION,
|
|
7128
|
-
root,
|
|
7129
|
-
warnings,
|
|
7130
|
-
failures,
|
|
7131
|
-
fixed,
|
|
7132
|
-
healthy: failures === 0,
|
|
7133
|
-
fixApplied: fix,
|
|
7134
|
-
strict: has('--strict'),
|
|
7135
|
-
strictThreshold: has('--strict') ? parseInt(arg('--threshold', '1'), 10) : null,
|
|
7136
|
-
summary: `warnings=${warnings} failures=${failures}${fix ? ` fixed=${fixed}` : ''}`,
|
|
7137
|
-
findings,
|
|
7138
|
-
};
|
|
7139
|
-
process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
|
|
7140
|
-
}
|
|
7141
|
-
if (failures) process.exitCode = 1;
|
|
7142
|
-
}
|
|
6874
|
+
const _audit = require('../lib/audit');
|
|
6875
|
+
// 1.9.421 (UR-0025/UR-0125 큰 핸들러 모듈화 6번째): audit → lib/audit.js (DI 위임, thin wrapper)
|
|
6876
|
+
function audit(root, opts = {}) { return _audit.audit(root, opts, { VERSION, arg, has, planPath, readProgressRows, currentStatePath, handoffPath, envDiff, _readFeatureGraph, _matchAPISkills, _listAPISkills }); }
|
|
7143
6877
|
|
|
7144
6878
|
// 1.9.312 (UR-0050, 설치리뷰 3중수렴): secret 스캐너 현대 키 패턴 보강.
|
|
7145
6879
|
// 배경: 기존 OpenAI 패턴 `sk-[A-Za-z0-9]{32,}` 은 하이픈에서 끊겨 sk-proj-/sk-svcacct- (modern 프로젝트/서비스 키)를 놓침.
|
|
@@ -20418,283 +20152,9 @@ function lspCmd(root, sub, ...args) {
|
|
|
20418
20152
|
//
|
|
20419
20153
|
// REPL: :review <request> (1.9.175 slash 패턴)
|
|
20420
20154
|
// 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
|
-
}
|
|
20155
|
+
const _reviewRequest = require('../lib/review-request');
|
|
20156
|
+
// 1.9.420 (UR-0125 큰 핸들러 모듈화 5번째): reviewRequestCmd → lib/review-request.js (DI 위임, thin wrapper)
|
|
20157
|
+
function reviewRequestCmd(root, request) { return _reviewRequest.reviewRequestCmd(root, request, { has, harnessPath: __filename, _checkRequestConstraints, _recordRun }); }
|
|
20698
20158
|
|
|
20699
20159
|
// 1.9.164: leerness which — 진단 도구 (구버전 충돌 / npx 캐시 / PATH 충돌 해결)
|
|
20700
20160
|
// 사용자가 "최신 버전 작동 안 함" 의심 시: 실제 실행 중인 leerness 의 경로 / 버전 / npm 캐시 / PATH 후보 표시.
|