sqlew 3.5.3 → 3.6.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 (257) hide show
  1. package/CHANGELOG.md +247 -1772
  2. package/README.md +70 -304
  3. package/assets/config.example.toml +106 -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 +11 -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 +56 -121
  37. package/dist/database.d.ts.map +1 -1
  38. package/dist/database.js +266 -414
  39. package/dist/database.js.map +1 -1
  40. package/dist/index.js +329 -245
  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/knex/20251027000000_add_agent_reuse_system.d.ts +14 -0
  107. package/dist/migrations/knex/20251027000000_add_agent_reuse_system.d.ts.map +1 -0
  108. package/dist/migrations/knex/20251027000000_add_agent_reuse_system.js +34 -0
  109. package/dist/migrations/knex/20251027000000_add_agent_reuse_system.js.map +1 -0
  110. package/dist/migrations/knex/20251027010000_add_task_constraint_to_decision_context.d.ts +4 -0
  111. package/dist/migrations/knex/20251027010000_add_task_constraint_to_decision_context.d.ts.map +1 -0
  112. package/dist/migrations/knex/20251027010000_add_task_constraint_to_decision_context.js +24 -0
  113. package/dist/migrations/knex/20251027010000_add_task_constraint_to_decision_context.js.map +1 -0
  114. package/dist/migrations/knex/20251027020000_update_agent_reusability.d.ts +16 -0
  115. package/dist/migrations/knex/20251027020000_update_agent_reusability.d.ts.map +1 -0
  116. package/dist/migrations/knex/20251027020000_update_agent_reusability.js +27 -0
  117. package/dist/migrations/knex/20251027020000_update_agent_reusability.js.map +1 -0
  118. package/dist/migrations/seed-help-data.d.ts +48 -0
  119. package/dist/migrations/seed-help-data.d.ts.map +1 -0
  120. package/dist/migrations/seed-help-data.js +1466 -0
  121. package/dist/migrations/seed-help-data.js.map +1 -0
  122. package/dist/migrations/seed-tool-metadata.d.ts +24 -0
  123. package/dist/migrations/seed-tool-metadata.d.ts.map +1 -0
  124. package/dist/migrations/seed-tool-metadata.js +392 -0
  125. package/dist/migrations/seed-tool-metadata.js.map +1 -0
  126. package/dist/migrations/v3.6.0-help-system-refactor.d.ts +46 -0
  127. package/dist/migrations/v3.6.0-help-system-refactor.d.ts.map +1 -0
  128. package/dist/migrations/v3.6.0-help-system-refactor.js +223 -0
  129. package/dist/migrations/v3.6.0-help-system-refactor.js.map +1 -0
  130. package/dist/schema.d.ts.map +1 -1
  131. package/dist/schema.js +2 -0
  132. package/dist/schema.js.map +1 -1
  133. package/dist/tests/agent-reuse.test.d.ts +6 -0
  134. package/dist/tests/agent-reuse.test.d.ts.map +1 -0
  135. package/dist/tests/agent-reuse.test.js +242 -0
  136. package/dist/tests/agent-reuse.test.js.map +1 -0
  137. package/dist/tests/all-features.test.d.ts +7 -0
  138. package/dist/tests/all-features.test.d.ts.map +1 -0
  139. package/dist/tests/all-features.test.js +514 -0
  140. package/dist/tests/all-features.test.js.map +1 -0
  141. package/dist/tests/git-aware-completion.test.js +89 -70
  142. package/dist/tests/git-aware-completion.test.js.map +1 -1
  143. package/dist/tests/help-system.test.d.ts +23 -0
  144. package/dist/tests/help-system.test.d.ts.map +1 -0
  145. package/dist/tests/help-system.test.js +374 -0
  146. package/dist/tests/help-system.test.js.map +1 -0
  147. package/dist/tests/tasks.auto-pruning-decision-link.test.js +92 -78
  148. package/dist/tests/tasks.auto-pruning-decision-link.test.js.map +1 -1
  149. package/dist/tests/tasks.auto-pruning-partial.test.js +106 -95
  150. package/dist/tests/tasks.auto-pruning-partial.test.js.map +1 -1
  151. package/dist/tests/tasks.auto-pruning-persistence.test.js +115 -97
  152. package/dist/tests/tasks.auto-pruning-persistence.test.js.map +1 -1
  153. package/dist/tests/tasks.auto-pruning-safety.test.js +124 -103
  154. package/dist/tests/tasks.auto-pruning-safety.test.js.map +1 -1
  155. package/dist/tests/tasks.dependencies.test.js +338 -307
  156. package/dist/tests/tasks.dependencies.test.js.map +1 -1
  157. package/dist/tests/tasks.link-file-backward-compat.test.js +116 -104
  158. package/dist/tests/tasks.link-file-backward-compat.test.js.map +1 -1
  159. package/dist/tests/tasks.watch-files-action.test.js +122 -101
  160. package/dist/tests/tasks.watch-files-action.test.js.map +1 -1
  161. package/dist/tests/tasks.watch-files-parameter.test.js +105 -94
  162. package/dist/tests/tasks.watch-files-parameter.test.js.map +1 -1
  163. package/dist/tests/two-step-git-completion.test.js +176 -133
  164. package/dist/tests/two-step-git-completion.test.js.map +1 -1
  165. package/dist/tests/vcs-staging.test.js +1 -1
  166. package/dist/tests/vcs-staging.test.js.map +1 -1
  167. package/dist/tools/config.d.ts +9 -6
  168. package/dist/tools/config.d.ts.map +1 -1
  169. package/dist/tools/config.js +16 -14
  170. package/dist/tools/config.js.map +1 -1
  171. package/dist/tools/constraints.d.ts +10 -7
  172. package/dist/tools/constraints.d.ts.map +1 -1
  173. package/dist/tools/constraints.js +73 -51
  174. package/dist/tools/constraints.js.map +1 -1
  175. package/dist/tools/context.d.ts +36 -33
  176. package/dist/tools/context.d.ts.map +1 -1
  177. package/dist/tools/context.js +441 -340
  178. package/dist/tools/context.js.map +1 -1
  179. package/dist/tools/files.d.ts +12 -9
  180. package/dist/tools/files.d.ts.map +1 -1
  181. package/dist/tools/files.js +173 -95
  182. package/dist/tools/files.js.map +1 -1
  183. package/dist/tools/help-queries.d.ts +130 -0
  184. package/dist/tools/help-queries.d.ts.map +1 -0
  185. package/dist/tools/help-queries.js +393 -0
  186. package/dist/tools/help-queries.js.map +1 -0
  187. package/dist/tools/messaging.d.ts +14 -11
  188. package/dist/tools/messaging.d.ts.map +1 -1
  189. package/dist/tools/messaging.js +239 -133
  190. package/dist/tools/messaging.js.map +1 -1
  191. package/dist/tools/tasks.d.ts +18 -16
  192. package/dist/tools/tasks.d.ts.map +1 -1
  193. package/dist/tools/tasks.js +519 -442
  194. package/dist/tools/tasks.js.map +1 -1
  195. package/dist/tools/utils.d.ts +14 -11
  196. package/dist/tools/utils.d.ts.map +1 -1
  197. package/dist/tools/utils.js +90 -122
  198. package/dist/tools/utils.js.map +1 -1
  199. package/dist/types.d.ts +1 -0
  200. package/dist/types.d.ts.map +1 -1
  201. package/dist/utils/activity-logging.d.ts +114 -0
  202. package/dist/utils/activity-logging.d.ts.map +1 -0
  203. package/dist/utils/activity-logging.js +162 -0
  204. package/dist/utils/activity-logging.js.map +1 -0
  205. package/dist/utils/batch.d.ts +2 -2
  206. package/dist/utils/batch.d.ts.map +1 -1
  207. package/dist/utils/batch.js +8 -8
  208. package/dist/utils/batch.js.map +1 -1
  209. package/dist/utils/cleanup.d.ts +24 -14
  210. package/dist/utils/cleanup.d.ts.map +1 -1
  211. package/dist/utils/cleanup.js +37 -27
  212. package/dist/utils/cleanup.js.map +1 -1
  213. package/dist/utils/debug-logger.d.ts +99 -0
  214. package/dist/utils/debug-logger.d.ts.map +1 -0
  215. package/dist/utils/debug-logger.js +267 -0
  216. package/dist/utils/debug-logger.js.map +1 -0
  217. package/dist/utils/error-handler.d.ts +28 -0
  218. package/dist/utils/error-handler.d.ts.map +1 -0
  219. package/dist/utils/error-handler.js +121 -0
  220. package/dist/utils/error-handler.js.map +1 -0
  221. package/dist/utils/help-tracking.d.ts +55 -0
  222. package/dist/utils/help-tracking.d.ts.map +1 -0
  223. package/dist/utils/help-tracking.js +88 -0
  224. package/dist/utils/help-tracking.js.map +1 -0
  225. package/dist/utils/param-parser.d.ts +23 -0
  226. package/dist/utils/param-parser.d.ts.map +1 -0
  227. package/dist/utils/param-parser.js +52 -0
  228. package/dist/utils/param-parser.js.map +1 -0
  229. package/dist/utils/retention.d.ts +17 -7
  230. package/dist/utils/retention.d.ts.map +1 -1
  231. package/dist/utils/retention.js +31 -12
  232. package/dist/utils/retention.js.map +1 -1
  233. package/dist/utils/task-stale-detection.d.ts +15 -13
  234. package/dist/utils/task-stale-detection.d.ts.map +1 -1
  235. package/dist/utils/task-stale-detection.js +100 -302
  236. package/dist/utils/task-stale-detection.js.map +1 -1
  237. package/dist/utils/token-estimation.d.ts +72 -0
  238. package/dist/utils/token-estimation.d.ts.map +1 -0
  239. package/dist/utils/token-estimation.js +71 -0
  240. package/dist/utils/token-estimation.js.map +1 -0
  241. package/dist/utils/token-logging.d.ts +48 -0
  242. package/dist/utils/token-logging.d.ts.map +1 -0
  243. package/dist/utils/token-logging.js +112 -0
  244. package/dist/utils/token-logging.js.map +1 -0
  245. package/dist/utils/view-queries.d.ts +34 -0
  246. package/dist/utils/view-queries.d.ts.map +1 -0
  247. package/dist/utils/view-queries.js +192 -0
  248. package/dist/utils/view-queries.js.map +1 -0
  249. package/dist/watcher/file-watcher.d.ts.map +1 -1
  250. package/dist/watcher/file-watcher.js +25 -11
  251. package/dist/watcher/file-watcher.js.map +1 -1
  252. package/docs/BEST_PRACTICES.md +56 -448
  253. package/docs/MIGRATION_v3.6.0.md +170 -0
  254. package/docs/SHARED_CONCEPTS.md +63 -208
  255. package/docs/TASK_OVERVIEW.md +2 -2
  256. package/docs/TOOL_SELECTION.md +41 -248
  257. package/package.json +17 -4
