sqlew 2.0.0 → 2.1.1
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/CHANGELOG.md +222 -0
- package/README.md +314 -209
- package/assets/schema.sql +122 -36
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +312 -0
- package/dist/cli.js.map +1 -0
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +18 -0
- package/dist/database.js.map +1 -1
- package/dist/index.js +136 -42
- package/dist/index.js.map +1 -1
- package/dist/migrations/add-v2.1.0-features.d.ts +29 -0
- package/dist/migrations/add-v2.1.0-features.d.ts.map +1 -0
- package/dist/migrations/add-v2.1.0-features.js +198 -0
- package/dist/migrations/add-v2.1.0-features.js.map +1 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +5 -0
- package/dist/schema.js.map +1 -1
- package/dist/tools/context.d.ts +91 -1
- package/dist/tools/context.d.ts.map +1 -1
- package/dist/tools/context.js +695 -70
- package/dist/tools/context.js.map +1 -1
- package/dist/tools/files.d.ts +10 -1
- package/dist/tools/files.d.ts.map +1 -1
- package/dist/tools/files.js +98 -1
- package/dist/tools/files.js.map +1 -1
- package/dist/tools/messaging.d.ts +10 -1
- package/dist/tools/messaging.d.ts.map +1 -1
- package/dist/tools/messaging.js +107 -1
- package/dist/tools/messaging.js.map +1 -1
- package/dist/tools/utils.d.ts +9 -1
- package/dist/tools/utils.d.ts.map +1 -1
- package/dist/tools/utils.js +115 -0
- package/dist/tools/utils.js.map +1 -1
- package/dist/types.d.ts +196 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/cleanup.d.ts +12 -1
- package/dist/utils/cleanup.d.ts.map +1 -1
- package/dist/utils/cleanup.js +20 -3
- package/dist/utils/cleanup.js.map +1 -1
- package/package.json +4 -2
package/dist/tools/context.js
CHANGED
|
@@ -5,15 +5,14 @@
|
|
|
5
5
|
import { getDatabase, getOrCreateAgent, getOrCreateContextKey, getOrCreateTag, getOrCreateScope, getLayerId, transaction } from '../database.js';
|
|
6
6
|
import { STRING_TO_STATUS, DEFAULT_VERSION, DEFAULT_STATUS } from '../constants.js';
|
|
7
7
|
/**
|
|
8
|
-
* Set
|
|
9
|
-
*
|
|
10
|
-
* Supports tags, layers, scopes, and version tracking
|
|
8
|
+
* Internal helper: Set decision without wrapping in transaction
|
|
9
|
+
* Used by setDecision (with transaction) and setDecisionBatch (manages its own transaction)
|
|
11
10
|
*
|
|
12
11
|
* @param params - Decision parameters
|
|
12
|
+
* @param db - Database instance
|
|
13
13
|
* @returns Response with success status and metadata
|
|
14
14
|
*/
|
|
15
|
-
|
|
16
|
-
const db = getDatabase();
|
|
15
|
+
function setDecisionInternal(params, db) {
|
|
17
16
|
// Validate required parameters
|
|
18
17
|
if (!params.key || params.key.trim() === '') {
|
|
19
18
|
throw new Error('Parameter "key" is required and cannot be empty');
|
|
@@ -40,74 +39,86 @@ export function setDecision(params) {
|
|
|
40
39
|
throw new Error(`Invalid layer: ${params.layer}. Must be one of: presentation, business, data, infrastructure, cross-cutting`);
|
|
41
40
|
}
|
|
42
41
|
}
|
|
42
|
+
// Get or create master records
|
|
43
|
+
const agentId = getOrCreateAgent(db, agentName);
|
|
44
|
+
const keyId = getOrCreateContextKey(db, params.key);
|
|
45
|
+
// Current timestamp
|
|
46
|
+
const ts = Math.floor(Date.now() / 1000);
|
|
47
|
+
// Insert or update decision based on value type
|
|
48
|
+
if (isNumeric) {
|
|
49
|
+
// Numeric decision
|
|
50
|
+
const stmt = db.prepare(`
|
|
51
|
+
INSERT INTO t_decisions_numeric (key_id, value, agent_id, layer_id, version, status, ts)
|
|
52
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
53
|
+
ON CONFLICT(key_id) DO UPDATE SET
|
|
54
|
+
value = excluded.value,
|
|
55
|
+
agent_id = excluded.agent_id,
|
|
56
|
+
layer_id = excluded.layer_id,
|
|
57
|
+
version = excluded.version,
|
|
58
|
+
status = excluded.status,
|
|
59
|
+
ts = excluded.ts
|
|
60
|
+
`);
|
|
61
|
+
stmt.run(keyId, value, agentId, layerId, version, status, ts);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// String decision
|
|
65
|
+
const stmt = db.prepare(`
|
|
66
|
+
INSERT INTO t_decisions (key_id, value, agent_id, layer_id, version, status, ts)
|
|
67
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
68
|
+
ON CONFLICT(key_id) DO UPDATE SET
|
|
69
|
+
value = excluded.value,
|
|
70
|
+
agent_id = excluded.agent_id,
|
|
71
|
+
layer_id = excluded.layer_id,
|
|
72
|
+
version = excluded.version,
|
|
73
|
+
status = excluded.status,
|
|
74
|
+
ts = excluded.ts
|
|
75
|
+
`);
|
|
76
|
+
stmt.run(keyId, String(value), agentId, layerId, version, status, ts);
|
|
77
|
+
}
|
|
78
|
+
// Handle m_tags (many-to-many)
|
|
79
|
+
if (params.tags && params.tags.length > 0) {
|
|
80
|
+
// Clear existing tags
|
|
81
|
+
db.prepare('DELETE FROM t_decision_tags WHERE decision_key_id = ?').run(keyId);
|
|
82
|
+
// Insert new tags
|
|
83
|
+
const tagStmt = db.prepare('INSERT INTO t_decision_tags (decision_key_id, tag_id) VALUES (?, ?)');
|
|
84
|
+
for (const tagName of params.tags) {
|
|
85
|
+
const tagId = getOrCreateTag(db, tagName);
|
|
86
|
+
tagStmt.run(keyId, tagId);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Handle m_scopes (many-to-many)
|
|
90
|
+
if (params.scopes && params.scopes.length > 0) {
|
|
91
|
+
// Clear existing scopes
|
|
92
|
+
db.prepare('DELETE FROM t_decision_scopes WHERE decision_key_id = ?').run(keyId);
|
|
93
|
+
// Insert new scopes
|
|
94
|
+
const scopeStmt = db.prepare('INSERT INTO t_decision_scopes (decision_key_id, scope_id) VALUES (?, ?)');
|
|
95
|
+
for (const scopeName of params.scopes) {
|
|
96
|
+
const scopeId = getOrCreateScope(db, scopeName);
|
|
97
|
+
scopeStmt.run(keyId, scopeId);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
success: true,
|
|
102
|
+
key: params.key,
|
|
103
|
+
key_id: keyId,
|
|
104
|
+
version: version,
|
|
105
|
+
message: `Decision "${params.key}" set successfully`
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Set or update a decision in the context
|
|
110
|
+
* Auto-detects numeric vs string values and routes to appropriate table
|
|
111
|
+
* Supports tags, layers, scopes, and version tracking
|
|
112
|
+
*
|
|
113
|
+
* @param params - Decision parameters
|
|
114
|
+
* @returns Response with success status and metadata
|
|
115
|
+
*/
|
|
116
|
+
export function setDecision(params) {
|
|
117
|
+
const db = getDatabase();
|
|
43
118
|
try {
|
|
44
119
|
// Use transaction for atomicity
|
|
45
120
|
return transaction(db, () => {
|
|
46
|
-
|
|
47
|
-
const agentId = getOrCreateAgent(db, agentName);
|
|
48
|
-
const keyId = getOrCreateContextKey(db, params.key);
|
|
49
|
-
// Current timestamp
|
|
50
|
-
const ts = Math.floor(Date.now() / 1000);
|
|
51
|
-
// Insert or update decision based on value type
|
|
52
|
-
if (isNumeric) {
|
|
53
|
-
// Numeric decision
|
|
54
|
-
const stmt = db.prepare(`
|
|
55
|
-
INSERT INTO t_decisions_numeric (key_id, value, agent_id, layer_id, version, status, ts)
|
|
56
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
57
|
-
ON CONFLICT(key_id) DO UPDATE SET
|
|
58
|
-
value = excluded.value,
|
|
59
|
-
agent_id = excluded.agent_id,
|
|
60
|
-
layer_id = excluded.layer_id,
|
|
61
|
-
version = excluded.version,
|
|
62
|
-
status = excluded.status,
|
|
63
|
-
ts = excluded.ts
|
|
64
|
-
`);
|
|
65
|
-
stmt.run(keyId, value, agentId, layerId, version, status, ts);
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
// String decision
|
|
69
|
-
const stmt = db.prepare(`
|
|
70
|
-
INSERT INTO t_decisions (key_id, value, agent_id, layer_id, version, status, ts)
|
|
71
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
72
|
-
ON CONFLICT(key_id) DO UPDATE SET
|
|
73
|
-
value = excluded.value,
|
|
74
|
-
agent_id = excluded.agent_id,
|
|
75
|
-
layer_id = excluded.layer_id,
|
|
76
|
-
version = excluded.version,
|
|
77
|
-
status = excluded.status,
|
|
78
|
-
ts = excluded.ts
|
|
79
|
-
`);
|
|
80
|
-
stmt.run(keyId, String(value), agentId, layerId, version, status, ts);
|
|
81
|
-
}
|
|
82
|
-
// Handle m_tags (many-to-many)
|
|
83
|
-
if (params.tags && params.tags.length > 0) {
|
|
84
|
-
// Clear existing tags
|
|
85
|
-
db.prepare('DELETE FROM t_decision_tags WHERE decision_key_id = ?').run(keyId);
|
|
86
|
-
// Insert new tags
|
|
87
|
-
const tagStmt = db.prepare('INSERT INTO t_decision_tags (decision_key_id, tag_id) VALUES (?, ?)');
|
|
88
|
-
for (const tagName of params.tags) {
|
|
89
|
-
const tagId = getOrCreateTag(db, tagName);
|
|
90
|
-
tagStmt.run(keyId, tagId);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
// Handle m_scopes (many-to-many)
|
|
94
|
-
if (params.scopes && params.scopes.length > 0) {
|
|
95
|
-
// Clear existing scopes
|
|
96
|
-
db.prepare('DELETE FROM t_decision_scopes WHERE decision_key_id = ?').run(keyId);
|
|
97
|
-
// Insert new scopes
|
|
98
|
-
const scopeStmt = db.prepare('INSERT INTO t_decision_scopes (decision_key_id, scope_id) VALUES (?, ?)');
|
|
99
|
-
for (const scopeName of params.scopes) {
|
|
100
|
-
const scopeId = getOrCreateScope(db, scopeName);
|
|
101
|
-
scopeStmt.run(keyId, scopeId);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return {
|
|
105
|
-
success: true,
|
|
106
|
-
key: params.key,
|
|
107
|
-
key_id: keyId,
|
|
108
|
-
version: version,
|
|
109
|
-
message: `Decision "${params.key}" set successfully`
|
|
110
|
-
};
|
|
121
|
+
return setDecisionInternal(params, db);
|
|
111
122
|
});
|
|
112
123
|
}
|
|
113
124
|
catch (error) {
|
|
@@ -439,4 +450,618 @@ export function searchByLayer(params) {
|
|
|
439
450
|
throw new Error(`Failed to search by layer: ${message}`);
|
|
440
451
|
}
|
|
441
452
|
}
|
|
453
|
+
/**
|
|
454
|
+
* Quick set decision with smart defaults and inference
|
|
455
|
+
* Reduces required parameters from 7 to 2 (key + value only)
|
|
456
|
+
*
|
|
457
|
+
* Inference Rules:
|
|
458
|
+
* - Layer: Inferred from key prefix
|
|
459
|
+
* - api/*, endpoint/*, ui/* → "presentation"
|
|
460
|
+
* - service/*, logic/*, workflow/* → "business"
|
|
461
|
+
* - db/*, model/*, schema/* → "data"
|
|
462
|
+
* - config/*, deploy/* → "infrastructure"
|
|
463
|
+
* - Default → "business"
|
|
464
|
+
*
|
|
465
|
+
* - Tags: Extracted from key hierarchy
|
|
466
|
+
* - Key "api/instruments/synthesis" → tags: ["api", "instruments", "synthesis"]
|
|
467
|
+
*
|
|
468
|
+
* - Scope: Inferred from key hierarchy
|
|
469
|
+
* - Key "api/instruments/synthesis" → scope: "api/instruments"
|
|
470
|
+
*
|
|
471
|
+
* - Auto-defaults:
|
|
472
|
+
* - status: "active"
|
|
473
|
+
* - version: "1.0.0"
|
|
474
|
+
*
|
|
475
|
+
* All inferred fields can be overridden via optional parameters.
|
|
476
|
+
*
|
|
477
|
+
* @param params - Quick set parameters (key and value required)
|
|
478
|
+
* @returns Response with success status and inferred metadata
|
|
479
|
+
*/
|
|
480
|
+
export function quickSetDecision(params) {
|
|
481
|
+
// Validate required parameters
|
|
482
|
+
if (!params.key || params.key.trim() === '') {
|
|
483
|
+
throw new Error('Parameter "key" is required and cannot be empty');
|
|
484
|
+
}
|
|
485
|
+
if (params.value === undefined || params.value === null) {
|
|
486
|
+
throw new Error('Parameter "value" is required');
|
|
487
|
+
}
|
|
488
|
+
// Track what was inferred
|
|
489
|
+
const inferred = {};
|
|
490
|
+
// Infer layer from key prefix (if not provided)
|
|
491
|
+
let inferredLayer = params.layer;
|
|
492
|
+
if (!inferredLayer) {
|
|
493
|
+
const keyLower = params.key.toLowerCase();
|
|
494
|
+
if (keyLower.startsWith('api/') || keyLower.startsWith('endpoint/') || keyLower.startsWith('ui/')) {
|
|
495
|
+
inferredLayer = 'presentation';
|
|
496
|
+
}
|
|
497
|
+
else if (keyLower.startsWith('service/') || keyLower.startsWith('logic/') || keyLower.startsWith('workflow/')) {
|
|
498
|
+
inferredLayer = 'business';
|
|
499
|
+
}
|
|
500
|
+
else if (keyLower.startsWith('db/') || keyLower.startsWith('model/') || keyLower.startsWith('schema/')) {
|
|
501
|
+
inferredLayer = 'data';
|
|
502
|
+
}
|
|
503
|
+
else if (keyLower.startsWith('config/') || keyLower.startsWith('deploy/')) {
|
|
504
|
+
inferredLayer = 'infrastructure';
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
// Default layer
|
|
508
|
+
inferredLayer = 'business';
|
|
509
|
+
}
|
|
510
|
+
inferred.layer = inferredLayer;
|
|
511
|
+
}
|
|
512
|
+
// Extract tags from key hierarchy (if not provided)
|
|
513
|
+
let inferredTags = params.tags;
|
|
514
|
+
if (!inferredTags || inferredTags.length === 0) {
|
|
515
|
+
// Split key by '/', '-', or '_' to get hierarchy parts
|
|
516
|
+
const parts = params.key.split(/[\/\-_]/).filter(p => p.trim() !== '');
|
|
517
|
+
inferredTags = parts;
|
|
518
|
+
inferred.tags = inferredTags;
|
|
519
|
+
}
|
|
520
|
+
// Infer scope from key hierarchy (if not provided)
|
|
521
|
+
let inferredScopes = params.scopes;
|
|
522
|
+
if (!inferredScopes || inferredScopes.length === 0) {
|
|
523
|
+
// Get parent scope from key (everything except last part)
|
|
524
|
+
const parts = params.key.split('/');
|
|
525
|
+
if (parts.length > 1) {
|
|
526
|
+
// Take all but the last part
|
|
527
|
+
const scopeParts = parts.slice(0, -1);
|
|
528
|
+
const scope = scopeParts.join('/');
|
|
529
|
+
inferredScopes = [scope];
|
|
530
|
+
inferred.scope = scope;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// Build full params for setDecision
|
|
534
|
+
const fullParams = {
|
|
535
|
+
key: params.key,
|
|
536
|
+
value: params.value,
|
|
537
|
+
agent: params.agent, // May be undefined, setDecision will default to 'system'
|
|
538
|
+
layer: inferredLayer,
|
|
539
|
+
version: params.version || DEFAULT_VERSION,
|
|
540
|
+
status: params.status || 'active',
|
|
541
|
+
tags: inferredTags,
|
|
542
|
+
scopes: inferredScopes
|
|
543
|
+
};
|
|
544
|
+
// Call setDecision with full params
|
|
545
|
+
const result = setDecision(fullParams);
|
|
546
|
+
// Return response with inferred metadata
|
|
547
|
+
return {
|
|
548
|
+
success: result.success,
|
|
549
|
+
key: result.key,
|
|
550
|
+
key_id: result.key_id,
|
|
551
|
+
version: result.version,
|
|
552
|
+
inferred: inferred,
|
|
553
|
+
message: `Decision "${params.key}" set successfully with smart defaults`
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Advanced query composition with complex filtering capabilities
|
|
558
|
+
* Supports multiple filter types, sorting, and pagination
|
|
559
|
+
*
|
|
560
|
+
* Filter Logic:
|
|
561
|
+
* - layers: OR relationship - match any layer in the array
|
|
562
|
+
* - tags_all: AND relationship - must have ALL tags
|
|
563
|
+
* - tags_any: OR relationship - must have ANY tag
|
|
564
|
+
* - exclude_tags: Exclude decisions with these tags
|
|
565
|
+
* - scopes: Wildcard support (e.g., "api/instruments/*")
|
|
566
|
+
* - updated_after/before: Temporal filtering (ISO timestamp or relative like "7d")
|
|
567
|
+
* - decided_by: Filter by agent names (OR relationship)
|
|
568
|
+
* - statuses: Multiple statuses (OR relationship)
|
|
569
|
+
* - search_text: Full-text search in value field
|
|
570
|
+
*
|
|
571
|
+
* @param params - Advanced search parameters with filtering, sorting, pagination
|
|
572
|
+
* @returns Filtered decisions with total count for pagination
|
|
573
|
+
*/
|
|
574
|
+
export function searchAdvanced(params = {}) {
|
|
575
|
+
const db = getDatabase();
|
|
576
|
+
try {
|
|
577
|
+
// Parse relative time to Unix timestamp
|
|
578
|
+
const parseRelativeTime = (relativeTime) => {
|
|
579
|
+
const match = relativeTime.match(/^(\d+)(m|h|d)$/);
|
|
580
|
+
if (!match) {
|
|
581
|
+
// Try parsing as ISO timestamp
|
|
582
|
+
const date = new Date(relativeTime);
|
|
583
|
+
if (isNaN(date.getTime())) {
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
return Math.floor(date.getTime() / 1000);
|
|
587
|
+
}
|
|
588
|
+
const value = parseInt(match[1], 10);
|
|
589
|
+
const unit = match[2];
|
|
590
|
+
const now = Math.floor(Date.now() / 1000);
|
|
591
|
+
switch (unit) {
|
|
592
|
+
case 'm': return now - (value * 60);
|
|
593
|
+
case 'h': return now - (value * 3600);
|
|
594
|
+
case 'd': return now - (value * 86400);
|
|
595
|
+
default: return null;
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
// Build base query using v_tagged_decisions view
|
|
599
|
+
let query = 'SELECT * FROM v_tagged_decisions WHERE 1=1';
|
|
600
|
+
const queryParams = [];
|
|
601
|
+
// Filter by layers (OR relationship)
|
|
602
|
+
if (params.layers && params.layers.length > 0) {
|
|
603
|
+
const layerConditions = params.layers.map(() => 'layer = ?').join(' OR ');
|
|
604
|
+
query += ` AND (${layerConditions})`;
|
|
605
|
+
queryParams.push(...params.layers);
|
|
606
|
+
}
|
|
607
|
+
// Filter by tags_all (AND relationship - must have ALL tags)
|
|
608
|
+
if (params.tags_all && params.tags_all.length > 0) {
|
|
609
|
+
for (const tag of params.tags_all) {
|
|
610
|
+
query += ' AND (tags LIKE ? OR tags = ?)';
|
|
611
|
+
queryParams.push(`%${tag}%`, tag);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
// Filter by tags_any (OR relationship - must have ANY tag)
|
|
615
|
+
if (params.tags_any && params.tags_any.length > 0) {
|
|
616
|
+
const tagConditions = params.tags_any.map(() => '(tags LIKE ? OR tags = ?)').join(' OR ');
|
|
617
|
+
query += ` AND (${tagConditions})`;
|
|
618
|
+
for (const tag of params.tags_any) {
|
|
619
|
+
queryParams.push(`%${tag}%`, tag);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
// Exclude tags
|
|
623
|
+
if (params.exclude_tags && params.exclude_tags.length > 0) {
|
|
624
|
+
for (const tag of params.exclude_tags) {
|
|
625
|
+
query += ' AND (tags IS NULL OR (tags NOT LIKE ? AND tags != ?))';
|
|
626
|
+
queryParams.push(`%${tag}%`, tag);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
// Filter by scopes with wildcard support
|
|
630
|
+
if (params.scopes && params.scopes.length > 0) {
|
|
631
|
+
const scopeConditions = [];
|
|
632
|
+
for (const scope of params.scopes) {
|
|
633
|
+
if (scope.includes('*')) {
|
|
634
|
+
// Wildcard pattern - convert to LIKE pattern
|
|
635
|
+
const likePattern = scope.replace(/\*/g, '%');
|
|
636
|
+
scopeConditions.push('(scopes LIKE ? OR scopes = ?)');
|
|
637
|
+
queryParams.push(`%${likePattern}%`, likePattern);
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
// Exact match
|
|
641
|
+
scopeConditions.push('(scopes LIKE ? OR scopes = ?)');
|
|
642
|
+
queryParams.push(`%${scope}%`, scope);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
query += ` AND (${scopeConditions.join(' OR ')})`;
|
|
646
|
+
}
|
|
647
|
+
// Temporal filtering - updated_after
|
|
648
|
+
if (params.updated_after) {
|
|
649
|
+
const timestamp = parseRelativeTime(params.updated_after);
|
|
650
|
+
if (timestamp !== null) {
|
|
651
|
+
query += ' AND (SELECT unixepoch(updated)) >= ?';
|
|
652
|
+
queryParams.push(timestamp);
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
throw new Error(`Invalid updated_after format: ${params.updated_after}. Use ISO timestamp or relative time like "7d", "2h", "30m"`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// Temporal filtering - updated_before
|
|
659
|
+
if (params.updated_before) {
|
|
660
|
+
const timestamp = parseRelativeTime(params.updated_before);
|
|
661
|
+
if (timestamp !== null) {
|
|
662
|
+
query += ' AND (SELECT unixepoch(updated)) <= ?';
|
|
663
|
+
queryParams.push(timestamp);
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
throw new Error(`Invalid updated_before format: ${params.updated_before}. Use ISO timestamp or relative time like "7d", "2h", "30m"`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
// Filter by decided_by (OR relationship)
|
|
670
|
+
if (params.decided_by && params.decided_by.length > 0) {
|
|
671
|
+
const agentConditions = params.decided_by.map(() => 'decided_by = ?').join(' OR ');
|
|
672
|
+
query += ` AND (${agentConditions})`;
|
|
673
|
+
queryParams.push(...params.decided_by);
|
|
674
|
+
}
|
|
675
|
+
// Filter by statuses (OR relationship)
|
|
676
|
+
if (params.statuses && params.statuses.length > 0) {
|
|
677
|
+
const statusConditions = params.statuses.map(() => 'status = ?').join(' OR ');
|
|
678
|
+
query += ` AND (${statusConditions})`;
|
|
679
|
+
queryParams.push(...params.statuses);
|
|
680
|
+
}
|
|
681
|
+
// Full-text search in value field
|
|
682
|
+
if (params.search_text) {
|
|
683
|
+
query += ' AND value LIKE ?';
|
|
684
|
+
queryParams.push(`%${params.search_text}%`);
|
|
685
|
+
}
|
|
686
|
+
// Count total matching records (before pagination)
|
|
687
|
+
const countQuery = query.replace('SELECT * FROM', 'SELECT COUNT(*) as total FROM');
|
|
688
|
+
const countStmt = db.prepare(countQuery);
|
|
689
|
+
const countResult = countStmt.get(...queryParams);
|
|
690
|
+
const totalCount = countResult.total;
|
|
691
|
+
// Sorting
|
|
692
|
+
const sortBy = params.sort_by || 'updated';
|
|
693
|
+
const sortOrder = params.sort_order || 'desc';
|
|
694
|
+
// Validate sort parameters
|
|
695
|
+
if (!['updated', 'key', 'version'].includes(sortBy)) {
|
|
696
|
+
throw new Error(`Invalid sort_by: ${sortBy}. Must be 'updated', 'key', or 'version'`);
|
|
697
|
+
}
|
|
698
|
+
if (!['asc', 'desc'].includes(sortOrder)) {
|
|
699
|
+
throw new Error(`Invalid sort_order: ${sortOrder}. Must be 'asc' or 'desc'`);
|
|
700
|
+
}
|
|
701
|
+
query += ` ORDER BY ${sortBy} ${sortOrder.toUpperCase()}`;
|
|
702
|
+
// Pagination
|
|
703
|
+
const limit = params.limit !== undefined ? params.limit : 20;
|
|
704
|
+
const offset = params.offset || 0;
|
|
705
|
+
// Validate pagination parameters
|
|
706
|
+
if (limit < 0 || limit > 1000) {
|
|
707
|
+
throw new Error('Parameter "limit" must be between 0 and 1000');
|
|
708
|
+
}
|
|
709
|
+
if (offset < 0) {
|
|
710
|
+
throw new Error('Parameter "offset" must be non-negative');
|
|
711
|
+
}
|
|
712
|
+
query += ' LIMIT ? OFFSET ?';
|
|
713
|
+
queryParams.push(limit, offset);
|
|
714
|
+
// Execute query
|
|
715
|
+
const stmt = db.prepare(query);
|
|
716
|
+
const rows = stmt.all(...queryParams);
|
|
717
|
+
return {
|
|
718
|
+
decisions: rows,
|
|
719
|
+
count: rows.length,
|
|
720
|
+
total_count: totalCount
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
catch (error) {
|
|
724
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
725
|
+
throw new Error(`Failed to execute advanced search: ${message}`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Set multiple decisions in a single batch operation (FR-005)
|
|
730
|
+
* Supports atomic (all succeed or all fail) and non-atomic modes
|
|
731
|
+
* Limit: 50 items per batch (constraint #3)
|
|
732
|
+
*
|
|
733
|
+
* @param params - Batch parameters with array of decisions and atomic flag
|
|
734
|
+
* @returns Response with success status and detailed results for each item
|
|
735
|
+
*/
|
|
736
|
+
export function setDecisionBatch(params) {
|
|
737
|
+
const db = getDatabase();
|
|
738
|
+
// Validate required parameters
|
|
739
|
+
if (!params.decisions || !Array.isArray(params.decisions)) {
|
|
740
|
+
throw new Error('Parameter "decisions" is required and must be an array');
|
|
741
|
+
}
|
|
742
|
+
// Enforce limit (constraint #3)
|
|
743
|
+
if (params.decisions.length === 0) {
|
|
744
|
+
throw new Error('Parameter "decisions" must contain at least one item');
|
|
745
|
+
}
|
|
746
|
+
if (params.decisions.length > 50) {
|
|
747
|
+
throw new Error('Batch operations are limited to 50 items maximum (constraint #3)');
|
|
748
|
+
}
|
|
749
|
+
const atomic = params.atomic !== undefined ? params.atomic : true;
|
|
750
|
+
const results = [];
|
|
751
|
+
let inserted = 0;
|
|
752
|
+
let failed = 0;
|
|
753
|
+
// Helper function to process a single decision
|
|
754
|
+
const processSingleDecision = (decision) => {
|
|
755
|
+
try {
|
|
756
|
+
const result = setDecisionInternal(decision, db);
|
|
757
|
+
results.push({
|
|
758
|
+
key: decision.key,
|
|
759
|
+
key_id: result.key_id,
|
|
760
|
+
version: result.version,
|
|
761
|
+
success: true
|
|
762
|
+
});
|
|
763
|
+
inserted++;
|
|
764
|
+
}
|
|
765
|
+
catch (error) {
|
|
766
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
767
|
+
results.push({
|
|
768
|
+
key: decision.key,
|
|
769
|
+
success: false,
|
|
770
|
+
error: errorMessage
|
|
771
|
+
});
|
|
772
|
+
failed++;
|
|
773
|
+
// In atomic mode, throw immediately to trigger rollback
|
|
774
|
+
if (atomic) {
|
|
775
|
+
throw error;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
try {
|
|
780
|
+
if (atomic) {
|
|
781
|
+
// Atomic mode: use transaction, all succeed or all fail
|
|
782
|
+
return transaction(db, () => {
|
|
783
|
+
for (const decision of params.decisions) {
|
|
784
|
+
processSingleDecision(decision);
|
|
785
|
+
}
|
|
786
|
+
return {
|
|
787
|
+
success: failed === 0,
|
|
788
|
+
inserted,
|
|
789
|
+
failed,
|
|
790
|
+
results
|
|
791
|
+
};
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
// Non-atomic mode: process all, return individual results
|
|
796
|
+
for (const decision of params.decisions) {
|
|
797
|
+
processSingleDecision(decision);
|
|
798
|
+
}
|
|
799
|
+
return {
|
|
800
|
+
success: failed === 0,
|
|
801
|
+
inserted,
|
|
802
|
+
failed,
|
|
803
|
+
results
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
catch (error) {
|
|
808
|
+
if (atomic) {
|
|
809
|
+
// In atomic mode, if any error occurred, all failed
|
|
810
|
+
throw new Error(`Batch operation failed (atomic mode): ${error instanceof Error ? error.message : String(error)}`);
|
|
811
|
+
}
|
|
812
|
+
else {
|
|
813
|
+
// In non-atomic mode, return partial results
|
|
814
|
+
return {
|
|
815
|
+
success: false,
|
|
816
|
+
inserted,
|
|
817
|
+
failed,
|
|
818
|
+
results
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Check for updates since a given timestamp (FR-003 Phase A)
|
|
825
|
+
* Lightweight polling mechanism using COUNT queries
|
|
826
|
+
* Token cost: ~5-10 tokens per check
|
|
827
|
+
*
|
|
828
|
+
* @param params - Agent name and since_timestamp (ISO 8601)
|
|
829
|
+
* @returns Boolean flag and counts for decisions, messages, files
|
|
830
|
+
*/
|
|
831
|
+
export function hasUpdates(params) {
|
|
832
|
+
const db = getDatabase();
|
|
833
|
+
// Validate required parameters
|
|
834
|
+
if (!params.agent_name || params.agent_name.trim() === '') {
|
|
835
|
+
throw new Error('Parameter "agent_name" is required and cannot be empty');
|
|
836
|
+
}
|
|
837
|
+
if (!params.since_timestamp || params.since_timestamp.trim() === '') {
|
|
838
|
+
throw new Error('Parameter "since_timestamp" is required and cannot be empty');
|
|
839
|
+
}
|
|
840
|
+
try {
|
|
841
|
+
// Parse ISO timestamp to Unix epoch
|
|
842
|
+
const sinceDate = new Date(params.since_timestamp);
|
|
843
|
+
if (isNaN(sinceDate.getTime())) {
|
|
844
|
+
throw new Error(`Invalid since_timestamp format: ${params.since_timestamp}. Use ISO 8601 format (e.g., "2025-10-14T08:00:00Z")`);
|
|
845
|
+
}
|
|
846
|
+
const sinceTs = Math.floor(sinceDate.getTime() / 1000);
|
|
847
|
+
// Count decisions updated since timestamp (both string and numeric tables)
|
|
848
|
+
const decisionCountStmt = db.prepare(`
|
|
849
|
+
SELECT COUNT(*) as count FROM (
|
|
850
|
+
SELECT ts FROM t_decisions WHERE ts > ?
|
|
851
|
+
UNION ALL
|
|
852
|
+
SELECT ts FROM t_decisions_numeric WHERE ts > ?
|
|
853
|
+
)
|
|
854
|
+
`);
|
|
855
|
+
const decisionResult = decisionCountStmt.get(sinceTs, sinceTs);
|
|
856
|
+
const decisionsCount = decisionResult.count;
|
|
857
|
+
// Get agent_id for the requesting agent
|
|
858
|
+
const agentResult = db.prepare('SELECT id FROM m_agents WHERE name = ?').get(params.agent_name);
|
|
859
|
+
// Count messages for the agent (received messages - to_agent_id matches OR broadcast messages)
|
|
860
|
+
let messagesCount = 0;
|
|
861
|
+
if (agentResult) {
|
|
862
|
+
const agentId = agentResult.id;
|
|
863
|
+
const messageCountStmt = db.prepare(`
|
|
864
|
+
SELECT COUNT(*) as count FROM t_agent_messages
|
|
865
|
+
WHERE ts > ? AND (to_agent_id = ? OR to_agent_id IS NULL)
|
|
866
|
+
`);
|
|
867
|
+
const messageResult = messageCountStmt.get(sinceTs, agentId);
|
|
868
|
+
messagesCount = messageResult.count;
|
|
869
|
+
}
|
|
870
|
+
// Count file changes since timestamp
|
|
871
|
+
const fileCountStmt = db.prepare(`
|
|
872
|
+
SELECT COUNT(*) as count FROM t_file_changes WHERE ts > ?
|
|
873
|
+
`);
|
|
874
|
+
const fileResult = fileCountStmt.get(sinceTs);
|
|
875
|
+
const filesCount = fileResult.count;
|
|
876
|
+
// Determine if there are any updates
|
|
877
|
+
const hasUpdates = decisionsCount > 0 || messagesCount > 0 || filesCount > 0;
|
|
878
|
+
return {
|
|
879
|
+
has_updates: hasUpdates,
|
|
880
|
+
counts: {
|
|
881
|
+
decisions: decisionsCount,
|
|
882
|
+
messages: messagesCount,
|
|
883
|
+
files: filesCount
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
catch (error) {
|
|
888
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
889
|
+
throw new Error(`Failed to check for updates: ${message}`);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Set decision from template with defaults and required field validation (FR-006)
|
|
894
|
+
* Applies template defaults while allowing overrides
|
|
895
|
+
* Validates required fields if template specifies any
|
|
896
|
+
*
|
|
897
|
+
* @param params - Template name, key, value, and optional overrides
|
|
898
|
+
* @returns Response with success status and applied defaults metadata
|
|
899
|
+
*/
|
|
900
|
+
export function setFromTemplate(params) {
|
|
901
|
+
const db = getDatabase();
|
|
902
|
+
// Validate required parameters
|
|
903
|
+
if (!params.template || params.template.trim() === '') {
|
|
904
|
+
throw new Error('Parameter "template" is required and cannot be empty');
|
|
905
|
+
}
|
|
906
|
+
if (!params.key || params.key.trim() === '') {
|
|
907
|
+
throw new Error('Parameter "key" is required and cannot be empty');
|
|
908
|
+
}
|
|
909
|
+
if (params.value === undefined || params.value === null) {
|
|
910
|
+
throw new Error('Parameter "value" is required');
|
|
911
|
+
}
|
|
912
|
+
try {
|
|
913
|
+
// Get template
|
|
914
|
+
const templateRow = db.prepare('SELECT * FROM t_decision_templates WHERE name = ?').get(params.template);
|
|
915
|
+
if (!templateRow) {
|
|
916
|
+
throw new Error(`Template not found: ${params.template}`);
|
|
917
|
+
}
|
|
918
|
+
// Parse template defaults
|
|
919
|
+
const defaults = JSON.parse(templateRow.defaults);
|
|
920
|
+
// Parse required fields
|
|
921
|
+
const requiredFields = templateRow.required_fields ? JSON.parse(templateRow.required_fields) : null;
|
|
922
|
+
// Validate required fields if specified
|
|
923
|
+
if (requiredFields && requiredFields.length > 0) {
|
|
924
|
+
for (const field of requiredFields) {
|
|
925
|
+
if (!(field in params) || params[field] === undefined || params[field] === null) {
|
|
926
|
+
throw new Error(`Template "${params.template}" requires field: ${field}`);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
// Build decision params with template defaults (overridable)
|
|
931
|
+
const appliedDefaults = {};
|
|
932
|
+
const decisionParams = {
|
|
933
|
+
key: params.key,
|
|
934
|
+
value: params.value,
|
|
935
|
+
agent: params.agent,
|
|
936
|
+
layer: params.layer || defaults.layer,
|
|
937
|
+
version: params.version,
|
|
938
|
+
status: params.status || defaults.status,
|
|
939
|
+
tags: params.tags || defaults.tags,
|
|
940
|
+
scopes: params.scopes
|
|
941
|
+
};
|
|
942
|
+
// Track what defaults were applied
|
|
943
|
+
if (!params.layer && defaults.layer) {
|
|
944
|
+
appliedDefaults.layer = defaults.layer;
|
|
945
|
+
}
|
|
946
|
+
if (!params.tags && defaults.tags) {
|
|
947
|
+
appliedDefaults.tags = defaults.tags;
|
|
948
|
+
}
|
|
949
|
+
if (!params.status && defaults.status) {
|
|
950
|
+
appliedDefaults.status = defaults.status;
|
|
951
|
+
}
|
|
952
|
+
// Call setDecision with merged params
|
|
953
|
+
const result = setDecision(decisionParams);
|
|
954
|
+
return {
|
|
955
|
+
success: result.success,
|
|
956
|
+
key: result.key,
|
|
957
|
+
key_id: result.key_id,
|
|
958
|
+
version: result.version,
|
|
959
|
+
template_used: params.template,
|
|
960
|
+
applied_defaults: appliedDefaults,
|
|
961
|
+
message: `Decision "${params.key}" set successfully using template "${params.template}"`
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
catch (error) {
|
|
965
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
966
|
+
throw new Error(`Failed to set decision from template: ${message}`);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Create a new decision template (FR-006)
|
|
971
|
+
* Defines reusable defaults and required fields for decisions
|
|
972
|
+
*
|
|
973
|
+
* @param params - Template name, defaults, required fields, and creator
|
|
974
|
+
* @returns Response with success status and template ID
|
|
975
|
+
*/
|
|
976
|
+
export function createTemplate(params) {
|
|
977
|
+
const db = getDatabase();
|
|
978
|
+
// Validate required parameters
|
|
979
|
+
if (!params.name || params.name.trim() === '') {
|
|
980
|
+
throw new Error('Parameter "name" is required and cannot be empty');
|
|
981
|
+
}
|
|
982
|
+
if (!params.defaults || typeof params.defaults !== 'object') {
|
|
983
|
+
throw new Error('Parameter "defaults" is required and must be an object');
|
|
984
|
+
}
|
|
985
|
+
try {
|
|
986
|
+
return transaction(db, () => {
|
|
987
|
+
// Validate layer if provided in defaults
|
|
988
|
+
if (params.defaults.layer) {
|
|
989
|
+
const layerId = getLayerId(db, params.defaults.layer);
|
|
990
|
+
if (layerId === null) {
|
|
991
|
+
throw new Error(`Invalid layer in defaults: ${params.defaults.layer}. Must be one of: presentation, business, data, infrastructure, cross-cutting`);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
// Validate status if provided in defaults
|
|
995
|
+
if (params.defaults.status && !STRING_TO_STATUS[params.defaults.status]) {
|
|
996
|
+
throw new Error(`Invalid status in defaults: ${params.defaults.status}. Must be 'active', 'deprecated', or 'draft'`);
|
|
997
|
+
}
|
|
998
|
+
// Get or create agent if creator specified
|
|
999
|
+
let createdById = null;
|
|
1000
|
+
if (params.created_by) {
|
|
1001
|
+
createdById = getOrCreateAgent(db, params.created_by);
|
|
1002
|
+
}
|
|
1003
|
+
// Serialize defaults and required fields
|
|
1004
|
+
const defaultsJson = JSON.stringify(params.defaults);
|
|
1005
|
+
const requiredFieldsJson = params.required_fields ? JSON.stringify(params.required_fields) : null;
|
|
1006
|
+
// Insert template
|
|
1007
|
+
const stmt = db.prepare(`
|
|
1008
|
+
INSERT INTO t_decision_templates (name, defaults, required_fields, created_by)
|
|
1009
|
+
VALUES (?, ?, ?, ?)
|
|
1010
|
+
`);
|
|
1011
|
+
const info = stmt.run(params.name, defaultsJson, requiredFieldsJson, createdById);
|
|
1012
|
+
return {
|
|
1013
|
+
success: true,
|
|
1014
|
+
template_id: info.lastInsertRowid,
|
|
1015
|
+
template_name: params.name,
|
|
1016
|
+
message: `Template "${params.name}" created successfully`
|
|
1017
|
+
};
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
catch (error) {
|
|
1021
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1022
|
+
throw new Error(`Failed to create template: ${message}`);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* List all available decision templates (FR-006)
|
|
1027
|
+
* Returns all templates with their defaults and metadata
|
|
1028
|
+
*
|
|
1029
|
+
* @param params - No parameters required
|
|
1030
|
+
* @returns Array of all templates with parsed JSON fields
|
|
1031
|
+
*/
|
|
1032
|
+
export function listTemplates(params = {}) {
|
|
1033
|
+
const db = getDatabase();
|
|
1034
|
+
try {
|
|
1035
|
+
const stmt = db.prepare(`
|
|
1036
|
+
SELECT
|
|
1037
|
+
t.id,
|
|
1038
|
+
t.name,
|
|
1039
|
+
t.defaults,
|
|
1040
|
+
t.required_fields,
|
|
1041
|
+
a.name as created_by,
|
|
1042
|
+
datetime(t.ts, 'unixepoch') as created_at
|
|
1043
|
+
FROM t_decision_templates t
|
|
1044
|
+
LEFT JOIN m_agents a ON t.created_by = a.id
|
|
1045
|
+
ORDER BY t.name ASC
|
|
1046
|
+
`);
|
|
1047
|
+
const rows = stmt.all();
|
|
1048
|
+
// Parse JSON fields
|
|
1049
|
+
const templates = rows.map(row => ({
|
|
1050
|
+
id: row.id,
|
|
1051
|
+
name: row.name,
|
|
1052
|
+
defaults: JSON.parse(row.defaults),
|
|
1053
|
+
required_fields: row.required_fields ? JSON.parse(row.required_fields) : null,
|
|
1054
|
+
created_by: row.created_by,
|
|
1055
|
+
created_at: row.created_at
|
|
1056
|
+
}));
|
|
1057
|
+
return {
|
|
1058
|
+
templates: templates,
|
|
1059
|
+
count: templates.length
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
catch (error) {
|
|
1063
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1064
|
+
throw new Error(`Failed to list templates: ${message}`);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
442
1067
|
//# sourceMappingURL=context.js.map
|