leerness 1.35.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 +40 -1
- package/README.md +5 -4
- package/bin/leerness.js +3 -1
- package/lib/graph.js +11 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
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
|
+
|
|
3
42
|
## 1.35.0 — 2026-06-27 — 🕸️ [안정화/Stable] 온톨로지 그래프(graph --html + 자동생성) 안정 minor
|
|
4
43
|
|
|
5
44
|
**신규 기능 minor**: leerness 적용 프로젝트의 하네스(5 메모리 표면 + skills + feature-graph)를 인터랙티브 온톨로지 그래프 HTML(`leerness.html`)로 표면화하는 기능을 도입하고, 누적 수정(1.34.1~1.34.4)을 묶어 안정화. 0 런타임 의존 자기완결 vanilla JS — 노드 클릭으로 하네스 내용을 손쉽게 조회.
|
|
@@ -15,7 +54,7 @@
|
|
|
15
54
|
- **배포 대기**: `npm publish` 는 2FA OTP 필요 → 사용자 게시 후 게시본 클린룸 재실증(graph --html 행위 포함, re-verify-published-artifact 교훈).
|
|
16
55
|
|
|
17
56
|
### 검증 (회귀 0)
|
|
18
|
-
- selftest **262** · full **e2e 381/381**.
|
|
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 일치.
|
|
19
58
|
|
|
20
59
|
## 1.34.4 — 2026-06-27 — 📊 graph 자동생성(opt-in): handoff 시 leerness.html 자동 갱신
|
|
21
60
|
|
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.
|
|
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.35.
|
|
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.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.
|
|
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.35.
|
|
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.
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -2978,6 +2978,8 @@ function _selfTestCases() {
|
|
|
2978
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
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
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; } },
|
|
2981
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; } },
|
|
2982
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 를 더 밀어냄)
|
|
2983
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; } },
|
package/lib/graph.js
CHANGED
|
@@ -49,7 +49,7 @@ 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 · 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'};
|
|
@@ -78,10 +78,12 @@ types.forEach(function(t){var c=DATA.counts&&DATA.counts[t]; var el=document.cre
|
|
|
78
78
|
var view={x:0,y:0,k:1};
|
|
79
79
|
var sel=null,hover=null,nbr={};
|
|
80
80
|
var cam={cx:W/2,cy:H/2};
|
|
81
|
+
var _fit=false;
|
|
81
82
|
|
|
82
83
|
// physics
|
|
83
84
|
var alpha=1;
|
|
84
85
|
function tick(){
|
|
86
|
+
if(alpha<0.006) return;
|
|
85
87
|
if(alpha>0.005) alpha*=0.992;
|
|
86
88
|
var REP=2600,SPR=0.012,LEN=70,CEN=0.012;
|
|
87
89
|
for(var i=0;i<nodes.length;i++){var a=nodes[i]; if(off[a.type])continue;
|
|
@@ -94,6 +96,7 @@ function tick(){
|
|
|
94
96
|
}
|
|
95
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};}
|
|
96
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;}
|
|
97
100
|
|
|
98
101
|
function draw(){
|
|
99
102
|
ctx.clearRect(0,0,W,H);
|
|
@@ -108,18 +111,18 @@ function draw(){
|
|
|
108
111
|
ctx.globalAlpha=1;
|
|
109
112
|
});
|
|
110
113
|
}
|
|
111
|
-
function loop(){tick();draw();requestAnimationFrame(loop);} loop();
|
|
114
|
+
function loop(){tick(); if(!_fit&&nodes.length&&alpha<0.08){_fit=true;fitView();} draw();requestAnimationFrame(loop);} loop();
|
|
112
115
|
|
|
113
116
|
// interaction
|
|
114
117
|
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};}});
|
|
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};}});
|
|
116
119
|
window.addEventListener('mousemove',function(ev){var r=cv.getBoundingClientRect();var mx=ev.clientX-r.left,my=ev.clientY-r.top;
|
|
117
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;}
|
|
118
121
|
else if(panning){view.x=panning.vx+(mx-panning.x);view.y=panning.vy+(my-panning.y);moved=true;}
|
|
119
122
|
else{hover=hit(mx,my);cv.style.cursor=hover?'pointer':'default';}
|
|
120
123
|
});
|
|
121
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;} });
|
|
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});
|
|
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});
|
|
123
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;}
|
|
124
127
|
|
|
125
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);}
|
|
@@ -136,6 +139,9 @@ function showPanel(n){
|
|
|
136
139
|
}
|
|
137
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;}};
|
|
138
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();} });
|
|
139
145
|
</script></body></html>`;
|
|
140
146
|
|
|
141
147
|
const _txt = v => (v == null ? '' : String(v));
|
|
@@ -170,7 +176,7 @@ function buildGraphData(root, deps = {}) {
|
|
|
170
176
|
// edges — 같은 (source,target) 쌍 dedup: task→milestone 가 _ms 추출 + blob M-#### 정규식에서 이중 추가되는 것 방지(엣지수/degree 정확).
|
|
171
177
|
const edges = [];
|
|
172
178
|
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 + '
|
|
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 }); }
|
|
174
180
|
for (const n of nodes) {
|
|
175
181
|
if (n._ms) for (const mid of n._ms) linkIds(n.id, mid, 'milestone');
|
|
176
182
|
const blob = Object.values(n.detail || {}).join(' ');
|