leerness 1.35.0 → 1.35.5

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 CHANGED
@@ -1,5 +1,66 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.35.5 — 2026-06-27 — 17th 버그헌트: scan .json5/.jsonc FN + verify-claim git 매칭 정밀도
4
+
5
+ **비-graph 버그헌트(게시본 1.35.4 후, R-0011)**. Explore 에이전트가 낸 6개 후보를 **맹신 X 재현으로 검증** → 강한 후보 다수가 거짓 판명(멀티라인 시크릿·`test_`고엔트로피 FN은 실제 재현에서 이미 flagged; agent의 `api_secret`↔`secret` 매칭 가정도 word-boundary로 오류), **확정 2건만** 수정.
6
+
7
+ ### 수정 (재현 확인)
8
+ - **scan secrets — .json5/.jsonc 미스캔 FN (확인)**: `SCAN_TEXT_EXT` 가 `.json` 만 포함하고 `.json5`/`.jsonc` 를 제외 → 해당 설정 파일의 시크릿이 스캔에서 누락. 두 확장자 추가. 재현: JSON5 의 `api_key: "sk-…"` 시크릿이 이제 flagged. (참고: 따옴표 JSON 키 `"api_key":` 미매칭은 `.json` 포함 **기존 패턴 한계** — 본 변경 무관, 별도 영역.)
9
+ - **verify-claim — git 교차검사 basename 충돌 (정밀도)**: `_claimFileInGit` 의 역방향 매칭 `c.endsWith('/'+g)` 가 bare basename 도 매칭 → claimed `src/test.js` 가 무관한 git 변경 `test.js` 와 오매칭(외과적-변경 신호 약화). git 경로가 다중세그먼트일 때만 역매칭하도록 한정. forward/exact 정상. (정직: 완전한 false-pass 시나리오는 미재현 — 정밀도 개선으로 분류.)
10
+
11
+ ### 검증
12
+ - selftest **265** (신규: `_claimFileInGit` 단위 — 충돌 차단 + forward/exact/nested-reverse 보존, .json5/.jsonc 멤버십). full e2e (verify-claim git 로직 무회귀). patch — npm 미배포(R-0011).
13
+
14
+ ## 1.35.4 — 2026-06-27 — graph 폴리시: 엣지 종류별 색상 + PNG 내보내기
15
+
16
+ **graph 시각/공유(1.35.3 게시 후 누적, R-0011)**.
17
+
18
+ ### 변경 (lib/graph.js 템플릿)
19
+ - **엣지 종류별 색상**: milestone(amber)/ref(blue)/link(green)/feature(gray) 엣지를 색으로 구분 — 관계 종류를 한눈에. 선택 노드 연결 엣지는 기존 하이라이트 유지.
20
+ - **PNG 내보내기**: `p` 키 → 현재 그래프를 `leerness-graph.png`로 저장(배경색 합성 후 canvas toDataURL). 문서/PR 공유용. hint 바 안내 추가.
21
+
22
+ ### 검증
23
+ - selftest **264** (임베드-script JS 신택스 가드가 EKIND/exportPng/keydown 추가분 컴파일 유효성 자동 검증) · lib/graph.js 템플릿만 변경(데이터/엣지 로직 무변경). patch — npm 미배포(R-0011).
24
+
25
+ ## 1.35.3 — 2026-06-27 — graph 네비게이션: 검색 Enter 점프 + f/dblclick fit + Esc
26
+
27
+ **graph "손쉽게 조회" 강화(1.35.0 게시 후 누적, R-0011)**: 온톨로지 그래프에서 노드를 빠르게 찾아 조회하는 키보드/마우스 네비게이션.
28
+
29
+ ### 변경 (lib/graph.js 템플릿, 기존 select/goto/fitView/closePanel 재사용)
30
+ - **검색 + Enter**: search 박스 입력 후 Enter → 첫 매치 노드로 점프(center + 내용 패널 자동 오픈). `T-0042` 류 ID/라벨 즉시 조회.
31
+ - **f / 배경 더블클릭**: 전체 노드 화면 맞춤(re-fit) — 탐색 후 원위치.
32
+ - **Esc**: 내용 패널 닫기.
33
+ - hint 바에 단축키 안내 추가.
34
+
35
+ ### 검증
36
+ - selftest **264** (기존 임베드-script JS 신택스 가드가 신규 핸들러 컴파일 유효성 자동 검증) · lib/graph.js 템플릿만 변경(데이터/엣지 로직 무변경). patch — npm 미배포(R-0011).
37
+
38
+ ## 1.35.2 — 2026-06-27 — graph 시각/성능 다듬기: 정착-freeze + auto-fit + 템플릿 JS 신택스 가드
39
+
40
+ **graph 폴리시(1.35.0 게시 후 누적, R-0011)**: 온톨로지 그래프 뷰 성능·사용성·견고성 보강.
41
+
42
+ ### 변경 (lib/graph.js 템플릿)
43
+ - **성능 가드**: force 시뮬이 정착(alpha<0.006)하면 `tick()` 조기반환 → 대용량 하네스에서 매 프레임 O(n²) 반발 계산 정지(드래그/상호작용 시 alpha 재가열로 자동 재개). 정착 후 CPU ~0.
44
+ - **auto-fit 뷰**: 정착 시(alpha<0.08) 1회 `fitView()` — 전체 노드 경계 계산 → 화면에 맞게 zoom/center(수동 팬 없이 전부 보임). 사용자가 zoom/pan 하면 취소(뷰 가로채기 방지).
45
+ - **소스 위생**: dedup 구분자 리터럴 NUL → `\u0000` ASCII 이스케이프(런타임 동일, rg/에디터 binary 오인 해소).
46
+
47
+ ### 하드닝 (selftest)
48
+ - **임베드 script JS 신택스 가드**: 생성 HTML 인라인 스크립트를 `new Function()`으로 컴파일 검증 → U+2028/정규식 리터럴 류 템플릿 신택스 회귀를 selftest에서 **영구 차단**(지난 라운드 U+2028 사고 재발 방지). 데이터에 `</script>`·`${}` 포함시켜도 유효 JS 유지 확인.
49
+
50
+ ### 검증
51
+ - selftest **264** · lib/graph.js 템플릿만 변경(데이터/엣지 로직 무변경)이라 기존 graph selftest(데이터/XSS/dedup/빈하네스) 유지. patch — npm 미배포(R-0011).
52
+
53
+ ## 1.35.1 — 2026-06-27 — graph 후속(1.35.0 게시 후): README Visualize 문서 + 빈-하네스 방어가드
54
+
55
+ **1.35.0 게시본 클린룸 재실증 후 정합 패치(정직성)**: 게시본 1.35.0(selftest 262)에서 `graph --html`(7노드 생성) + `LEERNESS_AUTO_GRAPH=1` 자동생성이 정상 동작 확인됨. 게시 시점 직후 추가된 변경 2건(README 문서 · 방어가드)이 git 상 1.35.0과 버전이 겹쳐, **1.35.1로 정합 분리**(같은 버전에 두 내용 공존 방지). 런타임 동작 무변경.
56
+
57
+ ### 변경 (문서 + 테스트만 — 런타임 무변경)
58
+ - **README "Visualize" 항목**: `graph --html`(자기완결 온톨로지 그래프, 노드 클릭 조회) + 자동갱신(`LEERNESS_AUTO_GRAPH=1`)을 60초 투어에 문서화(관리영역 밖 손편집 → readme sync 무영향).
59
+ - **방어가드 selftest +1**: `graph --html` 빈/미초기화 하네스 무크래시 + 유효 빈 HTML(0노드·단일 script 닫힘) 회귀가드 — 신규 프로젝트 init 직후 시나리오, deps 미주입 worst-case 포함.
60
+
61
+ ### 검증
62
+ - selftest **263** · 동작 무변경(문서/테스트 only)이라 e2e 무영향(381 유지). patch — npm 미배포(R-0011, 다음 묶음 게시).
63
+
3
64
  ## 1.35.0 — 2026-06-27 — 🕸️ [안정화/Stable] 온톨로지 그래프(graph --html + 자동생성) 안정 minor
