@team-semicolon/semo-cli 4.7.3 → 4.8.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/index.js +193 -5
- package/dist/kb.d.ts +55 -0
- package/dist/kb.js +187 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1665,7 +1665,7 @@ kbCmd
|
|
|
1665
1665
|
});
|
|
1666
1666
|
kbCmd
|
|
1667
1667
|
.command("upsert <domain> <key> [sub_key]")
|
|
1668
|
-
.description("KB 항목 쓰기 (upsert) — 임베딩 자동 생성 + 스키마 검증")
|
|
1668
|
+
.description("KB 항목 쓰기 (upsert) — 임베딩 자동 생성 + 스키마 검증 (key는 kebab-case만 허용)")
|
|
1669
1669
|
.requiredOption("--content <text>", "항목 본문")
|
|
1670
1670
|
.option("--metadata <json>", "추가 메타데이터 (JSON 문자열)")
|
|
1671
1671
|
.option("--created-by <name>", "작성자 식별자", "semo-cli")
|
|
@@ -1702,12 +1702,69 @@ kbCmd
|
|
|
1702
1702
|
process.exit(1);
|
|
1703
1703
|
}
|
|
1704
1704
|
});
|
|
1705
|
+
kbCmd
|
|
1706
|
+
.command("delete <domain> <key> [sub_key]")
|
|
1707
|
+
.description("KB 항목 삭제 (domain + key + sub_key)")
|
|
1708
|
+
.option("--yes", "확인 프롬프트 건너뛰기 (크론/스크립트용)")
|
|
1709
|
+
.action(async (domain, key, subKey, options) => {
|
|
1710
|
+
try {
|
|
1711
|
+
const pool = (0, database_1.getPool)();
|
|
1712
|
+
// 삭제 전 항목 확인
|
|
1713
|
+
const entry = await (0, kb_1.kbGet)(pool, domain, key, subKey);
|
|
1714
|
+
if (!entry) {
|
|
1715
|
+
console.log(chalk_1.default.yellow(`\n 항목 없음: ${domain}/${key}${subKey ? '/' + subKey : ''}\n`));
|
|
1716
|
+
await (0, database_1.closeConnection)();
|
|
1717
|
+
process.exit(1);
|
|
1718
|
+
}
|
|
1719
|
+
// 확인 프롬프트 (--yes가 없으면)
|
|
1720
|
+
if (!options.yes) {
|
|
1721
|
+
const fullPath = `${domain}/${entry.key}${entry.sub_key ? '/' + entry.sub_key : ''}`;
|
|
1722
|
+
console.log(chalk_1.default.cyan(`\n📄 삭제 대상: ${fullPath}`));
|
|
1723
|
+
console.log(chalk_1.default.gray(` version: ${entry.version} | created_by: ${entry.created_by} | updated: ${entry.updated_at}`));
|
|
1724
|
+
console.log(chalk_1.default.gray(` content: ${entry.content.substring(0, 120)}${entry.content.length > 120 ? '...' : ''}\n`));
|
|
1725
|
+
const { createInterface } = await import("readline");
|
|
1726
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1727
|
+
const answer = await new Promise((resolve) => {
|
|
1728
|
+
rl.question(chalk_1.default.yellow(" 정말 삭제하시겠습니까? (y/N): "), resolve);
|
|
1729
|
+
});
|
|
1730
|
+
rl.close();
|
|
1731
|
+
if (answer.toLowerCase() !== "y") {
|
|
1732
|
+
console.log(chalk_1.default.gray(" 취소됨.\n"));
|
|
1733
|
+
await (0, database_1.closeConnection)();
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
const result = await (0, kb_1.kbDelete)(pool, domain, key, subKey);
|
|
1738
|
+
if (result.deleted) {
|
|
1739
|
+
const fullPath = `${domain}/${result.entry.key}${result.entry.sub_key ? '/' + result.entry.sub_key : ''}`;
|
|
1740
|
+
console.log(chalk_1.default.green(`✔ KB 삭제 완료: ${fullPath}`));
|
|
1741
|
+
}
|
|
1742
|
+
else {
|
|
1743
|
+
console.log(chalk_1.default.red(`✖ KB 삭제 실패: ${result.error}`));
|
|
1744
|
+
process.exit(1);
|
|
1745
|
+
}
|
|
1746
|
+
await (0, database_1.closeConnection)();
|
|
1747
|
+
}
|
|
1748
|
+
catch (err) {
|
|
1749
|
+
console.error(chalk_1.default.red(`삭제 실패: ${err}`));
|
|
1750
|
+
await (0, database_1.closeConnection)();
|
|
1751
|
+
process.exit(1);
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1705
1754
|
kbCmd
|
|
1706
1755
|
.command("ontology")
|
|
1707
1756
|
.description("온톨로지 조회 — 도메인/타입/스키마/라우팅 테이블")
|
|
1708
|
-
.option("--action <type>", "
|
|
1709
|
-
.option("--domain <name>", "action=show 시 도메인")
|
|
1710
|
-
.option("--type <name>", "action=schema 시 타입 키")
|
|
1757
|
+
.option("--action <type>", "동작 (list|show|services|types|instances|schema|routing-table|register|add-key|remove-key)", "list")
|
|
1758
|
+
.option("--domain <name>", "action=show|register 시 도메인")
|
|
1759
|
+
.option("--type <name>", "action=schema|register|add-key|remove-key 시 타입 키")
|
|
1760
|
+
.option("--key <name>", "action=add-key|remove-key 시 스키마 키")
|
|
1761
|
+
.option("--key-type <type>", "action=add-key 시 키 유형 (singleton|collection)", "singleton")
|
|
1762
|
+
.option("--required", "action=add-key 시 필수 여부")
|
|
1763
|
+
.option("--hint <text>", "action=add-key 시 값 힌트")
|
|
1764
|
+
.option("--description <text>", "action=register|add-key 시 설명")
|
|
1765
|
+
.option("--service <name>", "action=register 시 서비스 그룹")
|
|
1766
|
+
.option("--tags <tags>", "action=register 시 태그 (쉼표 구분)")
|
|
1767
|
+
.option("--no-init", "action=register 시 필수 KB entry 자동 생성 건너뛰기")
|
|
1711
1768
|
.option("--format <type>", "출력 형식 (json|table)", "table")
|
|
1712
1769
|
.action(async (options) => {
|
|
1713
1770
|
try {
|
|
@@ -1852,8 +1909,91 @@ kbCmd
|
|
|
1852
1909
|
console.log();
|
|
1853
1910
|
}
|
|
1854
1911
|
}
|
|
1912
|
+
else if (action === "register") {
|
|
1913
|
+
if (!options.domain) {
|
|
1914
|
+
console.log(chalk_1.default.red("--domain 옵션이 필요합니다."));
|
|
1915
|
+
process.exit(1);
|
|
1916
|
+
}
|
|
1917
|
+
if (!options.type) {
|
|
1918
|
+
console.log(chalk_1.default.red("--type 옵션이 필요합니다. (예: --type service, --type team, --type person)"));
|
|
1919
|
+
const types = await (0, kb_1.ontoListTypes)(pool);
|
|
1920
|
+
console.log(chalk_1.default.gray(`사용 가능한 타입: ${types.map(t => t.type_key).join(", ")}`));
|
|
1921
|
+
process.exit(1);
|
|
1922
|
+
}
|
|
1923
|
+
const tags = options.tags ? options.tags.split(",").map((t) => t.trim()) : undefined;
|
|
1924
|
+
const result = await (0, kb_1.ontoRegister)(pool, {
|
|
1925
|
+
domain: options.domain,
|
|
1926
|
+
entity_type: options.type,
|
|
1927
|
+
description: options.description,
|
|
1928
|
+
service: options.service,
|
|
1929
|
+
tags,
|
|
1930
|
+
init_required: options.init !== false,
|
|
1931
|
+
});
|
|
1932
|
+
if (options.format === "json") {
|
|
1933
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1934
|
+
}
|
|
1935
|
+
else {
|
|
1936
|
+
if (result.success) {
|
|
1937
|
+
console.log(chalk_1.default.green(`\n✅ 도메인 '${options.domain}' 등록 완료 (타입: ${options.type})`));
|
|
1938
|
+
if (result.created_entries && result.created_entries.length > 0) {
|
|
1939
|
+
console.log(chalk_1.default.gray(` 초기 KB entry ${result.created_entries.length}건 생성:`));
|
|
1940
|
+
for (const e of result.created_entries) {
|
|
1941
|
+
console.log(chalk_1.default.gray(` - ${e.key}`));
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
console.log(chalk_1.default.gray(`\n 다음 단계: semo kb upsert ${options.domain} <key> --content "..." 으로 데이터 입력\n`));
|
|
1945
|
+
}
|
|
1946
|
+
else {
|
|
1947
|
+
console.log(chalk_1.default.red(`\n❌ 등록 실패: ${result.error}\n`));
|
|
1948
|
+
process.exit(1);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
else if (action === "add-key") {
|
|
1953
|
+
if (!options.type) {
|
|
1954
|
+
console.log(chalk_1.default.red("--type 옵션이 필요합니다. (예: --type service)"));
|
|
1955
|
+
process.exit(1);
|
|
1956
|
+
}
|
|
1957
|
+
if (!options.key) {
|
|
1958
|
+
console.log(chalk_1.default.red("--key 옵션이 필요합니다. (예: --key slack_channel)"));
|
|
1959
|
+
process.exit(1);
|
|
1960
|
+
}
|
|
1961
|
+
const result = await (0, kb_1.ontoAddKey)(pool, {
|
|
1962
|
+
type_key: options.type,
|
|
1963
|
+
scheme_key: options.key,
|
|
1964
|
+
description: options.description,
|
|
1965
|
+
key_type: options.keyType,
|
|
1966
|
+
required: options.required || false,
|
|
1967
|
+
value_hint: options.hint,
|
|
1968
|
+
});
|
|
1969
|
+
if (result.success) {
|
|
1970
|
+
console.log(chalk_1.default.green(`\n✅ 스키마 키 추가 완료: ${options.type}.${options.key} (${options.keyType})\n`));
|
|
1971
|
+
}
|
|
1972
|
+
else {
|
|
1973
|
+
console.log(chalk_1.default.red(`\n❌ 스키마 키 추가 실패: ${result.error}\n`));
|
|
1974
|
+
process.exit(1);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
else if (action === "remove-key") {
|
|
1978
|
+
if (!options.type) {
|
|
1979
|
+
console.log(chalk_1.default.red("--type 옵션이 필요합니다."));
|
|
1980
|
+
process.exit(1);
|
|
1981
|
+
}
|
|
1982
|
+
if (!options.key) {
|
|
1983
|
+
console.log(chalk_1.default.red("--key 옵션이 필요합니다."));
|
|
1984
|
+
process.exit(1);
|
|
1985
|
+
}
|
|
1986
|
+
const result = await (0, kb_1.ontoRemoveKey)(pool, options.type, options.key);
|
|
1987
|
+
if (result.success) {
|
|
1988
|
+
console.log(chalk_1.default.green(`\n✅ 스키마 키 삭제 완료: ${options.type}.${options.key}\n`));
|
|
1989
|
+
}
|
|
1990
|
+
else {
|
|
1991
|
+
console.log(chalk_1.default.red(`\n❌ 스키마 키 삭제 실패: ${result.error}\n`));
|
|
1992
|
+
process.exit(1);
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1855
1995
|
else {
|
|
1856
|
-
console.log(chalk_1.default.red(`알 수 없는 action: '${action}'. 사용 가능: list, show, services, types, instances, schema, routing-table`));
|
|
1996
|
+
console.log(chalk_1.default.red(`알 수 없는 action: '${action}'. 사용 가능: list, show, services, types, instances, schema, routing-table, register, add-key, remove-key`));
|
|
1857
1997
|
process.exit(1);
|
|
1858
1998
|
}
|
|
1859
1999
|
await (0, database_1.closeConnection)();
|
|
@@ -2033,6 +2173,54 @@ ontoCmd
|
|
|
2033
2173
|
process.exit(1);
|
|
2034
2174
|
}
|
|
2035
2175
|
});
|
|
2176
|
+
ontoCmd
|
|
2177
|
+
.command("register <domain>")
|
|
2178
|
+
.description("새 온톨로지 도메인 등록")
|
|
2179
|
+
.requiredOption("--type <type>", "엔티티 타입 (예: service, team, person, bot)")
|
|
2180
|
+
.option("--description <text>", "도메인 설명")
|
|
2181
|
+
.option("--service <name>", "서비스 그룹")
|
|
2182
|
+
.option("--tags <tags>", "태그 (쉼표 구분)")
|
|
2183
|
+
.option("--no-init", "필수 KB entry 자동 생성 건너뛰기")
|
|
2184
|
+
.option("--format <type>", "출력 형식 (json|table)", "table")
|
|
2185
|
+
.action(async (domain, options) => {
|
|
2186
|
+
try {
|
|
2187
|
+
const pool = (0, database_1.getPool)();
|
|
2188
|
+
const tags = options.tags ? options.tags.split(",").map((t) => t.trim()) : undefined;
|
|
2189
|
+
const result = await (0, kb_1.ontoRegister)(pool, {
|
|
2190
|
+
domain,
|
|
2191
|
+
entity_type: options.type,
|
|
2192
|
+
description: options.description,
|
|
2193
|
+
service: options.service,
|
|
2194
|
+
tags,
|
|
2195
|
+
init_required: options.init !== false,
|
|
2196
|
+
});
|
|
2197
|
+
if (options.format === "json") {
|
|
2198
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2199
|
+
}
|
|
2200
|
+
else {
|
|
2201
|
+
if (result.success) {
|
|
2202
|
+
console.log(chalk_1.default.green(`\n✅ 도메인 '${domain}' 등록 완료 (타입: ${options.type})`));
|
|
2203
|
+
if (result.created_entries && result.created_entries.length > 0) {
|
|
2204
|
+
console.log(chalk_1.default.gray(` 초기 KB entry ${result.created_entries.length}건 생성:`));
|
|
2205
|
+
for (const e of result.created_entries) {
|
|
2206
|
+
console.log(chalk_1.default.gray(` - ${e.key}`));
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
console.log(chalk_1.default.gray(`\n 다음 단계: semo kb upsert ${domain} <key> --content "..." 으로 데이터 입력\n`));
|
|
2210
|
+
}
|
|
2211
|
+
else {
|
|
2212
|
+
console.log(chalk_1.default.red(`\n❌ 등록 실패: ${result.error}\n`));
|
|
2213
|
+
process.exit(1);
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
await (0, database_1.closeConnection)();
|
|
2217
|
+
}
|
|
2218
|
+
catch (err) {
|
|
2219
|
+
console.error(chalk_1.default.red(`등록 실패: ${err}`));
|
|
2220
|
+
await (0, database_1.closeConnection)();
|
|
2221
|
+
process.exit(1);
|
|
2222
|
+
}
|
|
2223
|
+
});
|
|
2036
2224
|
// === 신규 v4 커맨드 그룹 등록 ===
|
|
2037
2225
|
(0, context_1.registerContextCommands)(program);
|
|
2038
2226
|
(0, bots_1.registerBotsCommands)(program);
|
package/dist/kb.d.ts
CHANGED
|
@@ -135,6 +135,15 @@ export declare function kbDigest(pool: Pool, since: string, domain?: string): Pr
|
|
|
135
135
|
* Get a single KB entry by domain + key + sub_key
|
|
136
136
|
*/
|
|
137
137
|
export declare function kbGet(pool: Pool, domain: string, rawKey: string, rawSubKey?: string): Promise<KBEntry | null>;
|
|
138
|
+
/**
|
|
139
|
+
* Delete a single KB entry by domain/key/sub_key.
|
|
140
|
+
* Returns the deleted entry content for confirmation, or null if not found.
|
|
141
|
+
*/
|
|
142
|
+
export declare function kbDelete(pool: Pool, domain: string, rawKey: string, rawSubKey?: string): Promise<{
|
|
143
|
+
deleted: boolean;
|
|
144
|
+
entry?: KBEntry;
|
|
145
|
+
error?: string;
|
|
146
|
+
}>;
|
|
138
147
|
/**
|
|
139
148
|
* Upsert a single KB entry with domain/key validation and embedding generation
|
|
140
149
|
*/
|
|
@@ -198,6 +207,52 @@ export declare function ontoListServices(pool: Pool): Promise<ServiceInfo[]>;
|
|
|
198
207
|
* List service instances (entity_type = 'service')
|
|
199
208
|
*/
|
|
200
209
|
export declare function ontoListInstances(pool: Pool): Promise<ServiceInstance[]>;
|
|
210
|
+
export interface OntoRegisterOptions {
|
|
211
|
+
domain: string;
|
|
212
|
+
entity_type: string;
|
|
213
|
+
description?: string;
|
|
214
|
+
service?: string;
|
|
215
|
+
tags?: string[];
|
|
216
|
+
init_required?: boolean;
|
|
217
|
+
}
|
|
218
|
+
export interface OntoRegisterResult {
|
|
219
|
+
success: boolean;
|
|
220
|
+
error?: string;
|
|
221
|
+
created_entries?: Array<{
|
|
222
|
+
key: string;
|
|
223
|
+
sub_key: string;
|
|
224
|
+
}>;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Register a new ontology domain with optional initial required KB entries.
|
|
228
|
+
*
|
|
229
|
+
* 1. Validate entity_type exists in ontology_types
|
|
230
|
+
* 2. Check domain doesn't already exist
|
|
231
|
+
* 3. INSERT into semo.ontology
|
|
232
|
+
* 4. If init_required (default true), create KB entries for required keys in kb_type_schema
|
|
233
|
+
*/
|
|
234
|
+
export declare function ontoRegister(pool: Pool, opts: OntoRegisterOptions): Promise<OntoRegisterResult>;
|
|
235
|
+
/**
|
|
236
|
+
* Add a key to a type schema
|
|
237
|
+
*/
|
|
238
|
+
export declare function ontoAddKey(pool: Pool, opts: {
|
|
239
|
+
type_key: string;
|
|
240
|
+
scheme_key: string;
|
|
241
|
+
description?: string;
|
|
242
|
+
key_type?: "singleton" | "collection";
|
|
243
|
+
required?: boolean;
|
|
244
|
+
value_hint?: string;
|
|
245
|
+
}): Promise<{
|
|
246
|
+
success: boolean;
|
|
247
|
+
error?: string;
|
|
248
|
+
}>;
|
|
249
|
+
/**
|
|
250
|
+
* Remove a key from a type schema
|
|
251
|
+
*/
|
|
252
|
+
export declare function ontoRemoveKey(pool: Pool, typeKey: string, schemeKey: string): Promise<{
|
|
253
|
+
success: boolean;
|
|
254
|
+
error?: string;
|
|
255
|
+
}>;
|
|
201
256
|
/**
|
|
202
257
|
* Write ontology schemas to local cache
|
|
203
258
|
*/
|
package/dist/kb.js
CHANGED
|
@@ -55,11 +55,15 @@ exports.ontoListTypes = ontoListTypes;
|
|
|
55
55
|
exports.ontoValidate = ontoValidate;
|
|
56
56
|
exports.kbDigest = kbDigest;
|
|
57
57
|
exports.kbGet = kbGet;
|
|
58
|
+
exports.kbDelete = kbDelete;
|
|
58
59
|
exports.kbUpsert = kbUpsert;
|
|
59
60
|
exports.ontoListSchema = ontoListSchema;
|
|
60
61
|
exports.ontoRoutingTable = ontoRoutingTable;
|
|
61
62
|
exports.ontoListServices = ontoListServices;
|
|
62
63
|
exports.ontoListInstances = ontoListInstances;
|
|
64
|
+
exports.ontoRegister = ontoRegister;
|
|
65
|
+
exports.ontoAddKey = ontoAddKey;
|
|
66
|
+
exports.ontoRemoveKey = ontoRemoveKey;
|
|
63
67
|
exports.ontoPullToLocal = ontoPullToLocal;
|
|
64
68
|
const fs = __importStar(require("fs"));
|
|
65
69
|
const path = __importStar(require("path"));
|
|
@@ -682,6 +686,37 @@ async function kbGet(pool, domain, rawKey, rawSubKey) {
|
|
|
682
686
|
client.release();
|
|
683
687
|
}
|
|
684
688
|
}
|
|
689
|
+
/**
|
|
690
|
+
* Delete a single KB entry by domain/key/sub_key.
|
|
691
|
+
* Returns the deleted entry content for confirmation, or null if not found.
|
|
692
|
+
*/
|
|
693
|
+
async function kbDelete(pool, domain, rawKey, rawSubKey) {
|
|
694
|
+
let key;
|
|
695
|
+
let subKey;
|
|
696
|
+
if (rawSubKey !== undefined) {
|
|
697
|
+
key = rawKey;
|
|
698
|
+
subKey = rawSubKey;
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
const split = splitKey(rawKey);
|
|
702
|
+
key = split.key;
|
|
703
|
+
subKey = split.subKey;
|
|
704
|
+
}
|
|
705
|
+
const client = await pool.connect();
|
|
706
|
+
try {
|
|
707
|
+
const result = await client.query(`DELETE FROM semo.knowledge_base
|
|
708
|
+
WHERE domain = $1 AND key = $2 AND sub_key = $3
|
|
709
|
+
RETURNING domain, key, sub_key, content, metadata, created_by, version,
|
|
710
|
+
created_at::text, updated_at::text`, [domain, key, subKey]);
|
|
711
|
+
if (result.rows.length === 0) {
|
|
712
|
+
return { deleted: false, error: `항목 없음: ${domain}/${combineKey(key, subKey)}` };
|
|
713
|
+
}
|
|
714
|
+
return { deleted: true, entry: result.rows[0] };
|
|
715
|
+
}
|
|
716
|
+
finally {
|
|
717
|
+
client.release();
|
|
718
|
+
}
|
|
719
|
+
}
|
|
685
720
|
/**
|
|
686
721
|
* Upsert a single KB entry with domain/key validation and embedding generation
|
|
687
722
|
*/
|
|
@@ -713,6 +748,22 @@ async function kbUpsert(pool, entry) {
|
|
|
713
748
|
finally {
|
|
714
749
|
client.release();
|
|
715
750
|
}
|
|
751
|
+
// Naming convention check: kebab-case only
|
|
752
|
+
const warnings = [];
|
|
753
|
+
if (/_/.test(key)) {
|
|
754
|
+
const suggested = key.replace(/_/g, "-");
|
|
755
|
+
return {
|
|
756
|
+
success: false,
|
|
757
|
+
error: `키 '${key}'에 snake_case가 포함되어 있습니다. kebab-case를 사용하세요: '${suggested}'`,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
if (/_/.test(subKey)) {
|
|
761
|
+
const suggested = subKey.replace(/_/g, "-");
|
|
762
|
+
return {
|
|
763
|
+
success: false,
|
|
764
|
+
error: `sub_key '${subKey}'에 snake_case가 포함되어 있습니다. kebab-case를 사용하세요: '${suggested}'`,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
716
767
|
// Key validation against type schema
|
|
717
768
|
{
|
|
718
769
|
const schemaClient = await pool.connect();
|
|
@@ -886,6 +937,142 @@ async function ontoListInstances(pool) {
|
|
|
886
937
|
client.release();
|
|
887
938
|
}
|
|
888
939
|
}
|
|
940
|
+
/**
|
|
941
|
+
* Register a new ontology domain with optional initial required KB entries.
|
|
942
|
+
*
|
|
943
|
+
* 1. Validate entity_type exists in ontology_types
|
|
944
|
+
* 2. Check domain doesn't already exist
|
|
945
|
+
* 3. INSERT into semo.ontology
|
|
946
|
+
* 4. If init_required (default true), create KB entries for required keys in kb_type_schema
|
|
947
|
+
*/
|
|
948
|
+
async function ontoRegister(pool, opts) {
|
|
949
|
+
const client = await pool.connect();
|
|
950
|
+
try {
|
|
951
|
+
// 1. Validate entity_type
|
|
952
|
+
const typeCheck = await client.query("SELECT type_key FROM semo.ontology_types WHERE type_key = $1", [opts.entity_type]);
|
|
953
|
+
if (typeCheck.rows.length === 0) {
|
|
954
|
+
const known = await client.query("SELECT type_key FROM semo.ontology_types ORDER BY type_key");
|
|
955
|
+
const knownTypes = known.rows.map((r) => r.type_key);
|
|
956
|
+
return {
|
|
957
|
+
success: false,
|
|
958
|
+
error: `타입 '${opts.entity_type}'은(는) 존재하지 않습니다. 사용 가능한 타입: [${knownTypes.join(", ")}]`,
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
// 2. Check domain doesn't already exist
|
|
962
|
+
const existCheck = await client.query("SELECT domain FROM semo.ontology WHERE domain = $1", [opts.domain]);
|
|
963
|
+
if (existCheck.rows.length > 0) {
|
|
964
|
+
return { success: false, error: `도메인 '${opts.domain}'은(는) 이미 등록되어 있습니다.` };
|
|
965
|
+
}
|
|
966
|
+
// 3. INSERT into ontology
|
|
967
|
+
await client.query(`INSERT INTO semo.ontology (domain, entity_type, description, service, tags, schema)
|
|
968
|
+
VALUES ($1, $2, $3, $4, $5, $6)`, [
|
|
969
|
+
opts.domain,
|
|
970
|
+
opts.entity_type,
|
|
971
|
+
opts.description || null,
|
|
972
|
+
opts.service || "_global",
|
|
973
|
+
opts.tags || [opts.entity_type],
|
|
974
|
+
JSON.stringify({}),
|
|
975
|
+
]);
|
|
976
|
+
// 4. Create required KB entries
|
|
977
|
+
const createdEntries = [];
|
|
978
|
+
const initRequired = opts.init_required !== false;
|
|
979
|
+
if (initRequired) {
|
|
980
|
+
const schemaResult = await client.query(`SELECT scheme_key, scheme_description, COALESCE(key_type, 'singleton') as key_type, value_hint
|
|
981
|
+
FROM semo.kb_type_schema
|
|
982
|
+
WHERE type_key = $1 AND required = true
|
|
983
|
+
ORDER BY sort_order`, [opts.entity_type]);
|
|
984
|
+
for (const s of schemaResult.rows) {
|
|
985
|
+
if (s.key_type === "collection")
|
|
986
|
+
continue; // collection은 sub_key가 필요하므로 스킵
|
|
987
|
+
const placeholder = s.value_hint
|
|
988
|
+
? `(미입력 — hint: ${s.value_hint})`
|
|
989
|
+
: `(미입력)`;
|
|
990
|
+
const text = `${s.scheme_key}: ${placeholder}`;
|
|
991
|
+
const embedding = await generateEmbedding(text);
|
|
992
|
+
const embeddingStr = embedding ? `[${embedding.join(",")}]` : null;
|
|
993
|
+
try {
|
|
994
|
+
await client.query(`INSERT INTO semo.knowledge_base (domain, key, sub_key, content, metadata, created_by, embedding)
|
|
995
|
+
VALUES ($1, $2, '', $3, '{}', 'semo-cli:onto-register', $4::vector)
|
|
996
|
+
ON CONFLICT (domain, key, sub_key) DO NOTHING`, [opts.domain, s.scheme_key, placeholder, embeddingStr]);
|
|
997
|
+
createdEntries.push({ key: s.scheme_key, sub_key: "" });
|
|
998
|
+
}
|
|
999
|
+
catch {
|
|
1000
|
+
// 개별 entry 실패는 무시 — 도메인 등록 자체는 성공
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return { success: true, created_entries: createdEntries };
|
|
1005
|
+
}
|
|
1006
|
+
catch (err) {
|
|
1007
|
+
return { success: false, error: String(err) };
|
|
1008
|
+
}
|
|
1009
|
+
finally {
|
|
1010
|
+
client.release();
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Add a key to a type schema
|
|
1015
|
+
*/
|
|
1016
|
+
async function ontoAddKey(pool, opts) {
|
|
1017
|
+
const client = await pool.connect();
|
|
1018
|
+
try {
|
|
1019
|
+
// Naming convention: kebab-case only
|
|
1020
|
+
if (/_/.test(opts.scheme_key)) {
|
|
1021
|
+
const suggested = opts.scheme_key.replace(/_/g, "-");
|
|
1022
|
+
return { success: false, error: `키 '${opts.scheme_key}'에 snake_case가 포함되어 있습니다. kebab-case를 사용하세요: '${suggested}'` };
|
|
1023
|
+
}
|
|
1024
|
+
// Check type exists
|
|
1025
|
+
const typeCheck = await client.query("SELECT DISTINCT type_key FROM semo.kb_type_schema WHERE type_key = $1", [opts.type_key]);
|
|
1026
|
+
if (typeCheck.rows.length === 0) {
|
|
1027
|
+
// Check if this type exists in ontology at all
|
|
1028
|
+
const ontoCheck = await client.query("SELECT DISTINCT entity_type FROM semo.ontology WHERE entity_type = $1", [opts.type_key]);
|
|
1029
|
+
if (ontoCheck.rows.length === 0) {
|
|
1030
|
+
return { success: false, error: `타입 '${opts.type_key}'이(가) 존재하지 않습니다.` };
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
// Check duplicate
|
|
1034
|
+
const dupCheck = await client.query("SELECT id FROM semo.kb_type_schema WHERE type_key = $1 AND scheme_key = $2", [opts.type_key, opts.scheme_key]);
|
|
1035
|
+
if (dupCheck.rows.length > 0) {
|
|
1036
|
+
return { success: false, error: `키 '${opts.scheme_key}'은(는) '${opts.type_key}' 타입에 이미 존재합니다.` };
|
|
1037
|
+
}
|
|
1038
|
+
// Get max sort_order
|
|
1039
|
+
const maxOrder = await client.query("SELECT COALESCE(MAX(sort_order), 0) + 10 as next_order FROM semo.kb_type_schema WHERE type_key = $1", [opts.type_key]);
|
|
1040
|
+
const sortOrder = maxOrder.rows[0].next_order;
|
|
1041
|
+
await client.query(`INSERT INTO semo.kb_type_schema (type_key, scheme_key, scheme_description, key_type, required, value_hint, sort_order)
|
|
1042
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)`, [
|
|
1043
|
+
opts.type_key,
|
|
1044
|
+
opts.scheme_key,
|
|
1045
|
+
opts.description || opts.scheme_key,
|
|
1046
|
+
opts.key_type || "singleton",
|
|
1047
|
+
opts.required || false,
|
|
1048
|
+
opts.value_hint || null,
|
|
1049
|
+
sortOrder,
|
|
1050
|
+
]);
|
|
1051
|
+
return { success: true };
|
|
1052
|
+
}
|
|
1053
|
+
catch (err) {
|
|
1054
|
+
return { success: false, error: String(err) };
|
|
1055
|
+
}
|
|
1056
|
+
finally {
|
|
1057
|
+
client.release();
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Remove a key from a type schema
|
|
1062
|
+
*/
|
|
1063
|
+
async function ontoRemoveKey(pool, typeKey, schemeKey) {
|
|
1064
|
+
const client = await pool.connect();
|
|
1065
|
+
try {
|
|
1066
|
+
const result = await client.query("DELETE FROM semo.kb_type_schema WHERE type_key = $1 AND scheme_key = $2 RETURNING id", [typeKey, schemeKey]);
|
|
1067
|
+
if (result.rows.length === 0) {
|
|
1068
|
+
return { success: false, error: `키 '${schemeKey}'은(는) '${typeKey}' 타입에 존재하지 않습니다.` };
|
|
1069
|
+
}
|
|
1070
|
+
return { success: true };
|
|
1071
|
+
}
|
|
1072
|
+
finally {
|
|
1073
|
+
client.release();
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
889
1076
|
/**
|
|
890
1077
|
* Write ontology schemas to local cache
|
|
891
1078
|
*/
|