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.
Files changed (42) hide show
  1. package/CHANGELOG.md +222 -0
  2. package/README.md +314 -209
  3. package/assets/schema.sql +122 -36
  4. package/dist/cli.d.ts +7 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +312 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/database.d.ts.map +1 -1
  9. package/dist/database.js +18 -0
  10. package/dist/database.js.map +1 -1
  11. package/dist/index.js +136 -42
  12. package/dist/index.js.map +1 -1
  13. package/dist/migrations/add-v2.1.0-features.d.ts +29 -0
  14. package/dist/migrations/add-v2.1.0-features.d.ts.map +1 -0
  15. package/dist/migrations/add-v2.1.0-features.js +198 -0
  16. package/dist/migrations/add-v2.1.0-features.js.map +1 -0
  17. package/dist/schema.d.ts.map +1 -1
  18. package/dist/schema.js +5 -0
  19. package/dist/schema.js.map +1 -1
  20. package/dist/tools/context.d.ts +91 -1
  21. package/dist/tools/context.d.ts.map +1 -1
  22. package/dist/tools/context.js +695 -70
  23. package/dist/tools/context.js.map +1 -1
  24. package/dist/tools/files.d.ts +10 -1
  25. package/dist/tools/files.d.ts.map +1 -1
  26. package/dist/tools/files.js +98 -1
  27. package/dist/tools/files.js.map +1 -1
  28. package/dist/tools/messaging.d.ts +10 -1
  29. package/dist/tools/messaging.d.ts.map +1 -1
  30. package/dist/tools/messaging.js +107 -1
  31. package/dist/tools/messaging.js.map +1 -1
  32. package/dist/tools/utils.d.ts +9 -1
  33. package/dist/tools/utils.d.ts.map +1 -1
  34. package/dist/tools/utils.js +115 -0
  35. package/dist/tools/utils.js.map +1 -1
  36. package/dist/types.d.ts +196 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/utils/cleanup.d.ts +12 -1
  39. package/dist/utils/cleanup.d.ts.map +1 -1
  40. package/dist/utils/cleanup.js +20 -3
  41. package/dist/utils/cleanup.js.map +1 -1
  42. package/package.json +4 -2
@@ -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 or update a decision in the context
9
- * Auto-detects numeric vs string values and routes to appropriate table
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
- export function setDecision(params) {
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
- // Get or create master records
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