workq-mcp 0.1.5 → 0.1.6
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 +8 -3
- package/lib/natural-git.mjs +285 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,9 +67,14 @@ cd /Users/jin/code/lotto
|
|
|
67
67
|
workq natural-git --project-key LOTTO
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
This command reads the local repository
|
|
71
|
-
`natural-git/
|
|
72
|
-
|
|
70
|
+
This command reads the local repository and creates a hierarchical natural-spec
|
|
71
|
+
repo: `natural-git/README.md`, `natural-git/source-index.md`, and feature
|
|
72
|
+
directories such as `natural-git/features/.../README.md`,
|
|
73
|
+
`natural-git/features/.../rules.md`, and
|
|
74
|
+
`natural-git/features/.../verification.md`. API, CLI, runtime, data, and test
|
|
75
|
+
evidence are grouped under `interfaces/`, `operations/`, `data/`, and
|
|
76
|
+
`verification/`. The generated docs are then imported into the Work Q v2
|
|
77
|
+
natural-spec repo.
|
|
73
78
|
After that, open Work Q v2 and run `자연어 커밋 -> 명세 머지 -> 커밋 승인·Agent 큐`.
|
|
74
79
|
Work Q creates a queued MCP work packet from the accepted natural commit. Your
|
|
75
80
|
desktop Codex/Claude agent should claim that task, update the local repository,
|
package/lib/natural-git.mjs
CHANGED
|
@@ -136,10 +136,6 @@ function languageForExtension(extension) {
|
|
|
136
136
|
return map[extension] ?? extension.replace(".", "").toUpperCase();
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
function docPathForSource(sourcePath) {
|
|
140
|
-
return `natural-git/code/${sourcePath.replace(/\//g, "__")}.md`;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
139
|
function cleanLine(line) {
|
|
144
140
|
const trimmed = line.trim();
|
|
145
141
|
if (!trimmed || SECRET_LINE.test(trimmed)) return "";
|
|
@@ -158,6 +154,31 @@ function unique(values, limit = 40) {
|
|
|
158
154
|
|
|
159
155
|
const NATURAL_SPEC_FORMAT_COMMENT = "<!-- workq-natural-spec-format: feature-v2 -->";
|
|
160
156
|
|
|
157
|
+
function slugSegment(value, fallback = "spec") {
|
|
158
|
+
return (
|
|
159
|
+
String(value ?? "")
|
|
160
|
+
.toLowerCase()
|
|
161
|
+
.replace(/\.[a-z0-9]+$/i, "")
|
|
162
|
+
.replace(/[^a-z0-9가-힣]+/g, "-")
|
|
163
|
+
.replace(/^-+|-+$/g, "")
|
|
164
|
+
.slice(0, 72) || fallback
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function sourcePathSlug(sourcePath) {
|
|
169
|
+
return slugSegment(sourcePath.replace(/\.[^.]+$/i, ""), "source");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isTestFile(file) {
|
|
173
|
+
const normalized = file.path.toLowerCase();
|
|
174
|
+
const basename = path.basename(normalized);
|
|
175
|
+
return (
|
|
176
|
+
/(^|\/)(tests?|e2e|__tests__|spec)\//.test(normalized) ||
|
|
177
|
+
/\.(spec|test)\.[cm]?[jt]sx?$/.test(normalized) ||
|
|
178
|
+
/^test[_-]/.test(basename)
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
161
182
|
function stripTags(value) {
|
|
162
183
|
return value.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
|
163
184
|
}
|
|
@@ -178,6 +199,11 @@ function extractVisibleTexts(file) {
|
|
|
178
199
|
return unique(texts.map((item) => compact(item, 100)).filter((item) => item && !/^[{}()[\];,.]+$/.test(item)), 18);
|
|
179
200
|
}
|
|
180
201
|
|
|
202
|
+
function primaryVisibleTitle(file) {
|
|
203
|
+
const texts = extractVisibleTexts(file);
|
|
204
|
+
return texts.find((item) => item.length >= 2 && item.length <= 40) ?? "";
|
|
205
|
+
}
|
|
206
|
+
|
|
181
207
|
function extractDesignFacts(file) {
|
|
182
208
|
if (![".css", ".html", ".tsx", ".jsx"].includes(file.extension)) return [];
|
|
183
209
|
const facts = [];
|
|
@@ -235,6 +261,63 @@ function isCliEntryFile(file) {
|
|
|
235
261
|
);
|
|
236
262
|
}
|
|
237
263
|
|
|
264
|
+
function isApiOrServerFile(file) {
|
|
265
|
+
const normalized = file.path.toLowerCase();
|
|
266
|
+
return (
|
|
267
|
+
/(^|\/)(api|routes?|server|controllers?)\//.test(normalized) ||
|
|
268
|
+
/(^|\/)(server|route|router|api)\.[cm]?[jt]sx?$/.test(normalized) ||
|
|
269
|
+
/\b(app|router)\.(get|post|put|patch|delete|use)\s*\(/i.test(file.content)
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function isDataModelFile(file) {
|
|
274
|
+
const normalized = file.path.toLowerCase();
|
|
275
|
+
return (
|
|
276
|
+
/(^|\/)(models?|schemas?|entities|migrations?)\//.test(normalized) ||
|
|
277
|
+
/(model|schema|entity)\.[cm]?[jt]sx?$/.test(normalized) ||
|
|
278
|
+
/new\s+mongoose\.Schema\s*\(/.test(file.content)
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function isConfigurationFile(file) {
|
|
283
|
+
const normalized = file.path.toLowerCase();
|
|
284
|
+
const basename = path.basename(normalized);
|
|
285
|
+
return (
|
|
286
|
+
file.path === "package.json" ||
|
|
287
|
+
isEnvironmentConfigFile(file) ||
|
|
288
|
+
/(^|\/)(config|configs|scripts?|ci|\.github)\//.test(normalized) ||
|
|
289
|
+
/^(package-lock|pnpm-lock|yarn|tsconfig|next\.config|vite\.config|playwright\.config|dockerfile)/.test(basename)
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function isUserFacingFile(file) {
|
|
294
|
+
const normalized = file.path.toLowerCase();
|
|
295
|
+
return (
|
|
296
|
+
[".html", ".css", ".jsx", ".tsx"].includes(file.extension) ||
|
|
297
|
+
extractVisibleTexts(file).length > 0 ||
|
|
298
|
+
/(^|\/)(app|pages|components|ui|views|screens)\//.test(normalized)
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function hasImplementationLogic(file) {
|
|
303
|
+
return /(?:function\s+[A-Za-z_$][\w$]*|def\s+[A-Za-z_][\w]*|=>|\bfor\s*\(|\bif\s*\()/.test(file.content);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function specDirectoryForSource(file) {
|
|
307
|
+
if (isTestFile(file)) return `natural-git/verification/e2e/${sourcePathSlug(file.path)}`;
|
|
308
|
+
if (isCliEntryFile(file)) return `natural-git/interfaces/cli/${/\bworkq\s+/.test(file.content) ? "workq-cli" : sourcePathSlug(file.path)}`;
|
|
309
|
+
if (isApiOrServerFile(file)) return `natural-git/interfaces/api/${sourcePathSlug(file.path)}`;
|
|
310
|
+
if (isDataModelFile(file)) return `natural-git/data/models/${sourcePathSlug(file.path)}`;
|
|
311
|
+
if (isConfigurationFile(file)) {
|
|
312
|
+
if (file.path === "package.json") return "natural-git/operations/runtime/project-runtime";
|
|
313
|
+
if (isEnvironmentConfigFile(file)) return "natural-git/operations/runtime/runtime-environment";
|
|
314
|
+
return `natural-git/operations/configuration/${sourcePathSlug(file.path)}`;
|
|
315
|
+
}
|
|
316
|
+
if (isUserFacingFile(file)) return `natural-git/features/${slugSegment(primaryVisibleTitle(file) || file.path, sourcePathSlug(file.path))}`;
|
|
317
|
+
if (hasImplementationLogic(file)) return `natural-git/features/${sourcePathSlug(file.path)}`;
|
|
318
|
+
return `natural-git/supporting/${sourcePathSlug(file.path)}`;
|
|
319
|
+
}
|
|
320
|
+
|
|
238
321
|
function extractCliFacts(file) {
|
|
239
322
|
if (!isCliEntryFile(file)) return [];
|
|
240
323
|
const facts = [];
|
|
@@ -360,7 +443,113 @@ function packageJsonDoc(content) {
|
|
|
360
443
|
}
|
|
361
444
|
}
|
|
362
445
|
|
|
446
|
+
function humanizeTestTitle(title) {
|
|
447
|
+
const normalized = title.replace(/\s+/g, " ").trim();
|
|
448
|
+
const known = [
|
|
449
|
+
[/regenerates legacy line-by-line natural specs on import/i, "예전 line-by-line 자연어 명세를 가져오면 기능 중심 명세로 재생성되는지 검증한다."],
|
|
450
|
+
[/registers a repo and turns its code into natural-language git specs/i, "새 프로젝트를 연결하고 로컬 코드 저장소를 자연어 명세 저장소로 변환하는 전체 흐름을 검증한다."],
|
|
451
|
+
[/bug report to desktop agent MCP completion flow/i, "버그 리포트가 MCP 작업 큐로 전달되고 데스크톱 에이전트 완료 처리까지 이어지는지 검증한다."],
|
|
452
|
+
[/project selector persists/i, "화면 새로고침 뒤에도 사용자가 선택한 프로젝트가 유지되는지 검증한다."],
|
|
453
|
+
[/clipboard/i, "사용자가 클립보드나 첨부 입력으로 리포트 근거를 남길 수 있는지 검증한다."],
|
|
454
|
+
[/natural specs/i, "자연어 명세 생성과 반영 흐름을 검증한다."],
|
|
455
|
+
[/local repository analyzer/i, "로컬 레포 분석기가 제품 기능과 유즈케이스를 추출하는지 검증한다."]
|
|
456
|
+
];
|
|
457
|
+
const match = known.find(([pattern]) => pattern.test(normalized));
|
|
458
|
+
if (match) return match[1];
|
|
459
|
+
return `"${normalized}" 시나리오가 사용자 관점의 기대 동작을 만족하는지 검증한다.`;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function extractTestScenarios(file) {
|
|
463
|
+
const scenarios = [];
|
|
464
|
+
for (const match of file.content.matchAll(/\b(?:test|it)\s*\(\s*["'`]([^"'`]{3,180})["'`]/g)) {
|
|
465
|
+
scenarios.push(humanizeTestTitle(match[1]));
|
|
466
|
+
}
|
|
467
|
+
return unique(scenarios, 16);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function extractApiInteractions(file) {
|
|
471
|
+
const endpoints = [];
|
|
472
|
+
for (const match of file.content.matchAll(/\brequest\.(get|post|put|patch|delete)\s*\(\s*["'`]([^"'`]+)["'`]/g)) {
|
|
473
|
+
endpoints.push(`${match[1].toUpperCase()} ${match[2]}`);
|
|
474
|
+
}
|
|
475
|
+
for (const match of file.content.matchAll(/\bfetch\s*\(\s*["'`]([^"'`]+)["'`]/g)) {
|
|
476
|
+
endpoints.push(`FETCH ${match[1]}`);
|
|
477
|
+
}
|
|
478
|
+
return unique(endpoints, 20);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function extractMcpToolNames(file) {
|
|
482
|
+
const tools = [];
|
|
483
|
+
for (const match of file.content.matchAll(/\bname\s*:\s*["'`](workq_[^"'`]+)["'`]/g)) {
|
|
484
|
+
tools.push(match[1]);
|
|
485
|
+
}
|
|
486
|
+
return unique(tools, 20);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function testFileTitle(file) {
|
|
490
|
+
if (/v2/i.test(file.path)) return "Work Q v2 자연어 명세 저장소 검증 시나리오";
|
|
491
|
+
if (/smoke/i.test(file.path)) return "운영 스모크 검증 시나리오";
|
|
492
|
+
if (/analy/i.test(file.path)) return "레포 분석과 자연어 명세 생성 검증 시나리오";
|
|
493
|
+
return "제품 검증 시나리오 명세";
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function testFileToDoc(file) {
|
|
497
|
+
const scenarios = extractTestScenarios(file);
|
|
498
|
+
const endpoints = extractApiInteractions(file);
|
|
499
|
+
const mcpTools = extractMcpToolNames(file);
|
|
500
|
+
const inferred = [];
|
|
501
|
+
if (/\/api\/auth\/login/.test(file.content)) inferred.push("오너가 테스트 계정으로 로그인한다.");
|
|
502
|
+
if (/connect-project/.test(file.content)) inferred.push("사용자가 프로젝트 키, 레포, 로컬 경로를 등록한다.");
|
|
503
|
+
if (/workq_import_natural_git_specs/.test(file.content)) inferred.push("로컬 MCP가 생성한 자연어 명세 문서를 Work Q에 업로드한다.");
|
|
504
|
+
if (/\/api\/v2\/spec-git\/commit/.test(file.content)) inferred.push("사용자가 자연어 명세 변경을 커밋으로 승인한다.");
|
|
505
|
+
if (/\/api\/v2\/spec-git\/merge/.test(file.content)) inferred.push("Work Q가 자연어 커밋을 머지 가능한 상태인지 검사한다.");
|
|
506
|
+
if (/\/api\/v2\/spec-git\/deploy/.test(file.content)) inferred.push("승인된 자연어 커밋이 MCP Agent 작업 큐로 전달된다.");
|
|
507
|
+
if (/workq_get_work_packet/.test(file.content)) inferred.push("데스크톱 에이전트가 작업 패킷을 받아 실제 코드 작업 기준을 확인한다.");
|
|
508
|
+
if (/workq_finish_task/.test(file.content)) inferred.push("에이전트가 작업 완료 후 검증 결과를 Work Q에 남긴다.");
|
|
509
|
+
return [
|
|
510
|
+
`# ${testFileTitle(file)}`,
|
|
511
|
+
NATURAL_SPEC_FORMAT_COMMENT,
|
|
512
|
+
"",
|
|
513
|
+
"## 기능 중심 자연어 명세",
|
|
514
|
+
"이 명세는 제품 기능 자체가 아니라, 사용자가 기대하는 Work Q 흐름이 실제로 동작하는지 확인하는 검증 시나리오를 정의한다.",
|
|
515
|
+
"",
|
|
516
|
+
"## 검증하는 사용자 흐름",
|
|
517
|
+
...unique([...scenarios, ...inferred], 20).map((item) => `- ${item}`),
|
|
518
|
+
"",
|
|
519
|
+
endpoints.length ? "## 사용되는 웹 API" : "",
|
|
520
|
+
...endpoints.map((item) => `- ${item}`),
|
|
521
|
+
"",
|
|
522
|
+
mcpTools.length ? "## 사용되는 MCP 작업" : "",
|
|
523
|
+
...mcpTools.map((item) => `- ${item}`),
|
|
524
|
+
"",
|
|
525
|
+
"## 핵심 규칙",
|
|
526
|
+
"- 테스트 코드는 제품 명세가 아니라 명세와 구현이 맞는지 확인하는 검증 근거로만 사용한다.",
|
|
527
|
+
"- 자연어 명세, 커밋, 머지, Agent 큐, 완료 처리 중 하나라도 끊기면 제품 흐름은 통과한 것으로 보지 않는다.",
|
|
528
|
+
"- 검증 시나리오의 기대값은 사용자가 보는 상태와 MCP Agent가 받는 작업 기준을 함께 확인해야 한다.",
|
|
529
|
+
"",
|
|
530
|
+
"## 검증 관점",
|
|
531
|
+
"- 화면에 표시되는 명세 경로와 편집 내용이 실제 자연어 git 저장소 상태와 일치하는지 확인한다.",
|
|
532
|
+
"- API 응답, MCP 응답, 화면 표시가 같은 프로젝트 키와 같은 자연어 커밋을 가리키는지 확인한다.",
|
|
533
|
+
"- 이 문서는 리포트 분류 시 구현 완료 여부와 회귀 여부를 판단하는 검증 증거로 사용한다."
|
|
534
|
+
].filter((line, index, array) => line || array[index - 1]).join("\n");
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function titleForSourceFile(file) {
|
|
538
|
+
if (isTestFile(file)) return testFileTitle(file);
|
|
539
|
+
if (isCliEntryFile(file)) return "Work Q CLI 기능명세";
|
|
540
|
+
if (file.path === "package.json") return "프로젝트 실행 환경 기능명세";
|
|
541
|
+
const titleText = primaryVisibleTitle(file);
|
|
542
|
+
if (titleText && file.extension === ".html") return `${titleText} 기능명세`;
|
|
543
|
+
if (extractEnvironmentFacts(file).length) return "런타임 환경 설정 기능명세";
|
|
544
|
+
if (extractDataModelFacts(file).length) return "데이터 모델 기능명세";
|
|
545
|
+
if (file.extension === ".css") return "화면 디자인 기능명세";
|
|
546
|
+
if (isApiOrServerFile(file)) return "서버 API 기능명세";
|
|
547
|
+
if (hasImplementationLogic(file)) return `${sourcePathSlug(file.path).replace(/-/g, " ")} 기능명세`;
|
|
548
|
+
return "보조 기능명세";
|
|
549
|
+
}
|
|
550
|
+
|
|
363
551
|
function sourceFileToDoc(file) {
|
|
552
|
+
if (isTestFile(file)) return testFileToDoc(file);
|
|
364
553
|
const structure = {
|
|
365
554
|
imports: [],
|
|
366
555
|
exports: [],
|
|
@@ -399,20 +588,7 @@ function sourceFileToDoc(file) {
|
|
|
399
588
|
],
|
|
400
589
|
12
|
|
401
590
|
);
|
|
402
|
-
const
|
|
403
|
-
const title = isCliEntryFile(file)
|
|
404
|
-
? "Work Q CLI 기능명세"
|
|
405
|
-
: file.path === "package.json"
|
|
406
|
-
? "프로젝트 실행 환경 기능명세"
|
|
407
|
-
: titleText && file.extension === ".html"
|
|
408
|
-
? `${titleText} 기능명세`
|
|
409
|
-
: extractEnvironmentFacts(file).length
|
|
410
|
-
? "런타임 환경 설정 기능명세"
|
|
411
|
-
: dataModelFacts.length
|
|
412
|
-
? "데이터 모델 기능명세"
|
|
413
|
-
: file.extension === ".css"
|
|
414
|
-
? "화면 디자인 기능명세"
|
|
415
|
-
: `${file.path} 기능명세`;
|
|
591
|
+
const title = titleForSourceFile(file);
|
|
416
592
|
return [
|
|
417
593
|
`# ${title}`,
|
|
418
594
|
NATURAL_SPEC_FORMAT_COMMENT,
|
|
@@ -439,6 +615,83 @@ function sourceFileToDoc(file) {
|
|
|
439
615
|
].filter((line, index, array) => line || array[index - 1]).join("\n");
|
|
440
616
|
}
|
|
441
617
|
|
|
618
|
+
function sourceFileRulesDoc(file) {
|
|
619
|
+
const structure = {
|
|
620
|
+
imports: [],
|
|
621
|
+
exports: [],
|
|
622
|
+
classes: [],
|
|
623
|
+
routes: [],
|
|
624
|
+
events: [],
|
|
625
|
+
selectors: [],
|
|
626
|
+
...extractStructure(file.lines)
|
|
627
|
+
};
|
|
628
|
+
const dataModelFacts = extractDataModelFacts(file);
|
|
629
|
+
const functionalFacts = extractFunctionalFacts(file, structure);
|
|
630
|
+
const cliRules = extractCliRules(file);
|
|
631
|
+
const environmentFacts = extractEnvironmentFacts(file);
|
|
632
|
+
const designFacts = extractDesignFacts(file);
|
|
633
|
+
const rules = unique([...dataModelFacts, ...cliRules, ...environmentFacts, ...functionalFacts, ...designFacts], 30);
|
|
634
|
+
return [
|
|
635
|
+
`# ${titleForSourceFile(file)} - 규칙`,
|
|
636
|
+
NATURAL_SPEC_FORMAT_COMMENT,
|
|
637
|
+
"",
|
|
638
|
+
"## 핵심 규칙",
|
|
639
|
+
...(rules.length ? rules.map((item) => `- ${item}`) : ["- 이 기능은 연결된 상위 명세의 기대 동작을 보조한다."]),
|
|
640
|
+
"",
|
|
641
|
+
"## 판단 기준",
|
|
642
|
+
"- 리포트가 이 규칙과 다르게 동작한다고 주장하고 실제 구현도 다르면 버그 후보로 본다.",
|
|
643
|
+
"- 기대 동작은 있지만 이 규칙에 없으면 기획부재 또는 명세추가 후보로 본다.",
|
|
644
|
+
"- 서로 다른 규칙이 동시에 만족될 수 없으면 기능충돌 후보로 본다."
|
|
645
|
+
].join("\n");
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function sourceFileVerificationDoc(file) {
|
|
649
|
+
const structure = {
|
|
650
|
+
imports: [],
|
|
651
|
+
exports: [],
|
|
652
|
+
classes: [],
|
|
653
|
+
routes: [],
|
|
654
|
+
events: [],
|
|
655
|
+
selectors: [],
|
|
656
|
+
...extractStructure(file.lines)
|
|
657
|
+
};
|
|
658
|
+
const verificationFacts = isTestFile(file) ? extractTestScenarios(file) : buildVerificationFacts(file, structure);
|
|
659
|
+
return [
|
|
660
|
+
`# ${titleForSourceFile(file)} - 검증`,
|
|
661
|
+
NATURAL_SPEC_FORMAT_COMMENT,
|
|
662
|
+
"",
|
|
663
|
+
"## 검증해야 할 것",
|
|
664
|
+
...verificationFacts.map((item) => `- ${item}`),
|
|
665
|
+
"",
|
|
666
|
+
"## 완료 기준",
|
|
667
|
+
"- 관련 화면, API, MCP 응답, 또는 데이터 상태가 이 명세의 기대 동작과 일치한다.",
|
|
668
|
+
"- 자동 테스트가 있으면 통과 결과를 Work Q 작업 완료 기록에 남긴다.",
|
|
669
|
+
"- 자동 테스트가 없으면 수동 검증 방법과 남은 위험을 Work Q에 남긴다."
|
|
670
|
+
].join("\n");
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function sourceFileToDocs(file) {
|
|
674
|
+
const directory = specDirectoryForSource(file);
|
|
675
|
+
const title = titleForSourceFile(file);
|
|
676
|
+
return [
|
|
677
|
+
{
|
|
678
|
+
path: `${directory}/README.md`,
|
|
679
|
+
title,
|
|
680
|
+
content: sourceFileToDoc(file)
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
path: `${directory}/rules.md`,
|
|
684
|
+
title: `${title} 규칙`,
|
|
685
|
+
content: sourceFileRulesDoc(file)
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
path: `${directory}/verification.md`,
|
|
689
|
+
title: `${title} 검증`,
|
|
690
|
+
content: sourceFileVerificationDoc(file)
|
|
691
|
+
}
|
|
692
|
+
];
|
|
693
|
+
}
|
|
694
|
+
|
|
442
695
|
function overviewDoc({ projectKey, projectName, repository, root, sourceFiles }) {
|
|
443
696
|
const byLanguage = new Map();
|
|
444
697
|
for (const file of sourceFiles) byLanguage.set(file.language, (byLanguage.get(file.language) ?? 0) + 1);
|
|
@@ -459,6 +712,14 @@ function overviewDoc({ projectKey, projectName, repository, root, sourceFiles })
|
|
|
459
712
|
"## 언어별 파일 수",
|
|
460
713
|
...Array.from(byLanguage.entries()).map(([language, count]) => `- ${language}: ${count}개`),
|
|
461
714
|
"",
|
|
715
|
+
"## 디렉토리 구조",
|
|
716
|
+
"- `natural-git/features/`: 사용자가 직접 경험하는 화면과 기능 명세",
|
|
717
|
+
"- `natural-git/interfaces/`: CLI, API, 외부 연결처럼 기능이 드나드는 접점 명세",
|
|
718
|
+
"- `natural-git/data/`: 데이터 모델과 저장 규칙 명세",
|
|
719
|
+
"- `natural-git/operations/`: 실행 환경, 설정, 배포/운영 조건 명세",
|
|
720
|
+
"- `natural-git/verification/`: 기능이 실제로 동작하는지 확인하는 검증 시나리오 명세",
|
|
721
|
+
"- 각 기능 디렉토리는 `README.md`, `rules.md`, `verification.md`로 나뉘어 개요, 판단 규칙, 검증 기준을 따로 관리한다.",
|
|
722
|
+
"",
|
|
462
723
|
"## 운영 규칙",
|
|
463
724
|
"- 자연어 문서는 코드에서 관찰한 제품 기능과 검증 가능한 동작을 설명한다.",
|
|
464
725
|
"- 각 코드 파일 문서는 라인별 번역이 아니라 기능, 규칙, 화면, 데이터, 검증 관점 중심으로 작성한다.",
|
|
@@ -470,11 +731,11 @@ function overviewDoc({ projectKey, projectName, repository, root, sourceFiles })
|
|
|
470
731
|
|
|
471
732
|
function indexDoc(sourceFiles) {
|
|
472
733
|
return [
|
|
473
|
-
"#
|
|
734
|
+
"# 자연어 명세 색인",
|
|
474
735
|
"",
|
|
475
|
-
"이 문서는 분석된 파일과
|
|
736
|
+
"이 문서는 분석된 파일과 계층형 자연어 명세 디렉토리를 연결한다. 사용자는 보통 이 색인보다 각 기능 디렉토리의 README, rules, verification 문서를 읽는다.",
|
|
476
737
|
"",
|
|
477
|
-
...sourceFiles.map((file) => `- \`${file.path}\` -> \`${
|
|
738
|
+
...sourceFiles.map((file) => `- \`${file.path}\` -> \`${specDirectoryForSource(file)}/\``)
|
|
478
739
|
].join("\n");
|
|
479
740
|
}
|
|
480
741
|
|
|
@@ -506,15 +767,11 @@ export function generateNaturalGitDocs({ localPath, projectKey, projectName = ""
|
|
|
506
767
|
content: overviewDoc({ projectKey, projectName, repository, root, sourceFiles })
|
|
507
768
|
},
|
|
508
769
|
{
|
|
509
|
-
path: "natural-git/
|
|
510
|
-
title: "
|
|
770
|
+
path: "natural-git/source-index.md",
|
|
771
|
+
title: "자연어 명세 색인",
|
|
511
772
|
content: indexDoc(sourceFiles)
|
|
512
773
|
},
|
|
513
|
-
...sourceFiles.
|
|
514
|
-
path: docPathForSource(file.path),
|
|
515
|
-
title: file.path,
|
|
516
|
-
content: sourceFileToDoc(file)
|
|
517
|
-
}))
|
|
774
|
+
...sourceFiles.flatMap((file) => sourceFileToDocs(file))
|
|
518
775
|
];
|
|
519
776
|
return {
|
|
520
777
|
root,
|