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 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, creates `natural-git/README.md`,
71
- `natural-git/code-index.md`, and one `natural-git/code/*.md` document per
72
- code/config file, then imports those docs into the Work Q v2 natural-spec repo.
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,
@@ -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 titleText = extractVisibleTexts(file)[0];
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}\` -> \`${docPathForSource(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/code-index.md",
510
- title: "코드 파일 자연어 색인",
770
+ path: "natural-git/source-index.md",
771
+ title: "자연어 명세 색인",
511
772
  content: indexDoc(sourceFiles)
512
773
  },
513
- ...sourceFiles.map((file) => ({
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workq-mcp",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Work Q MCP stdio bridge and local repository connection CLI.",
5
5
  "type": "module",
6
6
  "bin": {