leerness 1.34.0 → 1.35.3
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 +122 -0
- package/README.md +5 -4
- package/bin/leerness.js +36 -6
- package/lib/graph.js +224 -0
- package/lib/pure-utils.js +1426 -1422
- package/package.json +1 -1
- package/scripts/e2e.js +34 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,127 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.35.3 — 2026-06-27 — graph 네비게이션: 검색 Enter 점프 + f/dblclick fit + Esc
|
|
4
|
+
|
|
5
|
+
**graph "손쉽게 조회" 강화(1.35.0 게시 후 누적, R-0011)**: 온톨로지 그래프에서 노드를 빠르게 찾아 조회하는 키보드/마우스 네비게이션.
|
|
6
|
+
|
|
7
|
+
### 변경 (lib/graph.js 템플릿, 기존 select/goto/fitView/closePanel 재사용)
|
|
8
|
+
- **검색 + Enter**: search 박스 입력 후 Enter → 첫 매치 노드로 점프(center + 내용 패널 자동 오픈). `T-0042` 류 ID/라벨 즉시 조회.
|
|
9
|
+
- **f / 배경 더블클릭**: 전체 노드 화면 맞춤(re-fit) — 탐색 후 원위치.
|
|
10
|
+
- **Esc**: 내용 패널 닫기.
|
|
11
|
+
- hint 바에 단축키 안내 추가.
|
|
12
|
+
|
|
13
|
+
### 검증
|
|
14
|
+
- selftest **264** (기존 임베드-script JS 신택스 가드가 신규 핸들러 컴파일 유효성 자동 검증) · lib/graph.js 템플릿만 변경(데이터/엣지 로직 무변경). patch — npm 미배포(R-0011).
|
|
15
|
+
|
|
16
|
+
## 1.35.2 — 2026-06-27 — graph 시각/성능 다듬기: 정착-freeze + auto-fit + 템플릿 JS 신택스 가드
|
|
17
|
+
|
|
18
|
+
**graph 폴리시(1.35.0 게시 후 누적, R-0011)**: 온톨로지 그래프 뷰 성능·사용성·견고성 보강.
|
|
19
|
+
|
|
20
|
+
### 변경 (lib/graph.js 템플릿)
|
|
21
|
+
- **성능 가드**: force 시뮬이 정착(alpha<0.006)하면 `tick()` 조기반환 → 대용량 하네스에서 매 프레임 O(n²) 반발 계산 정지(드래그/상호작용 시 alpha 재가열로 자동 재개). 정착 후 CPU ~0.
|
|
22
|
+
- **auto-fit 뷰**: 정착 시(alpha<0.08) 1회 `fitView()` — 전체 노드 경계 계산 → 화면에 맞게 zoom/center(수동 팬 없이 전부 보임). 사용자가 zoom/pan 하면 취소(뷰 가로채기 방지).
|
|
23
|
+
- **소스 위생**: dedup 구분자 리터럴 NUL → `\u0000` ASCII 이스케이프(런타임 동일, rg/에디터 binary 오인 해소).
|
|
24
|
+
|
|
25
|
+
### 하드닝 (selftest)
|
|
26
|
+
- **임베드 script JS 신택스 가드**: 생성 HTML 인라인 스크립트를 `new Function()`으로 컴파일 검증 → U+2028/정규식 리터럴 류 템플릿 신택스 회귀를 selftest에서 **영구 차단**(지난 라운드 U+2028 사고 재발 방지). 데이터에 `</script>`·`${}` 포함시켜도 유효 JS 유지 확인.
|
|
27
|
+
|
|
28
|
+
### 검증
|
|
29
|
+
- selftest **264** · lib/graph.js 템플릿만 변경(데이터/엣지 로직 무변경)이라 기존 graph selftest(데이터/XSS/dedup/빈하네스) 유지. patch — npm 미배포(R-0011).
|
|
30
|
+
|
|
31
|
+
## 1.35.1 — 2026-06-27 — graph 후속(1.35.0 게시 후): README Visualize 문서 + 빈-하네스 방어가드
|
|
32
|
+
|
|
33
|
+
**1.35.0 게시본 클린룸 재실증 후 정합 패치(정직성)**: 게시본 1.35.0(selftest 262)에서 `graph --html`(7노드 생성) + `LEERNESS_AUTO_GRAPH=1` 자동생성이 정상 동작 확인됨. 게시 시점 직후 추가된 변경 2건(README 문서 · 방어가드)이 git 상 1.35.0과 버전이 겹쳐, **1.35.1로 정합 분리**(같은 버전에 두 내용 공존 방지). 런타임 동작 무변경.
|
|
34
|
+
|
|
35
|
+
### 변경 (문서 + 테스트만 — 런타임 무변경)
|
|
36
|
+
- **README "Visualize" 항목**: `graph --html`(자기완결 온톨로지 그래프, 노드 클릭 조회) + 자동갱신(`LEERNESS_AUTO_GRAPH=1`)을 60초 투어에 문서화(관리영역 밖 손편집 → readme sync 무영향).
|
|
37
|
+
- **방어가드 selftest +1**: `graph --html` 빈/미초기화 하네스 무크래시 + 유효 빈 HTML(0노드·단일 script 닫힘) 회귀가드 — 신규 프로젝트 init 직후 시나리오, deps 미주입 worst-case 포함.
|
|
38
|
+
|
|
39
|
+
### 검증
|
|
40
|
+
- selftest **263** · 동작 무변경(문서/테스트 only)이라 e2e 무영향(381 유지). patch — npm 미배포(R-0011, 다음 묶음 게시).
|
|
41
|
+
|
|
42
|
+
## 1.35.0 — 2026-06-27 — 🕸️ [안정화/Stable] 온톨로지 그래프(graph --html + 자동생성) 안정 minor
|
|
43
|
+
|
|
44
|
+
**신규 기능 minor**: leerness 적용 프로젝트의 하네스(5 메모리 표면 + skills + feature-graph)를 인터랙티브 온톨로지 그래프 HTML(`leerness.html`)로 표면화하는 기능을 도입하고, 누적 수정(1.34.1~1.34.4)을 묶어 안정화. 0 런타임 의존 자기완결 vanilla JS — 노드 클릭으로 하네스 내용을 손쉽게 조회.
|
|
45
|
+
|
|
46
|
+
### 이번 minor 통합 (1.34.1~1.34.4)
|
|
47
|
+
- **🕸️ leerness graph --html (1.34.3)**: 프로젝트 루트에 `leerness.html`(Obsidian graph-view 스타일 force-directed 캔버스) 생성. 노드=task(status색)/plan/decision/lesson/rule/skill/feature, 엣지=`M-####`·`[TURDL]-####`·`[[wikilink]]`·feature-graph. 기존 `graph`(mermaid 코드그래프)는 비파괴 유지(`--html` 분기). 새 모듈 `lib/graph.js`(DI). 맹신X: 엣지 이중계수 dedup + (자가발견)U+2028-in-regex SyntaxError 회귀를 중첩 spawn 재현으로 반증·revert.
|
|
48
|
+
- **📊 graph 자동생성 (1.34.4)**: `LEERNESS_AUTO_GRAPH=1`(기본 OFF, Always-Off Opt-In) 시 `handoff` 마다 `leerness.html` 자동 갱신. dispatch 래퍼 격리 + `quiet` 모드. 맹신X: `handoff --json` notice 오염 차단(`!has('--json')`).
|
|
49
|
+
- **🔬 gate --claims 정직화 (1.34.1)**: gate 정밀성 문구 정직화 + 판별 케이스 e2e 가드.
|
|
50
|
+
- **🐶 scan secrets FP 억제 (1.34.2)**: dogfood(leerness Gate) 발견 — 'test' 픽스처 토큰 FP 억제(FN-safe, 실키 회귀 0).
|
|
51
|
+
|
|
52
|
+
### 안정화 표시 (R-0006)
|
|
53
|
+
- annotated tag `v1.35.0` (Stable) · GitHub release · npm dist-tag `stable` — 게시 단계에서 수행.
|
|
54
|
+
- **배포 대기**: `npm publish` 는 2FA OTP 필요 → 사용자 게시 후 게시본 클린룸 재실증(graph --html 행위 포함, re-verify-published-artifact 교훈).
|
|
55
|
+
|
|
56
|
+
### 검증 (회귀 0)
|
|
57
|
+
- 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 일치.
|
|
58
|
+
|
|
59
|
+
## 1.34.4 — 2026-06-27 — 📊 graph 자동생성(opt-in): handoff 시 leerness.html 자동 갱신
|
|
60
|
+
|
|
61
|
+
**T-0077 후속(사용자 비전 "자동으로 작성되게" 충족)**: `leerness graph --html`(1.34.3)은 수동이었음. 이제 `leerness handoff` 시 leerness.html 을 자동 재생성하는 opt-in 훅 추가 — 매 세션 시작마다 온톨로지 그래프가 최신 상태로 유지.
|
|
62
|
+
|
|
63
|
+
### 기능 (Always-Off Opt-In)
|
|
64
|
+
- 환경변수 `LEERNESS_AUTO_GRAPH=1` 일 때만 동작(기본 OFF — 1.9.213 "Always-Off Opt-In" 원칙). handoff 출력 끝에 `_maybeAutoGraph(root)` 가 leerness.html 을 조용히(quiet) 재생성하고 한 줄 알림.
|
|
65
|
+
- 비치명: dispatch 래퍼에서 `handoffCmd()` 반환 후 호출 → `.harness` 미존재/오류 시 try/catch 로 handoff 본문 무영향. `--quiet`/`--compact` 시 알림 생략.
|
|
66
|
+
- `lib/graph.js`: `graphHtmlCmd` 에 `quiet` 옵션 추가(자동생성 시 사람용 3줄 로그 억제). 거대 handoff 본문 미수정(격리).
|
|
67
|
+
|
|
68
|
+
### 검증 (회귀 0)
|
|
69
|
+
- selftest **262** (신규: handoff opt-in 배선 grep[split-literal 자기참조 회피] + quiet 모드 무로그 행위).
|
|
70
|
+
- **end-to-end**: init 한 임시 하네스에서 `handoff` — env 미설정 → leerness.html 미생성(기본 off 증명), `LEERNESS_AUTO_GRAPH=1` → 생성 + 알림 라인 확인.
|
|
71
|
+
- full **e2e 381/381**(711s, 기본 off → 기본경로 무영향) · bin+package.json 동시 bump. patch(1.34.4) — npm 미배포(R-0011).
|
|
72
|
+
|
|
73
|
+
## 1.34.3 — 2026-06-26 — 🕸️ leerness graph --html: 하네스 온톨로지 그래프(leerness.html) 자동 생성
|
|
74
|
+
|
|
75
|
+
**사용자 요청 신규 기능(T-0077)**: leerness 적용 프로젝트 루트에 인터랙티브 온톨로지 그래프 HTML(`leerness.html`)을 생성. Obsidian graph-view 스타일 force-directed 캔버스로 5 메모리 표면(task/plan/decision/lesson/rule) + skills + feature-graph 를 노드/엣지로 시각화하고, 노드 클릭 → 내용 패널로 하네스를 손쉽게 조회. **0 런타임 의존 자기완결 vanilla JS**(차트 라이브러리 X). codex 와 설계 협업(통합 지점·데이터 재사용·DI 배치), 검수는 메인.
|
|
76
|
+
|
|
77
|
+
### 기능
|
|
78
|
+
- `leerness graph [path] --html [--out <file>] [--json]` — 기존 `graph`(mermaid 코드 의존성 그래프)는 기본 동작 유지, `--html` 분기로 온톨로지 HTML 생성(비파괴).
|
|
79
|
+
- 데이터: in-process 로더 주입(`_roadmapData`·`_loadDecisions`·`_loadLessons`) — 자식 프로세스 셸링 0. 노드: task(status별 색상)/plan/decision/lesson/rule/skill/feature. 엣지: `M-####`·`[TURDL]-####` 참조 · `[[wikilink]]` · `feature-graph.md`.
|
|
80
|
+
- 새 모듈 `lib/graph.js`(DI 패턴 — lib/diagnostics 류와 일관). bin/leerness.js 는 thin wrapper + dispatch 분기 + help 만.
|
|
81
|
+
- 임베드 안전: 모든 `<` → `<` 치환으로 `</script>`·`<!--` 차단(JSON 문자열 내부라 런타임 복원), function 치환기로 `$`-특수문자 회피.
|
|
82
|
+
|
|
83
|
+
### 맹신X (자기검수에서 실버그 2건 발견·해결)
|
|
84
|
+
- **P2 엣지 이중계수**: task→milestone 가 `_roadmapData` 추출(`_ms`) + blob `M-####` 정규식 양쪽에서 추가 → 엣지수/degree 부풀림. `(source,target)` dedup 으로 정확화(루트 하네스 20→10 정확).
|
|
85
|
+
- **자가발견 회귀 + 반증**: U+2028/U+2029 escape 를 정규식 리터럴 안에 raw 로 넣어 `SyntaxError`(정규식은 줄종결자 불가) → bin 이 require 하는 공유 모듈이라 e2e 자식 전부 크래시(균일 75 실패). "다수 균일 실패=환경" 유혹을 **중첩 spawn 으로 `--version` 재현**해 반증(내 신택스 에러) → 이익 미미한 escape 는 단순 검증형으로 revert.
|
|
86
|
+
|
|
87
|
+
### 검증 (회귀 0)
|
|
88
|
+
- selftest **261** (신규: `graph --html` 행위 — 임시 하네스 seed → `leerness.html` 생성 + 노드/엣지 존재 + XSS 무결성(`</script>` 정확히 1개); 자기참조 트랩 회피 split-literal).
|
|
89
|
+
- full **e2e 381/381**(750s) · 라이브: 루트 119 nodes/10 edges, `DATA` JSON.parse OK.
|
|
90
|
+
- patch(1.34.3) — npm 미배포(R-0011). bin+package.json 동시 bump 일치.
|
|
91
|
+
|
|
92
|
+
## 1.34.2 — 2026-06-20 — 🐶 dogfood 발견: scan secrets 테스트-픽스처 FP 억제 ('test' 토큰)
|
|
93
|
+
|
|
94
|
+
**🐶 product(leerness Gate, GitHub App)를 leerness 하니스로 dogfooding 하다 발견한 leerness 개선.** 실제 product 코드를 leerness 로 빌드·검증하니 `scan secrets` 가 테스트 픽스처(`const SECRET = 'test-webhook-secret-123'`)를 "Hardcoded password assignment" FP 로 플래그함. (웹 Opus 4.8 리뷰의 "self-dev 밖 실제 product 에서 검증" 가치가 첫 leerness 개선으로 결실.)
|
|
95
|
+
|
|
96
|
+
### 맹신X (양방향 재현 — FP 억제 + 실키 FN-guard 0)
|
|
97
|
+
- 재현: `_isPlaceholderSecret('test-webhook-secret-123')` → false(플래그). 전체-값 placeholder 토큰도, marker 단어도 아니고(markers 에 bare 'test'/'secret' 없음), 저엔트로피(alnum 20 < 24)라 실키 가드도 미적용 → 최종 `return false`(플래그)에 도달.
|
|
98
|
+
- **FN-guard 재현**: 실키(`AKIAJQXMP7RZ2KL9WXYZ` · `ghp_…` · 고엔트로피 랜덤 · `prod-database-secret-…` · `latestKEY…`)는 전부 FLAGGED 유지 확인. 수정 규칙은 함수 끝(실키 prefix·고엔트로피가 이미 return 한 뒤)에만 위치해 실키엔 **도달 불가** → 보안 회귀 0.
|
|
99
|
+
|
|
100
|
+
### 변경 (FN-safe, 최소)
|
|
101
|
+
- `_isPlaceholderSecret`: 'test' 구분자 토큰(`test-…`/`…-test`/`…-tests`/`TEST_…`)을 가진 **저엔트로피 비-실키** 값을 placeholder 로 판정 — 함수 끝(고엔트로피·실키 prefix 분기 뒤)에 추가해 실키 무영향. `test-webhook-secret-123`/`webhook-secret-for-tests` 류 FP 억제.
|
|
102
|
+
- **whack-a-mole 회피(lesson)**: placeholder 휴리스틱 무한 확장은 FN 위험만 키움 — leerness 는 가장 명확한 'test' 픽스처 클래스만 잡고, 그 외(PEM 델리미터-in-코드·일반 fixture 값)는 product-side 에서 정직하게 해결(게이밍 X). 완전 해소는 allowlist 기제(향후)로.
|
|
103
|
+
|
|
104
|
+
### 검증 (회귀 0)
|
|
105
|
+
- selftest **260** (신규: _isPlaceholderSecret FP 억제 5종 + 실키 FN-guard 5종 + 소스 가드 '규칙이 고엔트로피 분기 뒤').
|
|
106
|
+
- dogfood 검증: leerness-gate `scan secrets` → ✓ clean(FP 0), product 37 tests 유지.
|
|
107
|
+
- patch(1.34.2) — npm 미배포(R-0011). bin+package.json 동시 bump 일치.
|
|
108
|
+
|
|
109
|
+
## 1.34.1 — 2026-06-19 — 🔬 16th 리뷰(게시본 1.34.0 신규 표면) — gate --claims 정밀성 정직화 + 판별 가드
|
|
110
|
+
|
|
111
|
+
**🔬 게시본 1.34.0 신규 표면(gate --claims · verify-claim --all · MCP)을 적대적 리뷰 + 맹신X 양방향 재현.** R-0006(멀티에이전트 보수)에 따라 무거운 fan-out 대신 단일 컨텍스트 적대 버그헌트로 수행. 신규 표면은 행위상 견고했으나(스텁/부풀린카운트 차단·json 무오염·MCP 라운드트립 정상), **`gate --claims` 의 가치 설명 문구가 부정확**함을 발견.
|
|
112
|
+
|
|
113
|
+
### 맹신X (양방향 재현 — 정밀성 가치는 REAL, 문구만 과장)
|
|
114
|
+
- **재현으로 기각한 과장**: 종전 코드주석이 "기본 게이트는 lazy/audit 으로 거짓완료를 잡지만 부풀린카운트·스텁은 verify-claim 이 더 잘 잡음"이라 표현 → lazy/audit 이 **콘텐츠를 검사하는 듯 오해**를 줌. 실제 재현: 기본 게이트가 거짓완료를 잡은 건 `lazy detect` 의 **워크스페이스-상태 신호**(handoff 미생성·test-run 미기록 — done-work 있으면 blocking)였지, 부풀린카운트/스텁 **콘텐츠 탐지가 아니었음**.
|
|
115
|
+
- **재현으로 확인한 진짜 가치(REAL)**: `lazy detect` 를 완전히 깨끗하게(유효 handoff + test-run 기록) 만든 성숙 프로젝트 + 콘텐츠-레벨 거짓(테스트 50개 주장, 실제 1개) → **기본 게이트 5체크 전부 통과(exit 0)**, **`--claims` 만 차단(exit 1, verify-claims FAIL)**. 즉 `--claims` 는 5체크가 안 보는 **콘텐츠-레벨 차원**(파일/카운트/스텁/optimism)을 추가하는 게 맞음.
|
|
116
|
+
|
|
117
|
+
### 변경 (정직화 + 가드)
|
|
118
|
+
- **문구 정직화**: `verifyClaimAllCmd`/`gate --claims` 코드주석을 "워크스페이스-상태 휴리스틱(5체크) vs 콘텐츠-레벨 검증(--claims)" 으로 교정 — lazy/audit 이 콘텐츠를 본다는 오해 제거. README enforcement 문구("via heuristics … --claims makes it precise")는 이미 정확해 유지.
|
|
119
|
+
- **결정적 판별 e2e 가드 B(1.34.1)**: lazy detect 0 finding + 부풀린카운트 → 기본 게이트 exit 0, `--claims` exit 1(verify-claims FAIL · lazy detect ok). 미래에 `--claims` 가 조용히 무가치(기본도 잡거나 --claims 가 못 잡음)해지면 즉시 실패하게 고정.
|
|
120
|
+
|
|
121
|
+
### 검증 (회귀 0)
|
|
122
|
+
- selftest 259 · E2E 381(B(1.34.1) 추가). 신규 표면 적대 프로브(스텁·부풀린카운트·json오염·MCP) 모두 정상.
|
|
123
|
+
- patch(1.34.1) — npm 미배포(R-0011, 다음 minor 에 게시). bin+package.json 동시 bump 일치.
|
|
124
|
+
|
|
3
125
|
## 1.34.0 — 2026-06-19 — 🛡️ [안정화/Stable] verify-claim + CI gate 슬라이스 강화 (CLI→CI→MCP 완결) 안정 minor
|
|
4
126
|
|
|
5
127
|
**🛡️ 안정화(Stable) minor — 직전 minor(1.33.0) 이후 누적된 패치 3건(1.33.1~1.33.3)을 검증·통합해 npm 공개.** R-0011 정책의 25번째 stable minor. **이번 minor 의 핵심은 차별화 슬라이스(verify-claim + CI gate)를 한 테마로 완결한 것** — 웹 Opus 4.8 외부리뷰가 "leerness 의 진짜 가치 = verify-claim + CI gate"라 짚었고, 사용자가 그 방향을 선택해 3라운드에 걸쳐 플래그십을 CLI → CI 게이트 → MCP 까지 연결했습니다. 한국어 우선 기본은 그대로.
|
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.
|
|
118
|
+
이 프로젝트는 Leerness v1.35.3 하네스를 사용합니다. 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.
|
|
172
|
+
Leerness v1.35.3는 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.34.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.
|
|
193
|
+
현재 누적: **70 라운드 (1.9.40 → 1.35.3)** · 매 라운드 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.
|
|
231
|
+
Last synced by Leerness v1.35.3: 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
|
+
const VERSION = '1.35.3';
|
|
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,6 +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; } },
|
|
2981
|
+
{ 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; } },
|
|
2982
|
+
{ 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; } },
|
|
2968
2983
|
{ 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
2984
|
{ 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
2985
|
{ 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; } },
|
|
@@ -10787,7 +10802,8 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
10787
10802
|
|
|
10788
10803
|
// 1.33.2 (verify-claim+CI gate 슬라이스 강화): verify-claim --all — 모든 done/완료 주장을 한 번에 검증(CI·스케일용).
|
|
10789
10804
|
// per-task 경로(verifyClaimCmd)를 opts.collect 로 재사용 → verdict 만 집계(렌더/exit 분기는 collect 가 흡수). 통과 플래그(--run-tests/--strict-claims/--lenient 등)는 전역이라 각 task 에 그대로 적용됨.
|
|
10790
|
-
// 동기: 플래그십(verify-claim)이 종전 per-task 전용이라, "내 완료 주장 전부 증거와 맞는가?"를 한 명령으로 못 했음.
|
|
10805
|
+
// 동기: 플래그십(verify-claim)이 종전 per-task 전용이라, "내 완료 주장 전부 증거와 맞는가?"를 한 명령으로 못 했음.
|
|
10806
|
+
// 1.34.1 (16th리뷰 정직화): 기본 게이트 5체크는 워크스페이스-상태 휴리스틱(handoff/test-run/evidence 부재 등)으로 거짓완료를 잡고, 콘텐츠-레벨 주장(파일 존재·테스트 카운트·스텁·optimism)은 검사하지 않음. verify-claim --all 은 그 콘텐츠 차원을 추가 — lazy detect 가 깨끗한(handoff·테스트기록 완비) 성숙 프로젝트에선 5체크가 통과해도 이 검사만 콘텐츠 거짓을 잡음(실증: gate 기본 exit 0 vs --claims exit 1).
|
|
10791
10807
|
// 1.33.3: 일괄 검증 코어 — 렌더/exit 없이 결과만 반환. verifyClaimAllCmd(CLI 렌더+exit) 와 gate --claims(opt-in 체크) 가 공유.
|
|
10792
10808
|
// gate 의 step() 은 process.exit 가 아니라 process.exitCode 로 실패를 감지하므로, 코어는 절대 process.exit 하지 않음(게이트 프로세스 조기종료 방지).
|
|
10793
10809
|
function _verifyClaimsAll(root) {
|
|
@@ -12715,7 +12731,8 @@ function gate(root) {
|
|
|
12715
12731
|
const checks = [];
|
|
12716
12732
|
let bad = 0;
|
|
12717
12733
|
// 1.33.3 (verify-claim+CI gate 슬라이스 강화): --claims opt-in — 모든 done 주장을 정밀 per-claim 검증(verify-claim --all)으로 추가(6번째). 기본 5체크는 무변경(기존 어댑터 회귀 0).
|
|
12718
|
-
//
|
|
12734
|
+
// 1.34.1 (16th리뷰 정직화): 기본 5체크(특히 lazy detect)는 워크스페이스-상태(handoff/test-run/evidence 부재) 신호로 거짓완료를 잡지, 콘텐츠(파일/카운트/스텁)는 검사하지 않음.
|
|
12735
|
+
// --claims 는 콘텐츠-레벨 검증을 추가 — 워크스페이스가 깨끗한 성숙 프로젝트에선 기본 5체크가 통과(exit 0)해도 --claims 만 콘텐츠 거짓을 잡아(exit 1) README 약속("claims fail → cannot merge")을 문자 그대로 강제. (실증 가드: e2e B(1.34.1))
|
|
12719
12736
|
const withClaims = has('--claims');
|
|
12720
12737
|
if (!jsonMode) log(`# leerness gate (${withClaims ? 6 : 5} checks)`);
|
|
12721
12738
|
function step(label, fn) {
|
|
@@ -20111,6 +20128,19 @@ function reviewRequestCmd(root, request) { return _reviewRequest.reviewRequestCm
|
|
|
20111
20128
|
const _diag = require('../lib/diagnostics');
|
|
20112
20129
|
function doctorCmd(opts = {}) { return _diag.doctorCmd(opts, { VERSION, uiLang: _uiLang(arg('--path', process.cwd())), _selfTestCases, _detectShellCtx, _mcpToolCount, has, harnessPath: __filename }); }
|
|
20113
20130
|
function whichCmd() { return _diag.whichCmd({ VERSION, uiLang: _uiLang(arg('--path', process.cwd())), has, harnessPath: __filename }); }
|
|
20131
|
+
// 1.34.3 (T-0077): `graph --html` → lib/graph.js 온톨로지 HTML(leerness.html) 생성기 위임. 데이터는 in-process 로더 주입(자식 프로세스 셸링 X).
|
|
20132
|
+
const _graph = require('../lib/graph');
|
|
20133
|
+
function graphHtmlCmd(root) { return _graph.graphHtmlCmd(root, { _roadmapData, _loadDecisions, _loadLessons, has, arg }); }
|
|
20134
|
+
// 1.34.4 (T-0077 후속): handoff 시 leerness.html 자동 재생성 — opt-in(LEERNESS_AUTO_GRAPH=1, 기본 OFF / "Always-Off Opt-In"). 사용자 비전 "자동으로 작성되게" 충족. 비치명(try/catch) · 기본경로 무영향.
|
|
20135
|
+
function _maybeAutoGraph(root) {
|
|
20136
|
+
if (process.env.LEERNESS_AUTO_GRAPH !== '1') return;
|
|
20137
|
+
try {
|
|
20138
|
+
const r0 = absRoot(root || process.cwd());
|
|
20139
|
+
if (!exists(path.join(r0, '.harness'))) return;
|
|
20140
|
+
const s = _graph.graphHtmlCmd(r0, { _roadmapData, _loadDecisions, _loadLessons, quiet: true });
|
|
20141
|
+
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 출력 오염 방지
|
|
20142
|
+
} catch {}
|
|
20143
|
+
}
|
|
20114
20144
|
|
|
20115
20145
|
// 1.23.1 (UR-0010 Phase 6): 영어 큐레이트 도움말 — 한국어 help 의 줄별 번역이 아니라, 카테고리별로 정리한 별도 영어판.
|
|
20116
20146
|
// 레거시 버전태그(1.9.x) 군더더기를 빼고 영어 사용자가 읽기 쉽게. 전체 전수 목록은 `leerness commands`.
|
|
@@ -20231,7 +20261,7 @@ function help() {
|
|
|
20231
20261
|
leerness skill install <SKILL.md|dir|url> · leerness skill discover --preset vercel|anthropic # 스킬 설치/탐색
|
|
20232
20262
|
leerness release bump [--patch|--minor|--major] # package.json 자동 bump (1.9.8)
|
|
20233
20263
|
leerness release note "<내용>" # CHANGELOG.md 자동 추가 (1.9.8)
|
|
20234
|
-
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`);
|
|
20264
|
+
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`);
|
|
20235
20265
|
}
|
|
20236
20266
|
|
|
20237
20267
|
async function main() {
|
|
@@ -20322,7 +20352,7 @@ async function main() {
|
|
|
20322
20352
|
if (cmd === 'encoding' && args[1] === 'check') return encodingCheck(arg('--path', args[2] || process.cwd()));
|
|
20323
20353
|
if (cmd === 'lazy' && args[1] === 'detect') return lazyDetect(_resolveRoot(args[2]), { json: has('--json') });
|
|
20324
20354
|
if (cmd === 'memory' && args[1] === 'search') return memorySearch(arg('--path', process.cwd()), args.slice(2).join(' '));
|
|
20325
|
-
if (cmd === 'handoff')
|
|
20355
|
+
if (cmd === 'handoff') { const _hp = arg('--path', args[1] || process.cwd()); const _hr = handoffCmd(_hp); _maybeAutoGraph(_hp); return _hr; }
|
|
20326
20356
|
if (cmd === 'reuse-map') return reuseMapCmd(arg('--path', args[1] || process.cwd()));
|
|
20327
20357
|
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 주장 일괄 검증
|
|
20328
20358
|
if (cmd === 'orchestrate') return await orchestrateCmd(arg('--path', process.cwd()), args.slice(1).filter(x => !x.startsWith('-')));
|
|
@@ -20554,7 +20584,7 @@ async function main() {
|
|
|
20554
20584
|
if (cmd === 'reuse' && args[1] === 'find') return reuseFind(arg('--path', process.cwd()), args.slice(2).filter(x => !x.startsWith('-')).join(' '));
|
|
20555
20585
|
if (cmd === 'reuse' && args[1] === 'register') return reuseRegister(arg('--path', process.cwd()), args[2]);
|
|
20556
20586
|
if (cmd === 'ui' && args[1] === 'consistency') return uiConsistency(arg('--path', args[2] || process.cwd()));
|
|
20557
|
-
if (cmd === 'graph') return graphCmd(arg('--path', args[1] || process.cwd()));
|
|
20587
|
+
if (cmd === 'graph') return has('--html') ? graphHtmlCmd(arg('--path', args[1] || process.cwd())) : graphCmd(arg('--path', args[1] || process.cwd()));
|
|
20558
20588
|
if (cmd === 'guide') return guideCmd(arg('--path', process.cwd()), args[1]);
|
|
20559
20589
|
// legacy duplicate routing removed below (was: skill list/info/add)
|
|
20560
20590
|
if (cmd === 'skill' && args[1] === 'info') return skillInfo(args[2]);
|
package/lib/graph.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
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 · search+Enter jump · f / dblclick fit · Esc close</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
|
+
var _fit=false;
|
|
82
|
+
|
|
83
|
+
// physics
|
|
84
|
+
var alpha=1;
|
|
85
|
+
function tick(){
|
|
86
|
+
if(alpha<0.006) return;
|
|
87
|
+
if(alpha>0.005) alpha*=0.992;
|
|
88
|
+
var REP=2600,SPR=0.012,LEN=70,CEN=0.012;
|
|
89
|
+
for(var i=0;i<nodes.length;i++){var a=nodes[i]; if(off[a.type])continue;
|
|
90
|
+
for(var j=i+1;j<nodes.length;j++){var b=nodes[j]; if(off[b.type])continue;
|
|
91
|
+
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;}
|
|
92
|
+
a.vx+=(cam.cx-a.x)*CEN; a.vy+=(cam.cy-a.y)*CEN;
|
|
93
|
+
}
|
|
94
|
+
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;});
|
|
95
|
+
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;});
|
|
96
|
+
}
|
|
97
|
+
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};}
|
|
98
|
+
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};}
|
|
99
|
+
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;}
|
|
100
|
+
|
|
101
|
+
function draw(){
|
|
102
|
+
ctx.clearRect(0,0,W,H);
|
|
103
|
+
// edges
|
|
104
|
+
ctx.lineWidth=1;
|
|
105
|
+
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
|
+
// nodes
|
|
107
|
+
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;
|
|
108
|
+
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();
|
|
109
|
+
if(n===sel||n===hover){ctx.strokeStyle='#fff';ctx.lineWidth=1.5;ctx.stroke();}
|
|
110
|
+
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);}
|
|
111
|
+
ctx.globalAlpha=1;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function loop(){tick(); if(!_fit&&nodes.length&&alpha<0.08){_fit=true;fitView();} draw();requestAnimationFrame(loop);} loop();
|
|
115
|
+
|
|
116
|
+
// interaction
|
|
117
|
+
var drag=null,panning=null,moved=false;
|
|
118
|
+
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};}});
|
|
119
|
+
window.addEventListener('mousemove',function(ev){var r=cv.getBoundingClientRect();var mx=ev.clientX-r.left,my=ev.clientY-r.top;
|
|
120
|
+
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;}
|
|
121
|
+
else if(panning){view.x=panning.vx+(mx-panning.x);view.y=panning.vy+(my-panning.y);moved=true;}
|
|
122
|
+
else{hover=hit(mx,my);cv.style.cursor=hover?'pointer':'default';}
|
|
123
|
+
});
|
|
124
|
+
window.addEventListener('mouseup',function(ev){ if(drag){drag.fixed=false; if(!moved)select(drag); drag=null;} else if(panning){ if(!moved){closePanel();} panning=null;} });
|
|
125
|
+
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});
|
|
126
|
+
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;}
|
|
127
|
+
|
|
128
|
+
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);}
|
|
129
|
+
function closePanel(){sel=null;document.getElementById('panel').classList.remove('show');}
|
|
130
|
+
function showPanel(n){
|
|
131
|
+
var nb=Object.keys(nbr).map(function(id){return idx[id];}).filter(Boolean);
|
|
132
|
+
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>';
|
|
133
|
+
h+='<h2>'+esc(n.label||n.id)+'</h2>';
|
|
134
|
+
var d=n.detail||{};
|
|
135
|
+
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>';});
|
|
136
|
+
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>';}
|
|
137
|
+
document.getElementById('pbody').innerHTML=h;
|
|
138
|
+
document.getElementById('panel').classList.add('show');
|
|
139
|
+
}
|
|
140
|
+
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;}};
|
|
141
|
+
document.getElementById('search').addEventListener('input',function(ev){window._q=ev.target.value.trim().toLowerCase()||null;});
|
|
142
|
+
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);} });
|
|
143
|
+
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==='Escape'){closePanel();} });
|
|
144
|
+
cv.addEventListener('dblclick',function(ev){ if(!hit(ev.offsetX,ev.offsetY)){_fit=true;fitView();} });
|
|
145
|
+
</script></body></html>`;
|
|
146
|
+
|
|
147
|
+
const _txt = v => (v == null ? '' : String(v));
|
|
148
|
+
function _trunc(s, n) { s = _txt(s); return s.length > n ? s.slice(0, n - 1) + '…' : s; }
|
|
149
|
+
|
|
150
|
+
// 하네스 표면 → {project, version, counts, nodes, edges}. deps 로 in-process 로더 주입(셸링 X).
|
|
151
|
+
function buildGraphData(root, deps = {}) {
|
|
152
|
+
const { _roadmapData, _loadDecisions, _loadLessons } = deps;
|
|
153
|
+
const rd = (typeof _roadmapData === 'function' ? _roadmapData(root) : {}) || {};
|
|
154
|
+
const decisions = (typeof _loadDecisions === 'function' ? _loadDecisions(root) : []) || [];
|
|
155
|
+
const lessons = (typeof _loadLessons === 'function' ? _loadLessons(root) : []) || [];
|
|
156
|
+
|
|
157
|
+
const nodes = []; const byId = new Map(); const byLabel = new Map();
|
|
158
|
+
function add(node) {
|
|
159
|
+
if (byId.has(node.id)) return;
|
|
160
|
+
nodes.push(node); byId.set(node.id, node);
|
|
161
|
+
if (node.label) byLabel.set(_txt(node.label).trim().toLowerCase(), node.id);
|
|
162
|
+
}
|
|
163
|
+
// task (status 색상) — _roadmapData 가 evidence 의 M-#### 를 t.milestones 로 이미 추출.
|
|
164
|
+
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 || [] });
|
|
165
|
+
// plan (milestone)
|
|
166
|
+
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) } });
|
|
167
|
+
// decision — title 은 제네릭("Decision")일 수 있어 실내용(decision) 우선. id 없으면 합성.
|
|
168
|
+
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) } }); }
|
|
169
|
+
// lesson — 내용은 text. id 없으면 합성.
|
|
170
|
+
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) } }); }
|
|
171
|
+
// rule
|
|
172
|
+
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) } }); }
|
|
173
|
+
// skill
|
|
174
|
+
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) } }); }
|
|
175
|
+
|
|
176
|
+
// edges — 같은 (source,target) 쌍 dedup: task→milestone 가 _ms 추출 + blob M-#### 정규식에서 이중 추가되는 것 방지(엣지수/degree 정확).
|
|
177
|
+
const edges = [];
|
|
178
|
+
const _seenEdge = new Set();
|
|
179
|
+
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 }); }
|
|
180
|
+
for (const n of nodes) {
|
|
181
|
+
if (n._ms) for (const mid of n._ms) linkIds(n.id, mid, 'milestone');
|
|
182
|
+
const blob = Object.values(n.detail || {}).join(' ');
|
|
183
|
+
for (const m of (blob.match(/\bM-\d{3,}\b/g) || [])) linkIds(n.id, m, 'milestone');
|
|
184
|
+
for (const r of (blob.match(/\b[TURDL]-\d{3,}\b/g) || [])) linkIds(n.id, r, 'ref');
|
|
185
|
+
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'); }
|
|
186
|
+
}
|
|
187
|
+
// feature-graph.md (선택) — "A -> B" / "A uses B" 의존 라인 → feature 노드/엣지.
|
|
188
|
+
const fg = path.join(root, '.harness', 'feature-graph.md');
|
|
189
|
+
if (exists(fg)) {
|
|
190
|
+
try {
|
|
191
|
+
for (const line of read(fg).split(/\r?\n/)) {
|
|
192
|
+
const m = line.match(/([\w./-]+)\s*(?:->|→|depends on|uses)\s*([\w./-]+)/i);
|
|
193
|
+
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'); }
|
|
194
|
+
}
|
|
195
|
+
} catch {}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
for (const n of nodes) delete n._ms; // 내부 보조 필드 임베드 제외
|
|
199
|
+
const counts = {};
|
|
200
|
+
for (const n of nodes) counts[n.type] = (counts[n.type] || 0) + 1;
|
|
201
|
+
return { project: rd.project || path.basename(root), version: rd.version || '', root, counts, nodes, edges };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// `leerness graph --html [path] [--out file] [--json]` 핸들러.
|
|
205
|
+
function graphHtmlCmd(root, deps = {}, outFile) {
|
|
206
|
+
root = absRoot(root);
|
|
207
|
+
const { has, quiet } = deps; // quiet: auto-gen(handoff) 시 사람용 3줄 로그 억제
|
|
208
|
+
const data = buildGraphData(root, deps);
|
|
209
|
+
const out = outFile || (has && has('--out') && deps.arg ? path.resolve(root, deps.arg('--out')) : path.join(root, 'leerness.html'));
|
|
210
|
+
// 임베드 안전화: 모든 '<' → < 로 치환해 </script>·<!-- 차단(JSON 문자열 내부라 런타임엔 '<' 복원). function 치환기로 $-특수문자(예: $&) 회피.
|
|
211
|
+
const json = JSON.stringify(data).replace(/</g, '\\u003c');
|
|
212
|
+
const html = TEMPLATE.replace('/*__DATA__*/null', () => json);
|
|
213
|
+
writeUtf8(out, html);
|
|
214
|
+
const summary = { ok: true, file: out, nodes: data.nodes.length, edges: data.edges.length, counts: data.counts };
|
|
215
|
+
if (has && has('--json')) { process.stdout.write(JSON.stringify(summary, null, 2) + '\n'); return summary; }
|
|
216
|
+
if (!quiet) {
|
|
217
|
+
log(`leerness.html → ${out}`);
|
|
218
|
+
log(` ${data.nodes.length} nodes · ${data.edges.length} links · ${Object.entries(data.counts).map(([k, v]) => k + ':' + v).join(' ')}`);
|
|
219
|
+
log(` open in a browser to explore the ontology graph (click a node → details).`);
|
|
220
|
+
}
|
|
221
|
+
return summary;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
module.exports = { graphHtmlCmd, buildGraphData };
|