4
65
 
5
66
  **신규 기능 minor**: leerness 적용 프로젝트의 하네스(5 메모리 표면 + skills + feature-graph)를 인터랙티브 온톨로지 그래프 HTML(`leerness.html`)로 표면화하는 기능을 도입하고, 누적 수정(1.34.1~1.34.4)을 묶어 안정화. 0 런타임 의존 자기완결 vanilla JS — 노드 클릭으로 하네스 내용을 손쉽게 조회.
@@ -15,7 +76,7 @@
15
76
  - **배포 대기**: `npm publish` 는 2FA OTP 필요 → 사용자 게시 후 게시본 클린룸 재실증(graph --html 행위 포함, re-verify-published-artifact 교훈).
16
77
 
17
78
  ### 검증 (회귀 0)
18
- - selftest **262** · full **e2e 381/381**. `files:["lib"]` 신규 `lib/graph.js` 게시 포함 확인. bin+package.json 동시 bump 일치.
79
+ - selftest **262** · full **e2e 381/381**. **게시본 클린룸 재실증(1.35.0)**: 버전 1.35.0 · lib/graph.js 게시 포함 · selftest 262 · `graph --html` 7노드 생성 + `LEERNESS_AUTO_GRAPH=1` auto-gen 동작 OK. bin+package.json 동시 bump 일치.
19
80
 
