axmap-cli 1.3.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CLAUDE.md +15 -9
  2. package/README.ko.md +87 -87
  3. package/README.md +88 -88
  4. package/bin/axmap.mjs +9 -3
  5. package/mcp/README.md +8 -8
  6. package/mcp/install.ps1 +6 -3
  7. package/mcp/install.sh +5 -3
  8. package/mcp/server.mjs +81 -32
  9. package/package.json +7 -11
  10. package/src/agentcli.mjs +19 -0
  11. package/src/bridge.mjs +245 -0
  12. package/src/invariants.mjs +1 -1
  13. package/src/managed-session.mjs +178 -0
  14. package/src/protocol.mjs +1 -1
  15. package/src/session-dialogue.mjs +131 -0
  16. package/src/session-usage.mjs +34 -0
  17. package/src/state.mjs +8 -0
  18. package/src/terminal-bridge.mjs +224 -0
  19. package/src/update.mjs +1 -1
  20. package/tools/bridge.mjs +85 -0
  21. package/tools/mcp-register.mjs +2 -2
  22. package/tools/run-tests.mjs +10 -3
  23. package/tools/run.mjs +60 -0
  24. package/tools/setup.mjs +11 -0
  25. package/app/README.md +0 -366
  26. package/app/eval/edges.mjs +0 -242
  27. package/app/lib/adjacent.mjs +0 -125
  28. package/app/lib/agentcli.mjs +0 -153
  29. package/app/lib/analyze.mjs +0 -1159
  30. package/app/lib/cochange.mjs +0 -421
  31. package/app/lib/datanodes.mjs +0 -127
  32. package/app/lib/entry.mjs +0 -192
  33. package/app/lib/featuregraph.mjs +0 -389
  34. package/app/lib/features.mjs +0 -645
  35. package/app/lib/fetchrepo-run.mjs +0 -37
  36. package/app/lib/fetchrepo.mjs +0 -164
  37. package/app/lib/flow.mjs +0 -1089
  38. package/app/lib/ladder.mjs +0 -387
  39. package/app/lib/langs.mjs +0 -630
  40. package/app/lib/live.mjs +0 -346
  41. package/app/lib/llm.mjs +0 -594
  42. package/app/lib/newfile.mjs +0 -126
  43. package/app/lib/prdiff.mjs +0 -651
  44. package/app/lib/reveal.mjs +0 -316
  45. package/app/lib/roots.mjs +0 -186
  46. package/app/lib/scope.mjs +0 -342
  47. package/app/lib/session.mjs +0 -389
  48. package/app/lib/slots.mjs +0 -233
  49. package/app/lib/ssot.mjs +0 -277
  50. package/app/lib/teamview.mjs +0 -975
  51. package/app/lib/terms.ko.mjs +0 -169
  52. package/app/server.mjs +0 -1959
  53. package/app/web/shell.css +0 -538
  54. package/app/web/shell.html +0 -197
  55. package/app/web/shell.js +0 -638
  56. package/app/web/stage.js +0 -347
  57. package/app/web/words.js +0 -85
  58. package/src/repotarget.mjs +0 -84
  59. package/tools/cluster-experiment.mjs +0 -256
  60. package/tools/cluster-sweep.mjs +0 -226
  61. package/tools/make-icon.mjs +0 -108
  62. package/tools/persona-bench.mjs +0 -362
  63. package/tools/pick-repo.mjs +0 -229
  64. package/tools/reveal-demo.mjs +0 -158
  65. package/tools/shortcut.mjs +0 -121
  66. package/tools/smoke.mjs +0 -174
  67. package/tools/topicgraph.py +0 -154
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs'
3
+ import { resolveBin } from '../src/agentcli.mjs'
4
+ import { codexInvocation } from './mcp-register.mjs'
5
+ import { dispatchRequest, installHooks, registerHook, requestResult, sessions, uninstallHooks } from '../src/bridge.mjs'
6
+ import { startManagedSession } from '../src/managed-session.mjs'
7
+ import { runDialogue, startBackgroundDialogue, dialogueResult, stopDialogue } from '../src/session-dialogue.mjs'
8
+ import { sendTerminal } from '../src/terminal-bridge.mjs'
9
+
10
+ const args = process.argv.slice(2)
11
+ const [command] = args
12
+ const option = (name) => { const i = args.indexOf(`--${name}`); return i < 0 ? undefined : args[i + 1] }
13
+ const output = (value) => console.log(JSON.stringify(value, null, 2))
14
+ try {
15
+ switch (command) {
16
+ case 'serve': {
17
+ const provider = args[1]
18
+ if (!['codex', 'agy'].includes(provider)) throw new Error('axmap bridge serve codex|agy를 사용하세요.')
19
+ const binary = resolveBin(provider)
20
+ if (!binary) throw new Error(`${provider} 실행 파일을 찾지 못했습니다.`)
21
+ const [executable, ...prefix] = provider === 'codex' ? codexInvocation(binary) : [binary]
22
+ const session = await startManagedSession({ provider, executable, prefix, model: option('model'), role: option('role'),
23
+ maxTokens: Number(option('max-tokens') || 100000), onEvent: output })
24
+ output({ event: 'connected', ...session, exited: undefined, close: undefined })
25
+ process.once('SIGINT', session.close); process.once('SIGTERM', session.close)
26
+ const exit = await session.exited
27
+ process.exitCode = exit.code || 0
28
+ break
29
+ }
30
+ case 'chat': {
31
+ const options = { ...(option('id') ? { id: option('id') } : {}), left: option('left'), right: option('right'), prompt: fs.readFileSync(0, 'utf8'),
32
+ maxTurns: Number(option('max-turns') || 4), maxTokens: Number(option('max-tokens') || 50000),
33
+ maxMessageChars: Number(option('max-message-chars') || 2000),
34
+ onEvent: event => console.error(JSON.stringify(event)) }
35
+ const r = args.includes('--background') ? await startBackgroundDialogue(options) : await runDialogue(options)
36
+ output(r); if (r.state !== 'completed') process.exitCode = 2
37
+ if (r.state === 'queued') process.exitCode = 0
38
+ break
39
+ }
40
+ case 'dialogue': output(dialogueResult(args[1])); break
41
+ case 'stop': output(stopDialogue(args[1])); break
42
+ case 'answer': output(await sendTerminal(option('to'), { action: 'answer', id: option('request'), decision: option('decision') })); break
43
+ case 'install':
44
+ if (!args.includes('--experimental')) throw new Error('실험용 연결입니다. 현재 검증한 CLI는 필요한 연결 정보를 훅에 제공하지 않습니다. 확인 후 --experimental로 명시하세요.')
45
+ output(installHooks({ executable: option('executable') })); break
46
+ case 'uninstall': output(uninstallHooks()); break
47
+ case 'hook': {
48
+ const input = JSON.parse(fs.readFileSync(0, 'utf8'))
49
+ try {
50
+ registerHook(input, option('event'), { ...process.env, AXMAP_AGY_EXECUTABLE: option('executable') })
51
+ } catch (e) { console.error(`axMap bridge: ${e.message}`) }
52
+ // Observe only. Never alter a provider's permission or continuation decision.
53
+ output({})
54
+ break
55
+ }
56
+ case 'sessions': output(sessions(process.cwd())); break
57
+ case 'send': {
58
+ const r = dispatchRequest({ to: option('to'), prompt: fs.readFileSync(0, 'utf8'), cwd: process.cwd(), from: process.env.AXMAP_AGENT || 'axmap' })
59
+ output(r)
60
+ if (r.state === 'unknown') process.exitCode = 2
61
+ break
62
+ }
63
+ case 'result': output(requestResult(args[1], process.cwd())); break
64
+ case undefined:
65
+ case '--help':
66
+ console.log(`axMap 로컬 세션 연결 — 관리형 Codex/agy와 제한된 자동 대화
67
+ axmap bridge serve codex|agy [--model 이름] [--role 역할] [--max-tokens 100000]
68
+ 명시적으로 새 관리형 CLI를 시작합니다. 기존 창에 임의로 붙지 않습니다.
69
+ axmap bridge chat --left <UUID> --right <UUID> [--max-turns 4] [--max-tokens 50000]
70
+ stdin 작업을 자동 중계합니다. 기본 메시지 한도 2000자, 대기 중 모델 호출 없음.
71
+ axmap bridge answer --to <UUID> --request <ID> --decision accept|decline|cancel
72
+ Codex가 요청한 이번 실행의 승인을 사용자가 결정합니다.
73
+ axmap bridge chat ... --background 접수 ID를 반환하고 백그라운드 중계
74
+ axmap bridge dialogue <대화 UUID> 상태·회신·누적 토큰 조회
75
+ axmap bridge stop <대화 UUID> 추가 중계 중단 (이미 보낸 요청은 계속될 수 있음)
76
+ axmap bridge install --experimental --executable <agy 절대 경로> 연결 훅 실험
77
+ axmap bridge uninstall 추가한 연결 훅 해제
78
+ axmap bridge sessions 이 repo에 연결된 세션
79
+ axmap bridge send --to <세션 UUID> 본문은 stdin. 기존 세션에 전달
80
+ axmap bridge result <요청 UUID> 제출/완료/불명 상태와 결과
81
+ 관리형 stdio: Codex App Server / agy stream-json. PTY와 agentapi는 별도 실험 경로입니다.`)
82
+ break
83
+ default: throw new Error(`알 수 없는 bridge 명령: ${command}`)
84
+ }
85
+ } catch (e) { console.error(e.message); process.exitCode = 1 }
@@ -30,7 +30,7 @@
30
30
  *
