workq-mcp 0.1.6 → 0.1.8

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,14 +67,13 @@ cd /Users/jin/code/lotto
67
67
  workq natural-git --project-key LOTTO
68
68
  ```
69
69
 
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.
70
+ This command reads the local repository and creates a screen/region-centered
71
+ natural-spec repo: `natural-git/README.md`, `natural-git/screens/.../README.md`,
72
+ `natural-git/screens/.../regions/*.md`, `natural-git/screens/.../rules.md`, and
73
+ `natural-git/screens/.../verification.md`. Source files, configuration, CLI, and
74
+ runtime details are kept under `natural-git/_evidence/` as traceability evidence,
75
+ not as the default user-facing feature spec. The generated docs are then imported
76
+ into the Work Q v2 natural-spec repo.
78
77
  After that, open Work Q v2 and run `자연어 커밋 -> 명세 머지 -> 커밋 승인·Agent 큐`.
79
78
  Work Q creates a queued MCP work packet from the accepted natural commit. Your
80
79
  desktop Codex/Claude agent should claim that task, update the local repository,
@@ -183,20 +183,58 @@ function stripTags(value) {
183
183
  return value.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
184
184
  }
185
185
 
186
+ function isProbablyCodeText(value) {
187
+ const text = value.trim();
188
+ if (!text) return true;
189
+ if (/[{}<>]|=>|\$\{/.test(text)) return true;
190
+ if (/[;]\s*$/.test(text)) return true;
191
+ if (/\b(?:on[A-Z]\w*|className|aria-label|text\.\w+|event\.|set[A-Z]\w*|function|const|let|return|await|async|JSON\.|api\(|onAction\(|document\.|querySelector)/.test(text)) return true;
192
+ if (/\w+\([^)]*\)/.test(text) && !/[가-힣]/.test(text)) return true;
193
+ if (/^[\w.$-]+\s*[=:]\s*[\w.$-]+$/.test(text)) return true;
194
+ return false;
195
+ }
196
+
197
+ function normalizeVisibleTextCandidate(value) {
198
+ const text = compact(stripTags(value).replace(/&nbsp;/g, " "), 100);
199
+ if (!text || /^[{}()[\];,.]+$/.test(text)) return "";
200
+ if (isProbablyCodeText(text)) return "";
201
+ return text;
202
+ }
203
+
204
+ function pushVisibleText(texts, value) {
205
+ const text = normalizeVisibleTextCandidate(value);
206
+ if (text) texts.push(text);
207
+ }
208
+
186
209
  function extractVisibleTexts(file) {
187
210
  const texts = [];
188
211
  const markupContent = file.content.replace(/<script\b[\s\S]*?<\/script>/gi, "").replace(/<style\b[\s\S]*?<\/style>/gi, "");
189
212
  if (file.extension === ".html") {
190
- for (const match of file.content.matchAll(/<title[^>]*>([\s\S]*?)<\/title>/gi)) texts.push(stripTags(match[1]));
191
- for (const match of markupContent.matchAll(/>([^<>{}][^<>{}]{1,80})</g)) texts.push(stripTags(match[1]));
213
+ for (const match of file.content.matchAll(/<title[^>]*>([\s\S]*?)<\/title>/gi)) pushVisibleText(texts, match[1]);
214
+ }
215
+ if ([".html", ".tsx", ".jsx"].includes(file.extension)) {
216
+ for (const match of markupContent.matchAll(/>([^<>{}][^<>{}]{1,100})</g)) {
217
+ pushVisibleText(texts, match[1]);
218
+ }
192
219
  }
193
220
  for (const match of file.content.matchAll(/\b(?:textContent|innerText|placeholder|aria-label|title)\s*=\s*["'`]([^"'`]{1,100})["'`]/g)) {
194
- texts.push(stripTags(match[1]));
221
+ pushVisibleText(texts, match[1]);
195
222
  }
196
- for (const match of file.content.matchAll(/<button[^>]*>([\s\S]{1,160}?)<\/button>/gi)) {
197
- texts.push(stripTags(match[1]));
223
+ if (file.extension === ".html") {
224
+ for (const match of file.content.matchAll(/<button[^>]*>([\s\S]{1,160}?)<\/button>/gi)) {
225
+ pushVisibleText(texts, match[1]);
226
+ }
198
227
  }
199
- return unique(texts.map((item) => compact(item, 100)).filter((item) => item && !/^[{}()[\];,.]+$/.test(item)), 18);
228
+ return unique(texts, 18);
229
+ }
230
+
231
+ function visibleTextInitialStatements(file) {
232
+ const texts = extractVisibleTexts(file).slice(0, 8);
233
+ if (texts.length) return texts.map((item) => `"${item}" 문구가 보인다.`);
234
+ return [
235
+ "초기 화면에는 이 URL의 기본 콘텐츠와 조작 영역이 표시된다.",
236
+ "동적 문구는 원본 코드 조각이 아니라 실제 렌더링 결과를 기준으로 확인한다."
237
+ ];
200
238
  }
201
239
 
202
240
  function primaryVisibleTitle(file) {
@@ -692,6 +730,528 @@ function sourceFileToDocs(file) {
692
730
  ];
693
731
  }
694
732
 
733
+ function isRoutePageFile(file) {
734
+ return (
735
+ /^src\/app\/(?:.*\/)?page\.(tsx|jsx|ts|js)$/.test(file.path) ||
736
+ /^pages\/(?:.*\/)?[^/]+\.(tsx|jsx|ts|js)$/.test(file.path) ||
737
+ file.path === "index.html"
738
+ );
739
+ }
740
+
741
+ function routeInfoForFile(file) {
742
+ if (file.path === "index.html") {
743
+ const title = primaryVisibleTitle(file) || "Root";
744
+ return {
745
+ url_pattern: "/",
746
+ directory: "natural-git/screens/root",
747
+ title: `${title} 화면 명세`,
748
+ page_path: file.path
749
+ };
750
+ }
751
+ if (["src/app/page.tsx", "src/app/page.jsx", "src/app/page.ts", "src/app/page.js"].includes(file.path)) {
752
+ return {
753
+ url_pattern: "/",
754
+ directory: "natural-git/screens/root",
755
+ title: "/ 화면 명세",
756
+ page_path: file.path
757
+ };
758
+ }
759
+ const appMatch = file.path.match(/^src\/app\/(.+)\/page\.(tsx|jsx|ts|js)$/);
760
+ if (appMatch) {
761
+ const route = appMatch[1]
762
+ .split("/")
763
+ .filter((segment) => segment && !/^\(.+\)$/.test(segment))
764
+ .join("/");
765
+ const normalized = route || "root";
766
+ const url = normalized === "root" ? "/" : `/${normalized}`;
767
+ return {
768
+ url_pattern: url.replace(/\[(.+?)\]/g, "[$1]"),
769
+ directory: `natural-git/screens/${normalized.replace(/\[(.+?)\]/g, "[$1]")}`,
770
+ title: `${url} 화면 명세`,
771
+ page_path: file.path
772
+ };
773
+ }
774
+ const pagesMatch = file.path.match(/^pages\/(.+)\.(tsx|jsx|ts|js)$/);
775
+ if (pagesMatch) {
776
+ const route = pagesMatch[1].replace(/\/index$/, "root");
777
+ const url = route === "root" ? "/" : `/${route}`;
778
+ return {
779
+ url_pattern: url,
780
+ directory: `natural-git/screens/${route}`,
781
+ title: `${url} 화면 명세`,
782
+ page_path: file.path
783
+ };
784
+ }
785
+ return null;
786
+ }
787
+
788
+ function sourceEvidencePath(file) {
789
+ return `natural-git/_evidence/source-files/${sourcePathSlug(file.path)}.md`;
790
+ }
791
+
792
+ function sourceEvidenceDoc(file) {
793
+ const structure = {
794
+ imports: [],
795
+ exports: [],
796
+ classes: [],
797
+ routes: [],
798
+ events: [],
799
+ selectors: [],
800
+ ...extractStructure(file.lines)
801
+ };
802
+ const visibleTexts = extractVisibleTexts(file);
803
+ const relatedScreens = routeInfoForFile(file);
804
+ const evidenceLines = unique(
805
+ [
806
+ visibleTexts.length ? `화면에 보이는 문구: ${visibleTexts.map((item) => `"${item}"`).join(", ")}` : "",
807
+ structure.routes?.length ? `API/요청 단서: ${structure.routes.slice(0, 8).map((item) => item.kind).join(", ")}` : "",
808
+ structure.events?.length ? `사용자 이벤트 단서: ${structure.events.slice(0, 8).map((item) => item.event).join(", ")}` : "",
809
+ isCliEntryFile(file) ? "CLI 명령과 로컬 MCP 연결 흐름을 구현한다." : "",
810
+ isEnvironmentConfigFile(file) ? "런타임 설정과 외부 연결 조건을 구현한다." : "",
811
+ isTestFile(file) ? "자동 검증 또는 회귀 검증 시나리오를 구현한다." : ""
812
+ ],
813
+ 12
814
+ );
815
+ return {
816
+ path: sourceEvidencePath(file),
817
+ title: `구현 근거: ${file.path}`,
818
+ content: [
819
+ `# 구현 근거: ${file.path}`,
820
+ NATURAL_SPEC_FORMAT_COMMENT,
821
+ "",
822
+ "## 성격",
823
+ "이 문서는 사용자 기능명세가 아니라, 화면/영역/워크플로우 명세를 뒷받침하는 구현 근거다.",
824
+ "",
825
+ "## 연결 가능한 명세",
826
+ relatedScreens ? `- ${relatedScreens.directory}/` : "- 아직 특정 화면 명세에 직접 연결되지 않았다.",
827
+ "",
828
+ "## 관찰 근거",
829
+ ...(evidenceLines.length ? evidenceLines.map((item) => `- ${item}`) : ["- 이 파일은 제품 동작을 구성하는 보조 구현 근거로 보존한다."]),
830
+ "",
831
+ "## 사용 원칙",
832
+ "- 이 문서를 사용자용 기능명세처럼 기본 화면에 노출하지 않는다.",
833
+ "- 버그/기획부재/충돌 판단 시에는 상위 screen/region/rules 명세를 먼저 읽고, 필요한 경우에만 구현 근거로 확인한다.",
834
+ "- 이 문서의 파일 경로와 내부 구현명은 제품 의도가 아니라 traceability 근거다."
835
+ ].join("\n")
836
+ };
837
+ }
838
+
839
+ function workQV2Regions() {
840
+ return [
841
+ {
842
+ slug: "project-repo-registration",
843
+ title: "프로젝트 · 레포 등록 영역",
844
+ purpose: "사용자는 Work Q에서 관리할 프로젝트와 연결된 코드 레포 정보를 등록하고 로컬 에이전트 연결 방법을 확인한다.",
845
+ initial: [
846
+ "Active project 선택값이 보인다.",
847
+ "프로젝트 키, 프로젝트 이름, GitHub owner, repo, default branch, branch prefix, local repo path, test command, smoke command 입력칸이 보인다.",
848
+ "검증된 로컬 바인딩이 없으면 아직 연결되지 않았다는 안내가 표시된다."
849
+ ],
850
+ actions: [
851
+ "사용자는 프로젝트/레포 정보를 입력한다.",
852
+ "`프로젝트/레포 저장` 버튼을 누르면 입력한 프로젝트 컨텍스트가 저장된다.",
853
+ "`레포 코드 자연어 분석` 버튼을 누르면 서버가 접근 가능한 로컬 경로를 분석해 natural-spec repo 문서를 만든다.",
854
+ "운영 환경에서는 안내된 명령을 따라 데스크톱에서 `workq natural-git`으로 업로드한다."
855
+ ],
856
+ rules: [
857
+ "project key는 여러 프로젝트를 구분하는 안정적인 식별자다.",
858
+ "local path는 서버가 읽을 수 있을 때만 직접 분석에 사용한다.",
859
+ "로컬 에이전트 바인딩은 repo owner, repo, local path, branch가 일치해야 검증된 연결로 취급한다."
860
+ ],
861
+ verification: ["저장 후 Active project가 해당 프로젝트로 유지된다.", "repo owner/repo/default branch/local path가 프로젝트 상태와 일치한다.", "잘못된 경로나 미연결 상태에서는 사용자가 다음 행동을 알 수 있어야 한다."]
862
+ },
863
+ {
864
+ slug: "project-status-summary",
865
+ title: "프로젝트 상태 요약 영역",
866
+ purpose: "사용자는 현재 프로젝트, 명세 상태, Agent 큐 준비 상태를 빠르게 확인한다.",
867
+ initial: ["프로젝트 키와 프로젝트 이름이 보인다.", "레포와 기본 브랜치가 보인다.", "문서 수, 수정 문서 수, 입력/결정 필요 수, 충돌 수가 보인다.", "Agent 큐가 생성됐는지 또는 승인 대기인지 보인다."],
868
+ actions: ["이 영역은 직접 입력 영역이 아니라 현재 상태를 읽는 요약 영역이다."],
869
+ rules: ["상태 숫자는 선택된 Active project 기준으로만 계산한다.", "프로젝트를 바꾸면 상태 요약도 같은 프로젝트 기준으로 바뀐다."],
870
+ verification: ["프로젝트 전환 후 다른 프로젝트의 상태가 섞이지 않는다.", "문서 수정, 커밋, 큐 생성 후 상태 숫자와 배지가 갱신된다."]
871
+ },
872
+ {
873
+ slug: "natural-spec-repo",
874
+ title: "natural-spec repo 파일 트리 영역",
875
+ purpose: "사용자는 자연어 명세 git에 들어 있는 화면/영역/규칙/검증 문서를 선택한다.",
876
+ initial: ["natural-spec repo 제목과 저장소 위치 안내가 보인다.", "기본적으로 사용자용 명세 문서가 보인다.", "구현 근거 문서는 evidence로 분리되어 기본 판단 문서와 구분된다."],
877
+ actions: ["사용자는 문서 경로를 클릭해 편집할 문서를 선택한다.", "선택된 문서는 활성 상태로 표시된다."],
878
+ rules: ["URL 화면 명세와 region 명세를 우선 보여준다.", "`_evidence`와 source index는 사용자 기능명세가 아니라 구현 근거로 취급한다.", "동적 URL은 숫자 id나 페이지 번호별로 나누지 않고 하나의 패턴 문서로 묶는다."],
879
+ verification: ["`natural-git/screens/v2/README.md`가 기본 기능명세로 보인다.", "`natural-git/_evidence/...`가 기능명세처럼 오해되지 않는다."]
880
+ },
881
+ {
882
+ slug: "natural-spec-editor",
883
+ title: "자연어 명세 편집 영역",
884
+ purpose: "사용자는 선택한 Markdown 자연어 명세를 읽고 수정한다.",
885
+ initial: ["파일 트리에서 선택한 문서의 경로와 제목이 보인다.", "문서 본문이 Markdown 편집기에 표시된다."],
886
+ actions: ["사용자는 Markdown 내용을 수정한다.", "`Save draft` 버튼을 눌러 draft를 저장한다."],
887
+ rules: ["draft 저장은 git commit이 아니다.", "명세 변경은 자연어 커밋을 거쳐야 기준 HEAD가 된다."],
888
+ verification: ["저장 전후 문서 상태가 수정됨/동기화로 구분된다.", "다른 문서를 선택해도 선택한 문서의 draft가 올바르게 표시된다."]
889
+ },
890
+ {
891
+ slug: "commit-merge-agent-queue",
892
+ title: "commit · merge · queue 영역",
893
+ purpose: "사용자는 자연어 명세 변경을 커밋하고, 머지 가능성을 검사하고, 승인된 커밋을 MCP Agent 작업 큐로 보낸다.",
894
+ initial: ["commit message 입력칸과 자연어 커밋, 명세 머지, 커밋 승인·Agent 큐, 롤백 버튼이 보인다."],
895
+ actions: ["`자연어 커밋`은 draft를 natural spec git commit으로 확정한다.", "`명세 머지`는 입력 필요, 결정 필요, 충돌을 검사한다.", "`커밋 승인·Agent 큐`는 승인된 자연어 커밋을 데스크톱 Agent 작업으로 만든다.", "`롤백`은 이전 natural spec commit 상태로 되돌린다."],
896
+ rules: ["수정된 문서가 없으면 자연어 커밋을 만들 수 없다.", "입력 필요나 충돌이 남아 있으면 Agent 큐를 만들지 않는다.", "Agent 큐 작업은 승인된 자연어 커밋을 기준으로 만들어진다."],
897
+ verification: ["커밋 id는 실제 git commit SHA다.", "merge/run 상태와 Agent queue run 상태가 화면에 기록된다.", "큐 생성 결과에 task id가 남는다."]
898
+ },
899
+ {
900
+ slug: "report-intake",
901
+ title: "리포트 · 이미지 접수 영역",
902
+ purpose: "사용자는 버그, 개선, 개발문의, 기획문의를 입력하고 스크린샷을 첨부해 자연어 커밋 제안을 만든다.",
903
+ initial: ["유형, 제목, 현재 URL, 실제 동작, 기대 동작, 재현 방법 입력칸이 보인다.", "스크린샷 붙여넣기/파일 선택 영역이 보인다."],
904
+ actions: ["사용자는 리포트 내용을 입력한다.", "스크린샷을 붙여넣거나 파일을 선택한다.", "`AI 분석 후 자연어 커밋 제안 만들기`를 누르면 Work Q가 리포트를 명세 기준으로 분류하고 제안 문서를 만든다.", "기존 리포트는 다시 분석하거나 문의 답변 또는 큐 등록으로 보낼 수 있다."],
905
+ rules: ["버그는 승인된 명세와 실제 동작이 다를 때만 버그로 본다.", "명세가 없으면 버그로 단정하지 않고 기획부재나 결정 필요로 남긴다.", "첨부 파일은 허용된 이미지 형식과 크기 제한을 따른다."],
906
+ verification: ["리포트 제출 후 suggested 문서가 생성된다.", "첨부 스크린샷이 리포트와 연결된다.", "개발문의/기획문의는 단순 버그 큐가 아니라 문의 답변 흐름으로 처리된다."]
907
+ },
908
+ {
909
+ slug: "existing-workq-queue",
910
+ title: "기존 WorkQ 큐 영역",
911
+ purpose: "사용자는 기존 WorkQ MCP 작업 큐에 쌓인 작업과 상태를 확인한다.",
912
+ initial: ["큐가 비어 있으면 빈 상태 안내가 보인다.", "작업이 있으면 상태 배지, 제목, 요약이 보인다."],
913
+ actions: ["이 영역은 작업 상태를 읽는 영역이며, 리포트 영역이나 자연어 커밋 승인에서 큐가 생성된다."],
914
+ rules: ["선택된 프로젝트의 작업만 보여준다.", "완료/결정 필요/진행 상태를 구분해 표시한다."],
915
+ verification: ["다른 프로젝트의 작업이 섞이지 않는다.", "Agent가 완료하면 상태가 완료로 보인다."]
916
+ },
917
+ {
918
+ slug: "mini-ai-model-settings",
919
+ title: "Mini AI 모델 설정 영역",
920
+ purpose: "사용자는 리포트 분류와 문의 답변에 사용할 OpenAI 호환 모델 provider를 설정하고 테스트한다.",
921
+ initial: ["Base URL, Model, API key 입력칸이 보인다.", "저장, 테스트, 삭제 버튼이 보인다."],
922
+ actions: ["사용자는 모델 정보를 저장한다.", "저장된 provider를 테스트한다.", "필요하면 provider 설정을 삭제한다."],
923
+ rules: ["API key는 저장 후 입력칸에 다시 노출하지 않는다.", "provider가 없으면 테스트 버튼은 동작하지 않는다."],
924
+ verification: ["저장 후 provider 이름과 endpoint 상태가 보인다.", "테스트 결과가 JSON으로 표시된다.", "삭제 후 provider 없음 상태가 된다."]
925
+ },
926
+ {
927
+ slug: "roadmap",
928
+ title: "로드맵 영역",
929
+ purpose: "사용자는 natural spec 근거와 분리된 가벼운 아이디어/검토/보류 메모를 관리한다.",
930
+ initial: ["제목, 상태, 메모 입력칸과 로드맵 추가 버튼이 보인다.", "기존 로드맵 항목 수가 보인다."],
931
+ actions: ["로드맵 항목을 추가한다.", "항목을 위/아래로 이동한다.", "항목을 삭제한다."],
932
+ rules: ["로드맵은 승인된 기능명세가 아니다.", "로드맵 순서는 참고 우선순위로만 사용한다."],
933
+ verification: ["항목 추가, 순서 이동, 삭제가 선택된 프로젝트 안에서만 반영된다."]
934
+ },
935
+ {
936
+ slug: "decision-needed-mcp",
937
+ title: "결정 필요 작업 · MCP 영역",
938
+ purpose: "사용자는 결정이 필요한 작업을 승인 또는 거절하고 MCP 엔드포인트 상태를 확인한다.",
939
+ initial: ["MCP URL과 token 준비 상태가 보인다.", "decision_needed 작업 목록이 있으면 카드로 표시된다."],
940
+ actions: ["사용자는 결정 내용을 입력한다.", "`승인하고 큐`를 누르면 Agent 작업으로 보낸다.", "`거절`을 누르면 작업을 닫는다."],
941
+ rules: ["결정 내용이 없으면 승인/거절할 수 없다.", "승인된 결정은 이후 명세 또는 작업 기준으로 남아야 한다."],
942
+ verification: ["결정 처리 후 decision_needed 목록에서 사라진다.", "승인한 작업은 Agent 큐에서 확인된다."]
943
+ }
944
+ ];
945
+ }
946
+
947
+ function isWorkQDashboardPage(file) {
948
+ return (
949
+ file.path === "src/app/page.tsx" &&
950
+ /ProjectContextPanel/.test(file.content) &&
951
+ /ReportPanel/.test(file.content) &&
952
+ /TriagePanel/.test(file.content) &&
953
+ /AgentQueuePanel/.test(file.content) &&
954
+ /McpPanel/.test(file.content)
955
+ );
956
+ }
957
+
958
+ function workQDashboardRegions() {
959
+ return [
960
+ {
961
+ slug: "topbar-and-login",
962
+ title: "상단 상태 · 로그인 영역",
963
+ purpose: "사용자는 Work Q에 접속해 언어를 바꾸고, 소유자 세션으로 로그인한 뒤 현재 작업 상태를 확인한다.",
964
+ initial: ["상단에 Work Q 서비스 이름과 언어 전환, 새로고침 조작이 보인다.", "로그인 전에는 비밀번호 입력과 로그인 버튼이 보인다."],
965
+ actions: ["사용자는 표시 언어를 전환한다.", "사용자는 새로고침으로 최신 프로젝트와 작업 상태를 다시 불러온다.", "소유자는 비밀번호를 입력해 관리 화면에 들어간다."],
966
+ rules: ["로그인 전에는 프로젝트, 리포트, 큐 관리 영역을 보여주지 않는다.", "새로고침은 현재 선택된 프로젝트 컨텍스트를 유지해야 한다."],
967
+ verification: ["로그인 전에는 로그인 폼만 주요 조작으로 표시된다.", "언어 전환 후 화면 문구가 같은 의미로 바뀐다.", "새로고침 후 선택 프로젝트가 임의로 바뀌지 않는다."]
968
+ },
969
+ {
970
+ slug: "project-context",
971
+ title: "프로젝트 컨텍스트 영역",
972
+ purpose: "사용자는 여러 프로젝트와 레포 중 현재 Work Q가 판단하고 작업할 대상을 선택한다.",
973
+ initial: ["프로젝트 선택 목록과 새 프로젝트 입력 흐름이 보인다.", "프로젝트 키, 이름, 레포, 로컬 연결 상태를 확인할 수 있다."],
974
+ actions: ["사용자는 기존 프로젝트를 선택한다.", "사용자는 새 프로젝트 정보를 저장한다.", "사용자는 로컬 MCP가 연결한 레포 정보와 웹 프로젝트 정보가 맞는지 확인한다."],
975
+ rules: ["모든 리포트, 명세, 큐, 결정 필요 항목은 선택된 프로젝트 기준으로만 표시한다.", "프로젝트 키는 서로 다른 레포나 서비스를 섞지 않기 위한 안정적인 식별자다."],
976
+ verification: ["프로젝트를 바꾸면 리포트, 명세 지도, 큐 목록도 같은 프로젝트 기준으로 바뀐다.", "저장된 프로젝트 키와 MCP 연결 정보가 일치하지 않으면 사용자가 확인할 수 있어야 한다."]
977
+ },
978
+ {
979
+ slug: "report-intake",
980
+ title: "리포트 접수 영역",
981
+ purpose: "사용자는 버그, 개선, 기획문의, 개발문의를 올리고 필요한 맥락과 이미지를 첨부한다.",
982
+ initial: ["리포트 제목, 유형, URL, 실제 동작, 기대 동작, 재현 방법 입력 영역이 보인다.", "스크린샷 파일 선택 또는 붙여넣기 영역이 보인다."],
983
+ actions: ["사용자는 문제나 문의 내용을 작성한다.", "사용자는 스크린샷을 붙여넣거나 첨부한다.", "사용자는 리포트를 제출하고 분석 결과를 기다린다."],
984
+ rules: ["재현 방법은 비어 있으면 사용자가 어떤 행동을 했는지 판단하기 어렵기 때문에 입력 필요 상태로 남긴다.", "첨부 이미지는 리포트 판단 근거로 연결되지만 그 자체가 승인된 명세는 아니다."],
985
+ verification: ["리포트 제출 후 선택 프로젝트의 리포트 목록에 추가된다.", "스크린샷이 있는 리포트는 첨부 정보가 함께 남는다.", "필수 맥락이 부족하면 작업 큐로 바로 보내지 않고 입력 필요로 구분한다."]
986
+ },
987
+ {
988
+ slug: "triage-and-spec-map",
989
+ title: "판단 근거 · 기능명세 지도 영역",
990
+ purpose: "사용자는 리포트가 버그, 기획부재, 충돌, 개선 중 무엇인지와 그 판단 근거를 확인한다.",
991
+ initial: ["선택된 리포트의 분석 결과와 관련 기능 노드가 보인다.", "입력 필요, 결정 필요, 충돌 같은 상태가 카드나 칩으로 표시된다."],
992
+ actions: ["사용자는 판단 근거를 읽고 필요한 결정을 입력한다.", "사용자는 승인 가능한 항목을 Agent 큐로 보낸다.", "사용자는 부족한 정보를 보충해 다시 판단하게 한다."],
993
+ rules: ["승인된 기능명세와 다른 실제 동작이면 버그 후보로 본다.", "기대 동작은 있지만 승인된 명세가 없으면 기획부재 또는 결정 필요로 본다.", "서로 다른 승인 명세가 같은 조건에서 다른 결과를 요구하면 충돌로 본다."],
994
+ verification: ["각 판단 결과에는 어떤 명세 노드와 리포트 내용이 근거였는지 표시된다.", "결정 필요 항목은 사용자의 승인 또는 거절 전까지 자동 개발 큐로 넘어가지 않는다."]
995
+ },
996
+ {
997
+ slug: "project-memory-and-roadmap",
998
+ title: "프로젝트 기억 · 로드맵 영역",
999
+ purpose: "사용자는 현재 프로젝트의 기능명세, 알려진 충돌, 검증 상태, 가벼운 로드맵 메모를 확인한다.",
1000
+ initial: ["기능명세 지도와 프로젝트 메모, 로드맵 항목이 선택 프로젝트 기준으로 보인다."],
1001
+ actions: ["사용자는 로드맵 메모를 추가하거나 순서를 조정한다.", "사용자는 기능명세 카드의 상태를 확인한다."],
1002
+ rules: ["로드맵은 참고 메모이며 승인된 기능명세가 아니다.", "기능명세 지도는 리포트 분류와 Agent 작업의 기준으로 사용한다."],
1003
+ verification: ["로드맵 순서 변경이 같은 프로젝트 안에서 유지된다.", "명세 업데이트 후 기능명세 지도와 판단 근거가 같은 기준을 참조한다."]
1004
+ },
1005
+ {
1006
+ slug: "agent-queue-and-mcp",
1007
+ title: "Agent 큐 · MCP 연결 영역",
1008
+ purpose: "사용자는 데스크톱 Codex 또는 Claude가 가져갈 작업 큐와 MCP 연결 준비 상태를 확인한다.",
1009
+ initial: ["선택 프로젝트의 Agent 작업 목록과 상태가 보인다.", "MCP URL과 token 준비 상태가 보인다."],
1010
+ actions: ["로컬 Agent는 MCP로 다음 작업을 가져간다.", "작업이 끝난 Agent는 완료 결과와 검증 내용을 Work Q에 남긴다.", "사용자는 완료, 진행, 결정 필요 상태를 확인한다."],
1011
+ rules: ["큐 작업은 선택 프로젝트와 연결된 레포를 기준으로 처리해야 한다.", "Agent가 작업을 완료하면 완료 표시와 함께 명세 변경 또는 검증 근거를 남겨야 한다."],
1012
+ verification: ["다른 프로젝트의 큐가 섞여 보이지 않는다.", "MCP token이 없으면 로컬 Agent 연결이 준비되지 않은 상태로 보인다.", "완료된 작업은 큐 상태가 완료로 바뀐다."]
1013
+ }
1014
+ ];
1015
+ }
1016
+
1017
+ function genericHtmlRegions(file) {
1018
+ const title = primaryVisibleTitle(file) || "화면";
1019
+ const hasOneToNineLoop = /\bfor\s*\(\s*let\s+\w+\s*=\s*1\s*;\s*\w+\s*<=\s*9\s*;/.test(file.content);
1020
+ if (hasOneToNineLoop && /단/.test(file.content)) {
1021
+ return [
1022
+ {
1023
+ slug: "dan-selection",
1024
+ title: "단 선택 영역",
1025
+ purpose: "사용자는 1단부터 9단까지 중 보고 싶은 단을 선택한다.",
1026
+ initial: ["1단부터 9단까지 9개의 선택 버튼이 보인다.", "아직 선택하지 않았을 때는 선택을 유도하는 안내 문구가 보인다."],
1027
+ actions: ["사용자가 특정 단 버튼을 누르면 그 단이 선택된다.", "다른 단을 누르면 이전 선택은 해제되고 새 단만 선택된다."],
1028
+ rules: ["선택 상태는 항상 하나만 유지된다.", "버튼은 1단부터 9단까지 자동 생성된다."],
1029
+ verification: ["1단부터 9단까지 모든 버튼이 표시된다.", "두 개 이상의 버튼이 동시에 선택 상태가 되지 않는다."]
1030
+ },
1031
+ {
1032
+ slug: "multiplication-result",
1033
+ title: "곱셈표 결과 영역",
1034
+ purpose: "사용자는 선택한 단의 1부터 9까지 곱셈 결과를 확인한다.",
1035
+ initial: ["처음에는 결과 대신 안내 문구가 보인다."],
1036
+ actions: ["단을 선택하면 결과 영역이 선택한 단의 곱셈표로 교체된다.", "다른 단을 선택하면 결과도 새 단으로 갱신된다."],
1037
+ rules: ["선택한 단에 대해 ×1부터 ×9까지 총 9줄을 보여준다.", "결과값은 즉석에서 계산하며 하드코딩하지 않는다.", "결과 상단에는 선택한 단 제목을 표시한다."],
1038
+ verification: ["7단 선택 시 7 × 1 = 7부터 7 × 9 = 63까지 표시된다.", "선택한 단을 바꾸면 이전 결과가 남지 않는다."]
1039
+ }
1040
+ ];
1041
+ }
1042
+ return [
1043
+ {
1044
+ slug: "main-content",
1045
+ title: `${title} 주요 화면 영역`,
1046
+ purpose: "사용자는 이 화면에서 주요 콘텐츠와 조작 가능한 요소를 확인한다.",
1047
+ initial: visibleTextInitialStatements(file),
1048
+ actions: ["사용자는 화면에 표시된 버튼, 링크, 입력 요소를 조작한다."],
1049
+ rules: ["화면에 보이는 문구와 조작 요소는 같은 URL 화면 명세 안에서 관리한다."],
1050
+ verification: ["초기 화면의 주요 문구와 조작 요소가 표시된다."]
1051
+ }
1052
+ ];
1053
+ }
1054
+
1055
+ function genericRouteRegions(file) {
1056
+ return [
1057
+ {
1058
+ slug: "main-screen",
1059
+ title: "주요 화면 영역",
1060
+ purpose: "사용자는 이 URL에서 제공되는 핵심 화면을 본다.",
1061
+ initial: visibleTextInitialStatements(file),
1062
+ actions: ["사용자는 화면에 표시된 버튼, 입력칸, 선택값을 조작한다."],
1063
+ rules: ["같은 URL 패턴에 속한 상태 변화는 같은 화면 명세 안에서 관리한다."],
1064
+ verification: ["URL 접속 시 주요 화면 문구와 조작 요소가 표시된다."]
1065
+ }
1066
+ ];
1067
+ }
1068
+
1069
+ function regionsForRoute(file, route) {
1070
+ if (isWorkQDashboardPage(file)) return workQDashboardRegions();
1071
+ if (route.url_pattern === "/v2" && file.path === "src/app/v2/page.tsx") return workQV2Regions();
1072
+ if (file.path === "index.html") return genericHtmlRegions(file);
1073
+ return genericRouteRegions(file);
1074
+ }
1075
+
1076
+ function screenReadmeDoc(file, route, regions) {
1077
+ const isWorkQDashboard = isWorkQDashboardPage(file);
1078
+ const isWorkQV2 = route.url_pattern === "/v2" && file.path === "src/app/v2/page.tsx";
1079
+ const title = isWorkQDashboard ? "Work Q 대시보드 화면 명세" : isWorkQV2 ? "Work Q v2 대시보드 화면 명세" : route.title;
1080
+ const purpose = isWorkQDashboard
1081
+ ? "사용자는 Work Q에서 프로젝트를 선택하고, 리포트를 등록하고, 기능명세 판단 근거와 Agent 큐 상태를 확인한다."
1082
+ : isWorkQV2
1083
+ ? "사용자는 Work Q v2에서 프로젝트와 레포를 연결하고, natural-spec repo를 편집하며, 리포트와 자연어 커밋을 Agent 작업 큐로 연결한다."
1084
+ : `${route.url_pattern} URL에서 사용자가 보는 화면의 목적, 영역, 조작, 결과를 정의한다.`;
1085
+ return {
1086
+ path: `${route.directory}/README.md`,
1087
+ title,
1088
+ content: [
1089
+ `# ${title}`,
1090
+ NATURAL_SPEC_FORMAT_COMMENT,
1091
+ "",
1092
+ "## URL 패턴",
1093
+ `- ${route.url_pattern}`,
1094
+ "",
1095
+ "## 화면 목적",
1096
+ purpose,
1097
+ "",
1098
+ "## 화면 영역",
1099
+ ...regions.map((region) => `- ${region.title}: ${region.purpose}`),
1100
+ "",
1101
+ "## 핵심 원칙",
1102
+ "- URL 패턴을 최상위 화면 명세로 삼는다.",
1103
+ "- 화면 안의 사용자 인식 영역을 region 명세로 나눈다.",
1104
+ "- 코드 파일 경로는 사용자 기능명세 제목으로 쓰지 않고 evidence로 분리한다.",
1105
+ "- 페이지네이션, 게시물 번호, 상세 id처럼 값만 바뀌는 URL은 하나의 URL 패턴으로 추상화한다.",
1106
+ "",
1107
+ "## 구현 근거",
1108
+ `- ${sourceEvidencePath(file)}`
1109
+ ].join("\n")
1110
+ };
1111
+ }
1112
+
1113
+ function screenRulesDoc(route, regions) {
1114
+ return {
1115
+ path: `${route.directory}/rules.md`,
1116
+ title: `${route.url_pattern} 화면 규칙`,
1117
+ content: [
1118
+ `# ${route.url_pattern} 화면 규칙`,
1119
+ NATURAL_SPEC_FORMAT_COMMENT,
1120
+ "",
1121
+ "## 공통 판단 기준",
1122
+ "- 리포트가 이 화면 또는 하위 영역의 규칙과 다른 실제 동작을 제보하면 bug 후보로 본다.",
1123
+ "- 기대 동작이 plausible하지만 이 화면/영역/rules에 없으면 기획부재 또는 decision needed 후보로 본다.",
1124
+ "- 같은 화면 안의 두 영역 규칙이 동시에 만족될 수 없으면 기능충돌 후보로 본다.",
1125
+ "- 코드에서 관찰된 구현만으로 제품 의도를 단정하지 않는다.",
1126
+ "",
1127
+ "## 영역별 핵심 규칙",
1128
+ ...regions.flatMap((region) => [`### ${region.title}`, ...region.rules.map((rule) => `- ${rule}`), ""])
1129
+ ].join("\n")
1130
+ };
1131
+ }
1132
+
1133
+ function screenVerificationDoc(route, regions) {
1134
+ return {
1135
+ path: `${route.directory}/verification.md`,
1136
+ title: `${route.url_pattern} 화면 검증`,
1137
+ content: [
1138
+ `# ${route.url_pattern} 화면 검증`,
1139
+ NATURAL_SPEC_FORMAT_COMMENT,
1140
+ "",
1141
+ "## 화면 검증 원칙",
1142
+ "- URL 접속 후 사용자가 보는 주요 영역이 표시되어야 한다.",
1143
+ "- 각 영역의 입력, 버튼, 상태 변화, 오류 상태를 검증한다.",
1144
+ "- Agent 완료 전에는 관련 영역의 verification 항목을 확인하고 Work Q 완료 기록에 남긴다.",
1145
+ "",
1146
+ "## 영역별 검증",
1147
+ ...regions.flatMap((region) => [`### ${region.title}`, ...region.verification.map((item) => `- ${item}`), ""])
1148
+ ].join("\n")
1149
+ };
1150
+ }
1151
+
1152
+ function regionDoc(route, region) {
1153
+ return {
1154
+ path: `${route.directory}/regions/${region.slug}.md`,
1155
+ title: region.title,
1156
+ content: [
1157
+ `# ${region.title}`,
1158
+ NATURAL_SPEC_FORMAT_COMMENT,
1159
+ "",
1160
+ "## 목적",
1161
+ region.purpose,
1162
+ "",
1163
+ "## 초기 상태",
1164
+ ...region.initial.map((item) => `- ${item}`),
1165
+ "",
1166
+ "## 사용자 동작",
1167
+ ...region.actions.map((item) => `- ${item}`),
1168
+ "",
1169
+ "## 핵심 규칙",
1170
+ ...region.rules.map((item) => `- ${item}`),
1171
+ "",
1172
+ "## 검증 방법",
1173
+ ...region.verification.map((item) => `- ${item}`)
1174
+ ].join("\n")
1175
+ };
1176
+ }
1177
+
1178
+ function fallbackRouteInfo() {
1179
+ return {
1180
+ url_pattern: "<product-overview>",
1181
+ directory: "natural-git/screens/product-overview",
1182
+ title: "제품 개요 화면 명세",
1183
+ page_path: "repository"
1184
+ };
1185
+ }
1186
+
1187
+ function fallbackRegions(sourceFiles) {
1188
+ const files = sourceFiles.map((file) => file.path.toLowerCase());
1189
+ const hasLottoGenerator = files.some((filePath) => /generate[_-]?numbers/.test(filePath));
1190
+ const hasServer = sourceFiles.some((file) => isApiOrServerFile(file));
1191
+ const regions = [];
1192
+
1193
+ if (hasLottoGenerator) {
1194
+ regions.push({
1195
+ slug: "lotto-number-generation",
1196
+ title: "로또 번호 생성 기능 영역",
1197
+ purpose: "사용자는 로또 번호 후보를 생성하고, 생성 결과가 로또 번호 규칙을 지키는지 확인한다.",
1198
+ initial: ["로또 번호 생성 기능은 코드 레포에 존재하지만 아직 특정 URL 화면 파일과 직접 연결되지 않았다."],
1199
+ actions: ["사용자는 번호 생성을 요청한다.", "시스템은 1부터 45 사이의 번호 6개를 결과로 제공한다."],
1200
+ rules: ["생성 결과는 1부터 45 사이의 숫자여야 한다.", "한 조합 안에서 같은 번호가 두 번 나오면 안 된다.", "번호 개수는 6개여야 한다."],
1201
+ verification: ["생성 결과의 길이가 6인지 확인한다.", "모든 번호가 1부터 45 사이인지 확인한다.", "중복 번호가 없는지 확인한다."]
1202
+ });
1203
+ }
1204
+
1205
+ if (hasServer) {
1206
+ regions.push({
1207
+ slug: "service-api",
1208
+ title: "서비스 API 영역",
1209
+ purpose: "외부 요청은 서버 API를 통해 제품 기능의 결과를 받는다.",
1210
+ initial: ["서버 API 구현 근거가 존재한다."],
1211
+ actions: ["사용자 또는 클라이언트는 API 요청을 보낸다.", "서버는 연결된 제품 기능의 결과를 응답한다."],
1212
+ rules: ["API 응답은 연결된 사용자 기능 명세의 규칙을 따라야 한다.", "API가 실패하면 호출자가 실패를 구분할 수 있어야 한다."],
1213
+ verification: ["API 호출 결과가 연결된 기능 명세와 같은 형식과 규칙을 만족하는지 확인한다."]
1214
+ });
1215
+ }
1216
+
1217
+ if (!regions.length) {
1218
+ regions.push({
1219
+ slug: "observed-product-behavior",
1220
+ title: "관찰된 제품 동작 영역",
1221
+ purpose: "레포에서 관찰된 구현을 바탕으로 아직 화면에 연결되지 않은 제품 동작 후보를 관리한다.",
1222
+ initial: ["명확한 URL 화면 파일이 발견되지 않았다."],
1223
+ actions: ["오너는 이 영역의 초안을 읽고 실제 제품 의도인지 승인하거나 수정한다."],
1224
+ rules: ["코드에서 관찰된 동작은 승인 전까지 제품 의도로 단정하지 않는다.", "사용자 기능으로 확정하려면 자연어 커밋으로 승인해야 한다."],
1225
+ verification: ["오너가 기능 의도를 승인하거나, 더 구체적인 화면/유즈케이스 명세로 분리한다."]
1226
+ });
1227
+ }
1228
+
1229
+ return regions;
1230
+ }
1231
+
1232
+ function routeScreenDocs(sourceFiles) {
1233
+ const docs = sourceFiles.flatMap((file) => {
1234
+ const route = routeInfoForFile(file);
1235
+ if (!route) return [];
1236
+ const regions = regionsForRoute(file, route);
1237
+ return [
1238
+ screenReadmeDoc(file, route, regions),
1239
+ screenRulesDoc(route, regions),
1240
+ screenVerificationDoc(route, regions),
1241
+ ...regions.map((region) => regionDoc(route, region))
1242
+ ];
1243
+ });
1244
+ if (docs.length || !sourceFiles.length) return docs;
1245
+ const route = fallbackRouteInfo();
1246
+ const regions = fallbackRegions(sourceFiles);
1247
+ return [
1248
+ screenReadmeDoc(sourceFiles[0], route, regions),
1249
+ screenRulesDoc(route, regions),
1250
+ screenVerificationDoc(route, regions),
1251
+ ...regions.map((region) => regionDoc(route, region))
1252
+ ];
1253
+ }
1254
+
695
1255
  function overviewDoc({ projectKey, projectName, repository, root, sourceFiles }) {
696
1256
  const byLanguage = new Map();
697
1257
  for (const file of sourceFiles) byLanguage.set(file.language, (byLanguage.get(file.language) ?? 0) + 1);
@@ -700,7 +1260,7 @@ function overviewDoc({ projectKey, projectName, repository, root, sourceFiles })
700
1260
  NATURAL_SPEC_FORMAT_COMMENT,
701
1261
  "",
702
1262
  "## 의미",
703
- "이 명세는 레포에 존재하는 코드 파일을 제품 기능, 사용자 동작, 입력/출력, 화면/디자인, 데이터 규칙 중심의 자연어 Markdown 문서로 해석한 것이다. Work Q v2에서는 이 자연어 문서를 기준으로 리포트와 코드 변경을 판단한다.",
1263
+ "이 명세는 레포에 존재하는 URL/화면을 최상위 기준으로 삼고, 화면 안의 사용자 인식 영역을 자연어 Markdown 문서로 정의한 것이다. Work Q v2에서는 이 자연어 문서를 기준으로 리포트와 코드 변경을 판단한다.",
704
1264
  "",
705
1265
  "## 프로젝트",
706
1266
  `- 프로젝트 키: ${projectKey}`,
@@ -713,16 +1273,16 @@ function overviewDoc({ projectKey, projectName, repository, root, sourceFiles })
713
1273
  ...Array.from(byLanguage.entries()).map(([language, count]) => `- ${language}: ${count}개`),
714
1274
  "",
715
1275
  "## 디렉토리 구조",
716
- "- `natural-git/features/`: 사용자가 직접 경험하는 화면과 기능 명세",
717
- "- `natural-git/interfaces/`: CLI, API, 외부 연결처럼 기능이 드나드는 접점 명세",
718
- "- `natural-git/data/`: 데이터 모델과 저장 규칙 명세",
719
- "- `natural-git/operations/`: 실행 환경, 설정, 배포/운영 조건 명세",
1276
+ "- `natural-git/screens/`: URL 패턴별 최상위 화면 명세",
1277
+ "- `natural-git/screens/<url>/regions/`: 화면 안의 사용자 인식 영역 명세",
1278
+ "- `natural-git/screens/<url>/components/`: 여러 화면에서 재사용되는 컴포넌트 명세",
1279
+ "- `natural-git/screens/<url>/dialogs/`: 모달, 팝업, 드로어 상태 명세",
720
1280
  "- `natural-git/verification/`: 기능이 실제로 동작하는지 확인하는 검증 시나리오 명세",
721
- "- 기능 디렉토리는 `README.md`, `rules.md`, `verification.md`로 나뉘어 개요, 판단 규칙, 검증 기준을 따로 관리한다.",
1281
+ "- `natural-git/_evidence/`: 코드 파일, 설정, 테스트의 구현 근거. 기본 사용자 명세가 아니라 traceability 근거다.",
722
1282
  "",
723
1283
  "## 운영 규칙",
724
- "- 자연어 문서는 코드에서 관찰한 제품 기능과 검증 가능한 동작을 설명한다.",
725
- "- 코드 파일 문서는 라인별 번역이 아니라 기능, 규칙, 화면, 데이터, 검증 관점 중심으로 작성한다.",
1284
+ "- 자연어 문서는 URL 화면과 사용자 인식 영역 중심으로 작성한다.",
1285
+ "- 코드 파일 문서는 사용자 기능명세가 아니라 `_evidence` 아래 구현 근거로 보존한다.",
726
1286
  "- 코드의 현재 동작이 곧 제품 의도라는 뜻은 아니다.",
727
1287
  "- 오너가 자연어 커밋으로 승인한 내용만 다음 코드 변경의 기준이 된다.",
728
1288
  "- 코드가 바뀌면 이 자연어 git 명세도 다시 분석하거나 수정해야 한다."
@@ -731,11 +1291,11 @@ function overviewDoc({ projectKey, projectName, repository, root, sourceFiles })
731
1291
 
732
1292
  function indexDoc(sourceFiles) {
733
1293
  return [
734
- "# 자연어 명세 색인",
1294
+ "# 구현 근거 색인",
735
1295
  "",
736
- "이 문서는 분석된 파일과 계층형 자연어 명세 디렉토리를 연결한다. 사용자는 보통 이 색인보다 기능 디렉토리의 README, rules, verification 문서를 읽는다.",
1296
+ "이 문서는 분석된 파일과 evidence 문서를 연결한다. 사용자는 보통 이 색인보다 `natural-git/screens/` 아래의 화면/영역 명세를 먼저 읽는다.",
737
1297
  "",
738
- ...sourceFiles.map((file) => `- \`${file.path}\` -> \`${specDirectoryForSource(file)}/\``)
1298
+ ...sourceFiles.map((file) => `- \`${file.path}\` -> \`${sourceEvidencePath(file)}\``)
739
1299
  ].join("\n");
740
1300
  }
741
1301
 
@@ -760,6 +1320,8 @@ export function generateNaturalGitDocs({ localPath, projectKey, projectName = ""
760
1320
  };
761
1321
  })
762
1322
  .filter(Boolean);
1323
+ const screenDocs = routeScreenDocs(sourceFiles);
1324
+ const verificationDocs = sourceFiles.filter(isTestFile).flatMap((file) => sourceFileToDocs(file));
763
1325
  const docs = [
764
1326
  {
765
1327
  path: "natural-git/README.md",
@@ -767,11 +1329,13 @@ export function generateNaturalGitDocs({ localPath, projectKey, projectName = ""
767
1329
  content: overviewDoc({ projectKey, projectName, repository, root, sourceFiles })
768
1330
  },
769
1331
  {
770
- path: "natural-git/source-index.md",
771
- title: "자연어 명세 색인",
1332
+ path: "natural-git/_evidence/source-index.md",
1333
+ title: "구현 근거 색인",
772
1334
  content: indexDoc(sourceFiles)
773
1335
  },
774
- ...sourceFiles.flatMap((file) => sourceFileToDocs(file))
1336
+ ...screenDocs,
1337
+ ...verificationDocs,
1338
+ ...sourceFiles.map((file) => sourceEvidenceDoc(file))
775
1339
  ];
776
1340
  return {
777
1341
  root,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workq-mcp",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Work Q MCP stdio bridge and local repository connection CLI.",
5
5
  "type": "module",
6
6
  "bin": {