sqlew 3.5.3 → 3.6.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 (227) hide show
  1. package/CHANGELOG.md +247 -1772
  2. package/README.md +70 -304
  3. package/assets/config.example.toml +97 -0
  4. package/dist/adapters/index.d.ts +11 -0
  5. package/dist/adapters/index.d.ts.map +1 -0
  6. package/dist/adapters/index.js +21 -0
  7. package/dist/adapters/index.js.map +1 -0
  8. package/dist/adapters/mysql-adapter.d.ts +31 -0
  9. package/dist/adapters/mysql-adapter.d.ts.map +1 -0
  10. package/dist/adapters/mysql-adapter.js +63 -0
  11. package/dist/adapters/mysql-adapter.js.map +1 -0
  12. package/dist/adapters/postgresql-adapter.d.ts +31 -0
  13. package/dist/adapters/postgresql-adapter.d.ts.map +1 -0
  14. package/dist/adapters/postgresql-adapter.js +63 -0
  15. package/dist/adapters/postgresql-adapter.js.map +1 -0
  16. package/dist/adapters/sqlite-adapter.d.ts +37 -0
  17. package/dist/adapters/sqlite-adapter.d.ts.map +1 -0
  18. package/dist/adapters/sqlite-adapter.js +129 -0
  19. package/dist/adapters/sqlite-adapter.js.map +1 -0
  20. package/dist/adapters/types.d.ts +33 -0
  21. package/dist/adapters/types.d.ts.map +1 -0
  22. package/dist/adapters/types.js +2 -0
  23. package/dist/adapters/types.js.map +1 -0
  24. package/dist/cli.js +55 -54
  25. package/dist/cli.js.map +1 -1
  26. package/dist/config/example-generator.d.ts +11 -0
  27. package/dist/config/example-generator.d.ts.map +1 -0
  28. package/dist/config/example-generator.js +48 -0
  29. package/dist/config/example-generator.js.map +1 -0
  30. package/dist/config/loader.d.ts.map +1 -1
  31. package/dist/config/loader.js +4 -0
  32. package/dist/config/loader.js.map +1 -1
  33. package/dist/config/types.d.ts +9 -0
  34. package/dist/config/types.d.ts.map +1 -1
  35. package/dist/config/types.js.map +1 -1
  36. package/dist/database.d.ts +44 -122
  37. package/dist/database.d.ts.map +1 -1
  38. package/dist/database.js +145 -416
  39. package/dist/database.js.map +1 -1
  40. package/dist/index.js +215 -185
  41. package/dist/index.js.map +1 -1
  42. package/dist/knexfile.d.ts +6 -0
  43. package/dist/knexfile.d.ts.map +1 -0
  44. package/dist/knexfile.js +85 -0
  45. package/dist/knexfile.js.map +1 -0
  46. package/dist/migrations/add-help-system-tables.d.ts +35 -0
  47. package/dist/migrations/add-help-system-tables.d.ts.map +1 -0
  48. package/dist/migrations/add-help-system-tables.js +206 -0
  49. package/dist/migrations/add-help-system-tables.js.map +1 -0
  50. package/dist/migrations/add-token-tracking.d.ts +28 -0
  51. package/dist/migrations/add-token-tracking.d.ts.map +1 -0
  52. package/dist/migrations/add-token-tracking.js +108 -0
  53. package/dist/migrations/add-token-tracking.js.map +1 -0
  54. package/dist/migrations/index.d.ts +25 -12
  55. package/dist/migrations/index.d.ts.map +1 -1
  56. package/dist/migrations/index.js +147 -20
  57. package/dist/migrations/index.js.map +1 -1
  58. package/dist/migrations/knex/20251025020452_create_master_tables.d.ts +4 -0
  59. package/dist/migrations/knex/20251025020452_create_master_tables.d.ts.map +1 -0
  60. package/dist/migrations/knex/20251025020452_create_master_tables.js +65 -0
  61. package/dist/migrations/knex/20251025020452_create_master_tables.js.map +1 -0
  62. package/dist/migrations/knex/20251025021152_create_transaction_tables.d.ts +4 -0
  63. package/dist/migrations/knex/20251025021152_create_transaction_tables.d.ts.map +1 -0
  64. package/dist/migrations/knex/20251025021152_create_transaction_tables.js +235 -0
  65. package/dist/migrations/knex/20251025021152_create_transaction_tables.js.map +1 -0
  66. package/dist/migrations/knex/20251025021351_create_indexes.d.ts +4 -0
  67. package/dist/migrations/knex/20251025021351_create_indexes.d.ts.map +1 -0
  68. package/dist/migrations/knex/20251025021351_create_indexes.js +62 -0
  69. package/dist/migrations/knex/20251025021351_create_indexes.js.map +1 -0
  70. package/dist/migrations/knex/20251025021416_seed_master_data.d.ts +4 -0
  71. package/dist/migrations/knex/20251025021416_seed_master_data.d.ts.map +1 -0
  72. package/dist/migrations/knex/20251025021416_seed_master_data.js +58 -0
  73. package/dist/migrations/knex/20251025021416_seed_master_data.js.map +1 -0
  74. package/dist/migrations/knex/20251025070349_create_views.d.ts +4 -0
  75. package/dist/migrations/knex/20251025070349_create_views.d.ts.map +1 -0
  76. package/dist/migrations/knex/20251025070349_create_views.js +143 -0
  77. package/dist/migrations/knex/20251025070349_create_views.js.map +1 -0
  78. package/dist/migrations/knex/20251025081221_add_link_type_to_task_decision_links.d.ts +4 -0
  79. package/dist/migrations/knex/20251025081221_add_link_type_to_task_decision_links.d.ts.map +1 -0
  80. package/dist/migrations/knex/20251025081221_add_link_type_to_task_decision_links.js +15 -0
  81. package/dist/migrations/knex/20251025081221_add_link_type_to_task_decision_links.js.map +1 -0
  82. package/dist/migrations/knex/20251025082220_fix_task_dependencies_columns.d.ts +8 -0
  83. package/dist/migrations/knex/20251025082220_fix_task_dependencies_columns.d.ts.map +1 -0
  84. package/dist/migrations/knex/20251025082220_fix_task_dependencies_columns.js +12 -0
  85. package/dist/migrations/knex/20251025082220_fix_task_dependencies_columns.js.map +1 -0
  86. package/dist/migrations/knex/20251025090000_create_help_system_tables.d.ts +19 -0
  87. package/dist/migrations/knex/20251025090000_create_help_system_tables.d.ts.map +1 -0
  88. package/dist/migrations/knex/20251025090000_create_help_system_tables.js +115 -0
  89. package/dist/migrations/knex/20251025090000_create_help_system_tables.js.map +1 -0
  90. package/dist/migrations/knex/20251025090100_seed_help_categories_and_use_cases.d.ts +13 -0
  91. package/dist/migrations/knex/20251025090100_seed_help_categories_and_use_cases.d.ts.map +1 -0
  92. package/dist/migrations/knex/20251025090100_seed_help_categories_and_use_cases.js +377 -0
  93. package/dist/migrations/knex/20251025090100_seed_help_categories_and_use_cases.js.map +1 -0
  94. package/dist/migrations/knex/20251025100000_seed_help_metadata.d.ts +15 -0
  95. package/dist/migrations/knex/20251025100000_seed_help_metadata.d.ts.map +1 -0
  96. package/dist/migrations/knex/20251025100000_seed_help_metadata.js +253 -0
  97. package/dist/migrations/knex/20251025100000_seed_help_metadata.js.map +1 -0
  98. package/dist/migrations/knex/20251025100100_seed_remaining_use_cases.d.ts +16 -0
  99. package/dist/migrations/knex/20251025100100_seed_remaining_use_cases.d.ts.map +1 -0
  100. package/dist/migrations/knex/20251025100100_seed_remaining_use_cases.js +276 -0
  101. package/dist/migrations/knex/20251025100100_seed_remaining_use_cases.js.map +1 -0
  102. package/dist/migrations/knex/20251025120000_add_cascade_to_task_dependencies.d.ts +8 -0
  103. package/dist/migrations/knex/20251025120000_add_cascade_to_task_dependencies.d.ts.map +1 -0
  104. package/dist/migrations/knex/20251025120000_add_cascade_to_task_dependencies.js +64 -0
  105. package/dist/migrations/knex/20251025120000_add_cascade_to_task_dependencies.js.map +1 -0
  106. package/dist/migrations/seed-help-data.d.ts +48 -0
  107. package/dist/migrations/seed-help-data.d.ts.map +1 -0
  108. package/dist/migrations/seed-help-data.js +1466 -0
  109. package/dist/migrations/seed-help-data.js.map +1 -0
  110. package/dist/migrations/seed-tool-metadata.d.ts +24 -0
  111. package/dist/migrations/seed-tool-metadata.d.ts.map +1 -0
  112. package/dist/migrations/seed-tool-metadata.js +392 -0
  113. package/dist/migrations/seed-tool-metadata.js.map +1 -0
  114. package/dist/migrations/v3.6.0-help-system-refactor.d.ts +46 -0
  115. package/dist/migrations/v3.6.0-help-system-refactor.d.ts.map +1 -0
  116. package/dist/migrations/v3.6.0-help-system-refactor.js +223 -0
  117. package/dist/migrations/v3.6.0-help-system-refactor.js.map +1 -0
  118. package/dist/schema.d.ts.map +1 -1
  119. package/dist/schema.js +2 -0
  120. package/dist/schema.js.map +1 -1
  121. package/dist/tests/git-aware-completion.test.js +89 -70
  122. package/dist/tests/git-aware-completion.test.js.map +1 -1
  123. package/dist/tests/help-system.test.d.ts +23 -0
  124. package/dist/tests/help-system.test.d.ts.map +1 -0
  125. package/dist/tests/help-system.test.js +374 -0
  126. package/dist/tests/help-system.test.js.map +1 -0
  127. package/dist/tests/tasks.auto-pruning-decision-link.test.js +92 -78
  128. package/dist/tests/tasks.auto-pruning-decision-link.test.js.map +1 -1
  129. package/dist/tests/tasks.auto-pruning-partial.test.js +106 -95
  130. package/dist/tests/tasks.auto-pruning-partial.test.js.map +1 -1
  131. package/dist/tests/tasks.auto-pruning-persistence.test.js +115 -97
  132. package/dist/tests/tasks.auto-pruning-persistence.test.js.map +1 -1
  133. package/dist/tests/tasks.auto-pruning-safety.test.js +124 -103
  134. package/dist/tests/tasks.auto-pruning-safety.test.js.map +1 -1
  135. package/dist/tests/tasks.dependencies.test.js +338 -307
  136. package/dist/tests/tasks.dependencies.test.js.map +1 -1
  137. package/dist/tests/tasks.link-file-backward-compat.test.js +116 -104
  138. package/dist/tests/tasks.link-file-backward-compat.test.js.map +1 -1
  139. package/dist/tests/tasks.watch-files-action.test.js +122 -101
  140. package/dist/tests/tasks.watch-files-action.test.js.map +1 -1
  141. package/dist/tests/tasks.watch-files-parameter.test.js +105 -94
  142. package/dist/tests/tasks.watch-files-parameter.test.js.map +1 -1
  143. package/dist/tests/two-step-git-completion.test.js +176 -133
  144. package/dist/tests/two-step-git-completion.test.js.map +1 -1
  145. package/dist/tests/vcs-staging.test.js +1 -1
  146. package/dist/tests/vcs-staging.test.js.map +1 -1
  147. package/dist/tools/config.d.ts +9 -6
  148. package/dist/tools/config.d.ts.map +1 -1
  149. package/dist/tools/config.js +16 -14
  150. package/dist/tools/config.js.map +1 -1
  151. package/dist/tools/constraints.d.ts +10 -7
  152. package/dist/tools/constraints.d.ts.map +1 -1
  153. package/dist/tools/constraints.js +66 -48
  154. package/dist/tools/constraints.js.map +1 -1
  155. package/dist/tools/context.d.ts +36 -33
  156. package/dist/tools/context.d.ts.map +1 -1
  157. package/dist/tools/context.js +374 -330
  158. package/dist/tools/context.js.map +1 -1
  159. package/dist/tools/files.d.ts +12 -9
  160. package/dist/tools/files.d.ts.map +1 -1
  161. package/dist/tools/files.js +173 -95
  162. package/dist/tools/files.js.map +1 -1
  163. package/dist/tools/help-queries.d.ts +130 -0
  164. package/dist/tools/help-queries.d.ts.map +1 -0
  165. package/dist/tools/help-queries.js +393 -0
  166. package/dist/tools/help-queries.js.map +1 -0
  167. package/dist/tools/messaging.d.ts +14 -11
  168. package/dist/tools/messaging.d.ts.map +1 -1
  169. package/dist/tools/messaging.js +217 -133
  170. package/dist/tools/messaging.js.map +1 -1
  171. package/dist/tools/tasks.d.ts +18 -16
  172. package/dist/tools/tasks.d.ts.map +1 -1
  173. package/dist/tools/tasks.js +513 -439
  174. package/dist/tools/tasks.js.map +1 -1
  175. package/dist/tools/utils.d.ts +14 -11
  176. package/dist/tools/utils.d.ts.map +1 -1
  177. package/dist/tools/utils.js +86 -121
  178. package/dist/tools/utils.js.map +1 -1
  179. package/dist/utils/activity-logging.d.ts +114 -0
  180. package/dist/utils/activity-logging.d.ts.map +1 -0
  181. package/dist/utils/activity-logging.js +162 -0
  182. package/dist/utils/activity-logging.js.map +1 -0
  183. package/dist/utils/batch.d.ts +2 -2
  184. package/dist/utils/batch.d.ts.map +1 -1
  185. package/dist/utils/batch.js +8 -8
  186. package/dist/utils/batch.js.map +1 -1
  187. package/dist/utils/cleanup.d.ts +21 -13
  188. package/dist/utils/cleanup.d.ts.map +1 -1
  189. package/dist/utils/cleanup.js +31 -24
  190. package/dist/utils/cleanup.js.map +1 -1
  191. package/dist/utils/debug-logger.d.ts +44 -0
  192. package/dist/utils/debug-logger.d.ts.map +1 -0
  193. package/dist/utils/debug-logger.js +116 -0
  194. package/dist/utils/debug-logger.js.map +1 -0
  195. package/dist/utils/help-tracking.d.ts +55 -0
  196. package/dist/utils/help-tracking.d.ts.map +1 -0
  197. package/dist/utils/help-tracking.js +88 -0
  198. package/dist/utils/help-tracking.js.map +1 -0
  199. package/dist/utils/retention.d.ts +7 -7
  200. package/dist/utils/retention.d.ts.map +1 -1
  201. package/dist/utils/retention.js +12 -12
  202. package/dist/utils/retention.js.map +1 -1
  203. package/dist/utils/task-stale-detection.d.ts +15 -13
  204. package/dist/utils/task-stale-detection.d.ts.map +1 -1
  205. package/dist/utils/task-stale-detection.js +100 -302
  206. package/dist/utils/task-stale-detection.js.map +1 -1
  207. package/dist/utils/token-estimation.d.ts +72 -0
  208. package/dist/utils/token-estimation.d.ts.map +1 -0
  209. package/dist/utils/token-estimation.js +71 -0
  210. package/dist/utils/token-estimation.js.map +1 -0
  211. package/dist/utils/token-logging.d.ts +48 -0
  212. package/dist/utils/token-logging.d.ts.map +1 -0
  213. package/dist/utils/token-logging.js +112 -0
  214. package/dist/utils/token-logging.js.map +1 -0
  215. package/dist/utils/view-queries.d.ts +34 -0
  216. package/dist/utils/view-queries.d.ts.map +1 -0
  217. package/dist/utils/view-queries.js +192 -0
  218. package/dist/utils/view-queries.js.map +1 -0
  219. package/dist/watcher/file-watcher.d.ts.map +1 -1
  220. package/dist/watcher/file-watcher.js +25 -11
  221. package/dist/watcher/file-watcher.js.map +1 -1
  222. package/docs/BEST_PRACTICES.md +56 -448
  223. package/docs/MIGRATION_v3.6.0.md +170 -0
  224. package/docs/SHARED_CONCEPTS.md +63 -208
  225. package/docs/TASK_OVERVIEW.md +2 -2
  226. package/docs/TOOL_SELECTION.md +41 -248
  227. package/package.json +16 -4
