@team-semicolon/semo-cli 4.2.0 → 4.4.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/kb.d.ts CHANGED
@@ -20,6 +20,7 @@ export declare function generateEmbeddings(texts: string[]): Promise<(number[] |
20
20
  export interface KBEntry {
21
21
  domain: string;
22
22
  key: string;
23
+ sub_key?: string;
23
24
  content: string;
24
25
  metadata?: Record<string, unknown>;
25
26
  created_by?: string;
@@ -32,8 +33,18 @@ export interface OntologyDomain {
32
33
  schema: Record<string, unknown>;
33
34
  description: string | null;
34
35
  version: number;
36
+ service?: string | null;
37
+ entity_type?: string | null;
38
+ parent?: string | null;
39
+ tags?: string[];
35
40
  updated_at?: string;
36
41
  }
42
+ export interface OntologyType {
43
+ type_key: string;
44
+ schema: Record<string, unknown>;
45
+ description: string | null;
46
+ version: number;
47
+ }
37
48
  export interface KBStatusInfo {
38
49
  shared: {
39
50
  total: number;
@@ -44,6 +55,7 @@ export interface KBStatusInfo {
44
55
  export interface KBDigestEntry {
45
56
  domain: string;
46
57
  key: string;
58
+ sub_key?: string;
47
59
  content: string;
48
60
  version: number;
49
61
  change_type: 'new' | 'updated';
@@ -79,6 +91,7 @@ export declare function kbStatus(pool: Pool): Promise<KBStatusInfo>;
79
91
  */
80
92
  export declare function kbList(pool: Pool, options: {
81
93
  domain?: string;
94
+ service?: string;
82
95
  limit?: number;
83
96
  offset?: number;
84
97
  }): Promise<KBEntry[]>;
@@ -87,6 +100,7 @@ export declare function kbList(pool: Pool, options: {
87
100
  */
88
101
  export declare function kbSearch(pool: Pool, query: string, options: {
89
102
  domain?: string;
103
+ service?: string;
90
104
  limit?: number;
91
105
  mode?: "semantic" | "text" | "hybrid";
92
106
  }): Promise<KBEntry[]>;
@@ -98,6 +112,10 @@ export declare function ontoList(pool: Pool): Promise<OntologyDomain[]>;
98
112
  * Show ontology detail for a domain
99
113
  */
100
114
  export declare function ontoShow(pool: Pool, domain: string): Promise<OntologyDomain | null>;
115
+ /**
116
+ * List all ontology types (structural templates)
117
+ */
118
+ export declare function ontoListTypes(pool: Pool): Promise<OntologyType[]>;
101
119
  /**
102
120
  * Validate KB entries against ontology schema (basic JSON Schema validation)
103
121
  */
@@ -113,6 +131,73 @@ export declare function ontoValidate(pool: Pool, domain: string, entries?: KBEnt
113
131
  * Returns all KB changes since the given ISO timestamp.
114
132
  */
115
133
  export declare function kbDigest(pool: Pool, since: string, domain?: string): Promise<KBDigestResult>;
134
+ /**
135
+ * Get a single KB entry by domain + key + sub_key
136
+ */
137
+ export declare function kbGet(pool: Pool, domain: string, rawKey: string, rawSubKey?: string): Promise<KBEntry | null>;
138
+ /**
139
+ * Upsert a single KB entry with domain/key validation and embedding generation
140
+ */
141
+ export declare function kbUpsert(pool: Pool, entry: {
142
+ domain: string;
143
+ key: string;
144
+ sub_key?: string;
145
+ content: string;
146
+ metadata?: Record<string, unknown>;
147
+ created_by?: string;
148
+ }): Promise<{
149
+ success: boolean;
150
+ error?: string;
151
+ warnings?: string[];
152
+ }>;
153
+ export interface TypeSchemaEntry {
154
+ type_key: string;
155
+ scheme_key: string;
156
+ scheme_description: string;
157
+ required: boolean;
158
+ value_hint: string | null;
159
+ sort_order: number;
160
+ key_type: "singleton" | "collection";
161
+ }
162
+ export interface RoutingEntry {
163
+ domain: string;
164
+ entity_type: string;
165
+ service: string | null;
166
+ domain_description: string | null;
167
+ scheme_key: string;
168
+ key_type: string;
169
+ scheme_description: string;
170
+ value_hint: string | null;
171
+ }
172
+ export interface ServiceInfo {
173
+ service: string;
174
+ domain_count: number;
175
+ domains: string[];
176
+ }
177
+ export interface ServiceInstance {
178
+ domain: string;
179
+ description: string | null;
180
+ service: string;
181
+ tags: string[];
182
+ scoped_domains: string[];
183
+ entry_count: number;
184
+ }
185
+ /**
186
+ * List type schema entries for a given entity type
187
+ */
188
+ export declare function ontoListSchema(pool: Pool, typeKey: string): Promise<TypeSchemaEntry[]>;
189
+ /**
190
+ * Full domain→key routing table for bot auto-classification
191
+ */
192
+ export declare function ontoRoutingTable(pool: Pool): Promise<RoutingEntry[]>;
193
+ /**
194
+ * List services grouped with domain counts
195
+ */
196
+ export declare function ontoListServices(pool: Pool): Promise<ServiceInfo[]>;
197
+ /**
198
+ * List service instances (entity_type = 'service')
199
+ */
200
+ export declare function ontoListInstances(pool: Pool): Promise<ServiceInstance[]>;
116
201
  /**
117
202
  * Write ontology schemas to local cache
118
203
  */
package/dist/kb.js CHANGED
@@ -51,11 +51,27 @@ exports.kbList = kbList;
51
51
  exports.kbSearch = kbSearch;
52
52
  exports.ontoList = ontoList;
53
53
  exports.ontoShow = ontoShow;
54
+ exports.ontoListTypes = ontoListTypes;
54
55
  exports.ontoValidate = ontoValidate;
55
56
  exports.kbDigest = kbDigest;
57
+ exports.kbGet = kbGet;
58
+ exports.kbUpsert = kbUpsert;
59
+ exports.ontoListSchema = ontoListSchema;
60
+ exports.ontoRoutingTable = ontoRoutingTable;
61
+ exports.ontoListServices = ontoListServices;
62
+ exports.ontoListInstances = ontoListInstances;
56
63
  exports.ontoPullToLocal = ontoPullToLocal;
57
64
  const fs = __importStar(require("fs"));
58
65
  const path = __importStar(require("path"));
66
+ function splitKey(combinedKey) {
67
+ const idx = combinedKey.indexOf('/');
68
+ if (idx === -1)
69
+ return { key: combinedKey, subKey: '' };
70
+ return { key: combinedKey.substring(0, idx), subKey: combinedKey.substring(idx + 1) };
71
+ }
72
+ function combineKey(key, subKey) {
73
+ return subKey ? `${key}/${subKey}` : key;
74
+ }
59
75
  // ============================================================
60
76
  // Embedding
61
77
  // ============================================================
@@ -184,7 +200,7 @@ async function kbPull(pool, domain, cwd) {
184
200
  const client = await pool.connect();
185
201
  try {
186
202
  let query = `
187
- SELECT domain, key, content, metadata, created_by, version,
203
+ SELECT domain, key, sub_key, content, metadata, created_by, version,
188
204
  created_at::text, updated_at::text
189
205
  FROM semo.knowledge_base
190
206
  `;
@@ -241,12 +257,13 @@ async function kbPush(pool, entries, createdBy, cwd) {
241
257
  try {
242
258
  const embedding = embeddings[i];
243
259
  const embeddingStr = embedding ? `[${embedding.join(",")}]` : null;
244
- await client.query(`INSERT INTO semo.knowledge_base (domain, key, content, metadata, created_by, embedding)
245
- VALUES ($1, $2, $3, $4, $5, $6::vector)
246
- ON CONFLICT (domain, key) DO UPDATE SET
260
+ const { key: flatKey, subKey } = splitKey(entry.key);
261
+ await client.query(`INSERT INTO semo.knowledge_base (domain, key, sub_key, content, metadata, created_by, embedding)
262
+ VALUES ($1, $2, $3, $4, $5, $6, $7::vector)
263
+ ON CONFLICT (domain, key, sub_key) DO UPDATE SET
247
264
  content = EXCLUDED.content,
248
265
  metadata = EXCLUDED.metadata,
249
- embedding = EXCLUDED.embedding`, [entry.domain, entry.key, entry.content, JSON.stringify(entry.metadata || {}), entry.created_by || createdBy || "unknown", embeddingStr]);
266
+ embedding = EXCLUDED.embedding`, [entry.domain, flatKey, subKey, entry.content, JSON.stringify(entry.metadata || {}), entry.created_by || createdBy || "unknown", embeddingStr]);
250
267
  upserted++;
251
268
  }
252
269
  catch (err) {
@@ -306,14 +323,22 @@ async function kbList(pool, options) {
306
323
  const limit = options.limit || 50;
307
324
  const offset = options.offset || 0;
308
325
  try {
309
- let query = "SELECT domain, key, content, metadata, created_by, version, updated_at::text FROM semo.knowledge_base";
326
+ let query = "SELECT domain, key, sub_key, content, metadata, created_by, version, updated_at::text FROM semo.knowledge_base";
310
327
  const params = [];
311
328
  let paramIdx = 1;
312
329
  if (options.domain) {
313
330
  query += ` WHERE domain = $${paramIdx++}`;
314
331
  params.push(options.domain);
315
332
  }
316
- query += ` ORDER BY domain, key LIMIT $${paramIdx++} OFFSET $${paramIdx++}`;
333
+ else if (options.service) {
334
+ // Resolve service to domain list: service name itself + dot-notation domains
335
+ const serviceDomains = await resolveServiceDomainsLocal(client, options.service);
336
+ if (serviceDomains.length > 0) {
337
+ query += ` WHERE domain = ANY($${paramIdx++})`;
338
+ params.push(serviceDomains);
339
+ }
340
+ }
341
+ query += ` ORDER BY domain, key, sub_key LIMIT $${paramIdx++} OFFSET $${paramIdx++}`;
317
342
  params.push(limit, offset);
318
343
  const result = await client.query(query, params);
319
344
  return result.rows;
@@ -329,6 +354,11 @@ async function kbSearch(pool, query, options) {
329
354
  const client = await pool.connect();
330
355
  const limit = options.limit || 10;
331
356
  const mode = options.mode || "hybrid";
357
+ // Resolve service → domain list for filtering
358
+ let serviceDomains = null;
359
+ if (options.service && !options.domain) {
360
+ serviceDomains = await resolveServiceDomainsLocal(client, options.service);
361
+ }
332
362
  try {
333
363
  let results = [];
334
364
  // Try semantic search first (if mode allows and embedding API available)
@@ -338,7 +368,7 @@ async function kbSearch(pool, query, options) {
338
368
  const embeddingStr = `[${queryEmbedding.join(",")}]`;
339
369
  // Vector search on shared KB
340
370
  let sql = `
341
- SELECT domain, key, content, metadata, created_by, version, updated_at::text,
371
+ SELECT domain, key, sub_key, content, metadata, created_by, version, updated_at::text,
342
372
  1 - (embedding <=> $1::vector) as score
343
373
  FROM semo.knowledge_base
344
374
  WHERE embedding IS NOT NULL
@@ -349,6 +379,10 @@ async function kbSearch(pool, query, options) {
349
379
  sql += ` AND domain = $${paramIdx++}`;
350
380
  params.push(options.domain);
351
381
  }
382
+ else if (serviceDomains && serviceDomains.length > 0) {
383
+ sql += ` AND domain = ANY($${paramIdx++})`;
384
+ params.push(serviceDomains);
385
+ }
352
386
  sql += ` ORDER BY embedding <=> $1::vector LIMIT $${paramIdx++}`;
353
387
  params.push(limit);
354
388
  const sharedResult = await client.query(sql, params);
@@ -368,7 +402,7 @@ async function kbSearch(pool, query, options) {
368
402
  // Build per-token ILIKE conditions + count matching tokens for scoring
369
403
  const tokenConditions = tokens.map(token => {
370
404
  textParams.push(`%${token}%`);
371
- return `(CASE WHEN content ILIKE $${tIdx} OR key ILIKE $${tIdx++} THEN 1 ELSE 0 END)`;
405
+ return `(CASE WHEN content ILIKE $${tIdx} OR key ILIKE $${tIdx} OR sub_key ILIKE $${tIdx++} THEN 1 ELSE 0 END)`;
372
406
  });
373
407
  // Score = 0.7 base + 0.15 * (matched_tokens / total_tokens), capped at 0.95
374
408
  const matchCountExpr = tokenConditions.length > 0
@@ -376,12 +410,12 @@ async function kbSearch(pool, query, options) {
376
410
  : "0";
377
411
  const scoreExpr = `LEAST(0.95, 0.7 + 0.15 * (${matchCountExpr})::float / ${Math.max(tokens.length, 1)})`;
378
412
  // WHERE: any token matches
379
- const whereTokens = tokens.map((_, i) => `(content ILIKE $${i + 1} OR key ILIKE $${i + 1})`);
413
+ const whereTokens = tokens.map((_, i) => `(content ILIKE $${i + 1} OR key ILIKE $${i + 1} OR sub_key ILIKE $${i + 1})`);
380
414
  const whereClause = whereTokens.length > 0
381
415
  ? whereTokens.join(" OR ")
382
416
  : "FALSE";
383
417
  let textSql = `
384
- SELECT domain, key, content, metadata, created_by, version, updated_at::text,
418
+ SELECT domain, key, sub_key, content, metadata, created_by, version, updated_at::text,
385
419
  ${scoreExpr} as score
386
420
  FROM semo.knowledge_base
387
421
  WHERE ${whereClause}
@@ -390,6 +424,10 @@ async function kbSearch(pool, query, options) {
390
424
  textSql += ` AND domain = $${tIdx++}`;
391
425
  textParams.push(options.domain);
392
426
  }
427
+ else if (serviceDomains && serviceDomains.length > 0) {
428
+ textSql += ` AND domain = ANY($${tIdx++})`;
429
+ textParams.push(serviceDomains);
430
+ }
393
431
  textSql += ` ORDER BY score DESC, updated_at DESC LIMIT $${tIdx++}`;
394
432
  textParams.push(limit);
395
433
  const textResult = await client.query(textSql, textParams);
@@ -397,10 +435,10 @@ async function kbSearch(pool, query, options) {
397
435
  // Deduplicate by domain/key; if already in semantic results, boost its score
398
436
  const resultMap = new Map();
399
437
  for (const r of results) {
400
- resultMap.set(`${r.domain}/${r.key}`, r);
438
+ resultMap.set(`${r.domain}/${r.key}/${r.sub_key}`, r);
401
439
  }
402
440
  for (const row of textResult.rows) {
403
- const k = `${row.domain}/${row.key}`;
441
+ const k = `${row.domain}/${row.key}/${row.sub_key}`;
404
442
  const existing = resultMap.get(k);
405
443
  if (existing) {
406
444
  // Boost: semantic match + text match = highest relevance
@@ -418,9 +456,9 @@ async function kbSearch(pool, query, options) {
418
456
  catch {
419
457
  // Ultimate fallback: simple ILIKE
420
458
  let sql = `
421
- SELECT domain, key, content, metadata, created_by, version, updated_at::text
459
+ SELECT domain, key, sub_key, content, metadata, created_by, version, updated_at::text
422
460
  FROM semo.knowledge_base
423
- WHERE content ILIKE $1 OR key ILIKE $1
461
+ WHERE content ILIKE $1 OR key ILIKE $1 OR sub_key ILIKE $1
424
462
  `;
425
463
  const params = [`%${query}%`];
426
464
  let paramIdx = 2;
@@ -447,8 +485,10 @@ async function ontoList(pool) {
447
485
  const client = await pool.connect();
448
486
  try {
449
487
  const result = await client.query(`
450
- SELECT domain, schema, description, version, updated_at::text
451
- FROM semo.ontology ORDER BY domain
488
+ SELECT domain, schema, description, version,
489
+ service, entity_type, parent, tags,
490
+ updated_at::text
491
+ FROM semo.ontology ORDER BY service NULLS FIRST, domain
452
492
  `);
453
493
  return result.rows;
454
494
  }
@@ -462,13 +502,52 @@ async function ontoList(pool) {
462
502
  async function ontoShow(pool, domain) {
463
503
  const client = await pool.connect();
464
504
  try {
465
- const result = await client.query(`SELECT domain, schema, description, version, updated_at::text FROM semo.ontology WHERE domain = $1`, [domain]);
505
+ const result = await client.query(`SELECT domain, schema, description, version,
506
+ service, entity_type, parent, tags,
507
+ updated_at::text
508
+ FROM semo.ontology WHERE domain = $1`, [domain]);
466
509
  return result.rows[0] || null;
467
510
  }
468
511
  finally {
469
512
  client.release();
470
513
  }
471
514
  }
515
+ /**
516
+ * List all ontology types (structural templates)
517
+ */
518
+ async function ontoListTypes(pool) {
519
+ const client = await pool.connect();
520
+ try {
521
+ const result = await client.query(`
522
+ SELECT type_key, schema, description, version
523
+ FROM semo.ontology_types ORDER BY type_key
524
+ `);
525
+ return result.rows;
526
+ }
527
+ catch {
528
+ return []; // Table may not exist yet (pre-016 migration)
529
+ }
530
+ finally {
531
+ client.release();
532
+ }
533
+ }
534
+ /**
535
+ * Resolve a service name to its associated domain list.
536
+ * Uses ontology.service column + dot-notation domain detection.
537
+ */
538
+ async function resolveServiceDomainsLocal(client, service) {
539
+ try {
540
+ const result = await client.query(`SELECT domain FROM semo.ontology WHERE service = $1
541
+ UNION
542
+ SELECT domain FROM semo.ontology WHERE domain LIKE $2
543
+ UNION
544
+ SELECT domain FROM semo.ontology WHERE domain = $1`, [service, `${service}.%`]);
545
+ return result.rows.map((r) => r.domain);
546
+ }
547
+ catch {
548
+ return [];
549
+ }
550
+ }
472
551
  /**
473
552
  * Validate KB entries against ontology schema (basic JSON Schema validation)
474
553
  */
@@ -540,7 +619,7 @@ async function kbDigest(pool, since, domain) {
540
619
  const generatedAt = new Date().toISOString();
541
620
  try {
542
621
  let sql = `
543
- SELECT domain, key, content, version, updated_at::text,
622
+ SELECT domain, key, sub_key, content, version, updated_at::text,
544
623
  CASE WHEN created_at > $1 THEN 'new' ELSE 'updated' END as change_type
545
624
  FROM semo.knowledge_base
546
625
  WHERE updated_at > $1 OR created_at > $1
@@ -559,6 +638,239 @@ async function kbDigest(pool, since, domain) {
559
638
  client.release();
560
639
  }
561
640
  }
641
+ // ============================================================
642
+ // KB Get / Upsert (CLI counterparts of MCP kb functions)
643
+ // ============================================================
644
+ /**
645
+ * Get a single KB entry by domain + key + sub_key
646
+ */
647
+ async function kbGet(pool, domain, rawKey, rawSubKey) {
648
+ let key;
649
+ let subKey;
650
+ if (rawSubKey !== undefined) {
651
+ key = rawKey;
652
+ subKey = rawSubKey;
653
+ }
654
+ else {
655
+ const split = splitKey(rawKey);
656
+ key = split.key;
657
+ subKey = split.subKey;
658
+ }
659
+ const client = await pool.connect();
660
+ try {
661
+ const result = await client.query(`SELECT domain, key, sub_key, content, metadata, created_by, version,
662
+ created_at::text, updated_at::text
663
+ FROM semo.knowledge_base
664
+ WHERE domain = $1 AND key = $2 AND sub_key = $3`, [domain, key, subKey]);
665
+ return result.rows[0] || null;
666
+ }
667
+ finally {
668
+ client.release();
669
+ }
670
+ }
671
+ /**
672
+ * Upsert a single KB entry with domain/key validation and embedding generation
673
+ */
674
+ async function kbUpsert(pool, entry) {
675
+ let key;
676
+ let subKey;
677
+ if (entry.sub_key !== undefined) {
678
+ key = entry.key;
679
+ subKey = entry.sub_key;
680
+ }
681
+ else {
682
+ const split = splitKey(entry.key);
683
+ key = split.key;
684
+ subKey = split.subKey;
685
+ }
686
+ // Domain validation
687
+ const client = await pool.connect();
688
+ try {
689
+ const ontoCheck = await client.query("SELECT domain FROM semo.ontology WHERE domain = $1", [entry.domain]);
690
+ if (ontoCheck.rows.length === 0) {
691
+ const known = await client.query("SELECT domain FROM semo.ontology ORDER BY domain");
692
+ const knownDomains = known.rows.map((r) => r.domain);
693
+ return {
694
+ success: false,
695
+ error: `도메인 '${entry.domain}'은(는) 온톨로지에 등록되지 않았습니다. 등록된 도메인: [${knownDomains.join(", ")}]`,
696
+ };
697
+ }
698
+ }
699
+ finally {
700
+ client.release();
701
+ }
702
+ // Key validation against type schema
703
+ try {
704
+ const schemaClient = await pool.connect();
705
+ try {
706
+ const typeResult = await schemaClient.query("SELECT entity_type FROM semo.ontology WHERE domain = $1 AND entity_type IS NOT NULL", [entry.domain]);
707
+ if (typeResult.rows.length > 0) {
708
+ const entityType = typeResult.rows[0].entity_type;
709
+ const schemaResult = await schemaClient.query("SELECT scheme_key, COALESCE(key_type, 'singleton') as key_type FROM semo.kb_type_schema WHERE type_key = $1", [entityType]);
710
+ const schemas = schemaResult.rows;
711
+ if (schemas.length > 0) {
712
+ const match = schemas.find(s => s.scheme_key === key);
713
+ if (!match) {
714
+ const allowedKeys = schemas.map(s => s.key_type === "singleton" ? s.scheme_key : `${s.scheme_key}/{sub_key}`);
715
+ return {
716
+ success: false,
717
+ error: `키 '${key}'은(는) '${entityType}' 타입의 스키마에 허용되지 않습니다. 허용 키: [${allowedKeys.join(", ")}]`,
718
+ };
719
+ }
720
+ if (match.key_type === "singleton" && subKey !== "") {
721
+ return { success: false, error: `키 '${key}'은(는) singleton이므로 sub_key가 비어야 합니다.` };
722
+ }
723
+ if (match.key_type === "collection" && subKey === "") {
724
+ return { success: false, error: `키 '${key}'은(는) collection이므로 sub_key가 필요합니다.` };
725
+ }
726
+ }
727
+ }
728
+ }
729
+ finally {
730
+ schemaClient.release();
731
+ }
732
+ }
733
+ catch {
734
+ // Validation failure is non-fatal
735
+ }
736
+ // Generate embedding (mandatory)
737
+ const fullKey = combineKey(key, subKey);
738
+ const text = `${fullKey}: ${entry.content}`;
739
+ const embedding = await generateEmbedding(text);
740
+ if (!embedding) {
741
+ const reason = process.env.OPENAI_API_KEY
742
+ ? "임베딩 생성 API 호출 실패"
743
+ : "OPENAI_API_KEY가 설정되지 않음";
744
+ return {
745
+ success: false,
746
+ error: `임베딩 생성 실패 — ${reason}. 임베딩 없이 저장하면 벡터 검색에서 누락되므로 저장이 거부됩니다.`,
747
+ };
748
+ }
749
+ const embeddingStr = `[${embedding.join(",")}]`;
750
+ const writeClient = await pool.connect();
751
+ try {
752
+ await writeClient.query(`INSERT INTO semo.knowledge_base (domain, key, sub_key, content, metadata, created_by, embedding)
753
+ VALUES ($1, $2, $3, $4, $5, $6, $7::vector)
754
+ ON CONFLICT (domain, key, sub_key) DO UPDATE SET
755
+ content = EXCLUDED.content,
756
+ metadata = EXCLUDED.metadata,
757
+ embedding = EXCLUDED.embedding`, [
758
+ entry.domain,
759
+ key,
760
+ subKey,
761
+ entry.content,
762
+ JSON.stringify(entry.metadata || {}),
763
+ entry.created_by || "semo-cli",
764
+ embeddingStr,
765
+ ]);
766
+ return { success: true };
767
+ }
768
+ catch (err) {
769
+ return { success: false, error: String(err) };
770
+ }
771
+ finally {
772
+ writeClient.release();
773
+ }
774
+ }
775
+ /**
776
+ * List type schema entries for a given entity type
777
+ */
778
+ async function ontoListSchema(pool, typeKey) {
779
+ const client = await pool.connect();
780
+ try {
781
+ const result = await client.query(`SELECT type_key, scheme_key, scheme_description, required, value_hint, sort_order,
782
+ COALESCE(key_type, 'singleton') as key_type
783
+ FROM semo.kb_type_schema
784
+ WHERE type_key = $1
785
+ ORDER BY sort_order, scheme_key`, [typeKey]);
786
+ return result.rows;
787
+ }
788
+ catch {
789
+ return [];
790
+ }
791
+ finally {
792
+ client.release();
793
+ }
794
+ }
795
+ /**
796
+ * Full domain→key routing table for bot auto-classification
797
+ */
798
+ async function ontoRoutingTable(pool) {
799
+ const client = await pool.connect();
800
+ try {
801
+ const result = await client.query(`
802
+ SELECT
803
+ o.domain,
804
+ o.entity_type,
805
+ o.service,
806
+ o.description AS domain_description,
807
+ s.scheme_key,
808
+ s.key_type,
809
+ s.scheme_description,
810
+ s.value_hint
811
+ FROM semo.ontology o
812
+ JOIN semo.kb_type_schema s ON s.type_key = o.entity_type
813
+ ORDER BY o.domain, s.sort_order
814
+ `);
815
+ return result.rows;
816
+ }
817
+ finally {
818
+ client.release();
819
+ }
820
+ }
821
+ /**
822
+ * List services grouped with domain counts
823
+ */
824
+ async function ontoListServices(pool) {
825
+ const client = await pool.connect();
826
+ try {
827
+ const result = await client.query(`
828
+ SELECT service, COUNT(*)::int as domain_count,
829
+ ARRAY_AGG(domain ORDER BY domain) as domains
830
+ FROM semo.ontology
831
+ WHERE service IS NOT NULL
832
+ GROUP BY service
833
+ ORDER BY service
834
+ `);
835
+ return result.rows;
836
+ }
837
+ catch {
838
+ return [];
839
+ }
840
+ finally {
841
+ client.release();
842
+ }
843
+ }
844
+ /**
845
+ * List service instances (entity_type = 'service')
846
+ */
847
+ async function ontoListInstances(pool) {
848
+ const client = await pool.connect();
849
+ try {
850
+ const result = await client.query(`
851
+ SELECT o.domain, o.description, o.service, o.tags,
852
+ COALESCE(
853
+ (SELECT ARRAY_AGG(o2.domain ORDER BY o2.domain)
854
+ FROM semo.ontology o2
855
+ WHERE o2.service = o.service AND o2.domain != o.domain),
856
+ '{}'
857
+ ) as scoped_domains,
858
+ (SELECT COUNT(*)::int FROM semo.knowledge_base k
859
+ WHERE k.domain = o.domain
860
+ OR k.domain LIKE o.service || '.%') as entry_count
861
+ FROM semo.ontology o
862
+ WHERE o.entity_type = 'service'
863
+ ORDER BY o.domain
864
+ `);
865
+ return result.rows;
866
+ }
867
+ catch {
868
+ return [];
869
+ }
870
+ finally {
871
+ client.release();
872
+ }
873
+ }
562
874
  /**
563
875
  * Write ontology schemas to local cache
564
876
  */
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Slack 알림 유틸리티
3
+ *
4
+ * SLACK_WEBHOOK 환경변수에서 URL을 읽어 알림 전송.
5
+ * ~/.semo.env에서 자동 로드됨 (database.ts loadSemoEnv).
6
+ */
7
+ export declare function sendSlackNotification(message: string, webhookUrl?: string): Promise<boolean>;
8
+ export declare function formatTestFailureMessage(suiteId: string, runId: string, pass: number, fail: number, warn: number, failedLabels: string[]): string;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ /**
3
+ * Slack 알림 유틸리티
4
+ *
5
+ * SLACK_WEBHOOK 환경변수에서 URL을 읽어 알림 전송.
6
+ * ~/.semo.env에서 자동 로드됨 (database.ts loadSemoEnv).
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.sendSlackNotification = sendSlackNotification;
10
+ exports.formatTestFailureMessage = formatTestFailureMessage;
11
+ async function sendSlackNotification(message, webhookUrl) {
12
+ const url = webhookUrl || process.env.SLACK_WEBHOOK;
13
+ if (!url)
14
+ return false;
15
+ try {
16
+ const res = await fetch(url, {
17
+ method: "POST",
18
+ headers: { "Content-Type": "application/json" },
19
+ body: JSON.stringify({ text: message }),
20
+ signal: AbortSignal.timeout(10000),
21
+ });
22
+ return res.ok;
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ }
28
+ function formatTestFailureMessage(suiteId, runId, pass, fail, warn, failedLabels) {
29
+ const timestamp = new Date().toLocaleString("ko-KR", {
30
+ timeZone: "Asia/Seoul",
31
+ });
32
+ const topFailed = failedLabels.slice(0, 10);
33
+ const more = failedLabels.length > 10 ? `\n ... +${failedLabels.length - 10}건` : "";
34
+ return [
35
+ `🚨 *Test Suite Failed* — ${suiteId}`,
36
+ `Run: \`${runId.substring(0, 8)}\` | ${timestamp}`,
37
+ `Pass: ${pass} | Fail: ${fail} | Warn: ${warn}`,
38
+ ``,
39
+ `Failed:`,
40
+ ...topFailed.map((l) => ` • ${l}`),
41
+ more,
42
+ ]
43
+ .filter(Boolean)
44
+ .join("\n");
45
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Declarative Workspace Audit Runner
3
+ *
4
+ * bot_workspace_standard 테이블에서 규칙을 로드하고,
5
+ * bot_status에서 봇 목록을 가져와 동적으로 TC를 생성·실행.
6
+ *
7
+ * 로컬 스크립트 의존성 없음 — DB가 SoT.
8
+ */
9
+ import { Pool } from "pg";
10
+ export interface TestOutputLine {
11
+ type: "case" | "summary";
12
+ id?: string;
13
+ status?: "pass" | "fail" | "warn" | "skip";
14
+ label?: string;
15
+ detail?: string;
16
+ }
17
+ export declare function runDeclarativeWorkspaceAudit(pool: Pool): Promise<TestOutputLine[]>;