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