@@ -1,20 +1,24 @@
1
1
  /**
2
2
  * Context management tools for MCP Shared Context Server
3
3
  * Implements set_decision, get_context, and get_decision tools
4
+ *
5
+ * CONVERTED: Using Knex.js with DatabaseAdapter (async/await)
4
6
  */
5
- import { getDatabase, getOrCreateAgent, getOrCreateContextKey, getOrCreateTag, getOrCreateScope, getLayerId, transaction, addDecisionContext as dbAddDecisionContext, getDecisionWithContext as dbGetDecisionWithContext, listDecisionContexts as dbListDecisionContexts } from '../database.js';
7
+ import { getAdapter, getOrCreateAgent, getOrCreateContextKey, getOrCreateTag, getOrCreateScope, getLayerId, addDecisionContext as dbAddDecisionContext, getDecisionWithContext as dbGetDecisionWithContext, listDecisionContexts as dbListDecisionContexts } from '../database.js';
6
8
  import { STRING_TO_STATUS, DEFAULT_VERSION, DEFAULT_STATUS } from '../constants.js';
7
- import { processBatch } from '../utils/batch.js';
8
- import { validateRequired, validateStatus, validateLayer } from '../utils/validators.js';
9
+ import { validateRequired, validateStatus } from '../utils/validators.js';
10
+ import { logDecisionSet, logDecisionUpdate, recordDecisionHistory } from '../utils/activity-logging.js';
9
11
  /**
10
12
  * Internal helper: Set decision without wrapping in transaction
11
13
  * Used by setDecision (with transaction) and setDecisionBatch (manages its own transaction)
12
14
  *
13
15
  * @param params - Decision parameters
14
- * @param db - Database instance
16
+ * @param adapter - Database adapter instance
17
+ * @param trx - Optional transaction
15
18
  * @returns Response with success status and metadata
16
19
  */