20
81
  ## 1.34.4 — 2026-06-27 — 📊 graph 자동생성(opt-in): handoff 시 leerness.html 자동 갱신
21
82
 
package/README.md CHANGED
@@ -98,6 +98,7 @@ The asymmetry is what makes a trial reasonable anyway: MIT, **0 runtime dependen
98
98
  - **Verification** — `verify-claim` (evidence vs reality, stub/fake-test/inflated-count detection, `--run-tests --test-cmd` for any language; `--all` checks **every** completed claim at once for CI) · `contract verify` (spec ↔ impl) · `gate` (one-call CI gate).
99
99
  - **Audit** — `audit` · `lazy detect` · `drift check` keep the workspace honest over time.
100
100
  - **Security** — `scan secrets` (committed-secret detection) · `encoding check` (BOM/CP949) — also runs at `session close`.
101
+ - **Visualize** — `graph --html` writes a self-contained interactive ontology graph (`leerness.html`) of the whole harness (memory surfaces + skills + feature-graph) — click a node to read its content. Optional auto-refresh on `handoff` (`LEERNESS_AUTO_GRAPH=1`).
101
102
 
102
103
  Full command reference, workflows, and architecture: **[README.ko.md](./README.ko.md)** (Korean) · `leerness commands` · `leerness help`.
103
104
 
@@ -114,7 +115,7 @@ MIT
114
115
  <!-- leerness:project-readme:start -->
115
116
  ## Leerness Project Harness
116
117
 
117
- 이 프로젝트는 Leerness v1.35.0 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
118
+ 이 프로젝트는 Leerness v1.35.5 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
118
119
 
119
120
  ### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
120
121
 
@@ -168,7 +169,7 @@ leerness memory restore decision <date|title>
168
169
 
169
170
  ### MCP server (외부 AI 통합)
170
171
 
171
- Leerness v1.35.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
172
+ Leerness v1.35.5는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
172
173
 
173
174
  ```jsonc
174
175
  // 카테고리별
@@ -189,7 +190,7 @@ Leerness v1.35.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code
189
190
  `<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
190
191
  1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) stress-v* 신규 작성 + 누적 회귀 → 4) e2e 219/219 → 5) npm pack + git tag + GitHub release → 6) main 자동 push (1.9.140+) → 7) session close → 8) 다음 라운드 예약.
191
192
 
192
- 현재 누적: **70 라운드 (1.9.40 → 1.35.0)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
193
+ 현재 누적: **70 라운드 (1.9.40 → 1.35.5)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
193
194
 
194
195
  ### 성능 가이드 (1.9.140 측정)
195
196
 
@@ -227,6 +228,6 @@ leerness release pack --close --auto-main-push
227
228
  - `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
228
229
  - `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
229
230
 
230
- Last synced by Leerness v1.35.0: 2026-06-27
231
+ Last synced by Leerness v1.35.5: 2026-06-27
231
232
  <!-- leerness:project-readme:end -->