@@ -1,20 +1,26 @@
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';
11
+ import { parseStringArray } from '../utils/param-parser.js';
12
+ import { debugLogFunctionEntry, debugLogFunctionExit, debugLogTransaction, debugLogCriticalError } from '../utils/debug-logger.js';
9
13
  /**
10
14
  * Internal helper: Set decision without wrapping in transaction
11
15
  * Used by setDecision (with transaction) and setDecisionBatch (manages its own transaction)
12
16
  *
13
17
  * @param params - Decision parameters
14
- * @param db - Database instance
18
+ * @param adapter - Database adapter instance
19
+ * @param trx - Optional transaction
15
20
  * @returns Response with success status and metadata
16
21
  */
17
- function setDecisionInternal(params, db) {
22
+ async function setDecisionInternal(params, adapter, trx) {
23
+ const knex = trx || adapter.getKnex();
18
24
  // Validate required parameters
19
25
  const trimmedKey = validateRequired(params.key, 'key');
20
26
  if (params.value === undefined || params.value === null) {
@@ -34,64 +40,99 @@ function setDecisionInternal(params, db) {
34
40
  // Validate layer if provided
35
41
  let layerId = null;
36
42
  if (params.layer) {
37
- layerId = validateLayer(db, params.layer);
43
+ const validLayers = ['presentation', 'business', 'data', 'infrastructure', 'cross-cutting'];
44
+ if (!validLayers.includes(params.layer)) {
45
+ throw new Error(`Invalid layer. Must be one of: ${validLayers.join(', ')}`);
46
+ }
47
+ layerId = await getLayerId(adapter, params.layer, trx);
48
+ if (layerId === null) {
49
+ throw new Error(`Layer not found in database: ${params.layer}`);
50
+ }
38
51
  }
39
52
  // Get or create master records
40
- const agentId = getOrCreateAgent(db, agentName);
41
- const keyId = getOrCreateContextKey(db, params.key);
53
+ const agentId = await getOrCreateAgent(adapter, agentName, trx);
54
+ const keyId = await getOrCreateContextKey(adapter, params.key, trx);
42
55
  // Current timestamp
43
56
  const ts = Math.floor(Date.now() / 1000);
57
+ // Check if decision already exists for activity logging
58
+ const existingDecision = await knex(isNumeric ? 't_decisions_numeric' : 't_decisions')
59
+ .where({ key_id: keyId })
60
+ .first();
44
61
  // 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);
62
+ const tableName = isNumeric ? 't_decisions_numeric' : 't_decisions';
63
+ const decisionData = {
64
+ key_id: keyId,
65
+ value: isNumeric ? value : String(value),
66
+ agent_id: agentId,
67
+ layer_id: layerId,
68
+ version: version,
69
+ status: status,
70
+ ts: ts
71
+ };
72
+ // Use transaction-aware upsert instead of adapter.upsert to avoid connection pool timeout
73
+ const conflictColumns = ['key_id'];
74
+ const updateColumns = Object.keys(decisionData).filter(key => !conflictColumns.includes(key));
75
+ const updateData = updateColumns.reduce((acc, col) => {
76
+ acc[col] = decisionData[col];
77
+ return acc;
78
+ }, {});
79
+ await knex(tableName)
80
+ .insert(decisionData)
81
+ .onConflict(conflictColumns)
82
+ .merge(updateData);
83
+ // Activity logging (replaces triggers)
84
+ if (existingDecision) {
85
+ // Update case - log update and record history
86
+ await logDecisionUpdate(knex, {
87
+ key: params.key,
88
+ old_value: String(existingDecision.value),
89
+ new_value: String(value),
90
+ old_version: existingDecision.version,
91
+ new_version: version,
92
+ agent_id: agentId,
93
+ layer_id: layerId || undefined
94
+ });
95
+ await recordDecisionHistory(knex, {
96
+ key_id: keyId,
97
+ version: existingDecision.version,
98
+ value: String(existingDecision.value),
99
+ agent_id: existingDecision.agent_id,
100
+ ts: existingDecision.ts
101
+ });
59
102
  }
60
103
  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);
104
+ // New decision case - log set
105
+ await logDecisionSet(knex, {
106
+ key: params.key,
107
+ value: String(value),
108
+ version: version,
109
+ status: status,
110
+ agent_id: agentId,
111
+ layer_id: layerId || undefined
112
+ });
74
113
  }
75
114
  // Handle m_tags (many-to-many)
76
115
  if (params.tags && params.tags.length > 0) {
116
+ // Parse tags (handles both arrays and JSON strings from MCP)
117
+ const tags = parseStringArray(params.tags);
77
118
  // Clear existing tags
78
- db.prepare('DELETE FROM t_decision_tags WHERE decision_key_id = ?').run(keyId);
119
+ await knex('t_decision_tags').where({ decision_key_id: keyId }).delete();
79
120
  // Insert new tags
80
- const tagStmt = db.prepare('INSERT INTO t_decision_tags (decision_key_id, tag_id) VALUES (?, ?)');
81
- for (const tagName of params.tags) {
82
- const tagId = getOrCreateTag(db, tagName);
83
- tagStmt.run(keyId, tagId);
121
+ for (const tagName of tags) {
122
+ const tagId = await getOrCreateTag(adapter, tagName, trx);
123
+ await knex('t_decision_tags').insert({ decision_key_id: keyId, tag_id: tagId });
84
124
  }
85
125
  }
86
126
  // Handle m_scopes (many-to-many)
87
127
  if (params.scopes && params.scopes.length > 0) {
128
+ // Parse scopes (handles both arrays and JSON strings from MCP)
129
+ const scopes = parseStringArray(params.scopes);
88
130
  // Clear existing scopes
89
- db.prepare('DELETE FROM t_decision_scopes WHERE decision_key_id = ?').run(keyId);
131
+ await knex('t_decision_scopes').where({ decision_key_id: keyId }).delete();
90
132
  // Insert new scopes
91
- const scopeStmt = db.prepare('INSERT INTO t_decision_scopes (decision_key_id, scope_id) VALUES (?, ?)');
92
- for (const scopeName of params.scopes) {
93
- const scopeId = getOrCreateScope(db, scopeName);
94
- scopeStmt.run(keyId, scopeId);
133
+ for (const scopeName of scopes) {
134
+ const scopeId = await getOrCreateScope(adapter, scopeName, trx);
135
+ await knex('t_decision_scopes').insert({ decision_key_id: keyId, scope_id: scopeId });
95
136
  }
96
137
  }
97
138
  return {
@@ -108,19 +149,32 @@ function setDecisionInternal(params, db) {
108
149
  * Supports tags, layers, scopes, and version tracking
109
150
  *
110
151
  * @param params - Decision parameters
111
- * @param db - Optional database instance (for testing)
152
+ * @param adapter - Optional database adapter (for testing)
112
153
  * @returns Response with success status and metadata
113
154
  */
114
- export function setDecision(params, db) {
115
- const actualDb = db ?? getDatabase();
155
+ export async function setDecision(params, adapter) {
156
+ debugLogFunctionEntry('setDecision', params);
157
+ const actualAdapter = adapter ?? getAdapter();
116
158
  try {
159
+ debugLogTransaction('START', 'setDecision');
117
160
  // Use transaction for atomicity
118
- return transaction(actualDb, () => {
119
- return setDecisionInternal(params, actualDb);
161
+ const result = await actualAdapter.transaction(async (trx) => {
162
+ debugLogTransaction('COMMIT', 'setDecision-transaction-begin');
163
+ const internalResult = await setDecisionInternal(params, actualAdapter, trx);
164
+ debugLogTransaction('COMMIT', 'setDecision-transaction-end');
165
+ return internalResult;
120
166
  });
167
+ debugLogFunctionExit('setDecision', true, result);
168
+ return result;
121
169
  }
122
170
  catch (error) {
123
171
  const message = error instanceof Error ? error.message : String(error);
172
+ debugLogCriticalError('setDecision', error, {
173
+ function: 'setDecision',
174
+ params
175
+ });
176
+ debugLogTransaction('ROLLBACK', 'setDecision');
177
+ debugLogFunctionExit('setDecision', false, undefined, error);
124
178
  throw new Error(`Failed to set decision: ${message}`);
125
179
  }
126
180
  }
@@ -130,33 +184,30 @@ export function setDecision(params, db) {
130
184
  * Supports filtering by status, layer, tags, and scope
131
185
  *
132
186
  * @param params - Filter parameters
133
- * @param db - Optional database instance (for testing)
187
+ * @param adapter - Optional database adapter (for testing)
134
188
  * @returns Array of t_decisions with metadata
135
189
  */
136
- export function getContext(params = {}, db) {
137
- const actualDb = db ?? getDatabase();
190
+ export async function getContext(params = {}, adapter) {
191
+ const actualAdapter = adapter ?? getAdapter();
192
+ const knex = actualAdapter.getKnex();
138
193
  try {
139
194
  // Build query dynamically based on filters
140
- let query = 'SELECT * FROM v_tagged_decisions WHERE 1=1';
141
- const queryParams = [];
195
+ let query = knex('v_tagged_decisions');
142
196
  // Filter by status
143
197
  if (params.status) {
144
198
  if (!STRING_TO_STATUS[params.status]) {
145
199
  throw new Error(`Invalid status: ${params.status}`);
146
200
  }
147
- query += ' AND status = ?';
148
- queryParams.push(params.status);
201
+ query = query.where('status', params.status);
149
202
  }
150
203
  // Filter by layer
151
204
  if (params.layer) {
152
- query += ' AND layer = ?';
153
- queryParams.push(params.layer);
205
+ query = query.where('layer', params.layer);
154
206
  }
155
207
  // Filter by scope
156
208
  if (params.scope) {
157
209
  // Use LIKE for comma-separated scopes
158
- query += ' AND scopes LIKE ?';
159
- queryParams.push(`%${params.scope}%`);
210
+ query = query.where('scopes', 'like', `%${params.scope}%`);
160
211
  }
161
212
  // Filter by tags
162
213
  if (params.tags && params.tags.length > 0) {
@@ -164,24 +215,22 @@ export function getContext(params = {}, db) {
164
215
  if (tagMatch === 'AND') {
165
216
  // All tags must be present
166
217
  for (const tag of params.tags) {
167
- query += ' AND tags LIKE ?';
168
- queryParams.push(`%${tag}%`);
218
+ query = query.where('tags', 'like', `%${tag}%`);
169
219
  }
170
220
  }
171
221
  else {
172
222
  // 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
- }
223
+ query = query.where((builder) => {
224
+ for (const tag of params.tags) {
225
+ builder.orWhere('tags', 'like', `%${tag}%`);
226
+ }
227
+ });
178
228
  }
179
229
  }
180
230
  // Order by most recent
181
- query += ' ORDER BY updated DESC';
231
+ query = query.orderBy('updated', 'desc');
182
232
  // Execute query
183
- const stmt = actualDb.prepare(query);
184
- const rows = stmt.all(...queryParams);
233
+ const rows = await query.select('*');
185
234
  return {
186
235
  decisions: rows,
187
236
  count: rows.length
@@ -198,11 +247,12 @@ export function getContext(params = {}, db) {
198
247
  * Optionally includes decision context (v3.2.2)
199
248
  *
200
249
  * @param params - Decision key and optional include_context flag
201
- * @param db - Optional database instance (for testing)
250
+ * @param adapter - Optional database adapter (for testing)
202
251
  * @returns Decision details or not found
203
252
  */
204
- export function getDecision(params, db) {
205
- const actualDb = db ?? getDatabase();
253
+ export async function getDecision(params, adapter) {
254
+ const actualAdapter = adapter ?? getAdapter();
255
+ const knex = actualAdapter.getKnex();
206
256
  // Validate parameter
207
257
  if (!params.key || params.key.trim() === '') {
208
258
  throw new Error('Parameter "key" is required and cannot be empty');
@@ -210,7 +260,7 @@ export function getDecision(params, db) {
210
260
  try {
211
261
  // If include_context is true, use the context-aware function
212
262
  if (params.include_context) {
213
- const result = dbGetDecisionWithContext(actualDb, params.key);
263
+ const result = await dbGetDecisionWithContext(actualAdapter, params.key);
214
264
  if (!result) {
215
265
  return {
216
266
  found: false
@@ -238,8 +288,9 @@ export function getDecision(params, db) {
238
288
  };
239
289
  }
240
290
  // 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);
291
+ const row = await knex('v_tagged_decisions')
292
+ .where({ key: params.key })
293
+ .first();
243
294
  if (!row) {
244
295
  return {
245
296
  found: false
@@ -260,34 +311,35 @@ export function getDecision(params, db) {
260
311
  * Provides flexible tag-based filtering with status and layer support
261
312
  *
262
313
  * @param params - Search parameters (tags, match_mode, status, layer)
263
- * @param db - Optional database instance (for testing)
314
+ * @param adapter - Optional database adapter (for testing)
264
315
  * @returns Array of t_decisions matching tag criteria
265
316
  */
266
- export function searchByTags(params, db) {
267
- const actualDb = db ?? getDatabase();
317
+ export async function searchByTags(params, adapter) {
318
+ const actualAdapter = adapter ?? getAdapter();
319
+ const knex = actualAdapter.getKnex();
268
320
  // Validate required parameters
269
321
  if (!params.tags || params.tags.length === 0) {
270
322
  throw new Error('Parameter "tags" is required and must contain at least one tag');
271
323
  }
272
324
  try {
325
+ // Parse tags (handles both arrays and JSON strings from MCP)
326
+ const tags = parseStringArray(params.tags);
273
327
  const matchMode = params.match_mode || 'OR';
274
- let query = 'SELECT * FROM v_tagged_decisions WHERE 1=1';
275
- const queryParams = [];
328
+ let query = knex('v_tagged_decisions');
276
329
  // Apply tag filtering based on match mode
277
330
  if (matchMode === 'AND') {
278
331
  // All tags must be present
279
- for (const tag of params.tags) {
280
- query += ' AND tags LIKE ?';
281
- queryParams.push(`%${tag}%`);
332
+ for (const tag of tags) {
333
+ query = query.where('tags', 'like', `%${tag}%`);
282
334
  }
283
335
  }
284
336
  else if (matchMode === 'OR') {
285
337
  // 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
- }
338
+ query = query.where((builder) => {
339
+ for (const tag of tags) {
340
+ builder.orWhere('tags', 'like', `%${tag}%`);
341
+ }
342
+ });
291
343
  }
292
344
  else {
293
345
  throw new Error(`Invalid match_mode: ${matchMode}. Must be 'AND' or 'OR'`);
@@ -297,24 +349,21 @@ export function searchByTags(params, db) {
297
349
  if (!STRING_TO_STATUS[params.status]) {
298
350
  throw new Error(`Invalid status: ${params.status}. Must be 'active', 'deprecated', or 'draft'`);
299
351
  }
300
- query += ' AND status = ?';
301
- queryParams.push(params.status);
352
+ query = query.where('status', params.status);
302
353
  }
303
354
  // Optional layer filter
304
355
  if (params.layer) {
305
356
  // Validate layer exists
306
- const layerId = getLayerId(actualDb, params.layer);
357
+ const layerId = await getLayerId(actualAdapter, params.layer);
307
358
  if (layerId === null) {
308
359
  throw new Error(`Invalid layer: ${params.layer}. Must be one of: presentation, business, data, infrastructure, cross-cutting`);
309
360
  }
310
- query += ' AND layer = ?';
311
- queryParams.push(params.layer);
361
+ query = query.where('layer', params.layer);
312
362
  }
313
363
  // Order by most recent
314
- query += ' ORDER BY updated DESC';
364
+ query = query.orderBy('updated', 'desc');
315
365
  // Execute query
316
- const stmt = actualDb.prepare(query);
317
- const rows = stmt.all(...queryParams);
366
+ const rows = await query.select('*');
318
367
  return {
319
368
  decisions: rows,
320
369
  count: rows.length
@@ -330,18 +379,21 @@ export function searchByTags(params, db) {
330
379
  * Returns all historical versions ordered by timestamp (newest first)
331
380
  *
332
381
  * @param params - Decision key to get history for
333
- * @param db - Optional database instance (for testing)
382
+ * @param adapter - Optional database adapter (for testing)
334
383
  * @returns Array of historical versions with metadata
335
384
  */
336
- export function getVersions(params, db) {
337
- const actualDb = db ?? getDatabase();
385
+ export async function getVersions(params, adapter) {
386
+ const actualAdapter = adapter ?? getAdapter();
387
+ const knex = actualAdapter.getKnex();
338
388
  // Validate required parameter
339
389
  if (!params.key || params.key.trim() === '') {
340
390
  throw new Error('Parameter "key" is required and cannot be empty');
341
391
  }
342
392
  try {
343
393
  // Get key_id for the decision
344
- const keyResult = actualDb.prepare('SELECT id FROM m_context_keys WHERE key = ?').get(params.key);
394
+ const keyResult = await knex('m_context_keys')
395
+ .where({ key: params.key })
396
+ .first('id');
345
397
  if (!keyResult) {
346
398
  // Key doesn't exist, return empty history
347
399
  return {
@@ -352,18 +404,11 @@ export function getVersions(params, db) {
352
404
  }
353
405
  const keyId = keyResult.id;
354
406
  // 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);
407
+ const rows = await knex('t_decision_history as dh')
408
+ .leftJoin('m_agents as a', 'dh.agent_id', 'a.id')
409
+ .where('dh.key_id', keyId)
410
+ .select('dh.version', 'dh.value', 'a.name as agent_name', knex.raw(`datetime(dh.ts, 'unixepoch') as timestamp`))
411
+ .orderBy('dh.ts', 'desc');
367
412
  // Transform to response format
368
413
  const history = rows.map(row => ({
369
414
  version: row.version,
@@ -387,18 +432,19 @@ export function getVersions(params, db) {
387
432
  * Supports status filtering and optional tag inclusion
388
433
  *
389
434
  * @param params - Layer name, optional status and include_tags
390
- * @param db - Optional database instance (for testing)
435
+ * @param adapter - Optional database adapter (for testing)
391
436
  * @returns Array of t_decisions in the specified layer
392
437
  */
393
- export function searchByLayer(params, db) {
394
- const actualDb = db ?? getDatabase();
438
+ export async function searchByLayer(params, adapter) {
439
+ const actualAdapter = adapter ?? getAdapter();
440
+ const knex = actualAdapter.getKnex();
395
441
  // Validate required parameter
396
442
  if (!params.layer || params.layer.trim() === '') {
397
443
  throw new Error('Parameter "layer" is required and cannot be empty');
398
444
  }
399
445
  try {
400
446
  // Validate layer exists
401
- const layerId = getLayerId(actualDb, params.layer);
447
+ const layerId = await getLayerId(actualAdapter, params.layer);
402
448
  if (layerId === null) {
403
449
  throw new Error(`Invalid layer: ${params.layer}. Must be one of: presentation, business, data, infrastructure, cross-cutting`);
404
450
  }
@@ -409,69 +455,34 @@ export function searchByLayer(params, db) {
409
455
  if (!STRING_TO_STATUS[statusValue]) {
410
456
  throw new Error(`Invalid status: ${statusValue}. Must be 'active', 'deprecated', or 'draft'`);
411
457
  }
412
- let query;
413
- const queryParams = [params.layer, statusValue];
458
+ let rows;
414
459
  if (includeTagsValue) {
415
460
  // 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
- `;
461
+ rows = await knex('v_tagged_decisions')
462
+ .where({ layer: params.layer, status: statusValue })
463
+ .orderBy('updated', 'desc')
464
+ .select('*');
421
465
  }
422
466
  else {
423
467
  // 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);
468
+ const statusInt = STRING_TO_STATUS[statusValue];
469
+ const stringDecisions = knex('t_decisions as d')
470
+ .innerJoin('m_context_keys as ck', 'd.key_id', 'ck.id')
471
+ .leftJoin('m_layers as l', 'd.layer_id', 'l.id')
472
+ .leftJoin('m_agents as a', 'd.agent_id', 'a.id')
473
+ .where('l.name', params.layer)
474
+ .where('d.status', statusInt)
475
+ .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`));
476
+ const numericDecisions = knex('t_decisions_numeric as dn')
477
+ .innerJoin('m_context_keys as ck', 'dn.key_id', 'ck.id')
478
+ .leftJoin('m_layers as l', 'dn.layer_id', 'l.id')
479
+ .leftJoin('m_agents as a', 'dn.agent_id', 'a.id')
480
+ .where('l.name', params.layer)
481
+ .where('dn.status', statusInt)
482
+ .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`));
483
+ // Union both queries
484
+ rows = await stringDecisions.union([numericDecisions]).orderBy('updated', 'desc');
471
485
  }
472
- // Execute query
473
- const stmt = actualDb.prepare(query);
474
- const rows = stmt.all(...queryParams);
475
486
  return {
476
487
  layer: params.layer,
477
488
  decisions: rows,
@@ -508,10 +519,10 @@ export function searchByLayer(params, db) {
508
519
  * All inferred fields can be overridden via optional parameters.
509
520
  *
510
521
  * @param params - Quick set parameters (key and value required)
511
- * @param db - Optional database instance (for testing)
522
+ * @param adapter - Optional database adapter (for testing)
512
523
  * @returns Response with success status and inferred metadata
513
524
  */
514
- export function quickSetDecision(params, db) {
525
+ export async function quickSetDecision(params, adapter) {
515
526
  // Validate required parameters
516
527
  if (!params.key || params.key.trim() === '') {
517
528
  throw new Error('Parameter "key" is required and cannot be empty');
@@ -575,8 +586,8 @@ export function quickSetDecision(params, db) {
575
586
  tags: inferredTags,
576
587
  scopes: inferredScopes
577
588
  };
578
- // Call setDecision with full params (pass db if provided)
579
- const result = setDecision(fullParams, db);
589
+ // Call setDecision with full params (pass adapter if provided)
590
+ const result = await setDecision(fullParams, adapter);
580
591
  // Return response with inferred metadata
581
592
  return {
582
593
  success: result.success,
@@ -603,11 +614,12 @@ export function quickSetDecision(params, db) {
603
614
  * - search_text: Full-text search in value field
604
615
  *
605
616
  * @param params - Advanced search parameters with filtering, sorting, pagination
606
- * @param db - Optional database instance (for testing)
617
+ * @param adapter - Optional database adapter (for testing)
607
618
  * @returns Filtered decisions with total count for pagination
608
619
  */
609
- export function searchAdvanced(params = {}, db) {
610
- const actualDb = db ?? getDatabase();
620
+ export async function searchAdvanced(params = {}, adapter) {
621
+ const actualAdapter = adapter ?? getAdapter();
622
+ const knex = actualAdapter.getKnex();
611
623
  try {
612
624
  // Parse relative time to Unix timestamp
613
625
  const parseRelativeTime = (relativeTime) => {
@@ -631,60 +643,70 @@ export function searchAdvanced(params = {}, db) {
631
643
  }
632
644
  };
633
645
  // Build base query using v_tagged_decisions view
634
- let query = 'SELECT * FROM v_tagged_decisions WHERE 1=1';
635
- const queryParams = [];
646
+ let query = knex('v_tagged_decisions');
636
647
  // Filter by layers (OR relationship)
637
648
  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);
649
+ query = query.whereIn('layer', params.layers);
641
650
  }
642
651
  // Filter by tags_all (AND relationship - must have ALL tags)
643
652
  if (params.tags_all && params.tags_all.length > 0) {
644
- for (const tag of params.tags_all) {
645
- query += ' AND (tags LIKE ? OR tags = ?)';
646
- queryParams.push(`%${tag}%`, tag);
653
+ // Parse tags (handles both arrays and JSON strings from MCP)
654
+ const tagsAll = parseStringArray(params.tags_all);
655
+ for (const tag of tagsAll) {
656
+ query = query.where((builder) => {
657
+ builder.where('tags', 'like', `%${tag}%`).orWhere('tags', tag);
658
+ });
647
659
  }
648
660
  }
649
661
  // Filter by tags_any (OR relationship - must have ANY tag)
650
662
  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
- }
663
+ // Parse tags (handles both arrays and JSON strings from MCP)
664
+ const tagsAny = parseStringArray(params.tags_any);
665
+ query = query.where((builder) => {
666
+ for (const tag of tagsAny) {
667
+ builder.orWhere('tags', 'like', `%${tag}%`).orWhere('tags', tag);
668
+ }
669
+ });
656
670
  }
657
671
  // Exclude tags
658
672
  if (params.exclude_tags && params.exclude_tags.length > 0) {
659
- for (const tag of params.exclude_tags) {
660
- query += ' AND (tags IS NULL OR (tags NOT LIKE ? AND tags != ?))';
661
- queryParams.push(`%${tag}%`, tag);
673
+ // Parse tags (handles both arrays and JSON strings from MCP)
674
+ const excludeTags = parseStringArray(params.exclude_tags);
675
+ for (const tag of excludeTags) {
676
+ query = query.where((builder) => {
677
+ builder.whereNull('tags')
678
+ .orWhere((subBuilder) => {
679
+ subBuilder.where('tags', 'not like', `%${tag}%`)
680
+ .where('tags', '!=', tag);
681
+ });
682
+ });
662
683
  }
663
684
  }
664
685
  // Filter by scopes with wildcard support
665
686
  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);
687
+ // Parse scopes (handles both arrays and JSON strings from MCP)
688
+ const scopes = parseStringArray(params.scopes);
689
+ query = query.where((builder) => {
690
+ for (const scope of scopes) {
691
+ if (scope.includes('*')) {
692
+ // Wildcard pattern - convert to LIKE pattern
693
+ const likePattern = scope.replace(/\*/g, '%');
694
+ builder.orWhere('scopes', 'like', `%${likePattern}%`)
695
+ .orWhere('scopes', likePattern);
696
+ }
697
+ else {
698
+ // Exact match
699
+ builder.orWhere('scopes', 'like', `%${scope}%`)
700
+ .orWhere('scopes', scope);
701
+ }
678
702
  }
679
- }
680
- query += ` AND (${scopeConditions.join(' OR ')})`;
703
+ });
681
704
  }
682
705
  // Temporal filtering - updated_after
683
706
  if (params.updated_after) {
684
707
  const timestamp = parseRelativeTime(params.updated_after);
685
708
  if (timestamp !== null) {
686
- query += ' AND (SELECT unixepoch(updated)) >= ?';
687
- queryParams.push(timestamp);
709
+ query = query.whereRaw('unixepoch(updated) >= ?', [timestamp]);
688
710
  }
689
711
  else {
690
712
  throw new Error(`Invalid updated_after format: ${params.updated_after}. Use ISO timestamp or relative time like "7d", "2h", "30m"`);
@@ -694,8 +716,7 @@ export function searchAdvanced(params = {}, db) {
694
716
  if (params.updated_before) {
695
717
  const timestamp = parseRelativeTime(params.updated_before);
696
718
  if (timestamp !== null) {
697
- query += ' AND (SELECT unixepoch(updated)) <= ?';
698
- queryParams.push(timestamp);
719
+ query = query.whereRaw('unixepoch(updated) <= ?', [timestamp]);
699
720
  }
700
721
  else {
701
722
  throw new Error(`Invalid updated_before format: ${params.updated_before}. Use ISO timestamp or relative time like "7d", "2h", "30m"`);
@@ -703,25 +724,19 @@ export function searchAdvanced(params = {}, db) {
703
724
  }
704
725
  // Filter by decided_by (OR relationship)
705
726
  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);
727
+ query = query.whereIn('decided_by', params.decided_by);
709
728
  }
710
729
  // Filter by statuses (OR relationship)
711
730
  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);
731
+ query = query.whereIn('status', params.statuses);
715
732
  }
716
733
  // Full-text search in value field
717
734
  if (params.search_text) {
718
- query += ' AND value LIKE ?';
719
- queryParams.push(`%${params.search_text}%`);
735
+ query = query.where('value', 'like', `%${params.search_text}%`);
720
736
  }
721
737
  // 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);
738
+ const countQuery = query.clone().count('* as total');
739
+ const countResult = await countQuery.first();
725
740
  const totalCount = countResult.total;
726
741
  // Sorting
727
742
  const sortBy = params.sort_by || 'updated';
@@ -733,7 +748,7 @@ export function searchAdvanced(params = {}, db) {
733
748
  if (!['asc', 'desc'].includes(sortOrder)) {
734
749
  throw new Error(`Invalid sort_order: ${sortOrder}. Must be 'asc' or 'desc'`);
735
750
  }
736
- query += ` ORDER BY ${sortBy} ${sortOrder.toUpperCase()}`;
751
+ query = query.orderBy(sortBy, sortOrder);
737
752
  // Pagination
738
753
  const limit = params.limit !== undefined ? params.limit : 20;
739
754
  const offset = params.offset || 0;
@@ -744,11 +759,9 @@ export function searchAdvanced(params = {}, db) {
744
759
  if (offset < 0) {
745
760
  throw new Error('Parameter "offset" must be non-negative');
746
761
  }
747
- query += ' LIMIT ? OFFSET ?';
748
- queryParams.push(limit, offset);
762
+ query = query.limit(limit).offset(offset);
749
763
  // Execute query
750
- const stmt = actualDb.prepare(query);
751
- const rows = stmt.all(...queryParams);
764
+ const rows = await query.select('*');
752
765
  return {
753
766
  decisions: rows,
754
767
  count: rows.length,
@@ -766,38 +779,100 @@ export function searchAdvanced(params = {}, db) {
766
779
  * Limit: 50 items per batch (constraint #3)
767
780
  *
768
781
  * @param params - Batch parameters with array of decisions and atomic flag
769
- * @param db - Optional database instance (for testing)
782
+ * @param adapter - Optional database adapter (for testing)
770
783
  * @returns Response with success status and detailed results for each item
771
784
  */
772
- export function setDecisionBatch(params, db) {
773
- const actualDb = db ?? getDatabase();
785
+ export async function setDecisionBatch(params, adapter) {
786
+ const actualAdapter = adapter ?? getAdapter();
774
787
  // Validate required parameters
775
788
  if (!params.decisions || !Array.isArray(params.decisions)) {
776
789
  throw new Error('Parameter "decisions" is required and must be an array');
777
790
  }
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);
791
+ if (params.decisions.length === 0) {
782
792
  return {
783
- key: decision.key,
784
- key_id: result.key_id,
785
- version: result.version
793
+ success: true,
794
+ inserted: 0,
795
+ failed: 0,
796
+ results: []
786
797
  };
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
- };
798
+ }
799
+ if (params.decisions.length > 50) {
800
+ throw new Error('Parameter "decisions" must contain at most 50 items');
801
+ }
802
+ const atomic = params.atomic !== undefined ? params.atomic : true;
803
+ try {
804
+ if (atomic) {
805
+ // Atomic mode: All or nothing
806
+ const results = await actualAdapter.transaction(async (trx) => {
807
+ const processedResults = [];
808
+ for (const decision of params.decisions) {
809
+ try {
810
+ const result = await setDecisionInternal(decision, actualAdapter, trx);
811
+ processedResults.push({
812
+ key: decision.key,
813
+ key_id: result.key_id,
814
+ version: result.version,
815
+ success: true,
816
+ error: undefined
817
+ });
818
+ }
819
+ catch (error) {
820
+ const message = error instanceof Error ? error.message : String(error);
821
+ throw new Error(`Batch failed at decision "${decision.key}": ${message}`);
822
+ }
823
+ }
824
+ return processedResults;
825
+ });
826
+ return {
827
+ success: true,
828
+ inserted: results.length,
829
+ failed: 0,
830
+ results: results
831
+ };
832
+ }
833
+ else {
834
+ // Non-atomic mode: Process each independently
835
+ const results = [];
836
+ let inserted = 0;
837
+ let failed = 0;
838
+ for (const decision of params.decisions) {
839
+ try {
840
+ const result = await actualAdapter.transaction(async (trx) => {
841
+ return await setDecisionInternal(decision, actualAdapter, trx);
842
+ });
843
+ results.push({
844
+ key: decision.key,
845
+ key_id: result.key_id,
846
+ version: result.version,
847
+ success: true,
848
+ error: undefined
849
+ });
850
+ inserted++;
851
+ }
852
+ catch (error) {
853
+ const message = error instanceof Error ? error.message : String(error);
854
+ results.push({
855
+ key: decision.key,
856
+ key_id: undefined,
857
+ version: undefined,
858
+ success: false,
859
+ error: message
860
+ });
861
+ failed++;
862
+ }
863
+ }
864
+ return {
865
+ success: failed === 0,
866
+ inserted: inserted,
867
+ failed: failed,
868
+ results: results
869
+ };
870
+ }
871
+ }
872
+ catch (error) {
873
+ const message = error instanceof Error ? error.message : String(error);
874
+ throw new Error(`Failed to execute batch operation: ${message}`);
875
+ }
801
876
  }
802
877
  /**
803
878
  * Check for updates since a given timestamp (FR-003 Phase A)
@@ -805,11 +880,12 @@ export function setDecisionBatch(params, db) {
805
880
  * Token cost: ~5-10 tokens per check
806
881
  *
807
882
  * @param params - Agent name and since_timestamp (ISO 8601)
808
- * @param db - Optional database instance (for testing)
883
+ * @param adapter - Optional database adapter (for testing)
809
884
  * @returns Boolean flag and counts for decisions, messages, files
810
885
  */
811
- export function hasUpdates(params, db) {
812
- const actualDb = db ?? getDatabase();
886
+ export async function hasUpdates(params, adapter) {
887
+ const actualAdapter = adapter ?? getAdapter();
888
+ const knex = actualAdapter.getKnex();
813
889
  // Validate required parameters
814
890
  if (!params.agent_name || params.agent_name.trim() === '') {
815
891
  throw new Error('Parameter "agent_name" is required and cannot be empty');
@@ -825,38 +901,43 @@ export function hasUpdates(params, db) {
825
901
  }
826
902
  const sinceTs = Math.floor(sinceDate.getTime() / 1000);
827
903
  // 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;
904
+ const decisionCount1 = await knex('t_decisions')
905
+ .where('ts', '>', sinceTs)
906
+ .count('* as count')
907
+ .first();
908
+ const decisionCount2 = await knex('t_decisions_numeric')
909
+ .where('ts', '>', sinceTs)
910
+ .count('* as count')
911
+ .first();
912
+ const decisionsCount = (decisionCount1?.count || 0) + (decisionCount2?.count || 0);
837
913
  // Get agent_id for the requesting agent
838
- const agentResult = actualDb.prepare('SELECT id FROM m_agents WHERE name = ?').get(params.agent_name);
914
+ const agentResult = await knex('m_agents')
915
+ .where({ name: params.agent_name })
916
+ .first('id');
839
917
  // Count messages for the agent (received messages - to_agent_id matches OR broadcast messages)
840
918
  let messagesCount = 0;
841
919
  if (agentResult) {
842
920
  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;
921
+ const messageResult = await knex('t_agent_messages')
922
+ .where('ts', '>', sinceTs)
923
+ .where((builder) => {
924
+ builder.where('to_agent_id', agentId)
925
+ .orWhereNull('to_agent_id');
926
+ })
927
+ .count('* as count')
928
+ .first();
929
+ messagesCount = messageResult?.count || 0;
849
930
  }
850
931
  // 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;
932
+ const fileResult = await knex('t_file_changes')
933
+ .where('ts', '>', sinceTs)
934
+ .count('* as count')
935
+ .first();
936
+ const filesCount = fileResult?.count || 0;
856
937
  // Determine if there are any updates
857
- const hasUpdates = decisionsCount > 0 || messagesCount > 0 || filesCount > 0;
938
+ const hasUpdatesFlag = decisionsCount > 0 || messagesCount > 0 || filesCount > 0;
858
939
  return {
859
- has_updates: hasUpdates,
940
+ has_updates: hasUpdatesFlag,
860
941
  counts: {
861
942
  decisions: decisionsCount,
862
943
  messages: messagesCount,
@@ -875,11 +956,12 @@ export function hasUpdates(params, db) {
875
956
  * Validates required fields if template specifies any
876
957
  *
877
958
  * @param params - Template name, key, value, and optional overrides
878
- * @param db - Optional database instance (for testing)
959
+ * @param adapter - Optional database adapter (for testing)
879
960
  * @returns Response with success status and applied defaults metadata
880
961
  */
881
- export function setFromTemplate(params, db) {
882
- const actualDb = db ?? getDatabase();
962
+ export async function setFromTemplate(params, adapter) {
963
+ const actualAdapter = adapter ?? getAdapter();
964
+ const knex = actualAdapter.getKnex();
883
965
  // Validate required parameters
884
966
  if (!params.template || params.template.trim() === '') {
885
967
  throw new Error('Parameter "template" is required and cannot be empty');
@@ -892,7 +974,9 @@ export function setFromTemplate(params, db) {
892
974
  }
893
975
  try {
894
976
  // Get template
895
- const templateRow = actualDb.prepare('SELECT * FROM t_decision_templates WHERE name = ?').get(params.template);
977
+ const templateRow = await knex('t_decision_templates')
978
+ .where({ name: params.template })
979
+ .first();
896
980
  if (!templateRow) {
897
981
  throw new Error(`Template not found: ${params.template}`);
898
982
  }
@@ -930,8 +1014,8 @@ export function setFromTemplate(params, db) {
930
1014
  if (!params.status && defaults.status) {
931
1015
  appliedDefaults.status = defaults.status;
932
1016
  }
933
- // Call setDecision with merged params (pass db if provided)
934
- const result = setDecision(decisionParams, actualDb);
1017
+ // Call setDecision with merged params (pass adapter if provided)
1018
+ const result = await setDecision(decisionParams, actualAdapter);
935
1019
  return {
936
1020
  success: result.success,
937
1021
  key: result.key,
@@ -952,11 +1036,12 @@ export function setFromTemplate(params, db) {
952
1036
  * Defines reusable defaults and required fields for decisions
953
1037
  *
954
1038
  * @param params - Template name, defaults, required fields, and creator
955
- * @param db - Optional database instance (for testing)
1039
+ * @param adapter - Optional database adapter (for testing)
956
1040
  * @returns Response with success status and template ID
957
1041
  */
958
- export function createTemplate(params, db) {
959
- const actualDb = db ?? getDatabase();
1042
+ export async function createTemplate(params, adapter) {
1043
+ const actualAdapter = adapter ?? getAdapter();
1044
+ const knex = actualAdapter.getKnex();
960
1045
  // Validate required parameters
961
1046
  if (!params.name || params.name.trim() === '') {
962
1047
  throw new Error('Parameter "name" is required and cannot be empty');
@@ -965,10 +1050,10 @@ export function createTemplate(params, db) {
965
1050
  throw new Error('Parameter "defaults" is required and must be an object');
966
1051
  }
967
1052
  try {
968
- return transaction(actualDb, () => {
1053
+ return await actualAdapter.transaction(async (trx) => {
969
1054
  // Validate layer if provided in defaults
970
1055
  if (params.defaults.layer) {
971
- const layerId = getLayerId(actualDb, params.defaults.layer);
1056
+ const layerId = await getLayerId(actualAdapter, params.defaults.layer, trx);
972
1057
  if (layerId === null) {
973
1058
  throw new Error(`Invalid layer in defaults: ${params.defaults.layer}. Must be one of: presentation, business, data, infrastructure, cross-cutting`);
974
1059
  }
@@ -980,20 +1065,21 @@ export function createTemplate(params, db) {
980
1065
  // Get or create agent if creator specified
981
1066
  let createdById = null;
982
1067
  if (params.created_by) {
983
- createdById = getOrCreateAgent(actualDb, params.created_by);
1068
+ createdById = await getOrCreateAgent(actualAdapter, params.created_by, trx);
984
1069
  }
985
1070
  // Serialize defaults and required fields
986
1071
  const defaultsJson = JSON.stringify(params.defaults);
987
1072
  const requiredFieldsJson = params.required_fields ? JSON.stringify(params.required_fields) : null;
988
1073
  // 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);
1074
+ const [id] = await trx('t_decision_templates').insert({
1075
+ name: params.name,
1076
+ defaults: defaultsJson,
1077
+ required_fields: requiredFieldsJson,
1078
+ created_by: createdById
1079
+ });
994
1080
  return {
995
1081
  success: true,
996
- template_id: info.lastInsertRowid,
1082
+ template_id: id,
997
1083
  template_name: params.name,
998
1084
  message: `Template "${params.name}" created successfully`
999
1085
  };
@@ -1009,25 +1095,17 @@ export function createTemplate(params, db) {
1009
1095
  * Returns all templates with their defaults and metadata
1010
1096
  *
1011
1097
  * @param params - No parameters required
1012
- * @param db - Optional database instance (for testing)
1098
+ * @param adapter - Optional database adapter (for testing)
1013
1099
  * @returns Array of all templates with parsed JSON fields
1014
1100
  */
1015
- export function listTemplates(params = {}, db) {
1016
- const actualDb = db ?? getDatabase();
1101
+ export async function listTemplates(params = {}, adapter) {
1102
+ const actualAdapter = adapter ?? getAdapter();
1103
+ const knex = actualAdapter.getKnex();
1017
1104
  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();
1105
+ const rows = await knex('t_decision_templates as t')
1106
+ .leftJoin('m_agents as a', 't.created_by', 'a.id')
1107
+ .select('t.id', 't.name', 't.defaults', 't.required_fields', 'a.name as created_by', knex.raw(`datetime(t.ts, 'unixepoch') as created_at`))
1108
+ .orderBy('t.name', 'asc');
1031
1109
  // Parse JSON fields
1032
1110
  const templates = rows.map(row => ({
1033
1111
  id: row.id,
@@ -1060,19 +1138,22 @@ export function listTemplates(params = {}, db) {
1060
1138
  * (tags, scopes) will also be deleted due to CASCADE constraints.
1061
1139
  *
1062
1140
  * @param params - Decision key to delete
1063
- * @param db - Optional database instance (for testing)
1141
+ * @param adapter - Optional database adapter (for testing)
1064
1142
  * @returns Response with success status
1065
1143
  */
1066
- export function hardDeleteDecision(params, db) {
1067
- const actualDb = db ?? getDatabase();
1144
+ export async function hardDeleteDecision(params, adapter) {
1145
+ const actualAdapter = adapter ?? getAdapter();
1146
+ const knex = actualAdapter.getKnex();
1068
1147
  // Validate parameter
1069
1148
  if (!params.key || params.key.trim() === '') {
1070
1149
  throw new Error('Parameter "key" is required and cannot be empty');
1071
1150
  }
1072
1151
  try {
1073
- return transaction(actualDb, () => {
1152
+ return await actualAdapter.transaction(async (trx) => {
1074
1153
  // Get key_id
1075
- const keyResult = actualDb.prepare('SELECT id FROM m_context_keys WHERE key = ?').get(params.key);
1154
+ const keyResult = await trx('m_context_keys')
1155
+ .where({ key: params.key })
1156
+ .first('id');
1076
1157
  if (!keyResult) {
1077
1158
  // Key doesn't exist - still return success (idempotent)
1078
1159
  return {
@@ -1083,17 +1164,17 @@ export function hardDeleteDecision(params, db) {
1083
1164
  }
1084
1165
  const keyId = keyResult.id;
1085
1166
  // Delete from t_decisions (if exists)
1086
- const deletedString = actualDb.prepare('DELETE FROM t_decisions WHERE key_id = ?').run(keyId);
1167
+ const deletedString = await trx('t_decisions').where({ key_id: keyId }).delete();
1087
1168
  // Delete from t_decisions_numeric (if exists)
1088
- const deletedNumeric = actualDb.prepare('DELETE FROM t_decisions_numeric WHERE key_id = ?').run(keyId);
1169
+ const deletedNumeric = await trx('t_decisions_numeric').where({ key_id: keyId }).delete();
1089
1170
  // 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);
1171
+ const deletedHistory = await trx('t_decision_history').where({ key_id: keyId }).delete();
1091
1172
  // 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);
1173
+ const deletedTags = await trx('t_decision_tags').where({ decision_key_id: keyId }).delete();
1093
1174
  // 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);
1175
+ const deletedScopes = await trx('t_decision_scopes').where({ decision_key_id: keyId }).delete();
1095
1176
  // Calculate total deleted records
1096
- const totalDeleted = deletedString.changes + deletedNumeric.changes + deletedHistory.changes + deletedTags.changes + deletedScopes.changes;
1177
+ const totalDeleted = deletedString + deletedNumeric + deletedHistory + deletedTags + deletedScopes;
1097
1178
  if (totalDeleted === 0) {
1098
1179
  return {
1099
1180
  success: true,
@@ -1121,11 +1202,11 @@ export function hardDeleteDecision(params, db) {
1121
1202
  * Adds rich context (rationale, alternatives, tradeoffs) to a decision
1122
1203
  *
1123
1204
  * @param params - Context parameters
1124
- * @param db - Optional database instance (for testing)
1205
+ * @param adapter - Optional database adapter (for testing)
1125
1206
  * @returns Response with success status
1126
1207
  */
1127
- export function addDecisionContextAction(params, db) {
1128
- const actualDb = db ?? getDatabase();
1208
+ export async function addDecisionContextAction(params, adapter) {
1209
+ const actualAdapter = adapter ?? getAdapter();
1129
1210
  // Validate required parameters
1130
1211
  if (!params.key || params.key.trim() === '') {
1131
1212
  throw new Error('Parameter "key" is required and cannot be empty');
@@ -1137,14 +1218,34 @@ export function addDecisionContextAction(params, db) {
1137
1218
  // Parse JSON if provided as strings
1138
1219
  let alternatives = params.alternatives_considered || null;
1139
1220
  let tradeoffs = params.tradeoffs || null;
1140
- // If already objects, stringify them
1141
- if (alternatives && typeof alternatives === 'object') {
1142
- alternatives = JSON.stringify(alternatives);
1221
+ // Convert to JSON strings
1222
+ if (alternatives !== null) {
1223
+ if (typeof alternatives === 'object') {
1224
+ alternatives = JSON.stringify(alternatives);
1225
+ }
1226
+ else if (typeof alternatives === 'string') {
1227
+ try {
1228
+ JSON.parse(alternatives);
1229
+ }
1230
+ catch {
1231
+ alternatives = JSON.stringify([alternatives]);
1232
+ }
1233
+ }
1143
1234
  }
1144
- if (tradeoffs && typeof tradeoffs === 'object') {
1145
- tradeoffs = JSON.stringify(tradeoffs);
1235
+ if (tradeoffs !== null) {
1236
+ if (typeof tradeoffs === 'object') {
1237
+ tradeoffs = JSON.stringify(tradeoffs);
1238
+ }
1239
+ else if (typeof tradeoffs === 'string') {
1240
+ try {
1241
+ JSON.parse(tradeoffs);
1242
+ }
1243
+ catch {
1244
+ tradeoffs = JSON.stringify({ description: tradeoffs });
1245
+ }
1246
+ }
1146
1247
  }
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);
1248
+ 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
1249
  return {
1149
1250
  success: true,
1150
1251
  context_id: contextId,
@@ -1162,13 +1263,13 @@ export function addDecisionContextAction(params, db) {
1162
1263
  * Query decision contexts with optional filters
1163
1264
  *
1164
1265
  * @param params - Filter parameters
1165
- * @param db - Optional database instance (for testing)
1266
+ * @param adapter - Optional database adapter (for testing)
1166
1267
  * @returns Array of decision contexts
1167
1268
  */
1168
- export function listDecisionContextsAction(params, db) {
1169
- const actualDb = db ?? getDatabase();
1269
+ export async function listDecisionContextsAction(params, adapter) {
1270
+ const actualAdapter = adapter ?? getAdapter();
1170
1271
  try {
1171
- const contexts = dbListDecisionContexts(actualDb, {
1272
+ const contexts = await dbListDecisionContexts(actualAdapter, {
1172
1273
  decisionKey: params.decision_key,
1173
1274
  relatedTaskId: params.related_task_id,
1174
1275
  relatedConstraintId: params.related_constraint_id,