17
- function setDecisionInternal(params, db) {
20
+ async function setDecisionInternal(params, adapter, trx) {
21
+ const knex = trx || adapter.getKnex();
18
22
  // Validate required parameters
19
23
  const trimmedKey = validateRequired(params.key, 'key');
20
24
  if (params.value === undefined || params.value === null) {
@@ -34,64 +38,85 @@ function setDecisionInternal(params, db) {
34
38
  // Validate layer if provided
35
39
  let layerId = null;
36
40
  if (params.layer) {
37
- layerId = validateLayer(db, params.layer);
41
+ const validLayers = ['presentation', 'business', 'data', 'infrastructure', 'cross-cutting'];
42
+ if (!validLayers.includes(params.layer)) {
43
+ throw new Error(`Invalid layer. Must be one of: ${validLayers.join(', ')}`);
44
+ }
45
+ layerId = await getLayerId(adapter, params.layer, trx);
46
+ if (layerId === null) {
47
+ throw new Error(`Layer not found in database: ${params.layer}`);
48
+ }
38
49
  }
39
50
  // Get or create master records
40
- const agentId = getOrCreateAgent(db, agentName);
41
- const keyId = getOrCreateContextKey(db, params.key);
51
+ const agentId = await getOrCreateAgent(adapter, agentName, trx);
52
+ const keyId = await getOrCreateContextKey(adapter, params.key, trx);
42
53
  // Current timestamp
43
54
  const ts = Math.floor(Date.now() / 1000);
55
+ // Check if decision already exists for activity logging
56
+ const existingDecision = await knex(isNumeric ? 't_decisions_numeric' : 't_decisions')
57
+ .where({ key_id: keyId })
58
+ .first();
44
59
  // Insert or update decision based on value type
45
- if (isNumeric) {
46
- // Numeric decision
47
- const stmt = db.prepare(`
48
- INSERT INTO t_decisions_numeric (key_id, value, agent_id, layer_id, version, status, ts)
49
- VALUES (?, ?, ?, ?, ?, ?, ?)
50
- ON CONFLICT(key_id) DO UPDATE SET
51
- value = excluded.value,
52
- agent_id = excluded.agent_id,
53
- layer_id = excluded.layer_id,
54
- version = excluded.version,
55
- status = excluded.status,
56
- ts = excluded.ts
57
- `);
58
- stmt.run(keyId, value, agentId, layerId, version, status, ts);
60
+ const tableName = isNumeric ? 't_decisions_numeric' : 't_decisions';
61
+ const decisionData = {
62
+ key_id: keyId,
63
+ value: isNumeric ? value : String(value),
64
+ agent_id: agentId,
65
+ layer_id: layerId,
66
+ version: version,
67
+ status: status,
68
+ ts: ts
69
+ };
70
+ await adapter.upsert(tableName, decisionData, ['key_id']);
71
+ // Activity logging (replaces triggers)
72
+ if (existingDecision) {
73
+ // Update case - log update and record history
74
+ await logDecisionUpdate(knex, {
75
+ key: params.key,
76
+ old_value: String(existingDecision.value),
77
+ new_value: String(value),
78
+ old_version: existingDecision.version,
79
+ new_version: version,
80
+ agent_id: agentId,
81
+ layer_id: layerId || undefined
82
+ });
83
+ await recordDecisionHistory(knex, {
84
+ key_id: keyId,
85
+ version: existingDecision.version,
86
+ value: String(existingDecision.value),
87
+ agent_id: existingDecision.agent_id,
88
+ ts: existingDecision.ts
89
+ });
59
90
  }
60
91
  else {
61
- // String decision
62
- const stmt = db.prepare(`
63
- INSERT INTO t_decisions (key_id, value, agent_id, layer_id, version, status, ts)
64
- VALUES (?, ?, ?, ?, ?, ?, ?)
65
- ON CONFLICT(key_id) DO UPDATE SET
66
- value = excluded.value,
67
- agent_id = excluded.agent_id,
68
- layer_id = excluded.layer_id,
69
- version = excluded.version,
70
- status = excluded.status,
71
- ts = excluded.ts
72
- `);
73
- stmt.run(keyId, String(value), agentId, layerId, version, status, ts);
92
+ // New decision case - log set
93
+ await logDecisionSet(knex, {
94
+ key: params.key,
95
+ value: String(value),
96
+ version: version,
97
+ status: status,
98
+ agent_id: agentId,
99
+ layer_id: layerId || undefined
100
+ });
74
101
  }
75
102
  // Handle m_tags (many-to-many)
76
103
  if (params.tags && params.tags.length > 0) {
77
104
  // Clear existing tags
78
- db.prepare('DELETE FROM t_decision_tags WHERE decision_key_id = ?').run(keyId);
105
+ await knex('t_decision_tags').where({ decision_key_id: keyId }).delete();
79
106
  // Insert new tags
80
- const tagStmt = db.prepare('INSERT INTO t_decision_tags (decision_key_id, tag_id) VALUES (?, ?)');
81
107
  for (const tagName of params.tags) {
82
- const tagId = getOrCreateTag(db, tagName);
83
- tagStmt.run(keyId, tagId);
108
+ const tagId = await getOrCreateTag(adapter, tagName, trx);
109
+ await knex('t_decision_tags').insert({ decision_key_id: keyId, tag_id: tagId });
84
110
  }
85
111
  }
86
112
  // Handle m_scopes (many-to-many)
87
113
  if (params.scopes && params.scopes.length > 0) {
88
114
  // Clear existing scopes
89
- db.prepare('DELETE FROM t_decision_scopes WHERE decision_key_id = ?').run(keyId);
115
+ await knex('t_decision_scopes').where({ decision_key_id: keyId }).delete();
90
116
  // Insert new scopes
91
- const scopeStmt = db.prepare('INSERT INTO t_decision_scopes (decision_key_id, scope_id) VALUES (?, ?)');
92
117
  for (const scopeName of params.scopes) {
93
- const scopeId = getOrCreateScope(db, scopeName);
94
- scopeStmt.run(keyId, scopeId);
118
+ const scopeId = await getOrCreateScope(adapter, scopeName, trx);
119
+ await knex('t_decision_scopes').insert({ decision_key_id: keyId, scope_id: scopeId });
95
120
  }
96
121
  }
97
122
  return {
@@ -108,15 +133,15 @@ function setDecisionInternal(params, db) {
108
133
  * Supports tags, layers, scopes, and version tracking
109
134
  *
110
135
  * @param params - Decision parameters
111
- * @param db - Optional database instance (for testing)
136
+ * @param adapter - Optional database adapter (for testing)
112
137
  * @returns Response with success status and metadata
113
138
  */
114
- export function setDecision(params, db) {
115
- const actualDb = db ?? getDatabase();
139
+ export async function setDecision(params, adapter) {
140
+ const actualAdapter = adapter ?? getAdapter();
116
141
  try {
117
142
  // Use transaction for atomicity
118
- return transaction(actualDb, () => {
119
- return setDecisionInternal(params, actualDb);
143
+ return await actualAdapter.transaction(async (trx) => {
144
+ return await setDecisionInternal(params, actualAdapter, trx);
120
145
  });
121
146
  }
122
147
  catch (error) {
@@ -130,33 +155,30 @@ export function setDecision(params, db) {
130
155
  * Supports filtering by status, layer, tags, and scope
131
156
  *
132
157
  * @param params - Filter parameters
133
- * @param db - Optional database instance (for testing)
158
+ * @param adapter - Optional database adapter (for testing)
134
159
  * @returns Array of t_decisions with metadata
135
160
  */
136
- export function getContext(params = {}, db) {
137
- const actualDb = db ?? getDatabase();
161
+ export async function getContext(params = {}, adapter) {
162
+ const actualAdapter = adapter ?? getAdapter();
163
+ const knex = actualAdapter.getKnex();
138
164
  try {
139
165
  // Build query dynamically based on filters
140
- let query = 'SELECT * FROM v_tagged_decisions WHERE 1=1';
141
- const queryParams = [];
166
+ let query = knex('v_tagged_decisions');
142
167
  // Filter by status
143
168
  if (params.status) {
144
169
  if (!STRING_TO_STATUS[params.status]) {
145
170
  throw new Error(`Invalid status: ${params.status}`);
146
171
  }
147
- query += ' AND status = ?';
148
- queryParams.push(params.status);
172
+ query = query.where('status', params.status);
149
173
  }
150
174
  // Filter by layer
151
175
  if (params.layer) {
152
- query += ' AND layer = ?';
153
- queryParams.push(params.layer);
176
+ query = query.where('layer', params.layer);
154
177
  }
155
178
  // Filter by scope
156
179
  if (params.scope) {
157
180
  // Use LIKE for comma-separated scopes
158
- query += ' AND scopes LIKE ?';
159
- queryParams.push(`%${params.scope}%`);
181
+ query = query.where('scopes', 'like', `%${params.scope}%`);
160
182
  }
161
183
  // Filter by tags
162
184
  if (params.tags && params.tags.length > 0) {
@@ -164,24 +186,22 @@ export function getContext(params = {}, db) {
164
186
  if (tagMatch === 'AND') {
165
187
  // All tags must be present
166
188
  for (const tag of params.tags) {
167
- query += ' AND tags LIKE ?';
168
- queryParams.push(`%${tag}%`);
189
+ query = query.where('tags', 'like', `%${tag}%`);
169
190
  }
170
191
  }
171
192
  else {
172
193
  // Any tag must be present (OR)
173
- const tagConditions = params.tags.map(() => 'tags LIKE ?').join(' OR ');
174
- query += ` AND (${tagConditions})`;
175
- for (const tag of params.tags) {
176
- queryParams.push(`%${tag}%`);
177
- }
194
+ query = query.where((builder) => {
195
+ for (const tag of params.tags) {
196
+ builder.orWhere('tags', 'like', `%${tag}%`);
197
+ }
198
+ });
178
199
  }
179
200
  }
180
201
  // Order by most recent
181
- query += ' ORDER BY updated DESC';
202
+ query = query.orderBy('updated', 'desc');
182
203
  // Execute query
183
- const stmt = actualDb.prepare(query);
184
- const rows = stmt.all(...queryParams);
204
+ const rows = await query.select('*');
185
205
  return {
186
206
  decisions: rows,
187
207
  count: rows.length
@@ -198,11 +218,12 @@ export function getContext(params = {}, db) {
198
218
  * Optionally includes decision context (v3.2.2)
199
219
  *
200
220
  * @param params - Decision key and optional include_context flag
201
- * @param db - Optional database instance (for testing)
221
+ * @param adapter - Optional database adapter (for testing)
202
222
  * @returns Decision details or not found
203
223
  */
204
- export function getDecision(params, db) {
205
- const actualDb = db ?? getDatabase();
224
+ export async function getDecision(params, adapter) {
225
+ const actualAdapter = adapter ?? getAdapter();
226
+ const knex = actualAdapter.getKnex();
206
227
  // Validate parameter
207
228
  if (!params.key || params.key.trim() === '') {
208
229
  throw new Error('Parameter "key" is required and cannot be empty');
@@ -210,7 +231,7 @@ export function getDecision(params, db) {
210
231
  try {
211
232
  // If include_context is true, use the context-aware function
212
233
  if (params.include_context) {
213
- const result = dbGetDecisionWithContext(actualDb, params.key);
234
+ const result = await dbGetDecisionWithContext(actualAdapter, params.key);
214
235
  if (!result) {
215
236
  return {
216
237
  found: false
@@ -238,8 +259,9 @@ export function getDecision(params, db) {
238
259
  };
239
260
  }
240
261
  // Standard query without context (backward compatible)
241
- const stmt = actualDb.prepare('SELECT * FROM v_tagged_decisions WHERE key = ?');
242
- const row = stmt.get(params.key);
262
+ const row = await knex('v_tagged_decisions')
263
+ .where({ key: params.key })
264
+ .first();
243
265
  if (!row) {
244
266
  return {
245
267
  found: false
@@ -260,34 +282,33 @@ export function getDecision(params, db) {
260
282
  * Provides flexible tag-based filtering with status and layer support
261
283
  *
262
284
  * @param params - Search parameters (tags, match_mode, status, layer)
263
- * @param db - Optional database instance (for testing)
285
+ * @param adapter - Optional database adapter (for testing)
264
286
  * @returns Array of t_decisions matching tag criteria
265
287
  */
266
- export function searchByTags(params, db) {
267
- const actualDb = db ?? getDatabase();
288
+ export async function searchByTags(params, adapter) {
289
+ const actualAdapter = adapter ?? getAdapter();
290
+ const knex = actualAdapter.getKnex();
268
291
  // Validate required parameters
269
292
  if (!params.tags || params.tags.length === 0) {
270
293
  throw new Error('Parameter "tags" is required and must contain at least one tag');
271
294
  }
272
295
  try {
273
296
  const matchMode = params.match_mode || 'OR';
274
- let query = 'SELECT * FROM v_tagged_decisions WHERE 1=1';
275
- const queryParams = [];
297
+ let query = knex('v_tagged_decisions');
276
298
  // Apply tag filtering based on match mode
277
299
  if (matchMode === 'AND') {
278
300
  // All tags must be present
279
301
  for (const tag of params.tags) {
280
- query += ' AND tags LIKE ?';
281
- queryParams.push(`%${tag}%`);
302
+ query = query.where('tags', 'like', `%${tag}%`);
282
303
  }
283
304
  }
284
305
  else if (matchMode === 'OR') {
285
306
  // Any tag must be present
286
- const tagConditions = params.tags.map(() => 'tags LIKE ?').join(' OR ');
287
- query += ` AND (${tagConditions})`;
288
- for (const tag of params.tags) {
289
- queryParams.push(`%${tag}%`);
290
- }
307
+ query = query.where((builder) => {
308
+ for (const tag of params.tags) {
309
+ builder.orWhere('tags', 'like', `%${tag}%`);
310
+ }
311
+ });
291
312
  }
292
313
  else {
293
314
  throw new Error(`Invalid match_mode: ${matchMode}. Must be 'AND' or 'OR'`);
@@ -297,24 +318,21 @@ export function searchByTags(params, db) {
297
318
  if (!STRING_TO_STATUS[params.status]) {
298
319
  throw new Error(`Invalid status: ${params.status}. Must be 'active', 'deprecated', or 'draft'`);
299
320
  }
300
- query += ' AND status = ?';
301
- queryParams.push(params.status);
321
+ query = query.where('status', params.status);
302
322
  }
303
323
  // Optional layer filter
304
324
  if (params.layer) {
305
325
  // Validate layer exists
306
- const layerId = getLayerId(actualDb, params.layer);
326
+ const layerId = await getLayerId(actualAdapter, params.layer);
307
327
  if (layerId === null) {
308
328
  throw new Error(`Invalid layer: ${params.layer}. Must be one of: presentation, business, data, infrastructure, cross-cutting`);
309
329
  }
310
- query += ' AND layer = ?';
311
- queryParams.push(params.layer);
330
+ query = query.where('layer', params.layer);
312
331
  }
313
332
  // Order by most recent
314
- query += ' ORDER BY updated DESC';
333
+ query = query.orderBy('updated', 'desc');
315
334
  // Execute query
316
- const stmt = actualDb.prepare(query);
317
- const rows = stmt.all(...queryParams);
335
+ const rows = await query.select('*');
318
336
  return {
319
337
  decisions: rows,
320
338
  count: rows.length
@@ -330,18 +348,21 @@ export function searchByTags(params, db) {
330
348
  * Returns all historical versions ordered by timestamp (newest first)
331
349
  *
332
350
  * @param params - Decision key to get history for
333
- * @param db - Optional database instance (for testing)
351
+ * @param adapter - Optional database adapter (for testing)
334
352
  * @returns Array of historical versions with metadata
335
353
  */
336
- export function getVersions(params, db) {
337
- const actualDb = db ?? getDatabase();
354
+ export async function getVersions(params, adapter) {
355
+ const actualAdapter = adapter ?? getAdapter();
356
+ const knex = actualAdapter.getKnex();
338
357
  // Validate required parameter
339
358
  if (!params.key || params.key.trim() === '') {
340
359
  throw new Error('Parameter "key" is required and cannot be empty');
341
360
  }
342
361
  try {
343
362
  // Get key_id for the decision
344
- const keyResult = actualDb.prepare('SELECT id FROM m_context_keys WHERE key = ?').get(params.key);
363
+ const keyResult = await knex('m_context_keys')
364
+ .where({ key: params.key })
365
+ .first('id');
345
366
  if (!keyResult) {
346
367
  // Key doesn't exist, return empty history
347
368
  return {
@@ -352,18 +373,11 @@ export function getVersions(params, db) {
352
373
  }
353
374
  const keyId = keyResult.id;
354
375
  // Query t_decision_history with agent join
355
- const stmt = actualDb.prepare(`
356
- SELECT
357
- dh.version,
358
- dh.value,
359
- a.name as agent_name,
360
- datetime(dh.ts, 'unixepoch') as timestamp
361
- FROM t_decision_history dh
362
- LEFT JOIN m_agents a ON dh.agent_id = a.id
363
- WHERE dh.key_id = ?
364
- ORDER BY dh.ts DESC
365
- `);
366
- const rows = stmt.all(keyId);
376
+ const rows = await knex('t_decision_history as dh')
377
+ .leftJoin('m_agents as a', 'dh.agent_id', 'a.id')
378
+ .where('dh.key_id', keyId)
379
+ .select('dh.version', 'dh.value', 'a.name as agent_name', knex.raw(`datetime(dh.ts, 'unixepoch') as timestamp`))
380
+ .orderBy('dh.ts', 'desc');
367
381
  // Transform to response format
368
382
  const history = rows.map(row => ({
369
383
  version: row.version,
@@ -387,18 +401,19 @@ export function getVersions(params, db) {
387
401
  * Supports status filtering and optional tag inclusion
388
402
  *
389
403
  * @param params - Layer name, optional status and include_tags
390
- * @param db - Optional database instance (for testing)
404
+ * @param adapter - Optional database adapter (for testing)
391
405
  * @returns Array of t_decisions in the specified layer
392
406
  */
393
- export function searchByLayer(params, db) {
394
- const actualDb = db ?? getDatabase();
407
+ export async function searchByLayer(params, adapter) {
408
+ const actualAdapter = adapter ?? getAdapter();
409
+ const knex = actualAdapter.getKnex();
395
410
  // Validate required parameter
396
411
  if (!params.layer || params.layer.trim() === '') {
397
412
  throw new Error('Parameter "layer" is required and cannot be empty');
398
413
  }
399
414
  try {
400
415
  // Validate layer exists
401
- const layerId = getLayerId(actualDb, params.layer);
416
+ const layerId = await getLayerId(actualAdapter, params.layer);
402
417
  if (layerId === null) {
403
418
  throw new Error(`Invalid layer: ${params.layer}. Must be one of: presentation, business, data, infrastructure, cross-cutting`);
404
419
  }
@@ -409,69 +424,34 @@ export function searchByLayer(params, db) {
409
424
  if (!STRING_TO_STATUS[statusValue]) {
410
425
  throw new Error(`Invalid status: ${statusValue}. Must be 'active', 'deprecated', or 'draft'`);
411
426
  }
412
- let query;
413
- const queryParams = [params.layer, statusValue];
427
+ let rows;
414
428
  if (includeTagsValue) {
415
429
  // Use v_tagged_decisions view for full metadata
416
- query = `
417
- SELECT * FROM v_tagged_decisions
418
- WHERE layer = ? AND status = ?
419
- ORDER BY updated DESC
420
- `;
430
+ rows = await knex('v_tagged_decisions')
431
+ .where({ layer: params.layer, status: statusValue })
432
+ .orderBy('updated', 'desc')
433
+ .select('*');
421
434
  }
422
435
  else {
423
436
  // Use base t_decisions table with minimal joins
424
- query = `
425
- SELECT
426
- ck.key,
427
- d.value,
428
- d.version,
429
- CASE d.status
430
- WHEN 1 THEN 'active'
431
- WHEN 2 THEN 'deprecated'
432
- WHEN 3 THEN 'draft'
433
- END as status,
434
- l.name as layer,
435
- NULL as tags,
436
- NULL as scopes,
437
- a.name as decided_by,
438
- datetime(d.ts, 'unixepoch') as updated
439
- FROM t_decisions d
440
- INNER JOIN m_context_keys ck ON d.key_id = ck.id
441
- LEFT JOIN m_layers l ON d.layer_id = l.id
442
- LEFT JOIN m_agents a ON d.agent_id = a.id
443
- WHERE l.name = ? AND d.status = ?
444
-
445
- UNION ALL
446
-
447
- SELECT
448
- ck.key,
449
- CAST(dn.value AS TEXT) as value,
450
- dn.version,
451
- CASE dn.status
452
- WHEN 1 THEN 'active'
453
- WHEN 2 THEN 'deprecated'
454
- WHEN 3 THEN 'draft'
455
- END as status,
456
- l.name as layer,
457
- NULL as tags,
458
- NULL as scopes,
459
- a.name as decided_by,
460
- datetime(dn.ts, 'unixepoch') as updated
461
- FROM t_decisions_numeric dn
462
- INNER JOIN m_context_keys ck ON dn.key_id = ck.id
463
- LEFT JOIN m_layers l ON dn.layer_id = l.id
464
- LEFT JOIN m_agents a ON dn.agent_id = a.id
465
- WHERE l.name = ? AND dn.status = ?
466
-
467
- ORDER BY updated DESC
468
- `;
469
- // Add params for the numeric table part of UNION
470
- queryParams.push(params.layer, statusValue);
437
+ const statusInt = STRING_TO_STATUS[statusValue];
438
+ const stringDecisions = knex('t_decisions as d')
439
+ .innerJoin('m_context_keys as ck', 'd.key_id', 'ck.id')
440
+ .leftJoin('m_layers as l', 'd.layer_id', 'l.id')
441
+ .leftJoin('m_agents as a', 'd.agent_id', 'a.id')
442
+ .where('l.name', params.layer)
443
+ .where('d.status', statusInt)
444
+ .select('ck.key', 'd.value', 'd.version', knex.raw(`CASE d.status WHEN 1 THEN 'active' WHEN 2 THEN 'deprecated' WHEN 3 THEN 'draft' END as status`), 'l.name as layer', knex.raw('NULL as tags'), knex.raw('NULL as scopes'), 'a.name as decided_by', knex.raw(`datetime(d.ts, 'unixepoch') as updated`));
445
+ const numericDecisions = knex('t_decisions_numeric as dn')
446
+ .innerJoin('m_context_keys as ck', 'dn.key_id', 'ck.id')
447
+ .leftJoin('m_layers as l', 'dn.layer_id', 'l.id')
448
+ .leftJoin('m_agents as a', 'dn.agent_id', 'a.id')
449
+ .where('l.name', params.layer)
450
+ .where('dn.status', statusInt)
451
+ .select('ck.key', knex.raw('CAST(dn.value AS TEXT) as value'), 'dn.version', knex.raw(`CASE dn.status WHEN 1 THEN 'active' WHEN 2 THEN 'deprecated' WHEN 3 THEN 'draft' END as status`), 'l.name as layer', knex.raw('NULL as tags'), knex.raw('NULL as scopes'), 'a.name as decided_by', knex.raw(`datetime(dn.ts, 'unixepoch') as updated`));
452
+ // Union both queries
453
+ rows = await stringDecisions.union([numericDecisions]).orderBy('updated', 'desc');
471
454
  }
472
- // Execute query
473
- const stmt = actualDb.prepare(query);
474
- const rows = stmt.all(...queryParams);
475
455
  return {
476
456
  layer: params.layer,
477
457
  decisions: rows,
@@ -508,10 +488,10 @@ export function searchByLayer(params, db) {
508
488
  * All inferred fields can be overridden via optional parameters.
509
489
  *
510
490
  * @param params - Quick set parameters (key and value required)
511
- * @param db - Optional database instance (for testing)
491
+ * @param adapter - Optional database adapter (for testing)
512
492
  * @returns Response with success status and inferred metadata
513
493
  */
514
- export function quickSetDecision(params, db) {
494
+ export async function quickSetDecision(params, adapter) {
515
495
  // Validate required parameters
516
496
  if (!params.key || params.key.trim() === '') {
517
497
  throw new Error('Parameter "key" is required and cannot be empty');
@@ -575,8 +555,8 @@ export function quickSetDecision(params, db) {
575
555
  tags: inferredTags,
576
556
  scopes: inferredScopes
577
557
  };
578
- // Call setDecision with full params (pass db if provided)
579
- const result = setDecision(fullParams, db);
558
+ // Call setDecision with full params (pass adapter if provided)
559
+ const result = await setDecision(fullParams, adapter);
580
560
  // Return response with inferred metadata
581
561
  return {
582
562
  success: result.success,
@@ -603,11 +583,12 @@ export function quickSetDecision(params, db) {
603
583
  * - search_text: Full-text search in value field
604
584
  *
605
585
  * @param params - Advanced search parameters with filtering, sorting, pagination
606
- * @param db - Optional database instance (for testing)
586
+ * @param adapter - Optional database adapter (for testing)
607
587
  * @returns Filtered decisions with total count for pagination
608
588
  */
609
- export function searchAdvanced(params = {}, db) {
610
- const actualDb = db ?? getDatabase();
589
+ export async function searchAdvanced(params = {}, adapter) {
590
+ const actualAdapter = adapter ?? getAdapter();
591
+ const knex = actualAdapter.getKnex();
611
592
  try {
612
593
  // Parse relative time to Unix timestamp
613
594
  const parseRelativeTime = (relativeTime) => {
@@ -631,60 +612,64 @@ export function searchAdvanced(params = {}, db) {
631
612
  }
632
613
  };
633
614
  // Build base query using v_tagged_decisions view
634
- let query = 'SELECT * FROM v_tagged_decisions WHERE 1=1';
635
- const queryParams = [];
615
+ let query = knex('v_tagged_decisions');
636
616
  // Filter by layers (OR relationship)
637
617
  if (params.layers && params.layers.length > 0) {
638
- const layerConditions = params.layers.map(() => 'layer = ?').join(' OR ');
639
- query += ` AND (${layerConditions})`;
640
- queryParams.push(...params.layers);
618
+ query = query.whereIn('layer', params.layers);
641
619
  }
642
620
  // Filter by tags_all (AND relationship - must have ALL tags)
643
621
  if (params.tags_all && params.tags_all.length > 0) {
644
622
  for (const tag of params.tags_all) {
645
- query += ' AND (tags LIKE ? OR tags = ?)';
646
- queryParams.push(`%${tag}%`, tag);
623
+ query = query.where((builder) => {
624
+ builder.where('tags', 'like', `%${tag}%`).orWhere('tags', tag);
625
+ });
647
626
  }
648
627
  }
649
628
  // Filter by tags_any (OR relationship - must have ANY tag)
650
629
  if (params.tags_any && params.tags_any.length > 0) {
651
- const tagConditions = params.tags_any.map(() => '(tags LIKE ? OR tags = ?)').join(' OR ');
652
- query += ` AND (${tagConditions})`;
653
- for (const tag of params.tags_any) {
654
- queryParams.push(`%${tag}%`, tag);
655
- }
630
+ const tagsAny = params.tags_any; // Capture for closure
631
+ query = query.where((builder) => {
632
+ for (const tag of tagsAny) {
633
+ builder.orWhere('tags', 'like', `%${tag}%`).orWhere('tags', tag);
634
+ }
635
+ });
656
636
  }
657
637
  // Exclude tags
658
638
  if (params.exclude_tags && params.exclude_tags.length > 0) {
659
639
  for (const tag of params.exclude_tags) {
660
- query += ' AND (tags IS NULL OR (tags NOT LIKE ? AND tags != ?))';
661
- queryParams.push(`%${tag}%`, tag);
640
+ query = query.where((builder) => {
641
+ builder.whereNull('tags')
642
+ .orWhere((subBuilder) => {
643
+ subBuilder.where('tags', 'not like', `%${tag}%`)
644
+ .where('tags', '!=', tag);
645
+ });
646
+ });
662
647
  }
663
648
  }
664
649
  // Filter by scopes with wildcard support
665
650
  if (params.scopes && params.scopes.length > 0) {
666
- const scopeConditions = [];
667
- for (const scope of params.scopes) {
668
- if (scope.includes('*')) {
669
- // Wildcard pattern - convert to LIKE pattern
670
- const likePattern = scope.replace(/\*/g, '%');
671
- scopeConditions.push('(scopes LIKE ? OR scopes = ?)');
672
- queryParams.push(`%${likePattern}%`, likePattern);
673
- }
674
- else {
675
- // Exact match
676
- scopeConditions.push('(scopes LIKE ? OR scopes = ?)');
677
- queryParams.push(`%${scope}%`, scope);
651
+ const scopes = params.scopes; // Capture for closure
652
+ query = query.where((builder) => {
653
+ for (const scope of scopes) {
654
+ if (scope.includes('*')) {
655
+ // Wildcard pattern - convert to LIKE pattern
656
+ const likePattern = scope.replace(/\*/g, '%');
657
+ builder.orWhere('scopes', 'like', `%${likePattern}%`)
658
+ .orWhere('scopes', likePattern);
659
+ }
660
+ else {
661
+ // Exact match
662
+ builder.orWhere('scopes', 'like', `%${scope}%`)
663
+ .orWhere('scopes', scope);
664
+ }
678
665
  }
679
- }
680
- query += ` AND (${scopeConditions.join(' OR ')})`;
666
+ });
681
667
  }
682
668
  // Temporal filtering - updated_after
683
669
  if (params.updated_after) {
684
670
  const timestamp = parseRelativeTime(params.updated_after);
685
671
  if (timestamp !== null) {
686
- query += ' AND (SELECT unixepoch(updated)) >= ?';
687
- queryParams.push(timestamp);
672
+ query = query.whereRaw('unixepoch(updated) >= ?', [timestamp]);
688
673
  }
689
674
  else {
690
675
  throw new Error(`Invalid updated_after format: ${params.updated_after}. Use ISO timestamp or relative time like "7d", "2h", "30m"`);
@@ -694,8 +679,7 @@ export function searchAdvanced(params = {}, db) {
694
679
  if (params.updated_before) {
695
680
  const timestamp = parseRelativeTime(params.updated_before);
696
681
  if (timestamp !== null) {
697
- query += ' AND (SELECT unixepoch(updated)) <= ?';
698
- queryParams.push(timestamp);
682
+ query = query.whereRaw('unixepoch(updated) <= ?', [timestamp]);
699
683
  }
700
684
  else {
701
685
  throw new Error(`Invalid updated_before format: ${params.updated_before}. Use ISO timestamp or relative time like "7d", "2h", "30m"`);
@@ -703,25 +687,19 @@ export function searchAdvanced(params = {}, db) {
703
687
  }
704
688
  // Filter by decided_by (OR relationship)
705
689
  if (params.decided_by && params.decided_by.length > 0) {
706
- const agentConditions = params.decided_by.map(() => 'decided_by = ?').join(' OR ');
707
- query += ` AND (${agentConditions})`;
708
- queryParams.push(...params.decided_by);
690
+ query = query.whereIn('decided_by', params.decided_by);
709
691
  }
710
692
  // Filter by statuses (OR relationship)
711
693
  if (params.statuses && params.statuses.length > 0) {
712
- const statusConditions = params.statuses.map(() => 'status = ?').join(' OR ');
713
- query += ` AND (${statusConditions})`;
714
- queryParams.push(...params.statuses);
694
+ query = query.whereIn('status', params.statuses);
715
695
  }
716
696
  // Full-text search in value field
717
697
  if (params.search_text) {
718
- query += ' AND value LIKE ?';
719
- queryParams.push(`%${params.search_text}%`);
698
+ query = query.where('value', 'like', `%${params.search_text}%`);
720
699
  }
721
700
  // Count total matching records (before pagination)
722
- const countQuery = query.replace('SELECT * FROM', 'SELECT COUNT(*) as total FROM');
723
- const countStmt = actualDb.prepare(countQuery);
724
- const countResult = countStmt.get(...queryParams);
701
+ const countQuery = query.clone().count('* as total');
702
+ const countResult = await countQuery.first();
725
703
  const totalCount = countResult.total;
726
704
  // Sorting
727
705
  const sortBy = params.sort_by || 'updated';
@@ -733,7 +711,7 @@ export function searchAdvanced(params = {}, db) {
733
711
  if (!['asc', 'desc'].includes(sortOrder)) {
734
712
  throw new Error(`Invalid sort_order: ${sortOrder}. Must be 'asc' or 'desc'`);
735
713
  }
736
- query += ` ORDER BY ${sortBy} ${sortOrder.toUpperCase()}`;
714
+ query = query.orderBy(sortBy, sortOrder);
737
715
  // Pagination
738
716
  const limit = params.limit !== undefined ? params.limit : 20;
739
717
  const offset = params.offset || 0;
@@ -744,11 +722,9 @@ export function searchAdvanced(params = {}, db) {
744
722
  if (offset < 0) {
745
723
  throw new Error('Parameter "offset" must be non-negative');
746
724
  }
747
- query += ' LIMIT ? OFFSET ?';
748
- queryParams.push(limit, offset);
725
+ query = query.limit(limit).offset(offset);
749
726
  // Execute query
750
- const stmt = actualDb.prepare(query);
751
- const rows = stmt.all(...queryParams);
727
+ const rows = await query.select('*');
752
728
  return {
753
729
  decisions: rows,
754
730
  count: rows.length,
@@ -766,38 +742,100 @@ export function searchAdvanced(params = {}, db) {
766
742
  * Limit: 50 items per batch (constraint #3)
767
743
  *
768
744
  * @param params - Batch parameters with array of decisions and atomic flag
769
- * @param db - Optional database instance (for testing)
745
+ * @param adapter - Optional database adapter (for testing)
770
746
  * @returns Response with success status and detailed results for each item
771
747
  */
772
- export function setDecisionBatch(params, db) {
773
- const actualDb = db ?? getDatabase();
748
+ export async function setDecisionBatch(params, adapter) {
749
+ const actualAdapter = adapter ?? getAdapter();
774
750
  // Validate required parameters
775
751
  if (!params.decisions || !Array.isArray(params.decisions)) {
776
752
  throw new Error('Parameter "decisions" is required and must be an array');
777
753
  }
778
- const atomic = params.atomic !== undefined ? params.atomic : true;
779
- // Use processBatch utility
780
- const batchResult = processBatch(actualDb, params.decisions, (decision, db) => {
781
- const result = setDecisionInternal(decision, db);
754
+ if (params.decisions.length === 0) {
782
755
  return {
783
- key: decision.key,
784
- key_id: result.key_id,
785
- version: result.version
756
+ success: true,
757
+ inserted: 0,
758
+ failed: 0,
759
+ results: []
786
760
  };
787
- }, atomic, 50);
788
- // Map batch results to SetDecisionBatchResponse format
789
- return {
790
- success: batchResult.success,
791
- inserted: batchResult.processed,
792
- failed: batchResult.failed,
793
- results: batchResult.results.map(r => ({
794
- key: r.data?.key || '',
795
- key_id: r.data?.key_id,
796
- version: r.data?.version,
797
- success: r.success,
798
- error: r.error
799
- }))
800
- };
761
+ }
762
+ if (params.decisions.length > 50) {
763
+ throw new Error('Parameter "decisions" must contain at most 50 items');
764
+ }
765
+ const atomic = params.atomic !== undefined ? params.atomic : true;
766
+ try {
767
+ if (atomic) {
768
+ // Atomic mode: All or nothing
769
+ const results = await actualAdapter.transaction(async (trx) => {
770
+ const processedResults = [];
771
+ for (const decision of params.decisions) {
772
+ try {
773
+ const result = await setDecisionInternal(decision, actualAdapter, trx);
774
+ processedResults.push({
775
+ key: decision.key,
776
+ key_id: result.key_id,
777
+ version: result.version,
778
+ success: true,
779
+ error: undefined
780
+ });
781
+ }
782
+ catch (error) {
783
+ const message = error instanceof Error ? error.message : String(error);
784
+ throw new Error(`Batch failed at decision "${decision.key}": ${message}`);
785
+ }
786
+ }
787
+ return processedResults;
788
+ });
789
+ return {
790
+ success: true,
791
+ inserted: results.length,
792
+ failed: 0,
793
+ results: results
794
+ };
795
+ }
796
+ else {
797
+ // Non-atomic mode: Process each independently
798
+ const results = [];
799
+ let inserted = 0;
800
+ let failed = 0;
801
+ for (const decision of params.decisions) {
802
+ try {
803
+ const result = await actualAdapter.transaction(async (trx) => {
804
+ return await setDecisionInternal(decision, actualAdapter, trx);
805
+ });
806
+ results.push({
807
+ key: decision.key,
808
+ key_id: result.key_id,
809
+ version: result.version,
810
+ success: true,
811
+ error: undefined
812
+ });
813
+ inserted++;
814
+ }
815
+ catch (error) {
816
+ const message = error instanceof Error ? error.message : String(error);
817
+ results.push({
818
+ key: decision.key,
819
+ key_id: undefined,
820
+ version: undefined,
821
+ success: false,
822
+ error: message
823
+ });
824
+ failed++;
825
+ }
826
+ }
827
+ return {
828
+ success: failed === 0,
829
+ inserted: inserted,
830
+ failed: failed,
831
+ results: results
832
+ };
833
+ }
834
+ }
835
+ catch (error) {
836
+ const message = error instanceof Error ? error.message : String(error);
837
+ throw new Error(`Failed to execute batch operation: ${message}`);
838
+ }
801
839
  }
802
840
  /**
803
841
  * Check for updates since a given timestamp (FR-003 Phase A)
@@ -805,11 +843,12 @@ export function setDecisionBatch(params, db) {
805
843
  * Token cost: ~5-10 tokens per check
806
844
  *
807
845
  * @param params - Agent name and since_timestamp (ISO 8601)
808
- * @param db - Optional database instance (for testing)
846
+ * @param adapter - Optional database adapter (for testing)
809
847
  * @returns Boolean flag and counts for decisions, messages, files
810
848
  */
811
- export function hasUpdates(params, db) {
812
- const actualDb = db ?? getDatabase();
849
+ export async function hasUpdates(params, adapter) {
850
+ const actualAdapter = adapter ?? getAdapter();
851
+ const knex = actualAdapter.getKnex();
813
852
  // Validate required parameters
814
853
  if (!params.agent_name || params.agent_name.trim() === '') {
815
854
  throw new Error('Parameter "agent_name" is required and cannot be empty');
@@ -825,38 +864,43 @@ export function hasUpdates(params, db) {
825
864
  }
826
865
  const sinceTs = Math.floor(sinceDate.getTime() / 1000);
827
866
  // Count decisions updated since timestamp (both string and numeric tables)
828
- const decisionCountStmt = actualDb.prepare(`
829
- SELECT COUNT(*) as count FROM (
830
- SELECT ts FROM t_decisions WHERE ts > ?
831
- UNION ALL
832
- SELECT ts FROM t_decisions_numeric WHERE ts > ?
833
- )
834
- `);
835
- const decisionResult = decisionCountStmt.get(sinceTs, sinceTs);
836
- const decisionsCount = decisionResult.count;
867
+ const decisionCount1 = await knex('t_decisions')
868
+ .where('ts', '>', sinceTs)
869
+ .count('* as count')
870
+ .first();
871
+ const decisionCount2 = await knex('t_decisions_numeric')
872
+ .where('ts', '>', sinceTs)
873
+ .count('* as count')
874
+ .first();
875
+ const decisionsCount = (decisionCount1?.count || 0) + (decisionCount2?.count || 0);
837
876
  // Get agent_id for the requesting agent
838
- const agentResult = actualDb.prepare('SELECT id FROM m_agents WHERE name = ?').get(params.agent_name);
877
+ const agentResult = await knex('m_agents')
878
+ .where({ name: params.agent_name })
879
+ .first('id');
839
880
  // Count messages for the agent (received messages - to_agent_id matches OR broadcast messages)
840
881
  let messagesCount = 0;
841
882
  if (agentResult) {
842
883
  const agentId = agentResult.id;
843
- const messageCountStmt = actualDb.prepare(`
844
- SELECT COUNT(*) as count FROM t_agent_messages
845
- WHERE ts > ? AND (to_agent_id = ? OR to_agent_id IS NULL)
846
- `);
847
- const messageResult = messageCountStmt.get(sinceTs, agentId);
848
- messagesCount = messageResult.count;
884
+ const messageResult = await knex('t_agent_messages')
885
+ .where('ts', '>', sinceTs)
886
+ .where((builder) => {
887
+ builder.where('to_agent_id', agentId)
888
+ .orWhereNull('to_agent_id');
889
+ })
890
+ .count('* as count')
891
+ .first();
892
+ messagesCount = messageResult?.count || 0;
849
893
  }
850
894
  // Count file changes since timestamp
851
- const fileCountStmt = actualDb.prepare(`
852
- SELECT COUNT(*) as count FROM t_file_changes WHERE ts > ?
853
- `);
854
- const fileResult = fileCountStmt.get(sinceTs);
855
- const filesCount = fileResult.count;
895
+ const fileResult = await knex('t_file_changes')
896
+ .where('ts', '>', sinceTs)
897
+ .count('* as count')
898
+ .first();
899
+ const filesCount = fileResult?.count || 0;
856
900
  // Determine if there are any updates
857
- const hasUpdates = decisionsCount > 0 || messagesCount > 0 || filesCount > 0;
901
+ const hasUpdatesFlag = decisionsCount > 0 || messagesCount > 0 || filesCount > 0;
858
902
  return {
859
- has_updates: hasUpdates,
903
+ has_updates: hasUpdatesFlag,
860
904
  counts: {
861
905
  decisions: decisionsCount,
862
906
  messages: messagesCount,
@@ -875,11 +919,12 @@ export function hasUpdates(params, db) {
875
919
  * Validates required fields if template specifies any
876
920
  *
877
921
  * @param params - Template name, key, value, and optional overrides
878
- * @param db - Optional database instance (for testing)
922
+ * @param adapter - Optional database adapter (for testing)
879
923
  * @returns Response with success status and applied defaults metadata
880
924
  */
881
- export function setFromTemplate(params, db) {
882
- const actualDb = db ?? getDatabase();
925
+ export async function setFromTemplate(params, adapter) {
926
+ const actualAdapter = adapter ?? getAdapter();
927
+ const knex = actualAdapter.getKnex();
883
928
  // Validate required parameters
884
929
  if (!params.template || params.template.trim() === '') {
885
930
  throw new Error('Parameter "template" is required and cannot be empty');
@@ -892,7 +937,9 @@ export function setFromTemplate(params, db) {
892
937
  }
893
938
  try {
894
939
  // Get template
895
- const templateRow = actualDb.prepare('SELECT * FROM t_decision_templates WHERE name = ?').get(params.template);
940
+ const templateRow = await knex('t_decision_templates')
941
+ .where({ name: params.template })
942
+ .first();
896
943
  if (!templateRow) {
897
944
  throw new Error(`Template not found: ${params.template}`);
898
945
  }
@@ -930,8 +977,8 @@ export function setFromTemplate(params, db) {
930
977
  if (!params.status && defaults.status) {
931
978
  appliedDefaults.status = defaults.status;
932
979
  }
933
- // Call setDecision with merged params (pass db if provided)
934
- const result = setDecision(decisionParams, actualDb);
980
+ // Call setDecision with merged params (pass adapter if provided)
981
+ const result = await setDecision(decisionParams, actualAdapter);
935
982
  return {
936
983
  success: result.success,
937
984
  key: result.key,
@@ -952,11 +999,12 @@ export function setFromTemplate(params, db) {
952
999
  * Defines reusable defaults and required fields for decisions
953
1000
  *
954
1001
  * @param params - Template name, defaults, required fields, and creator
955
- * @param db - Optional database instance (for testing)
1002
+ * @param adapter - Optional database adapter (for testing)
956
1003
  * @returns Response with success status and template ID
957
1004
  */
958
- export function createTemplate(params, db) {
959
- const actualDb = db ?? getDatabase();
1005
+ export async function createTemplate(params, adapter) {
1006
+ const actualAdapter = adapter ?? getAdapter();
1007
+ const knex = actualAdapter.getKnex();
960
1008
  // Validate required parameters
961
1009
  if (!params.name || params.name.trim() === '') {
962
1010
  throw new Error('Parameter "name" is required and cannot be empty');
@@ -965,10 +1013,10 @@ export function createTemplate(params, db) {
965
1013
  throw new Error('Parameter "defaults" is required and must be an object');
966
1014
  }
967
1015
  try {
968
- return transaction(actualDb, () => {
1016
+ return await actualAdapter.transaction(async (trx) => {
969
1017
  // Validate layer if provided in defaults
970
1018
  if (params.defaults.layer) {
971
- const layerId = getLayerId(actualDb, params.defaults.layer);
1019
+ const layerId = await getLayerId(actualAdapter, params.defaults.layer, trx);
972
1020
  if (layerId === null) {
973
1021
  throw new Error(`Invalid layer in defaults: ${params.defaults.layer}. Must be one of: presentation, business, data, infrastructure, cross-cutting`);
974
1022
  }
@@ -980,20 +1028,21 @@ export function createTemplate(params, db) {
980
1028
  // Get or create agent if creator specified
981
1029
  let createdById = null;
982
1030
  if (params.created_by) {
983
- createdById = getOrCreateAgent(actualDb, params.created_by);
1031
+ createdById = await getOrCreateAgent(actualAdapter, params.created_by, trx);
984
1032
  }
985
1033
  // Serialize defaults and required fields
986
1034
  const defaultsJson = JSON.stringify(params.defaults);
987
1035
  const requiredFieldsJson = params.required_fields ? JSON.stringify(params.required_fields) : null;
988
1036
  // Insert template
989
- const stmt = actualDb.prepare(`
990
- INSERT INTO t_decision_templates (name, defaults, required_fields, created_by)
991
- VALUES (?, ?, ?, ?)
992
- `);
993
- const info = stmt.run(params.name, defaultsJson, requiredFieldsJson, createdById);
1037
+ const [id] = await trx('t_decision_templates').insert({
1038
+ name: params.name,
1039
+ defaults: defaultsJson,
1040
+ required_fields: requiredFieldsJson,
1041
+ created_by: createdById
1042
+ });
994
1043
  return {
995
1044
  success: true,
996
- template_id: info.lastInsertRowid,
1045
+ template_id: id,
997
1046
  template_name: params.name,
998
1047
  message: `Template "${params.name}" created successfully`
999
1048
  };
@@ -1009,25 +1058,17 @@ export function createTemplate(params, db) {
1009
1058
  * Returns all templates with their defaults and metadata
1010
1059
  *
1011
1060
  * @param params - No parameters required
1012
- * @param db - Optional database instance (for testing)
1061
+ * @param adapter - Optional database adapter (for testing)
1013
1062
  * @returns Array of all templates with parsed JSON fields
1014
1063
  */
1015
- export function listTemplates(params = {}, db) {
1016
- const actualDb = db ?? getDatabase();
1064
+ export async function listTemplates(params = {}, adapter) {
1065
+ const actualAdapter = adapter ?? getAdapter();
1066
+ const knex = actualAdapter.getKnex();
1017
1067
  try {
1018
- const stmt = actualDb.prepare(`
1019
- SELECT
1020
- t.id,
1021
- t.name,
1022
- t.defaults,
1023
- t.required_fields,
1024
- a.name as created_by,
1025
- datetime(t.ts, 'unixepoch') as created_at
1026
- FROM t_decision_templates t
1027
- LEFT JOIN m_agents a ON t.created_by = a.id
1028
- ORDER BY t.name ASC
1029
- `);
1030
- const rows = stmt.all();
1068
+ const rows = await knex('t_decision_templates as t')
1069
+ .leftJoin('m_agents as a', 't.created_by', 'a.id')
1070
+ .select('t.id', 't.name', 't.defaults', 't.required_fields', 'a.name as created_by', knex.raw(`datetime(t.ts, 'unixepoch') as created_at`))
1071
+ .orderBy('t.name', 'asc');
1031
1072
  // Parse JSON fields
1032
1073
  const templates = rows.map(row => ({
1033
1074
  id: row.id,
@@ -1060,19 +1101,22 @@ export function listTemplates(params = {}, db) {
1060
1101
  * (tags, scopes) will also be deleted due to CASCADE constraints.
1061
1102
  *
1062
1103
  * @param params - Decision key to delete
1063
- * @param db - Optional database instance (for testing)
1104
+ * @param adapter - Optional database adapter (for testing)
1064
1105
  * @returns Response with success status
1065
1106
  */
1066
- export function hardDeleteDecision(params, db) {
1067
- const actualDb = db ?? getDatabase();
1107
+ export async function hardDeleteDecision(params, adapter) {
1108
+ const actualAdapter = adapter ?? getAdapter();
1109
+ const knex = actualAdapter.getKnex();
1068
1110
  // Validate parameter
1069
1111
  if (!params.key || params.key.trim() === '') {
1070
1112
  throw new Error('Parameter "key" is required and cannot be empty');
1071
1113
  }
1072
1114
  try {
1073
- return transaction(actualDb, () => {
1115
+ return await actualAdapter.transaction(async (trx) => {
1074
1116
  // Get key_id
1075
- const keyResult = actualDb.prepare('SELECT id FROM m_context_keys WHERE key = ?').get(params.key);
1117
+ const keyResult = await trx('m_context_keys')
1118
+ .where({ key: params.key })
1119
+ .first('id');
1076
1120
  if (!keyResult) {
1077
1121
  // Key doesn't exist - still return success (idempotent)
1078
1122
  return {
@@ -1083,17 +1127,17 @@ export function hardDeleteDecision(params, db) {
1083
1127
  }
1084
1128
  const keyId = keyResult.id;
1085
1129
  // Delete from t_decisions (if exists)
1086
- const deletedString = actualDb.prepare('DELETE FROM t_decisions WHERE key_id = ?').run(keyId);
1130
+ const deletedString = await trx('t_decisions').where({ key_id: keyId }).delete();
1087
1131
  // Delete from t_decisions_numeric (if exists)
1088
- const deletedNumeric = actualDb.prepare('DELETE FROM t_decisions_numeric WHERE key_id = ?').run(keyId);
1132
+ const deletedNumeric = await trx('t_decisions_numeric').where({ key_id: keyId }).delete();
1089
1133
  // Delete from t_decision_history (CASCADE should handle this, but explicit for clarity)
1090
- const deletedHistory = actualDb.prepare('DELETE FROM t_decision_history WHERE key_id = ?').run(keyId);
1134
+ const deletedHistory = await trx('t_decision_history').where({ key_id: keyId }).delete();
1091
1135
  // Delete from t_decision_tags (CASCADE should handle this)
1092
- const deletedTags = actualDb.prepare('DELETE FROM t_decision_tags WHERE decision_key_id = ?').run(keyId);
1136
+ const deletedTags = await trx('t_decision_tags').where({ decision_key_id: keyId }).delete();
1093
1137
  // Delete from t_decision_scopes (CASCADE should handle this)
1094
- const deletedScopes = actualDb.prepare('DELETE FROM t_decision_scopes WHERE decision_key_id = ?').run(keyId);
1138
+ const deletedScopes = await trx('t_decision_scopes').where({ decision_key_id: keyId }).delete();
1095
1139
  // Calculate total deleted records
1096
- const totalDeleted = deletedString.changes + deletedNumeric.changes + deletedHistory.changes + deletedTags.changes + deletedScopes.changes;
1140
+ const totalDeleted = deletedString + deletedNumeric + deletedHistory + deletedTags + deletedScopes;
1097
1141
  if (totalDeleted === 0) {
1098
1142
  return {
1099
1143
  success: true,
@@ -1121,11 +1165,11 @@ export function hardDeleteDecision(params, db) {
1121
1165
  * Adds rich context (rationale, alternatives, tradeoffs) to a decision
1122
1166
  *
1123
1167
  * @param params - Context parameters
1124
- * @param db - Optional database instance (for testing)
1168
+ * @param adapter - Optional database adapter (for testing)
1125
1169
  * @returns Response with success status
1126
1170
  */
1127
- export function addDecisionContextAction(params, db) {
1128
- const actualDb = db ?? getDatabase();
1171
+ export async function addDecisionContextAction(params, adapter) {
1172
+ const actualAdapter = adapter ?? getAdapter();
1129
1173
  // Validate required parameters
1130
1174
  if (!params.key || params.key.trim() === '') {
1131
1175
  throw new Error('Parameter "key" is required and cannot be empty');
@@ -1144,7 +1188,7 @@ export function addDecisionContextAction(params, db) {
1144
1188
  if (tradeoffs && typeof tradeoffs === 'object') {
1145
1189
  tradeoffs = JSON.stringify(tradeoffs);
1146
1190
  }
1147
- const contextId = dbAddDecisionContext(actualDb, params.key, params.rationale, alternatives, tradeoffs, params.decided_by || null, params.related_task_id || null, params.related_constraint_id || null);
1191
+ const contextId = await dbAddDecisionContext(actualAdapter, params.key, params.rationale, alternatives, tradeoffs, params.decided_by || null, params.related_task_id || null, params.related_constraint_id || null);
1148
1192
  return {
1149
1193
  success: true,
1150
1194
  context_id: contextId,
@@ -1162,13 +1206,13 @@ export function addDecisionContextAction(params, db) {
1162
1206
  * Query decision contexts with optional filters
1163
1207
  *
1164
1208
  * @param params - Filter parameters
1165
- * @param db - Optional database instance (for testing)
1209
+ * @param adapter - Optional database adapter (for testing)
1166
1210
  * @returns Array of decision contexts
1167
1211
  */
1168
- export function listDecisionContextsAction(params, db) {
1169
- const actualDb = db ?? getDatabase();
1212
+ export async function listDecisionContextsAction(params, adapter) {
1213
+ const actualAdapter = adapter ?? getAdapter();
1170
1214
  try {
1171
- const contexts = dbListDecisionContexts(actualDb, {
1215
+ const contexts = await dbListDecisionContexts(actualAdapter, {
1172
1216
  decisionKey: params.decision_key,
1173
1217
  relatedTaskId: params.related_task_id,
1174
1218
  relatedConstraintId: params.related_constraint_id,