232
233
 
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.0';
35
+ const VERSION = '1.35.5';
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') 시 호스트 프로세스 오염.
@@ -2873,6 +2873,7 @@ function _selfTestCases() {
2873
2873
  { name: 'verify-claim git diff 시맨틱 교차검증: _gitChangedFiles/_claimFileInGit + strict FAIL 통합 (UR-0042 외부리뷰 1.9.302)', run: () => { const fnOk = typeof _gitChangedFiles === 'function' && typeof _claimFileInGit === 'function'; const matchOk = _claimFileInGit('src/api.js', new Set(['src/api.js'])) === true && _claimFileInGit('./src/api.js', new Set(['src/api.js'])) === true && _claimFileInGit('other.js', new Set(['src/api.js'])) === false && _claimFileInGit('x', null) === null; const src = read(__filename); const wired = /git diff 교차검증/.test(src) && /\|\| !gitClaimOk/.test(src) && /_gitChangedFiles\(root\)/.test(src); return fnOk && matchOk && wired; } },
2874
2874
  { name: '_withLock/_updateRun: lost-update 락(O_EXCL+재진입) + 적용 (UR-0043 외부리뷰 1.9.303)', run: () => { const src = read(__filename); const fnOk = typeof _withLock === 'function' && typeof _sleepSyncMs === 'function' && typeof _updateRun === 'function'; const reentrant = /if \(_heldLocks\.has\(lockPath\)\) return fn\(\)/.test(src); const excl = /fs\.openSync\(lockPath, 'wx'\)/.test(src); const applied = /const id = _withLock\(progressPath\(root\)/.test(src) && /_updateRun\(root, curId/.test(src); return fnOk && reentrant && excl && applied; } },
2875
2875
  { name: 'lib/analyzers: 분석/검증 함수 4종 모듈 단일출처 분리 (UR-0025 1.9.304)', run: () => { const m = require('../lib/analyzers'); return m._evidenceQuality === _evidenceQuality && m._shellGuardAnalyze === _shellGuardAnalyze && m._parseEvidenceStats === _parseEvidenceStats && m._claimFileInGit === _claimFileInGit && !/function _evidenceQuality\(evidence\) \{/.test(read(__filename)) && !/function _shellGuardAnalyze\(cmd, ctx\) \{/.test(read(__filename)); } },
2876
+ { name: '17th헌트: _claimFileInGit bare-basename 충돌 차단 + scan .json5/.jsonc 포함 (1.35.5)', run: () => { const a = require('../lib/analyzers'); const collisionFixed = a._claimFileInGit('src/test.js', new Set(['test.js'])) !== true; const forwardOk = a._claimFileInGit('test.js', new Set(['src/test.js'])) === true; const exactOk = a._claimFileInGit('src/a.js', new Set(['src/a.js'])) === true; const nestedReverseOk = a._claimFileInGit('x/src/a.js', new Set(['src/a.js'])) === true; const src = read(__filename); const json5Ext = src.includes("'.js" + "on5'") && src.includes("'.js" + "onc'"); return collisionFixed && forwardOk && exactOk && nestedReverseOk && json5Ext; } },
2876
2877
  { name: 'honesty-check: AI 인식론적 정직성 3차원 + MCP/CLI/strict 통합 (사용자명시 1.9.305)', run: () => { const h = _epistemicHonestyCheck; const d1 = h('이 기능은 항상 정상 동작합니다').findings.some(f => f.dim === 'pretend-knowledge'); const d2 = h('아마 될 것 같습니다. 구현 완료했습니다').findings.some(f => f.dim === 'premature-judgment'); const d3 = h('이 API 의 rate limit 은 초당 5회입니다').findings.some(f => f.dim === 'no-info-gathering'); const clean = h('src/api.js 수정, 12/12 통과 (Exit: 0)').ok === true; const src = read(__filename); const wired = require('../lib/mcp-tools').some(t => t.name === 'leerness_honesty_check') && /if \(cmd === 'honesty-check'\)/.test(src) && /honestyFindings = _epistemicHonestyCheck/.test(src); return d1 && d2 && d3 && clean && wired; } },
2877
2878
  { name: 'exit code 일관성: fail()→exitCode 1 행위 + unknown 명령 안내 (UR-0045 / CV-5 행위화 1.9.366)', run: () => { if (typeof fail !== 'function') return false; const saved = process.exitCode; const _w = process.stdout.write; let setOk = false; try { process.stdout.write = () => true; process.exitCode = 0; fail('selftest probe'); setOk = process.exitCode === 1; } finally { process.stdout.write = _w; process.exitCode = saved; } const src = read(__filename); const dispatchOk = /알 수 없는 명령: \$\{cmd\}/.test(src); return setOk && dispatchOk; } },
2878
2879
  { name: 'brief: 프로젝트 청사진 set/show/export + README 개요 섹션 (UR-0055 사용자명시 1.9.307)', run: () => { const src = read(__filename); const fnOk = typeof briefCmd === 'function' && typeof _loadBrief === 'function' && typeof _briefBlueprint === 'function' && _BRIEF_FIELDS.length === 10; const b = { project: 'X', intro: 'i', purpose: 'p', problem: '', features: ['f1', 'f2'], stack: ['s'], architecture: '', users: [], success: [], nonGoals: [], currentState: '' }; const bp = _briefBlueprint(b, VERSION); const bpOk = /Blueprint/.test(bp) && /소개 \(Intro\)/.test(bp) && /f1/.test(bp) && /신규 프로젝트 시작 가이드/.test(bp); const rb = _briefReadmeBlock(b); const rbOk = rb.includes(BRIEF_START) && rb.includes(BRIEF_END) && /프로젝트 개요/.test(rb) && /\*\*목적\*\*/.test(rb); return fnOk && bpOk && rbOk && /if \(cmd === 'brief'\)/.test(src); } },
@@ -2978,6 +2979,8 @@ function _selfTestCases() {
2978
2979
  { 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
2980
  { 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
2981
  { 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; } },
2982
+ { name: 'graph --html: 빈/미초기화 하네스 무크래시 + 유효 빈 HTML (1.35.0 방어가드)', run: () => { const m = require('../lib/graph'); let ok = false; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_emptyg_')); const _w = process.stdout.write; let so = ''; try { process.stdout.write = s => { so += s; return true; }; fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true }); const data = m.buildGraphData(tmp, {}); const out = path.join(tmp, 'leerness.html'); const r = m.graphHtmlCmd(tmp, { quiet: true }, out); const html = fs.readFileSync(out, 'utf8'); const closers = html.split('</' + 'script>').length - 1; const dm = html.match(/var DATA = (\{[\s\S]*?\});/); let parsed = null; try { parsed = JSON.parse(dm[1]); } catch (e) {} ok = !!data && Array.isArray(data.nodes) && data.nodes.length === 0 && !!r && r.ok === true && r.nodes === 0 && fs.existsSync(out) && closers === 1 && !!parsed && parsed.nodes.length === 0 && so === ''; } catch (e) { ok = false; } finally { process.stdout.write = _w; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } return ok; } },
2983
+ { name: 'graph --html: 임베드 script JS 신택스 유효성(U+2028/정규식 리터럴 회귀 영구 차단, 1.35.2)', run: () => { const m = require('../lib/graph'); let ok = false; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_gjs_')); 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 </scr' + 'ipt> & <b> ' + String.fromCharCode(0x24) + '{y} | M-0002 | - | 2026-06-26 |\n'); const out = path.join(tmp, 'leerness.html'); m.graphHtmlCmd(tmp, { _roadmapData, _loadDecisions, _loadLessons, quiet: true }, out); const html = fs.readFileSync(out, 'utf8'); const o = '<scr' + 'ipt>', c = '</scr' + 'ipt>'; const js = html.slice(html.indexOf(o) + o.length, html.lastIndexOf(c)); let synOk = false; try { new Function(js); synOk = true; } catch (e) { synOk = false; } ok = js.length > 200 && synOk; } catch (e) { ok = false; } finally { process.stdout.write = _w; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } return ok; } },
2981
2984
  { 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; } },
2982
2985
  { 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 를 더 밀어냄)
2983
2986
  { 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; } },
@@ -8118,7 +8121,7 @@ function isSkippedRel(rel, extras = []) {
8118
8121
  if (segs.some(s => SCAN_SKIP_DIRS.has(s))) return true; // SCAN_SKIP_DIRS 는 Set
8119
8122
  return extras.some(d => rel === d || rel.startsWith(d + '/'));
8120
8123
  }
8121
- const SCAN_TEXT_EXT = new Set(['.js','.ts','.jsx','.tsx','.mjs','.cjs','.json','.md','.txt','.env','.bash','.sh','.yml','.yaml','.toml','.ini','.cfg','.py','.rb','.go','.rs','.java','.kt','.swift','.cs','.php','.sql','.html','.css','.scss','.less','.xml','.bat','.ps1','']);
8124
+ const SCAN_TEXT_EXT = new Set(['.js','.ts','.jsx','.tsx','.mjs','.cjs','.json','.json5','.jsonc','.md','.txt','.env','.bash','.sh','.yml','.yaml','.toml','.ini','.cfg','.py','.rb','.go','.rs','.java','.kt','.swift','.cs','.php','.sql','.html','.css','.scss','.less','.xml','.bat','.ps1','']);
8122
8125
  function* walk(root, base = root, depth = 0, extras = null) {
8123
8126
  if (depth > 12) return;
8124
8127
  if (extras === null) extras = getExtraSkipDirs(root);
package/lib/analyzers.js CHANGED
@@ -48,7 +48,7 @@ function _evidenceQuality(evidence) {
48
48
  function _claimFileInGit(claimed, gitSet) {
49
49
  if (!gitSet) return null;
50
50
  const c = String(claimed).replace(/\\/g, '/').replace(/^\.\//, '');
51
- for (const g of gitSet) { if (g === c || g.endsWith('/' + c) || c.endsWith('/' + g)) return true; }
51
+ for (const g of gitSet) { if (g === c || g.endsWith('/' + c) || (g.indexOf('/') >= 0 && c.endsWith('/' + g))) return true; } // 1.35.5: reverse match 는 git 경로가 다중세그먼트일 때만 — bare basename 충돌(src/test.js ↔ test.js) 차단
52
52
  return false;
53
53
  }
54
54
  function _parseEvidenceStats(text) {
package/lib/graph.js CHANGED
@@ -49,11 +49,12 @@ canvas{position:fixed;inset:0;top:46px}
49
49
  <canvas id="c"></canvas>
50
50
  <div id="panel"><span class="x" onclick="closePanel()">✕</span><div id="pbody"></div></div>
51
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>
52
+ <div id="hint">drag node · scroll zoom · drag bg pan · click node → details · search+Enter jump · f / dblclick fit · p export PNG · Esc close</div>
53
53
  <script>
54
54
  var DATA = /*__DATA__*/null;
55
55
  var COLORS={task:'#58a6ff',plan:'#d29922',decision:'#39d0d8',lesson:'#e3b341',rule:'#bc8cff',skill:'#2dd4bf',feature:'#6e7681'};
56
56
  var STATUSCOL={done:'#3fb950',verified:'#3fb950','in-progress':'#58a6ff',in_progress:'#58a6ff',blocked:'#f85149',waiting:'#d29922',planned:'#8b949e',requested:'#8b949e'};
57
+ var EKIND={milestone:'rgba(210,153,34,.22)',ref:'rgba(88,166,255,.20)',link:'rgba(57,211,83,.20)',feature:'rgba(110,118,129,.26)'};
57
58
  function nodeColor(n){ if(n.type==='task'&&STATUSCOL[n.status])return STATUSCOL[n.status]; return COLORS[n.type]||'#8b949e'; }
58
59
  function esc(s){return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
59
60
 
@@ -78,10 +79,12 @@ types.forEach(function(t){var c=DATA.counts&&DATA.counts[t]; var el=document.cre
78
79
  var view={x:0,y:0,k:1};
79
80
  var sel=null,hover=null,nbr={};
80
81
  var cam={cx:W/2,cy:H/2};
82
+ var _fit=false;
81
83
 
82
84
  // physics
83
85
  var alpha=1;
84
86
  function tick(){
87
+ if(alpha<0.006) return;
85
88
  if(alpha>0.005) alpha*=0.992;
86
89
  var REP=2600,SPR=0.012,LEN=70,CEN=0.012;
87
90
  for(var i=0;i<nodes.length;i++){var a=nodes[i]; if(off[a.type])continue;
@@ -94,12 +97,13 @@ function tick(){
94
97
  }
95
98
  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
99
  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};}
100
+ function fitView(){var minx=1e9,miny=1e9,maxx=-1e9,maxy=-1e9,c=0; nodes.forEach(function(n){if(off[n.type])return;c++;if(n.x<minx)minx=n.x;if(n.x>maxx)maxx=n.x;if(n.y<miny)miny=n.y;if(n.y>maxy)maxy=n.y;}); if(c<1)return; var gw=Math.max(1,maxx-minx),gh=Math.max(1,maxy-miny); view.k=Math.min(2.2,Math.max(0.2,0.82*Math.min(W/gw,H/gh))); cam.cx=(minx+maxx)/2; cam.cy=(miny+maxy)/2; view.x=0;view.y=0;}
97
101
 
98
102
  function draw(){
99
103
  ctx.clearRect(0,0,W,H);
100
104
  // edges
101
105
  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();});
106
+ 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)':(EKIND[e.kind]||'rgba(120,130,145,.16)'); ctx.beginPath();ctx.moveTo(p.x,p.y);ctx.lineTo(q.x,q.y);ctx.stroke();});
103
107
  // nodes
104
108
  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
109
  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();
@@ -108,18 +112,18 @@ function draw(){
108
112
  ctx.globalAlpha=1;
109
113
  });
110
114
  }
111
- function loop(){tick();draw();requestAnimationFrame(loop);} loop();
115
+ function loop(){tick(); if(!_fit&&nodes.length&&alpha<0.08){_fit=true;fitView();} draw();requestAnimationFrame(loop);} loop();
112
116
 
113
117
  // interaction
114
118
  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};}});
119
+ cv.addEventListener('mousedown',function(ev){var m=hit(ev.offsetX,ev.offsetY);moved=false; if(m){drag=m;m.fixed=true;}else{_fit=true;panning={x:ev.offsetX,y:ev.offsetY,vx:view.x,vy:view.y};}});
116
120
  window.addEventListener('mousemove',function(ev){var r=cv.getBoundingClientRect();var mx=ev.clientX-r.left,my=ev.clientY-r.top;
117
121
  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
122
  else if(panning){view.x=panning.vx+(mx-panning.x);view.y=panning.vy+(my-panning.y);moved=true;}
119
123
  else{hover=hit(mx,my);cv.style.cursor=hover?'pointer':'default';}
120
124
  });
121
125
  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});
126
+ 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)); _fit=true; view.k=nk;},{passive:false});
123
127
  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
