sonamu 0.8.19 → 0.8.21
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/config.d.ts +1 -1
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +1 -1
- package/dist/api/decorators.js +2 -2
- package/dist/api/sonamu.d.ts +1 -1
- package/dist/api/sonamu.d.ts.map +1 -1
- package/dist/api/sonamu.js +19 -14
- package/dist/bin/cli.js +30 -1
- package/dist/database/base-model.js +2 -2
- package/dist/database/db.d.ts +1 -1
- package/dist/database/db.d.ts.map +1 -1
- package/dist/database/db.js +1 -1
- package/dist/stream/sse.d.ts +2 -0
- package/dist/stream/sse.d.ts.map +1 -1
- package/dist/stream/sse.js +29 -6
- package/dist/syncer/syncer.js +2 -2
- package/dist/testing/global-setup.d.ts.map +1 -1
- package/dist/testing/global-setup.js +4 -1
- package/dist/ui/api.d.ts.map +1 -1
- package/dist/ui/api.js +15 -12
- package/dist/ui/cdd-service.d.ts +4 -2
- package/dist/ui/cdd-service.d.ts.map +1 -1
- package/dist/ui/cdd-service.js +99 -1
- package/dist/ui/cdd-types.d.ts +25 -0
- package/dist/ui/cdd-types.d.ts.map +1 -1
- package/dist/ui/cdd-types.js +1 -1
- package/dist/ui-web/assets/index-B7gc0Ygb.css +1 -0
- package/dist/ui-web/assets/{index-D_19-Pi4.js → index-DP968oXY.js} +71 -71
- package/dist/ui-web/index.html +2 -2
- package/package.json +3 -3
- package/src/api/config.ts +1 -1
- package/src/api/decorators.ts +1 -1
- package/src/api/sonamu.ts +20 -15
- package/src/bin/cli.ts +34 -0
- package/src/database/base-model.ts +1 -1
- package/src/database/db.ts +1 -1
- package/src/shared/app.shared.ts.txt +10 -1
- package/src/shared/web.shared.ts.txt +1 -1
- package/src/skills/AGENTS.md +10 -0
- package/src/skills/sonamu/auth.md +40 -1
- package/src/skills/sonamu/cdd.md +42 -0
- package/src/skills/sonamu/model.md +1 -1
- package/src/skills/sonamu/scaffolding.md +6 -0
- package/src/skills/sonamu/workflow.md +1 -1
- package/src/stream/sse.ts +34 -5
- package/src/syncer/syncer.ts +1 -1
- package/src/testing/global-setup.ts +4 -0
- package/src/ui/api.ts +20 -6
- package/src/ui/cdd-service.ts +106 -0
- package/src/ui/cdd-types.ts +28 -0
- package/dist/ui-web/assets/index-D4XFBV-f.css +0 -1
package/dist/ui-web/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/sonamu-ui/setting.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>{{projectName}}: Sonamu UI</title>
|
|
8
|
-
<script type="module" crossorigin src="/sonamu-ui/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/sonamu-ui/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/sonamu-ui/assets/index-DP968oXY.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/sonamu-ui/assets/index-B7gc0Ygb.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonamu",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.21",
|
|
4
4
|
"description": "Sonamu — TypeScript Fullstack API Framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -124,9 +124,9 @@
|
|
|
124
124
|
"vite": "7.3.0",
|
|
125
125
|
"vitest": "^4.0.10",
|
|
126
126
|
"@sonamu-kit/hmr-hook": "^0.4.1",
|
|
127
|
-
"@sonamu-kit/hmr-runner": "^0.1.1",
|
|
128
127
|
"@sonamu-kit/tasks": "^0.2.0",
|
|
129
|
-
"@sonamu-kit/ts-loader": "^2.1.3"
|
|
128
|
+
"@sonamu-kit/ts-loader": "^2.1.3",
|
|
129
|
+
"@sonamu-kit/hmr-runner": "^0.1.1"
|
|
130
130
|
},
|
|
131
131
|
"devDependencies": {
|
|
132
132
|
"@biomejs/biome": "^2.3.13",
|
package/src/api/config.ts
CHANGED
|
@@ -83,7 +83,7 @@ export type SonamuConfig<TSinkId extends string = string, TFilterId extends stri
|
|
|
83
83
|
targets: string[]; // "web", "app" 등
|
|
84
84
|
};
|
|
85
85
|
|
|
86
|
-
database
|
|
86
|
+
database?: {
|
|
87
87
|
// 데이터베이스(pg는 pg 모듈, pgnative는 pg-native 모듈의 설치가 필요합니다.)
|
|
88
88
|
database?: "pg" | "pgnative";
|
|
89
89
|
// 기본 데이터베이스 이름
|
package/src/api/decorators.ts
CHANGED
|
@@ -111,7 +111,7 @@ function checkSingleDecorator(target: DecoratorTarget, propertyKey: string, deco
|
|
|
111
111
|
const method = target[propertyKey as keyof typeof target] as { __decoratorType?: symbol };
|
|
112
112
|
if (method?.__decoratorType && method?.__decoratorType !== decoratorType) {
|
|
113
113
|
throw new Error(
|
|
114
|
-
`@${String(decoratorType)} decorator can only be used once on ${target.constructor.name}.${propertyKey}. You can use only one of @api or @stream decorator on the same method.`,
|
|
114
|
+
`@${decoratorType.description ?? String(decoratorType)} decorator can only be used once on ${target.constructor.name}.${propertyKey}. You can use only one of @api or @stream decorator on the same method.`,
|
|
115
115
|
);
|
|
116
116
|
} else {
|
|
117
117
|
method.__decoratorType = decoratorType;
|
package/src/api/sonamu.ts
CHANGED
|
@@ -144,11 +144,7 @@ class SonamuClass {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
private _workflows: WorkflowManager | null = null;
|
|
147
|
-
get workflows(): WorkflowManager {
|
|
148
|
-
if (this._workflows === null) {
|
|
149
|
-
throw new Error("Sonamu has not been initialized");
|
|
150
|
-
}
|
|
151
|
-
|
|
147
|
+
get workflows(): WorkflowManager | null {
|
|
152
148
|
return this._workflows;
|
|
153
149
|
}
|
|
154
150
|
|
|
@@ -204,8 +200,10 @@ class SonamuClass {
|
|
|
204
200
|
const { loadConfig } = await import("./config");
|
|
205
201
|
this.config = await loadConfig(this.apiRootPath);
|
|
206
202
|
// sonamu.config.ts 기본값 설정
|
|
207
|
-
|
|
208
|
-
|
|
203
|
+
if (this.config.database) {
|
|
204
|
+
this.config.database.database = this.config.database.database ?? "pg";
|
|
205
|
+
this.config.database.defaultOptions.client = this.config.database.database ?? "pg";
|
|
206
|
+
}
|
|
209
207
|
|
|
210
208
|
// 로깅 설정
|
|
211
209
|
const { configureLogTape } = await import("../logger/configure");
|
|
@@ -216,11 +214,13 @@ class SonamuClass {
|
|
|
216
214
|
}
|
|
217
215
|
|
|
218
216
|
// DB 로드
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
217
|
+
if (this.config.database) {
|
|
218
|
+
const { DB } = await import("../database/db");
|
|
219
|
+
this.dbConfig = DB.generateDBConfig(this.config.database);
|
|
220
|
+
if (!doSilent) {
|
|
221
|
+
const chalk = (await import("chalk")).default;
|
|
222
|
+
console.log(chalk.green("DB Config Loaded!"));
|
|
223
|
+
}
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
// Entity 로드
|
|
@@ -1306,6 +1306,11 @@ class SonamuClass {
|
|
|
1306
1306
|
}
|
|
1307
1307
|
|
|
1308
1308
|
private async initializeWorkflows(options: SonamuTaskOptions | undefined) {
|
|
1309
|
+
// database 설정이 없으면 WorkflowManager를 초기화하지 않음
|
|
1310
|
+
if (!this.config.database) {
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1309
1314
|
const { WorkflowManager } = await import("../tasks/workflow-manager");
|
|
1310
1315
|
// NOTE: @sonamu-kit/tasks 안에선 knex config를 수정하기 때문에 connection이 아닌 config 째로 보냅니다.
|
|
1311
1316
|
this._workflows = new WorkflowManager(DB.getDBConfig("w"));
|
|
@@ -1321,7 +1326,7 @@ class SonamuClass {
|
|
|
1321
1326
|
};
|
|
1322
1327
|
|
|
1323
1328
|
if (enableWorker) {
|
|
1324
|
-
this.
|
|
1329
|
+
this._workflows?.setupWorker({
|
|
1325
1330
|
...defaultWorkerOptions,
|
|
1326
1331
|
...options.workerOptions,
|
|
1327
1332
|
});
|
|
@@ -1334,7 +1339,7 @@ class SonamuClass {
|
|
|
1334
1339
|
|
|
1335
1340
|
server.addHook("onClose", async () => {
|
|
1336
1341
|
await options.lifecycle?.onShutdown?.(server);
|
|
1337
|
-
await this.workflows
|
|
1342
|
+
await this.workflows?.destroy();
|
|
1338
1343
|
await this.destroy();
|
|
1339
1344
|
});
|
|
1340
1345
|
|
|
@@ -1358,7 +1363,7 @@ class SonamuClass {
|
|
|
1358
1363
|
server
|
|
1359
1364
|
.listen({ port, host })
|
|
1360
1365
|
.then(async () => {
|
|
1361
|
-
await this.workflows
|
|
1366
|
+
await this.workflows?.startWorker();
|
|
1362
1367
|
await options.lifecycle?.onStart?.(server);
|
|
1363
1368
|
})
|
|
1364
1369
|
.catch(async (err) => {
|
package/src/bin/cli.ts
CHANGED
|
@@ -1059,6 +1059,40 @@ async function skills_sync_to(
|
|
|
1059
1059
|
}
|
|
1060
1060
|
}
|
|
1061
1061
|
|
|
1062
|
+
// settings.local.json — project-local 모드에서만, 없을 때만 생성
|
|
1063
|
+
if (options.copyProjectTemplates) {
|
|
1064
|
+
const settingsLocalPath = path.join(claudeDir, "settings.local.json");
|
|
1065
|
+
if (!(await exists(settingsLocalPath))) {
|
|
1066
|
+
try {
|
|
1067
|
+
const settingsContent = {
|
|
1068
|
+
hooks: {
|
|
1069
|
+
PostToolUse: [
|
|
1070
|
+
{
|
|
1071
|
+
matcher: "Edit|Write|MultiEdit",
|
|
1072
|
+
hooks: [
|
|
1073
|
+
{
|
|
1074
|
+
type: "command",
|
|
1075
|
+
command: "pnpm biome check --changed 2>&1 | head -60",
|
|
1076
|
+
},
|
|
1077
|
+
],
|
|
1078
|
+
},
|
|
1079
|
+
],
|
|
1080
|
+
},
|
|
1081
|
+
};
|
|
1082
|
+
await writeFile(settingsLocalPath, `${JSON.stringify(settingsContent, null, 2)}\n`);
|
|
1083
|
+
console.log(chalk.green(`✓ .claude/settings.local.json created`));
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
console.error(
|
|
1086
|
+
chalk.red(
|
|
1087
|
+
`✗ Failed to create settings.local.json: ${error instanceof Error ? error.message : String(error)}`,
|
|
1088
|
+
),
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
} else {
|
|
1092
|
+
console.log(chalk.dim(`⏭ .claude/settings.local.json already exists (preserved)`));
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1062
1096
|
// CLAUDE.md 복사/업데이트
|
|
1063
1097
|
if (await exists(sourceClaudeMd)) {
|
|
1064
1098
|
try {
|
|
@@ -415,7 +415,7 @@ export class BaseModelClass<
|
|
|
415
415
|
const { default: SqlParser } = await import("node-sql-parser");
|
|
416
416
|
const parser = new SqlParser.Parser();
|
|
417
417
|
const parsedQuery = parser.astify(countPuri.toQuery(), {
|
|
418
|
-
database: Sonamu.config.database
|
|
418
|
+
database: Sonamu.config.database?.database,
|
|
419
419
|
});
|
|
420
420
|
|
|
421
421
|
const leftJoinTables = getJoinTables(parsedQuery, ["LEFT JOIN"]);
|
package/src/database/db.ts
CHANGED
|
@@ -157,7 +157,7 @@ export class DBClass {
|
|
|
157
157
|
this.workerDBs.clear();
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
public generateDBConfig(config: SonamuConfig["database"]): SonamuDBConfig {
|
|
160
|
+
public generateDBConfig(config: NonNullable<SonamuConfig["database"]>): SonamuDBConfig {
|
|
161
161
|
const defaultKnexConfig: Partial<DatabaseConfig> = assign(
|
|
162
162
|
{
|
|
163
163
|
client: "postgresql",
|
|
@@ -65,8 +65,17 @@ axios.defaults.transformResponse = [
|
|
|
65
65
|
},
|
|
66
66
|
];
|
|
67
67
|
|
|
68
|
+
// Axios + React Native FormData 호환성: Content-Type을 multipart/form-data로 명시 설정하고
|
|
69
|
+
// transformRequest를 우회하여 Android에서 application/x-www-form-urlencoded로 잘못 설정되는 문제 방지
|
|
70
|
+
// ref: https://github.com/axios/axios/issues/4800
|
|
68
71
|
axios.interceptors.request.use((config) => {
|
|
69
72
|
config.headers["Accept-Language"] = getCurrentLocale();
|
|
73
|
+
if (config.data instanceof FormData) {
|
|
74
|
+
if (config.headers instanceof axios.AxiosHeaders) {
|
|
75
|
+
config.headers.setContentType("multipart/form-data");
|
|
76
|
+
}
|
|
77
|
+
config.transformRequest = [(data: unknown) => data];
|
|
78
|
+
}
|
|
70
79
|
return config;
|
|
71
80
|
});
|
|
72
81
|
|
|
@@ -77,7 +86,7 @@ export async function fetch(options: AxiosRequestConfig) {
|
|
|
77
86
|
});
|
|
78
87
|
return res.data;
|
|
79
88
|
} catch (e: unknown) {
|
|
80
|
-
if (axios.isAxiosError(e) && e.response
|
|
89
|
+
if (axios.isAxiosError(e) && e.response?.data) {
|
|
81
90
|
const d = e.response.data as {
|
|
82
91
|
message: string;
|
|
83
92
|
issues: core.$ZodIssue[];
|
|
@@ -61,7 +61,7 @@ export async function fetch(options: AxiosRequestConfig) {
|
|
|
61
61
|
});
|
|
62
62
|
return res.data;
|
|
63
63
|
} catch (e: unknown) {
|
|
64
|
-
if (axios.isAxiosError(e) && e.response
|
|
64
|
+
if (axios.isAxiosError(e) && e.response?.data) {
|
|
65
65
|
const d = e.response.data as {
|
|
66
66
|
message: string;
|
|
67
67
|
issues: core.$ZodIssue[];
|
package/src/skills/AGENTS.md
CHANGED
|
@@ -46,6 +46,16 @@ See `.claude/skills/sonamu/SKILL.md` for the full skill list.
|
|
|
46
46
|
- Resolve type errors through correct type annotations, generic constraints, type narrowing, or interface extension.
|
|
47
47
|
- Do not use `as any` to work around "excessively deep" or similar TypeScript inference limits — find the correct access pattern instead (e.g. use `getPuri("r")` directly rather than casting the result).
|
|
48
48
|
- Chaining methods after `as any` bypasses all TypeScript signature checks and leads directly to runtime bugs.
|
|
49
|
+
- Non-null assertion (`!`) is prohibited. Use optional chaining (`?.`) or type guard filters instead.
|
|
50
|
+
|
|
51
|
+
## Code quality gate
|
|
52
|
+
|
|
53
|
+
After editing any `.ts` or `.tsx` file, always run both checks before considering the task done:
|
|
54
|
+
|
|
55
|
+
1. `npx tsc --noEmit --skipLibCheck` — type errors
|
|
56
|
+
2. `pnpm biome check <file>` — lint and format
|
|
57
|
+
|
|
58
|
+
Do not skip biome check even when tsc passes. Biome catches `noNonNullAssertion`, import order, and formatting issues that tsc does not.
|
|
49
59
|
|
|
50
60
|
## Skill read triggers
|
|
51
61
|
|
|
@@ -14,8 +14,44 @@ description: Sonamu better-auth 인증 시스템. 엔티티 자동 생성, Guard
|
|
|
14
14
|
- 생성 로직: `modules/sonamu/src/auth/auth-generator.ts`
|
|
15
15
|
- 엔티티 정의: `modules/sonamu/src/auth/better-auth-entities.ts`
|
|
16
16
|
|
|
17
|
+
**IMPORTANT: generate 실행 전에 반드시 사용자에게 플러그인 사용 여부를 확인해야 합니다.**
|
|
18
|
+
|
|
19
|
+
플러그인 선택은 generate 시점에 함께 이루어지며, 나중에 추가도 가능하지만 처음부터 명시하는 것이 좋습니다.
|
|
20
|
+
지원 플러그인 목록과 용도는 `auth-plugins.md`를 참고하세요.
|
|
21
|
+
|
|
22
|
+
### 플러그인 확인 흐름
|
|
23
|
+
|
|
24
|
+
**[Step 1] generate 전 확인 (필수)**
|
|
25
|
+
|
|
26
|
+
> "어떤 인증 방식을 사용할 계획인가요? 기본 이메일/소셜 로그인 외에 추가 플러그인이 필요한지 확인해 주세요.
|
|
27
|
+
> 지원 플러그인: `admin`, `organization`, `2fa`, `username`, `phone-number`, `api-key`, `jwt`, `passkey`, `sso`, `anonymous`"
|
|
28
|
+
|
|
29
|
+
**[Step 1-A] 사용자가 "나중에 하겠다"고 응답한 경우:**
|
|
30
|
+
|
|
31
|
+
아래 안내를 제공한 후 플러그인 없이 generate를 진행합니다:
|
|
32
|
+
|
|
33
|
+
> "알겠습니다. 플러그인은 초기 마이그레이션 실행 전까지 추가하는 것이 가장 좋습니다.
|
|
34
|
+
> 마이그레이션 전에 다시 확인드리겠습니다."
|
|
35
|
+
|
|
36
|
+
그리고 **`plugins_deferred: true`** 상태를 기억합니다.
|
|
37
|
+
|
|
38
|
+
**[Step 2] migrate run 직전 재확인 (CRITICAL — `plugins_deferred: true`인 경우 반드시 실행)**
|
|
39
|
+
|
|
40
|
+
마이그레이션을 실행하기 전, 반드시 다시 확인합니다:
|
|
41
|
+
|
|
42
|
+
> "마이그레이션 실행 전입니다. 지금이 플러그인을 추가하기 가장 좋은 시점입니다.
|
|
43
|
+
> 추가할 플러그인이 있으면 알려주세요. 없으면 그대로 진행합니다.
|
|
44
|
+
> 지원 플러그인: `admin`, `organization`, `2fa`, `username`, `phone-number`, `api-key`, `jwt`, `passkey`, `sso`, `anonymous`"
|
|
45
|
+
|
|
46
|
+
- 플러그인 추가 시: `pnpm sonamu auth generate --plugins <목록>` 실행 후 migrate 진행
|
|
47
|
+
- 없으면: migrate 그대로 진행
|
|
48
|
+
|
|
17
49
|
```bash
|
|
50
|
+
# 플러그인 없이 기본 엔티티만
|
|
18
51
|
pnpm sonamu auth generate
|
|
52
|
+
|
|
53
|
+
# 플러그인 포함
|
|
54
|
+
pnpm sonamu auth generate --plugins admin,2fa,username
|
|
19
55
|
```
|
|
20
56
|
|
|
21
57
|
생성되는 4개 엔티티 (`betterAuthV1` 배열):
|
|
@@ -242,7 +278,10 @@ Enum 추가:
|
|
|
242
278
|
## 체크리스트
|
|
243
279
|
|
|
244
280
|
설정 후 확인 사항:
|
|
245
|
-
- [ ]
|
|
281
|
+
- [ ] **[generate 전] 사용자에게 플러그인 필요 여부 확인**
|
|
282
|
+
- "나중에" 응답 시 → `plugins_deferred: true` 기억, 최적 시점 안내
|
|
283
|
+
- [ ] `pnpm sonamu auth generate [--plugins ...]` 실행
|
|
284
|
+
- [ ] **[migrate 전] `plugins_deferred: true`인 경우 플러그인 재확인** (CRITICAL)
|
|
246
285
|
- [ ] 마이그레이션 생성 및 적용
|
|
247
286
|
- [ ] `sonamu.config.ts`에 `server.auth` 설정
|
|
248
287
|
- [ ] `guardHandler` 구현
|
package/src/skills/sonamu/cdd.md
CHANGED
|
@@ -26,6 +26,8 @@ contract/
|
|
|
26
26
|
|- schemas/
|
|
27
27
|
| |- default-contract.schema.json # contract schema definition
|
|
28
28
|
| \- default-spec.schema.json # spec schema definition
|
|
29
|
+
|- rules/
|
|
30
|
+
| \- *.rules.json # reusable implementation conventions
|
|
29
31
|
|- main.contract.json # project root contract
|
|
30
32
|
|- {domain}/
|
|
31
33
|
| |- main.contract.json # domain representative contract
|
|
@@ -295,6 +297,43 @@ git log --follow -- contract/auth/login.spec.json # track renames
|
|
|
295
297
|
|
|
296
298
|
---
|
|
297
299
|
|
|
300
|
+
## Rule Files
|
|
301
|
+
|
|
302
|
+
`contract/rules/*.rules.json` 파일로 프로젝트별 반복 개발 규칙을 관리한다.
|
|
303
|
+
|
|
304
|
+
### 파일 포맷
|
|
305
|
+
|
|
306
|
+
```json
|
|
307
|
+
{
|
|
308
|
+
"description": "Rule-set의 범위와 목적 설명",
|
|
309
|
+
"rules": [
|
|
310
|
+
{
|
|
311
|
+
"id": "readonly-money-display-uses-numf",
|
|
312
|
+
"when": "금액을 읽기 전용 텍스트나 테이블 셀로 표시할 때",
|
|
313
|
+
"instruction": "numF()를 적용합니다.",
|
|
314
|
+
"examples": ["numF(row.totalAmount)", "numF(summary.budget)"]
|
|
315
|
+
}
|
|
316
|
+
]
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
| 필드 | 설명 |
|
|
321
|
+
|---|---|
|
|
322
|
+
| `description` | rule set의 범위와 의도 |
|
|
323
|
+
| `rules[].id` | 안정적인 식별자 (프롬프트 참조, diff, 리뷰 노트에 사용) |
|
|
324
|
+
| `rules[].when` | 규칙의 적용 조건 |
|
|
325
|
+
| `rules[].instruction` | worker가 따라야 할 구체적 지침 |
|
|
326
|
+
| `rules[].examples` | 선택적 코드/사용 예시 |
|
|
327
|
+
|
|
328
|
+
### 운영 규칙
|
|
329
|
+
|
|
330
|
+
- Orchestrator는 phase 라우팅 전에 `contract/rules/`를 확인하고, 현재 작업에 해당하는 `*.rules.json` 파일을 읽는다.
|
|
331
|
+
- Orchestrator는 worker를 스폰할 때 해당 파일 경로를 `rules_paths`로 전달한다.
|
|
332
|
+
- 모든 worker는 변경 작업 시작 전에 `rules_paths`의 각 파일을 읽고, 담당 범위에 맞는 규칙을 적용한다.
|
|
333
|
+
- 작업에 매칭되는 rule 파일이 없거나, 참조된 파일이 누락/형식 오류인 경우 blind하게 진행하지 않고 중단 후 보고한다.
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
298
337
|
## Development Process
|
|
299
338
|
|
|
300
339
|
All processes follow Waterfall. Each stage starts only after the previous stage is complete. If you need to change a previous stage artifact, go back, update the document first, then re-run downstream stages.
|
|
@@ -346,6 +385,7 @@ Impact analysis -> Contract/Spec review -> Spec update/fix -> Code update -> Tes
|
|
|
346
385
|
**Step 1: Impact analysis**
|
|
347
386
|
- Find all Spec files whose `sources` include the target files.
|
|
348
387
|
- Check `contracts` and `dependsOnSpecs` in those Specs to identify chained impact scope.
|
|
388
|
+
- 코드/테스트 변경 시: 변경된 파일을 `sources`로 참조하는 모든 Spec을 찾고, 그 Spec의 `contracts`와 `dependsOnSpecs`까지 정합성을 확인한다.
|
|
349
389
|
|
|
350
390
|
**Step 2: Contract/Spec review**
|
|
351
391
|
- Read related Specs to understand current module structure and interfaces.
|
|
@@ -374,6 +414,7 @@ Impact analysis -> Contract/Spec review -> Spec update/fix -> Code update -> Tes
|
|
|
374
414
|
- Validate that updated code follows confirmed Spec exactly.
|
|
375
415
|
- Verify all `acceptanceCriteria` items are satisfied.
|
|
376
416
|
- **If mismatch exists, fix code.**
|
|
417
|
+
- 관련 Spec(`dependsOnSpecs`에서 참조하는 Spec 포함), Contract 정합성도 함께 확인한다.
|
|
377
418
|
- After all validations pass, set `status` to `"done"` and update `lastModified` to today.
|
|
378
419
|
|
|
379
420
|
### 3. Bug fixes
|
|
@@ -487,6 +528,7 @@ The `cdd` CLI tool automates CDD workflow tasks. Run via `pnpm cdd <command>`.
|
|
|
487
528
|
| `cdd spec create <n>` | Create a Spec template. Requires `--domain <n>` or `--contract <path>` |
|
|
488
529
|
| `cdd contract create [name]` | Contract 템플릿 생성. `name` 미지정 시 `main` |
|
|
489
530
|
| `cdd spec set-status <spec> <status>` | Change Spec status |
|
|
531
|
+
| `cdd rules validate` | `contract/rules/*.rules.json` 포맷 검증 |
|
|
490
532
|
| `cdd spec list` | List Specs. Filters: `--status`, `--domain`, `--contract` |
|
|
491
533
|
| `cdd spec get <spec>` | Show full Spec or a specific field (`--field`) |
|
|
492
534
|
| `cdd spec set <spec>` | Update a Spec field (`--field`, `--value`, `--json`) |
|
|
@@ -395,7 +395,7 @@ async enroll(courseId: number, userId: number): Promise<Enrollment> {
|
|
|
395
395
|
const { total } = await this.findMany({ course_id: courseId });
|
|
396
396
|
|
|
397
397
|
if (total >= course.max_students) {
|
|
398
|
-
throw new Error("정원이
|
|
398
|
+
throw new Error("정원이 가득 찼습니다");
|
|
399
399
|
}
|
|
400
400
|
|
|
401
401
|
// 3단계: 실행
|
|
@@ -243,6 +243,12 @@ export const {Entity}SaveParams = {Entity}BaseSchema.partial({
|
|
|
243
243
|
|
|
244
244
|
### exhaustive() 타입 에러
|
|
245
245
|
|
|
246
|
+
`exhaustive`는 sonamu에서 제공하는 유틸리티 함수입니다.
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
import { exhaustive } from "sonamu";
|
|
250
|
+
```
|
|
251
|
+
|
|
246
252
|
Scaffolding 템플릿은 `OrderBy` enum의 **첫 번째 값만** 자동 처리합니다.
|
|
247
253
|
|
|
248
254
|
```typescript
|
|
@@ -246,7 +246,7 @@ description: Sonamu 전체 개발 워크플로우. 프로젝트 생성부터 Fro
|
|
|
246
246
|
|
|
247
247
|
42. 사용자에게 fixture 생성할지 확인
|
|
248
248
|
43. 모든 엔티티의 prop에 `cone.note`가 존재하는지 체크
|
|
249
|
-
- cone.note가 비어있는 prop이 있으면 사용자에게 보고하고 `pnpm sonamu cone
|
|
249
|
+
- cone.note가 비어있는 prop이 있으면 사용자에게 보고하고 `pnpm sonamu cone gen --use-llm`으로 cone을 재생성할지 확인
|
|
250
250
|
- cone.note가 있어야 LLM이 맥락에 맞는 fixture 데이터를 생성할 수 있다
|
|
251
251
|
44. 생성할 데이터의 최소 row 수 확인 (최소 10 ~ 최대 100)
|
|
252
252
|
45. **better-auth 엔티티 먼저 생성** (의존성 순서 필수):
|
package/src/stream/sse.ts
CHANGED
|
@@ -12,28 +12,54 @@ export function createSSEFactory<T extends z.ZodObject>(
|
|
|
12
12
|
|
|
13
13
|
export function createMockSSEFactory<T extends z.ZodObject>(_events: T): SSEConnection<T> {
|
|
14
14
|
return {
|
|
15
|
+
get closed() {
|
|
16
|
+
return false;
|
|
17
|
+
},
|
|
18
|
+
onClose: (_callback) => {},
|
|
15
19
|
publish: (_event, _data) => {},
|
|
16
20
|
end: () => Promise.resolve(),
|
|
17
21
|
};
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
export interface SSEConnection<T extends z.ZodObject> {
|
|
25
|
+
get closed(): boolean;
|
|
26
|
+
onClose(callback: () => void): void;
|
|
21
27
|
publish<K extends keyof z.infer<T>>(event: K, data: z.infer<T>[K]): void;
|
|
22
28
|
end(): Promise<void>;
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
class SSEConnectionImpl<T extends z.ZodObject> implements SSEConnection<T> {
|
|
26
32
|
private _closed = false;
|
|
33
|
+
private _closeCallbacks: Array<() => void> = [];
|
|
34
|
+
|
|
35
|
+
private readonly markClosed = () => {
|
|
36
|
+
this._closed = true;
|
|
37
|
+
this.fireCloseCallbacks();
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
get closed(): boolean {
|
|
41
|
+
return this._closed;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
onClose(callback: () => void): void {
|
|
45
|
+
this._closeCallbacks.push(callback);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 콜백을 한 번만 실행하고 배열을 비워 중복 호출을 방지
|
|
49
|
+
private fireCloseCallbacks(): void {
|
|
50
|
+
const callbacks = this._closeCallbacks;
|
|
51
|
+
this._closeCallbacks = [];
|
|
52
|
+
for (const cb of callbacks) {
|
|
53
|
+
cb();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
27
56
|
|
|
28
57
|
constructor(
|
|
29
58
|
private readonly socket: FastifyRequest["socket"],
|
|
30
59
|
private readonly reply: FastifyReply,
|
|
31
60
|
) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
};
|
|
35
|
-
this.socket.on("close", markClosed);
|
|
36
|
-
this.socket.on("error", markClosed);
|
|
61
|
+
this.socket.on("close", this.markClosed);
|
|
62
|
+
this.socket.on("error", this.markClosed);
|
|
37
63
|
}
|
|
38
64
|
|
|
39
65
|
publish<K extends keyof z.infer<T>>(event: K, data: z.infer<T>[K]): void {
|
|
@@ -53,6 +79,9 @@ class SSEConnectionImpl<T extends z.ZodObject> implements SSEConnection<T> {
|
|
|
53
79
|
}
|
|
54
80
|
|
|
55
81
|
this._closed = true;
|
|
82
|
+
this.socket.off("close", this.markClosed);
|
|
83
|
+
this.socket.off("error", this.markClosed);
|
|
84
|
+
this.fireCloseCallbacks();
|
|
56
85
|
|
|
57
86
|
this.reply.sse({
|
|
58
87
|
event: "end",
|
package/src/syncer/syncer.ts
CHANGED
|
@@ -283,7 +283,7 @@ export class Syncer {
|
|
|
283
283
|
|
|
284
284
|
async autoloadWorkflows() {
|
|
285
285
|
this.workflows = await loadWorkflows();
|
|
286
|
-
await Sonamu.workflows
|
|
286
|
+
await Sonamu.workflows?.synchronize(this.workflows);
|
|
287
287
|
}
|
|
288
288
|
|
|
289
289
|
async autoloadSSRRoutes(): Promise<void> {
|
package/src/ui/api.ts
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
editContent,
|
|
42
42
|
editSchema,
|
|
43
43
|
getCddTree,
|
|
44
|
+
getDashboard,
|
|
44
45
|
listSchemas,
|
|
45
46
|
openSourceFile,
|
|
46
47
|
readContent,
|
|
@@ -1086,15 +1087,12 @@ export async function sonamuUIApiPlugin(fastify: FastifyInstance) {
|
|
|
1086
1087
|
|
|
1087
1088
|
// Tasks API
|
|
1088
1089
|
server.get("/api/tasks/status", async () => {
|
|
1089
|
-
|
|
1090
|
-
Sonamu.workflows;
|
|
1091
|
-
return { active: true };
|
|
1092
|
-
} catch {
|
|
1093
|
-
return { active: false };
|
|
1094
|
-
}
|
|
1090
|
+
return { active: Sonamu.workflows !== null };
|
|
1095
1091
|
});
|
|
1096
1092
|
|
|
1097
1093
|
server.get("/api/tasks/workflowDefinitions", async () => {
|
|
1094
|
+
if (!Sonamu.workflows)
|
|
1095
|
+
throw new Error("Workflows not initialized (database not configured)");
|
|
1098
1096
|
const definitions = Sonamu.workflows.workflowDefinitions;
|
|
1099
1097
|
return { definitions };
|
|
1100
1098
|
});
|
|
@@ -1111,6 +1109,8 @@ export async function sonamuUIApiPlugin(fastify: FastifyInstance) {
|
|
|
1111
1109
|
createdBefore?: string;
|
|
1112
1110
|
};
|
|
1113
1111
|
}>("/api/tasks/workflowRuns", async (request) => {
|
|
1112
|
+
if (!Sonamu.workflows)
|
|
1113
|
+
throw new Error("Workflows not initialized (database not configured)");
|
|
1114
1114
|
const backend = Sonamu.workflows.backend;
|
|
1115
1115
|
const { limit, after, before, order, status, workflowName, createdAfter, createdBefore } =
|
|
1116
1116
|
request.query;
|
|
@@ -1129,6 +1129,8 @@ export async function sonamuUIApiPlugin(fastify: FastifyInstance) {
|
|
|
1129
1129
|
server.get<{
|
|
1130
1130
|
Params: { id: string };
|
|
1131
1131
|
}>("/api/tasks/workflowRuns/:id", async (request) => {
|
|
1132
|
+
if (!Sonamu.workflows)
|
|
1133
|
+
throw new Error("Workflows not initialized (database not configured)");
|
|
1132
1134
|
const backend = Sonamu.workflows.backend;
|
|
1133
1135
|
const workflowRun = await backend.getWorkflowRun({
|
|
1134
1136
|
workflowRunId: request.params.id,
|
|
@@ -1142,6 +1144,8 @@ export async function sonamuUIApiPlugin(fastify: FastifyInstance) {
|
|
|
1142
1144
|
server.post<{
|
|
1143
1145
|
Params: { id: string };
|
|
1144
1146
|
}>("/api/tasks/workflowRuns/:id/cancel", async (request) => {
|
|
1147
|
+
if (!Sonamu.workflows)
|
|
1148
|
+
throw new Error("Workflows not initialized (database not configured)");
|
|
1145
1149
|
const backend = Sonamu.workflows.backend;
|
|
1146
1150
|
return backend.cancelWorkflowRun({
|
|
1147
1151
|
workflowRunId: request.params.id,
|
|
@@ -1151,6 +1155,8 @@ export async function sonamuUIApiPlugin(fastify: FastifyInstance) {
|
|
|
1151
1155
|
server.post<{
|
|
1152
1156
|
Params: { id: string };
|
|
1153
1157
|
}>("/api/tasks/workflowRuns/:id/pause", async (request) => {
|
|
1158
|
+
if (!Sonamu.workflows)
|
|
1159
|
+
throw new Error("Workflows not initialized (database not configured)");
|
|
1154
1160
|
const backend = Sonamu.workflows.backend;
|
|
1155
1161
|
return backend.pauseWorkflowRun({
|
|
1156
1162
|
workflowRunId: request.params.id,
|
|
@@ -1160,6 +1166,8 @@ export async function sonamuUIApiPlugin(fastify: FastifyInstance) {
|
|
|
1160
1166
|
server.post<{
|
|
1161
1167
|
Params: { id: string };
|
|
1162
1168
|
}>("/api/tasks/workflowRuns/:id/resume", async (request) => {
|
|
1169
|
+
if (!Sonamu.workflows)
|
|
1170
|
+
throw new Error("Workflows not initialized (database not configured)");
|
|
1163
1171
|
const backend = Sonamu.workflows.backend;
|
|
1164
1172
|
return backend.resumeWorkflowRun({
|
|
1165
1173
|
workflowRunId: request.params.id,
|
|
@@ -1174,6 +1182,8 @@ export async function sonamuUIApiPlugin(fastify: FastifyInstance) {
|
|
|
1174
1182
|
before?: string;
|
|
1175
1183
|
};
|
|
1176
1184
|
}>("/api/tasks/workflowRuns/:id/steps", async (request) => {
|
|
1185
|
+
if (!Sonamu.workflows)
|
|
1186
|
+
throw new Error("Workflows not initialized (database not configured)");
|
|
1177
1187
|
const backend = Sonamu.workflows.backend;
|
|
1178
1188
|
const { limit, after, before } = request.query;
|
|
1179
1189
|
return backend.listStepAttempts({
|
|
@@ -1403,6 +1413,10 @@ export async function sonamuUIApiPlugin(fastify: FastifyInstance) {
|
|
|
1403
1413
|
});
|
|
1404
1414
|
|
|
1405
1415
|
// CDD API
|
|
1416
|
+
server.get("/api/cdd/dashboard", async () => {
|
|
1417
|
+
return getDashboard();
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1406
1420
|
server.get("/api/cdd/tree", async () => {
|
|
1407
1421
|
return getCddTree();
|
|
1408
1422
|
});
|