@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/commands/audit.d.ts +12 -4
- package/dist/commands/audit.js +219 -27
- package/dist/commands/bots.js +60 -37
- package/dist/commands/context.d.ts +2 -2
- package/dist/commands/context.js +11 -30
- package/dist/commands/get.js +31 -17
- package/dist/commands/memory.d.ts +8 -0
- package/dist/commands/memory.js +299 -0
- package/dist/commands/skill-sync.js +1 -1
- package/dist/commands/test.d.ts +11 -0
- package/dist/commands/test.js +520 -0
- package/dist/database.d.ts +1 -0
- package/dist/database.js +4 -3
- package/dist/global-cache.js +13 -0
- package/dist/index.js +434 -45
- package/dist/kb.d.ts +85 -0
- package/dist/kb.js +331 -19
- package/dist/slack-notify.d.ts +8 -0
- package/dist/slack-notify.js +45 -0
- package/dist/test-runners/workspace-audit.d.ts +17 -0
- package/dist/test-runners/workspace-audit.js +366 -0
- package/package.json +1 -1
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
451
|
-
|
|
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,
|
|
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[]>;
|