axmap-cli 0.0.1
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/.claude/commands/ax-done.md +10 -0
- package/.claude/commands/ax-setup.md +31 -0
- package/.claude/commands/ax-start.md +15 -0
- package/.claude/commands/ax-tell.md +14 -0
- package/.claude/commands/ax-update.md +19 -0
- package/.claude/commands/ax.md +13 -0
- package/CLAUDE.md +309 -0
- package/LICENSE +20 -0
- package/README.md +207 -0
- package/app/README.md +366 -0
- package/app/eval/edges.mjs +242 -0
- package/app/lib/adjacent.mjs +125 -0
- package/app/lib/agentcli.mjs +153 -0
- package/app/lib/analyze.mjs +1159 -0
- package/app/lib/cochange.mjs +421 -0
- package/app/lib/datanodes.mjs +127 -0
- package/app/lib/entry.mjs +192 -0
- package/app/lib/featuregraph.mjs +389 -0
- package/app/lib/features.mjs +645 -0
- package/app/lib/fetchrepo-run.mjs +37 -0
- package/app/lib/fetchrepo.mjs +164 -0
- package/app/lib/flow.mjs +1089 -0
- package/app/lib/ladder.mjs +387 -0
- package/app/lib/langs.mjs +630 -0
- package/app/lib/live.mjs +346 -0
- package/app/lib/llm.mjs +594 -0
- package/app/lib/newfile.mjs +126 -0
- package/app/lib/prdiff.mjs +651 -0
- package/app/lib/reveal.mjs +316 -0
- package/app/lib/roots.mjs +186 -0
- package/app/lib/scope.mjs +342 -0
- package/app/lib/session.mjs +389 -0
- package/app/lib/slots.mjs +233 -0
- package/app/lib/ssot.mjs +277 -0
- package/app/lib/teamview.mjs +962 -0
- package/app/lib/terms.ko.mjs +169 -0
- package/app/server.mjs +1959 -0
- package/app/web/shell.css +538 -0
- package/app/web/shell.html +197 -0
- package/app/web/shell.js +638 -0
- package/app/web/stage.js +347 -0
- package/app/web/words.js +85 -0
- package/bin/axmap.mjs +1918 -0
- package/governance/GOVERNANCE.md +433 -0
- package/governance/gate.mjs +526 -0
- package/governance/vote.mjs +501 -0
- package/mcp/README.md +254 -0
- package/mcp/SETUP-FOR-AI.md +186 -0
- package/mcp/install.ps1 +341 -0
- package/mcp/install.sh +339 -0
- package/mcp/server.mjs +969 -0
- package/package.json +48 -0
- package/src/closure.mjs +343 -0
- package/src/governance.mjs +839 -0
- package/src/invariants.mjs +226 -0
- package/src/mrtarget.mjs +284 -0
- package/src/promote.mjs +177 -0
- package/src/protocol.mjs +423 -0
- package/src/repotarget.mjs +81 -0
- package/src/update.mjs +177 -0
- package/src/version.mjs +186 -0
- package/tools/bus.mjs +520 -0
- package/tools/cluster-experiment.mjs +256 -0
- package/tools/cluster-sweep.mjs +226 -0
- package/tools/make-icon.mjs +108 -0
- package/tools/mcp-register.mjs +269 -0
- package/tools/mr-target.mjs +49 -0
- package/tools/persona-bench.mjs +362 -0
- package/tools/pick-repo.mjs +229 -0
- package/tools/promote.mjs +550 -0
- package/tools/reveal-demo.mjs +158 -0
- package/tools/run-tests.mjs +42 -0
- package/tools/setup.mjs +490 -0
- package/tools/shortcut.mjs +121 -0
- package/tools/smoke.mjs +166 -0
- package/tools/topicgraph.py +154 -0
- package/tools/vendor.mjs +382 -0
- package/tools/version.mjs +115 -0
package/app/web/stage.js
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 가운데 칸 — `ladder()` 가 내는 것을 번호 카드로 그린다.
|
|
3
|
+
*
|
|
4
|
+
* 🔴 새 시각화를 만드는 것이 아니다. **이미 이긴 텍스트를 다시 조판하는 것**이다.
|
|
5
|
+
*
|
|
6
|
+
* 벤치 3회차는 다섯 조건 전부에서 7/7 이었다 — 그림 없이 글자만 있어도 답은
|
|
7
|
+
* 같았다. 그런데 `answers-graph.md` 가 이렇게도 적었다:
|
|
8
|
+
*
|
|
9
|
+
* "걸음 ②에서 노드 2 / 엣지 0, 걸음 ③에서 노드 8 / 엣지 3 — 화면의 3분의 2가
|
|
10
|
+
* 검은 여백인 채로, 정작 그 걸음의 답은 전부 왼쪽 좁은 패널에 글자로 쏟아졌다"
|
|
11
|
+
*
|
|
12
|
+
* 그건 렌더링 문제가 아니라 **데이터 공급 문제**다. 예쁜 캔버스를 만들어도 거기에
|
|
13
|
+
* 노드 2개만 흘려보내면 똑같이 빈다. 그래서 이 파일이 하는 일은 하나다 —
|
|
14
|
+
* 왼쪽 패널이 글자로 쏟아내던 것을 가운데 큰 자리로 옮긴다.
|
|
15
|
+
* 자세한 근거는 `docs/RESEARCH-FLOWCANVAS.md` 4.1.
|
|
16
|
+
*
|
|
17
|
+
* ── 규율 (Zap 편집기에서 가져온 것) ──────────────────────────────────────
|
|
18
|
+
*
|
|
19
|
+
* 번호가 붙는다 그림을 안 봐도 "2.5 를 보세요" 라고 말할 수 있다
|
|
20
|
+
* 카드 폭이 같다 읽는 눈이 좌우로 흔들리지 않는다
|
|
21
|
+
* 분기는 버스 하나 자식마다 선을 뽑으면 "각각의 관계" 로 읽힌다.
|
|
22
|
+
* 한 줄기에서 갈라져야 "여기서 한 번 갈린다" 가 된다
|
|
23
|
+
* 선은 조용하다 주인공은 카드의 글자다. 실측한 Zapier 연결선은
|
|
24
|
+
* 바탕 대비 4.4:1 밖에 안 된다
|
|
25
|
+
* 아무것도 안 움직인다 물리도 rAF 도 없다. 온보딩 실험 네 회차가 보고한
|
|
26
|
+
* "찍고 누르는 사이에 대상이 이동" 이 구조적으로 사라진다
|
|
27
|
+
*
|
|
28
|
+
* 🔴 **좌표를 우리가 계산하지 않는다.** 카드는 평범한 DOM 이고 배치는 CSS 가
|
|
29
|
+
* 한다. 선만 그릴 때 실제로 놓인 자리를 읽어서(`getBoundingClientRect`) 잇는다.
|
|
30
|
+
* 손으로 좌표를 짜기 시작하면 창 크기가 바뀔 때마다 어긋난다.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const $ = (s) => document.querySelector(s)
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 한 화면에 그리는 카드의 상한.
|
|
37
|
+
*
|
|
38
|
+
* Deutsch limit — 화면에 시각 요소를 50개 넘게 두면 사람이 못 읽는다 — 은
|
|
39
|
+
* Zapier 에게는 제약이 아니다. 워크플로가 원래 30단계 미만이니까.
|
|
40
|
+
* 우리는 저장소 하나가 2,000개를 넘는다. "전부 보여주고 사용자가 줄이게 한다" 는
|
|
41
|
+
* D14 가 이미 버린 대안이므로, 넘치면 **넘쳤다고 말하고 자른다.**
|
|
42
|
+
*/
|
|
43
|
+
const MAX_CARDS = 60
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 화면에 나가는 말은 전부 여기서 온다.
|
|
47
|
+
*
|
|
48
|
+
* 🔴 문자열을 이 파일에 직접 쓰지 않는다. 어휘를 바꾸려면 그리기 코드를 읽어야
|
|
49
|
+
* 하는 상태가 되면 `docs/WORDS.md` 의 판정 규칙("벤치에서 막힌 것만 바꾼다")을
|
|
50
|
+
* 한 회차도 못 돌린다. 바꾸는 비용이 싸야 재서 정할 수 있다.
|
|
51
|
+
*/
|
|
52
|
+
import { W } from './words.js'
|
|
53
|
+
|
|
54
|
+
/** 부모 카드 아래에서 버스까지 내려가는 길이(px). CSS 의 층 간격과 맞춰 둔다. */
|
|
55
|
+
const STUB = 18
|
|
56
|
+
|
|
57
|
+
const el = (tag, cls, text) => {
|
|
58
|
+
const n = document.createElement(tag)
|
|
59
|
+
if (cls) n.className = cls
|
|
60
|
+
if (text != null) n.textContent = text
|
|
61
|
+
return n
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const svgEl = (tag) => document.createElementNS('http://www.w3.org/2000/svg', tag)
|
|
65
|
+
|
|
66
|
+
/** 마지막으로 그린 결과. 창 크기가 바뀌면 선만 다시 긋는다. */
|
|
67
|
+
let drawn = null
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 지금 보고 있는 하위 프로젝트. `''` 면 저장소 전체다.
|
|
71
|
+
*
|
|
72
|
+
* 🔴 모노레포에서 "어디서부터 읽나" 의 **첫 답은 파일이 아니라 프로젝트**다.
|
|
73
|
+
* 실측(SSAFY S15P11E101): 저장소 전체로 겹을 만들면 549개 중 14개만 닿았다.
|
|
74
|
+
* `FE/bbiyong-react` 하나로 좁히면 150개 중 93개가 5겹으로 이어진다.
|
|
75
|
+
* 자세한 근거는 `app/lib/roots.mjs` 머리말.
|
|
76
|
+
*/
|
|
77
|
+
let pick = null
|
|
78
|
+
const REMEMBER = 'axmap.root'
|
|
79
|
+
|
|
80
|
+
// ── 그리기 ──────────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 프로젝트 고르는 줄. 하나뿐이면 그리지 않는다 —
|
|
84
|
+
* 고를 것이 없는데 고르라고 하면 걸음만 하나 는다.
|
|
85
|
+
*/
|
|
86
|
+
function projectBar(info) {
|
|
87
|
+
if (!info?.multi) return null
|
|
88
|
+
const bar = el('div', 'projs')
|
|
89
|
+
for (const r of info.roots) {
|
|
90
|
+
const b = el('button', `proj${r.dir === pick ? ' on' : ''}`)
|
|
91
|
+
// 저장소 루트 자체가 프로젝트일 수 있다. 빈 문자열을 그대로 그리면 이름이 없다.
|
|
92
|
+
b.append(el('span', 'pn', r.dir || W.allRoots))
|
|
93
|
+
b.append(el('span', 'pf', String(r.files)))
|
|
94
|
+
// 🔴 선언된 것과 우리가 짐작한 것을 구분해 보여준다 (D5).
|
|
95
|
+
b.title = r.manifest ? W.declaredBy(r.manifest) : W.guessedDir
|
|
96
|
+
if (!r.manifest) b.append(el('span', 'guess', W.guess))
|
|
97
|
+
b.onclick = () => {
|
|
98
|
+
pick = r.dir
|
|
99
|
+
try { localStorage.setItem(REMEMBER, pick) } catch { /* 그만 */ }
|
|
100
|
+
drawStage()
|
|
101
|
+
}
|
|
102
|
+
bar.append(b)
|
|
103
|
+
}
|
|
104
|
+
if (info.outside) {
|
|
105
|
+
bar.append(el('span', 'pout', W.outside(info.outside)))
|
|
106
|
+
}
|
|
107
|
+
return bar
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function drawStage() {
|
|
111
|
+
const stage = $('#stage')
|
|
112
|
+
stage.replaceChildren()
|
|
113
|
+
stage.className = 'loading'
|
|
114
|
+
stage.textContent = W.loading
|
|
115
|
+
|
|
116
|
+
let info = null
|
|
117
|
+
try { info = await (await fetch('/api/roots')).json() } catch { /* 없으면 전체로 간다 */ }
|
|
118
|
+
|
|
119
|
+
if (info?.multi && pick === null) {
|
|
120
|
+
let saved = null
|
|
121
|
+
try { saved = localStorage.getItem(REMEMBER) } catch { /* 그만 */ }
|
|
122
|
+
// 기억해 둔 것이 아직 있으면 그것, 없으면 **가장 큰 프로젝트**로 시작한다.
|
|
123
|
+
pick = info.roots.some((r) => r.dir === saved) ? saved : (info.roots[0]?.dir ?? '')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let d
|
|
127
|
+
try {
|
|
128
|
+
const q = pick ? `?root=${encodeURIComponent(pick)}` : ''
|
|
129
|
+
const r = await fetch(`/api/ladder${q}`)
|
|
130
|
+
d = await r.json()
|
|
131
|
+
if (!r.ok) throw new Error(d.error ?? `HTTP ${r.status}`)
|
|
132
|
+
} catch (e) {
|
|
133
|
+
stage.className = 'note'
|
|
134
|
+
stage.textContent = W.readFailed(e.message)
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 🔴 조용히 빈 화면을 내지 않는다.
|
|
140
|
+
* `ladder()` 는 시작점을 못 찾으면 `why` 에 이유를 담아 준다. 그것을 삼키면
|
|
141
|
+
* 사용자는 "저장소가 비었나" 로 잘못 읽는다 (fail-closed, CLAUDE.md).
|
|
142
|
+
*/
|
|
143
|
+
if (d.why) {
|
|
144
|
+
stage.className = 'note'
|
|
145
|
+
stage.textContent = d.why
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
if (!d.layers?.length) {
|
|
149
|
+
stage.className = 'note'
|
|
150
|
+
stage.textContent = W.noLayers
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 🔴 "읽는 중…" 을 지운다. `append` 는 있던 것 뒤에 붙이므로 안 지우면 남는다.
|
|
155
|
+
stage.replaceChildren()
|
|
156
|
+
stage.className = ''
|
|
157
|
+
const wires = svgEl('svg')
|
|
158
|
+
wires.setAttribute('class', 'wires')
|
|
159
|
+
stage.append(wires)
|
|
160
|
+
|
|
161
|
+
const board = el('div', 'board')
|
|
162
|
+
const bar = projectBar(info)
|
|
163
|
+
if (bar) board.append(bar)
|
|
164
|
+
stage.append(board)
|
|
165
|
+
|
|
166
|
+
/** path → 그 카드의 DOM. 선을 그을 때 부모를 찾는 데 쓴다. */
|
|
167
|
+
const byPath = new Map()
|
|
168
|
+
let cards = 0
|
|
169
|
+
let cut = 0
|
|
170
|
+
|
|
171
|
+
for (const layer of d.layers) {
|
|
172
|
+
const row = el('div', 'layer')
|
|
173
|
+
|
|
174
|
+
const head = el('div', 'lhead')
|
|
175
|
+
head.append(el('span', 'lno', W.layerNo(layer.depth)))
|
|
176
|
+
head.append(el('span', 'lcount', W.layerCount(layer.count)))
|
|
177
|
+
row.append(head)
|
|
178
|
+
|
|
179
|
+
const cols = el('div', 'cols')
|
|
180
|
+
row.append(cols)
|
|
181
|
+
|
|
182
|
+
for (const g of layer.groups) {
|
|
183
|
+
const col = el('div', 'grp')
|
|
184
|
+
// 묶음 이름 = 디렉터리. Zap 편집기가 가지 머리에 조건 칩을 두는 자리다.
|
|
185
|
+
const chip = el('div', 'chip', g.dir || W.topDir)
|
|
186
|
+
chip.append(el('span', 'cn', String(g.count)))
|
|
187
|
+
col.append(chip)
|
|
188
|
+
|
|
189
|
+
for (const f of g.files) {
|
|
190
|
+
if (cards >= MAX_CARDS) { cut++; continue }
|
|
191
|
+
const card = card_(f, layer.depth, cards)
|
|
192
|
+
byPath.set(f.path, card)
|
|
193
|
+
col.append(card)
|
|
194
|
+
cards++
|
|
195
|
+
}
|
|
196
|
+
if (g.truncated) col.append(el('div', 'more', W.moreFiles(g.truncated)))
|
|
197
|
+
cols.append(col)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (layer.moreGroups) cols.append(el('div', 'more', W.moreGroups(layer.moreGroups)))
|
|
201
|
+
board.append(row)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const foot = el('div', 'foot2')
|
|
205
|
+
foot.append(el('span', null, W.reach(d.reached, d.total, d.maxDepth)))
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 🔴 "1 / 213" 을 숫자만 내면 고장으로 읽힌다.
|
|
209
|
+
*
|
|
210
|
+
* 실측: BE_system 은 진입점을 정확히 찾았는데(`@SpringBootApplication`)
|
|
211
|
+
* 거기서 닿는 파일이 1개다. 고장이 아니라 **Spring 이 import 가 아니라
|
|
212
|
+
* 애노테이션으로 배선**하기 때문이다. D5 가 예고한 한계이고, 그 문서는
|
|
213
|
+
* "확신 있게 틀린 답이 가장 해롭다" 고 적었다 — 아무 말 없이 빈 화면을 내는
|
|
214
|
+
* 것이 정확히 그 실패다. 무엇을 못 보고 있는지 말한다.
|
|
215
|
+
*/
|
|
216
|
+
if (d.total >= 20 && d.reached / d.total < 0.25) {
|
|
217
|
+
foot.append(el('span', 'warn2', W.staticLimit))
|
|
218
|
+
}
|
|
219
|
+
for (const g of d.unreached?.by?.groups ?? []) {
|
|
220
|
+
foot.append(el('span', null, W.groupCount(g.label, g.count)))
|
|
221
|
+
}
|
|
222
|
+
// 🔴 자른 것은 자랐다고 말한다. 조용히 자르면 "이게 전부" 로 읽힌다.
|
|
223
|
+
if (cut) foot.append(el('span', 'warn2', W.cutCards(MAX_CARDS, cut)))
|
|
224
|
+
if (d.unreached?.count) foot.append(el('span', null, W.unreached(d.unreached.count)))
|
|
225
|
+
board.append(foot)
|
|
226
|
+
|
|
227
|
+
drawn = { wires, byPath, stage }
|
|
228
|
+
wire()
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 카드 두 줄.
|
|
233
|
+
* 줄 1 — 여는 파일 수 · 테스트/예제 표시
|
|
234
|
+
* 줄 2 — `겹.순번` + 파일 이름
|
|
235
|
+
*
|
|
236
|
+
* 🔴 `title` 을 반드시 붙인다. 벤치의 ladder 회차가 잘린 이름 때문에 `zoom` 을
|
|
237
|
+
* 썼다가 **"잘린 글자는 픽셀을 키운다고 되살아나지 않는다"** 고 적고, `title`
|
|
238
|
+
* 하나면 막힌 지점이 없었을 것이라고 스스로 진단했다. DOM 에서는 공짜다.
|
|
239
|
+
*/
|
|
240
|
+
function card_(f, depth, seq) {
|
|
241
|
+
const c = el('div', `card${f.aux ? ' aux' : ''}`)
|
|
242
|
+
c.title = f.path
|
|
243
|
+
|
|
244
|
+
const l1 = el('div', 'r1')
|
|
245
|
+
l1.append(el('span', 'opens', W.opens(f.opens)))
|
|
246
|
+
if (f.aux) l1.append(el('span', 'tag', W.aux))
|
|
247
|
+
if (f.lines) l1.append(el('span', 'lines', W.lines(f.lines)))
|
|
248
|
+
c.append(l1)
|
|
249
|
+
|
|
250
|
+
const l2 = el('div', 'r2')
|
|
251
|
+
// 번호가 이 카드의 정체다. 그림을 안 봐도 이 번호로 서로를 가리킬 수 있다.
|
|
252
|
+
l2.append(el('span', 'no', `${depth}.${seq + 1}`))
|
|
253
|
+
l2.append(el('span', 'nm', f.path.split('/').pop()))
|
|
254
|
+
c.append(l2)
|
|
255
|
+
|
|
256
|
+
const dir = f.path.split('/').slice(0, -1).join('/')
|
|
257
|
+
if (dir) c.append(el('div', 'dir', dir))
|
|
258
|
+
|
|
259
|
+
c.dataset.path = f.path
|
|
260
|
+
c.dataset.from = f.from ?? ''
|
|
261
|
+
return c
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── 선 ──────────────────────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* 부모 아래 → 가로 버스 한 줄 → 각 자식 위.
|
|
268
|
+
*
|
|
269
|
+
* 🔴 자식마다 부모에서 각각 선을 뽑으면 **세 개의 독립된 관계**로 보인다.
|
|
270
|
+
* 한 줄기에서 갈라지면 "여기서 한 번 갈린다" 로 읽힌다. Zapier 가 분기를
|
|
271
|
+
* 하나의 결정으로 읽히게 만드는 장치가 정확히 이것이고, 순수 기하라 공짜로 온다.
|
|
272
|
+
* 모서리는 둥글지 않다 — Zap 편집기가 직각이다.
|
|
273
|
+
*/
|
|
274
|
+
function busPath(px, py, kids) {
|
|
275
|
+
if (!kids.length) return ''
|
|
276
|
+
/**
|
|
277
|
+
* 🔴 버스는 부모 바로 아래가 아니라 **부모와 첫 자식 사이**에 둔다.
|
|
278
|
+
*
|
|
279
|
+
* Zapier 는 부모와 자식이 붙어 있어서 어디에 두든 같다. 우리는 겹 사이가
|
|
280
|
+
* 멀어서, 부모 바로 아래에 두면 거기서부터 자식까지 **긴 세로선이 다른 카드를
|
|
281
|
+
* 가로지른다.** 실제로 그렇게 그려졌다. 중간에 두면 낙하가 짧아지고
|
|
282
|
+
* "여기서 갈린다" 가 눈에 남는다.
|
|
283
|
+
*/
|
|
284
|
+
const top = Math.min(...kids.map((k) => k.y))
|
|
285
|
+
const bus = Math.max(py + STUB, (py + top) / 2)
|
|
286
|
+
const left = Math.min(px, ...kids.map((k) => k.x))
|
|
287
|
+
const right = Math.max(px, ...kids.map((k) => k.x))
|
|
288
|
+
let d = `M${px} ${py}V${bus}`
|
|
289
|
+
if (right - left > 0.5) d += `M${left} ${bus}H${right}`
|
|
290
|
+
for (const k of kids) d += `M${k.x} ${bus}V${k.y}`
|
|
291
|
+
return d
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* 놓인 자리를 읽어서 잇는다.
|
|
296
|
+
*
|
|
297
|
+
* 카드 배치는 CSS 가 했으므로 우리는 결과만 본다. 좌표를 직접 계산하면
|
|
298
|
+
* 창 크기·글꼴·줄바꿈이 바뀔 때마다 선이 어긋난다.
|
|
299
|
+
*/
|
|
300
|
+
function wire() {
|
|
301
|
+
if (!drawn) return
|
|
302
|
+
const { wires, byPath, stage } = drawn
|
|
303
|
+
const base = stage.getBoundingClientRect()
|
|
304
|
+
|
|
305
|
+
wires.setAttribute('width', String(stage.scrollWidth))
|
|
306
|
+
wires.setAttribute('height', String(stage.scrollHeight))
|
|
307
|
+
wires.setAttribute('viewBox', `0 0 ${stage.scrollWidth} ${stage.scrollHeight}`)
|
|
308
|
+
wires.replaceChildren()
|
|
309
|
+
|
|
310
|
+
const mid = (r) => r.left - base.left + stage.scrollLeft + r.width / 2
|
|
311
|
+
|
|
312
|
+
// 부모별로 자식을 모은다 — 그래야 버스를 공유할 수 있다
|
|
313
|
+
const kidsOf = new Map()
|
|
314
|
+
for (const card of byPath.values()) {
|
|
315
|
+
const from = card.dataset.from
|
|
316
|
+
if (!from || !byPath.has(from)) continue
|
|
317
|
+
if (!kidsOf.has(from)) kidsOf.set(from, [])
|
|
318
|
+
kidsOf.get(from).push(card)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
for (const [from, kids] of kidsOf) {
|
|
322
|
+
const p = byPath.get(from).getBoundingClientRect()
|
|
323
|
+
const px = mid(p)
|
|
324
|
+
const py = p.bottom - base.top + stage.scrollTop
|
|
325
|
+
const pts = kids.map((k) => {
|
|
326
|
+
const r = k.getBoundingClientRect()
|
|
327
|
+
return { x: mid(r), y: r.top - base.top + stage.scrollTop }
|
|
328
|
+
})
|
|
329
|
+
const path = svgEl('path')
|
|
330
|
+
path.setAttribute('class', 'w')
|
|
331
|
+
path.setAttribute('d', busPath(px, py, pts))
|
|
332
|
+
wires.append(path)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* 창이나 칸 크기가 바뀌면 선만 다시 긋는다.
|
|
338
|
+
*
|
|
339
|
+
* 🔴 `requestAnimationFrame` 을 걸지 않는다. 카드는 안 움직이므로 다시 그릴 일이
|
|
340
|
+
* 바뀔 때뿐이다. D16 이 "정지한 화면을 초당 60번 다시 칠하지 않는다" 로 정한
|
|
341
|
+
* 방향 그대로이고, DOM 이라 저절로 그렇게 된다.
|
|
342
|
+
*/
|
|
343
|
+
export function watchStage() {
|
|
344
|
+
const stage = $('#stage')
|
|
345
|
+
if (!stage || typeof ResizeObserver === 'undefined') return
|
|
346
|
+
new ResizeObserver(() => wire()).observe(stage)
|
|
347
|
+
}
|
package/app/web/words.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 사용자에게 보이는 말이 사는 곳 — 화면 문자열을 한 파일에 모은다.
|
|
3
|
+
*
|
|
4
|
+
* 🔴 이 파일이 있는 이유는 번역이 아니라 **어휘를 실험 가능하게 만드는 것**이다.
|
|
5
|
+
*
|
|
6
|
+
* `docs/WORDS.md` 가 정한 판정 규칙은 "비전공자 벤치에서 그 단어 때문에 막힌
|
|
7
|
+
* 기록이 있을 때만 바꾼다" 이다. 그러려면 **바꾸는 비용이 싸야** 한다.
|
|
8
|
+
* 문자열이 `stage.js` 안에 흩어져 있으면 한 번 바꿀 때마다 그리기 코드를 읽어야
|
|
9
|
+
* 하고, 그러면 어휘 실험을 한 회차도 못 돌린다. 여기 모으면 이 파일만 고친다.
|
|
10
|
+
*
|
|
11
|
+
* ── 🔴 지금은 글자를 한 자도 바꾸지 않았다 ───────────────────────────────
|
|
12
|
+
*
|
|
13
|
+
* 옮기기만 했다. 화면 출력이 **바이트 단위로 이전과 같다.** 일부러 그랬다 —
|
|
14
|
+
* `docs/BENCH.md` 의 3회차가 이 문장들을 보고 잰 결과이고, 여기서 말을 바꾸면
|
|
15
|
+
* 다음 회차가 **도구가 좋아져서 달라진 것인지 말이 달라져서인지 구분되지 않는다.**
|
|
16
|
+
* 벤치가 "글자 하나까지 같은 프롬프트" 를 요구하는 것과 같은 이유다.
|
|
17
|
+
*
|
|
18
|
+
* 바꾸는 순서는 언제나 이것이다.
|
|
19
|
+
*
|
|
20
|
+
* 비전공자 벤치를 돌린다 → 막힌 지점을 읽는다 → 그 항목만 고친다 → 다시 잰다
|
|
21
|
+
*
|
|
22
|
+
* ── 무엇을 여기 넣나 ─────────────────────────────────────────────────────
|
|
23
|
+
*
|
|
24
|
+
* 넣는다 사용자가 화면에서 읽는 문장·낱말
|
|
25
|
+
* 안 넣는다 코드의 이름(`entryCandidates` · `edges` · `cochange` · `membership`)
|
|
26
|
+
*
|
|
27
|
+
* 코드 어휘를 안 바꾸는 이유는 `docs/WORDS.md` 원칙 C 에 있다. 요약하면 —
|
|
28
|
+
* 논문·문서·API 와의 대응이 끊기고, 개발이 느려지고, 얻는 것이 없다.
|
|
29
|
+
* 사용자는 코드 이름을 안 본다.
|
|
30
|
+
*
|
|
31
|
+
* ⚠️ `🟡` 가 붙은 항목이 **전공자 어휘라서 벤치의 1순위 후보**인 것들이다.
|
|
32
|
+
* 표를 따로 만들지 않는다 — 목록을 다른 곳에 적으면 그 목록이 먼저 낡는다
|
|
33
|
+
* (CLAUDE.md §0.5). 표시는 문자열 바로 옆에 있어야 같이 움직인다.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
export const W = {
|
|
37
|
+
// ── 프로젝트 고르는 줄 ────────────────────────────────────────────────
|
|
38
|
+
/** 🟡 "저장소" — git 을 모르는 사람에게 통하지 않는다 */
|
|
39
|
+
allRoots: '(저장소 전체)',
|
|
40
|
+
/** 🟡 "매니페스트" — package.json·build.gradle 을 아는 사람만 읽는다 */
|
|
41
|
+
guessedDir: '매니페스트가 없어 디렉터리로 짐작한 것',
|
|
42
|
+
declaredBy: (manifest) => `${manifest} 가 선언한 프로젝트`,
|
|
43
|
+
guess: '추측',
|
|
44
|
+
outside: (n) => `어디에도 안 속한 파일 ${n}개`,
|
|
45
|
+
|
|
46
|
+
// ── 상태 ──────────────────────────────────────────────────────────────
|
|
47
|
+
loading: '읽는 중…',
|
|
48
|
+
readFailed: (msg) => `읽지 못했습니다 — ${msg}`,
|
|
49
|
+
/** 🟡 "겹" · "진입점" — 둘 다 전공자 어휘다. 게다가 이 문장은 실패 화면이라
|
|
50
|
+
* 비전공자가 제일 먼저 만나는 벽이 될 수 있다 */
|
|
51
|
+
noLayers: '겹을 만들지 못했습니다 — 진입점을 못 찾았을 수 있습니다.',
|
|
52
|
+
|
|
53
|
+
// ── 겹 머리 ───────────────────────────────────────────────────────────
|
|
54
|
+
/** 🟡 "겹" */
|
|
55
|
+
layerNo: (depth) => `${depth}겹`,
|
|
56
|
+
layerCount: (n) => `${n}개`,
|
|
57
|
+
topDir: '(최상위)',
|
|
58
|
+
moreFiles: (n) => `+${n}개 더`,
|
|
59
|
+
moreGroups: (n) => `+묶음 ${n}개 더`,
|
|
60
|
+
|
|
61
|
+
// ── 바닥 요약 ─────────────────────────────────────────────────────────
|
|
62
|
+
/** 🟡 "진입점" · "겹" */
|
|
63
|
+
reach: (reached, total, maxDepth) =>
|
|
64
|
+
`${reached} / ${total} 개가 진입점에서 닿습니다 · 최대 ${maxDepth}겹`,
|
|
65
|
+
/**
|
|
66
|
+
* 🟡 이 문장이 이 파일에서 **가장 어려운 말**이다 —
|
|
67
|
+
* "정적 import" · "애노테이션" · "의존성 주입" · "배선" 이 한 문장에 넷이다.
|
|
68
|
+
*
|
|
69
|
+
* 그런데 지우면 안 된다. 이 경고가 없으면 "1 / 213" 이 고장으로 읽히고,
|
|
70
|
+
* 그것이 `stage.js` 가 적어둔 대로 **확신 있게 틀린 답**이다.
|
|
71
|
+
* 고칠 방향은 삭제가 아니라 **원칙 A** — 용어를 몰라도 문장이 성립하게 쓰고
|
|
72
|
+
* 용어는 괄호로 뒤에 붙인다.
|
|
73
|
+
*/
|
|
74
|
+
staticLimit:
|
|
75
|
+
'정적 import 로 이어지는 것이 여기까지입니다 — 애노테이션·설정·의존성 주입으로 배선하는 프로젝트는 이렇게 보입니다',
|
|
76
|
+
groupCount: (label, n) => `${label} ${n}개`,
|
|
77
|
+
cutCards: (max, cut) => `화면에는 ${max}개까지만 그렸습니다 (${cut}개 생략)`,
|
|
78
|
+
unreached: (n) => `안 닿는 파일 ${n}개`,
|
|
79
|
+
|
|
80
|
+
// ── 카드 ──────────────────────────────────────────────────────────────
|
|
81
|
+
/** import 를 "여는" 으로 옮긴 자리. 이미 원칙 A 를 한 번 적용한 예다 */
|
|
82
|
+
opens: (n) => `여는 파일 ${n}`,
|
|
83
|
+
aux: '테스트·예제',
|
|
84
|
+
lines: (n) => `${n}줄`,
|
|
85
|
+
}
|