sonamu 0.8.24 → 0.8.26
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/dist/api/__tests__/config.test.js +189 -0
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +7 -2
- package/dist/api/sonamu.d.ts.map +1 -1
- package/dist/api/sonamu.js +14 -10
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +2 -1
- package/dist/auth/knex-adapter.d.ts +23 -0
- package/dist/auth/knex-adapter.d.ts.map +1 -0
- package/dist/auth/knex-adapter.js +163 -0
- package/dist/auth/plugins/wrappers/admin.d.ts +2 -2
- package/dist/bin/__tests__/ts-loader-register.test.js +45 -0
- package/dist/bin/cli.js +47 -9
- package/dist/bin/ts-loader-register.js +3 -29
- package/dist/bin/ts-loader-registration.d.ts +2 -0
- package/dist/bin/ts-loader-registration.d.ts.map +1 -0
- package/dist/bin/ts-loader-registration.js +42 -0
- package/dist/cone/cone-generator.js +3 -3
- package/dist/database/puri-subset.test-d.js +9 -1
- package/dist/database/puri-subset.types.d.ts +1 -1
- package/dist/database/puri-subset.types.d.ts.map +1 -1
- package/dist/database/puri-subset.types.js +1 -1
- package/dist/testing/fixture-generator.js +5 -5
- package/dist/ui/ai-client.js +2 -2
- package/dist/ui/api.d.ts.map +1 -1
- package/dist/ui/api.js +14 -14
- package/dist/ui/cdd-service.d.ts +15 -18
- package/dist/ui/cdd-service.d.ts.map +1 -1
- package/dist/ui/cdd-service.js +246 -222
- package/dist/ui/cdd-types.d.ts +41 -68
- package/dist/ui/cdd-types.d.ts.map +1 -1
- package/dist/ui/cdd-types.js +2 -2
- package/dist/ui-web/assets/index-CKo0Z2Iu.css +1 -0
- package/dist/ui-web/assets/{index-CxiydzeC.js → index-DK-2aacv.js} +83 -83
- package/dist/ui-web/index.html +2 -2
- package/package.json +6 -2
- package/src/api/__tests__/config.test.ts +225 -0
- package/src/api/config.ts +10 -4
- package/src/api/sonamu.ts +16 -13
- package/src/auth/index.ts +1 -0
- package/src/auth/knex-adapter.ts +208 -0
- package/src/bin/__tests__/ts-loader-register.test.ts +62 -0
- package/src/bin/cli.ts +52 -9
- package/src/bin/ts-loader-register.ts +2 -32
- package/src/bin/ts-loader-registration.ts +55 -0
- package/src/cone/cone-generator.ts +2 -2
- package/src/database/puri-subset.test-d.ts +102 -0
- package/src/database/puri-subset.types.ts +1 -1
- package/src/skills/commands/sonamu-skills.md +20 -0
- package/src/skills/sonamu/SKILL.md +179 -137
- package/src/skills/sonamu/ai-agents.md +69 -69
- package/src/skills/sonamu/api.md +147 -147
- package/src/skills/sonamu/auth-migration.md +220 -220
- package/src/skills/sonamu/auth-plugins.md +83 -83
- package/src/skills/sonamu/auth.md +106 -106
- package/src/skills/sonamu/cdd.md +65 -200
- package/src/skills/sonamu/cone.md +138 -138
- package/src/skills/sonamu/config.md +191 -191
- package/src/skills/sonamu/create-sonamu.md +66 -66
- package/src/skills/sonamu/database.md +158 -158
- package/src/skills/sonamu/entity-basic.md +292 -293
- package/src/skills/sonamu/entity-relations.md +246 -246
- package/src/skills/sonamu/entity-validation-checklist.md +124 -124
- package/src/skills/sonamu/fixture-cli.md +231 -231
- package/src/skills/sonamu/framework-change.md +37 -37
- package/src/skills/sonamu/frontend.md +223 -223
- package/src/skills/sonamu/i18n.md +82 -82
- package/src/skills/sonamu/migration.md +77 -77
- package/src/skills/sonamu/model.md +222 -222
- package/src/skills/sonamu/naite.md +86 -86
- package/src/skills/sonamu/project-init.md +228 -228
- package/src/skills/sonamu/puri.md +122 -122
- package/src/skills/sonamu/scaffolding.md +154 -154
- package/src/skills/sonamu/skill-contribution.md +124 -124
- package/src/skills/sonamu/subset.md +46 -46
- package/src/skills/sonamu/tasks.md +82 -82
- package/src/skills/sonamu/testing-devrunner.md +147 -147
- package/src/skills/sonamu/testing.md +673 -673
- package/src/skills/sonamu/upsert.md +79 -79
- package/src/skills/sonamu/vector.md +67 -67
- package/src/testing/fixture-generator.ts +4 -4
- package/src/ui/ai-client.ts +1 -1
- package/src/ui/api.ts +18 -17
- package/src/ui/cdd-service.ts +264 -254
- package/src/ui/cdd-types.ts +40 -75
- package/dist/ui-web/assets/index-BrQKU3j9.css +0 -1
- package/src/skills/sonamu/workflow.md +0 -317
package/src/ui/cdd-service.ts
CHANGED
|
@@ -4,30 +4,28 @@ import os from "os";
|
|
|
4
4
|
import path from "path";
|
|
5
5
|
import { Sonamu } from "../api/sonamu";
|
|
6
6
|
import type {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
CddAcEntry,
|
|
8
|
+
CddAcFile,
|
|
9
|
+
CddAcListResult,
|
|
10
|
+
CddAddRuleRequest,
|
|
11
|
+
CddContentResult,
|
|
10
12
|
CddFileType,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
CddSchemaSummary,
|
|
15
|
-
CddSpecStatus,
|
|
13
|
+
CddRuleDetail,
|
|
14
|
+
CddRuleEntry,
|
|
15
|
+
CddRuleSummary,
|
|
16
16
|
CddTreeNode,
|
|
17
17
|
} from "./cdd-types";
|
|
18
18
|
|
|
19
19
|
export type {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
CddAcEntry,
|
|
21
|
+
CddAcFile,
|
|
22
|
+
CddAcListResult,
|
|
23
|
+
CddAddRuleRequest,
|
|
24
|
+
CddContentResult,
|
|
23
25
|
CddFileType,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
CddSchemaFieldType,
|
|
28
|
-
CddSchemaReference,
|
|
29
|
-
CddSchemaSummary,
|
|
30
|
-
CddSpecStatus,
|
|
26
|
+
CddRuleDetail,
|
|
27
|
+
CddRuleEntry,
|
|
28
|
+
CddRuleSummary,
|
|
31
29
|
CddTreeNode,
|
|
32
30
|
} from "./cdd-types";
|
|
33
31
|
|
|
@@ -36,6 +34,11 @@ function getContractDir(): string {
|
|
|
36
34
|
return path.join(Sonamu.apiRootPath, "..", "..", "contract");
|
|
37
35
|
}
|
|
38
36
|
|
|
37
|
+
/** 프로젝트 루트 경로 반환 */
|
|
38
|
+
function getProjectRoot(): string {
|
|
39
|
+
return path.join(Sonamu.apiRootPath, "..", "..");
|
|
40
|
+
}
|
|
41
|
+
|
|
39
42
|
/** 경로가 contract/ 디렉터리 내부인지 검증 */
|
|
40
43
|
function assertInsideContractDir(filePath: string): void {
|
|
41
44
|
const contractDir = getContractDir();
|
|
@@ -47,8 +50,8 @@ function assertInsideContractDir(filePath: string): void {
|
|
|
47
50
|
|
|
48
51
|
/** 파일명에서 CddFileType 판별 */
|
|
49
52
|
function detectFileType(fileName: string): CddFileType | undefined {
|
|
50
|
-
if (fileName.endsWith(".contract.
|
|
51
|
-
if (fileName.endsWith(".
|
|
53
|
+
if (fileName.endsWith(".contract.md")) return "contract";
|
|
54
|
+
if (fileName.endsWith(".rules.json")) return "rules";
|
|
52
55
|
return undefined;
|
|
53
56
|
}
|
|
54
57
|
|
|
@@ -62,16 +65,19 @@ function scanDirectory(dirPath: string, relativeTo: string): CddTreeNode[] {
|
|
|
62
65
|
const relPath = path.relative(relativeTo, fullPath);
|
|
63
66
|
|
|
64
67
|
if (entry.isDirectory()) {
|
|
68
|
+
if (entry.name === "rules") continue;
|
|
65
69
|
const children = scanDirectory(fullPath, relativeTo);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
if (children.length > 0) {
|
|
71
|
+
nodes.push({
|
|
72
|
+
name: entry.name,
|
|
73
|
+
path: relPath,
|
|
74
|
+
type: "directory",
|
|
75
|
+
children,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
72
78
|
} else if (entry.isFile()) {
|
|
73
79
|
const fileType = detectFileType(entry.name);
|
|
74
|
-
if (fileType) {
|
|
80
|
+
if (fileType && fileType !== "rules") {
|
|
75
81
|
nodes.push({
|
|
76
82
|
name: entry.name,
|
|
77
83
|
path: relPath,
|
|
@@ -95,17 +101,8 @@ export function getCddTree(): { exists: boolean; tree: CddTreeNode[] } {
|
|
|
95
101
|
return { exists: true, tree };
|
|
96
102
|
}
|
|
97
103
|
|
|
98
|
-
/**
|
|
99
|
-
function
|
|
100
|
-
const contractDir = getContractDir();
|
|
101
|
-
const schemaPath = path.join(contractDir, "schemas", `${schemaId}.schema.json`);
|
|
102
|
-
if (!fs.existsSync(schemaPath)) return null;
|
|
103
|
-
const raw = fs.readFileSync(schemaPath, "utf-8");
|
|
104
|
-
return JSON.parse(raw) as CddSchema;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** JSON 파일의 전체 내용을 읽어 schema와 함께 envelope로 반환 */
|
|
108
|
-
export function readContent(filePath: string): CddContentEnvelope {
|
|
104
|
+
/** 파일 내용을 읽어 반환 (contract.md → markdown 원문, rules.json → JSON 문자열) */
|
|
105
|
+
export function readContent(filePath: string): CddContentResult {
|
|
109
106
|
assertInsideContractDir(filePath);
|
|
110
107
|
|
|
111
108
|
const contractDir = getContractDir();
|
|
@@ -115,20 +112,16 @@ export function readContent(filePath: string): CddContentEnvelope {
|
|
|
115
112
|
throw new Error(`파일을 찾을 수 없습니다: ${filePath}`);
|
|
116
113
|
}
|
|
117
114
|
|
|
118
|
-
const
|
|
119
|
-
const document = JSON.parse(raw) as Record<string, unknown>;
|
|
115
|
+
const content = fs.readFileSync(absPath, "utf-8");
|
|
120
116
|
const fileType = detectFileType(path.basename(filePath));
|
|
121
|
-
const schemaId = typeof document.schema === "string" ? document.schema : null;
|
|
122
|
-
const schema = schemaId ? resolveSchema(schemaId) : null;
|
|
123
117
|
|
|
124
118
|
return {
|
|
125
|
-
|
|
126
|
-
schema,
|
|
119
|
+
content,
|
|
127
120
|
fileType: fileType ?? "contract",
|
|
128
121
|
};
|
|
129
122
|
}
|
|
130
123
|
|
|
131
|
-
/**
|
|
124
|
+
/** 파일을 외부 에디터로 직접 편집 */
|
|
132
125
|
export async function editContent(
|
|
133
126
|
filePath: string,
|
|
134
127
|
): Promise<{ success: boolean; filePath: string }> {
|
|
@@ -203,269 +196,286 @@ function runEditor(editor: { bin: string; args: string[] }, filePath: string): P
|
|
|
203
196
|
});
|
|
204
197
|
}
|
|
205
198
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
/** contract/schemas/ 디렉터리 내 .schema.json 파일 경로 목록 반환 */
|
|
211
|
-
function scanSchemaFiles(): { absPath: string; relPath: string; fileName: string }[] {
|
|
212
|
-
const schemasDir = path.join(getContractDir(), "schemas");
|
|
213
|
-
if (!fs.existsSync(schemasDir)) return [];
|
|
214
|
-
|
|
215
|
-
return fs
|
|
216
|
-
.readdirSync(schemasDir, { withFileTypes: true })
|
|
217
|
-
.filter((e) => e.isFile() && e.name.endsWith(".schema.json"))
|
|
218
|
-
.map((e) => ({
|
|
219
|
-
absPath: path.join(schemasDir, e.name),
|
|
220
|
-
relPath: `schemas/${e.name}`,
|
|
221
|
-
fileName: e.name,
|
|
222
|
-
}));
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** 특정 schemaId를 참조하는 contract/spec 문서들을 재귀 수집 */
|
|
226
|
-
function collectSchemaReferences(
|
|
227
|
-
schemaId: string,
|
|
228
|
-
dirPath: string,
|
|
229
|
-
relativeTo: string,
|
|
230
|
-
): CddSchemaReference[] {
|
|
231
|
-
if (!fs.existsSync(dirPath)) return [];
|
|
232
|
-
const refs: CddSchemaReference[] = [];
|
|
233
|
-
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
199
|
+
/** 소스 파일을 외부 에디터로 열기 (대기하지 않음) */
|
|
200
|
+
export function openSourceFile(filePath: string): void {
|
|
201
|
+
const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(Sonamu.apiRootPath, filePath);
|
|
234
202
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (entry.isDirectory()) {
|
|
238
|
-
if (entry.name === "schemas") continue;
|
|
239
|
-
refs.push(...collectSchemaReferences(schemaId, fullPath, relativeTo));
|
|
240
|
-
} else if (entry.isFile()) {
|
|
241
|
-
const fileType = detectFileType(entry.name);
|
|
242
|
-
if (!fileType) continue;
|
|
243
|
-
try {
|
|
244
|
-
const raw = fs.readFileSync(fullPath, "utf-8");
|
|
245
|
-
const doc = JSON.parse(raw) as Record<string, unknown>;
|
|
246
|
-
if (doc.schema === schemaId) {
|
|
247
|
-
refs.push({
|
|
248
|
-
path: path.relative(relativeTo, fullPath),
|
|
249
|
-
fileType,
|
|
250
|
-
name: entry.name,
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
} catch {
|
|
254
|
-
// JSON 파싱 실패 시 무시
|
|
255
|
-
}
|
|
256
|
-
}
|
|
203
|
+
if (!fs.existsSync(absPath)) {
|
|
204
|
+
throw new Error(`파일을 찾을 수 없습니다: ${filePath}`);
|
|
257
205
|
}
|
|
258
|
-
return refs;
|
|
259
|
-
}
|
|
260
206
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
207
|
+
const editor = resolveEditorCli({ wait: false });
|
|
208
|
+
const child = spawn(editor.bin, [...editor.args, absPath], {
|
|
209
|
+
stdio: "ignore",
|
|
210
|
+
detached: true,
|
|
211
|
+
});
|
|
212
|
+
child.unref();
|
|
264
213
|
}
|
|
265
214
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const resolved = path.resolve(schemasDir, `${schemaKey}.schema.json`);
|
|
270
|
-
if (!resolved.startsWith(schemasDir + path.sep) && resolved !== schemasDir) {
|
|
271
|
-
throw new Error(`유효하지 않은 스키마 키입니다: ${schemaKey}`);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
215
|
+
/* ========================================================================
|
|
216
|
+
* Rules API
|
|
217
|
+
* ======================================================================== */
|
|
274
218
|
|
|
275
|
-
/**
|
|
276
|
-
export function
|
|
219
|
+
/** contract/rules/ 디렉터리 내 .rules.json 파일 목록 반환 */
|
|
220
|
+
export function listRules(): { rules: CddRuleSummary[] } {
|
|
277
221
|
const contractDir = getContractDir();
|
|
278
|
-
const
|
|
279
|
-
|
|
222
|
+
const rulesDir = path.join(contractDir, "rules");
|
|
223
|
+
if (!fs.existsSync(rulesDir)) return { rules: [] };
|
|
224
|
+
|
|
225
|
+
const entries = fs.readdirSync(rulesDir, { withFileTypes: true });
|
|
226
|
+
const rules: CddRuleSummary[] = [];
|
|
227
|
+
|
|
228
|
+
for (const entry of entries) {
|
|
229
|
+
if (!entry.isFile() || !entry.name.endsWith(".rules.json")) continue;
|
|
230
|
+
|
|
231
|
+
const key = entry.name.replace(/\.rules\.json$/, "");
|
|
232
|
+
const relPath = `rules/${entry.name}`;
|
|
233
|
+
const absPath = path.join(rulesDir, entry.name);
|
|
280
234
|
|
|
281
|
-
for (const file of files) {
|
|
282
|
-
const key = expectedSchemaId(file.fileName);
|
|
283
235
|
try {
|
|
284
|
-
const raw = fs.readFileSync(
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
schemas.push({
|
|
236
|
+
const raw = fs.readFileSync(absPath, "utf-8");
|
|
237
|
+
const doc = JSON.parse(raw) as { description?: string; rules?: unknown[] };
|
|
238
|
+
rules.push({
|
|
288
239
|
key,
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
fieldCount: schema.fields.length,
|
|
293
|
-
referenceCount: refs.length,
|
|
294
|
-
hasIdMismatch: schema.id !== key,
|
|
240
|
+
path: relPath,
|
|
241
|
+
description: typeof doc.description === "string" ? doc.description : "",
|
|
242
|
+
ruleCount: Array.isArray(doc.rules) ? doc.rules.length : 0,
|
|
295
243
|
});
|
|
296
244
|
} catch (err) {
|
|
297
|
-
|
|
245
|
+
rules.push({
|
|
298
246
|
key,
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
fieldCount: 0,
|
|
303
|
-
referenceCount: 0,
|
|
304
|
-
hasIdMismatch: false,
|
|
247
|
+
path: relPath,
|
|
248
|
+
description: "",
|
|
249
|
+
ruleCount: 0,
|
|
305
250
|
parseError: err instanceof Error ? err.message : String(err),
|
|
306
251
|
});
|
|
307
252
|
}
|
|
308
253
|
}
|
|
309
254
|
|
|
310
|
-
return {
|
|
255
|
+
return { rules };
|
|
311
256
|
}
|
|
312
257
|
|
|
313
|
-
/**
|
|
314
|
-
export function
|
|
315
|
-
assertInsideSchemaDir(schemaKey);
|
|
316
|
-
|
|
258
|
+
/** rules 파일 상세 반환 */
|
|
259
|
+
export function readRule(ruleKey: string): CddRuleDetail {
|
|
317
260
|
const contractDir = getContractDir();
|
|
318
|
-
const absPath = path.join(contractDir, "
|
|
261
|
+
const absPath = path.join(contractDir, "rules", `${ruleKey}.rules.json`);
|
|
319
262
|
|
|
320
263
|
if (!fs.existsSync(absPath)) {
|
|
321
|
-
throw new Error(
|
|
264
|
+
throw new Error(`Rules 파일을 찾을 수 없습니다: ${ruleKey}`);
|
|
322
265
|
}
|
|
323
266
|
|
|
324
267
|
const raw = fs.readFileSync(absPath, "utf-8");
|
|
325
|
-
const
|
|
326
|
-
const relPath = path.relative(contractDir, absPath);
|
|
327
|
-
const references = collectSchemaReferences(schema.id, contractDir, contractDir);
|
|
268
|
+
const doc = JSON.parse(raw) as { description?: string; rules?: CddRuleEntry[] };
|
|
328
269
|
|
|
329
270
|
return {
|
|
330
|
-
key:
|
|
331
|
-
path:
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
hasIdMismatch: schema.id !== schemaKey,
|
|
271
|
+
key: ruleKey,
|
|
272
|
+
path: `rules/${ruleKey}.rules.json`,
|
|
273
|
+
description: typeof doc.description === "string" ? doc.description : "",
|
|
274
|
+
rules: Array.isArray(doc.rules) ? doc.rules : [],
|
|
335
275
|
};
|
|
336
276
|
}
|
|
337
277
|
|
|
338
|
-
/**
|
|
339
|
-
export
|
|
340
|
-
schemaKey: string,
|
|
341
|
-
): Promise<{ success: boolean; schemaKey: string }> {
|
|
342
|
-
assertInsideSchemaDir(schemaKey);
|
|
343
|
-
|
|
278
|
+
/** rules 파일에 규칙 추가 */
|
|
279
|
+
export function addRule(req: CddAddRuleRequest): CddRuleDetail {
|
|
344
280
|
const contractDir = getContractDir();
|
|
345
|
-
const
|
|
281
|
+
const rulesDir = path.join(contractDir, "rules");
|
|
282
|
+
const absPath = path.join(rulesDir, `${req.ruleKey}.rules.json`);
|
|
346
283
|
|
|
347
284
|
if (!fs.existsSync(absPath)) {
|
|
348
|
-
throw new Error(
|
|
285
|
+
throw new Error(`Rules 파일을 찾을 수 없습니다: ${req.ruleKey}`);
|
|
349
286
|
}
|
|
350
287
|
|
|
351
|
-
const
|
|
352
|
-
|
|
288
|
+
const raw = fs.readFileSync(absPath, "utf-8");
|
|
289
|
+
const doc = JSON.parse(raw) as { description?: string; rules?: CddRuleEntry[] };
|
|
290
|
+
const rules: CddRuleEntry[] = Array.isArray(doc.rules) ? doc.rules : [];
|
|
291
|
+
|
|
292
|
+
const nextId = generateNextRuleId(rules);
|
|
293
|
+
const newEntry: CddRuleEntry = {
|
|
294
|
+
id: nextId,
|
|
295
|
+
when: req.when,
|
|
296
|
+
instruction: req.instruction,
|
|
297
|
+
};
|
|
298
|
+
if (req.examples && req.examples.length > 0) {
|
|
299
|
+
newEntry.examples = req.examples;
|
|
300
|
+
}
|
|
353
301
|
|
|
354
|
-
|
|
355
|
-
|
|
302
|
+
rules.push(newEntry);
|
|
303
|
+
doc.rules = rules;
|
|
356
304
|
|
|
357
|
-
|
|
358
|
-
"draft",
|
|
359
|
-
"specifying",
|
|
360
|
-
"implementing",
|
|
361
|
-
"validating",
|
|
362
|
-
"done",
|
|
363
|
-
]);
|
|
305
|
+
fs.writeFileSync(absPath, `${JSON.stringify(doc, null, 2)}\n`, "utf-8");
|
|
364
306
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
307
|
+
return {
|
|
308
|
+
key: req.ruleKey,
|
|
309
|
+
path: `rules/${req.ruleKey}.rules.json`,
|
|
310
|
+
description: typeof doc.description === "string" ? doc.description : "",
|
|
311
|
+
rules,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** 기존 id 패턴을 분석하여 다음 순번 id 생성 */
|
|
316
|
+
function generateNextRuleId(rules: CddRuleEntry[]): string {
|
|
317
|
+
if (rules.length === 0) return "R-001";
|
|
318
|
+
|
|
319
|
+
const numericPattern = /^(.+?)(\d+)$/;
|
|
320
|
+
let bestPrefix = "R-";
|
|
321
|
+
let maxNum = 0;
|
|
322
|
+
|
|
323
|
+
for (const rule of rules) {
|
|
324
|
+
const match = numericPattern.exec(rule.id);
|
|
325
|
+
if (match) {
|
|
326
|
+
const prefix = match[1];
|
|
327
|
+
const num = Number.parseInt(match[2], 10);
|
|
328
|
+
if (num > maxNum) {
|
|
329
|
+
bestPrefix = prefix;
|
|
330
|
+
maxNum = num;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
378
333
|
}
|
|
379
334
|
|
|
380
|
-
const
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
implementing: 0,
|
|
385
|
-
validating: 0,
|
|
386
|
-
done: 0,
|
|
387
|
-
};
|
|
335
|
+
const nextNum = maxNum + 1;
|
|
336
|
+
const padLen = Math.max(3, String(maxNum).length);
|
|
337
|
+
return `${bestPrefix}${String(nextNum).padStart(padLen, "0")}`;
|
|
338
|
+
}
|
|
388
339
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
340
|
+
/* ========================================================================
|
|
341
|
+
* AC API (modules/cdd ac-list 파싱 로직 복사)
|
|
342
|
+
* ======================================================================== */
|
|
343
|
+
|
|
344
|
+
/** describe/test 패턴 파싱 */
|
|
345
|
+
function parseAcEntries(content: string): CddAcEntry[] {
|
|
346
|
+
const entries: CddAcEntry[] = [];
|
|
347
|
+
const lines = content.split("\n");
|
|
348
|
+
|
|
349
|
+
let currentDescribe: string | null = null;
|
|
350
|
+
let describeDepth = 0;
|
|
351
|
+
let braceDepth = 0;
|
|
352
|
+
let pendingTestAs = false;
|
|
353
|
+
let testAsBraceDepth = 0;
|
|
354
|
+
let testAsNeedName = false;
|
|
355
|
+
|
|
356
|
+
for (const line of lines) {
|
|
357
|
+
const trimmed = line.trim();
|
|
358
|
+
|
|
359
|
+
const describeMatch = trimmed.match(/^describe\(["'`](.+?)["'`]/);
|
|
360
|
+
if (describeMatch) {
|
|
361
|
+
currentDescribe = describeMatch[1];
|
|
362
|
+
describeDepth = braceDepth;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const testMatch = trimmed.match(/^(?:test|it)\(["'`](.+?)["'`]/);
|
|
366
|
+
if (testMatch) {
|
|
367
|
+
entries.push({
|
|
368
|
+
describe: currentDescribe,
|
|
369
|
+
test: testMatch[1],
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (!pendingTestAs && trimmed.match(/^testAs\s*\(/)) {
|
|
374
|
+
const inlineMatch = trimmed.match(/^testAs\s*\(\s*\{[^}]*\}\s*,\s*["'`](.+?)["'`]/);
|
|
375
|
+
if (inlineMatch) {
|
|
376
|
+
entries.push({ describe: currentDescribe, test: inlineMatch[1] });
|
|
377
|
+
} else {
|
|
378
|
+
pendingTestAs = true;
|
|
379
|
+
testAsBraceDepth = 0;
|
|
380
|
+
testAsNeedName = false;
|
|
381
|
+
for (const ch of trimmed) {
|
|
382
|
+
if (ch === "{") testAsBraceDepth++;
|
|
383
|
+
if (ch === "}") testAsBraceDepth--;
|
|
384
|
+
}
|
|
385
|
+
if (testAsBraceDepth <= 0) {
|
|
386
|
+
testAsNeedName = true;
|
|
387
|
+
const nameMatch = trimmed.match(/}\s*,\s*["'`](.+?)["'`]/);
|
|
388
|
+
if (nameMatch) {
|
|
389
|
+
entries.push({ describe: currentDescribe, test: nameMatch[1] });
|
|
390
|
+
pendingTestAs = false;
|
|
391
|
+
testAsNeedName = false;
|
|
430
392
|
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
} else if (pendingTestAs) {
|
|
396
|
+
if (!testAsNeedName) {
|
|
397
|
+
for (const ch of trimmed) {
|
|
398
|
+
if (ch === "{") testAsBraceDepth++;
|
|
399
|
+
if (ch === "}") testAsBraceDepth--;
|
|
400
|
+
}
|
|
401
|
+
if (testAsBraceDepth <= 0) {
|
|
402
|
+
testAsNeedName = true;
|
|
403
|
+
const nameMatch = trimmed.match(/}\s*,\s*["'`](.+?)["'`]/);
|
|
404
|
+
if (nameMatch) {
|
|
405
|
+
entries.push({ describe: currentDescribe, test: nameMatch[1] });
|
|
406
|
+
pendingTestAs = false;
|
|
407
|
+
testAsNeedName = false;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
} else {
|
|
411
|
+
const nameMatch = trimmed.match(/^["'`](.+?)["'`]/);
|
|
412
|
+
if (nameMatch) {
|
|
413
|
+
entries.push({ describe: currentDescribe, test: nameMatch[1] });
|
|
414
|
+
pendingTestAs = false;
|
|
415
|
+
testAsNeedName = false;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
431
419
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
parseError: err instanceof Error ? err.message : String(err),
|
|
439
|
-
});
|
|
420
|
+
for (const ch of trimmed) {
|
|
421
|
+
if (ch === "{") braceDepth++;
|
|
422
|
+
if (ch === "}") {
|
|
423
|
+
braceDepth--;
|
|
424
|
+
if (currentDescribe && braceDepth <= describeDepth) {
|
|
425
|
+
currentDescribe = null;
|
|
440
426
|
}
|
|
441
427
|
}
|
|
442
428
|
}
|
|
443
429
|
}
|
|
444
430
|
|
|
445
|
-
|
|
431
|
+
return entries;
|
|
432
|
+
}
|
|
446
433
|
|
|
447
|
-
|
|
448
|
-
|
|
434
|
+
/** 프로젝트 내 *.test.ts 파일을 스캔하여 AC 목록 반환 */
|
|
435
|
+
export function getAcList(): CddAcListResult {
|
|
436
|
+
const projectRoot = getProjectRoot();
|
|
437
|
+
const files = findTestFiles(projectRoot);
|
|
438
|
+
|
|
439
|
+
const acFiles: CddAcFile[] = [];
|
|
440
|
+
for (const absPath of files) {
|
|
441
|
+
const content = fs.readFileSync(absPath, "utf-8");
|
|
442
|
+
const entries = parseAcEntries(content);
|
|
443
|
+
if (entries.length > 0) {
|
|
444
|
+
acFiles.push({
|
|
445
|
+
path: path.relative(projectRoot, absPath),
|
|
446
|
+
entries,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
449
450
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
stats: { totalContracts, totalSpecs, statusDistribution },
|
|
453
|
-
documents,
|
|
454
|
-
};
|
|
451
|
+
const total = acFiles.reduce((sum, f) => sum + f.entries.length, 0);
|
|
452
|
+
return { files: acFiles, total };
|
|
455
453
|
}
|
|
456
454
|
|
|
457
|
-
/**
|
|
458
|
-
|
|
459
|
-
const
|
|
455
|
+
/** *.test.ts 파일을 재귀 탐색 (node_modules, dist 제외) */
|
|
456
|
+
function findTestFiles(dir: string): string[] {
|
|
457
|
+
const results: string[] = [];
|
|
458
|
+
const IGNORE = new Set(["node_modules", "dist", ".git", "contract"]);
|
|
460
459
|
|
|
461
|
-
|
|
462
|
-
|
|
460
|
+
function walk(currentDir: string): void {
|
|
461
|
+
let entries: fs.Dirent[];
|
|
462
|
+
try {
|
|
463
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
464
|
+
} catch {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
for (const entry of entries) {
|
|
468
|
+
if (IGNORE.has(entry.name)) continue;
|
|
469
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
470
|
+
if (entry.isDirectory()) {
|
|
471
|
+
walk(fullPath);
|
|
472
|
+
} else if (entry.isFile() && entry.name.endsWith(".test.ts")) {
|
|
473
|
+
results.push(fullPath);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
463
476
|
}
|
|
464
477
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
detached: true,
|
|
469
|
-
});
|
|
470
|
-
child.unref();
|
|
478
|
+
walk(dir);
|
|
479
|
+
results.sort();
|
|
480
|
+
return results;
|
|
471
481
|
}
|