workq-mcp 0.1.1 → 0.1.3
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/lib/natural-git.mjs +223 -70
- package/package.json +1 -1
package/lib/natural-git.mjs
CHANGED
|
@@ -152,48 +152,172 @@ function compact(value, maxLength = 220) {
|
|
|
152
152
|
return `${normalized.slice(0, maxLength - 3).trim()}...`;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
function
|
|
156
|
-
|
|
157
|
-
if (/^(import\s|from\s+|const\s+\w+\s*=\s*require\(|require\(|#include|using\s+|use\s+)/.test(line)) return "import";
|
|
158
|
-
if (/^(export\s|module\.exports|exports\.)/.test(line)) return "export";
|
|
159
|
-
if (/^(?:async\s+)?function\s+|^(?:export\s+)?(?:const|let|var)\s+\w+\s*=.*=>|^def\s+|^function\s+/.test(line)) return "function";
|
|
160
|
-
if (/^class\s+|^(?:export\s+)?class\s+|^(?:public\s+)?(?:final\s+)?class\s+/.test(line)) return "class";
|
|
161
|
-
if (/\bif\s*\(|^if\s+|^elif\s+|\belse\s+if\s*\(/.test(line)) return "if";
|
|
162
|
-
if (/\bfor\s*\(|^for\s+|\bwhile\s*\(|^while\s+/.test(line)) return "loop";
|
|
163
|
-
if (/\bswitch\s*\(|^match\s+/.test(line)) return "switch";
|
|
164
|
-
if (/\bcatch\s*\(|^except\s+/.test(line)) return "catch";
|
|
165
|
-
if (/^return\b/.test(line)) return "return";
|
|
166
|
-
if (/^throw\b|^raise\b/.test(line)) return "throw";
|
|
167
|
-
if (/^(const|let|var)\s+\w+\s*=|^[A-Za-z_$][\w$]*\s*=/.test(line)) return "assignment";
|
|
168
|
-
if (/\b(app|router)\.(get|post|put|patch|delete|use)\s*\(/i.test(line)) return "route";
|
|
169
|
-
if (/\.addEventListener\(/.test(line)) return "event";
|
|
170
|
-
if (/^SELECT\b|^INSERT\b|^UPDATE\b|^DELETE\b|^CREATE\b|^ALTER\b/i.test(line)) return "sql";
|
|
171
|
-
if (/^\{|\}|\]|\[|,$/.test(line)) return "structure";
|
|
172
|
-
return "statement";
|
|
155
|
+
function unique(values, limit = 40) {
|
|
156
|
+
return Array.from(new Set(values.filter(Boolean))).slice(0, limit);
|
|
173
157
|
}
|
|
174
158
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
159
|
+
const NATURAL_SPEC_FORMAT_COMMENT = "<!-- workq-natural-spec-format: feature-v2 -->";
|
|
160
|
+
|
|
161
|
+
function stripTags(value) {
|
|
162
|
+
return value.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function extractVisibleTexts(file) {
|
|
166
|
+
const texts = [];
|
|
167
|
+
const markupContent = file.content.replace(/<script\b[\s\S]*?<\/script>/gi, "").replace(/<style\b[\s\S]*?<\/style>/gi, "");
|
|
168
|
+
if (file.extension === ".html") {
|
|
169
|
+
for (const match of file.content.matchAll(/<title[^>]*>([\s\S]*?)<\/title>/gi)) texts.push(stripTags(match[1]));
|
|
170
|
+
for (const match of markupContent.matchAll(/>([^<>{}][^<>{}]{1,80})</g)) texts.push(stripTags(match[1]));
|
|
171
|
+
}
|
|
172
|
+
for (const match of file.content.matchAll(/\b(?:textContent|innerText|placeholder|aria-label|title)\s*=\s*["'`]([^"'`]{1,100})["'`]/g)) {
|
|
173
|
+
texts.push(stripTags(match[1]));
|
|
174
|
+
}
|
|
175
|
+
for (const match of file.content.matchAll(/<button[^>]*>([\s\S]{1,160}?)<\/button>/gi)) {
|
|
176
|
+
texts.push(stripTags(match[1]));
|
|
177
|
+
}
|
|
178
|
+
return unique(texts.map((item) => compact(item, 100)).filter((item) => item && !/^[{}()[\];,.]+$/.test(item)), 18);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function extractDesignFacts(file) {
|
|
182
|
+
if (![".css", ".html", ".tsx", ".jsx"].includes(file.extension)) return [];
|
|
183
|
+
const facts = [];
|
|
184
|
+
const colors = unique(Array.from(file.content.matchAll(/#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\)/g)).map((match) => match[0]), 10);
|
|
185
|
+
const gradients = unique(Array.from(file.content.matchAll(/linear-gradient\([^)]+\)/g)).map((match) => match[0]), 6);
|
|
186
|
+
const radii = unique(Array.from(file.content.matchAll(/border-radius\s*:\s*([^;]+);/g)).map((match) => match[1].trim()), 6);
|
|
187
|
+
const shadows = unique(Array.from(file.content.matchAll(/box-shadow\s*:\s*([^;]+);/g)).map((match) => match[1].trim()), 6);
|
|
188
|
+
const layout = [];
|
|
189
|
+
if (/display\s*:\s*grid/.test(file.content)) layout.push("grid");
|
|
190
|
+
if (/display\s*:\s*flex/.test(file.content)) layout.push("flex");
|
|
191
|
+
if (/min-height\s*:\s*100vh/.test(file.content)) layout.push("full viewport height");
|
|
192
|
+
if (/justify-content\s*:\s*center|align-items\s*:\s*center/.test(file.content)) layout.push("centered alignment");
|
|
193
|
+
if (colors.length) facts.push(`색상 팔레트는 ${colors.join(", ")} 값을 포함한다.`);
|
|
194
|
+
if (gradients.length) facts.push(`그라데이션 배경/강조는 ${gradients.join(", ")} 형태를 사용한다.`);
|
|
195
|
+
if (layout.length) facts.push(`레이아웃은 ${unique(layout).join(", ")} 배치를 사용한다.`);
|
|
196
|
+
if (radii.length) facts.push(`둥근 모서리 값은 ${radii.join(", ")} 등이 관찰된다.`);
|
|
197
|
+
if (shadows.length) facts.push(`그림자 표현은 ${shadows.slice(0, 3).join(", ")} 값을 사용한다.`);
|
|
198
|
+
if (/@keyframes\s+([A-Za-z0-9_-]+)/.test(file.content)) {
|
|
199
|
+
facts.push(`애니메이션은 ${unique(Array.from(file.content.matchAll(/@keyframes\s+([A-Za-z0-9_-]+)/g)).map((match) => match[1])).join(", ")} keyframes를 사용한다.`);
|
|
200
|
+
}
|
|
201
|
+
return facts;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function extractDataModelFacts(file) {
|
|
205
|
+
const facts = [];
|
|
206
|
+
const schemaMatch = file.content.match(/new\s+mongoose\.Schema\s*\(\s*\{([\s\S]*?)\}\s*\)/m);
|
|
207
|
+
if (schemaMatch) {
|
|
208
|
+
facts.push("서비스가 저장하고 조회하는 데이터 구조와 값의 종류를 정의한다.");
|
|
209
|
+
}
|
|
210
|
+
if (file.path === "package.json") {
|
|
211
|
+
const packageDoc = packageJsonDoc(file.content).split(/\r?\n/).filter((line) => line.startsWith("- `") || line.startsWith("- 패키지 이름"));
|
|
212
|
+
facts.push(...packageDoc.slice(0, 12));
|
|
213
|
+
}
|
|
214
|
+
return facts;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function isEnvironmentConfigFile(file) {
|
|
218
|
+
const normalized = file.path.toLowerCase();
|
|
219
|
+
const basename = path.basename(normalized);
|
|
220
|
+
return (
|
|
221
|
+
/(^|\/)(env|environment)\.[cm]?[jt]sx?$/.test(normalized) ||
|
|
222
|
+
/(^|\/)(env|environment)\.(mjs|cjs|js|ts)$/.test(normalized) ||
|
|
223
|
+
/(^|\/)(lib|config)\/env\.[cm]?[jt]sx?$/.test(normalized) ||
|
|
224
|
+
/(^|\/)(lib|config)\/environment\.[cm]?[jt]sx?$/.test(normalized) ||
|
|
225
|
+
/^(env|environment)\./.test(basename)
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function isCliEntryFile(file) {
|
|
230
|
+
return (
|
|
231
|
+
/^#!\/usr\/bin\/env\s+node/m.test(file.content) ||
|
|
232
|
+
/\bprocess\.argv\b/.test(file.content) ||
|
|
233
|
+
/\bUsage:\s*[\r\n]/.test(file.content) ||
|
|
234
|
+
/\bworkq\s+(connect|bootstrap|natural-git|reset|mcp)\b/.test(file.content)
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function extractCliFacts(file) {
|
|
239
|
+
if (!isCliEntryFile(file)) return [];
|
|
240
|
+
const facts = [];
|
|
241
|
+
if (/\bworkq\s+connect\b/.test(file.content)) facts.push("사용자는 터미널에서 현재 로컬 git 저장소를 Work Q 프로젝트에 연결한다.");
|
|
242
|
+
if (/\bworkq\s+bootstrap\b/.test(file.content)) facts.push("사용자는 현재 저장소를 분석해 프로젝트 컨텍스트와 기능 지도를 Work Q에 초기 등록한다.");
|
|
243
|
+
if (/\bworkq\s+natural-git\b/.test(file.content)) facts.push("사용자는 현재 저장소의 구현을 기능 중심 자연어 명세로 변환해 Work Q natural-spec repo에 업로드한다.");
|
|
244
|
+
if (/\bworkq\s+reset\b/.test(file.content)) facts.push("사용자는 프로젝트 키를 다시 확인한 뒤 Work Q에 저장된 프로젝트 데이터를 초기화한다.");
|
|
245
|
+
if (/\bworkq\s+mcp\b/.test(file.content)) facts.push("사용자는 데스크톱 AI 클라이언트가 연결할 수 있는 Work Q MCP bridge를 실행한다.");
|
|
246
|
+
if (/git\s*\(\s*\[\s*["']rev-parse["']|git\s+rev-parse/.test(file.content)) facts.push("명령 실행 시 현재 저장소의 로컬 경로, 현재 브랜치, 현재 커밋을 읽어 Work Q에 전달한다.");
|
|
247
|
+
if (/remote["']?,\s*["']get-url|git\s+remote\s+get-url/.test(file.content)) facts.push("명령 실행 시 원격 GitHub 저장소 주소를 읽어 프로젝트의 owner와 repo를 구분한다.");
|
|
248
|
+
if (/WORKQ_MCP_TOKEN|WORKQ_MCP_TOKEN_FILE/.test(file.content)) facts.push("MCP 인증 토큰은 환경 변수 또는 토큰 파일에서 읽어 Work Q API 호출에 사용한다.");
|
|
249
|
+
if (/WORKQ_MCP_URL/.test(file.content)) facts.push("Work Q MCP API 주소는 환경 변수로 바꿀 수 있고, 없으면 기본 운영 endpoint를 사용한다.");
|
|
250
|
+
return unique(facts, 14);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function extractCliRules(file) {
|
|
254
|
+
if (!isCliEntryFile(file)) return [];
|
|
255
|
+
const rules = [];
|
|
256
|
+
if (/--project-key/.test(file.content)) rules.push("프로젝트를 식별하는 key는 연결, 초기 분석, 자연어 명세 업로드, 초기화 명령에서 필수 입력이다.");
|
|
257
|
+
if (/--confirm-project-key/.test(file.content)) rules.push("프로젝트 초기화는 실수를 막기 위해 같은 프로젝트 key를 확인값으로 한 번 더 요구한다.");
|
|
258
|
+
if (/Only github\.com remotes are supported|github\.com/.test(file.content)) rules.push("원격 저장소는 GitHub 주소만 지원한다.");
|
|
259
|
+
if (/WORKQ_MCP_TOKEN_FILE|\.workq\/mcp-token/.test(file.content)) rules.push("직접 토큰이 없으면 지정된 토큰 파일을 읽고, 기본 토큰 파일 경로를 사용한다.");
|
|
260
|
+
if (/throw new Error/.test(file.content)) rules.push("필수 입력이나 연결 정보가 없으면 조용히 진행하지 않고 사용자에게 오류를 알린다.");
|
|
261
|
+
return unique(rules, 10);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function extractEnvironmentFacts(file) {
|
|
265
|
+
if (!isEnvironmentConfigFile(file)) return [];
|
|
266
|
+
const envVars = unique(
|
|
267
|
+
Array.from(file.content.matchAll(/process\.env(?:\.([A-Za-z_][A-Za-z0-9_]*)|\[["'`]([^"'`]+)["'`]\])/g)).map((match) => match[1] || match[2]),
|
|
268
|
+
18
|
|
269
|
+
);
|
|
270
|
+
const facts = [];
|
|
271
|
+
if (!envVars.length && !/get(DataDir|DbDir|WorkDir|AppUrl)|isMcpConfigured|getMiniAiConfig|isGithubCrudConfigured|requireEnv/.test(file.content)) return facts;
|
|
272
|
+
if (envVars.length) facts.push("데이터 저장 위치, 외부 접속 주소, 에이전트 연결, AI 분류, GitHub 자동화, 테스트 모드 같은 런타임 설정을 외부 설정값으로 결정한다.");
|
|
273
|
+
if (/getDataDir|getDbDir|getWorkDir/.test(file.content)) facts.push("서비스 데이터, 내부 데이터베이스, 작업 파일이 저장될 위치를 외부 설정과 기본값으로 분리한다.");
|
|
274
|
+
if (/getAppUrl/.test(file.content)) facts.push("앱 외부 접속 주소는 배포 환경에서 지정하고, 없으면 로컬 개발 주소를 기본값으로 사용한다.");
|
|
275
|
+
if (/isMcpConfigured|WORKQ_MCP_TOKEN/.test(file.content)) facts.push("MCP 인증 정보가 설정되어 있는지 확인해 로컬 에이전트 연결 가능 상태를 구분한다.");
|
|
276
|
+
if (/getMiniAiConfig|isMiniAiConfigured|WORKQ_MINI_AI/.test(file.content)) facts.push("AI 분류 서버 주소, 사용할 모델, 인증 정보를 하나의 설정으로 묶어 자동 분류 가능 여부를 판단한다.");
|
|
277
|
+
if (/isGithubCrudConfigured|WORKQ_GITHUB_TOKEN/.test(file.content)) facts.push("GitHub 자동화 인증 정보가 있으면 코드 변경 자동화 기능을 사용할 수 있는 상태로 판단한다.");
|
|
278
|
+
if (/requireEnv/.test(file.content)) facts.push("필수 환경 변수가 없으면 조용히 진행하지 않고 명시적인 설정 오류를 발생시킨다.");
|
|
279
|
+
return unique(facts, 10);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function extractFunctionalFacts(file, structure) {
|
|
283
|
+
const facts = [];
|
|
284
|
+
facts.push(...extractCliFacts(file));
|
|
285
|
+
const environmentFacts = extractEnvironmentFacts(file);
|
|
286
|
+
const visibleTexts = extractVisibleTexts(file);
|
|
287
|
+
facts.push(...environmentFacts);
|
|
288
|
+
if (visibleTexts.length) facts.push(`사용자에게 보이는 주요 문구는 ${visibleTexts.map((item) => `"${item}"`).join(", ")} 이다.`);
|
|
289
|
+
if (structure.events.length) facts.push("사용자 조작에 반응해 화면 상태나 결과 표시를 갱신한다.");
|
|
290
|
+
if (structure.routes.length) facts.push("HTTP 요청을 받아 서비스 상태 조회, 저장, 갱신 같은 서버 기능을 처리한다.");
|
|
291
|
+
if (/\bfor\s*\(\s*let\s+([A-Za-z_$][\w$]*)\s*=\s*(\d+)\s*;\s*\1\s*<=\s*(\d+)/.test(file.content)) {
|
|
292
|
+
const loop = file.content.match(/\bfor\s*\(\s*let\s+([A-Za-z_$][\w$]*)\s*=\s*(\d+)\s*;\s*\1\s*<=\s*(\d+)/);
|
|
293
|
+
facts.push(`반복문은 ${loop[2]}부터 ${loop[3]}까지의 선택지 또는 결과 행을 자동 생성한다.`);
|
|
294
|
+
}
|
|
295
|
+
if (/document\.createElement\(["']button["']\)/.test(file.content)) facts.push("버튼 요소를 코드에서 자동 생성한다.");
|
|
296
|
+
if (/classList\.add\(["']active["']\)|classList\.remove\(["']active["']\)/.test(file.content)) facts.push("선택 상태는 active 클래스로 표시하고 이전 선택은 해제한다.");
|
|
297
|
+
if (/innerHTML\s*=/.test(file.content)) facts.push("사용자 선택 이후 결과 영역의 HTML을 새 내용으로 교체한다.");
|
|
298
|
+
return facts;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function buildVerificationFacts(file, structure) {
|
|
302
|
+
const facts = [];
|
|
303
|
+
if (isCliEntryFile(file)) {
|
|
304
|
+
facts.push("각 CLI 명령이 필수 입력을 요구하고 누락 시 명확한 오류를 보여주는지 확인한다.");
|
|
305
|
+
facts.push("로컬 git 저장소 정보와 Work Q 프로젝트 정보가 의도한 프로젝트에만 연결되는지 확인한다.");
|
|
306
|
+
facts.push("자연어 명세 업로드 명령이 기능 중심 문서를 만들고 Work Q에 정상 반영하는지 확인한다.");
|
|
307
|
+
return facts;
|
|
308
|
+
}
|
|
309
|
+
if (isEnvironmentConfigFile(file)) {
|
|
310
|
+
facts.push("외부 설정값이 있을 때 해당 값이 우선 적용되는지 확인한다.");
|
|
311
|
+
facts.push("외부 설정값이 없을 때 기본값이나 명확한 설정 오류가 의도대로 동작하는지 확인한다.");
|
|
312
|
+
facts.push("에이전트 연결, AI 분류, GitHub 자동화 가능 상태가 설정 유무에 따라 올바르게 구분되는지 확인한다.");
|
|
313
|
+
return facts;
|
|
314
|
+
}
|
|
315
|
+
if (extractVisibleTexts(file).length) facts.push("화면에 노출되는 문구와 버튼이 의도한 사용자 흐름을 설명하는지 확인한다.");
|
|
316
|
+
if (structure.events.length) facts.push("사용자 이벤트 실행 후 화면 상태와 데이터가 기대값으로 바뀌는지 확인한다.");
|
|
317
|
+
if (structure.conditions.length) facts.push("반복/조건의 경계값과 예외 흐름을 테스트한다.");
|
|
318
|
+
if (extractDataModelFacts(file).length) facts.push("저장되는 필드 이름, 타입, 필수 여부가 실제 데이터와 맞는지 확인한다.");
|
|
319
|
+
if (!facts.length) facts.push("이 파일이 제공하는 설정 또는 보조 로직이 연결된 기능에서 실제로 사용되는지 확인한다.");
|
|
320
|
+
return facts;
|
|
197
321
|
}
|
|
198
322
|
|
|
199
323
|
function extractStructure(lines) {
|
|
@@ -237,11 +361,22 @@ function packageJsonDoc(content) {
|
|
|
237
361
|
}
|
|
238
362
|
|
|
239
363
|
function sourceFileToDoc(file) {
|
|
240
|
-
const structure =
|
|
241
|
-
|
|
364
|
+
const structure = {
|
|
365
|
+
imports: [],
|
|
366
|
+
exports: [],
|
|
367
|
+
classes: [],
|
|
368
|
+
routes: [],
|
|
369
|
+
events: [],
|
|
370
|
+
selectors: [],
|
|
371
|
+
...extractStructure(file.lines)
|
|
372
|
+
};
|
|
242
373
|
const purpose =
|
|
243
374
|
file.path === "package.json"
|
|
244
375
|
? "패키지 이름, 실행 스크립트, 의존성, Node 런타임 조건을 정의한다."
|
|
376
|
+
: isCliEntryFile(file)
|
|
377
|
+
? "터미널 명령으로 Work Q 프로젝트 연결, 초기 분석, 자연어 명세 업로드, 프로젝트 초기화, MCP bridge 실행을 수행한다."
|
|
378
|
+
: extractEnvironmentFacts(file).length
|
|
379
|
+
? "런타임 환경 변수와 기본값을 해석해 저장소, URL, MCP, Mini AI, GitHub 연결 상태를 결정한다."
|
|
245
380
|
: file.extension === ".css"
|
|
246
381
|
? "화면 요소의 레이아웃, 색상, 반응형 배치, 상호작용 상태를 정의한다."
|
|
247
382
|
: file.extension === ".html"
|
|
@@ -249,39 +384,56 @@ function sourceFileToDoc(file) {
|
|
|
249
384
|
: structure.functions.length
|
|
250
385
|
? "함수 단위로 데이터를 계산하거나 화면/파일/네트워크 동작을 수행한다."
|
|
251
386
|
: "레포 동작을 구성하는 설정, 데이터, 또는 보조 텍스트를 제공한다.";
|
|
387
|
+
const dataModelFacts = extractDataModelFacts(file);
|
|
388
|
+
const functionalFacts = extractFunctionalFacts(file, structure);
|
|
389
|
+
const designFacts = extractDesignFacts(file);
|
|
390
|
+
const verificationFacts = buildVerificationFacts(file, structure);
|
|
391
|
+
const coreRules = unique(
|
|
392
|
+
[
|
|
393
|
+
...dataModelFacts,
|
|
394
|
+
...extractCliRules(file),
|
|
395
|
+
...extractEnvironmentFacts(file),
|
|
396
|
+
...functionalFacts.filter((item) => /반복문|선택 상태|자동 생성|결과 영역|HTTP 요청|필수|초기화|GitHub|토큰/.test(item))
|
|
397
|
+
],
|
|
398
|
+
12
|
|
399
|
+
);
|
|
400
|
+
const titleText = extractVisibleTexts(file)[0];
|
|
401
|
+
const title = isCliEntryFile(file)
|
|
402
|
+
? "Work Q CLI 기능명세"
|
|
403
|
+
: file.path === "package.json"
|
|
404
|
+
? "프로젝트 실행 환경 기능명세"
|
|
405
|
+
: titleText && file.extension === ".html"
|
|
406
|
+
? `${titleText} 기능명세`
|
|
407
|
+
: extractEnvironmentFacts(file).length
|
|
408
|
+
? "런타임 환경 설정 기능명세"
|
|
409
|
+
: dataModelFacts.length
|
|
410
|
+
? "데이터 모델 기능명세"
|
|
411
|
+
: file.extension === ".css"
|
|
412
|
+
? "화면 디자인 기능명세"
|
|
413
|
+
: `${file.path} 기능명세`;
|
|
252
414
|
return [
|
|
253
|
-
`# ${
|
|
254
|
-
|
|
255
|
-
"## 자연어 명세",
|
|
256
|
-
`이 파일은 ${file.language} 파일이다. ${purpose}`,
|
|
257
|
-
"",
|
|
258
|
-
"## 파일 정보",
|
|
259
|
-
`- 원본 경로: \`${file.path}\``,
|
|
260
|
-
`- 언어/형식: ${file.language}`,
|
|
261
|
-
`- 총 라인 수: ${file.lines.length}`,
|
|
262
|
-
`- 감지된 함수/메서드: ${structure.functions.length}개`,
|
|
263
|
-
`- 감지된 조건/반복/예외 흐름: ${structure.conditions.length}개`,
|
|
415
|
+
`# ${title}`,
|
|
416
|
+
NATURAL_SPEC_FORMAT_COMMENT,
|
|
264
417
|
"",
|
|
265
|
-
|
|
266
|
-
|
|
418
|
+
"## 기능 중심 자연어 명세",
|
|
419
|
+
`제품 관점에서 이 기능은 ${purpose}`,
|
|
267
420
|
"",
|
|
268
|
-
|
|
269
|
-
...
|
|
421
|
+
functionalFacts.length ? "## 사용자 기능과 동작" : "",
|
|
422
|
+
...functionalFacts.map((item) => `- ${item}`),
|
|
270
423
|
"",
|
|
271
|
-
|
|
272
|
-
...
|
|
424
|
+
coreRules.length ? "## 핵심 규칙" : "",
|
|
425
|
+
...coreRules.map((item) => `- ${item}`),
|
|
273
426
|
"",
|
|
274
|
-
|
|
427
|
+
designFacts.length ? "## 화면과 디자인 명세" : "",
|
|
428
|
+
...designFacts.map((item) => `- ${item}`),
|
|
275
429
|
"",
|
|
276
|
-
"##
|
|
277
|
-
|
|
278
|
-
...lineSpec,
|
|
430
|
+
dataModelFacts.length && file.path !== "package.json" ? "## 데이터와 상태" : "",
|
|
431
|
+
...(file.path !== "package.json" ? dataModelFacts.map((item) => `- ${item}`) : []),
|
|
279
432
|
"",
|
|
280
433
|
"## 검증 관점",
|
|
281
|
-
|
|
282
|
-
"-
|
|
283
|
-
"-
|
|
284
|
-
"- 리포트 분류 시 이 문서의 함수, 조건, 라우트, 이벤트 설명을 근거로 실제 구현 여부를 판단한다."
|
|
434
|
+
...verificationFacts.map((item) => `- ${item}`),
|
|
435
|
+
"- 이 문서는 현재 구현에서 관찰한 기능 초안이며, 오너가 승인한 뒤 기준 명세가 된다.",
|
|
436
|
+
"- 리포트 분류 시 이 문서는 기능 존재, 화면 동작, 입력/출력, 핵심 규칙의 근거로 사용한다."
|
|
285
437
|
].filter((line, index, array) => line || array[index - 1]).join("\n");
|
|
286
438
|
}
|
|
287
439
|
|
|
@@ -290,9 +442,10 @@ function overviewDoc({ projectKey, projectName, repository, root, sourceFiles })
|
|
|
290
442
|
for (const file of sourceFiles) byLanguage.set(file.language, (byLanguage.get(file.language) ?? 0) + 1);
|
|
291
443
|
return [
|
|
292
444
|
`# ${projectKey} 자연어 git 명세`,
|
|
445
|
+
NATURAL_SPEC_FORMAT_COMMENT,
|
|
293
446
|
"",
|
|
294
447
|
"## 의미",
|
|
295
|
-
"이 명세는 레포에 존재하는 코드 파일을
|
|
448
|
+
"이 명세는 레포에 존재하는 코드 파일을 제품 기능, 사용자 동작, 입력/출력, 화면/디자인, 데이터 규칙 중심의 자연어 Markdown 문서로 해석한 것이다. Work Q v2에서는 이 자연어 문서를 기준으로 리포트와 코드 변경을 판단한다.",
|
|
296
449
|
"",
|
|
297
450
|
"## 프로젝트",
|
|
298
451
|
`- 프로젝트 키: ${projectKey}`,
|
|
@@ -305,8 +458,8 @@ function overviewDoc({ projectKey, projectName, repository, root, sourceFiles })
|
|
|
305
458
|
...Array.from(byLanguage.entries()).map(([language, count]) => `- ${language}: ${count}개`),
|
|
306
459
|
"",
|
|
307
460
|
"## 운영 규칙",
|
|
308
|
-
"- 자연어 문서는 코드에서 관찰한
|
|
309
|
-
"- 각 코드 파일 문서는
|
|
461
|
+
"- 자연어 문서는 코드에서 관찰한 제품 기능과 검증 가능한 동작을 설명한다.",
|
|
462
|
+
"- 각 코드 파일 문서는 라인별 번역이 아니라 기능, 규칙, 화면, 데이터, 검증 관점 중심으로 작성한다.",
|
|
310
463
|
"- 코드의 현재 동작이 곧 제품 의도라는 뜻은 아니다.",
|
|
311
464
|
"- 오너가 자연어 커밋으로 승인한 내용만 다음 코드 변경의 기준이 된다.",
|
|
312
465
|
"- 코드가 바뀌면 이 자연어 git 명세도 다시 분석하거나 수정해야 한다."
|