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