leerness 1.33.0 → 1.35.0
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 +155 -0
- package/README.ko.md +2 -2
- package/README.md +11 -9
- package/bin/leerness.js +140 -15
- package/lib/graph.js +218 -0
- package/lib/mcp-tools.js +1 -0
- package/lib/pure-utils.js +1426 -1422
- package/package.json +1 -1
- package/scripts/e2e.js +138 -1
package/bin/leerness.js
CHANGED
|
@@ -32,7 +32,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
|
|
|
32
32
|
// 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
|
|
33
33
|
const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST, _DEFAULT_PLATFORM_CONSTRAINTS, _DEFAULT_DOMAIN_CATALOG, _TOOL_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 분리 · 1.11.4 (UR-0007): _TOOL_CATALOG
|
|
34
34
|
|
|
35
|
-
const VERSION = '1.
|
|
35
|
+
const VERSION = '1.35.0';
|
|
36
36
|
|
|
37
37
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
38
38
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -2881,6 +2881,17 @@ function _selfTestCases() {
|
|
|
2881
2881
|
{ name: '입력 스키마 검증: task status/rule trigger 무효값 거부 + every-round 보존 (UR-0046 설치리뷰 1.9.310)', run: () => { const src = read(__filename); const sets = TASK_STATUSES.has('done') && TASK_STATUSES.has('in-progress') && !TASK_STATUSES.has('nonsense') && RULE_TRIGGERS.has('every-round') && RULE_TRIGGERS.has('every-update') && !RULE_TRIGGERS.has('not-a-trigger'); const helper = typeof _validateChoice === 'function' && _validateChoice('done', TASK_STATUSES, 'x') === true; const wired = /_validateChoice\(arg\('--status', null\), TASK_STATUSES/.test(src) && /_validateChoice\(trigger, RULE_TRIGGERS/.test(src); return sets && helper && wired; } },
|
|
2882
2882
|
{ name: 'init 가드: 미초기화 write 차단 + 다중마커 판별 + --force 우회 (UR-0047 설치리뷰 1.9.311)', run: () => { const src = read(__filename); const fnOk = typeof _isInitialized === 'function' && typeof _requireInit === 'function'; const _fix = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_initfix_')); let liveOk = false; try { fs.writeFileSync(path.join(_fix, 'AGENTS.md'), 'x'); liveOk = _isInitialized(_fix) === true; } finally { try { fs.rmSync(_fix, { recursive: true, force: true }); } catch {} } const emptyOk = _isInitialized(path.join(os.tmpdir(), '__leerness_noinit_marker__')) === false; const wired = ["task add", "task update", "plan add", "decision add", "rule add", "lesson save", "brief set"].every(l => src.includes(`_requireInit(root, '${l}')`)) && !src.includes("_requireInit(root, 'state " + "start')"); const force = /if \(_isInitialized\(root\) \|\| has\('--force'\)\) return true/.test(src); return fnOk && liveOk && emptyOk && wired && force; } },
|
|
2883
2883
|
{ name: 'secret 스캐너 현대 키: OpenAI proj/svcacct·Anthropic api03(_)·GitHub 변종·Stripe·npm 검출 + 오탐 가드 (UR-0050 설치리뷰 1.9.312)', run: () => { const hit = (s) => SECRET_PATTERNS.some(p => { p.re.lastIndex = 0; return p.re.test(s); }); const named = (s, nm) => SECRET_PATTERNS.some(p => { p.re.lastIndex = 0; return p.re.test(s) && p.name === nm; }); const A = 'A'.repeat(40); const projKey = 'sk-' + 'proj-' + A + '_' + A; const svcKey = 'sk-' + 'svcacct-' + A; const antKey = 'sk-' + 'ant-api03-' + A + '_' + A; const ghoKey = 'gho_' + 'a1B2'.repeat(9); const stripeKey = 'sk_' + 'live_' + A; const npmKey = 'npm_' + 'a1B2'.repeat(9); const asiaKey = 'ASIA' + 'ABCD1234EFGH5678'; const legacy = 'sk-' + A; const hits = hit(projKey) && hit(svcKey) && hit(antKey) && hit(ghoKey) && hit(stripeKey) && hit(npmKey) && hit(asiaKey) && hit(legacy); const names = named(projKey, 'OpenAI project/service key') && named(antKey, 'Anthropic API key') && named(stripeKey, 'Stripe secret key') && named(npmKey, 'npm token'); const clean = !hit('const userName = "john' + '_doe_2024";') && !hit('https://example.com/path/to/page'); return hits && names && clean; } },
|
|
2884
|
+
{ name: '1.34.2 (dogfood #177): _isPlaceholderSecret — test 픽스처 토큰 FP 억제 + 실키 FN-guard 0', run: () => {
|
|
2885
|
+
const ph = _isPlaceholderSecret;
|
|
2886
|
+
// FP 억제 (테스트 픽스처 'test' 토큰): leerness-gate dogfood 에서 발견.
|
|
2887
|
+
const fpOk = ph('test-webhook-secret-123') && ph('webhook-secret-for-tests') && ph('test-token-abc') && ph('TEST_KEY_value') && ph('my-test-password');
|
|
2888
|
+
// FN-guard: 실키/고엔트로피는 절대 placeholder 아님 (보안 회귀 0).
|
|
2889
|
+
const fnOk = !ph('AKIAJQXMP7RZ2KL9WXYZ') && !ph('ghp_' + 'a1B2'.repeat(9)) && !ph('x9Kp2mQ7vL4nR8tW1cY6bN3dF5gH0jS') && !ph('prod-database-secret-9a8b7c6d5e4f') && !ph('latestKEY9a8b7c6d5e4f3a2b1c0d9e8f7a6b');
|
|
2890
|
+
// 소스 가드: test 토큰 규칙이 고엔트로피/실키 분기 '뒤'에 위치(도달 불가 → FN-safe).
|
|
2891
|
+
const src = read(path.join(path.dirname(__filename), '..', 'lib', 'pure-utils.js'));
|
|
2892
|
+
const placed = /alnum\.length >= 24 && distinct >= 12\) return false;[\s\S]{0,400}\(\?:\^\|\[-_\]\)test/.test(src);
|
|
2893
|
+
return fpOk && fnOk && placed;
|
|
2894
|
+
} },
|
|
2884
2895
|
{ name: 'MCP notification 준수: id없는 요청 무응답 가드 + ping {} (UR-0049 설치리뷰 1.9.313)', run: () => { const src = read(__filename); const guard = src.includes("const isNotification = !('id' in req)") && src.includes("req.method.startsWith('notifications/')") && src.includes('if (isNotification) return;'); const ping = src.includes("req.method === 'ping'") && /ping[\s\S]{0,140}result: \{\} \}/.test(src); return guard && ping; } },
|
|
2885
2896
|
{ name: 'PowerShell 감지: pwsh7(channel/Documents\\PowerShell/install) + ps5.1 영구경로 과경고 안함 (UR-0052 설치리뷰 1.9.314)', run: () => { const f = _detectPwshFromEnv; const pwsh7a = f({ POWERSHELL_DISTRIBUTION_CHANNEL: 'MSI:Windows 10' }).version === '7'; const pwsh7b = f({ PSModulePath: 'C:\\Users\\me\\Documents\\PowerShell\\Modules' }).version === '7'; const pwsh7c = f({ PSModulePath: 'C:\\Program Files\\PowerShell\\7\\Modules' }).version === '7'; const noFalsePs5 = f({ PSModulePath: 'C:\\Users\\me\\Documents\\WindowsPowerShell\\Modules' }).isPowerShell === false; const cmdSys = f({ PSModulePath: 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\Modules' }).isPowerShell === false; const empty = f({}).isPowerShell === false; const src = read(__filename); const wired = src.includes('const fromEnv = _detectPwshFromEnv()') && src.includes('const pwshEnv = _detectPwshFromEnv()'); return pwsh7a && pwsh7b && pwsh7c && noFalsePs5 && cmdSys && empty && wired; } },
|
|
2886
2897
|
{ name: 'doc/surface 정합: doctor 명령 + stale MCP 카운트 동적화(commands/banner) (UR-0054 설치리뷰 1.9.315)', run: () => { const src = read(__filename); const doctorOk = typeof doctorCmd === 'function' && /cmd === 'doctor'/.test(src) && /# leerness doctor/.test(src); const dynCount = /MCP 도구: \$\{_mcpToolCount\(\)\}/.test(src) && /외부 AI 통합 \(MCP \$\{_mcpToolCount\(\)\} 도구\)/.test(src); return doctorOk && dynCount; } },
|
|
@@ -2965,8 +2976,10 @@ function _selfTestCases() {
|
|
|
2965
2976
|
{ name: '6번째 외부평가/codex P1-B: task drop 존재확인 가드 — 없는 ID 가짜 row 방지 (1.9.396)', run: () => { const src = read(__filename); const i = src.indexOf('function taskDrop(root, id)'); if (i < 0) return false; const body = src.slice(i, i + 700); return body.includes('not found in progress-tracker.md') && body.includes('rows.find(r => r.id === id)') && body.includes('_requireInit'); } },
|
|
2966
2977
|
{ name: '6번째 외부평가/codex P1-A (UR-0098): install-safety 레시피 셸-무관 + hardeningNote (1.9.397)', run: () => { if (typeof installSafetyCmd !== 'function') return false; const save = process.argv; const _w = process.stdout.write; let out = ''; try { process.argv = ['node', 'h', 'install-safety', '--json']; process.stdout.write = s => { out += s; return true; }; installSafetyCmd({ json: true }); } catch {} finally { process.stdout.write = _w; process.argv = save; } let j; try { j = JSON.parse(out); } catch {} const noPosixPrefix = !!j && Array.isArray(j.safeInstall) && !j.safeInstall.some(x => /^npm_config_\w+=/.test(String(x).trim())); const crossShell = !!j && j.safeInstall.filter(x => String(x).includes('npx --yes')).length >= 2; const noteOk = !!j && typeof j.hardeningNote === 'string' && j.hardeningNote.includes('PowerShell'); return noPosixPrefix && crossShell && noteOk; } },
|
|
2967
2978
|
{ name: '6번째 외부평가/codex P1-C (UR-0099): --json 에러 경로 구조화 failJson + 와이어 (1.9.398)', run: () => { const io = require('../lib/io'); if (io.failJson !== failJson) return false; const _w = process.stdout.write; const saved = process.exitCode; let jOut = '', hOut = ''; let jExit = 0; try { process.stdout.write = s => { jOut += s; return true; }; process.exitCode = 0; failJson(true, 'tc', 'm'); jExit = process.exitCode; process.stdout.write = s => { hOut += s; return true; }; process.exitCode = 0; failJson(false, 'c', 'humanmsg'); } catch {} finally { process.stdout.write = _w; process.exitCode = saved; } let pj; try { pj = JSON.parse(jOut); } catch {} const jsonOk = !!pj && pj.ok === false && pj.code === 'tc' && pj.error === 'm' && jExit === 1; const humanOk = hOut.includes('✗') && hOut.includes('humanmsg') && !hOut.includes('{'); const src = read(__filename); const wired = src.includes("failJson(_j, 'missing_args'") && src.includes("failJson(_j, 'spec_not_found'"); return jsonOk && humanOk && wired; } },
|
|
2979
|
+
{ name: 'T-0077 graph --html: leerness.html 온톨로지 생성 + 노드/엣지/XSS 무결성 (1.34.3)', run: () => { const m = require('../lib/graph'); const expOk = typeof m.graphHtmlCmd === 'function' && typeof m.buildGraphData === 'function'; const src = read(__filename); const delegated = src.includes("require('../lib/" + "graph')") && src.includes('function graphHtmlCmd(root) { return ' + '_graph.graphHtmlCmd('); const gSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'graph.js')); const movedToLib = gSrc.includes('buildGraphData') && gSrc.includes('String.raw') && gSrc.includes('/*__DATA__' + '*/null'); let behavOk = false; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_graph_')); const _w = process.stdout.write; try { process.stdout.write = () => true; fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true }); fs.writeFileSync(path.join(tmp, '.harness', 'progress-tracker.md'), '| ID | Status | Request | Evidence | Next | Updated |\n|---|---|---|---|---|---|\n| T-0001 | done | first task | - | - | 2026-06-26 |\n| T-0002 | in-progress | follow-up to T-0001 </scr' + 'ipt> | - | - | 2026-06-26 |\n'); const deps = { _roadmapData, _loadDecisions, _loadLessons }; const data = m.buildGraphData(tmp, deps); const dataOk = data.nodes.some(n => n.id === 'T-0001') && data.nodes.some(n => n.id === 'T-0002') && data.counts.task >= 2; const edgeOk = data.edges.some(e => e.source === 'T-0002' && e.target === 'T-0001'); const out = path.join(tmp, 'leerness.html'); const r = m.graphHtmlCmd(tmp, Object.assign({ has: () => false }, deps), out); const html = fs.readFileSync(out, 'utf8'); const placeholderGone = !html.includes('/*__DATA__' + '*/null'); const hasNode = html.includes('T-0002'); const xssSafe = (html.match(/<\/script>/g) || []).length === 1; behavOk = dataOk && edgeOk && placeholderGone && hasNode && xssSafe && !!r && r.ok === true && fs.existsSync(out); } catch (e) { behavOk = false; } finally { process.stdout.write = _w; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } return expOk && delegated && movedToLib && behavOk; } },
|
|
2980
|
+
{ name: 'T-0077 후속 graph auto-gen: handoff opt-in 배선 + quiet 무로그 (1.34.4)', run: () => { const m = require('../lib/graph'); const src = read(__filename); const wired = src.includes('_maybeAuto' + 'Graph(_hp)') && src.includes('LEERNESS_AUTO_' + 'GRAPH'); let quietOk = false; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_autograph_')); const _w = process.stdout.write; let so = ''; try { process.stdout.write = s => { so += s; return true; }; fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true }); fs.writeFileSync(path.join(tmp, '.harness', 'progress-tracker.md'), '| ID | Status | Request | Evidence | Next | Updated |\n|---|---|---|---|---|---|\n| T-0001 | done | x | - | - | 2026-06-26 |\n'); const out = path.join(tmp, 'leerness.html'); const r = m.graphHtmlCmd(tmp, { _roadmapData, _loadDecisions, _loadLessons, quiet: true }, out); quietOk = !!r && r.ok === true && fs.existsSync(out) && so === ''; } catch (e) { quietOk = false; } finally { process.stdout.write = _w; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } return wired && quietOk; } },
|
|
2968
2981
|
{ name: '7번째 버그헌트 P1-A (UR-0104): 테이블셀 안전화 _cellSafe/_cellUnescape (파이프/개행 injection 차단) (1.9.399)', run: () => { const m = require('../lib/pure-utils'); if (m._cellSafe !== _cellSafe || m._cellUnescape !== _cellUnescape) return false; const safe = _cellSafe('fix | bug\nrow2'); const noRaw = !/(?<!\\)\|/.test(safe) && !/[\r\n]/.test(safe); const pipeRt = _cellUnescape(_cellSafe('a | b | c')) === 'a | b | c'; const nlGone = _cellSafe('a\nb') === 'a b'; const src = read(__filename); const wired = src.includes('_cellSafe(r.request)') && src.includes('_cellSafe(r.rule)'); return noRaw && pipeRt && nlGone && wired; } },
|
|
2969
|
-
{ name: '7번째 버그헌트 P1-B (UR-0105): verify-claim/optimism-check/honesty-check --json 에러 구조화 (1.9.400)', run: () => { const src = read(__filename); const vc = /function verifyClaimCmd[\s\S]{0,
|
|
2982
|
+
{ name: '7번째 버그헌트 P1-B (UR-0105): verify-claim/optimism-check/honesty-check --json 에러 구조화 (1.9.400)', run: () => { const src = read(__filename); const vc = /function verifyClaimCmd[\s\S]{0,1200}?failJson\(_j, 'not_found'/.test(src); const oc = /function optimismCheckCmd[\s\S]{0,700}?failJson\(_j, 'not_found'/.test(src); const hc = /function honestyCheckCmd[\s\S]{0,900}?failJson\(has\('--json'\), 'not_found'/.test(src); return vc && oc && hc; } }, // 1.30.5: {0,400}→{0,700} (F4 가 missing_args 라인을 en/ko 로 늘려 not_found 가 창 밖) · 1.33.2: vc {0,700}→{0,1200} (opts.collect 가드 라인이 not_found 를 더 밀어냄)
|
|
2970
2983
|
{ name: '7번째 버그헌트 P1-C (UR-0106): 시크릿 FN — gitignore 부정(!) + placeholder substring 정밀화 (1.9.401)', run: () => { const m = require('../lib/pure-utils'); const gm = m._gitignoreMatch; const negOk = gm('*.example\n!.env.example', '.env.example') === false && gm('*.log', 'a.log') === true && gm('a.log\n!a.log', 'a.log') === false && gm('.env', '.env') === true; const ph = m._isPlaceholderSecret; const phOk = ph('sk-EXAMPLEab12cd34ef56gh78ij90kl') === false && ph('sk-proj-realKEYexample9988776655') === false && ph('your-key-here') === true && ph('changeme') === true && ph('example') === true && ph('xxxxxxxxxxxxxxxxxxxxxxxxxxxx') === true; return negOk && phOk; } },
|
|
2971
2984
|
{ name: '7번째 버그헌트 P1-A 잔여 (UR-0108): decisions/lessons MD projection 개행 주입 차단 _lineSafe (1.9.402)', run: () => { const m = require('../lib/pure-utils'); if (m._lineSafe !== _lineSafe) return false; const lsOk = _lineSafe('a\nb\r\nc') === 'a b c'; const md = m._renderDecisionsMd([{ date: '2026-06-07', title: 'real\n### 2099-01-01 — FAKE\n- Decision: forged', decision: 'd', reason: 'r' }]); const re = m._decisionsFromMd(md); const noInject = re.length === 1 && !/^### 2099-01-01 — FAKE/m.test(md); const lmd = m._renderLessonsMd([{ date: '2026-06-07', text: 'l1\n### FAKE\n- Lesson: x', tag: 't' }]); const lre = m._parseLessonEntries(lmd); const lNoInject = lre.length === 1; return lsOk && noInject && lNoInject; } },
|
|
2972
2985
|
{ name: '7번째 버그헌트 P2 (UR-0107): api-skill show/drop 에러 exit code 1 (1.9.403)', run: () => { const src = read(__filename); const showId = src.includes("api-skill show <id>')); process.exitCode = 1"); const dropId = src.includes("api-skill drop <id>')); process.exitCode = 1"); const addUrl = src.includes("api-skill add <url> [--direction") && src.includes('process.exitCode = 1'); return showId && dropId && addUrl; } },
|
|
@@ -3304,13 +3317,17 @@ function _selfTestCases() {
|
|
|
3304
3317
|
&& src.includes('completion_claim_allowed: _ccaH'); // handoff json + latest.json
|
|
3305
3318
|
return pure && wired;
|
|
3306
3319
|
} },
|
|
3307
|
-
{ name: 'GPT-5.5 전략리뷰 §6.7 (UR-0152): ci init — PR gate 워크플로 생성 + exit-code 정책 (1.9.444)', run: () => {
|
|
3320
|
+
{ name: 'GPT-5.5 전략리뷰 §6.7 (UR-0152): ci init — PR gate 워크플로 생성 + exit-code 정책 (1.9.444) + 강화(1.33.1 버전핀/권한/concurrency)', run: () => {
|
|
3308
3321
|
if (typeof ciInitCmd !== 'function') return false;
|
|
3309
3322
|
const wf = LEERNESS_GATE_WORKFLOW;
|
|
3310
|
-
const contentOk = /name:\s*leerness-gate/.test(wf) && /on:\s*\n\s*pull_request:/.test(wf) && /
|
|
3323
|
+
const contentOk = /name:\s*leerness-gate/.test(wf) && /on:\s*\n\s*pull_request:/.test(wf) && /exit code 정책/.test(wf) && /actions\/checkout@v4/.test(wf);
|
|
3324
|
+
// 1.33.1 강화: 버전 핀(leerness@x.y.z gate, latest 와 동일) + 최소권한 permissions + concurrency cancel
|
|
3325
|
+
const hardened = new RegExp('leerness@' + VERSION.replace(/\./g, '\\.') + ' gate \\.').test(wf)
|
|
3326
|
+
&& /permissions:\s*\n\s*contents: read/.test(wf)
|
|
3327
|
+
&& /concurrency:\s*\n\s*group: leerness-gate/.test(wf) && /cancel-in-progress: true/.test(wf);
|
|
3311
3328
|
const src = read(__filename);
|
|
3312
3329
|
const wired = src.includes("cmd === 'ci' && (args[1] === 'init'") && src.includes('ciInitCmd(absRoot(_resolveRoot(args[2]))');
|
|
3313
|
-
return contentOk && wired;
|
|
3330
|
+
return contentOk && hardened && wired;
|
|
3314
3331
|
} },
|
|
3315
3332
|
{ name: 'UR-0151: decision/lesson/rule add positional path 지원(_taskPositionalPath 재사용, cwd 오염 차단) (1.9.445)', run: () => {
|
|
3316
3333
|
const src = read(__filename);
|
|
@@ -3451,6 +3468,28 @@ function _selfTestCases() {
|
|
|
3451
3468
|
const gitAdvisory = src.includes("const gitClaimOk = !(has('--strict-claims') && gitStrongMismatch);");
|
|
3452
3469
|
return defaultGate && jsonExposed && jsonGate && overall && wholeScan && gitAdvisory;
|
|
3453
3470
|
} },
|
|
3471
|
+
{ name: '1.33.2 (verify-claim --all): 집계 모드 — collect early-return(per-task verdict 재사용) + done 필터 + 라우팅 + 게이팅 부울 공유', run: () => {
|
|
3472
|
+
const src = read(__filename);
|
|
3473
|
+
const sigOpts = /function verifyClaimCmd\(root, taskId, opts = \{\}\)/.test(src);
|
|
3474
|
+
const fn = /function verifyClaimAllCmd\(root\)/.test(src);
|
|
3475
|
+
const doneFilter = src.includes("rows.filter(r => /done|완료|completed/i.test(String(r.status || '')))");
|
|
3476
|
+
const reuse = src.includes("doneRows.map(r => verifyClaimCmd(root, r.id, { collect: true }))");
|
|
3477
|
+
// collect 게이팅 부울이 --json/human 경로와 동일 출처(분기 없음) — 정밀 검사를 일괄 모드에서도 그대로 재사용
|
|
3478
|
+
const collectGate = src.includes('if (opts.collect) {') && src.includes("if (!filesAllExist) reasons.push('files-missing')") && src.includes("if (claimsChecked && !strictOk) reasons.push('optimism/honesty')") && src.includes("if (!gitClaimOk) reasons.push('git-mismatch')") && src.includes("if (claimsChecked && stubFiles.length > 0) reasons.push('stub-impl')");
|
|
3479
|
+
const routed = src.includes("if (args[1] === '--all' || has('--all')) return verifyClaimAllCmd(_p)");
|
|
3480
|
+
return sigOpts && fn && doneFilter && reuse && collectGate && routed;
|
|
3481
|
+
} },
|
|
3482
|
+
{ name: '1.33.3 (verify-claim --all → gate+MCP): _verifyClaimsAll 코어(exit 없음) + gate --claims opt-in 6번째(기본 5 유지) + MCP leerness_verify_claim_all def↔case', run: () => {
|
|
3483
|
+
const src = read(__filename);
|
|
3484
|
+
const coreDef = src.match(/function _verifyClaimsAll\(root\) \{[\s\S]*?\n\}/); // 함수 본문만 캡처(첫 줄머리 } 까지)
|
|
3485
|
+
const core = !!coreDef && coreDef[0].includes('return { ok: failed.length === 0, total: doneRows.length, failed: failed.length, results };') && !/process\.exit\(/.test(coreDef[0]); // 코어는 절대 process.exit( 안 함(게이트 step 집계 보호)
|
|
3486
|
+
const gateOptIn = src.includes("const withClaims = has('--claims');") && src.includes('# leerness gate (${withClaims ? 6 : 5} checks)') && src.includes("if (withClaims) step('verify-claims', () => { const r = _verifyClaimsAll(root);");
|
|
3487
|
+
const reuse = src.includes('const res = _verifyClaimsAll(root);'); // CLI 도 코어 공유(분기 없음)
|
|
3488
|
+
const tools = require('../lib/mcp-tools');
|
|
3489
|
+
const def = tools.find(t => t.name === 'leerness_verify_claim_all');
|
|
3490
|
+
const mcpOk = !!def && def.requiredTier === 'read-only' && src.includes("case 'leerness_verify_claim_all':") && /case 'leerness_verify_claim_all':[\s\S]{0,160}'verify-claim', '--all'[\s\S]{0,80}'--json'/.test(src);
|
|
3491
|
+
return core && gateOptIn && reuse && mcpOk && tools.length >= 84;
|
|
3492
|
+
} },
|
|
3454
3493
|
{ name: '14th 버그헌트 P2 (UR-0178/0179/0180): completed→done 정규화 + rule archive _cellSafe + nextRuleId 아카이브 스캔 (1.11.3)', run: () => {
|
|
3455
3494
|
if (_normTaskStatus('completed') !== 'done' || _normTaskStatus('verified') !== 'done' || _normTaskStatus('in-progress') !== 'in-progress') return false;
|
|
3456
3495
|
const src = read(__filename);
|
|
@@ -4647,7 +4686,7 @@ function commandsCmd(root) {
|
|
|
4647
4686
|
{ cmd: 'scan secrets [path]', desc: '시크릿 탐지' },
|
|
4648
4687
|
{ cmd: 'encoding check [path]', desc: '인코딩 검증' },
|
|
4649
4688
|
{ cmd: 'lazy detect [path] [--json]', desc: '게으른 작업 감지 (1.9.101)' },
|
|
4650
|
-
{ cmd: 'verify-claim <T-ID> [--run-tests] [--test-cmd "<명령>"] [--strict-claims] [--require-evidence]', desc: '주장 검증 (1.9.18~26) — --require-evidence: done 주장에 파일+테스트 근거 강제 (1.9.287) · --test-cmd: 비-JS 테스트 명령 (1.17.2)' },
|
|
4689
|
+
{ cmd: 'verify-claim <T-ID|--all> [--run-tests] [--test-cmd "<명령>"] [--strict-claims] [--require-evidence]', desc: '주장 검증 (1.9.18~26) — --all: 모든 done 주장 일괄 검증(CI·스케일, 1.33.2) · --require-evidence: done 주장에 파일+테스트 근거 강제 (1.9.287) · --test-cmd: 비-JS 테스트 명령 (1.17.2)' },
|
|
4651
4690
|
{ cmd: 'lens [code|design|docs|test|security] [--json]', desc: '분야별 자기질문 품질 렌즈 + 분야간 인과관계 (1.18.3)' },
|
|
4652
4691
|
{ cmd: 'optimism-check <T-ID>', desc: '낙관적 API 감지 (1.9.26)' },
|
|
4653
4692
|
{ cmd: 'requests audit|list|complete|drop|auto-complete', desc: '사용자 요청 추적 (1.9.207/223)' },
|
|
@@ -10385,13 +10424,14 @@ function _vcImplIsEmpty(body) {
|
|
|
10385
10424
|
return residue === ''; // 의미 토큰이 하나도 안 남으면 스텁
|
|
10386
10425
|
}
|
|
10387
10426
|
|
|
10388
|
-
function verifyClaimCmd(root, taskId) {
|
|
10427
|
+
function verifyClaimCmd(root, taskId, opts = {}) {
|
|
10389
10428
|
root = absRoot(root);
|
|
10390
10429
|
const _j = has('--json'); // 1.9.400 (7번째 버그헌트 P1-B, UR-0105): --json 에러도 구조화
|
|
10391
|
-
|
|
10430
|
+
// 1.33.2: opts.collect — verify-claim --all 집계 모드. 렌더/exit 없이 verdict 객체만 반환 (per-task 경로 무변경, collect 일 때만 분기).
|
|
10431
|
+
if (!taskId) { if (opts.collect) return { id: taskId, ok: false, reasons: ['no-id'] }; return failJson(_j, 'missing_args', _uiLang(root) === 'en' ? 'verify-claim <T-ID> required. ex: leerness verify-claim T-0008' : 'verify-claim <T-ID> 필요. 예: leerness verify-claim T-0008'); } // 1.30.5 (#156 F4)
|
|
10392
10432
|
const rows = readProgressRows(root);
|
|
10393
10433
|
const row = rows.find(r => r.id === taskId);
|
|
10394
|
-
if (!row) return failJson(_j, 'not_found', `progress-tracker.md에 ${taskId} 없음.`);
|
|
10434
|
+
if (!row) { if (opts.collect) return { id: taskId, ok: false, reasons: ['not-found'] }; return failJson(_j, 'not_found', `progress-tracker.md에 ${taskId} 없음.`); }
|
|
10395
10435
|
|
|
10396
10436
|
const evidence = row.evidence || '';
|
|
10397
10437
|
// 1.9.20: 파일 경로 추출 — 도메인 폴더 자동 인식 + 루트 메타파일
|
|
@@ -10589,6 +10629,22 @@ function verifyClaimCmd(root, taskId) {
|
|
|
10589
10629
|
// 1.11.2 (UR-0175): git strongMismatch 는 기본 게이트에서 제외(advisory) — 커밋 후 검증(정상 흐름)에서 커밋된 파일이 working-tree 변경에 없어 false-fail 하므로. --strict-claims 시에만 FAIL 기여. 기본 게이트는 신뢰도 높은 optimism(claimsConsistent)이 담당.
|
|
10590
10630
|
const gitClaimOk = !(has('--strict-claims') && gitStrongMismatch);
|
|
10591
10631
|
|
|
10632
|
+
// 1.33.2 (verify-claim --all): 집계 모드는 렌더/exit 없이 verdict 만 반환. 게이팅 부울은 아래 --json/human 경로와 동일 계산을 공유(분기 없음).
|
|
10633
|
+
if (opts.collect) {
|
|
10634
|
+
const _tcMatch = declaredTestCount == null ? null : (!testMeasured ? null : actualTestCount >= declaredTestCount);
|
|
10635
|
+
const _runFail = !!(runResult && !runResult.skipped && !runResult.allPassed);
|
|
10636
|
+
const reasons = [];
|
|
10637
|
+
if (!filesAllExist) reasons.push('files-missing');
|
|
10638
|
+
if (_tcMatch === false) reasons.push('test-count-short');
|
|
10639
|
+
if (!evidenceQualityOk) reasons.push('evidence-incomplete');
|
|
10640
|
+
if (claimsChecked && !strictOk) reasons.push('optimism/honesty');
|
|
10641
|
+
if (!gitClaimOk) reasons.push('git-mismatch');
|
|
10642
|
+
if (claimsChecked && stubFiles.length > 0) reasons.push('stub-impl');
|
|
10643
|
+
if (has('--strict-claims') && testLinkOk === false) reasons.push('test-impl-unlinked');
|
|
10644
|
+
if (_runFail) reasons.push('tests-failed');
|
|
10645
|
+
return { id: taskId, request: row.request, status: row.status, ok: reasons.length === 0, reasons };
|
|
10646
|
+
}
|
|
10647
|
+
|
|
10592
10648
|
if (has('--json')) {
|
|
10593
10649
|
const out = {
|
|
10594
10650
|
project: path.basename(root),
|
|
@@ -10742,6 +10798,44 @@ function verifyClaimCmd(root, taskId) {
|
|
|
10742
10798
|
log(t(` ✓ evidence 주장이 실제 파일·테스트${runResult && !runResult.skipped ? '·실행 결과' : ''}와 일치`, ` ✓ evidence claim matches actual files·tests${runResult && !runResult.skipped ? '·run results' : ''}`));
|
|
10743
10799
|
}
|
|
10744
10800
|
|
|
10801
|
+
// 1.33.2 (verify-claim+CI gate 슬라이스 강화): verify-claim --all — 모든 done/완료 주장을 한 번에 검증(CI·스케일용).
|
|
10802
|
+
// per-task 경로(verifyClaimCmd)를 opts.collect 로 재사용 → verdict 만 집계(렌더/exit 분기는 collect 가 흡수). 통과 플래그(--run-tests/--strict-claims/--lenient 등)는 전역이라 각 task 에 그대로 적용됨.
|
|
10803
|
+
// 동기: 플래그십(verify-claim)이 종전 per-task 전용이라, "내 완료 주장 전부 증거와 맞는가?"를 한 명령으로 못 했음.
|
|
10804
|
+
// 1.34.1 (16th리뷰 정직화): 기본 게이트 5체크는 워크스페이스-상태 휴리스틱(handoff/test-run/evidence 부재 등)으로 거짓완료를 잡고, 콘텐츠-레벨 주장(파일 존재·테스트 카운트·스텁·optimism)은 검사하지 않음. verify-claim --all 은 그 콘텐츠 차원을 추가 — lazy detect 가 깨끗한(handoff·테스트기록 완비) 성숙 프로젝트에선 5체크가 통과해도 이 검사만 콘텐츠 거짓을 잡음(실증: gate 기본 exit 0 vs --claims exit 1).
|
|
10805
|
+
// 1.33.3: 일괄 검증 코어 — 렌더/exit 없이 결과만 반환. verifyClaimAllCmd(CLI 렌더+exit) 와 gate --claims(opt-in 체크) 가 공유.
|
|
10806
|
+
// gate 의 step() 은 process.exit 가 아니라 process.exitCode 로 실패를 감지하므로, 코어는 절대 process.exit 하지 않음(게이트 프로세스 조기종료 방지).
|
|
10807
|
+
function _verifyClaimsAll(root) {
|
|
10808
|
+
root = absRoot(root);
|
|
10809
|
+
const rows = readProgressRows(root);
|
|
10810
|
+
const doneRows = rows.filter(r => /done|완료|completed/i.test(String(r.status || '')));
|
|
10811
|
+
const results = doneRows.map(r => verifyClaimCmd(root, r.id, { collect: true }));
|
|
10812
|
+
const failed = results.filter(x => x && !x.ok);
|
|
10813
|
+
return { ok: failed.length === 0, total: doneRows.length, failed: failed.length, results };
|
|
10814
|
+
}
|
|
10815
|
+
function verifyClaimAllCmd(root) {
|
|
10816
|
+
root = absRoot(root);
|
|
10817
|
+
const _j = has('--json');
|
|
10818
|
+
const _L = _uiLang(root); const t = (ko, en) => (_L === 'en' ? en : ko);
|
|
10819
|
+
const res = _verifyClaimsAll(root);
|
|
10820
|
+
const { results, total } = res;
|
|
10821
|
+
if (_j) {
|
|
10822
|
+
log(JSON.stringify({ ok: res.ok, root: path.basename(root), total, failed: res.failed, results }, null, 2));
|
|
10823
|
+
if (res.failed) process.exitCode = 1;
|
|
10824
|
+
return;
|
|
10825
|
+
}
|
|
10826
|
+
log(`# verify-claim --all (${path.basename(root)})`);
|
|
10827
|
+
if (!total) { log(t(' 완료(done) 주장이 없어 검증할 항목이 없습니다.', ' No completed (done) claims to verify.')); return; }
|
|
10828
|
+
log(t(` 완료 주장 ${total}건 검증 — 통과 ${total - res.failed} · 실패 ${res.failed}`, ` Verified ${total} completed claim(s) — pass ${total - res.failed} · fail ${res.failed}`));
|
|
10829
|
+
log('');
|
|
10830
|
+
for (const r of results) {
|
|
10831
|
+
if (r.ok) log(` ✓ ${r.id} ${String(r.request || '').slice(0, 60)}`);
|
|
10832
|
+
else log(` ✗ ${r.id} ${String(r.request || '').slice(0, 50)} ${t('← 불일치', '← mismatch')}: ${r.reasons.join(', ')}`);
|
|
10833
|
+
}
|
|
10834
|
+
log('');
|
|
10835
|
+
if (res.failed) { log(t(` ⚠ ${res.failed}건의 완료 주장이 증거와 불일치 — 재검토 권장 (개별 상세: leerness verify-claim <T-ID>)`, ` ⚠ ${res.failed} completed claim(s) do not match evidence — review (per-task detail: leerness verify-claim <T-ID>)`)); return process.exit(1); }
|
|
10836
|
+
log(t(` ✓ 모든 완료 주장이 증거와 일치`, ` ✓ all completed claims match evidence`));
|
|
10837
|
+
}
|
|
10838
|
+
|
|
10745
10839
|
// 1.9.22: orchestrate — Ollama 로컬 LLM으로 best-of-N 멀티 에이전트 시뮬
|
|
10746
10840
|
// 정책 (사용자 명시 1.9.22):
|
|
10747
10841
|
// 1) 자동 적용 금지. LEERNESS_OLLAMA_BASE_URL 환경변수 감지 opt-in 전용
|
|
@@ -12634,7 +12728,11 @@ function gate(root) {
|
|
|
12634
12728
|
const jsonMode = has('--json'); // 외부리뷰 C2: --json 일관성 — 이전엔 텍스트 헤더+단계별 JSON 혼재로 파싱 불가. 단일 객체로 집계.
|
|
12635
12729
|
const checks = [];
|
|
12636
12730
|
let bad = 0;
|
|
12637
|
-
|
|
12731
|
+
// 1.33.3 (verify-claim+CI gate 슬라이스 강화): --claims opt-in — 모든 done 주장을 정밀 per-claim 검증(verify-claim --all)으로 추가(6번째). 기본 5체크는 무변경(기존 어댑터 회귀 0).
|
|
12732
|
+
// 1.34.1 (16th리뷰 정직화): 기본 5체크(특히 lazy detect)는 워크스페이스-상태(handoff/test-run/evidence 부재) 신호로 거짓완료를 잡지, 콘텐츠(파일/카운트/스텁)는 검사하지 않음.
|
|
12733
|
+
// --claims 는 콘텐츠-레벨 검증을 추가 — 워크스페이스가 깨끗한 성숙 프로젝트에선 기본 5체크가 통과(exit 0)해도 --claims 만 콘텐츠 거짓을 잡아(exit 1) README 약속("claims fail → cannot merge")을 문자 그대로 강제. (실증 가드: e2e B(1.34.1))
|
|
12734
|
+
const withClaims = has('--claims');
|
|
12735
|
+
if (!jsonMode) log(`# leerness gate (${withClaims ? 6 : 5} checks)`);
|
|
12638
12736
|
function step(label, fn) {
|
|
12639
12737
|
const code0 = process.exitCode || 0;
|
|
12640
12738
|
if (!jsonMode) log(`\n## ${label}`);
|
|
@@ -12653,6 +12751,8 @@ function gate(root) {
|
|
|
12653
12751
|
step('scan secrets', () => scanSecrets(root));
|
|
12654
12752
|
step('encoding check', () => encodingCheck(root));
|
|
12655
12753
|
step('lazy detect', () => lazyDetect(root));
|
|
12754
|
+
// 1.33.3: opt-in 정밀 per-claim 검증. 코어(_verifyClaimsAll)는 exit 안 하고 결과만 반환 → step 의 exitCode 감지로 실패 집계. 비-json 모드는 불일치 task 를 fail() 로 표면화.
|
|
12755
|
+
if (withClaims) step('verify-claims', () => { const r = _verifyClaimsAll(root); if (!r.ok) { process.exitCode = 1; if (!jsonMode) r.results.filter(x => x && !x.ok).forEach(x => fail(`${x.id} 불일치: ${x.reasons.join(', ')}`)); } });
|
|
12656
12756
|
if (jsonMode) { log(JSON.stringify({ version: VERSION, root, ok: bad === 0, total: checks.length, failed: bad, checks }, null, 2)); if (bad) process.exitCode = 1; return; }
|
|
12657
12757
|
log(`\n# gate summary: ${bad} 단계 실패`);
|
|
12658
12758
|
if (bad) process.exitCode = 1;
|
|
@@ -16041,6 +16141,7 @@ function _mcpToCliArgs(name, args, targetPath) {
|
|
|
16041
16141
|
case 'leerness_drift_check': cliArgs = ['drift', 'check', targetPath, '--json']; break;
|
|
16042
16142
|
case 'leerness_audit': cliArgs = ['audit', targetPath, '--json', ...(args.fix ? ['--fix'] : []), ...(args.strict ? ['--strict'] : [])]; break;
|
|
16043
16143
|
case 'leerness_verify_claim': cliArgs = ['verify-claim', args.taskId, '--path', targetPath, ...(args.runTests ? ['--run-tests'] : []), ...(args.strictClaims ? ['--strict-claims'] : []), ...(args.lenient ? ['--lenient'] : [])]; break;
|
|
16144
|
+
case 'leerness_verify_claim_all': cliArgs = ['verify-claim', '--all', '--path', targetPath, '--json', ...(args.runTests ? ['--run-tests'] : []), ...(args.strictClaims ? ['--strict-claims'] : []), ...(args.lenient ? ['--lenient'] : [])]; break; // 1.33.3: 모든 done 주장 일괄 검증(--json 강제 → process.exitCode 만, 하드 exit 없음)
|
|
16044
16145
|
case 'leerness_contract_verify': cliArgs = ['contract', 'verify', args.spec, args.impl]; break;
|
|
16045
16146
|
case 'leerness_agents_list': cliArgs = ['agents', 'list', '--json']; break;
|
|
16046
16147
|
case 'leerness_reuse_map': cliArgs = ['reuse-map', targetPath, ...(args.allApps ? ['--all-apps'] : []), ...(args.strictElements ? ['--strict-elements'] : []), '--json']; break;
|
|
@@ -17227,13 +17328,22 @@ function runsShowCmd(root, id) {
|
|
|
17227
17328
|
|
|
17228
17329
|
// 1.9.444 (GPT-5.5 전략리뷰 §6.7, UR-0152): CI/PR 턴키 — PR 마다 leerness gate 를 실행하는 GitHub Actions 워크플로 생성.
|
|
17229
17330
|
// gate = verify + audit + scan secrets + encoding + lazy. exit 1 시 PR 체크 실패 → 증거 없는 완료/시크릿/규칙 위반을 CI 에서 차단.
|
|
17331
|
+
// 1.33.1 (verify-claim+gate 슬라이스 강화, 웹 Opus 4.8 리뷰 "pin a version"): 생성 워크플로를 production-grade 로 —
|
|
17332
|
+
// (1) leerness 버전 핀(재현성·공급망: ci init 가 설치 버전 주입) (2) permissions 최소권한 (3) concurrency 중복 취소.
|
|
17230
17333
|
const LEERNESS_GATE_WORKFLOW = [
|
|
17231
17334
|
'# leerness gate — AI 코딩 작업 증거/규칙/검증 게이트 (PR CI). leerness ci init 로 생성.',
|
|
17232
17335
|
'# exit code 정책: 통과=0, 실패(테스트 실패 / 커밋 시크릿 / 인코딩 깨짐 / 게으름·증거 누락 / 필수 규칙 파일 없음)=1 → PR 체크 실패.',
|
|
17336
|
+
'# 버전 핀(재현성·공급망 안전): 아래 leerness@' + VERSION + ' 을 의도적으로만 갱신하세요 (leerness ci init --force 재생성).',
|
|
17337
|
+
'# unpinned latest 는 새 릴리스가 나오면 게이트 판정이 조용히 바뀔 수 있어 지양 — CI 게이트는 재현 가능해야 합니다.',
|
|
17233
17338
|
'name: leerness-gate',
|
|
17234
17339
|
'on:',
|
|
17235
17340
|
' pull_request:',
|
|
17236
17341
|
' workflow_dispatch:',
|
|
17342
|
+
'permissions:',
|
|
17343
|
+
' contents: read # 최소 권한 — gate 는 읽기·검증만 (least-privilege)',
|
|
17344
|
+
'concurrency:',
|
|
17345
|
+
' group: leerness-gate-${{ github.ref }}',
|
|
17346
|
+
' cancel-in-progress: true',
|
|
17237
17347
|
'jobs:',
|
|
17238
17348
|
' gate:',
|
|
17239
17349
|
' runs-on: ubuntu-latest',
|
|
@@ -17243,7 +17353,7 @@ const LEERNESS_GATE_WORKFLOW = [
|
|
|
17243
17353
|
' with:',
|
|
17244
17354
|
" node-version: '20'",
|
|
17245
17355
|
' - name: leerness gate',
|
|
17246
|
-
' run: npx -y leerness gate .',
|
|
17356
|
+
' run: npx -y leerness@' + VERSION + ' gate .',
|
|
17247
17357
|
'',
|
|
17248
17358
|
].join('\n');
|
|
17249
17359
|
function ciInitCmd(root, opts = {}) {
|
|
@@ -17261,6 +17371,8 @@ function ciInitCmd(root, opts = {}) {
|
|
|
17261
17371
|
if (json) { log(JSON.stringify({ ok: true, created: true, path: relPath })); return; }
|
|
17262
17372
|
ok(`생성: ${relPath} — PR 마다 leerness gate 실행 (exit 1 시 PR 체크 실패)`);
|
|
17263
17373
|
log(' gate = verify + audit + scan secrets + encoding + lazy. 증거 없는 완료·시크릿·규칙 위반을 CI 에서 차단.');
|
|
17374
|
+
log(` 버전 핀 leerness@${VERSION}(재현성·공급망) · permissions 최소권한(contents: read) · 중복 실행 자동 취소.`);
|
|
17375
|
+
log(' ⮕ 다음 단계(가드레일 완성): GitHub branch protection 에서 leerness-gate 를 "required" 체크로 지정 — 그래야 우회 불가.');
|
|
17264
17376
|
}
|
|
17265
17377
|
// 1.9.294 (UR-0025 3단계): 역할/모델 카탈로그(_PROVIDER_MODEL_CATALOG + _AGENT_ROLE_PROMPTS + ROLE_CATALOG + _ROLE_ALIASES) 데이터 모듈 분리 (비파괴, require-based).
|
|
17266
17378
|
const { _PROVIDER_MODEL_CATALOG, _AGENT_ROLE_PROMPTS, ROLE_CATALOG, _ROLE_ALIASES } = require('../lib/role-catalog');
|
|
@@ -20014,6 +20126,19 @@ function reviewRequestCmd(root, request) { return _reviewRequest.reviewRequestCm
|
|
|
20014
20126
|
const _diag = require('../lib/diagnostics');
|
|
20015
20127
|
function doctorCmd(opts = {}) { return _diag.doctorCmd(opts, { VERSION, uiLang: _uiLang(arg('--path', process.cwd())), _selfTestCases, _detectShellCtx, _mcpToolCount, has, harnessPath: __filename }); }
|
|
20016
20128
|
function whichCmd() { return _diag.whichCmd({ VERSION, uiLang: _uiLang(arg('--path', process.cwd())), has, harnessPath: __filename }); }
|
|
20129
|
+
// 1.34.3 (T-0077): `graph --html` → lib/graph.js 온톨로지 HTML(leerness.html) 생성기 위임. 데이터는 in-process 로더 주입(자식 프로세스 셸링 X).
|
|
20130
|
+
const _graph = require('../lib/graph');
|
|
20131
|
+
function graphHtmlCmd(root) { return _graph.graphHtmlCmd(root, { _roadmapData, _loadDecisions, _loadLessons, has, arg }); }
|
|
20132
|
+
// 1.34.4 (T-0077 후속): handoff 시 leerness.html 자동 재생성 — opt-in(LEERNESS_AUTO_GRAPH=1, 기본 OFF / "Always-Off Opt-In"). 사용자 비전 "자동으로 작성되게" 충족. 비치명(try/catch) · 기본경로 무영향.
|
|
20133
|
+
function _maybeAutoGraph(root) {
|
|
20134
|
+
if (process.env.LEERNESS_AUTO_GRAPH !== '1') return;
|
|
20135
|
+
try {
|
|
20136
|
+
const r0 = absRoot(root || process.cwd());
|
|
20137
|
+
if (!exists(path.join(r0, '.harness'))) return;
|
|
20138
|
+
const s = _graph.graphHtmlCmd(r0, { _roadmapData, _loadDecisions, _loadLessons, quiet: true });
|
|
20139
|
+
if (!has('--json') && !has('--quiet') && !has('--compact')) log(`📊 ontology graph auto-regenerated: leerness.html (${s.nodes} nodes · ${s.edges} links) — LEERNESS_AUTO_GRAPH=1`); // --json 출력 오염 방지
|
|
20140
|
+
} catch {}
|
|
20141
|
+
}
|
|
20017
20142
|
|
|
20018
20143
|
// 1.23.1 (UR-0010 Phase 6): 영어 큐레이트 도움말 — 한국어 help 의 줄별 번역이 아니라, 카테고리별로 정리한 별도 영어판.
|
|
20019
20144
|
// 레거시 버전태그(1.9.x) 군더더기를 빼고 영어 사용자가 읽기 쉽게. 전체 전수 목록은 `leerness commands`.
|
|
@@ -20134,7 +20259,7 @@ function help() {
|
|
|
20134
20259
|
leerness skill install <SKILL.md|dir|url> · leerness skill discover --preset vercel|anthropic # 스킬 설치/탐색
|
|
20135
20260
|
leerness release bump [--patch|--minor|--major] # package.json 자동 bump (1.9.8)
|
|
20136
20261
|
leerness release note "<내용>" # CHANGELOG.md 자동 추가 (1.9.8)
|
|
20137
|
-
leerness release publish [--dry-run] [--pack] [--git-push] [--gh-release] [--gh-pages] [--gh-pages-src file] [--npm-publish] [--auto] # 통합 배포 (1.9.8 + 1.9.10)\n leerness impact <target> [--all] # 변경 전 영향 분석 (기본 strong, --all로 weak 포함)\n leerness reuse find <query> # 기존 자원 검색 (재귀 안내)\n leerness reuse register <name> --where <p> --kind component|hook|util|api [--note ...]\n leerness ui consistency [path] [--strict] [--fail-on-violation]\n leerness graph [path] [--out <file>] # mermaid 의존성 그래프\n leerness guide [target] # impact + reuse + ui consistency 통합 가이드\n leerness migrate audit|apply|plan [path] [--json] [--yes] # 크로스버전 마이그레이션 진단/적용(canonical 백필)/플랜(임시폴더 비교) (UR-0075, 1.9.356~358)\n leerness migrate --guide # AI 에이전트용 크로스버전 마이그레이션 가이드 (1.9.355)\n leerness install-safety [--json] # 설치 안전 프로필 — 0 런타임 deps / 0 install-script (1.9.359)\n leerness capabilities [--json] # 권한·보안 표면 공개 (1.9.272)\n leerness feature add|link|impact|list|show # 기능 그래프(feature-graph) 추적\n leerness permissions list|set # agent 권한 모드 (1.9.174)\n leerness creds list|register|check|refresh # 크리덴셜 메타 추적 (값 미저장)\n leerness incident list|show|handle · webhook serve · deploy auto · runs list|show # 운영(ops)\n leerness whats-new [path] # 최근 버전 변경 요약\n leerness team list|add|show|remove|preview|deploy <id> [--personas a,b --members claude,codex --schedule every-session --task "..." --deploy "<배포명령>" --yes] # 에이전트 팀 정의/미리보기/배포 — UR-0073 A~D, opt-in · 배포는 2중게이트(--yes + LEERNESS_TEAM_DEPLOY=1)\n leerness release channel|cadence [path] [--json] # 릴리스 채널 정책 + 빈도 진단 (UR-0074 케이던스 가시화, 1.9.275/374)\n leerness commands [--json] # 전체 명령 전수 목록 (누락 없이 이 명령으로 확인)\n`);
|
|
20262
|
+
leerness release publish [--dry-run] [--pack] [--git-push] [--gh-release] [--gh-pages] [--gh-pages-src file] [--npm-publish] [--auto] # 통합 배포 (1.9.8 + 1.9.10)\n leerness impact <target> [--all] # 변경 전 영향 분석 (기본 strong, --all로 weak 포함)\n leerness reuse find <query> # 기존 자원 검색 (재귀 안내)\n leerness reuse register <name> --where <p> --kind component|hook|util|api [--note ...]\n leerness ui consistency [path] [--strict] [--fail-on-violation]\n leerness graph [path] [--out <file>] # mermaid 의존성 그래프\n leerness graph [path] --html [--out <file>] # 온톨로지 그래프 HTML(leerness.html) 자동생성 — 노드 클릭으로 하네스 조회 (1.34.3)\n leerness guide [target] # impact + reuse + ui consistency 통합 가이드\n leerness migrate audit|apply|plan [path] [--json] [--yes] # 크로스버전 마이그레이션 진단/적용(canonical 백필)/플랜(임시폴더 비교) (UR-0075, 1.9.356~358)\n leerness migrate --guide # AI 에이전트용 크로스버전 마이그레이션 가이드 (1.9.355)\n leerness install-safety [--json] # 설치 안전 프로필 — 0 런타임 deps / 0 install-script (1.9.359)\n leerness capabilities [--json] # 권한·보안 표면 공개 (1.9.272)\n leerness feature add|link|impact|list|show # 기능 그래프(feature-graph) 추적\n leerness permissions list|set # agent 권한 모드 (1.9.174)\n leerness creds list|register|check|refresh # 크리덴셜 메타 추적 (값 미저장)\n leerness incident list|show|handle · webhook serve · deploy auto · runs list|show # 운영(ops)\n leerness whats-new [path] # 최근 버전 변경 요약\n leerness team list|add|show|remove|preview|deploy <id> [--personas a,b --members claude,codex --schedule every-session --task "..." --deploy "<배포명령>" --yes] # 에이전트 팀 정의/미리보기/배포 — UR-0073 A~D, opt-in · 배포는 2중게이트(--yes + LEERNESS_TEAM_DEPLOY=1)\n leerness release channel|cadence [path] [--json] # 릴리스 채널 정책 + 빈도 진단 (UR-0074 케이던스 가시화, 1.9.275/374)\n leerness commands [--json] # 전체 명령 전수 목록 (누락 없이 이 명령으로 확인)\n`);
|
|
20138
20263
|
}
|
|
20139
20264
|
|
|
20140
20265
|
async function main() {
|
|
@@ -20225,9 +20350,9 @@ async function main() {
|
|
|
20225
20350
|
if (cmd === 'encoding' && args[1] === 'check') return encodingCheck(arg('--path', args[2] || process.cwd()));
|
|
20226
20351
|
if (cmd === 'lazy' && args[1] === 'detect') return lazyDetect(_resolveRoot(args[2]), { json: has('--json') });
|
|
20227
20352
|
if (cmd === 'memory' && args[1] === 'search') return memorySearch(arg('--path', process.cwd()), args.slice(2).join(' '));
|
|
20228
|
-
if (cmd === 'handoff')
|
|
20353
|
+
if (cmd === 'handoff') { const _hp = arg('--path', args[1] || process.cwd()); const _hr = handoffCmd(_hp); _maybeAutoGraph(_hp); return _hr; }
|
|
20229
20354
|
if (cmd === 'reuse-map') return reuseMapCmd(arg('--path', args[1] || process.cwd()));
|
|
20230
|
-
if (cmd === 'verify-claim')
|
|
20355
|
+
if (cmd === 'verify-claim') { const _p = arg('--path', process.cwd()); if (args[1] === '--all' || has('--all')) return verifyClaimAllCmd(_p); return verifyClaimCmd(_p, args[1]); } // 1.33.2: --all → 모든 done 주장 일괄 검증
|
|
20231
20356
|
if (cmd === 'orchestrate') return await orchestrateCmd(arg('--path', process.cwd()), args.slice(1).filter(x => !x.startsWith('-')));
|
|
20232
20357
|
if (cmd === 'llm-bench' && args[1] === 'record') return llmBenchRecordCmd(arg('--path', process.cwd()));
|
|
20233
20358
|
if (cmd === 'deps') return depsImpactCmd(arg('--path', process.cwd()), args[1]);
|
|
@@ -20457,7 +20582,7 @@ async function main() {
|
|
|
20457
20582
|
if (cmd === 'reuse' && args[1] === 'find') return reuseFind(arg('--path', process.cwd()), args.slice(2).filter(x => !x.startsWith('-')).join(' '));
|
|
20458
20583
|
if (cmd === 'reuse' && args[1] === 'register') return reuseRegister(arg('--path', process.cwd()), args[2]);
|
|
20459
20584
|
if (cmd === 'ui' && args[1] === 'consistency') return uiConsistency(arg('--path', args[2] || process.cwd()));
|
|
20460
|
-
if (cmd === 'graph') return graphCmd(arg('--path', args[1] || process.cwd()));
|
|
20585
|
+
if (cmd === 'graph') return has('--html') ? graphHtmlCmd(arg('--path', args[1] || process.cwd())) : graphCmd(arg('--path', args[1] || process.cwd()));
|
|
20461
20586
|
if (cmd === 'guide') return guideCmd(arg('--path', process.cwd()), args[1]);
|
|
20462
20587
|
// legacy duplicate routing removed below (was: skill list/info/add)
|
|
20463
20588
|
if (cmd === 'skill' && args[1] === 'info') return skillInfo(args[2]);
|
package/lib/graph.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// lib/graph.js — leerness ontology graph (interactive HTML) generator.
|
|
2
|
+
// 1.34.3 (T-0077): `leerness graph --html` 분기 → 프로젝트 루트에 자기완결 leerness.html 생성.
|
|
3
|
+
// Obsidian graph-view 스타일 force-directed 캔버스로 5 메모리 표면(task/plan/decision/lesson/rule)
|
|
4
|
+
// + skills + feature-graph 를 노드/엣지로 렌더, 노드 클릭 → 내용 패널.
|
|
5
|
+
// - 데이터: deps 주입(_roadmapData · _loadDecisions · _loadLessons) — 자식 프로세스 셸링 없이 in-process.
|
|
6
|
+
// - I/O: ./io(absRoot · exists · read · writeUtf8 · log). 0 런타임 의존 · 자기완결 vanilla JS(차트 라이브러리 X).
|
|
7
|
+
// - XSS/주입: 임베드 직전 모든 '<' 를 < 로 치환(</script>·<!-- 무력화) + function 치환기로 $-특수문자 회피.
|
|
8
|
+
'use strict';
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { absRoot, exists, read, writeUtf8, log } = require('./io');
|
|
11
|
+
|
|
12
|
+
// 검증된 프로토타입 템플릿(Claude Preview 렌더+클릭조회 확인). `/*__DATA__*/null` 자리표시자에 JSON 주입.
|
|
13
|
+
// String.raw 필수: 내부 JS 의 `\'` 같은 escape 가 원문 그대로 출력돼 브라우저 JS 엔진이 해석하도록 보존.
|
|
14
|
+
const TEMPLATE = String.raw`<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
15
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"><title>leerness — ontology</title>
|
|
16
|
+
<style>
|
|
17
|
+
:root{--bg:#0a0d12;--panel:#0f141a;--line:#222a33;--txt:#e6edf3;--mut:#8b949e;--brand:#39d353;--mono:ui-monospace,'SF Mono',Menlo,monospace}
|
|
18
|
+
*{box-sizing:border-box}html,body{margin:0;height:100%;background:var(--bg);color:var(--txt);font-family:var(--mono);font-size:13px;overflow:hidden}
|
|
19
|
+
#bar{position:fixed;top:0;left:0;right:0;height:46px;display:flex;align-items:center;gap:14px;padding:0 16px;background:rgba(10,13,18,.85);backdrop-filter:blur(8px);border-bottom:1px solid var(--line);z-index:10}
|
|
20
|
+
#bar .ttl{font-weight:700;color:#fff;display:flex;align-items:center;gap:8px}
|
|
21
|
+
#bar .dot{width:9px;height:9px;border-radius:50%;background:var(--brand);box-shadow:0 0 10px var(--brand)}
|
|
22
|
+
#bar .stat{color:var(--mut);font-size:11px}
|
|
23
|
+
#search{background:#0b0f14;border:1px solid var(--line);color:var(--txt);border-radius:7px;padding:6px 10px;font:inherit;width:200px;outline:none}
|
|
24
|
+
#search:focus{border-color:var(--brand)}
|
|
25
|
+
#chips{display:flex;gap:6px;flex-wrap:wrap;margin-left:auto}
|
|
26
|
+
.chip{display:flex;align-items:center;gap:5px;border:1px solid var(--line);border-radius:100px;padding:3px 10px;cursor:pointer;font-size:11px;user-select:none}
|
|
27
|
+
.chip .sw{width:9px;height:9px;border-radius:50%}
|
|
28
|
+
.chip.off{opacity:.35}
|
|
29
|
+
canvas{position:fixed;inset:0;top:46px}
|
|
30
|
+
#panel{position:fixed;top:60px;right:14px;width:340px;max-height:calc(100% - 80px);overflow:auto;background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px;box-shadow:0 24px 60px -20px #000;display:none;z-index:9}
|
|
31
|
+
#panel.show{display:block}
|
|
32
|
+
#panel .pt{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--mut);text-transform:uppercase;letter-spacing:.06em;margin-bottom:8px}
|
|
33
|
+
#panel .pt .sw{width:10px;height:10px;border-radius:50%}
|
|
34
|
+
#panel h2{margin:0 0 12px;font-size:15px;line-height:1.4;color:#fff;word-break:break-word}
|
|
35
|
+
#panel .row{margin:0 0 10px;border-top:1px solid var(--line);padding-top:10px}
|
|
36
|
+
#panel .k{color:var(--mut);font-size:10px;text-transform:uppercase;letter-spacing:.05em;margin-bottom:3px}
|
|
37
|
+
#panel .v{white-space:pre-wrap;word-break:break-word;line-height:1.55}
|
|
38
|
+
#panel .nbrs a{color:#58a6ff;cursor:pointer;display:block;padding:2px 0}
|
|
39
|
+
#panel .x{position:absolute;top:12px;right:14px;color:var(--mut);cursor:pointer;font-size:16px}
|
|
40
|
+
#hint{position:fixed;bottom:12px;left:16px;color:var(--mut);font-size:11px;opacity:.7}
|
|
41
|
+
#empty{position:fixed;inset:0;display:none;place-items:center;color:var(--mut);text-align:center}
|
|
42
|
+
</style></head><body>
|
|
43
|
+
<div id="bar">
|
|
44
|
+
<div class="ttl"><span class="dot"></span><span id="proj">leerness</span><span style="color:var(--mut);font-weight:400">/ ontology</span></div>
|
|
45
|
+
<span class="stat" id="stat"></span>
|
|
46
|
+
<input id="search" placeholder="search nodes…" autocomplete="off">
|
|
47
|
+
<div id="chips"></div>
|
|
48
|
+
</div>
|
|
49
|
+
<canvas id="c"></canvas>
|
|
50
|
+
<div id="panel"><span class="x" onclick="closePanel()">✕</span><div id="pbody"></div></div>
|
|
51
|
+
<div id="empty">No nodes — run <b>leerness handoff .</b> to populate the harness, then regenerate.</div>
|
|
52
|
+
<div id="hint">drag node · scroll zoom · drag bg pan · click node → details</div>
|
|
53
|
+
<script>
|
|
54
|
+
var DATA = /*__DATA__*/null;
|
|
55
|
+
var COLORS={task:'#58a6ff',plan:'#d29922',decision:'#39d0d8',lesson:'#e3b341',rule:'#bc8cff',skill:'#2dd4bf',feature:'#6e7681'};
|
|
56
|
+
var STATUSCOL={done:'#3fb950',verified:'#3fb950','in-progress':'#58a6ff',in_progress:'#58a6ff',blocked:'#f85149',waiting:'#d29922',planned:'#8b949e',requested:'#8b949e'};
|
|
57
|
+
function nodeColor(n){ if(n.type==='task'&&STATUSCOL[n.status])return STATUSCOL[n.status]; return COLORS[n.type]||'#8b949e'; }
|
|
58
|
+
function esc(s){return String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
|
59
|
+
|
|
60
|
+
var cv=document.getElementById('c'),ctx=cv.getContext('2d'),DPR=Math.min(2,window.devicePixelRatio||1);
|
|
61
|
+
var W,H; function resize(){W=cv.clientWidth=window.innerWidth;H=cv.clientHeight=window.innerHeight-46;cv.width=W*DPR;cv.height=H*DPR;ctx.setTransform(DPR,0,0,DPR,0,0);} window.addEventListener('resize',resize);resize();
|
|
62
|
+
|
|
63
|
+
var nodes=DATA?DATA.nodes:[],edges=DATA?DATA.edges:[];
|
|
64
|
+
var idx={}; nodes.forEach(function(n,i){idx[n.id]=n; n.x=W/2+Math.cos(i)*Math.min(W,H)*0.32*Math.random()+ (Math.random()-0.5)*80; n.y=H/2+Math.sin(i)*Math.min(W,H)*0.32*Math.random()+(Math.random()-0.5)*80; n.vx=0;n.vy=0; n.deg=0;});
|
|
65
|
+
edges=edges.filter(function(e){return idx[e.source]&&idx[e.target];});
|
|
66
|
+
edges.forEach(function(e){idx[e.source].deg++;idx[e.target].deg++;});
|
|
67
|
+
var off={}; // hidden types
|
|
68
|
+
document.getElementById('proj').textContent=(DATA&&DATA.project)||'leerness';
|
|
69
|
+
document.getElementById('stat').textContent=nodes.length+' nodes · '+edges.length+' links';
|
|
70
|
+
if(!nodes.length){document.getElementById('empty').style.display='grid';}
|
|
71
|
+
|
|
72
|
+
// chips
|
|
73
|
+
var types=Array.from(new Set(nodes.map(function(n){return n.type;})));
|
|
74
|
+
var chipsEl=document.getElementById('chips');
|
|
75
|
+
types.forEach(function(t){var c=DATA.counts&&DATA.counts[t]; var el=document.createElement('div');el.className='chip';el.innerHTML='<span class="sw" style="background:'+(COLORS[t]||'#888')+'"></span>'+t+(c!=null?' '+c:'');el.onclick=function(){off[t]=!off[t];el.classList.toggle('off',!!off[t]);};chipsEl.appendChild(el);});
|
|
76
|
+
|
|
77
|
+
// view transform
|
|
78
|
+
var view={x:0,y:0,k:1};
|
|
79
|
+
var sel=null,hover=null,nbr={};
|
|
80
|
+
var cam={cx:W/2,cy:H/2};
|
|
81
|
+
|
|
82
|
+
// physics
|
|
83
|
+
var alpha=1;
|
|
84
|
+
function tick(){
|
|
85
|
+
if(alpha>0.005) alpha*=0.992;
|
|
86
|
+
var REP=2600,SPR=0.012,LEN=70,CEN=0.012;
|
|
87
|
+
for(var i=0;i<nodes.length;i++){var a=nodes[i]; if(off[a.type])continue;
|
|
88
|
+
for(var j=i+1;j<nodes.length;j++){var b=nodes[j]; if(off[b.type])continue;
|
|
89
|
+
var dx=a.x-b.x,dy=a.y-b.y,d2=dx*dx+dy*dy+0.01; if(d2>360000)continue; var d=Math.sqrt(d2);var f=REP/d2; var ux=dx/d,uy=dy/d; a.vx+=ux*f;a.vy+=uy*f;b.vx-=ux*f;b.vy-=uy*f;}
|
|
90
|
+
a.vx+=(cam.cx-a.x)*CEN; a.vy+=(cam.cy-a.y)*CEN;
|
|
91
|
+
}
|
|
92
|
+
edges.forEach(function(e){var a=idx[e.source],b=idx[e.target]; if(off[a.type]||off[b.type])return; var dx=b.x-a.x,dy=b.y-a.y,d=Math.sqrt(dx*dx+dy*dy)+0.01;var f=(d-LEN)*SPR;var ux=dx/d,uy=dy/d; a.vx+=ux*f;a.vy+=uy*f;b.vx-=ux*f;b.vy-=uy*f;});
|
|
93
|
+
nodes.forEach(function(n){ if(n.fixed)return; n.vx*=0.86;n.vy*=0.86; n.x+=n.vx*alpha*2.2;n.y+=n.vy*alpha*2.2;});
|
|
94
|
+
}
|
|
95
|
+
function toScreen(n){return{x:(n.x-cam.cx)*view.k+W/2+view.x,y:(n.y-cam.cy)*view.k+H/2+view.y};}
|
|
96
|
+
function fromScreen(sx,sy){return{x:(sx-W/2-view.x)/view.k+cam.cx,y:(sy-H/2-view.y)/view.k+cam.cy};}
|
|
97
|
+
|
|
98
|
+
function draw(){
|
|
99
|
+
ctx.clearRect(0,0,W,H);
|
|
100
|
+
// edges
|
|
101
|
+
ctx.lineWidth=1;
|
|
102
|
+
edges.forEach(function(e){var a=idx[e.source],b=idx[e.target]; if(off[a.type]||off[b.type])return; var p=toScreen(a),q=toScreen(b); var on=sel&&(e.source===sel.id||e.target===sel.id); ctx.strokeStyle=on?'rgba(57,211,83,.55)':'rgba(120,130,145,.16)'; ctx.beginPath();ctx.moveTo(p.x,p.y);ctx.lineTo(q.x,q.y);ctx.stroke();});
|
|
103
|
+
// nodes
|
|
104
|
+
nodes.forEach(function(n){ if(off[n.type])return; var p=toScreen(n); var r=(3+Math.min(7,n.deg*0.7))*Math.max(.6,view.k*.9); var dim=sel&&!nbr[n.id]&&n.id!==sel.id; var srch=window._q&&(n.label||'').toLowerCase().indexOf(window._q)<0&&n.id.toLowerCase().indexOf(window._q)<0;
|
|
105
|
+
ctx.globalAlpha=(dim||srch)?0.18:1; ctx.fillStyle=nodeColor(n); ctx.beginPath();ctx.arc(p.x,p.y,r,0,6.2832);ctx.fill();
|
|
106
|
+
if(n===sel||n===hover){ctx.strokeStyle='#fff';ctx.lineWidth=1.5;ctx.stroke();}
|
|
107
|
+
if(view.k>1.35||n===sel||n===hover||(window._q&&!srch)){ ctx.globalAlpha=(dim)?0.3:0.92; ctx.fillStyle='#cdd9e5';ctx.font='10px ui-monospace';ctx.fillText((n.label||n.id).slice(0,42),p.x+r+3,p.y+3.5);}
|
|
108
|
+
ctx.globalAlpha=1;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function loop(){tick();draw();requestAnimationFrame(loop);} loop();
|
|
112
|
+
|
|
113
|
+
// interaction
|
|
114
|
+
var drag=null,panning=null,moved=false;
|
|
115
|
+
cv.addEventListener('mousedown',function(ev){var m=hit(ev.offsetX,ev.offsetY);moved=false; if(m){drag=m;m.fixed=true;}else{panning={x:ev.offsetX,y:ev.offsetY,vx:view.x,vy:view.y};}});
|
|
116
|
+
window.addEventListener('mousemove',function(ev){var r=cv.getBoundingClientRect();var mx=ev.clientX-r.left,my=ev.clientY-r.top;
|
|
117
|
+
if(drag){var w=fromScreen(mx,my);drag.x=w.x;drag.y=w.y;drag.vx=0;drag.vy=0;alpha=Math.max(alpha,.3);moved=true;}
|
|
118
|
+
else if(panning){view.x=panning.vx+(mx-panning.x);view.y=panning.vy+(my-panning.y);moved=true;}
|
|
119
|
+
else{hover=hit(mx,my);cv.style.cursor=hover?'pointer':'default';}
|
|
120
|
+
});
|
|
121
|
+
window.addEventListener('mouseup',function(ev){ if(drag){drag.fixed=false; if(!moved)select(drag); drag=null;} else if(panning){ if(!moved){closePanel();} panning=null;} });
|
|
122
|
+
cv.addEventListener('wheel',function(ev){ev.preventDefault();var f=ev.deltaY<0?1.12:0.89;var nk=Math.max(0.2,Math.min(6,view.k*f)); view.k=nk;},{passive:false});
|
|
123
|
+
function hit(sx,sy){var best=null,bd=18*18; nodes.forEach(function(n){if(off[n.type])return;var p=toScreen(n);var dx=p.x-sx,dy=p.y-sy,d=dx*dx+dy*dy; if(d<bd){bd=d;best=n;}});return best;}
|
|
124
|
+
|
|
125
|
+
function select(n){sel=n;nbr={}; edges.forEach(function(e){if(e.source===n.id)nbr[e.target]=1;if(e.target===n.id)nbr[e.source]=1;}); showPanel(n);}
|
|
126
|
+
function closePanel(){sel=null;document.getElementById('panel').classList.remove('show');}
|
|
127
|
+
function showPanel(n){
|
|
128
|
+
var nb=Object.keys(nbr).map(function(id){return idx[id];}).filter(Boolean);
|
|
129
|
+
var h='<div class="pt"><span class="sw" style="background:'+nodeColor(n)+'"></span>'+esc(n.type)+(n.status?' · '+esc(n.status):'')+' · '+esc(n.id)+'</div>';
|
|
130
|
+
h+='<h2>'+esc(n.label||n.id)+'</h2>';
|
|
131
|
+
var d=n.detail||{};
|
|
132
|
+
Object.keys(d).forEach(function(k){ if(!d[k]||k==='request'&&d[k]===n.label)return; if(String(d[k]).trim()==='')return; h+='<div class="row"><div class="k">'+esc(k)+'</div><div class="v">'+esc(d[k])+'</div></div>';});
|
|
133
|
+
if(nb.length){h+='<div class="row"><div class="k">connected ('+nb.length+')</div><div class="nbrs">'+nb.slice(0,30).map(function(x){return '<a onclick="goto(\''+x.id.replace(/'/g,"")+'\')">'+esc(x.label||x.id)+'</a>';}).join('')+'</div></div>';}
|
|
134
|
+
document.getElementById('pbody').innerHTML=h;
|
|
135
|
+
document.getElementById('panel').classList.add('show');
|
|
136
|
+
}
|
|
137
|
+
window.goto=function(id){var n=idx[id];if(n){select(n);cam.cx=n.x;cam.cy=n.y;view.x=0;view.y=0;}};
|
|
138
|
+
document.getElementById('search').addEventListener('input',function(ev){window._q=ev.target.value.trim().toLowerCase()||null;});
|
|
139
|
+
</script></body></html>`;
|
|
140
|
+
|
|
141
|
+
const _txt = v => (v == null ? '' : String(v));
|
|
142
|
+
function _trunc(s, n) { s = _txt(s); return s.length > n ? s.slice(0, n - 1) + '…' : s; }
|
|
143
|
+
|
|
144
|
+
// 하네스 표면 → {project, version, counts, nodes, edges}. deps 로 in-process 로더 주입(셸링 X).
|
|
145
|
+
function buildGraphData(root, deps = {}) {
|
|
146
|
+
const { _roadmapData, _loadDecisions, _loadLessons } = deps;
|
|
147
|
+
const rd = (typeof _roadmapData === 'function' ? _roadmapData(root) : {}) || {};
|
|
148
|
+
const decisions = (typeof _loadDecisions === 'function' ? _loadDecisions(root) : []) || [];
|
|
149
|
+
const lessons = (typeof _loadLessons === 'function' ? _loadLessons(root) : []) || [];
|
|
150
|
+
|
|
151
|
+
const nodes = []; const byId = new Map(); const byLabel = new Map();
|
|
152
|
+
function add(node) {
|
|
153
|
+
if (byId.has(node.id)) return;
|
|
154
|
+
nodes.push(node); byId.set(node.id, node);
|
|
155
|
+
if (node.label) byLabel.set(_txt(node.label).trim().toLowerCase(), node.id);
|
|
156
|
+
}
|
|
157
|
+
// task (status 색상) — _roadmapData 가 evidence 의 M-#### 를 t.milestones 로 이미 추출.
|
|
158
|
+
for (const t of (rd.tasks || [])) add({ id: t.id, type: 'task', status: t.status || 'requested', label: _trunc(t.request, 64), detail: { request: _txt(t.request), status: _txt(t.status), evidence: _txt(t.evidence), nextAction: _txt(t.nextAction), updated: _txt(t.updated) }, _ms: t.milestones || [] });
|
|
159
|
+
// plan (milestone)
|
|
160
|
+
for (const m of (rd.milestones || [])) add({ id: m.id, type: 'plan', status: m.status || 'planned', label: _trunc(m.title, 64), detail: { title: _txt(m.title), status: _txt(m.status), progress: _txt(m.progress), doneWhen: _txt(m.doneWhen), nextAction: _txt(m.nextAction) } });
|
|
161
|
+
// decision — title 은 제네릭("Decision")일 수 있어 실내용(decision) 우선. id 없으면 합성.
|
|
162
|
+
let di = 0; for (const d of decisions) { const id = d.id || ('D-' + (++di)); add({ id, type: 'decision', status: '', label: _trunc(d.decision || d.title || d.text, 64), detail: { decision: _txt(d.decision || d.title), reason: _txt(d.reason), impact: _txt(d.impact), date: _txt(d.date) } }); }
|
|
163
|
+
// lesson — 내용은 text. id 없으면 합성.
|
|
164
|
+
let li = 0; for (const l of lessons) { const id = l.id || ('L-' + (++li)); add({ id, type: 'lesson', status: '', label: _trunc(l.title || l.lesson || l.text, 64), detail: { lesson: _txt(l.title || l.lesson || l.text), tag: _txt(l.tag), date: _txt(l.date) } }); }
|
|
165
|
+
// rule
|
|
166
|
+
let ri = 0; for (const r of (rd.rules || [])) { const id = r.id || ('R-' + (++ri)); add({ id, type: 'rule', status: r.status || '', label: _trunc(r.rule || r.text || r.title, 64), detail: { rule: _txt(r.rule || r.text), trigger: _txt(r.trigger), status: _txt(r.status), lastVerified: _txt(r.lastVerified) } }); }
|
|
167
|
+
// skill
|
|
168
|
+
let si = 0; for (const s of (rd.skills || [])) { const id = s.id || s.name || ('S-' + (++si)); add({ id: 'skill:' + id, type: 'skill', status: '', label: _trunc(s.name || s.title || id, 52), detail: { name: _txt(s.name || id), description: _txt(s.description || s.summary), category: _txt(s.category) } }); }
|
|
169
|
+
|
|
170
|
+
// edges — 같은 (source,target) 쌍 dedup: task→milestone 가 _ms 추출 + blob M-#### 정규식에서 이중 추가되는 것 방지(엣지수/degree 정확).
|
|
171
|
+
const edges = [];
|
|
172
|
+
const _seenEdge = new Set();
|
|
173
|
+
function linkIds(a, b, kind) { if (!(a && b && byId.has(a) && byId.has(b) && a !== b)) return; const k = a + '' + b; if (_seenEdge.has(k)) return; _seenEdge.add(k); edges.push({ source: a, target: b, kind }); }
|
|
174
|
+
for (const n of nodes) {
|
|
175
|
+
if (n._ms) for (const mid of n._ms) linkIds(n.id, mid, 'milestone');
|
|
176
|
+
const blob = Object.values(n.detail || {}).join(' ');
|
|
177
|
+
for (const m of (blob.match(/\bM-\d{3,}\b/g) || [])) linkIds(n.id, m, 'milestone');
|
|
178
|
+
for (const r of (blob.match(/\b[TURDL]-\d{3,}\b/g) || [])) linkIds(n.id, r, 'ref');
|
|
179
|
+
for (const w of (blob.match(/\[\[([^\]]+)\]\]/g) || [])) { const raw = w.slice(2, -2).trim(); const tid = byLabel.get(raw.toLowerCase()) || (byId.has(raw) ? raw : null); if (tid) linkIds(n.id, tid, 'link'); }
|
|
180
|
+
}
|
|
181
|
+
// feature-graph.md (선택) — "A -> B" / "A uses B" 의존 라인 → feature 노드/엣지.
|
|
182
|
+
const fg = path.join(root, '.harness', 'feature-graph.md');
|
|
183
|
+
if (exists(fg)) {
|
|
184
|
+
try {
|
|
185
|
+
for (const line of read(fg).split(/\r?\n/)) {
|
|
186
|
+
const m = line.match(/([\w./-]+)\s*(?:->|→|depends on|uses)\s*([\w./-]+)/i);
|
|
187
|
+
if (m) { const a = 'feat:' + m[1], b = 'feat:' + m[2]; add({ id: a, type: 'feature', status: '', label: _trunc(m[1], 40), detail: { feature: m[1] } }); add({ id: b, type: 'feature', status: '', label: _trunc(m[2], 40), detail: { feature: m[2] } }); linkIds(a, b, 'feature'); }
|
|
188
|
+
}
|
|
189
|
+
} catch {}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for (const n of nodes) delete n._ms; // 내부 보조 필드 임베드 제외
|
|
193
|
+
const counts = {};
|
|
194
|
+
for (const n of nodes) counts[n.type] = (counts[n.type] || 0) + 1;
|
|
195
|
+
return { project: rd.project || path.basename(root), version: rd.version || '', root, counts, nodes, edges };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// `leerness graph --html [path] [--out file] [--json]` 핸들러.
|
|
199
|
+
function graphHtmlCmd(root, deps = {}, outFile) {
|
|
200
|
+
root = absRoot(root);
|
|
201
|
+
const { has, quiet } = deps; // quiet: auto-gen(handoff) 시 사람용 3줄 로그 억제
|
|
202
|
+
const data = buildGraphData(root, deps);
|
|
203
|
+
const out = outFile || (has && has('--out') && deps.arg ? path.resolve(root, deps.arg('--out')) : path.join(root, 'leerness.html'));
|
|
204
|
+
// 임베드 안전화: 모든 '<' → < 로 치환해 </script>·<!-- 차단(JSON 문자열 내부라 런타임엔 '<' 복원). function 치환기로 $-특수문자(예: $&) 회피.
|
|
205
|
+
const json = JSON.stringify(data).replace(/</g, '\\u003c');
|
|
206
|
+
const html = TEMPLATE.replace('/*__DATA__*/null', () => json);
|
|
207
|
+
writeUtf8(out, html);
|
|
208
|
+
const summary = { ok: true, file: out, nodes: data.nodes.length, edges: data.edges.length, counts: data.counts };
|
|
209
|
+
if (has && has('--json')) { process.stdout.write(JSON.stringify(summary, null, 2) + '\n'); return summary; }
|
|
210
|
+
if (!quiet) {
|
|
211
|
+
log(`leerness.html → ${out}`);
|
|
212
|
+
log(` ${data.nodes.length} nodes · ${data.edges.length} links · ${Object.entries(data.counts).map(([k, v]) => k + ':' + v).join(' ')}`);
|
|
213
|
+
log(` open in a browser to explore the ontology graph (click a node → details).`);
|
|
214
|
+
}
|
|
215
|
+
return summary;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = { graphHtmlCmd, buildGraphData };
|
package/lib/mcp-tools.js
CHANGED
|
@@ -8,6 +8,7 @@ module.exports = [
|
|
|
8
8
|
{ name: 'leerness_drift_check', requiredTier: 'read-only', description: '1.9.136 — AI 에이전트 leerness 미사용 drift 자동 감지 JSON ({ root, score, level, signals[], healthy }). 5+ 신호 + 4단계 레벨 (🟢 healthy / 🟡 warning / 🟠 caution / 🔴 critical). 보안 신호 통합 (1.9.78)', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
|
|
9
9
|
{ name: 'leerness_audit', requiredTier: 'read-only', description: '1.9.102 — 워크스페이스 일관성 감사 JSON (warnings/failures/fixed/healthy + findings[]. kind 11종: design_dup/design_system_default/reuse_map_empty/milestone_unlinked/handoff_not_generated/current_state_stale/readme_version_mismatch/npm_cve/gitignore_missing_secrets/env_keys_missing/strict_promoted)', inputSchema: { type: 'object', properties: { path: { type: 'string' }, fix: { type: 'boolean' }, strict: { type: 'boolean' } } } },
|
|
10
10
|
{ name: 'leerness_verify_claim', requiredTier: 'read-only', description: 'AI 거짓 완료 자동 검증 (evidence 파일 + 실 테스트 실행). 1.9.309(UR-0048): done/완료 주장은 evidence(수정파일+테스트) 기본 강제 — 증거 없는 done 은 FAIL(exit 1). lenient=true 로 완화, runTests/strictClaims 추가 가능. 응답 verdict.evidenceComplete 포함.', inputSchema: { type: 'object', properties: { taskId: { type: 'string' }, path: { type: 'string' }, runTests: { type: 'boolean' }, strictClaims: { type: 'boolean' }, lenient: { type: 'boolean' } }, required: ['taskId'] } },
|
|
11
|
+
{ name: 'leerness_verify_claim_all', requiredTier: 'read-only', description: '1.33.3 — 모든 done/완료 주장을 한 번에 검증(CI·스케일). progress-tracker 의 done 항목 전부를 verify-claim 정밀 검사(파일 존재·스텁·부풀린 카운트·증거 완전성)로 일괄 점검. 응답 { ok, total, failed, results:[{id,ok,reasons}] }. 세션 마감 전 "내 완료 주장 전부 증거와 맞는가?" 자가 점검용. runTests/strictClaims/lenient 추가 가능.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, runTests: { type: 'boolean' }, strictClaims: { type: 'boolean' }, lenient: { type: 'boolean' } } } },
|
|
11
12
|
{ name: 'leerness_contract_verify', requiredTier: 'read-only', description: '명세 ↔ 구현 함수/필드 일치 자동 검사', inputSchema: { type: 'object', properties: { spec: { type: 'string' }, impl: { type: 'string' } }, required: ['spec', 'impl'] } },
|
|
12
13
|
{ name: 'leerness_agents_list', requiredTier: 'read-only', description: '외부 AI CLI 가용성 표 (claude/codex/agy/copilot 상태 + 환경변수 활성화 여부)', inputSchema: { type: 'object', properties: {} } },
|
|
13
14
|
{ name: 'leerness_reuse_map', requiredTier: 'read-only', description: '워크스페이스 중복 함수/capability 자동 감지 (--all-apps + fuzzy 매칭)', inputSchema: { type: 'object', properties: { path: { type: 'string' }, allApps: { type: 'boolean' }, strictElements: { type: 'boolean' } } } },
|