@team-semicolon/semo-cli 4.13.0 → 4.15.0
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/commands/context.js +23 -3
- package/dist/commands/harness.d.ts +8 -0
- package/dist/commands/harness.js +412 -0
- package/dist/commands/incubator.d.ts +10 -0
- package/dist/commands/incubator.js +517 -0
- package/dist/commands/service.js +144 -3
- package/dist/commands/sessions.js +156 -0
- package/dist/commands/skill-sync.d.ts +2 -1
- package/dist/commands/skill-sync.js +88 -23
- package/dist/commands/skill-sync.test.js +78 -45
- package/dist/database.d.ts +2 -1
- package/dist/database.js +109 -27
- package/dist/global-cache.js +26 -14
- package/dist/index.js +576 -522
- package/dist/kb.d.ts +4 -4
- package/dist/kb.js +203 -103
- package/dist/semo-workspace.js +51 -0
- package/dist/service-migrate.d.ts +47 -2
- package/dist/service-migrate.js +188 -13
- package/dist/templates/harness/commit-msg +1 -0
- package/dist/templates/harness/commitlint.config.js +11 -0
- package/dist/templates/harness/eslint.config.mjs +17 -0
- package/dist/templates/harness/pr-quality-gate.yml +19 -0
- package/dist/templates/harness/pre-commit +1 -0
- package/dist/templates/harness/pre-push +1 -0
- package/dist/templates/harness/prettierignore +5 -0
- package/dist/templates/harness/prettierrc.json +7 -0
- package/package.json +8 -4
package/dist/semo-workspace.js
CHANGED
|
@@ -322,6 +322,57 @@ function generateThinRouter(kbFirstBlock) {
|
|
|
322
322
|
- 사용자가 "다른 탭에서 뭐 돌아가고 있어?" 같은 질문을 하면 cmux 명령어로 확인
|
|
323
323
|
|
|
324
324
|
${kbFirstBlock}
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## 3자 동기화 규칙 (NON-NEGOTIABLE)
|
|
329
|
+
|
|
330
|
+
> 모든 SEMO 프로젝트의 변경은 3자 동기화 관점에서 평가되어야 한다.
|
|
331
|
+
> 3자 = **소스코드** ↔ **KB 포함 DB** ↔ **OpenClaw 봇 워크스페이스 로컬 파일**
|
|
332
|
+
|
|
333
|
+
| 축 | 위치 | 예시 |
|
|
334
|
+
|----|------|------|
|
|
335
|
+
| **소스코드** | 각 프로젝트 레포 | API 라우트, 타입, 마이그레이션 |
|
|
336
|
+
| **KB 포함 DB** | semo-kb (PostgreSQL + 벡터 임베딩) | 프로세스, 봇 역할, 온톨로지 |
|
|
337
|
+
| **봇 로컬 파일** | \`~/.openclaw-{botId}/workspace/\` + \`~/.claude/semo/\` | SOUL.md, skills/, memory/ |
|
|
338
|
+
|
|
339
|
+
**변경 시 체크리스트**:
|
|
340
|
+
1. **소스코드 변경** → KB와 봇 파일에 반영할 내용이 있는가?
|
|
341
|
+
2. **KB 변경** → 소스코드나 봇 파일과 불일치가 생기지 않는가?
|
|
342
|
+
3. **봇 파일 변경** → KB에 기록할 사항이 있는가? 소스코드와 정합성이 맞는가?
|
|
343
|
+
|
|
344
|
+
**추가 규칙**:
|
|
345
|
+
- SoT 위치: DB → DB에서 읽기 (하드코딩 금지). KB → \`semo kb get/search\` 조회.
|
|
346
|
+
- KB 쓰기: 반드시 \`semo kb upsert\` CLI 사용 (임베딩 + 스키마 검증 포함). raw SQL INSERT 금지.
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## KB CLI 참조 가이드
|
|
351
|
+
|
|
352
|
+
| 정보 | 조회 명령 |
|
|
353
|
+
|------|-----------|
|
|
354
|
+
| 봇 프로필 | \`semo kb get {botId} identity\` |
|
|
355
|
+
| 봇 역할 | \`semo kb get {botId} role\` |
|
|
356
|
+
| SEMO 워크스페이스 규격 | \`semo kb get semo spec workspace-v2\` |
|
|
357
|
+
| 환경변수 | \`semo kb get semo infra env-config\` |
|
|
358
|
+
| 코딩 컨벤션 | \`semo kb get semo process coding-convention\` |
|
|
359
|
+
| 인시던트 기록 | \`semo kb get {service} incident {slug}\` |
|
|
360
|
+
| 의사결정 기록 | \`semo kb get semicolon decision {slug}\` |
|
|
361
|
+
| 경로 모를 때 | \`semo kb search "검색어"\` 또는 \`semo kb ontology --action routing-table\` |
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## 공통 Enforcement Hooks
|
|
366
|
+
|
|
367
|
+
아래 훅은 \`~/.openclaw-shared/hooks/\`에 위치하며, 봇 + 로컬 세션 양쪽에서 사용:
|
|
368
|
+
|
|
369
|
+
| 훅 | 트리거 | 역할 |
|
|
370
|
+
|----|--------|------|
|
|
371
|
+
| \`context-router.sh\` | UserPromptSubmit | 메시지 키워드 → KB/GFP/INFRA 컨텍스트 힌트 주입 |
|
|
372
|
+
| \`decision-reminder.sh\` | Stop | 의사결정 패턴 감지 → KB 기록 안 했으면 차단 |
|
|
373
|
+
| \`commitment-guard.sh\` | Stop | 한국어 약속 패턴 → commitment 미등록 시 차단 |
|
|
374
|
+
| \`kb-first-guard.sh\` | Stop | KB 필요 질문에 KB 조회 없이 답변 시 차단 |
|
|
375
|
+
| \`response-length-guard.sh\` | Stop | 봇 응답 20줄 초과 시 차단 |
|
|
325
376
|
`;
|
|
326
377
|
// 기존 파일 백업 (마이그레이션)
|
|
327
378
|
if (fs.existsSync(globalClaudeMd)) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Service Migration — 기존 운영 서비스를
|
|
2
|
+
* Service Migration — 기존 운영 서비스를 services 테이블에 이식
|
|
3
3
|
*
|
|
4
|
-
* KB 온톨로지에 service 타입으로 등록된 도메인 중
|
|
4
|
+
* KB 온톨로지에 service 타입으로 등록된 도메인 중 services에 미등록된 것을
|
|
5
5
|
* 자동으로 이식. KB 엔트리 전수 조사(audit) 후 매핑/비매핑 분류.
|
|
6
6
|
*/
|
|
7
7
|
import { Pool } from "pg";
|
|
@@ -55,6 +55,10 @@ export interface MigrationResult {
|
|
|
55
55
|
audit: AuditResult;
|
|
56
56
|
error?: string;
|
|
57
57
|
}
|
|
58
|
+
export declare const STATUS_MAP: Record<string, {
|
|
59
|
+
lifecycle: string;
|
|
60
|
+
status: string;
|
|
61
|
+
}>;
|
|
58
62
|
export declare function getUnregisteredServices(pool: Pool): Promise<Array<{
|
|
59
63
|
domain: string;
|
|
60
64
|
description: string | null;
|
|
@@ -67,3 +71,44 @@ export declare function insertServiceProject(pool: Pool, row: MigrationRow): Pro
|
|
|
67
71
|
export declare function writePmSummaryToKB(pool: Pool, domain: string, projectId: string, row: MigrationRow, kbEntryCount: number): Promise<void>;
|
|
68
72
|
export declare function printAuditReport(results: MigrationResult[]): void;
|
|
69
73
|
export declare function printDryRunReport(results: MigrationResult[]): void;
|
|
74
|
+
export interface ServiceProjectRow {
|
|
75
|
+
service_id: string;
|
|
76
|
+
project_name: string;
|
|
77
|
+
service_domain: string | null;
|
|
78
|
+
owner_name: string;
|
|
79
|
+
owner_contact: string | null;
|
|
80
|
+
current_phase: number;
|
|
81
|
+
infra_phase: number | null;
|
|
82
|
+
status: string;
|
|
83
|
+
lifecycle: string;
|
|
84
|
+
launched_at: string | null;
|
|
85
|
+
metadata: Record<string, unknown>;
|
|
86
|
+
created_at: string;
|
|
87
|
+
updated_at: string;
|
|
88
|
+
}
|
|
89
|
+
export declare function getServiceProjectByDomain(pool: Pool, domain: string): Promise<ServiceProjectRow | null>;
|
|
90
|
+
export interface ServiceProjectUpdate {
|
|
91
|
+
status?: string;
|
|
92
|
+
lifecycle?: string;
|
|
93
|
+
current_phase?: number;
|
|
94
|
+
project_name?: string;
|
|
95
|
+
owner_name?: string;
|
|
96
|
+
metadata?: Record<string, unknown>;
|
|
97
|
+
}
|
|
98
|
+
export declare function updateServiceProject(pool: Pool, domain: string, updates: ServiceProjectUpdate): Promise<ServiceProjectRow | null>;
|
|
99
|
+
export interface DiagnoseMismatch {
|
|
100
|
+
field: string;
|
|
101
|
+
kbValue: string;
|
|
102
|
+
spValue: string;
|
|
103
|
+
expected: string;
|
|
104
|
+
}
|
|
105
|
+
export interface DiagnoseResult {
|
|
106
|
+
domain: string;
|
|
107
|
+
verdict: "healthy" | "mismatch" | "kb-only" | "sp-only" | "missing";
|
|
108
|
+
kbEntryCount: number;
|
|
109
|
+
kbKeys: string[];
|
|
110
|
+
serviceProject: ServiceProjectRow | null;
|
|
111
|
+
mismatches: DiagnoseMismatch[];
|
|
112
|
+
missingRequired: string[];
|
|
113
|
+
}
|
|
114
|
+
export declare function diagnoseServiceStatus(pool: Pool, domain: string): Promise<DiagnoseResult>;
|
package/dist/service-migrate.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Service Migration — 기존 운영 서비스를
|
|
3
|
+
* Service Migration — 기존 운영 서비스를 services 테이블에 이식
|
|
4
4
|
*
|
|
5
|
-
* KB 온톨로지에 service 타입으로 등록된 도메인 중
|
|
5
|
+
* KB 온톨로지에 service 타입으로 등록된 도메인 중 services에 미등록된 것을
|
|
6
6
|
* 자동으로 이식. KB 엔트리 전수 조사(audit) 후 매핑/비매핑 분류.
|
|
7
7
|
*/
|
|
8
8
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
9
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.STATUS_MAP = void 0;
|
|
12
13
|
exports.getUnregisteredServices = getUnregisteredServices;
|
|
13
14
|
exports.getRegisteredServices = getRegisteredServices;
|
|
14
15
|
exports.auditServiceKBEntries = auditServiceKBEntries;
|
|
@@ -17,17 +18,20 @@ exports.insertServiceProject = insertServiceProject;
|
|
|
17
18
|
exports.writePmSummaryToKB = writePmSummaryToKB;
|
|
18
19
|
exports.printAuditReport = printAuditReport;
|
|
19
20
|
exports.printDryRunReport = printDryRunReport;
|
|
21
|
+
exports.getServiceProjectByDomain = getServiceProjectByDomain;
|
|
22
|
+
exports.updateServiceProject = updateServiceProject;
|
|
23
|
+
exports.diagnoseServiceStatus = diagnoseServiceStatus;
|
|
20
24
|
const chalk_1 = __importDefault(require("chalk"));
|
|
21
25
|
const kb_1 = require("./kb");
|
|
22
26
|
// ── KB Status Mapping ──
|
|
23
|
-
|
|
27
|
+
exports.STATUS_MAP = {
|
|
24
28
|
active: { lifecycle: "ops", status: "active" },
|
|
25
29
|
hold: { lifecycle: "ops", status: "paused" },
|
|
26
30
|
maintenance: { lifecycle: "ops", status: "active" },
|
|
27
31
|
completed: { lifecycle: "sunset", status: "completed" },
|
|
28
32
|
deprecated: { lifecycle: "sunset", status: "completed" },
|
|
29
33
|
};
|
|
30
|
-
// Keys that map directly to
|
|
34
|
+
// Keys that map directly to services columns
|
|
31
35
|
const COLUMN_KEYS = {
|
|
32
36
|
"base-information": "project_name",
|
|
33
37
|
po: "owner_name",
|
|
@@ -66,13 +70,13 @@ async function getUnregisteredServices(pool) {
|
|
|
66
70
|
WHERE o.entity_type = 'service'
|
|
67
71
|
AND o.domain NOT LIKE 'e2e-%'
|
|
68
72
|
AND NOT EXISTS (
|
|
69
|
-
SELECT 1 FROM semo.
|
|
73
|
+
SELECT 1 FROM semo.services sp WHERE sp.service_domain = o.domain
|
|
70
74
|
)
|
|
71
75
|
ORDER BY o.domain`);
|
|
72
76
|
return result.rows;
|
|
73
77
|
}
|
|
74
78
|
async function getRegisteredServices(pool) {
|
|
75
|
-
const result = await pool.query(`SELECT service_domain FROM semo.
|
|
79
|
+
const result = await pool.query(`SELECT service_domain FROM semo.services WHERE service_domain IS NOT NULL`);
|
|
76
80
|
return result.rows.map((r) => r.service_domain);
|
|
77
81
|
}
|
|
78
82
|
async function auditServiceKBEntries(pool, domain, ontologyDescription, ontologyCreatedAt) {
|
|
@@ -106,7 +110,7 @@ async function auditServiceKBEntries(pool, domain, ontologyDescription, ontology
|
|
|
106
110
|
}
|
|
107
111
|
for (const [key, items] of byKey) {
|
|
108
112
|
if (COLUMN_KEYS[key]) {
|
|
109
|
-
// Maps to
|
|
113
|
+
// Maps to services column
|
|
110
114
|
const firstContent = items[0]?.content ?? "";
|
|
111
115
|
audit.mapped.push({
|
|
112
116
|
key,
|
|
@@ -168,7 +172,7 @@ function buildServiceProjectRow(audit) {
|
|
|
168
172
|
const rawStatus = statusEntry
|
|
169
173
|
? statusEntry.value.split("\n")[0].trim().toLowerCase()
|
|
170
174
|
: "active";
|
|
171
|
-
const mapping = STATUS_MAP[rawStatus] ?? STATUS_MAP["active"];
|
|
175
|
+
const mapping = exports.STATUS_MAP[rawStatus] ?? exports.STATUS_MAP["active"];
|
|
172
176
|
// Build metadata from repo, slack-channel, tech-stack, etc.
|
|
173
177
|
const meta = { source: "kb-migration" };
|
|
174
178
|
for (const m of audit.metadata) {
|
|
@@ -185,10 +189,10 @@ function buildServiceProjectRow(audit) {
|
|
|
185
189
|
};
|
|
186
190
|
}
|
|
187
191
|
async function insertServiceProject(pool, row) {
|
|
188
|
-
const result = await pool.query(`INSERT INTO semo.
|
|
192
|
+
const result = await pool.query(`INSERT INTO semo.services
|
|
189
193
|
(project_name, owner_name, service_domain, status, lifecycle, launched_at, metadata)
|
|
190
194
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
191
|
-
RETURNING
|
|
195
|
+
RETURNING service_id`, [
|
|
192
196
|
row.project_name,
|
|
193
197
|
row.owner_name,
|
|
194
198
|
row.service_domain,
|
|
@@ -197,11 +201,11 @@ async function insertServiceProject(pool, row) {
|
|
|
197
201
|
row.launched_at,
|
|
198
202
|
JSON.stringify(row.metadata),
|
|
199
203
|
]);
|
|
200
|
-
return result.rows[0].
|
|
204
|
+
return result.rows[0].service_id;
|
|
201
205
|
}
|
|
202
206
|
async function writePmSummaryToKB(pool, domain, projectId, row, kbEntryCount) {
|
|
203
207
|
const content = [
|
|
204
|
-
`
|
|
208
|
+
`service_id: ${projectId}`,
|
|
205
209
|
`project_name: ${row.project_name}`,
|
|
206
210
|
`lifecycle: ${row.lifecycle}`,
|
|
207
211
|
`status: ${row.status}`,
|
|
@@ -230,7 +234,7 @@ function printAuditReport(results) {
|
|
|
230
234
|
chalk_1.default.red("✗");
|
|
231
235
|
console.log(`\n ${icon} ${chalk_1.default.bold(r.domain)}`);
|
|
232
236
|
if (r.action === "created") {
|
|
233
|
-
console.log(chalk_1.default.gray(`
|
|
237
|
+
console.log(chalk_1.default.gray(` service_id: ${r.projectId}\n` +
|
|
234
238
|
` mapped: ${r.audit.mapped.map((m) => m.key).join(", ") || "(없음)"}\n` +
|
|
235
239
|
` metadata: ${r.audit.metadata.map((m) => m.key).join(", ") || "(없음)"}\n` +
|
|
236
240
|
` kb-only: ${r.audit.kbOnly.map((k) => `${k.key}(${k.count})`).join(", ") || "(없음)"}`));
|
|
@@ -280,3 +284,174 @@ function printDryRunReport(results) {
|
|
|
280
284
|
}
|
|
281
285
|
console.log(chalk_1.default.cyan(` 총 ${results.length}개 서비스 이식 대상\n`));
|
|
282
286
|
}
|
|
287
|
+
async function getServiceProjectByDomain(pool, domain) {
|
|
288
|
+
const result = await pool.query(`SELECT service_id, project_name, service_domain, owner_name, owner_contact,
|
|
289
|
+
current_phase, infra_phase, status, lifecycle,
|
|
290
|
+
launched_at::text, metadata,
|
|
291
|
+
created_at::text, updated_at::text
|
|
292
|
+
FROM semo.services
|
|
293
|
+
WHERE service_domain = $1`, [domain]);
|
|
294
|
+
return result.rows[0] ?? null;
|
|
295
|
+
}
|
|
296
|
+
async function updateServiceProject(pool, domain, updates) {
|
|
297
|
+
const sets = [];
|
|
298
|
+
const params = [];
|
|
299
|
+
let idx = 1;
|
|
300
|
+
if (updates.project_name !== undefined) {
|
|
301
|
+
sets.push(`project_name = $${idx++}`);
|
|
302
|
+
params.push(updates.project_name);
|
|
303
|
+
}
|
|
304
|
+
if (updates.owner_name !== undefined) {
|
|
305
|
+
sets.push(`owner_name = $${idx++}`);
|
|
306
|
+
params.push(updates.owner_name);
|
|
307
|
+
}
|
|
308
|
+
if (updates.current_phase !== undefined) {
|
|
309
|
+
sets.push(`current_phase = $${idx++}`);
|
|
310
|
+
params.push(updates.current_phase);
|
|
311
|
+
}
|
|
312
|
+
if (updates.status !== undefined) {
|
|
313
|
+
sets.push(`status = $${idx++}`);
|
|
314
|
+
params.push(updates.status);
|
|
315
|
+
}
|
|
316
|
+
if (updates.lifecycle !== undefined) {
|
|
317
|
+
sets.push(`lifecycle = $${idx++}`);
|
|
318
|
+
params.push(updates.lifecycle);
|
|
319
|
+
// ops 전환 시 launched_at 자동 설정
|
|
320
|
+
if (updates.lifecycle === "ops") {
|
|
321
|
+
sets.push(`launched_at = COALESCE(launched_at, NOW())`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (updates.metadata !== undefined) {
|
|
325
|
+
sets.push(`metadata = COALESCE(metadata, '{}'::jsonb) || $${idx++}::jsonb`);
|
|
326
|
+
params.push(JSON.stringify(updates.metadata));
|
|
327
|
+
}
|
|
328
|
+
if (sets.length === 0)
|
|
329
|
+
return getServiceProjectByDomain(pool, domain);
|
|
330
|
+
params.push(domain);
|
|
331
|
+
const result = await pool.query(`UPDATE semo.services SET ${sets.join(", ")}
|
|
332
|
+
WHERE service_domain = $${idx}
|
|
333
|
+
RETURNING service_id, project_name, service_domain, owner_name, owner_contact,
|
|
334
|
+
current_phase, infra_phase, status, lifecycle,
|
|
335
|
+
launched_at::text, metadata, created_at::text, updated_at::text`, params);
|
|
336
|
+
return result.rows[0] ?? null;
|
|
337
|
+
}
|
|
338
|
+
async function diagnoseServiceStatus(pool, domain) {
|
|
339
|
+
// 1. KB 도메인 존재 확인
|
|
340
|
+
const ontoResult = await pool.query(`SELECT domain, description, created_at::text
|
|
341
|
+
FROM semo.ontology WHERE domain = $1 AND entity_type = 'service'`, [domain]);
|
|
342
|
+
const kbExists = ontoResult.rows.length > 0;
|
|
343
|
+
// 2. services 조회
|
|
344
|
+
const sp = await getServiceProjectByDomain(pool, domain);
|
|
345
|
+
// 3. 분류
|
|
346
|
+
if (!kbExists && !sp) {
|
|
347
|
+
return {
|
|
348
|
+
domain,
|
|
349
|
+
verdict: "missing",
|
|
350
|
+
kbEntryCount: 0,
|
|
351
|
+
kbKeys: [],
|
|
352
|
+
serviceProject: null,
|
|
353
|
+
mismatches: [],
|
|
354
|
+
missingRequired: [],
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
if (!kbExists && sp) {
|
|
358
|
+
return {
|
|
359
|
+
domain,
|
|
360
|
+
verdict: "sp-only",
|
|
361
|
+
kbEntryCount: 0,
|
|
362
|
+
kbKeys: [],
|
|
363
|
+
serviceProject: sp,
|
|
364
|
+
mismatches: [],
|
|
365
|
+
missingRequired: [],
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
// KB 엔트리 목록
|
|
369
|
+
const entriesResult = await pool.query(`SELECT DISTINCT key FROM semo.knowledge_base WHERE domain = $1 ORDER BY key`, [domain]);
|
|
370
|
+
const kbKeys = entriesResult.rows.map((r) => r.key);
|
|
371
|
+
const countResult = await pool.query(`SELECT COUNT(*)::int as cnt FROM semo.knowledge_base WHERE domain = $1`, [domain]);
|
|
372
|
+
const kbEntryCount = countResult.rows[0]?.cnt ?? 0;
|
|
373
|
+
if (kbExists && !sp) {
|
|
374
|
+
// 필수 키 체크
|
|
375
|
+
const missingRequired = [];
|
|
376
|
+
for (const req of ["base-information", "po", "status"]) {
|
|
377
|
+
if (!kbKeys.includes(req))
|
|
378
|
+
missingRequired.push(req);
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
domain,
|
|
382
|
+
verdict: "kb-only",
|
|
383
|
+
kbEntryCount,
|
|
384
|
+
kbKeys,
|
|
385
|
+
serviceProject: null,
|
|
386
|
+
mismatches: [],
|
|
387
|
+
missingRequired,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
// 4. 양쪽 다 존재 → 교차 비교
|
|
391
|
+
const mismatches = [];
|
|
392
|
+
// KB status vs services status/lifecycle
|
|
393
|
+
const statusEntry = await pool.query(`SELECT content FROM semo.knowledge_base
|
|
394
|
+
WHERE domain = $1 AND key = 'status' AND (sub_key IS NULL OR sub_key = '')
|
|
395
|
+
LIMIT 1`, [domain]);
|
|
396
|
+
if (statusEntry.rows.length > 0 && sp) {
|
|
397
|
+
const kbStatus = statusEntry.rows[0].content.split("\n")[0].trim().toLowerCase();
|
|
398
|
+
const expected = exports.STATUS_MAP[kbStatus];
|
|
399
|
+
if (expected) {
|
|
400
|
+
if (expected.lifecycle !== sp.lifecycle) {
|
|
401
|
+
mismatches.push({
|
|
402
|
+
field: "lifecycle",
|
|
403
|
+
kbValue: `status=${kbStatus} → lifecycle=${expected.lifecycle}`,
|
|
404
|
+
spValue: sp.lifecycle,
|
|
405
|
+
expected: expected.lifecycle,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
if (expected.status !== sp.status) {
|
|
409
|
+
mismatches.push({
|
|
410
|
+
field: "status",
|
|
411
|
+
kbValue: `status=${kbStatus} → status=${expected.status}`,
|
|
412
|
+
spValue: sp.status,
|
|
413
|
+
expected: expected.status,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// KB po vs services owner_name
|
|
419
|
+
const poEntry = await pool.query(`SELECT content FROM semo.knowledge_base
|
|
420
|
+
WHERE domain = $1 AND key = 'po' AND (sub_key IS NULL OR sub_key = '')
|
|
421
|
+
LIMIT 1`, [domain]);
|
|
422
|
+
if (poEntry.rows.length > 0 && sp) {
|
|
423
|
+
const kbPo = poEntry.rows[0].content.split("\n")[0].trim().toLowerCase();
|
|
424
|
+
if (kbPo !== sp.owner_name.toLowerCase()) {
|
|
425
|
+
mismatches.push({
|
|
426
|
+
field: "owner",
|
|
427
|
+
kbValue: kbPo,
|
|
428
|
+
spValue: sp.owner_name,
|
|
429
|
+
expected: kbPo,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
// section 존재 여부 vs current_phase
|
|
434
|
+
if (sp && sp.lifecycle === "build" && sp.current_phase > 0) {
|
|
435
|
+
const sectionResult = await pool.query(`SELECT COUNT(*)::int as cnt
|
|
436
|
+
FROM semo.service_sections
|
|
437
|
+
WHERE service_id = $1 AND status = 'approved'`, [sp.service_id]);
|
|
438
|
+
const approvedSections = sectionResult.rows[0]?.cnt ?? 0;
|
|
439
|
+
if (approvedSections === 0 && sp.current_phase > 0) {
|
|
440
|
+
mismatches.push({
|
|
441
|
+
field: "phase",
|
|
442
|
+
kbValue: "승인된 섹션 0개",
|
|
443
|
+
spValue: `current_phase=${sp.current_phase}`,
|
|
444
|
+
expected: "phase=0 (승인된 섹션 없음)",
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
domain,
|
|
450
|
+
verdict: mismatches.length > 0 ? "mismatch" : "healthy",
|
|
451
|
+
kbEntryCount,
|
|
452
|
+
kbKeys,
|
|
453
|
+
serviceProject: sp,
|
|
454
|
+
mismatches,
|
|
455
|
+
missingRequired: [],
|
|
456
|
+
};
|
|
457
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
npx --no -- commitlint --edit $1
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import eslint from '@eslint/js';
|
|
2
|
+
import tseslint from 'typescript-eslint';
|
|
3
|
+
|
|
4
|
+
export default tseslint.config(
|
|
5
|
+
eslint.configs.recommended,
|
|
6
|
+
...tseslint.configs.recommended,
|
|
7
|
+
{
|
|
8
|
+
ignores: ['dist/', 'node_modules/'],
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
rules: {
|
|
12
|
+
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
|
13
|
+
'@typescript-eslint/no-explicit-any': 'warn',
|
|
14
|
+
'@typescript-eslint/no-require-imports': 'off',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: PR Quality Gate
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: [dev, main]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
quality-check:
|
|
9
|
+
name: Lint + Type Check + Build
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-node@v4
|
|
14
|
+
with:
|
|
15
|
+
node-version: 20
|
|
16
|
+
- run: npm ci
|
|
17
|
+
- run: npm run lint
|
|
18
|
+
- run: npx tsc --noEmit
|
|
19
|
+
- run: npm run build
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
npx lint-staged
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
npm run lint && npx tsc --noEmit
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-semicolon/semo-cli",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.15.0",
|
|
4
4
|
"description": "SEMO CLI - AI Agent Orchestration Framework Installer",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsc",
|
|
12
|
-
"postbuild": "xattr -cr dist/ 2>/dev/null || true",
|
|
12
|
+
"postbuild": "cp -r src/templates dist/templates && xattr -cr dist/ 2>/dev/null || true",
|
|
13
13
|
"start": "node dist/index.js",
|
|
14
|
-
"dev": "ts-node src/index.ts"
|
|
14
|
+
"dev": "ts-node src/index.ts",
|
|
15
|
+
"lint": "eslint src/"
|
|
15
16
|
},
|
|
16
17
|
"keywords": [
|
|
17
18
|
"semo",
|
|
@@ -35,11 +36,14 @@
|
|
|
35
36
|
"pg": "^8.17.2"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
39
|
+
"@eslint/js": "^9.39.4",
|
|
38
40
|
"@types/inquirer": "^8.2.0",
|
|
39
41
|
"@types/node": "^20.0.0",
|
|
40
42
|
"@types/pg": "^8.16.0",
|
|
43
|
+
"eslint": "^9.39.4",
|
|
41
44
|
"ts-node": "^10.0.0",
|
|
42
|
-
"typescript": "^5.0.0"
|
|
45
|
+
"typescript": "^5.0.0",
|
|
46
|
+
"typescript-eslint": "^8.58.0"
|
|
43
47
|
},
|
|
44
48
|
"engines": {
|
|
45
49
|
"node": ">=18.0.0"
|