128
 
125
129
  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);}
@@ -136,6 +140,10 @@ function showPanel(n){
136
140
  }
137
141
  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
142
  document.getElementById('search').addEventListener('input',function(ev){window._q=ev.target.value.trim().toLowerCase()||null;});
143
+ document.getElementById('search').addEventListener('keydown',function(ev){ if(ev.key!=='Enter'||!window._q)return; var h=null; for(var i=0;i<nodes.length;i++){var n=nodes[i]; if(off[n.type])continue; if((n.label||'').toLowerCase().indexOf(window._q)>=0||n.id.toLowerCase().indexOf(window._q)>=0){h=n;break;}} if(h){_fit=true;goto(h.id);} });
144
+ function exportPng(){ try{ var t=document.createElement('canvas'); t.width=cv.width; t.height=cv.height; var tx=t.getContext('2d'); tx.fillStyle='#0a0d12'; tx.fillRect(0,0,t.width,t.height); tx.drawImage(cv,0,0); var a=document.createElement('a'); a.download='leerness-graph.png'; a.href=t.toDataURL('image/png'); a.click(); }catch(e){} }
145
+ window.addEventListener('keydown',function(ev){ if(ev.target&&ev.target.tagName==='INPUT')return; if(ev.key==='f'||ev.key==='F'){_fit=true;fitView();} else if(ev.key==='p'||ev.key==='P'){exportPng();} else if(ev.key==='Escape'){closePanel();} });
146
+ cv.addEventListener('dblclick',function(ev){ if(!hit(ev.offsetX,ev.offsetY)){_fit=true;fitView();} });
139
147
  </script></body></html>`;
140
148
 
141
149
  const _txt = v => (v == null ? '' : String(v));
@@ -170,7 +178,7 @@ function buildGraphData(root, deps = {}) {
170
178
  // edges — 같은 (source,target) 쌍 dedup: task→milestone 가 _ms 추출 + blob M-#### 정규식에서 이중 추가되는 것 방지(엣지수/degree 정확).
171
179
  const edges = [];
172
180
  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 }); }
181
+ function linkIds(a, b, kind) { if (!(a && b && byId.has(a) && byId.has(b) && a !== b)) return; const k = a + '\u0000' + b; if (_seenEdge.has(k)) return; _seenEdge.add(k); edges.push({ source: a, target: b, kind }); }
174
182
  for (const n of nodes) {
175
183
  if (n._ms) for (const mid of n._ms) linkIds(n.id, mid, 'milestone');
176
184
  const blob = Object.values(n.detail || {}).join(' ');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.35.0",
3
+ "version": "1.35.5",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",