workq-mcp 0.1.0 → 0.1.2
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/README.md +53 -1
- package/bin/workq.mjs +174 -2
- package/lib/analyze-repo.mjs +1126 -0
- package/lib/natural-git.mjs +481 -0
- package/package.json +2 -1
|
@@ -0,0 +1,1126 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const IGNORE_DIRS = new Set([
|
|
5
|
+
".git",
|
|
6
|
+
".next",
|
|
7
|
+
".playwright-mcp",
|
|
8
|
+
".turbo",
|
|
9
|
+
".workq",
|
|
10
|
+
"build",
|
|
11
|
+
"coverage",
|
|
12
|
+
"dist",
|
|
13
|
+
"dist-e2e",
|
|
14
|
+
"node_modules",
|
|
15
|
+
"output",
|
|
16
|
+
"playwright-report",
|
|
17
|
+
"test-results",
|
|
18
|
+
"out"
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const TEXT_EXTENSIONS = new Set([
|
|
22
|
+
".css",
|
|
23
|
+
".html",
|
|
24
|
+
".js",
|
|
25
|
+
".json",
|
|
26
|
+
".jsx",
|
|
27
|
+
".md",
|
|
28
|
+
".mjs",
|
|
29
|
+
".sql",
|
|
30
|
+
".ts",
|
|
31
|
+
".tsx",
|
|
32
|
+
".txt",
|
|
33
|
+
".yml",
|
|
34
|
+
".yaml"
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const DOC_BASENAMES = new Set([
|
|
38
|
+
"README.md",
|
|
39
|
+
"FEATURES.md",
|
|
40
|
+
"USE_CASES.md",
|
|
41
|
+
"CLAUDE.md",
|
|
42
|
+
"CHANGELOG.md",
|
|
43
|
+
"TODOS.md",
|
|
44
|
+
"QA_CHECKLIST.md"
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
const SECRET_LINE = /(api[_-]?key|secret|token|password|passwd|pat|github_pat|ghp_|sk-[a-z0-9])/i;
|
|
48
|
+
const URL_RE = /https?:\/\/[^\s)"'<>]+/g;
|
|
49
|
+
|
|
50
|
+
function normalizePath(filePath) {
|
|
51
|
+
return filePath.split(path.sep).join("/");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function safeRead(filePath, maxBytes = 180_000) {
|
|
55
|
+
try {
|
|
56
|
+
const stat = fs.statSync(filePath);
|
|
57
|
+
if (!stat.isFile() || stat.size > maxBytes) return "";
|
|
58
|
+
return fs.readFileSync(filePath, "utf8");
|
|
59
|
+
} catch {
|
|
60
|
+
return "";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function safeJson(filePath) {
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(safeRead(filePath, 500_000));
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function walkRepo(rootDir, maxFiles = 2500) {
|
|
73
|
+
const files = [];
|
|
74
|
+
const stack = [rootDir];
|
|
75
|
+
while (stack.length && files.length < maxFiles) {
|
|
76
|
+
const current = stack.pop();
|
|
77
|
+
let entries = [];
|
|
78
|
+
try {
|
|
79
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
80
|
+
} catch {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
entries.sort((a, b) => b.name.localeCompare(a.name));
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
const fullPath = path.join(current, entry.name);
|
|
86
|
+
const relative = normalizePath(path.relative(rootDir, fullPath));
|
|
87
|
+
if (entry.isDirectory()) {
|
|
88
|
+
if (!shouldIgnoreDirectory(entry.name)) stack.push(fullPath);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (!entry.isFile()) continue;
|
|
92
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
93
|
+
if (!TEXT_EXTENSIONS.has(ext)) continue;
|
|
94
|
+
files.push(relative);
|
|
95
|
+
if (files.length >= maxFiles) break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return files.sort();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function shouldIgnoreDirectory(name) {
|
|
102
|
+
return IGNORE_DIRS.has(name) || /^\.workq[-_]/.test(name) || name === ".gstack";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function redactLine(line) {
|
|
106
|
+
if (SECRET_LINE.test(line)) return "";
|
|
107
|
+
return line.replace(URL_RE, (url) => url.replace(/[?&](token|key|secret|code|state)=[^&\s]+/gi, ""));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function cleanText(text) {
|
|
111
|
+
return text
|
|
112
|
+
.split(/\r?\n/)
|
|
113
|
+
.map((line) => redactLine(line).trim())
|
|
114
|
+
.filter(Boolean)
|
|
115
|
+
.filter((line) => !line.startsWith("```"))
|
|
116
|
+
.join("\n");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function extractDocLines(text, limit = 24) {
|
|
120
|
+
const cleaned = cleanText(text);
|
|
121
|
+
const lines = cleaned
|
|
122
|
+
.split(/\r?\n/)
|
|
123
|
+
.filter((line) => /^#{1,3}\s+/.test(line) || /^[-*]\s+/.test(line) || /^[가-힣A-Za-z0-9].{20,}$/.test(line))
|
|
124
|
+
.map((line) => line.replace(/^#{1,6}\s+/, "").replace(/^[-*]\s+/, ""))
|
|
125
|
+
.filter((line) => line.length <= 220);
|
|
126
|
+
return lines.slice(0, limit);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function firstParagraph(text) {
|
|
130
|
+
const cleaned = cleanText(text);
|
|
131
|
+
const paragraph = cleaned
|
|
132
|
+
.split(/\n\s*\n/)
|
|
133
|
+
.map((part) => part.replace(/^#\s+.*$/m, "").trim())
|
|
134
|
+
.find((part) => part.length >= 30);
|
|
135
|
+
return paragraph ? paragraph.split(/\r?\n/).join(" ").slice(0, 420) : "";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function compactText(value, maxLength = 180) {
|
|
139
|
+
const normalized = String(value ?? "").replace(/\s+/g, " ").trim();
|
|
140
|
+
if (normalized.length <= maxLength) return normalized;
|
|
141
|
+
const cutoff = normalized.slice(0, maxLength + 1);
|
|
142
|
+
const sentenceBreak = Math.max(cutoff.lastIndexOf("."), cutoff.lastIndexOf("다."), cutoff.lastIndexOf("?"), cutoff.lastIndexOf("!"));
|
|
143
|
+
if (sentenceBreak >= Math.floor(maxLength * 0.55)) return cutoff.slice(0, sentenceBreak + 1).trim();
|
|
144
|
+
const spaceBreak = cutoff.lastIndexOf(" ");
|
|
145
|
+
const end = spaceBreak >= Math.floor(maxLength * 0.65) ? spaceBreak : maxLength;
|
|
146
|
+
return `${cutoff.slice(0, end).trim()}...`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function hasHangul(value) {
|
|
150
|
+
return /[가-힣]/.test(value);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function titleCase(value) {
|
|
154
|
+
return value
|
|
155
|
+
.split(/[-_\s]+/)
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
.map((part) => part.slice(0, 1).toUpperCase() + part.slice(1))
|
|
158
|
+
.join(" ");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function findFiles(files, patterns, limit = 8) {
|
|
162
|
+
return files.filter((file) => patterns.some((pattern) => pattern.test(file))).slice(0, limit);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function hasAny(files, patterns) {
|
|
166
|
+
return findFiles(files, patterns, 1).length > 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function extractUrlsFromDocs(docs) {
|
|
170
|
+
const urls = [];
|
|
171
|
+
for (const doc of docs) {
|
|
172
|
+
for (const match of doc.text.matchAll(URL_RE)) {
|
|
173
|
+
const url = match[0].replace(/[),.]+$/, "");
|
|
174
|
+
if (!urls.includes(url)) urls.push(url);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return urls;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function extractUrlsFromFiles(rootDir, files, maxFiles = 320) {
|
|
181
|
+
const urls = [];
|
|
182
|
+
for (const file of files.slice(0, maxFiles)) {
|
|
183
|
+
const ext = path.extname(file).toLowerCase();
|
|
184
|
+
if (!TEXT_EXTENSIONS.has(ext)) continue;
|
|
185
|
+
const text = cleanText(safeRead(path.join(rootDir, file), 120_000));
|
|
186
|
+
for (const match of text.matchAll(URL_RE)) {
|
|
187
|
+
const url = match[0].replace(/[),.]+$/, "");
|
|
188
|
+
if (!urls.includes(url)) urls.push(url);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return urls;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function inferProductionUrl(urls) {
|
|
195
|
+
return (
|
|
196
|
+
urls.find((url) => /:\/\/(?!staging\.)(?!dev\.)(?!preview\.)[^/]*nado\.ai\.kr\/?$/i.test(url)) ??
|
|
197
|
+
urls.find((url) => /:\/\/[^/]*nado\.ai\.kr\/?$/i.test(url)) ??
|
|
198
|
+
urls.find((url) => /:\/\/[^/]*(azurestaticapps|web\.core\.windows|vercel)\./i.test(url)) ??
|
|
199
|
+
""
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function scriptCommand(packageJson, names) {
|
|
204
|
+
const scripts = packageJson?.scripts ?? {};
|
|
205
|
+
for (const name of names) {
|
|
206
|
+
if (scripts[name]) return `npm run ${name}`;
|
|
207
|
+
}
|
|
208
|
+
return "";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function buildMachineSpec({ nodeKey, type, status, productState, source, confidence, terms, rules, examples, rationale }) {
|
|
212
|
+
return {
|
|
213
|
+
schema: "human_intent_machine_validation_v1",
|
|
214
|
+
terms: {
|
|
215
|
+
product_state_node: "A Work Q product-state map node used to classify reports and guide local agent work.",
|
|
216
|
+
observed_evidence: "Behavior grounded in repository files inspected during local bootstrap.",
|
|
217
|
+
owner_intent_gap: "A behavior or policy that cannot be safely inferred from code and must stay input_required or decision_needed.",
|
|
218
|
+
...terms
|
|
219
|
+
},
|
|
220
|
+
variables: {
|
|
221
|
+
node_key: nodeKey,
|
|
222
|
+
type,
|
|
223
|
+
status,
|
|
224
|
+
product_state: productState,
|
|
225
|
+
source,
|
|
226
|
+
confidence
|
|
227
|
+
},
|
|
228
|
+
preconditions: ["repository identity has been verified before local bootstrap output is trusted"],
|
|
229
|
+
rules: [
|
|
230
|
+
{
|
|
231
|
+
if: "a report expectation is directly covered by this approved verified node and contradicts observed behavior",
|
|
232
|
+
then: "classify the report as bug evidence for the linked behavior"
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
if: "the report asks for behavior not present in this node or connected evidence",
|
|
236
|
+
then: "classify it as spec_gap, enhancement, input_required, or decision_needed instead of inventing product intent"
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
if: "evidence is stale, ambiguous, or only appears in old docs",
|
|
240
|
+
then: "lower confidence and require owner review before implementation"
|
|
241
|
+
},
|
|
242
|
+
...rules
|
|
243
|
+
],
|
|
244
|
+
outcomes: [productState, status],
|
|
245
|
+
invariants: [
|
|
246
|
+
"Code behavior is evidence, not automatically approved product intent.",
|
|
247
|
+
"Unknown product decisions remain explicit and blocked.",
|
|
248
|
+
"The node_key must remain stable so future tasks can reference the same behavior."
|
|
249
|
+
],
|
|
250
|
+
failure_semantics: {
|
|
251
|
+
missing_evidence: "lower confidence and request repository analysis or owner clarification",
|
|
252
|
+
ambiguous_intent: "create or keep an input_required or decision_needed node",
|
|
253
|
+
conflicting_evidence: "classify as spec_conflict and require owner decision before implementation"
|
|
254
|
+
},
|
|
255
|
+
examples,
|
|
256
|
+
rationale
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function makeNode({
|
|
261
|
+
node_key,
|
|
262
|
+
type = "feature",
|
|
263
|
+
title,
|
|
264
|
+
summary,
|
|
265
|
+
status = "approved",
|
|
266
|
+
product_state = "verified",
|
|
267
|
+
confidence = 0.82,
|
|
268
|
+
source = "workq-local-bootstrap",
|
|
269
|
+
evidence = [],
|
|
270
|
+
terms = {},
|
|
271
|
+
rules = [],
|
|
272
|
+
examples = [],
|
|
273
|
+
rationale = [],
|
|
274
|
+
display
|
|
275
|
+
}) {
|
|
276
|
+
return {
|
|
277
|
+
node_key,
|
|
278
|
+
type,
|
|
279
|
+
title,
|
|
280
|
+
summary,
|
|
281
|
+
status,
|
|
282
|
+
product_state,
|
|
283
|
+
confidence,
|
|
284
|
+
source,
|
|
285
|
+
metadata: {
|
|
286
|
+
...(display ? { display } : {}),
|
|
287
|
+
evidence,
|
|
288
|
+
machine_spec: buildMachineSpec({
|
|
289
|
+
nodeKey: node_key,
|
|
290
|
+
type,
|
|
291
|
+
status,
|
|
292
|
+
productState: product_state,
|
|
293
|
+
source,
|
|
294
|
+
confidence,
|
|
295
|
+
terms,
|
|
296
|
+
rules,
|
|
297
|
+
examples: examples.length ? examples : [{ given: title, expect: summary }],
|
|
298
|
+
rationale: rationale.length ? rationale : [summary]
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function makeEdge(from_node_key, to_node_key, type, label, evidence) {
|
|
305
|
+
return { from_node_key, to_node_key, type, label, evidence };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function addNode(nodes, node) {
|
|
309
|
+
if (nodes.some((existing) => existing.node_key === node.node_key)) return;
|
|
310
|
+
nodes.push(node);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function addConfiguredNodeIfEvidence(nodes, files, config, language) {
|
|
314
|
+
const evidence = findFiles(files, config.patterns, config.limit ?? 8);
|
|
315
|
+
if (!evidence.length) return null;
|
|
316
|
+
const ko = language === "ko";
|
|
317
|
+
addNode(
|
|
318
|
+
nodes,
|
|
319
|
+
makeNode({
|
|
320
|
+
node_key: config.node_key,
|
|
321
|
+
type: config.type ?? "feature",
|
|
322
|
+
title: ko ? config.title_ko : config.title_en,
|
|
323
|
+
summary: ko ? config.summary_ko : config.summary_en,
|
|
324
|
+
confidence: config.confidence ?? 0.84,
|
|
325
|
+
evidence,
|
|
326
|
+
display: {
|
|
327
|
+
ko: { title: config.title_ko, summary: config.summary_ko },
|
|
328
|
+
en: { title: config.title_en, summary: config.summary_en }
|
|
329
|
+
},
|
|
330
|
+
terms: config.terms ?? {},
|
|
331
|
+
rules: config.rules ?? [],
|
|
332
|
+
examples: config.examples ?? [],
|
|
333
|
+
rationale: config.rationale ?? []
|
|
334
|
+
})
|
|
335
|
+
);
|
|
336
|
+
return evidence;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function addFeatureIfEvidence(nodes, files, config, language) {
|
|
340
|
+
return Boolean(addConfiguredNodeIfEvidence(nodes, files, config, language));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function hasNode(nodes, nodeKey) {
|
|
344
|
+
return nodes.some((node) => node.node_key === nodeKey);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function edgeExists(edges, fromNodeKey, toNodeKey, type) {
|
|
348
|
+
return edges.some((edge) => edge.from_node_key === fromNodeKey && edge.to_node_key === toNodeKey && edge.type === type);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function specificFeatureConfigs(packageName) {
|
|
352
|
+
const configs = [];
|
|
353
|
+
if (packageName === "work-q") {
|
|
354
|
+
configs.push(
|
|
355
|
+
{
|
|
356
|
+
node_key: "feature.report_intake",
|
|
357
|
+
title_ko: "리포트 접수와 첨부",
|
|
358
|
+
title_en: "Report intake and attachments",
|
|
359
|
+
summary_ko: "사용자는 URL, 실제/기대 동작, 재현 단계, 오류 로그, 스크린샷을 제출하고 Work Q는 이를 중복 없이 접수한다.",
|
|
360
|
+
summary_en: "Users submit URL, actual/expected behavior, repro steps, logs, and screenshots; Work Q stores the intake without duplicate submissions.",
|
|
361
|
+
patterns: [/^src\/app\/page\.tsx$/, /^src\/app\/api\/\[\.\.\.path\]\/route\.ts$/, /^src\/lib\/artifacts\.ts$/]
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
node_key: "feature.report_classification",
|
|
365
|
+
title_ko: "리포트 명세 기반 분류",
|
|
366
|
+
title_en: "Spec-based report classification",
|
|
367
|
+
summary_ko: "리포트의 기대 동작을 기능명세 지도와 비교해 버그, 기획부재, 기능충돌, 개선, 정보부족, 재현불가로 분류한다.",
|
|
368
|
+
summary_en: "Compares report expectations against the product map to classify bug, spec gap, conflict, enhancement, needs-info, or not-reproduced.",
|
|
369
|
+
patterns: [/^src\/lib\/store\.ts$/, /^src\/lib\/ai\.ts$/, /^src\/app\/api\/\[\.\.\.path\]\/route\.ts$/]
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
node_key: "feature.owner_decision_gate",
|
|
373
|
+
title_ko: "소유자 결정 게이트",
|
|
374
|
+
title_en: "Owner decision gate",
|
|
375
|
+
summary_ko: "기획부재, 기능충돌, 개선 요청은 자동 수정하지 않고 소유자가 승인, 거절, 보류할 때까지 큐에서 멈춘다.",
|
|
376
|
+
summary_en: "Spec gaps, conflicts, and enhancements stay blocked until the owner approves, rejects, or holds them.",
|
|
377
|
+
patterns: [/^src\/app\/page\.tsx$/, /^src\/lib\/store\.ts$/]
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
node_key: "feature.desktop_agent_mcp_queue",
|
|
381
|
+
title_ko: "데스크톱 에이전트 MCP 큐",
|
|
382
|
+
title_en: "Desktop agent MCP queue",
|
|
383
|
+
summary_ko: "로컬 Codex/Claude가 MCP로 작업을 claim하고, 재현, 수정, 테스트, 완료 결과를 Work Q에 기록한다.",
|
|
384
|
+
summary_en: "Local Codex or Claude agents claim tasks over MCP, reproduce, patch, test, and post completion back to Work Q.",
|
|
385
|
+
patterns: [/^src\/lib\/local-agent\.ts$/, /^packages\/workq-mcp\/bin\/workq\.mjs$/, /^src\/app\/api\/\[\.\.\.path\]\/route\.ts$/]
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
node_key: "feature.product_state_map",
|
|
389
|
+
title_ko: "기능명세 지도와 제품 기억",
|
|
390
|
+
title_en: "Product state map and memory",
|
|
391
|
+
summary_ko: "기능, 유즈케이스, 결정, 근거, 관계를 노드와 엣지로 저장해 사람이 읽고 AI가 검증할 기준 명세로 사용한다.",
|
|
392
|
+
summary_en: "Stores features, use cases, decisions, evidence, and relationships as nodes and edges for humans and AI validation.",
|
|
393
|
+
patterns: [/^src\/app\/page\.tsx$/, /^src\/lib\/store\.ts$/, /^packages\/workq-mcp\/lib\/analyze-repo\.mjs$/]
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
node_key: "feature.local_project_binding",
|
|
397
|
+
title_ko: "로컬 프로젝트 신원 검증",
|
|
398
|
+
title_en: "Local project identity binding",
|
|
399
|
+
summary_ko: "프로젝트 키, 로컬 git root, origin remote, 브랜치를 검증해 에이전트가 엉뚱한 레포를 고치지 않게 한다.",
|
|
400
|
+
summary_en: "Verifies project key, local git root, origin remote, and branch so agents do not patch the wrong repository.",
|
|
401
|
+
patterns: [/^src\/lib\/local-agent\.ts$/, /^src\/lib\/store\.ts$/, /^packages\/workq-mcp\/bin\/workq\.mjs$/]
|
|
402
|
+
}
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (packageName === "type") {
|
|
407
|
+
configs.push(
|
|
408
|
+
{
|
|
409
|
+
node_key: "feature.typing_engine",
|
|
410
|
+
title_ko: "한글/영문 타자 입력 엔진",
|
|
411
|
+
title_en: "Korean and English typing engine",
|
|
412
|
+
summary_ko: "한글 2벌식과 영문 QWERTY 입력을 정규화하고 자리 조합, WPM, 정확도, 오타율을 계산해 연습 화면의 기준 지표로 사용한다.",
|
|
413
|
+
summary_en: "Normalizes Korean 2-set and English QWERTY input, then computes key-position groups, WPM, accuracy, and typo rate for practice sessions.",
|
|
414
|
+
patterns: [/^src\/typing\.js$/, /^src\/text-normalize\.js$/, /^src\/main\.js$/],
|
|
415
|
+
terms: { typing_engine: "Input normalization and scoring logic for typing practice." }
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
node_key: "feature.learning_curriculum_word_pools",
|
|
419
|
+
title_ko: "240일 커리큘럼과 자리별 단어 풀",
|
|
420
|
+
title_en: "240-day curriculum and key-position word pools",
|
|
421
|
+
summary_ko: "배우기 메뉴는 240일 단계형 경로를 제공하고, 단어 연습은 검수 시드와 공개 원전에서 생성한 자리별 한글/영문 단어 풀을 사용한다.",
|
|
422
|
+
summary_en: "The learn menu exposes a 240-day path, and word practice uses reviewed seeds plus public-domain corpus generated into key-position word pools.",
|
|
423
|
+
patterns: [/^src\/data\.js$/, /^src\/generated\/word-pools\.js$/, /^src\/content\/reviewed-word-seeds\.js$/]
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
node_key: "feature.short_sentence_practice",
|
|
427
|
+
title_ko: "짧은 문장 연습",
|
|
428
|
+
title_en: "Short sentence practice",
|
|
429
|
+
summary_ko: "검수된 한글/영문 격언과 문장을 한 문장씩 제시하고, 사용자는 입력 완료 후 Enter로 다음 문장에 진입한다.",
|
|
430
|
+
summary_en: "Presents reviewed Korean and English aphorisms one prompt at a time; after typing, Enter advances to the next sentence.",
|
|
431
|
+
patterns: [/reviewed-short-aphorisms\.js$/, /^src\/generated\/quotes\.js$/]
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
node_key: "feature.long_classics_practice",
|
|
435
|
+
title_ko: "긴 고전 본문 연습",
|
|
436
|
+
title_en: "Long-form classic text practice",
|
|
437
|
+
summary_ko: "이용조건을 확인한 한글/영문 고전 전문을 정적 자산으로 보관하고, 사용자는 작품별 페이지를 넘기며 긴 글을 완주한다.",
|
|
438
|
+
summary_en: "Stores licensed public classic texts as static assets so users can type complete long-form works page by page.",
|
|
439
|
+
patterns: [/^src\/classics\.js$/, /^src\/content\/classics\//, /^src\/generated\/classic-catalog\.js$/]
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
node_key: "feature.acid_rain_typing_game",
|
|
443
|
+
title_ko: "산성비 스타일 타자 게임",
|
|
444
|
+
title_en: "Falling-word typing game",
|
|
445
|
+
summary_ko: "선택한 언어와 자리 조합 단어가 떨어지고, 점수에 따라 단계와 압박이 올라가며 특수 단어 효과가 게임 상태를 바꾼다.",
|
|
446
|
+
summary_en: "Drops words matching the selected language and key-position filter, raises pressure by score, and applies special word effects to game state.",
|
|
447
|
+
patterns: [/^src\/game\.js$/]
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
node_key: "feature.account_records_ranking",
|
|
451
|
+
title_ko: "계정, 연습 기록, 랭킹 API",
|
|
452
|
+
title_en: "Accounts, practice records, and ranking API",
|
|
453
|
+
summary_ko: "간단한 계정 로그인, 세션 저장, 랭킹은 Azure Functions API와 Table Storage를 통해 관리하고 브라우저에는 최근 기록을 캐시한다.",
|
|
454
|
+
summary_en: "Simple account login, session persistence, and rankings are handled by Azure Functions plus Table Storage, with recent results cached in the browser.",
|
|
455
|
+
patterns: [/^api\/src\/functions\/index\.js$/, /api\/package\.json$/, /^src\/main\.js$/]
|
|
456
|
+
}
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (packageName === "english-learning" || packageName === "nado-language") {
|
|
461
|
+
configs.push(
|
|
462
|
+
{
|
|
463
|
+
node_key: "feature.reading_articles",
|
|
464
|
+
title_ko: "읽기 자료와 아티클 관리",
|
|
465
|
+
title_en: "Reading materials and article management",
|
|
466
|
+
summary_ko: "Read 탭과 아티클 상세 화면은 샘플/커스텀/책 챕터 자료를 보여주고, 선택 표현, 최근 읽기, 다음 자료 흐름을 관리한다.",
|
|
467
|
+
summary_en: "The Read tab and article detail screen manage sample, custom, and chapter materials, selected expressions, recent reading, and next-material flow.",
|
|
468
|
+
patterns: [/^app\/\(tabs\)\/index\.tsx$/, /^app\/article\/\[id\]\.tsx$/, /^src\/data\/sampleArticles\.ts$/, /^src\/utils\/articleGrouping\.ts$/]
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
node_key: "feature.flashcard_fsrs_review",
|
|
472
|
+
title_ko: "플래시카드와 FSRS 복습",
|
|
473
|
+
title_en: "Flashcards and FSRS review",
|
|
474
|
+
summary_ko: "저장된 표현은 카드가 되고 Review 탭은 FSRS 큐, 답 확인, Again/Good 평가, 제외, 되돌리기, Instant QA를 제공한다.",
|
|
475
|
+
summary_en: "Saved expressions become cards; the Review tab provides the FSRS queue, reveal, Again/Good grading, exclusion, undo, and Instant QA.",
|
|
476
|
+
patterns: [/^app\/\(tabs\)\/review\.tsx$/, /^src\/services\/fsrs\.ts$/, /^src\/services\/queueManager\.ts$/, /^test\/fsrs/]
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
node_key: "feature.writing_listening_practice",
|
|
480
|
+
title_ko: "쓰기 교정과 듣기 연습",
|
|
481
|
+
title_en: "Writing correction and listening practice",
|
|
482
|
+
summary_ko: "아티클 기반 쓰기 화면은 AI 교정과 점수/CEFR/수정 제안을 제공하고, 듣기 화면은 문장 TTS, 속도, 반복, 문장 이동을 제공한다.",
|
|
483
|
+
summary_en: "Article-based writing provides AI correction, score, CEFR, and corrections; listening provides sentence TTS, speed, repeat, and navigation.",
|
|
484
|
+
patterns: [/^app\/write\/\[articleId\]\.tsx$/, /^app\/listen\/\[articleId\]\.tsx$/, /^src\/utils\/sentences\.ts$/]
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
node_key: "feature.ai_quota_runtime",
|
|
488
|
+
title_ko: "AI 프록시, 모델 런타임, 사용량 제한",
|
|
489
|
+
title_en: "AI proxy, model runtime, and quota",
|
|
490
|
+
summary_ko: "AI 요청은 Edge Function에서 인증, 액션 검증, Free/Pro/Admin 한도, 활성 모델/프롬프트 선택을 거친 뒤 AI 런타임으로 전달된다.",
|
|
491
|
+
summary_en: "AI requests pass through an Edge Function for auth, action validation, Free/Pro/Admin limits, and active model/prompt selection before runtime execution.",
|
|
492
|
+
patterns: [/^supabase\/functions\/ai-proxy\/index\.ts$/, /^supabase\/functions\/_shared\/ai-runtime\.ts$/, /^src\/services\/ai\.ts$/]
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
node_key: "feature.ocr_capture",
|
|
496
|
+
title_ko: "OCR 이미지/클립보드 캡처",
|
|
497
|
+
title_en: "OCR image and clipboard capture",
|
|
498
|
+
summary_ko: "이미지 선택 또는 클립보드 이미지에서 영어 텍스트를 추출해 읽기 자료로 만들며, 입력 제한과 권한 경계를 적용한다.",
|
|
499
|
+
summary_en: "Extracts English text from selected or clipboard images into reading material while enforcing input limits and entitlement boundaries.",
|
|
500
|
+
patterns: [/^src\/services\/ocr\.ts$/, /^supabase\/functions\/image-ocr\/index\.ts$/, /ocr.*\.spec\.ts$/]
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
node_key: "feature.browser_extension_capture",
|
|
504
|
+
title_ko: "브라우저 확장 캡처",
|
|
505
|
+
title_en: "Browser extension capture",
|
|
506
|
+
summary_ko: "브라우저 확장은 선택한 웹 텍스트를 Nado로 보내 커스텀 아티클, 하이라이트, 또는 분석된 카드 저장 흐름에 연결한다.",
|
|
507
|
+
summary_en: "The browser extension sends selected web text to Nado for custom article, highlight, or analyzed card save flows.",
|
|
508
|
+
patterns: [/^extensions\/nado-browser\//, /^supabase\/functions\/extension-capture\/index\.ts$/]
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
node_key: "feature.mcp_article_practice",
|
|
512
|
+
title_ko: "MCP 아티클 저장과 저장 카드 연습",
|
|
513
|
+
title_en: "MCP article save and saved-card practice",
|
|
514
|
+
summary_ko: "데스크톱 AI는 MCP로 아티클을 저장하고 기존 학습 카드를 조회/수정하며 저장 카드 기반 연습을 생성한다.",
|
|
515
|
+
summary_en: "Desktop AI can save articles through MCP, list/update existing study cards, and generate practice from saved cards.",
|
|
516
|
+
patterns: [/^mcp\/nado-language-server\.mjs$/, /^scripts\/nado-mcp/, /^test\/nado-mcp/]
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
node_key: "feature.connect_hub",
|
|
520
|
+
title_ko: "커넥트 허브",
|
|
521
|
+
title_en: "Connect Hub",
|
|
522
|
+
summary_ko: "Connect Hub는 브라우저 확장 다운로드, Remote MCP URL, 로컬 연결 명령, 로그인 상태, 저장 테스트를 제공한다.",
|
|
523
|
+
summary_en: "Connect Hub provides extension download, Remote MCP URL, local connection command, login state, and save tests.",
|
|
524
|
+
patterns: [/^app\/connect\.tsx$/, /^app\/mcp\/connect\.tsx$/, /^app\/extension\/connect\.tsx$/, /^src\/services\/remoteMcp\.ts$/]
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
node_key: "feature.admin_ai_lab_metrics",
|
|
528
|
+
title_ko: "관리자 지표와 AI Lab",
|
|
529
|
+
title_en: "Admin metrics and AI Lab",
|
|
530
|
+
summary_ko: "관리자는 서비스 지표를 보고 AI Lab에서 task별 모델과 프롬프트를 비교, 편집, 배포해 운영 런타임에 반영한다.",
|
|
531
|
+
summary_en: "Admins view service metrics and use AI Lab to compare, edit, and deploy task-specific model/prompt settings into runtime.",
|
|
532
|
+
patterns: [/^app\/admin\.tsx$/, /^app\/admin-ai\.tsx$/, /^src\/services\/adminMetrics\.ts$/, /^src\/services\/aiLab\.ts$/, /^supabase\/functions\/admin-metrics\/index\.ts$/, /^supabase\/functions\/ai-lab\/index\.ts$/]
|
|
533
|
+
}
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
return configs;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function specificUseCaseConfigs(packageName) {
|
|
540
|
+
const configs = [];
|
|
541
|
+
if (packageName === "work-q") {
|
|
542
|
+
configs.push(
|
|
543
|
+
{
|
|
544
|
+
node_key: "use_case.submit_report_with_screenshot",
|
|
545
|
+
title_ko: "사용자가 스크린샷과 함께 리포트를 제출한다",
|
|
546
|
+
title_en: "User submits a report with screenshots",
|
|
547
|
+
summary_ko: "사용자는 기대와 다른 동작을 URL, 설명, 재현 단계, 로그, 스크린샷으로 제출하고 중복 제출은 하나로 처리된다.",
|
|
548
|
+
summary_en: "A user submits unexpected behavior with URL, description, repro steps, logs, and screenshots; duplicate submissions collapse into one report.",
|
|
549
|
+
patterns: [/^src\/app\/page\.tsx$/, /^src\/app\/api\/\[\.\.\.path\]\/route\.ts$/, /^src\/lib\/artifacts\.ts$/],
|
|
550
|
+
feature_keys: ["feature.report_intake"]
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
node_key: "use_case.classify_report_against_spec",
|
|
554
|
+
title_ko: "AI가 리포트를 기능명세 지도에 매칭한다",
|
|
555
|
+
title_en: "AI matches a report against the product map",
|
|
556
|
+
summary_ko: "Work Q는 리포트의 기대 동작을 제품 지도 노드와 관계에 매칭해 버그인지 결정 필요 항목인지 분류한다.",
|
|
557
|
+
summary_en: "Work Q matches report expectations against product-map nodes and edges to classify bugs versus decision-needed work.",
|
|
558
|
+
patterns: [/^src\/lib\/store\.ts$/, /^src\/lib\/ai\.ts$/],
|
|
559
|
+
feature_keys: ["feature.report_classification", "feature.product_state_map"]
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
node_key: "use_case.owner_approves_non_bug_work",
|
|
563
|
+
title_ko: "소유자가 비버그 작업을 승인한다",
|
|
564
|
+
title_en: "Owner approves non-bug work",
|
|
565
|
+
summary_ko: "기획부재, 기능충돌, 개선 요청은 사람이 웹에서 근거를 보고 승인한 뒤에만 에이전트 작업으로 이동한다.",
|
|
566
|
+
summary_en: "Spec gaps, conflicts, and enhancements move to agent work only after the owner reviews evidence and approves them in the web app.",
|
|
567
|
+
patterns: [/^src\/app\/page\.tsx$/, /^src\/lib\/store\.ts$/],
|
|
568
|
+
feature_keys: ["feature.owner_decision_gate", "feature.report_classification"]
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
node_key: "use_case.connect_local_project",
|
|
572
|
+
title_ko: "사용자가 로컬 레포를 Work Q 프로젝트에 연결한다",
|
|
573
|
+
title_en: "User connects a local repo to a Work Q project",
|
|
574
|
+
summary_ko: "사용자는 로컬 레포에서 Work Q CLI를 실행해 git root, remote, branch를 운영 프로젝트 키와 바인딩한다.",
|
|
575
|
+
summary_en: "The user runs the Work Q CLI inside a local repo to bind git root, remote, and branch to the production project key.",
|
|
576
|
+
patterns: [/^packages\/workq-mcp\/bin\/workq\.mjs$/, /^src\/lib\/local-agent\.ts$/, /^docs\/desktop-agent-mcp\.md$/],
|
|
577
|
+
feature_keys: ["feature.local_project_binding", "feature.desktop_agent_mcp_queue"]
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
node_key: "use_case.desktop_agent_claims_and_finishes_task",
|
|
581
|
+
title_ko: "로컬 에이전트가 작업을 claim하고 완료 처리한다",
|
|
582
|
+
title_en: "Desktop agent claims and completes a task",
|
|
583
|
+
summary_ko: "로컬 에이전트는 MCP 작업 패킷을 받아 수정하고, 테스트 결과와 완료 상태를 Work Q에 돌려준다.",
|
|
584
|
+
summary_en: "A local agent receives an MCP work packet, applies a change, and posts test evidence plus completion status back to Work Q.",
|
|
585
|
+
patterns: [/^src\/lib\/local-agent\.ts$/, /^packages\/workq-mcp\/README\.md$/, /^tests\/e2e\/workflow\.spec\.ts$/],
|
|
586
|
+
feature_keys: ["feature.desktop_agent_mcp_queue"]
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
node_key: "use_case.bootstrap_repo_product_map",
|
|
590
|
+
title_ko: "MCP가 로컬 레포에서 기능명세 지도를 부트스트랩한다",
|
|
591
|
+
title_en: "MCP bootstraps a product map from a local repo",
|
|
592
|
+
summary_ko: "CLI가 문서, 라우트, API, 테스트, 배포 설정을 읽어 기능, 유즈케이스, 상태 노드와 관계를 생성한다.",
|
|
593
|
+
summary_en: "The CLI reads docs, routes, APIs, tests, and deploy config to generate feature, use-case, state nodes, and relationships.",
|
|
594
|
+
patterns: [/^packages\/workq-mcp\/lib\/analyze-repo\.mjs$/, /^packages\/workq-mcp\/bin\/workq\.mjs$/],
|
|
595
|
+
feature_keys: ["feature.product_state_map", "feature.local_project_binding"]
|
|
596
|
+
}
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (packageName === "english-learning" || packageName === "nado-language") {
|
|
601
|
+
configs.push(
|
|
602
|
+
{
|
|
603
|
+
node_key: "use_case.read_existing_article",
|
|
604
|
+
title_ko: "사용자가 기존 영어 기사를 읽는다",
|
|
605
|
+
title_en: "User reads an existing English article",
|
|
606
|
+
summary_ko: "사용자는 Read 탭에서 샘플, 커스텀, 책 챕터 자료를 열고 아티클 상세 화면에서 문장을 읽는다.",
|
|
607
|
+
summary_en: "A user opens sample, custom, or book-chapter material from the Read tab and reads sentences in the article detail screen.",
|
|
608
|
+
patterns: [/^app\/\(tabs\)\/index\.tsx$/, /^app\/article\/\[id\]\.tsx$/, /^src\/data\/sampleArticles\.ts$/],
|
|
609
|
+
feature_keys: ["feature.reading_articles"]
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
node_key: "use_case.create_article_from_text_or_image",
|
|
613
|
+
title_ko: "사용자가 텍스트나 이미지로 읽기 자료를 만든다",
|
|
614
|
+
title_en: "User creates reading material from text or image",
|
|
615
|
+
summary_ko: "사용자는 직접 입력한 텍스트나 OCR 이미지/클립보드 캡처를 읽기 자료로 저장해 학습 흐름에 넣는다.",
|
|
616
|
+
summary_en: "A user turns typed text or OCR image/clipboard capture into reading material and adds it to the learning flow.",
|
|
617
|
+
patterns: [/^app\/\(tabs\)\/index\.tsx$/, /^src\/services\/ocr\.ts$/, /^supabase\/functions\/image-ocr\/index\.ts$/],
|
|
618
|
+
feature_keys: ["feature.reading_articles", "feature.ocr_capture"]
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
node_key: "use_case.save_expression_and_review",
|
|
622
|
+
title_ko: "사용자가 표현을 저장하고 복습한다",
|
|
623
|
+
title_en: "User saves an expression and reviews it",
|
|
624
|
+
summary_ko: "사용자는 아티클에서 모르는 표현을 저장하고 Review 탭에서 FSRS 큐에 따라 답을 확인하고 평가한다.",
|
|
625
|
+
summary_en: "A user saves an unfamiliar expression from an article, then reviews it in the FSRS queue with reveal and grading actions.",
|
|
626
|
+
patterns: [/^app\/article\/\[id\]\.tsx$/, /^app\/\(tabs\)\/review\.tsx$/, /^src\/services\/fsrs\.ts$/, /^src\/services\/queueManager\.ts$/],
|
|
627
|
+
feature_keys: ["feature.reading_articles", "feature.flashcard_fsrs_review"]
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
node_key: "use_case.practice_writing_from_article",
|
|
631
|
+
title_ko: "사용자가 아티클 기반 쓰기 연습을 한다",
|
|
632
|
+
title_en: "User practices writing from an article",
|
|
633
|
+
summary_ko: "사용자는 아티클을 바탕으로 글을 쓰고 AI 교정, 점수, CEFR, 수정 제안을 받는다.",
|
|
634
|
+
summary_en: "A user writes from an article and receives AI correction, score, CEFR level, and revision suggestions.",
|
|
635
|
+
patterns: [/^app\/write\/\[articleId\]\.tsx$/, /^src\/services\/ai\.ts$/],
|
|
636
|
+
feature_keys: ["feature.writing_listening_practice"]
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
node_key: "use_case.listen_to_article_sentences",
|
|
640
|
+
title_ko: "사용자가 아티클 문장을 듣고 반복한다",
|
|
641
|
+
title_en: "User listens to and repeats article sentences",
|
|
642
|
+
summary_ko: "사용자는 듣기 화면에서 문장 TTS를 재생하고 속도, 반복, 문장 이동을 조절한다.",
|
|
643
|
+
summary_en: "A user plays article sentence TTS and controls speed, repeat, and sentence navigation in the listening screen.",
|
|
644
|
+
patterns: [/^app\/listen\/\[articleId\]\.tsx$/, /^src\/utils\/sentences\.ts$/],
|
|
645
|
+
feature_keys: ["feature.writing_listening_practice"]
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
node_key: "use_case.capture_web_text_with_extension",
|
|
649
|
+
title_ko: "사용자가 브라우저 확장으로 웹 텍스트를 캡처한다",
|
|
650
|
+
title_en: "User captures web text with the browser extension",
|
|
651
|
+
summary_ko: "사용자는 웹페이지에서 선택한 영어 텍스트를 브라우저 확장으로 Nado에 보내 커스텀 아티클이나 카드 저장 흐름으로 연결한다.",
|
|
652
|
+
summary_en: "A user sends selected English web text through the browser extension into Nado as a custom article or card-save flow.",
|
|
653
|
+
patterns: [/^extensions\/nado-browser\/manifest\.json$/, /^extensions\/nado-browser\/src\/background\.js$/, /^extensions\/nado-browser\/src\/sidepanel\.js$/],
|
|
654
|
+
feature_keys: ["feature.browser_extension_capture", "feature.reading_articles"]
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
node_key: "use_case.desktop_ai_saves_article",
|
|
658
|
+
title_ko: "데스크톱 AI가 MCP로 아티클과 연습을 저장한다",
|
|
659
|
+
title_en: "Desktop AI saves articles and practice through MCP",
|
|
660
|
+
summary_ko: "Codex나 Claude 같은 데스크톱 AI는 MCP로 아티클을 저장하고 기존 카드 조회/수정 및 저장 카드 기반 연습 생성을 수행한다.",
|
|
661
|
+
summary_en: "Desktop AI such as Codex or Claude saves articles through MCP, lists or updates cards, and generates practice from saved cards.",
|
|
662
|
+
patterns: [/^mcp\/nado-language-server\.mjs$/, /^scripts\/nado-mcp/, /^packages\/nado-mcp\/README\.md$/],
|
|
663
|
+
feature_keys: ["feature.mcp_article_practice", "feature.reading_articles", "feature.flashcard_fsrs_review"]
|
|
664
|
+
}
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (packageName === "type") {
|
|
669
|
+
configs.push(
|
|
670
|
+
{
|
|
671
|
+
node_key: "use_case.start_daily_typing_lesson",
|
|
672
|
+
title_ko: "사용자가 일일 타자 학습을 시작한다",
|
|
673
|
+
title_en: "User starts a daily typing lesson",
|
|
674
|
+
summary_ko: "사용자는 240일 커리큘럼에서 단계와 언어를 선택하고 해당 단계의 연습을 시작한다.",
|
|
675
|
+
summary_en: "A user selects a step and language from the 240-day curriculum and starts that lesson's practice.",
|
|
676
|
+
patterns: [/^src\/data\.js$/, /^src\/main\.js$/],
|
|
677
|
+
feature_keys: ["feature.learning_curriculum_word_pools", "feature.typing_engine"]
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
node_key: "use_case.practice_key_position_words",
|
|
681
|
+
title_ko: "사용자가 자리별 단어를 연습한다",
|
|
682
|
+
title_en: "User practices key-position words",
|
|
683
|
+
summary_ko: "사용자는 선택한 자리 조합에 맞는 한글/영문 단어 풀을 입력하며 정확도와 속도를 확인한다.",
|
|
684
|
+
summary_en: "A user types Korean or English word pools for selected key-position groups while tracking accuracy and speed.",
|
|
685
|
+
patterns: [/^src\/generated\/word-pools\.js$/, /^src\/content\/reviewed-word-seeds\.js$/, /^src\/typing\.js$/],
|
|
686
|
+
feature_keys: ["feature.learning_curriculum_word_pools", "feature.typing_engine"]
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
node_key: "use_case.type_short_sentence",
|
|
690
|
+
title_ko: "사용자가 짧은 문장을 입력한다",
|
|
691
|
+
title_en: "User types a short sentence",
|
|
692
|
+
summary_ko: "사용자는 검수된 격언이나 문장을 한 문장씩 입력하고 완료 후 Enter로 다음 문장에 이동한다.",
|
|
693
|
+
summary_en: "A user types reviewed aphorisms or short sentences one prompt at a time, then presses Enter to advance.",
|
|
694
|
+
patterns: [/reviewed-short-aphorisms\.js$/, /^src\/generated\/quotes\.js$/, /^src\/typing\.js$/],
|
|
695
|
+
feature_keys: ["feature.short_sentence_practice", "feature.typing_engine"]
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
node_key: "use_case.complete_long_classic_page",
|
|
699
|
+
title_ko: "사용자가 긴 고전 본문 페이지를 완주한다",
|
|
700
|
+
title_en: "User completes a long classic text page",
|
|
701
|
+
summary_ko: "사용자는 작품을 선택하고 긴 본문을 페이지 단위로 입력하며 완주 기록을 쌓는다.",
|
|
702
|
+
summary_en: "A user selects a classic work and types long-form text page by page, building completion records.",
|
|
703
|
+
patterns: [/^src\/classics\.js$/, /^src\/content\/classics\//, /^src\/generated\/classic-catalog\.js$/, /^src\/typing\.js$/],
|
|
704
|
+
feature_keys: ["feature.long_classics_practice", "feature.typing_engine"]
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
node_key: "use_case.play_falling_word_game",
|
|
708
|
+
title_ko: "사용자가 산성비 타자 게임을 한다",
|
|
709
|
+
title_en: "User plays the falling-word typing game",
|
|
710
|
+
summary_ko: "사용자는 떨어지는 단어를 입력해 점수를 얻고 단계 상승, 압박, 특수 단어 효과를 경험한다.",
|
|
711
|
+
summary_en: "A user types falling words to score points while levels, pressure, and special-word effects change game state.",
|
|
712
|
+
patterns: [/^src\/game\.js$/, /^src\/typing\.js$/],
|
|
713
|
+
feature_keys: ["feature.acid_rain_typing_game", "feature.typing_engine"]
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
node_key: "use_case.sign_in_and_track_records",
|
|
717
|
+
title_ko: "사용자가 로그인하고 연습 기록을 확인한다",
|
|
718
|
+
title_en: "User signs in and tracks practice records",
|
|
719
|
+
summary_ko: "사용자는 간단한 계정으로 로그인하고 연습 세션 저장, 최근 기록, 랭킹을 확인한다.",
|
|
720
|
+
summary_en: "A user signs in with a simple account and checks saved practice sessions, recent records, and rankings.",
|
|
721
|
+
patterns: [/^api\/src\/functions\/index\.js$/, /^src\/main\.js$/],
|
|
722
|
+
feature_keys: ["feature.account_records_ranking"]
|
|
723
|
+
}
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return configs;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function docPriority(file) {
|
|
731
|
+
const base = path.basename(file);
|
|
732
|
+
if (/^README/i.test(base) && !file.includes("/")) return 0;
|
|
733
|
+
if (/product-plan/i.test(file) || /제품.*설계/i.test(file)) return 1;
|
|
734
|
+
if (/architecture|아키텍처/i.test(file)) return 2;
|
|
735
|
+
if (/서비스.*아키텍처|기능.*인벤토리|라인맵/i.test(file)) return 3;
|
|
736
|
+
if (base === "USE_CASES.md") return 4;
|
|
737
|
+
if (base === "FEATURES.md") return 5;
|
|
738
|
+
if (base === "CLAUDE.md") return 6;
|
|
739
|
+
if (/개요/i.test(file)) return 7;
|
|
740
|
+
if (/^README/i.test(base)) return 8;
|
|
741
|
+
if (base === "QA_CHECKLIST.md") return 9;
|
|
742
|
+
if (base === "CHANGELOG.md") return 10;
|
|
743
|
+
if (base === "TODOS.md") return 11;
|
|
744
|
+
return 20;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function genericFeatureConfigs() {
|
|
748
|
+
return [
|
|
749
|
+
{
|
|
750
|
+
node_key: "feature.web_entrypoints",
|
|
751
|
+
title_ko: "웹/앱 진입점과 화면 구조",
|
|
752
|
+
title_en: "Web/app entrypoints and screen structure",
|
|
753
|
+
summary_ko: "프로젝트의 HTML, 앱 라우트, 메인 스크립트, 화면 컴포넌트가 사용자가 실제로 보는 주요 화면 흐름을 구성한다.",
|
|
754
|
+
summary_en: "HTML, app routes, main scripts, and screen components define the primary user-visible product flow.",
|
|
755
|
+
patterns: [/^index\.html$/, /^app\//, /^pages\//, /^routes\//, /^src\/main\./, /^src\/App\./, /^src\/components\//],
|
|
756
|
+
confidence: 0.78
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
node_key: "feature.backend_api",
|
|
760
|
+
title_ko: "백엔드 API와 서버 함수",
|
|
761
|
+
title_en: "Backend APIs and server functions",
|
|
762
|
+
summary_ko: "API 라우트, 서버 함수, Edge Function은 클라이언트가 직접 처리하지 않는 인증, 저장, 외부 연동, 비즈니스 로직을 담당한다.",
|
|
763
|
+
summary_en: "API routes, server functions, and edge functions handle auth, persistence, integrations, and business logic outside the client.",
|
|
764
|
+
patterns: [/^api\//, /^server\//, /^src\/app\/api\//, /^supabase\/functions\//, /functions\/index\./],
|
|
765
|
+
confidence: 0.78
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
node_key: "feature.data_persistence",
|
|
769
|
+
title_ko: "데이터 저장과 동기화",
|
|
770
|
+
title_en: "Data persistence and sync",
|
|
771
|
+
summary_ko: "마이그레이션, 저장소 어댑터, 로컬 캐시, 동기화 서비스가 사용자 데이터와 운영 데이터를 보존한다.",
|
|
772
|
+
summary_en: "Migrations, storage adapters, local cache, and sync services preserve user and operational data.",
|
|
773
|
+
patterns: [/migration/i, /schema/i, /storage/i, /sync/i, /database/i, /localStorage/i],
|
|
774
|
+
confidence: 0.76
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
node_key: "feature.content_pipeline",
|
|
778
|
+
title_ko: "콘텐츠와 생성 파이프라인",
|
|
779
|
+
title_en: "Content and generation pipeline",
|
|
780
|
+
summary_ko: "정적 콘텐츠, 생성 스크립트, 공개 문서, 생성 산출물이 제품이 제공하는 읽기/학습/자료 경험의 원천이 된다.",
|
|
781
|
+
summary_en: "Static content, generation scripts, public docs, and generated artifacts feed the product's content experience.",
|
|
782
|
+
patterns: [/^content\//, /^src\/content\//, /^src\/generated\//, /^scripts\/generate/, /^public\//],
|
|
783
|
+
confidence: 0.76
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
node_key: "feature.testing_quality_gates",
|
|
787
|
+
title_ko: "테스트와 품질 게이트",
|
|
788
|
+
title_en: "Tests and quality gates",
|
|
789
|
+
summary_ko: "단위 테스트, E2E, 린트, 타입체크, smoke 스크립트가 수정 이후 기능 회귀를 확인하는 기준이 된다.",
|
|
790
|
+
summary_en: "Unit tests, E2E tests, lint, typecheck, and smoke scripts define regression checks after changes.",
|
|
791
|
+
patterns: [/^test\//, /^tests\//, /playwright\.config/, /vitest\.config/, /\.test\./, /\.spec\./],
|
|
792
|
+
confidence: 0.8
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
node_key: "feature.deployment_automation",
|
|
796
|
+
title_ko: "배포와 운영 자동화",
|
|
797
|
+
title_en: "Deployment and operations automation",
|
|
798
|
+
summary_ko: "배포 설정, 인프라 코드, 빌드 스크립트, CI 설정이 서비스 릴리즈와 운영 검증 흐름을 정의한다.",
|
|
799
|
+
summary_en: "Deploy config, infrastructure code, build scripts, and CI settings define service release and operational verification.",
|
|
800
|
+
patterns: [/staticwebapp\.config/, /vercel\.json/, /^infra\//, /^\.github\//, /^scripts\/.*deploy/i, /Dockerfile/, /bicep$/],
|
|
801
|
+
confidence: 0.78
|
|
802
|
+
}
|
|
803
|
+
];
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function addUseCases(nodes, edges, files, packageName, language) {
|
|
807
|
+
const ko = language === "ko";
|
|
808
|
+
for (const config of specificUseCaseConfigs(packageName)) {
|
|
809
|
+
const evidence = addConfiguredNodeIfEvidence(
|
|
810
|
+
nodes,
|
|
811
|
+
files,
|
|
812
|
+
{
|
|
813
|
+
...config,
|
|
814
|
+
type: "use_case",
|
|
815
|
+
confidence: config.confidence ?? 0.84,
|
|
816
|
+
terms: {
|
|
817
|
+
user_scenario: "An actor-goal flow that exercises one or more product features.",
|
|
818
|
+
...(config.terms ?? {})
|
|
819
|
+
},
|
|
820
|
+
rules: [
|
|
821
|
+
{
|
|
822
|
+
if: "a report expectation contradicts this user scenario while linked feature evidence says it should work",
|
|
823
|
+
then: "classify as a candidate bug for the linked feature"
|
|
824
|
+
},
|
|
825
|
+
...(config.rules ?? [])
|
|
826
|
+
]
|
|
827
|
+
},
|
|
828
|
+
language
|
|
829
|
+
);
|
|
830
|
+
if (!evidence) continue;
|
|
831
|
+
for (const featureKey of config.feature_keys ?? []) {
|
|
832
|
+
if (!hasNode(nodes, featureKey) || edgeExists(edges, config.node_key, featureKey, "exercises")) continue;
|
|
833
|
+
edges.push(
|
|
834
|
+
makeEdge(
|
|
835
|
+
config.node_key,
|
|
836
|
+
featureKey,
|
|
837
|
+
"exercises",
|
|
838
|
+
ko ? "사용자 시나리오가 이 기능을 실제 흐름에서 사용한다." : "The user scenario exercises this feature in a real product flow.",
|
|
839
|
+
evidence[0] ?? "local repository bootstrap"
|
|
840
|
+
)
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function buildProjectContext({ projectKey, packageJson, packageName, repoName, localPath, remote, branch, docs, files, urls, language }) {
|
|
847
|
+
const description = compactText(packageJson?.description || firstParagraph(docs[0]?.text ?? "") || `${repoName} repository`, 220);
|
|
848
|
+
const docLines = docs.flatMap((doc) => extractDocLines(doc.text, 10)).slice(0, 18);
|
|
849
|
+
const evidenceSummary = compactText(docLines.slice(0, 5).join(" / ") || "package scripts and source tree", 360);
|
|
850
|
+
const productionUrl = inferProductionUrl(urls);
|
|
851
|
+
const buildCommand = scriptCommand(packageJson, ["build", "web:azure:build", "web:e2e:build"]);
|
|
852
|
+
const testCommand = scriptCommand(packageJson, ["test", "typecheck", "lint"]);
|
|
853
|
+
const smokeCommand = scriptCommand(packageJson, ["test:e2e:smoke", "smoke", "seo:health"]);
|
|
854
|
+
const installCommand = files.includes("package-lock.json") ? "npm ci" : packageJson ? "npm install" : "";
|
|
855
|
+
const ko = language === "ko";
|
|
856
|
+
const observedDocs = docs.map((doc) => doc.relative).join(", ") || "no product docs";
|
|
857
|
+
|
|
858
|
+
return {
|
|
859
|
+
project_key: projectKey,
|
|
860
|
+
project_name: packageName || repoName || titleCase(projectKey.toLowerCase()),
|
|
861
|
+
local_path_hint: localPath,
|
|
862
|
+
repository_owner: remote.owner,
|
|
863
|
+
repository_repo: remote.repo,
|
|
864
|
+
default_branch: branch || "main",
|
|
865
|
+
production_url: productionUrl,
|
|
866
|
+
project_notes: ko
|
|
867
|
+
? `${description} 근거 문서: ${observedDocs}.`
|
|
868
|
+
: `${description} Evidence docs: ${observedDocs}.`,
|
|
869
|
+
spec_summary: ko
|
|
870
|
+
? `${packageName || repoName}의 현재 레포 기준 기능 표면을 자동 분석했다. 주요 근거: ${evidenceSummary}.`
|
|
871
|
+
: `Automatic bootstrap analyzed the current repository feature surface for ${packageName || repoName}. Main evidence: ${evidenceSummary}.`,
|
|
872
|
+
planning_considerations: ko
|
|
873
|
+
? "현재 코드, 테스트, 배포 스크립트, 최신 문서를 우선 근거로 삼는다. 코드에 보이지만 제품 의도가 불명확한 내용은 버그로 확정하지 않고 기획부재 또는 결정필요로 남긴다."
|
|
874
|
+
: "Prioritize current code, tests, deployment scripts, and recent docs. Behavior visible in code but unclear as product intent must remain spec_gap or decision_needed, not an automatic bug target.",
|
|
875
|
+
known_conflicts: ko
|
|
876
|
+
? "자동 부트스트랩은 문서와 코드의 시간차를 완전히 판정할 수 없다. 충돌이 보이면 최신 코드/테스트와 owner 결정을 우선하고, 오래된 문서만 근거인 기대 동작은 stale/spec_gap으로 분류한다."
|
|
877
|
+
: "Automatic bootstrap cannot fully resolve doc/code drift. If conflict appears, prefer current code/tests and owner decisions; expectations found only in stale docs should classify as stale/spec_gap.",
|
|
878
|
+
install_command: installCommand,
|
|
879
|
+
build_command: buildCommand,
|
|
880
|
+
test_command: testCommand,
|
|
881
|
+
smoke_command: smokeCommand
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function projectIntentSummary({ ko, packageName, repoName, hasIntentEvidence }) {
|
|
886
|
+
const productName = packageName || repoName;
|
|
887
|
+
if (hasIntentEvidence) {
|
|
888
|
+
return ko
|
|
889
|
+
? `${productName}의 제품 의도는 문서와 소스 근거로 확인됐다. 근거 밖 기대 동작은 결정 필요로 분류한다.`
|
|
890
|
+
: `${productName} product intent is grounded in docs and source evidence; expectations outside that evidence require owner decision.`;
|
|
891
|
+
}
|
|
892
|
+
return ko
|
|
893
|
+
? "제품 의도 근거가 아직 부족하다. 사용자가 목표, 대상 사용자, 핵심 흐름을 입력해야 다음 판단을 진행할 수 있다."
|
|
894
|
+
: "Product intent evidence is missing. The owner must provide goals, users, and core flows before downstream judgment.";
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
export function analyzeRepository({ localPath, projectKey, remote, branch = "main", commitSha = "", nodeDisplayLanguage = "" }) {
|
|
898
|
+
const root = path.resolve(localPath);
|
|
899
|
+
const files = walkRepo(root);
|
|
900
|
+
const packageJson = safeJson(path.join(root, "package.json"));
|
|
901
|
+
const packageName = packageJson?.name || "";
|
|
902
|
+
const repoName = remote?.repo || path.basename(root);
|
|
903
|
+
const docCandidates = files
|
|
904
|
+
.filter((file) => DOC_BASENAMES.has(path.basename(file)) || /^docs\/(product-plan|architecture|.*개요|.*설계|.*아키텍처).*\.md$/i.test(file))
|
|
905
|
+
.sort((a, b) => docPriority(a) - docPriority(b) || a.localeCompare(b))
|
|
906
|
+
.slice(0, 12);
|
|
907
|
+
const docs = docCandidates
|
|
908
|
+
.map((relative) => ({ relative, text: safeRead(path.join(root, relative), 220_000) }))
|
|
909
|
+
.filter((doc) => doc.text.trim());
|
|
910
|
+
const docText = docs.map((doc) => doc.text).join("\n");
|
|
911
|
+
const inferredLanguage = hasHangul(docText) || hasHangul(packageJson?.description ?? "") ? "ko" : "en";
|
|
912
|
+
const language = nodeDisplayLanguage === "ko" || nodeDisplayLanguage === "en" ? nodeDisplayLanguage : inferredLanguage;
|
|
913
|
+
const ko = language === "ko";
|
|
914
|
+
const urls = Array.from(new Set([...extractUrlsFromDocs(docs), ...extractUrlsFromFiles(root, files)]));
|
|
915
|
+
const nodes = [];
|
|
916
|
+
const edges = [];
|
|
917
|
+
|
|
918
|
+
const projectContext = buildProjectContext({
|
|
919
|
+
projectKey,
|
|
920
|
+
packageJson,
|
|
921
|
+
packageName,
|
|
922
|
+
repoName,
|
|
923
|
+
localPath: root,
|
|
924
|
+
remote,
|
|
925
|
+
branch,
|
|
926
|
+
docs,
|
|
927
|
+
files,
|
|
928
|
+
urls,
|
|
929
|
+
language
|
|
930
|
+
});
|
|
931
|
+
const hasIntentEvidence = Boolean(docs.length || packageJson?.description);
|
|
932
|
+
const repositorySummaryKo = `${remote.owner}/${remote.repo} 레포가 ${root} 경로와 ${branch || "unknown"} 브랜치로 검증되어 로컬 에이전트가 수정 대상을 식별할 수 있다.`;
|
|
933
|
+
const repositorySummaryEn = `${remote.owner}/${remote.repo} is verified on branch ${branch || "unknown"}, so local agents can identify the correct target repository.`;
|
|
934
|
+
|
|
935
|
+
addNode(
|
|
936
|
+
nodes,
|
|
937
|
+
makeNode({
|
|
938
|
+
node_key: "input_required.repository",
|
|
939
|
+
type: "state",
|
|
940
|
+
title: ko ? "코드 레포 연결 상태" : "Repository connection state",
|
|
941
|
+
summary: ko ? repositorySummaryKo : repositorySummaryEn,
|
|
942
|
+
product_state: "verified",
|
|
943
|
+
confidence: 0.94,
|
|
944
|
+
evidence: ["git rev-parse --show-toplevel", "git remote get-url origin", "git branch --show-current"],
|
|
945
|
+
display: {
|
|
946
|
+
ko: {
|
|
947
|
+
title: "코드 레포 연결 상태",
|
|
948
|
+
summary: repositorySummaryKo
|
|
949
|
+
},
|
|
950
|
+
en: {
|
|
951
|
+
title: "Repository connection state",
|
|
952
|
+
summary: repositorySummaryEn
|
|
953
|
+
}
|
|
954
|
+
},
|
|
955
|
+
terms: { repository_binding: "Verified identity tying a Work Q project to a local git repository." },
|
|
956
|
+
rules: [{ if: "observed repo/path does not match this binding", then: "do not claim or patch tasks for this project" }]
|
|
957
|
+
})
|
|
958
|
+
);
|
|
959
|
+
|
|
960
|
+
addNode(
|
|
961
|
+
nodes,
|
|
962
|
+
makeNode({
|
|
963
|
+
node_key: "input_required.project_intent",
|
|
964
|
+
type: "state",
|
|
965
|
+
title: ko ? "제품 의도 연결 상태" : "Product intent state",
|
|
966
|
+
summary: projectIntentSummary({ ko, packageName, repoName, hasIntentEvidence }),
|
|
967
|
+
product_state: hasIntentEvidence ? "verified" : "input_required",
|
|
968
|
+
status: hasIntentEvidence ? "approved" : "draft",
|
|
969
|
+
confidence: docs.length ? 0.88 : 0.64,
|
|
970
|
+
evidence: docs.map((doc) => doc.relative).concat(packageJson ? ["package.json"] : []),
|
|
971
|
+
display: {
|
|
972
|
+
ko: {
|
|
973
|
+
title: "제품 의도 연결 상태",
|
|
974
|
+
summary: projectIntentSummary({ ko: true, packageName, repoName, hasIntentEvidence })
|
|
975
|
+
},
|
|
976
|
+
en: {
|
|
977
|
+
title: "Product intent state",
|
|
978
|
+
summary: projectIntentSummary({ ko: false, packageName, repoName, hasIntentEvidence })
|
|
979
|
+
}
|
|
980
|
+
},
|
|
981
|
+
terms: { product_intent: "The product purpose used to classify reports against expected behavior." },
|
|
982
|
+
rules: [{ if: "owner intent is absent or ambiguous", then: "classify adjacent work as input_required or decision_needed" }],
|
|
983
|
+
rationale: [projectContext.spec_summary, projectContext.planning_considerations, projectContext.known_conflicts]
|
|
984
|
+
})
|
|
985
|
+
);
|
|
986
|
+
|
|
987
|
+
addNode(
|
|
988
|
+
nodes,
|
|
989
|
+
makeNode({
|
|
990
|
+
node_key: "input_required.deployment",
|
|
991
|
+
type: "state",
|
|
992
|
+
title: projectContext.production_url ? (ko ? "배포 대상 연결 상태" : "Deployment target state") : ko ? "배포 정보 입력 필요" : "Deployment information required",
|
|
993
|
+
summary: projectContext.production_url
|
|
994
|
+
? ko
|
|
995
|
+
? `운영 URL ${projectContext.production_url} 이 문서에서 확인되어 배포 후 smoke 기준으로 사용할 수 있다.`
|
|
996
|
+
: `Production URL ${projectContext.production_url} was found in docs and can be used for post-deploy smoke checks.`
|
|
997
|
+
: ko
|
|
998
|
+
? "운영 URL이나 배포 확인 기준이 문서에서 확인되지 않았다. 배포 검증이 필요한 작업은 입력 필요 상태로 남긴다."
|
|
999
|
+
: "No production URL or deploy verification target was found in docs. Work requiring deployment verification remains input_required.",
|
|
1000
|
+
product_state: projectContext.production_url ? "verified" : "input_required",
|
|
1001
|
+
status: projectContext.production_url ? "approved" : "draft",
|
|
1002
|
+
confidence: projectContext.production_url ? 0.86 : 0.62,
|
|
1003
|
+
evidence: docs.map((doc) => doc.relative).concat(findFiles(files, [/staticwebapp\.config/, /vercel\.json/, /^infra\//], 4)),
|
|
1004
|
+
display: {
|
|
1005
|
+
ko: {
|
|
1006
|
+
title: projectContext.production_url ? "배포 대상 연결 상태" : "배포 정보 입력 필요",
|
|
1007
|
+
summary: projectContext.production_url
|
|
1008
|
+
? `운영 URL ${projectContext.production_url} 이 문서에서 확인되어 배포 후 smoke 기준으로 사용할 수 있다.`
|
|
1009
|
+
: "운영 URL이나 배포 확인 기준이 문서에서 확인되지 않았다. 배포 검증이 필요한 작업은 입력 필요 상태로 남긴다."
|
|
1010
|
+
},
|
|
1011
|
+
en: {
|
|
1012
|
+
title: projectContext.production_url ? "Deployment target state" : "Deployment information required",
|
|
1013
|
+
summary: projectContext.production_url
|
|
1014
|
+
? `Production URL ${projectContext.production_url} was found in docs and can be used for post-deploy smoke checks.`
|
|
1015
|
+
: "No production URL or deploy verification target was found in docs. Work requiring deployment verification remains input_required."
|
|
1016
|
+
}
|
|
1017
|
+
},
|
|
1018
|
+
terms: { production_url: "URL used to verify deployed behavior." }
|
|
1019
|
+
})
|
|
1020
|
+
);
|
|
1021
|
+
|
|
1022
|
+
addNode(
|
|
1023
|
+
nodes,
|
|
1024
|
+
makeNode({
|
|
1025
|
+
node_key: "feature.project_bootstrap_analysis",
|
|
1026
|
+
title: ko ? "신규 프로젝트 분석과 지도 부트스트랩" : "New project analysis and map bootstrap",
|
|
1027
|
+
summary: ko
|
|
1028
|
+
? "로컬 MCP/CLI가 README, 제품 문서, package scripts, 라우트, API, 테스트, 배포 설정을 읽어 초기 기능명세 지도를 만들고 불확실한 의도는 별도 상태 노드로 남긴다."
|
|
1029
|
+
: "The local MCP/CLI reads README, product docs, package scripts, routes, APIs, tests, and deploy config to build the initial map while preserving uncertain intent as state nodes.",
|
|
1030
|
+
confidence: 0.9,
|
|
1031
|
+
evidence: docs.map((doc) => doc.relative).concat(["package.json"]).filter((item, index, arr) => item && arr.indexOf(item) === index),
|
|
1032
|
+
display: {
|
|
1033
|
+
ko: {
|
|
1034
|
+
title: "신규 프로젝트 분석과 지도 부트스트랩",
|
|
1035
|
+
summary:
|
|
1036
|
+
"로컬 MCP/CLI가 README, 제품 문서, package scripts, 라우트, API, 테스트, 배포 설정을 읽어 초기 기능명세 지도를 만들고 불확실한 의도는 별도 상태 노드로 남긴다."
|
|
1037
|
+
},
|
|
1038
|
+
en: {
|
|
1039
|
+
title: "New project analysis and map bootstrap",
|
|
1040
|
+
summary:
|
|
1041
|
+
"The local MCP/CLI reads README, product docs, package scripts, routes, APIs, tests, and deploy config to build the initial map while preserving uncertain intent as state nodes."
|
|
1042
|
+
}
|
|
1043
|
+
},
|
|
1044
|
+
terms: { bootstrap_analysis: "Local repository analysis that creates the first Work Q product-state map." }
|
|
1045
|
+
})
|
|
1046
|
+
);
|
|
1047
|
+
|
|
1048
|
+
for (const config of specificFeatureConfigs(packageName)) addFeatureIfEvidence(nodes, files, config, language);
|
|
1049
|
+
for (const config of genericFeatureConfigs()) addFeatureIfEvidence(nodes, files, config, language);
|
|
1050
|
+
|
|
1051
|
+
if (hasAny(files, [/^mcp\//, /^packages\/[^/]*mcp/i, /^scripts\/.*mcp/i, /^docs\/.*mcp/i])) {
|
|
1052
|
+
addFeatureIfEvidence(
|
|
1053
|
+
nodes,
|
|
1054
|
+
files,
|
|
1055
|
+
{
|
|
1056
|
+
node_key: "feature.mcp_integration",
|
|
1057
|
+
title_ko: "MCP/에이전트 연동",
|
|
1058
|
+
title_en: "MCP and agent integration",
|
|
1059
|
+
summary_ko: "MCP 서버, CLI, 인증 스크립트 또는 패키지가 외부 AI/데스크톱 도구와 제품 데이터를 연결한다.",
|
|
1060
|
+
summary_en: "MCP servers, CLIs, auth scripts, or packages connect product data to external AI/desktop tools.",
|
|
1061
|
+
patterns: [/^mcp\//, /^packages\/[^/]*mcp/i, /^scripts\/.*mcp/i, /^docs\/.*mcp/i],
|
|
1062
|
+
confidence: 0.78
|
|
1063
|
+
},
|
|
1064
|
+
language
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
if (hasAny(files, [/extension/i, /^extensions\//])) {
|
|
1069
|
+
addFeatureIfEvidence(
|
|
1070
|
+
nodes,
|
|
1071
|
+
files,
|
|
1072
|
+
{
|
|
1073
|
+
node_key: "feature.external_extension_integration",
|
|
1074
|
+
title_ko: "브라우저/외부 확장 연동",
|
|
1075
|
+
title_en: "Browser or external extension integration",
|
|
1076
|
+
summary_ko: "확장 프로그램 또는 외부 연동 코드가 제품의 데이터를 브라우저나 다른 도구에서 캡처하거나 전달한다.",
|
|
1077
|
+
summary_en: "Extension or external integration code captures or passes product data through browsers or other tools.",
|
|
1078
|
+
patterns: [/extension/i, /^extensions\//],
|
|
1079
|
+
confidence: 0.76
|
|
1080
|
+
},
|
|
1081
|
+
language
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
addUseCases(nodes, edges, files, packageName, language);
|
|
1086
|
+
|
|
1087
|
+
const featureKeys = nodes.filter((node) => node.type === "feature").map((node) => node.node_key);
|
|
1088
|
+
const useCaseKeys = nodes.filter((node) => node.type === "use_case").map((node) => node.node_key);
|
|
1089
|
+
for (const key of featureKeys) {
|
|
1090
|
+
if (key !== "feature.project_bootstrap_analysis") {
|
|
1091
|
+
edges.push(makeEdge("feature.project_bootstrap_analysis", key, "derived_from", ko ? "레포 부트스트랩 분석에서 발견된 기능이다." : "Feature found during repository bootstrap analysis.", "local repository bootstrap"));
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
for (const key of useCaseKeys) {
|
|
1095
|
+
edges.push(makeEdge("feature.project_bootstrap_analysis", key, "derived_from", ko ? "레포 부트스트랩 분석에서 발견된 사용자 시나리오다." : "User scenario found during repository bootstrap analysis.", "local repository bootstrap"));
|
|
1096
|
+
}
|
|
1097
|
+
if (featureKeys.includes("feature.testing_quality_gates")) {
|
|
1098
|
+
for (const key of featureKeys.filter((featureKey) => featureKey !== "feature.testing_quality_gates").slice(0, 8)) {
|
|
1099
|
+
edges.push(makeEdge("feature.testing_quality_gates", key, "covered_by", ko ? "테스트/품질 게이트가 이 기능의 회귀 검증 후보가 된다." : "Tests and quality gates are candidate regression checks for this feature.", "package scripts and test files"));
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
if (featureKeys.includes("feature.deployment_automation") && featureKeys.includes("feature.testing_quality_gates")) {
|
|
1103
|
+
edges.push(makeEdge("feature.deployment_automation", "feature.testing_quality_gates", "requires", ko ? "배포 전 품질 게이트 실행이 필요하다." : "Deployment should run quality gates first.", "package scripts"));
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
const summary = ko
|
|
1107
|
+
? `로컬 레포 자동 분석으로 ${nodes.length}개 노드와 ${edges.length}개 관계를 생성했다. 근거 파일 ${files.length}개 중 문서 ${docs.length}개와 주요 소스 경로를 사용했다.`
|
|
1108
|
+
: `Local repository bootstrap generated ${nodes.length} nodes and ${edges.length} edges from ${files.length} files, using ${docs.length} docs plus source paths.`;
|
|
1109
|
+
|
|
1110
|
+
return {
|
|
1111
|
+
projectContext,
|
|
1112
|
+
productMap: {
|
|
1113
|
+
summary,
|
|
1114
|
+
nodes,
|
|
1115
|
+
edges
|
|
1116
|
+
},
|
|
1117
|
+
analysis: {
|
|
1118
|
+
language,
|
|
1119
|
+
packageName,
|
|
1120
|
+
file_count: files.length,
|
|
1121
|
+
doc_files: docs.map((doc) => doc.relative),
|
|
1122
|
+
production_url: projectContext.production_url,
|
|
1123
|
+
commit_sha: commitSha
|
|
1124
|
+
}
|
|
1125
|
+
};
|
|
1126
|
+
}
|