31
31
  * ── 감지는 새로 짜지 않는다 ───────────────────────────────────────────────
32
32
  *
33
- * 어떤 CLI 가 이 PC 에 있는지는 `app/lib/agentcli.mjs` 가 이미 안다(`AGENTS`, `resolveBin`).
33
+ * 어떤 CLI 가 이 PC 에 있는지는 `src/agentcli.mjs` 가 이미 안다(`AGENTS`, `resolveBin`).
34
34
  * 거기서 그대로 가져온다. 바이너리 이름이 두 군데에 적히면 한쪽만 낡는다.
35
35
  */
36
36
 
@@ -40,7 +40,7 @@ import os from 'node:os'
40
40
  import path from 'node:path'
41
41
  import { fileURLToPath } from 'node:url'
42
42
 
43
- import { AGENTS, resolveBin } from '../app/lib/agentcli.mjs'
43
+ import { AGENTS, resolveBin } from '../src/agentcli.mjs'
44
44
  import { claudeUserEntry } from '../src/mcpstate.mjs'
45
45
 
46
46
  const HERE = path.dirname(fileURLToPath(import.meta.url))
@@ -37,6 +37,13 @@ if (files.length === 0) {
37
37
  process.exit(1)
38
38
  }
39
39
 
40
- const r = spawnSync(process.execPath, ['--test', ...files], { stdio: 'inherit', cwd: root })
41
- // status 가 null 이면 신호로 죽은 것이다. 통과로 세지 않는다.
42
- process.exit(r.status ?? 1)
40
+ // Windows ConPTY owns native console workers. Exercise it after other test
41
+ // processes have exited so temporary console teardown cannot race the suite's
42
+ // many concurrent subprocess fixtures. Both batches remain mandatory.
43
+ const native = process.platform === 'win32' ? files.filter(f => path.basename(f) === 'terminal-bridge.test.mjs') : []
44
+ const ordinary = files.filter(f => !native.includes(f))
45
+ for (const batch of [ordinary, native].filter(batch => batch.length)) {
46
+ const r = spawnSync(process.execPath, ['--test', ...batch], { stdio: 'inherit', cwd: root })
47
+ if (r.status !== 0) process.exit(r.status ?? 1)
48
+ }
49
+ process.exit(0)
package/tools/run.mjs ADDED
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { spawnSync } from 'node:child_process'
5
+ import { startTerminal, sendTerminal, installTerminalHooks, installTerminalRuntime } from '../src/terminal-bridge.mjs'
6
+
7
+ const [command, ...args] = process.argv.slice(2)
8
+ try {
9
+ if (command === 'install-runtime') {
10
+ console.log(JSON.stringify(installTerminalRuntime(args[0])))
11
+ } else if (command === 'install-hooks') {
12
+ console.log(JSON.stringify({ hooks: [installTerminalHooks(), installTerminalHooks(undefined, 'codex')] }))
13
+ } else if (command === 'rpc') {
14
+ console.log(JSON.stringify(await sendTerminal(args[0], JSON.parse(fs.readFileSync(0, 'utf8')))))
15
+ } else if (command === 'hook') {
16
+ // Installed once, harmless for sessions not launched through axMap.
17
+ if (process.env.AXMAP_TERMINAL_SESSION && process.env.AXMAP_TERMINAL_TOKEN) {
18
+ await sendTerminal(process.env.AXMAP_TERMINAL_SESSION, { action: 'hook', event: args[0], input: JSON.parse(fs.readFileSync(0, 'utf8')) }, process.env.AXMAP_TERMINAL_TOKEN)
19
+ }
20
+ console.log('{}')
21
+ } else if (command === '--help' || !command) {
22
+ console.log('axmap run agy|codex [기존 CLI 인수...] [--cwd 작업폴더]\n사용자가 여는 실제 CLI 터미널을 유지합니다. 별도 연결 URL은 필요 없습니다.')
23
+ } else {
24
+ if (!['agy', 'codex'].includes(command)) throw new Error('현재 실행 도구는 agy 또는 codex입니다.')
25
+ if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error('사용자가 연 대화형 터미널에서 실행하세요. 새 백그라운드 AI 세션은 만들지 않습니다.')
26
+ const cwdAt = args.indexOf('--cwd')
27
+ let cwd = process.cwd()
28
+ if (cwdAt >= 0) {
29
+ if (!args[cwdAt + 1]) throw new Error('--cwd 뒤에 작업 폴더를 지정하세요.')
30
+ cwd = path.resolve(args[cwdAt + 1]); args.splice(cwdAt, 2)
31
+ }
32
+ const found = spawnSync(process.platform === 'win32' ? 'where.exe' : 'which', [command], { encoding: 'utf8', windowsHide: true })
33
+ const executable = found.stdout?.split(/\r?\n/).map(s => s.trim()).find(s => s && (process.platform !== 'win32' || path.extname(s).toLowerCase() === '.exe'))
34
+ if (!executable) throw new Error(`${command} 실행 파일을 찾지 못했습니다.`)
35
+ installTerminalHooks(undefined, command)
36
+ if (command === 'codex') console.error('Codex가 새 훅의 신뢰 확인을 요청하면 /hooks에서 axMap 훅을 검토하세요. 승인을 자동으로 우회하지 않습니다.')
37
+ let terminal
38
+ const oldRaw = process.stdin.isRaw
39
+ try {
40
+ terminal = await startTerminal({ executable, args, provider: command, cwd, cols: process.stdout.columns || 100, rows: process.stdout.rows || 30, onData: data => process.stdout.write(data) })
41
+ console.error(`axMap 연결 세션: ${terminal.id}`)
42
+ process.stdin.setRawMode(true); process.stdin.resume()
43
+ const input = data => terminal.input(data.toString('utf8'))
44
+ const resize = () => terminal.resize(process.stdout.columns || 100, process.stdout.rows || 30)
45
+ process.stdin.on('data', input); process.stdout.on('resize', resize)
46
+ const stop = () => terminal.close()
47
+ process.once('SIGTERM', stop)
48
+ const ev = await terminal.exited
49
+ process.removeListener('SIGTERM', stop)
50
+ process.stdin.off('data', input); process.stdout.off('resize', resize)
51
+ process.exitCode = ev.exitCode
52
+ } finally {
53
+ process.stdin.setRawMode(oldRaw); process.stdin.pause()
54
+ process.stdout.write('\x1b[?2004l\x1b[?25h')
55
+ }
56
+ // node-pty's Windows output worker can outlive the exited console. This
57
+ // standalone terminal host has finished; release its remaining native handles.
58
+ process.exit(process.exitCode || 0)
59
+ }
60
+ } catch (e) { console.error(e.message); process.exitCode = 1 }
package/tools/setup.mjs CHANGED
@@ -251,6 +251,17 @@ function installAndRegister() {
251
251
 
252
252
  installAndRegister()
253
253
 
254
+ // The installed copy owns stable hook paths, including the bundled PTY runtime.
255
+ if (!has('--no-register')) {
256
+ if (DRY) hm('세션 연결', '(dry-run) 설치된 사본에 CLI 연결 훅을 등록합니다')
257
+ else {
258
+ const hookRunner = path.join(DEST, 'tools', 'run.mjs')
259
+ const r = spawnSync(process.execPath, [hookRunner, 'install-hooks'], { encoding: 'utf8', windowsHide: true })
260
+ if (r.status === 0) ok('세션 연결', 'axmap run agy로 여는 CLI가 자동 연결됩니다')
261
+ else no('세션 연결', r.stderr?.trim() || '연결 훅 등록 실패')
262
+ }
263
+ }
264
+
254
265
  // ── 6. 장부와 훅 ────────────────────────────────────────────────────────────
255
266
  //
256
267
  // 🔴 **설치된 사본**(`~/.axmap/app`)의 CLI 로 부른다. npx 가 푼 임시 폴더의 것으로
package/app/README.md DELETED
@@ -1,366 +0,0 @@
1
- # axMap 뷰어 (프로토타입)
2
-
3
- > 🔴 **이 문서는 낡았다. 아래 설명 중 상당수는 지금 화면에 없다.**
4
- >
5
- > 커밋 `aa571f6` 에서 `app/web/app.js` (1,065줄) 를 제거하면서
6
- > **트리 · 요약 패널 · 감시 모드 · 터미널 · 기능/찾기 탭 · LLM 스트리밍 요약**이
7
- > 화면에서 빠졌다. 코드는 git 히스토리에 남아 있다.
8
- >
9
- > 지금 실제로 뜨는 화면은 **정적 파싱 × git 공변경 오버레이** 하나다.
10
- > 그 설계는 [../docs/DECISIONS.md](../docs/DECISIONS.md) 의 **D12** 에 있다.
11
- > 이 문서는 방향이 굳으면 다시 쓴다 — 먼저 쓰면 아직 정하지 않은 것을
12
- > 정한 것처럼 보이게 된다 (루트 README 와 같은 이유).
13
-
14
- 코드베이스를 그래프로 보고, 지금 AI 가 어디를 건드리는지 지켜보고, 터미널을 쓰는 화면.
15
-
16
- ```bash
17
- node app/server.mjs <대상경로> [포트]
18
- node app/server.mjs ../e101_hj/redesign/S15P11E101/BE_robot 7777
19
- ```
20
-
21
- 브라우저로 `http://127.0.0.1:7777` 을 연다.
22
-
23
- ---
24
-
25
- ## 화면
26
-
27
- ```
28
- ┌──────────┬────────────────────────────────┐
29
- │ 트리 │ 그래프 │
30
- │ ↕ │ │
31
- │ 요약 ├────────────────────────────────┤
32
- │ │ 터미널 │
33
- └──────────┴────────────────────────────────┘
34
- ```
35
-
36
- 좌측은 한 자리를 둘이 나눠 쓴다. **아무것도 선택 안 하면 디렉터리 트리, 노드를 클릭하면 그 노드의 요약.**
37
- 빈 곳을 클릭하면 트리로 돌아온다.
38
-
39
- **노드를 클릭하면 그것이 곧 이해의 시작점**이 된다. 거기서부터 깊이별로 그래프가 옅어진다.
40
-
41
- ## 두 가지 모드
42
-
43
- | 모드 | 언제 | 보여주는 것 |
44
- |---|---|---|
45
- | **이해** | AI 를 시키기 전 | 시작점에서의 거리. 가까울수록 진하고, 3단계 밖은 옅다 |
46
- | **감시** | AI 가 작업하는 동안 | 누가 무엇을 선점했고 실제로 뭘 건드리는 중인지 |
47
-
48
- 한 화면에 다 넣지 않고 나눈 이유는 색이 다섯 가지를 동시에 뜻하면 아무것도 못 읽기 때문이다.
49
-
50
- ### 감시 모드의 색
51
-
52
- | | 뜻 |
53
- |---|---|
54
- | 🟡 노랑 | 선점했지만 아직 안 건드림 |
55
- | 🟢 초록 | 선점했고 실제로 작업 중 |
56
- | 🔴 빨강 | **선언 없이 수정됨** — 그 AI 는 자기가 뭘 할지 몰랐다 |
57
- | ⚪ 회색 | 관련 없음 |
58
-
59
- 빨강이 핵심이다. `docs/DECISIONS.md` D3 의 "선언 ⊂ 실제" 를 PR 시점이 아니라 **작업 중에** 잡는다.
60
-
61
- 감시 모드가 쓸모 있으려면 대상 저장소에 axMap 장부가 있어야 한다.
62
-
63
- ```bash
64
- cd <대상저장소>
65
- AXMAP_AGENT=agent-a node <axmap>/bin/axmap.mjs init
66
- AXMAP_AGENT=agent-a node <axmap>/bin/axmap.mjs claim src/auth --intent "무엇을 하려는지"
67
- ```
68
-
69
- 장부가 없으면 git 이 본 변경만 표시된다.
70
-
71
- ## 좌측 패널 순서
72
-
73
- 노드를 누르면 이 순서로 나온다. **핵심이 전체 코드보다 먼저다.**
74
-
75
- 1. **지금** — 그 파일을 누가 선점/수정 중인지 (감시 정보가 있을 때)
76
- 2. **전역 신호** — 이 파일이 쓰는 허브 채널
77
- 3. **요약** — LLM 한 문단, 스트리밍
78
- 4. **핵심** — LLM 이 고른 "반드시 볼 곳" 최대 3군데 + 왜 그런지
79
- 5. **코드** — 260줄 이하면 전체, 넘으면 구성 목록만
80
-
81
- 핵심 구간은 LLM 이 고르되 **줄 번호를 파일 길이로 잘라 검증**한다.
82
- 없는 줄을 가리키면 버린다.
83
-
84
- ## 색이 뜻하는 것
85
-
86
- ### 이해 모드
87
-
88
- | 채널 | 뜻 |
89
- |---|---|
90
- | **색상(hue)** | 어느 갈래인가 — 패키지/모듈 그룹. 황금각으로 배정해 서로 최대한 다르다 |
91
- | **투명도** | 시작점에서의 그래프 거리 |
92
- | **반지름(궤도)** | 시작점에서의 **단계**. 1단계·2단계·3단계가 껍질로 보인다 |
93
- | **크기·가림** | 카메라 거리 (원근) |
94
- | **흐르는 점** | 데이터가 가는 방향 (발행 → 구독) |
95
-
96
- ### 감시 모드 — 누가 건드리는가
97
-
98
- | 색 | 뜻 |
99
- |---|---|
100
- | 🟢 초록 | 사람이 작업 중 |
101
- | 🟣 보라 | 대화형 AI 에이전트 |
102
- | 🔵 하늘 | **백그라운드 에이전트** |
103
- | 🩷 분홍 | 다른 팀원 |
104
- | 🟡 노랑 | 선점만 (아직 안 건드림) |
105
- | 🔴 빨강 | **선언 없이 수정됨** ⚠ |
106
-
107
- 행위자 종류는 `axmap claim --actor human|agent|background|team` 으로 정한다.
108
- 지정하지 않으면 이름이 내 git user.name 과 같으면 `human`, 다르면 `team` 으로 본다.
109
-
110
- ## 작성자별 강조
111
-
112
- 좌측 위 작성자 버튼을 누르면 **그 사람이 건드린 파일만 밝고 나머지는 흐려진다.**
113
- `git log --relative` 로 뷰어가 보고 있는 경로 기준으로 집계한다.
114
-
115
- ## 코드는 필요한 만큼만
116
-
117
- 전체를 쏟아내면 "핵심만 읽게 한다"는 목적이 무너진다. D6 와 같은 기준을 쓴다.
118
-
119
- | 파일 | 보여주는 것 |
120
- |---|---|
121
- | 260줄 이하 | 통째로. 어차피 짧다 |
122
- | 260줄 초과 | **구성(함수 목록)만.** 항목을 누르면 그 부분만 펼친다 |
123
-
124
- 문법 강조는 직접 구현했다(`web/highlight.js`). 완전한 파서가 아니라 토큰 근사지만
125
- 눈이 구조를 잡는 데는 충분하다. 줄 번호는 원본 기준이라 큰 파일의 일부만 봐도 찾아갈 수 있다.
126
-
127
- ## 그래프는 3D 다
128
-
129
- | 조작 | 동작 |
130
- |---|---|
131
- | 클릭 | 그 노드를 선택 + **이해의 시작점**으로 지정 |
132
- | 왼쪽 드래그 | 빈 곳이면 화면 이동, 노드 위면 그 노드 끌기 |
133
- | **오른쪽 드래그** | 전체 회전 (yaw / pitch) |
134
- | 오른쪽 더블클릭 | 정면으로 복귀 |
135
- | 휠 | 확대 |
136
-
137
- 노드 끌기는 회전을 역변환해 **카메라 평면 위에서** 움직인다.
138
- 그래서 어느 각도로 돌려놓아도 마우스를 따라온다.
139
-
140
- ### 시각 채널을 의미별로 나눴다
141
-
142
- 3D 로 가면 원근감도 보통 크기·투명도로 표현하는데, 투명도는 이미
143
- "시작점에서의 그래프 거리"(D8)가 쓰고 있다. **같은 채널을 두 의미가 다투면 둘 다 못 읽는다.**
144
-
145
- | 채널 | 뜻 |
146
- |---|---|
147
- | 투명도 | 시작점에서의 **그래프 거리** (의미) |
148
- | 크기 · 가림 | 카메라에서의 **화면 거리** (원근) |
149
-
150
- 안개(거리에 따른 흐림)는 최대 25% 만 건다. 더 걸면 "멀리 있는 것"과 "관련 없는 것"이 섞인다.
151
- 노드는 먼 것부터 그려 앞의 것이 뒤를 가리게 한다 — 가림이 원근의 두 번째 단서다.
152
-
153
- ## 이해 모드의 표시
154
-
155
- - **진하기** = 시작점으로부터의 거리 (D8). 검정 바탕이라 색상이 아닌 **밝기**로 표현한다
156
- - 색은 의미가 있을 때만 쓴다 — 감시 모드의 세 상태, 그리고 문법 강조
157
-
158
- ## 콘솔에서 만지기
159
-
160
- 프로토타입이라 상태를 열어뒀다.
161
-
162
- ```js
163
- axmap.view.yaw = 0 // 정면으로
164
- axmap.view.maxDepth = 2 // 요약 깊이 한계 바꾸기
165
- axmap.S.live.summary // 지금 선점/수정 요약
166
- ```
167
- - **빨간 테두리 원** = 300줄 초과. 파일 단위로 보면 안 되고 함수로 펼쳐야 하는 노드 (D6)
168
- - **점선 회색 엣지** = 허브(전역 신호) 경유. 거리는 1이지만 의미는 멀다 (D9)
169
- - **허브 배지** = 좌측 요약에 `/bbiyong/estop` 같은 전역 신호 목록
170
-
171
- ## 로컬 LLM (선택)
172
-
173
- 없어도 전부 동작한다. 요약만 비활성화된다.
174
-
175
- ```bash
176
- winget install --id Ollama.Ollama -e --scope user
177
- ollama pull qwen2.5-coder:7b
178
- node app/server.mjs <경로> # 모델을 자동으로 찾아 미리 올린다
179
- ```
180
-
181
- RTX 5060 Laptop(VRAM 8GB) 기준 **7~8B 가 적정**이다. 27B 는 VRAM 을 넘겨 RAM 으로 밀려난다.
182
- 은행 서버 얘기와 노트북 얘기는 분리해야 한다.
183
- 9.7B(`qwen3.5:9b`, 6.6GB)는 올라가긴 하지만 13% 가 CPU 로 밀리고, **품질도 더 낫지 않다**
184
- — [아래 실측](#모델을-바꾸면-나아지는가--qwen359b-실측).
185
-
186
- 환경변수: `AXMAP_MODEL` (기본 `qwen2.5-coder:7b`), `OLLAMA_HOST`, `AXMAP_KEEP_ALIVE` (기본 `30m`).
187
-
188
- ### 실측 — RTX 5060 Laptop 8GB, qwen2.5-coder:7b (Q4_K_M), 100% GPU
189
-
190
- | 파일 | 크기 | 모델에 넣은 것 | 첫 글자 | 전체 |
191
- |---|---|---|---|---|
192
- | `cmd_mux_node.py` | 130줄 | 전체 | 0.5초 | 2.5초 |
193
- | `manual_drive_bridge.py` | 222줄 | 전체 | 0.7초 | 3.2초 |
194
- | `escape_recovery.py` | 801줄 | 앞 130줄 + 구성 | 1.9초 | 5.5초 |
195
- | 캐시 적중 | — | — | — | **4~32ms** |
196
-
197
- 생성 속도는 41 tok/s 로 일정하다. **변하는 것은 프롬프트 평가 시간**이므로
198
- 병목은 "얼마나 쓰느냐"가 아니라 **"얼마나 읽히느냐"**다.
199
-
200
- ### 그래서 이렇게 했다
201
-
202
- - **스트리밍** — 다 끝날 때까지 빈 화면을 보여주면 실제 속도와 무관하게 느리게 느껴진다.
203
- 첫 글자가 0.5~1.9초에 나오는 것이 전체가 5초에 끝나는 것보다 체감에 크게 작용한다.
204
- - **모델을 미리 올린다** — 콜드 스타트는 **44초**다(4.7GB 적재, 실측).
205
- 서버가 뜰 때 한 번 부르고 `keep_alive: 30m` 으로 붙잡는다. 기본값 5분이면
206
- 잠깐 딴짓하고 돌아올 때마다 44초를 다시 기다린다.
207
- - **파일 크기에 따라 넣는 것을 바꾼다** — 고정 줄 수로 자르면 정확도를 잃는다.
208
- 222줄 파일을 160줄로 자르자 뒤쪽 62줄에 있던 채널 두 개(`/bbiyong/manual_drive_status`,
209
- `/tmp/orincar_drive.json`)를 통째로 놓쳤다. 260줄 이하는 전체를, 그 이상은
210
- 앞 130줄 + 함수 목록을 준다. D6(적응형 노드)와 같은 원리다.
211
- - **content hash 캐시** — `app/.cache/`. 파일이 안 바뀌면 다시 돌리지 않는다.
212
-
213
- ### prefetch
214
-
215
- 노드를 클릭하면 그 요약이 끝난 뒤 **깊이 1~3 을 미리 만든다.** 깊이 순서대로 하므로
216
- 가장 클릭할 만한 것부터 준비된다. 다른 노드를 선택하면 즉시 취소한다.
217
-
218
- e101 의 `cmd_mux_node.py` 기준 깊이 1~3 은 **40개**(7 + 15 + 18)라
219
- 전부 준비되는 데 약 2분 걸린다. 상단에 `주변 요약 4/40` 으로 진행이 보인다.
220
-
221
- ### 검증 가능한 출력
222
-
223
- LLM 이 말한 채널 이름은 **파일에 그 문자열이 실제로 있는지 대조해서** `verified` 를 붙인다.
224
- 작은 모델을 쓸 수 있는 이유가 이것이다 — 환각이 기계적으로 걸러진다.
225
-
226
- 다만 정적 파싱과의 역할 분담은 처음 생각보다 미묘하다.
227
- 따옴표 안의 `/경로` 를 전부 긁는 정규식은 파라미터 기본값도 잡으므로 **연결 자체는 놓치지 않는다.**
228
- LLM 이 더해주는 것은 **방향**(발행인지 구독인지)과 **문서에만 적힌 흐름**이다.
229
- 데이터 흐름을 이해하려면 방향이 있어야 하므로 여전히 필요하지만,
230
- "정적 파싱은 못 보고 LLM 만 본다" 는 과장이다.
231
-
232
- ### 출력 길이는 문법으로 묶는다
233
-
234
- `format:'json'` 은 문법만 강제하고 **길이는 강제하지 못한다.** 채널 10개짜리 파일에
235
- 같은 채널을 방향만 바꿔가며 97줄 반복하다 `num_predict` 에 잘려 죽는 일이
236
- 40파일 중 1~2건씩 났다. 잘린 JSON 은 파싱에 실패하고 **그 파일의 엣지가 통째로 사라진다.**
237
- 하필 채널이 가장 많은 파일에서 터지므로 손실이 가장 큰 곳에서 조용히 진다.
238
-
239
- temperature 를 0.4 까지 올려도 재현된다 — 확률로는 못 막는다.
240
- `format` 에 JSON Schema 를 주고 `maxItems` 를 걸면 문법이 배열을 닫도록 강제하므로
241
- 이 실패가 원천적으로 사라진다. 최악 비용도 34초에서 8초로 묶인다.
242
- `verified` 가 환각을 문자열 대조로 막는 것과 같은 수법을 길이에 적용한 것이다.
243
-
244
- 문법이 배열 길이는 묶어도 **같은 채널을 두 번 쓰는 것은 막지 못하므로** 중복은 따로 제거한다.
245
- 제거하지 않으면 화면에 같은 선을 겹쳐 그리고, 세는 쪽에서는 개수를 부풀린다.
246
-
247
- ### 모델을 바꾸면 나아지는가 — qwen3.5:9b 실측
248
-
249
- **결론: 안 나아진다.** `qwen2.5-coder:7b` 를 유지한다. (2026-08-16 측정)
250
-
251
- `app/eval/edges.mjs` 로 40파일 × 5회를 설정별로 재고 정확 순열검정(252조합 전수)으로 판정했다.
252
-
253
- | 설정 | 부가가치 | 커버리지 | 정밀도 | 방향오류율 | ms중앙 |
254
- |---|---|---|---|---|---|
255
- | **qwen2.5-coder:7b** (temp 0.1) | **109.0±5.8** | **82.8%** | 72.7% | **3.00%** | 1977 |
256
- | qwen3.5:9b (temp 1.0 기본) | 86.2±2.2 | 76.1% | **90.0%** | 5.50% | 1774 |
257
- | qwen3.5:9b (temp 0.1 통제) | 83.4±1.7 | 75.6% | 83.6% | 4.08% | 1645 |
258
-
259
- 부가가치 p=0.008 — 252조합에서 나올 수 있는 최솟값이고 두 집단의 범위가 겹치지 않는다.
260
- 속도는 차이 없다(p=0.238~0.365).
261
-
262
- **왜 더 큰 모델이 지는가**
263
-
264
- - **3.5 는 사고를 켤 수 없다.** 문법을 걸면 첫 토큰부터 JSON 만 허용되어 사고할 자리가 없고
265
- 답이 통째로 `thinking` 필드로 가서 `response` 가 빈 문자열이 된다. 문법을 풀면 사고가
266
- 끝나지 않는다 — 컨텍스트 16384 · 예산 8192 로도 31,540자를 쓰고 287초 뒤 답 없이 잘렸다.
267
- 둘 사이에 안전지대가 없다. `think:false` 는 3.5 를 불리하게 만든 조치가 아니라
268
- **답이 나오는 유일한 설정**이다.
269
- - **온도 탓이 아니다.** 0.1 로 맞추자 오히려 내려갔다(86.2 → 83.4). 다만 방향오류율은
270
- 5.50% → 4.08% 로 내려가 두 모델이 대등해진다(충돌 p=0.556).
271
- - **애초에 동급이 아니다.** `qwen3.5:9b` 는 9.7B 중 **27블록이 vision tower** 다.
272
- 파이썬 채널 문자열을 읽는 데 한 글자도 안 쓰인다. `qwen2.5-coder:7b` 는 7.6B 전부가
273
- 코드 사전학습이다.
274
- - **`verified` 필터가 정밀도의 값을 깎는다.** 3.5 는 확실할 때만 말한다(정밀도 90% vs 72.7%).
275
- 좋은 성질이지만 여기서는 틀린 주장의 비용이 0이라, 신중함이 커버리지 손실로만 나타난다.
276
-
277
- 일반 벤치마크에서 3.5 가 앞선다는 것과 모순되지 않는다. 여기서 재는 것은 추론력이 아니라
278
- "ROS 파이썬에서 토픽 문자열과 pub/sub 방향 뽑기" 이고, 3.5 의 두 주무기가
279
- 각각 봉쇄되거나 무가치화된다.
280
-
281
- **미검증 변수 하나** — 프롬프트는 소형 모델에 맞춰 코드를 먼저 주고 지시를 뒤에 붙인 구조다
282
- (`llm.mjs` 의 `summaryPrompt` 주석). 3.5 에 맞춰 다시 쓰면 결과가 달라질 수 있다.
283
-
284
- ### 모델 비교 하네스
285
-
286
- ```bash
287
- node app/eval/edges.mjs <대상저장소> [--files N] # 기본 20파일
288
- AXMAP_MODEL=<다른모델> node app/eval/edges.mjs <같은저장소>
289
- node app/eval/edges.mjs --compare
290
- ```
291
-
292
- 정답 세트가 없어도 판정이 된다. 서로 독립된 기준이 이미 두 개 있기 때문이다 —
293
- `verified`(문자열 대조)와 `channelsOf`(정적 추출). 재는 것은 "모델이 똑똑한가"가 아니라
294
- **정적 파싱 위에 모델이 무엇을 더 얹는가**다. 대상은 채널이 실제로 배선된 저장소여야 한다.
295
-
296
- 읽는 순서는 **`형식위반`이 0인지 먼저**다. 0이 아니면 품질이 아니라 배관 문제를 보고 있는 것이다.
297
-
298
- 그리고 **확률적 모델은 1회 측정이 무의미하다.** 실제로 20파일 × 2회에서는 3.5 가 21%
299
- 앞서는 것처럼 보였고, 40파일 × 5회로 늘리자 부호가 반대로 나왔다.
300
-
301
- ---
302
-
303
- ## 지금 되는 것 / 안 되는 것
304
-
305
- | | 상태 |
306
- |---|---|
307
- | 디렉터리 트리 · 필터 | ✅ |
308
- | 토픽/import 그래프, 허브 분리 | ✅ |
309
- | 시작점 기준 깊이 명암 | ✅ |
310
- | 300줄 초과 파일의 함수 목록 | ✅ (정규식 추출 — 진짜 파서 아님) |
311
- | 실시간 감시 (선점 · 수정 · 선언 밖) | ✅ |
312
- | 터미널 | ✅ (pty 아님 — 아래 참조) |
313
- | LLM 요약 (스트리밍) · 캐시 · 워밍업 | ✅ (Ollama 있을 때) |
314
- | 깊이 1~3 prefetch · 취소 | ✅ |
315
- | LLM 엣지 독해 + 문자열 검증 | ✅ API 만, 화면 미연결 |
316
- | 결합도 추세 경고 | ⬜ API(`/api/coupling`)만 있음 |
317
-
318
- ### 터미널로 실제로 되는 것 / 안 되는 것
319
-
320
- **된다.** 대상 저장소를 작업 디렉터리로 PowerShell 이 진짜로 돈다. 확인한 것:
321
-
322
- ```
323
- $ git status --short 진짜 변경 목록 (빨간 D 까지 색으로)
324
- $ python -c "print('한글')" 한글 정상 출력
325
- $ Get-ChildItem ros2_ws/src 실제 디렉터리 목록
326
- $ Get-Location C:\...\BE_robot
327
- ```
328
-
329
- ANSI 색은 직접 파싱해 그린다. 진행 막대(`\r`)도 줄을 다시 그리는 방식으로 처리한다.
330
- 윈도우 콘솔이 기본 CP949 라 한글이 깨지므로 `[Console]::OutputEncoding` 과
331
- `PYTHONIOENCODING` 을 UTF-8 로 잡아 넣는다.
332
-
333
- **안 된다.** `spawn` 이지 pty 가 아니다.
334
-
335
- - 대화형 TUI (`vim`, `htop`, `less`) — 화면 제어 시퀀스를 흉내내지 않는다
336
- - 입력이 필요한 명령 (`ssh` 비밀번호, `git rebase -i`) — stdin 이 연결돼 있지 않다
337
- - `Ctrl+C` 로 중단 — 브라우저 탭을 닫으면 프로세스는 죽는다
338
- - 명령 사이에 상태가 남지 않는다 (`cd` 후 다음 명령은 다시 원래 위치)
339
-
340
- 필요해지면 `node-pty` + `xterm.js` 로 교체하면 되고 화면 구조는 그대로다.
341
-
342
- ### 그 밖의 한계
343
- - **함수 목록은 정규식이다.** `def`/`class` 를 줄 단위로 찾는다. 데코레이터·중첩·멀티라인
344
- 시그니처에서 틀린다. 화면에 "정규식 추출"이라고 적어 확신을 속이지 않는다.
345
- - **백업 디렉터리를 자동 제외한다.** `backup|snapshot|.bak|-old|archive` 가 경로에 있으면 건너뛴다.
346
- e101 의 `orin-live-backup/` 처럼 같은 파일이 수십 벌 있으면 그래프가 복제본으로 뒤덮인다.
347
- - **`.gitignore` 가 "선언 밖 수정"으로 잡힌다.** `axmap init` 자신이 고치기 때문이다. 오탐이다.
348
-
349
- ## 왜 이 스택인가
350
-
351
- - **웹 UI** — 그래프도 터미널도 웹 라이브러리가 압도적이다. 네이티브로 가면 둘 다 직접 만들어야 한다.
352
- - **Electron 셸** — 배포는 `desktop/` 이 맡는다 (D16). 이 웹 UI 를 그대로 싣기 때문에
353
- 프로토타입에서 버려지는 것이 없다. 브라우저로 여는 길도 그대로 산다 —
354
- `node app/server.mjs` 는 죽지 않는다.
355
- - **번들러 없음** — 브라우저 ES 모듈만 쓴다. 빌드 단계가 없어 저장하고 새로고침하면 끝이다.
356
- - **코어 외부 의존성 0** — 힘 기반 그래프도 직접 그린다. 이유는 **폐쇄망에서 npm 이 안 되기
357
- 때문이 아니다.** 앱은 번들되어 나가므로 설치본에는 의존성이 몇 개든 상관없다. 진짜 이유는 둘이다.
358
- - **감사 표면.** 이 도구는 누가 어떤 코드를 커밋할 수 있는지를 판정하고 git 자격증명을 들고
359
- 실행된다. `src/protocol.mjs` 가 도는 프로세스에 검토 안 된 코드가 같이 올라가면
360
- 도구가 지키려는 성질을 도구 자신이 깬다.
361
- - **승인 절차.** 폐쇄망 조직은 서드파티 패키지마다 검토·화이트리스트를 요구한다.
362
- 막히는 것은 네트워크가 아니라 결재다.
363
-
364
- 여기에 더해, 시각 인코딩이 전부 이 프로젝트 고유라 범용 라이브러리를 써도 대부분을 덮어써야 한다.
365
- 의존성을 쓰는 자리는 `desktop/` 이고, 경계는 **import 방향 하나**로 지킨다 —
366
- `desktop/` → 코어는 되고 반대는 안 된다.