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.
- package/CHANGELOG.md +247 -1772
- package/README.md +70 -304
- package/assets/config.example.toml +106 -0
- package/dist/adapters/index.d.ts +11 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +21 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/mysql-adapter.d.ts +31 -0
- package/dist/adapters/mysql-adapter.d.ts.map +1 -0
- package/dist/adapters/mysql-adapter.js +63 -0
- package/dist/adapters/mysql-adapter.js.map +1 -0
- package/dist/adapters/postgresql-adapter.d.ts +31 -0
- package/dist/adapters/postgresql-adapter.d.ts.map +1 -0
- package/dist/adapters/postgresql-adapter.js +63 -0
- package/dist/adapters/postgresql-adapter.js.map +1 -0
- package/dist/adapters/sqlite-adapter.d.ts +37 -0
- package/dist/adapters/sqlite-adapter.d.ts.map +1 -0
- package/dist/adapters/sqlite-adapter.js +129 -0
- package/dist/adapters/sqlite-adapter.js.map +1 -0
- package/dist/adapters/types.d.ts +33 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +2 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/cli.js +55 -54
- package/dist/cli.js.map +1 -1
- package/dist/config/example-generator.d.ts +11 -0
- package/dist/config/example-generator.d.ts.map +1 -0
- package/dist/config/example-generator.js +48 -0
- package/dist/config/example-generator.js.map +1 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +4 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/config/types.d.ts +11 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js.map +1 -1
- package/dist/database.d.ts +56 -121
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +266 -414
- package/dist/database.js.map +1 -1
- package/dist/index.js +329 -245
- package/dist/index.js.map +1 -1
- package/dist/knexfile.d.ts +6 -0
- package/dist/knexfile.d.ts.map +1 -0
- package/dist/knexfile.js +85 -0
- package/dist/knexfile.js.map +1 -0
- package/dist/migrations/add-help-system-tables.d.ts +35 -0
- package/dist/migrations/add-help-system-tables.d.ts.map +1 -0
- package/dist/migrations/add-help-system-tables.js +206 -0
- package/dist/migrations/add-help-system-tables.js.map +1 -0
- package/dist/migrations/add-token-tracking.d.ts +28 -0
- package/dist/migrations/add-token-tracking.d.ts.map +1 -0
- package/dist/migrations/add-token-tracking.js +108 -0
- package/dist/migrations/add-token-tracking.js.map +1 -0
- package/dist/migrations/index.d.ts +25 -12
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +147 -20
- package/dist/migrations/index.js.map +1 -1
- package/dist/migrations/knex/20251025020452_create_master_tables.d.ts +4 -0
- package/dist/migrations/knex/20251025020452_create_master_tables.d.ts.map +1 -0
- package/dist/migrations/knex/20251025020452_create_master_tables.js +65 -0
- package/dist/migrations/knex/20251025020452_create_master_tables.js.map +1 -0
- package/dist/migrations/knex/20251025021152_create_transaction_tables.d.ts +4 -0
- package/dist/migrations/knex/20251025021152_create_transaction_tables.d.ts.map +1 -0
- package/dist/migrations/knex/20251025021152_create_transaction_tables.js +235 -0
- package/dist/migrations/knex/20251025021152_create_transaction_tables.js.map +1 -0
- package/dist/migrations/knex/20251025021351_create_indexes.d.ts +4 -0
- package/dist/migrations/knex/20251025021351_create_indexes.d.ts.map +1 -0
- package/dist/migrations/knex/20251025021351_create_indexes.js +62 -0
- package/dist/migrations/knex/20251025021351_create_indexes.js.map +1 -0
- package/dist/migrations/knex/20251025021416_seed_master_data.d.ts +4 -0
- package/dist/migrations/knex/20251025021416_seed_master_data.d.ts.map +1 -0
- package/dist/migrations/knex/20251025021416_seed_master_data.js +58 -0
- package/dist/migrations/knex/20251025021416_seed_master_data.js.map +1 -0
- package/dist/migrations/knex/20251025070349_create_views.d.ts +4 -0
- package/dist/migrations/knex/20251025070349_create_views.d.ts.map +1 -0
- package/dist/migrations/knex/20251025070349_create_views.js +143 -0
- package/dist/migrations/knex/20251025070349_create_views.js.map +1 -0
- package/dist/migrations/knex/20251025081221_add_link_type_to_task_decision_links.d.ts +4 -0
- package/dist/migrations/knex/20251025081221_add_link_type_to_task_decision_links.d.ts.map +1 -0
- package/dist/migrations/knex/20251025081221_add_link_type_to_task_decision_links.js +15 -0
- package/dist/migrations/knex/20251025081221_add_link_type_to_task_decision_links.js.map +1 -0
- package/dist/migrations/knex/20251025082220_fix_task_dependencies_columns.d.ts +8 -0
- package/dist/migrations/knex/20251025082220_fix_task_dependencies_columns.d.ts.map +1 -0
- package/dist/migrations/knex/20251025082220_fix_task_dependencies_columns.js +12 -0
- package/dist/migrations/knex/20251025082220_fix_task_dependencies_columns.js.map +1 -0
- package/dist/migrations/knex/20251025090000_create_help_system_tables.d.ts +19 -0
- package/dist/migrations/knex/20251025090000_create_help_system_tables.d.ts.map +1 -0
- package/dist/migrations/knex/20251025090000_create_help_system_tables.js +115 -0
- package/dist/migrations/knex/20251025090000_create_help_system_tables.js.map +1 -0
- package/dist/migrations/knex/20251025090100_seed_help_categories_and_use_cases.d.ts +13 -0
- package/dist/migrations/knex/20251025090100_seed_help_categories_and_use_cases.d.ts.map +1 -0
- package/dist/migrations/knex/20251025090100_seed_help_categories_and_use_cases.js +377 -0
- package/dist/migrations/knex/20251025090100_seed_help_categories_and_use_cases.js.map +1 -0
- package/dist/migrations/knex/20251025100000_seed_help_metadata.d.ts +15 -0
- package/dist/migrations/knex/20251025100000_seed_help_metadata.d.ts.map +1 -0
- package/dist/migrations/knex/20251025100000_seed_help_metadata.js +253 -0
- package/dist/migrations/knex/20251025100000_seed_help_metadata.js.map +1 -0
- package/dist/migrations/knex/20251025100100_seed_remaining_use_cases.d.ts +16 -0
- package/dist/migrations/knex/20251025100100_seed_remaining_use_cases.d.ts.map +1 -0
- package/dist/migrations/knex/20251025100100_seed_remaining_use_cases.js +276 -0
- package/dist/migrations/knex/20251025100100_seed_remaining_use_cases.js.map +1 -0
- package/dist/migrations/knex/20251025120000_add_cascade_to_task_dependencies.d.ts +8 -0
- package/dist/migrations/knex/20251025120000_add_cascade_to_task_dependencies.d.ts.map +1 -0
- package/dist/migrations/knex/20251025120000_add_cascade_to_task_dependencies.js +64 -0
- package/dist/migrations/knex/20251025120000_add_cascade_to_task_dependencies.js.map +1 -0
- package/dist/migrations/knex/20251027000000_add_agent_reuse_system.d.ts +14 -0
- package/dist/migrations/knex/20251027000000_add_agent_reuse_system.d.ts.map +1 -0
- package/dist/migrations/knex/20251027000000_add_agent_reuse_system.js +34 -0
- package/dist/migrations/knex/20251027000000_add_agent_reuse_system.js.map +1 -0
- package/dist/migrations/knex/20251027010000_add_task_constraint_to_decision_context.d.ts +4 -0
- package/dist/migrations/knex/20251027010000_add_task_constraint_to_decision_context.d.ts.map +1 -0
- package/dist/migrations/knex/20251027010000_add_task_constraint_to_decision_context.js +24 -0
- package/dist/migrations/knex/20251027010000_add_task_constraint_to_decision_context.js.map +1 -0
- package/dist/migrations/knex/20251027020000_update_agent_reusability.d.ts +16 -0
- package/dist/migrations/knex/20251027020000_update_agent_reusability.d.ts.map +1 -0
- package/dist/migrations/knex/20251027020000_update_agent_reusability.js +27 -0
- package/dist/migrations/knex/20251027020000_update_agent_reusability.js.map +1 -0
- package/dist/migrations/seed-help-data.d.ts +48 -0
- package/dist/migrations/seed-help-data.d.ts.map +1 -0
- package/dist/migrations/seed-help-data.js +1466 -0
- package/dist/migrations/seed-help-data.js.map +1 -0
- package/dist/migrations/seed-tool-metadata.d.ts +24 -0
- package/dist/migrations/seed-tool-metadata.d.ts.map +1 -0
- package/dist/migrations/seed-tool-metadata.js +392 -0
- package/dist/migrations/seed-tool-metadata.js.map +1 -0
- package/dist/migrations/v3.6.0-help-system-refactor.d.ts +46 -0
- package/dist/migrations/v3.6.0-help-system-refactor.d.ts.map +1 -0
- package/dist/migrations/v3.6.0-help-system-refactor.js +223 -0
- package/dist/migrations/v3.6.0-help-system-refactor.js.map +1 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +2 -0
- package/dist/schema.js.map +1 -1
- package/dist/tests/agent-reuse.test.d.ts +6 -0
- package/dist/tests/agent-reuse.test.d.ts.map +1 -0
- package/dist/tests/agent-reuse.test.js +242 -0
- package/dist/tests/agent-reuse.test.js.map +1 -0
- package/dist/tests/all-features.test.d.ts +7 -0
- package/dist/tests/all-features.test.d.ts.map +1 -0
- package/dist/tests/all-features.test.js +514 -0
- package/dist/tests/all-features.test.js.map +1 -0
- package/dist/tests/git-aware-completion.test.js +89 -70
- package/dist/tests/git-aware-completion.test.js.map +1 -1
- package/dist/tests/help-system.test.d.ts +23 -0
- package/dist/tests/help-system.test.d.ts.map +1 -0
- package/dist/tests/help-system.test.js +374 -0
- package/dist/tests/help-system.test.js.map +1 -0
- package/dist/tests/tasks.auto-pruning-decision-link.test.js +92 -78
- package/dist/tests/tasks.auto-pruning-decision-link.test.js.map +1 -1
- package/dist/tests/tasks.auto-pruning-partial.test.js +106 -95
- package/dist/tests/tasks.auto-pruning-partial.test.js.map +1 -1
- package/dist/tests/tasks.auto-pruning-persistence.test.js +115 -97
- package/dist/tests/tasks.auto-pruning-persistence.test.js.map +1 -1
- package/dist/tests/tasks.auto-pruning-safety.test.js +124 -103
- package/dist/tests/tasks.auto-pruning-safety.test.js.map +1 -1
- package/dist/tests/tasks.dependencies.test.js +338 -307
- package/dist/tests/tasks.dependencies.test.js.map +1 -1
- package/dist/tests/tasks.link-file-backward-compat.test.js +116 -104
- package/dist/tests/tasks.link-file-backward-compat.test.js.map +1 -1
- package/dist/tests/tasks.watch-files-action.test.js +122 -101
- package/dist/tests/tasks.watch-files-action.test.js.map +1 -1
- package/dist/tests/tasks.watch-files-parameter.test.js +105 -94
- package/dist/tests/tasks.watch-files-parameter.test.js.map +1 -1
- package/dist/tests/two-step-git-completion.test.js +176 -133
- package/dist/tests/two-step-git-completion.test.js.map +1 -1
- package/dist/tests/vcs-staging.test.js +1 -1
- package/dist/tests/vcs-staging.test.js.map +1 -1
- package/dist/tools/config.d.ts +9 -6
- package/dist/tools/config.d.ts.map +1 -1
- package/dist/tools/config.js +16 -14
- package/dist/tools/config.js.map +1 -1
- package/dist/tools/constraints.d.ts +10 -7
- package/dist/tools/constraints.d.ts.map +1 -1
- package/dist/tools/constraints.js +73 -51
- package/dist/tools/constraints.js.map +1 -1
- package/dist/tools/context.d.ts +36 -33
- package/dist/tools/context.d.ts.map +1 -1
- package/dist/tools/context.js +441 -340
- package/dist/tools/context.js.map +1 -1
- package/dist/tools/files.d.ts +12 -9
- package/dist/tools/files.d.ts.map +1 -1
- package/dist/tools/files.js +173 -95
- package/dist/tools/files.js.map +1 -1
- package/dist/tools/help-queries.d.ts +130 -0
- package/dist/tools/help-queries.d.ts.map +1 -0
- package/dist/tools/help-queries.js +393 -0
- package/dist/tools/help-queries.js.map +1 -0
- package/dist/tools/messaging.d.ts +14 -11
- package/dist/tools/messaging.d.ts.map +1 -1
- package/dist/tools/messaging.js +239 -133
- package/dist/tools/messaging.js.map +1 -1
- package/dist/tools/tasks.d.ts +18 -16
- package/dist/tools/tasks.d.ts.map +1 -1
- package/dist/tools/tasks.js +519 -442
- package/dist/tools/tasks.js.map +1 -1
- package/dist/tools/utils.d.ts +14 -11
- package/dist/tools/utils.d.ts.map +1 -1
- package/dist/tools/utils.js +90 -122
- package/dist/tools/utils.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/activity-logging.d.ts +114 -0
- package/dist/utils/activity-logging.d.ts.map +1 -0
- package/dist/utils/activity-logging.js +162 -0
- package/dist/utils/activity-logging.js.map +1 -0
- package/dist/utils/batch.d.ts +2 -2
- package/dist/utils/batch.d.ts.map +1 -1
- package/dist/utils/batch.js +8 -8
- package/dist/utils/batch.js.map +1 -1
- package/dist/utils/cleanup.d.ts +24 -14
- package/dist/utils/cleanup.d.ts.map +1 -1
- package/dist/utils/cleanup.js +37 -27
- package/dist/utils/cleanup.js.map +1 -1
- package/dist/utils/debug-logger.d.ts +99 -0
- package/dist/utils/debug-logger.d.ts.map +1 -0
- package/dist/utils/debug-logger.js +267 -0
- package/dist/utils/debug-logger.js.map +1 -0
- package/dist/utils/error-handler.d.ts +28 -0
- package/dist/utils/error-handler.d.ts.map +1 -0
- package/dist/utils/error-handler.js +121 -0
- package/dist/utils/error-handler.js.map +1 -0
- package/dist/utils/help-tracking.d.ts +55 -0
- package/dist/utils/help-tracking.d.ts.map +1 -0
- package/dist/utils/help-tracking.js +88 -0
- package/dist/utils/help-tracking.js.map +1 -0
- package/dist/utils/param-parser.d.ts +23 -0
- package/dist/utils/param-parser.d.ts.map +1 -0
- package/dist/utils/param-parser.js +52 -0
- package/dist/utils/param-parser.js.map +1 -0
- package/dist/utils/retention.d.ts +17 -7
- package/dist/utils/retention.d.ts.map +1 -1
- package/dist/utils/retention.js +31 -12
- package/dist/utils/retention.js.map +1 -1
- package/dist/utils/task-stale-detection.d.ts +15 -13
- package/dist/utils/task-stale-detection.d.ts.map +1 -1
- package/dist/utils/task-stale-detection.js +100 -302
- package/dist/utils/task-stale-detection.js.map +1 -1
- package/dist/utils/token-estimation.d.ts +72 -0
- package/dist/utils/token-estimation.d.ts.map +1 -0
- package/dist/utils/token-estimation.js +71 -0
- package/dist/utils/token-estimation.js.map +1 -0
- package/dist/utils/token-logging.d.ts +48 -0
- package/dist/utils/token-logging.d.ts.map +1 -0
- package/dist/utils/token-logging.js +112 -0
- package/dist/utils/token-logging.js.map +1 -0
- package/dist/utils/view-queries.d.ts +34 -0
- package/dist/utils/view-queries.d.ts.map +1 -0
- package/dist/utils/view-queries.js +192 -0
- package/dist/utils/view-queries.js.map +1 -0
- package/dist/watcher/file-watcher.d.ts.map +1 -1
- package/dist/watcher/file-watcher.js +25 -11
- package/dist/watcher/file-watcher.js.map +1 -1
- package/docs/BEST_PRACTICES.md +56 -448
- package/docs/MIGRATION_v3.6.0.md +170 -0
- package/docs/SHARED_CONCEPTS.md +63 -208
- package/docs/TASK_OVERVIEW.md +2 -2
- package/docs/TOOL_SELECTION.md +41 -248
- package/package.json +17 -4
package/dist/tools/context.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
8
|
-
import {
|
|
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
|
|
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,
|
|
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
|
-
|
|
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(
|
|
41
|
-
const keyId = getOrCreateContextKey(
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
119
|
+
await knex('t_decision_tags').where({ decision_key_id: keyId }).delete();
|
|
79
120
|
// Insert new tags
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
131
|
+
await knex('t_decision_scopes').where({ decision_key_id: keyId }).delete();
|
|
90
132
|
// Insert new scopes
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
152
|
+
* @param adapter - Optional database adapter (for testing)
|
|
112
153
|
* @returns Response with success status and metadata
|
|
113
154
|
*/
|
|
114
|
-
export function setDecision(params,
|
|
115
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
|
187
|
+
* @param adapter - Optional database adapter (for testing)
|
|
134
188
|
* @returns Array of t_decisions with metadata
|
|
135
189
|
*/
|
|
136
|
-
export function getContext(params = {},
|
|
137
|
-
const
|
|
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 = '
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
231
|
+
query = query.orderBy('updated', 'desc');
|
|
182
232
|
// Execute query
|
|
183
|
-
const
|
|
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
|
|
250
|
+
* @param adapter - Optional database adapter (for testing)
|
|
202
251
|
* @returns Decision details or not found
|
|
203
252
|
*/
|
|
204
|
-
export function getDecision(params,
|
|
205
|
-
const
|
|
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(
|
|
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
|
|
242
|
-
|
|
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
|
|
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,
|
|
267
|
-
const
|
|
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 = '
|
|
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
|
|
280
|
-
query
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
|
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(
|
|
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
|
|
311
|
-
queryParams.push(params.layer);
|
|
361
|
+
query = query.where('layer', params.layer);
|
|
312
362
|
}
|
|
313
363
|
// Order by most recent
|
|
314
|
-
query
|
|
364
|
+
query = query.orderBy('updated', 'desc');
|
|
315
365
|
// Execute query
|
|
316
|
-
const
|
|
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
|
|
382
|
+
* @param adapter - Optional database adapter (for testing)
|
|
334
383
|
* @returns Array of historical versions with metadata
|
|
335
384
|
*/
|
|
336
|
-
export function getVersions(params,
|
|
337
|
-
const
|
|
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 =
|
|
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
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
|
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,
|
|
394
|
-
const
|
|
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(
|
|
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
|
|
413
|
-
const queryParams = [params.layer, statusValue];
|
|
458
|
+
let rows;
|
|
414
459
|
if (includeTagsValue) {
|
|
415
460
|
// Use v_tagged_decisions view for full metadata
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
|
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,
|
|
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
|
|
579
|
-
const result = setDecision(fullParams,
|
|
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
|
|
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 = {},
|
|
610
|
-
const
|
|
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 = '
|
|
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
|
-
|
|
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
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
723
|
-
const
|
|
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
|
|
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
|
|
748
|
-
queryParams.push(limit, offset);
|
|
762
|
+
query = query.limit(limit).offset(offset);
|
|
749
763
|
// Execute query
|
|
750
|
-
const
|
|
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
|
|
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,
|
|
773
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
784
|
-
|
|
785
|
-
|
|
793
|
+
success: true,
|
|
794
|
+
inserted: 0,
|
|
795
|
+
failed: 0,
|
|
796
|
+
results: []
|
|
786
797
|
};
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
|
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,
|
|
812
|
-
const
|
|
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
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
const decisionsCount =
|
|
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 =
|
|
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
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
const filesCount = fileResult
|
|
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
|
|
938
|
+
const hasUpdatesFlag = decisionsCount > 0 || messagesCount > 0 || filesCount > 0;
|
|
858
939
|
return {
|
|
859
|
-
has_updates:
|
|
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
|
|
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,
|
|
882
|
-
const
|
|
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 =
|
|
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
|
|
934
|
-
const result = setDecision(decisionParams,
|
|
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
|
|
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,
|
|
959
|
-
const
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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:
|
|
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
|
|
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 = {},
|
|
1016
|
-
const
|
|
1101
|
+
export async function listTemplates(params = {}, adapter) {
|
|
1102
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
1103
|
+
const knex = actualAdapter.getKnex();
|
|
1017
1104
|
try {
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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
|
|
1141
|
+
* @param adapter - Optional database adapter (for testing)
|
|
1064
1142
|
* @returns Response with success status
|
|
1065
1143
|
*/
|
|
1066
|
-
export function hardDeleteDecision(params,
|
|
1067
|
-
const
|
|
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(
|
|
1152
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
1074
1153
|
// Get key_id
|
|
1075
|
-
const keyResult =
|
|
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 =
|
|
1167
|
+
const deletedString = await trx('t_decisions').where({ key_id: keyId }).delete();
|
|
1087
1168
|
// Delete from t_decisions_numeric (if exists)
|
|
1088
|
-
const deletedNumeric =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1175
|
+
const deletedScopes = await trx('t_decision_scopes').where({ decision_key_id: keyId }).delete();
|
|
1095
1176
|
// Calculate total deleted records
|
|
1096
|
-
const totalDeleted = deletedString
|
|
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
|
|
1205
|
+
* @param adapter - Optional database adapter (for testing)
|
|
1125
1206
|
* @returns Response with success status
|
|
1126
1207
|
*/
|
|
1127
|
-
export function addDecisionContextAction(params,
|
|
1128
|
-
const
|
|
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
|
-
//
|
|
1141
|
-
if (alternatives
|
|
1142
|
-
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
|
|
1145
|
-
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(
|
|
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
|
|
1266
|
+
* @param adapter - Optional database adapter (for testing)
|
|
1166
1267
|
* @returns Array of decision contexts
|
|
1167
1268
|
*/
|
|
1168
|
-
export function listDecisionContextsAction(params,
|
|
1169
|
-
const
|
|
1269
|
+
export async function listDecisionContextsAction(params, adapter) {
|
|
1270
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
1170
1271
|
try {
|
|
1171
|
-
const contexts = dbListDecisionContexts(
|
|
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,
|