claude-flow-novice 2.16.0 → 2.16.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/.claude/cfn-extras/skills/GOOGLE_SHEETS_SKILLS_README.md +1 -1
- package/.claude/cfn-extras/skills/google-sheets-api-coordinator/SKILL.md +1 -1
- package/.claude/cfn-extras/skills/google-sheets-formula-builder/SKILL.md +1 -1
- package/.claude/cfn-extras/skills/google-sheets-progress/SKILL.md +1 -1
- package/.claude/commands/CFN_LOOP_FRONTEND.md +1 -1
- package/.claude/commands/cfn-loop-cli.md +124 -46
- package/.claude/commands/cfn-loop-frontend.md +1 -1
- package/.claude/commands/cfn-loop-task.md +2 -2
- package/.claude/commands/deprecated/cfn-loop.md +2 -2
- package/.claude/hooks/cfn-invoke-post-edit.sh +31 -5
- package/.claude/hooks/cfn-post-edit.config.json +9 -2
- package/.claude/root-claude-distribute/CFN-CLAUDE.md +1 -1
- package/.claude/skills/cfn-backlog-management/SKILL.md +1 -1
- package/.claude/skills/cfn-loop-orchestration/NORTH_STAR_INDEX.md +1 -1
- package/claude-assets/agents/cfn-dev-team/analysts/root-cause-analyst.md +2 -2
- package/claude-assets/agents/cfn-dev-team/architecture/base-template-generator.md +1 -1
- package/claude-assets/agents/cfn-dev-team/coordinators/cfn-frontend-coordinator.md +2 -2
- package/claude-assets/agents/cfn-dev-team/coordinators/handoff-coordinator.md +1 -1
- package/claude-assets/agents/cfn-dev-team/dev-ops/devops-engineer.md +1 -1
- package/claude-assets/agents/cfn-dev-team/dev-ops/docker-specialist.md +2 -2
- package/claude-assets/agents/cfn-dev-team/dev-ops/github-commit-agent.md +2 -2
- package/claude-assets/agents/cfn-dev-team/dev-ops/kubernetes-specialist.md +1 -1
- package/claude-assets/agents/cfn-dev-team/developers/api-gateway-specialist.md +1 -1
- package/claude-assets/agents/cfn-dev-team/developers/data/data-engineer.md +1 -1
- package/claude-assets/agents/cfn-dev-team/developers/database/database-architect.md +1 -1
- package/claude-assets/agents/cfn-dev-team/developers/frontend/typescript-specialist.md +1 -1
- package/claude-assets/agents/cfn-dev-team/developers/frontend/ui-designer.md +1 -1
- package/claude-assets/agents/cfn-dev-team/developers/graphql-specialist.md +1 -1
- package/claude-assets/agents/cfn-dev-team/documentation/pseudocode.md +1 -1
- package/claude-assets/agents/cfn-dev-team/product-owners/accessibility-advocate-persona.md +1 -1
- package/claude-assets/agents/cfn-dev-team/product-owners/cto-agent.md +1 -1
- package/claude-assets/agents/cfn-dev-team/product-owners/power-user-persona.md +1 -1
- package/claude-assets/agents/cfn-dev-team/reviewers/quality/security-specialist.md +1 -1
- package/claude-assets/agents/cfn-dev-team/testers/api-testing-specialist.md +1 -1
- package/claude-assets/agents/cfn-dev-team/testers/chaos-engineering-specialist.md +1 -1
- package/claude-assets/agents/cfn-dev-team/testers/contract-tester.md +1 -1
- package/claude-assets/agents/cfn-dev-team/testers/e2e/playwright-tester.md +1 -1
- package/claude-assets/agents/cfn-dev-team/testers/integration-tester.md +1 -1
- package/claude-assets/agents/cfn-dev-team/testers/load-testing-specialist.md +1 -1
- package/claude-assets/agents/cfn-dev-team/testers/mutation-testing-specialist.md +1 -1
- package/claude-assets/agents/cfn-dev-team/testers/unit/tdd-london-unit-swarm.md +1 -1
- package/claude-assets/agents/cfn-dev-team/utility/agent-builder.md +11 -0
- package/claude-assets/agents/cfn-dev-team/utility/analyst.md +1 -1
- package/claude-assets/agents/cfn-dev-team/utility/claude-code-expert.md +1 -1
- package/claude-assets/agents/cfn-dev-team/utility/epic-creator.md +1 -1
- package/claude-assets/agents/cfn-dev-team/utility/memory-leak-specialist.md +1 -1
- package/claude-assets/agents/cfn-dev-team/utility/researcher.md +1 -1
- package/claude-assets/agents/cfn-dev-team/utility/z-ai-specialist.md +1 -1
- package/claude-assets/agents/custom/cfn-docker-expert.md +1 -0
- package/claude-assets/agents/custom/cfn-loops-cli-expert.md +326 -17
- package/claude-assets/agents/custom/cfn-redis-operations.md +529 -529
- package/claude-assets/agents/custom/cfn-system-expert.md +1 -1
- package/claude-assets/agents/custom/trigger-dev-expert.md +369 -0
- package/claude-assets/agents/docker-team/micro-sprint-planner.md +747 -747
- package/claude-assets/agents/project-only-agents/npm-package-specialist.md +1 -1
- package/claude-assets/cfn-extras/skills/GOOGLE_SHEETS_SKILLS_README.md +1 -1
- package/claude-assets/cfn-extras/skills/google-sheets-api-coordinator/SKILL.md +1 -1
- package/claude-assets/cfn-extras/skills/google-sheets-formula-builder/SKILL.md +1 -1
- package/claude-assets/cfn-extras/skills/google-sheets-progress/SKILL.md +1 -1
- package/claude-assets/commands/CFN_LOOP_FRONTEND.md +1 -1
- package/claude-assets/commands/cfn-loop-cli.md +124 -46
- package/claude-assets/commands/cfn-loop-frontend.md +1 -1
- package/claude-assets/commands/cfn-loop-task.md +2 -2
- package/claude-assets/commands/deprecated/cfn-loop.md +2 -2
- package/claude-assets/hooks/GIT-HOOKS-USAGE-EXAMPLES.md +116 -0
- package/claude-assets/hooks/README-GIT-HOOKS.md +443 -0
- package/claude-assets/hooks/cfn-invoke-post-edit.sh +31 -5
- package/claude-assets/hooks/cfn-post-edit.config.json +9 -2
- package/claude-assets/hooks/install-git-hooks.sh +243 -0
- package/claude-assets/hooks/subagent-start.sh +98 -0
- package/claude-assets/hooks/subagent-stop.sh +93 -0
- package/claude-assets/hooks/validators/credential-scanner.sh +172 -0
- package/claude-assets/root-claude-distribute/CFN-CLAUDE.md +1 -1
- package/claude-assets/skills/cfn-backlog-management/SKILL.md +1 -1
- package/claude-assets/skills/cfn-dependency-ingestion/SKILL.md +41 -13
- package/claude-assets/skills/cfn-dependency-ingestion/ingest.sh +237 -0
- package/claude-assets/skills/cfn-dependency-ingestion/manifests/cli-mode-dependencies.txt +73 -0
- package/claude-assets/skills/cfn-dependency-ingestion/manifests/shared-dependencies.txt +57 -0
- package/claude-assets/skills/cfn-dependency-ingestion/manifests/trigger-dev-dependencies.txt +82 -0
- package/claude-assets/skills/cfn-dependency-ingestion/manifests/trigger-mode-dependencies.txt +80 -0
- package/claude-assets/skills/cfn-environment-sanitization/sanitize-environment.sh +14 -4
- package/claude-assets/skills/cfn-loop-orchestration/NORTH_STAR_INDEX.md +1 -1
- package/claude-assets/skills/cfn-provider-routing/SKILL.md +23 -0
- package/claude-assets/skills/docker-build/build.sh +1 -1
- package/dist/agent/skill-mcp-selector.js +2 -1
- package/dist/agent/skill-mcp-selector.js.map +1 -1
- package/dist/agents/agent-loader.js +165 -146
- package/dist/agents/agent-loader.js.map +1 -1
- package/dist/cli/agent-executor.js +470 -26
- package/dist/cli/agent-executor.js.map +1 -1
- package/dist/cli/agent-prompt-builder.js +2 -2
- package/dist/cli/agent-prompt-builder.js.map +1 -1
- package/dist/cli/agent-spawn.js +7 -4
- package/dist/cli/agent-spawn.js.map +1 -1
- package/dist/cli/agent-spawner.js +51 -4
- package/dist/cli/agent-spawner.js.map +1 -1
- package/dist/cli/agent-token-manager.js +2 -1
- package/dist/cli/agent-token-manager.js.map +1 -1
- package/dist/cli/anthropic-client.js +117 -11
- package/dist/cli/anthropic-client.js.map +1 -1
- package/dist/cli/cfn-context.js +2 -1
- package/dist/cli/cfn-context.js.map +1 -1
- package/dist/cli/cfn-metrics.js +2 -1
- package/dist/cli/cfn-metrics.js.map +1 -1
- package/dist/cli/cfn-redis.js +2 -1
- package/dist/cli/cfn-redis.js.map +1 -1
- package/dist/cli/cli-agent-context.js +2 -0
- package/dist/cli/cli-agent-context.js.map +1 -1
- package/dist/cli/config-manager.js +4 -252
- package/dist/cli/config-manager.js.map +1 -1
- package/dist/cli/conversation-fork-cleanup.js +2 -1
- package/dist/cli/conversation-fork-cleanup.js.map +1 -1
- package/dist/cli/conversation-fork.js +2 -1
- package/dist/cli/conversation-fork.js.map +1 -1
- package/dist/cli/coordination/agent-messaging.js +415 -0
- package/dist/cli/coordination/agent-messaging.js.map +1 -0
- package/dist/cli/coordination/wait-for-threshold.js +232 -0
- package/dist/cli/coordination/wait-for-threshold.js.map +1 -0
- package/dist/cli/iteration-history.js +2 -1
- package/dist/cli/iteration-history.js.map +1 -1
- package/dist/cli/process-lifecycle.js +5 -1
- package/dist/cli/process-lifecycle.js.map +1 -1
- package/dist/cli/spawn-agent-cli.js +41 -6
- package/dist/cli/spawn-agent-cli.js.map +1 -1
- package/dist/coordination/redis-waiting-mode.js +4 -0
- package/dist/coordination/redis-waiting-mode.js.map +1 -1
- package/dist/lib/artifact-registry.js +4 -0
- package/dist/lib/artifact-registry.js.map +1 -1
- package/dist/lib/connection-pool.js +390 -0
- package/dist/lib/connection-pool.js.map +1 -0
- package/dist/lib/environment-contract.js +258 -0
- package/dist/lib/environment-contract.js.map +1 -0
- package/dist/lib/query-optimizer.js +388 -0
- package/dist/lib/query-optimizer.js.map +1 -0
- package/dist/lib/result-cache.js +285 -0
- package/dist/lib/result-cache.js.map +1 -0
- package/dist/mcp/auth-middleware.js +2 -1
- package/dist/mcp/auth-middleware.js.map +1 -1
- package/dist/mcp/playwright-mcp-server-auth.js +2 -1
- package/dist/mcp/playwright-mcp-server-auth.js.map +1 -1
- package/package.json +3 -1
- package/scripts/build-agent-image.sh +1 -1
- package/scripts/cost-allocation-tracker.sh +632 -0
- package/scripts/docker-rebuild-all-agents.sh +2 -2
- package/scripts/reorganize-tests.sh +280 -0
- package/scripts/trigger-dev-setup.sh +12 -0
- package/tests/README.md +45 -0
- package/.claude/commands/cost-savings-status.md +0 -34
- package/.claude/commands/metrics-summary.md +0 -58
- package/claude-assets/agents/cfn-dev-team/dev-ops/monitoring-specialist.md +0 -768
- package/claude-assets/agents/custom/test-mcp-access.md +0 -24
- package/claude-assets/commands/cost-savings-status.md +0 -34
- package/claude-assets/commands/metrics-summary.md +0 -58
- package/tests/test-memory-leak-task-mode.sh +0 -435
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Variable Contract Resolver
|
|
3
|
+
*
|
|
4
|
+
* Provides single source of truth for environment variables with mode-specific overrides.
|
|
5
|
+
* Implements Phase 3 of CLI/Trigger.dev collision mitigation strategy.
|
|
6
|
+
*
|
|
7
|
+
* Reference: planning/trigger/CLI_TRIGGER_COLLISION_ANALYSIS.md (Phase 3)
|
|
8
|
+
* Contract: docker/runtime/cfn-runtime.contract.yml
|
|
9
|
+
*
|
|
10
|
+
* Variable Resolution Order (First Set Wins):
|
|
11
|
+
* 1. Mode-specific overrides (if specified in contract)
|
|
12
|
+
* 2. Environment variable with CFN_ prefix (standard)
|
|
13
|
+
* 3. Legacy environment variable (with deprecation warning)
|
|
14
|
+
* 4. Default value from contract
|
|
15
|
+
* 5. Error if no value found and required=true
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* import { getEnvValue } from './environment-contract';
|
|
19
|
+
*
|
|
20
|
+
* // CLI mode - uses mcp-network
|
|
21
|
+
* const cliRedisHost = getEnvValue('redis_host', 'cli');
|
|
22
|
+
*
|
|
23
|
+
* // Trigger.dev mode - uses trigger-cfn-network
|
|
24
|
+
* const triggerRedisHost = getEnvValue('redis_host', 'trigger');
|
|
25
|
+
*
|
|
26
|
+
* // Environment override takes precedence
|
|
27
|
+
* process.env.CFN_REDIS_HOST = 'custom-redis';
|
|
28
|
+
* const overriddenHost = getEnvValue('redis_host', 'cli'); // Returns 'custom-redis'
|
|
29
|
+
*/ import * as fs from 'fs';
|
|
30
|
+
import * as path from 'path';
|
|
31
|
+
import { fileURLToPath } from 'url';
|
|
32
|
+
import * as yaml from 'js-yaml';
|
|
33
|
+
// ESM-compatible __dirname
|
|
34
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
35
|
+
const __dirname = path.dirname(__filename);
|
|
36
|
+
/**
|
|
37
|
+
* Loaded contract cache (lazy-loaded on first use)
|
|
38
|
+
*/ let contractCache = null;
|
|
39
|
+
/**
|
|
40
|
+
* Clears the contract cache (for testing purposes)
|
|
41
|
+
* @internal
|
|
42
|
+
*/ export function _clearContractCache() {
|
|
43
|
+
contractCache = null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Loads the environment variable contract from YAML file
|
|
47
|
+
* Cached after first load for performance
|
|
48
|
+
*
|
|
49
|
+
* @returns Contract specification mapping
|
|
50
|
+
* @throws Error if contract file not found or invalid YAML
|
|
51
|
+
*/ function loadContract() {
|
|
52
|
+
if (contractCache) {
|
|
53
|
+
return contractCache;
|
|
54
|
+
}
|
|
55
|
+
const projectRoot = process.env.PROJECT_ROOT || path.resolve(__dirname, '../../');
|
|
56
|
+
const contractPath = path.resolve(projectRoot, 'docker/runtime/cfn-runtime.contract.yml');
|
|
57
|
+
if (!fs.existsSync(contractPath)) {
|
|
58
|
+
throw new Error(`Environment contract not found at ${contractPath}. ` + `Ensure docker/runtime/cfn-runtime.contract.yml exists.`);
|
|
59
|
+
}
|
|
60
|
+
const contractYaml = fs.readFileSync(contractPath, 'utf8');
|
|
61
|
+
const contractData = yaml.load(contractYaml);
|
|
62
|
+
// Flatten nested contract structure (e.g., redis.CFN_REDIS_HOST becomes redis_host)
|
|
63
|
+
contractCache = flattenContract(contractData);
|
|
64
|
+
return contractCache;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Flattens nested contract structure into flat key mapping
|
|
68
|
+
* Maps environment variable names (CFN_REDIS_HOST) to their specs
|
|
69
|
+
*
|
|
70
|
+
* @param nested - Nested contract from YAML
|
|
71
|
+
* @returns Flattened mapping with both simple keys and spec metadata
|
|
72
|
+
*/ function flattenContract(nested) {
|
|
73
|
+
const flattened = {};
|
|
74
|
+
// Process each top-level category (redis, agent, task, etc.)
|
|
75
|
+
for (const [category, variables] of Object.entries(nested)){
|
|
76
|
+
// Skip metadata fields
|
|
77
|
+
if (category === 'version' || category === 'last_updated') {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (typeof variables !== 'object' || variables === null) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
// Process each variable in category
|
|
84
|
+
for (const [envVarName, spec] of Object.entries(variables)){
|
|
85
|
+
if (typeof spec === 'object' && spec !== null) {
|
|
86
|
+
// Create a simple key from env var name (e.g., CFN_REDIS_HOST -> redis_host)
|
|
87
|
+
const simpleKey = envVarName.replace(/^CFN_/, '').toLowerCase().replace(/_/g, '_');
|
|
88
|
+
const specWithCfnName = {
|
|
89
|
+
...spec,
|
|
90
|
+
_cfnVarName: envVarName
|
|
91
|
+
};
|
|
92
|
+
flattened[simpleKey] = specWithCfnName;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return flattened;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Gets environment value with mode-specific override support
|
|
100
|
+
*
|
|
101
|
+
* Resolution order:
|
|
102
|
+
* 1. Mode-specific override from contract (if defined)
|
|
103
|
+
* 2. CFN_-prefixed environment variable
|
|
104
|
+
* 3. Legacy environment variable (with warning)
|
|
105
|
+
* 4. Default from contract
|
|
106
|
+
* 5. Error if required and no value found
|
|
107
|
+
*
|
|
108
|
+
* @param key - Contract key (e.g., 'redis_host')
|
|
109
|
+
* @param mode - Execution mode ('cli' or 'trigger')
|
|
110
|
+
* @returns Resolved environment variable value as string
|
|
111
|
+
* @throws Error if key not found in contract or required value missing
|
|
112
|
+
*/ export function getEnvValue(key, mode) {
|
|
113
|
+
const contract = loadContract();
|
|
114
|
+
const spec = contract[key];
|
|
115
|
+
if (!spec) {
|
|
116
|
+
throw new Error(`Unknown contract key: '${key}'. ` + `Available keys: ${Object.keys(contract).filter((k)=>!k.startsWith('_')).join(', ')}`);
|
|
117
|
+
}
|
|
118
|
+
// Get the CFN variable name for this spec
|
|
119
|
+
const cfnVarName = spec._cfnVarName || 'CFN_VAR';
|
|
120
|
+
// Step 1: Check CFN_ prefixed environment variable (highest priority explicit env var)
|
|
121
|
+
if (process.env[cfnVarName]) {
|
|
122
|
+
return process.env[cfnVarName];
|
|
123
|
+
}
|
|
124
|
+
// Step 2: Check legacy environment variables
|
|
125
|
+
if (spec.legacy_aliases && spec.legacy_aliases.length > 0) {
|
|
126
|
+
for (const legacy of spec.legacy_aliases){
|
|
127
|
+
if (process.env[legacy]) {
|
|
128
|
+
console.warn(`[ENV DEPRECATION] Using legacy environment variable '${legacy}', ` + `migrate to '${cfnVarName}' (see docker/runtime/cfn-runtime.contract.yml)`);
|
|
129
|
+
return process.env[legacy];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Step 3: Use mode-specific override if no explicit env var was set
|
|
134
|
+
if (spec.modes?.[mode]?.override !== undefined) {
|
|
135
|
+
return String(spec.modes[mode].override);
|
|
136
|
+
}
|
|
137
|
+
// Step 4: Use default value
|
|
138
|
+
if (spec.default !== null && spec.default !== undefined) {
|
|
139
|
+
return String(spec.default);
|
|
140
|
+
}
|
|
141
|
+
// Step 5: Error if required
|
|
142
|
+
if (spec.required) {
|
|
143
|
+
throw new Error(`Required environment variable '${cfnVarName}' not set. ` + `See docker/runtime/cfn-runtime.contract.yml for configuration.`);
|
|
144
|
+
}
|
|
145
|
+
// No value found and not required - return empty string
|
|
146
|
+
return '';
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Gets mode-specific network name from contract
|
|
150
|
+
*
|
|
151
|
+
* @param mode - Execution mode ('cli' or 'trigger')
|
|
152
|
+
* @returns Network name for the mode
|
|
153
|
+
*/ export function getNetworkName(mode) {
|
|
154
|
+
const contract = loadContract();
|
|
155
|
+
const spec = contract['network_name'];
|
|
156
|
+
if (!spec) {
|
|
157
|
+
// Fallback to default if not in contract
|
|
158
|
+
return mode === 'cli' ? 'mcp-network' : 'trigger-cfn-network';
|
|
159
|
+
}
|
|
160
|
+
return getEnvValue('network_name', mode);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Gets all environment variables for a specific mode
|
|
164
|
+
* Useful for Docker environment setup
|
|
165
|
+
*
|
|
166
|
+
* @param mode - Execution mode ('cli' or 'trigger')
|
|
167
|
+
* @returns Object with resolved environment variables
|
|
168
|
+
*/ export function getAllEnvValues(mode) {
|
|
169
|
+
const contract = loadContract();
|
|
170
|
+
const envVars = {};
|
|
171
|
+
for (const [key, spec] of Object.entries(contract)){
|
|
172
|
+
if (spec && typeof spec === 'object' && 'description' in spec) {
|
|
173
|
+
try {
|
|
174
|
+
const value = getEnvValue(key, mode);
|
|
175
|
+
if (value) {
|
|
176
|
+
envVars[key] = value;
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
// Skip variables that can't be resolved (optional)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return envVars;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Validates an environment variable against contract rules
|
|
187
|
+
*
|
|
188
|
+
* @param key - Contract key
|
|
189
|
+
* @param value - Value to validate
|
|
190
|
+
* @returns Validation result with optional error message
|
|
191
|
+
*/ export function validateEnvValue(key, value) {
|
|
192
|
+
const contract = loadContract();
|
|
193
|
+
const spec = contract[key];
|
|
194
|
+
if (!spec) {
|
|
195
|
+
return {
|
|
196
|
+
valid: false,
|
|
197
|
+
error: `Unknown contract key: '${key}'`
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const rules = spec.validation;
|
|
201
|
+
if (!rules) {
|
|
202
|
+
return {
|
|
203
|
+
valid: true
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
// Pattern validation
|
|
207
|
+
if (rules.pattern) {
|
|
208
|
+
const regex = new RegExp(rules.pattern);
|
|
209
|
+
if (!regex.test(value)) {
|
|
210
|
+
return {
|
|
211
|
+
valid: false,
|
|
212
|
+
error: `Value '${value}' does not match pattern '${rules.pattern}'`
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Numeric validations
|
|
217
|
+
if (spec.type === 'integer' || spec.type === 'float') {
|
|
218
|
+
const numValue = spec.type === 'integer' ? parseInt(value, 10) : parseFloat(value);
|
|
219
|
+
if (isNaN(numValue)) {
|
|
220
|
+
return {
|
|
221
|
+
valid: false,
|
|
222
|
+
error: `Value '${value}' is not a valid ${spec.type}`
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
if (rules.min !== undefined && numValue < rules.min) {
|
|
226
|
+
return {
|
|
227
|
+
valid: false,
|
|
228
|
+
error: `Value ${numValue} is less than minimum ${rules.min}`
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
if (rules.max !== undefined && numValue > rules.max) {
|
|
232
|
+
return {
|
|
233
|
+
valid: false,
|
|
234
|
+
error: `Value ${numValue} is greater than maximum ${rules.max}`
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Allowed values
|
|
239
|
+
if (rules.allowed_values && !rules.allowed_values.includes(value)) {
|
|
240
|
+
return {
|
|
241
|
+
valid: false,
|
|
242
|
+
error: `Value '${value}' is not in allowed values: ${rules.allowed_values.join(', ')}`
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
valid: true
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Exports for barrel import
|
|
251
|
+
*/ export default {
|
|
252
|
+
getEnvValue,
|
|
253
|
+
getNetworkName,
|
|
254
|
+
getAllEnvValues,
|
|
255
|
+
validateEnvValue
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
//# sourceMappingURL=environment-contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/environment-contract.ts"],"sourcesContent":["/**\r\n * Environment Variable Contract Resolver\r\n *\r\n * Provides single source of truth for environment variables with mode-specific overrides.\r\n * Implements Phase 3 of CLI/Trigger.dev collision mitigation strategy.\r\n *\r\n * Reference: planning/trigger/CLI_TRIGGER_COLLISION_ANALYSIS.md (Phase 3)\r\n * Contract: docker/runtime/cfn-runtime.contract.yml\r\n *\r\n * Variable Resolution Order (First Set Wins):\r\n * 1. Mode-specific overrides (if specified in contract)\r\n * 2. Environment variable with CFN_ prefix (standard)\r\n * 3. Legacy environment variable (with deprecation warning)\r\n * 4. Default value from contract\r\n * 5. Error if no value found and required=true\r\n *\r\n * Usage:\r\n * import { getEnvValue } from './environment-contract';\r\n *\r\n * // CLI mode - uses mcp-network\r\n * const cliRedisHost = getEnvValue('redis_host', 'cli');\r\n *\r\n * // Trigger.dev mode - uses trigger-cfn-network\r\n * const triggerRedisHost = getEnvValue('redis_host', 'trigger');\r\n *\r\n * // Environment override takes precedence\r\n * process.env.CFN_REDIS_HOST = 'custom-redis';\r\n * const overriddenHost = getEnvValue('redis_host', 'cli'); // Returns 'custom-redis'\r\n */\r\n\r\nimport * as fs from 'fs';\r\nimport * as path from 'path';\r\nimport { fileURLToPath } from 'url';\r\nimport * as yaml from 'js-yaml';\r\n\r\n// ESM-compatible __dirname\r\nconst __filename = fileURLToPath(import.meta.url);\r\nconst __dirname = path.dirname(__filename);\r\n\r\n/**\r\n * Validation constraints for contract values\r\n */\r\ninterface ValidationRules {\r\n pattern?: string;\r\n min?: number;\r\n max?: number;\r\n allowed_values?: string[];\r\n description?: string;\r\n}\r\n\r\n/**\r\n * Mode-specific overrides in contract\r\n */\r\ninterface ModeOverride {\r\n override?: string | number;\r\n network?: string;\r\n}\r\n\r\n/**\r\n * Contract specification for a single environment variable\r\n */\r\ninterface ContractSpec {\r\n description: string;\r\n default: string | number | null;\r\n type: 'string' | 'integer' | 'boolean' | 'float';\r\n scope: string[];\r\n legacy_aliases?: string[];\r\n required?: boolean;\r\n required_in_production?: boolean;\r\n example?: string;\r\n security_notes?: string;\r\n validation?: ValidationRules;\r\n mode_defaults?: Record<string, string | number>;\r\n modes?: {\r\n cli?: ModeOverride;\r\n trigger?: ModeOverride;\r\n };\r\n}\r\n\r\n/**\r\n * Loaded contract cache (lazy-loaded on first use)\r\n */\r\nlet contractCache: Record<string, ContractSpec & { _cfnVarName?: string }> | null = null;\r\n\r\n/**\r\n * Clears the contract cache (for testing purposes)\r\n * @internal\r\n */\r\nexport function _clearContractCache(): void {\r\n contractCache = null;\r\n}\r\n\r\n/**\r\n * Loads the environment variable contract from YAML file\r\n * Cached after first load for performance\r\n *\r\n * @returns Contract specification mapping\r\n * @throws Error if contract file not found or invalid YAML\r\n */\r\nfunction loadContract(): Record<string, ContractSpec> {\r\n if (contractCache) {\r\n return contractCache;\r\n }\r\n\r\n const projectRoot = process.env.PROJECT_ROOT || path.resolve(__dirname, '../../');\r\n const contractPath = path.resolve(projectRoot, 'docker/runtime/cfn-runtime.contract.yml');\r\n\r\n if (!fs.existsSync(contractPath)) {\r\n throw new Error(\r\n `Environment contract not found at ${contractPath}. ` +\r\n `Ensure docker/runtime/cfn-runtime.contract.yml exists.`\r\n );\r\n }\r\n\r\n const contractYaml = fs.readFileSync(contractPath, 'utf8');\r\n const contractData = yaml.load(contractYaml) as Record<string, any>;\r\n\r\n // Flatten nested contract structure (e.g., redis.CFN_REDIS_HOST becomes redis_host)\r\n contractCache = flattenContract(contractData);\r\n\r\n return contractCache;\r\n}\r\n\r\n/**\r\n * Flattens nested contract structure into flat key mapping\r\n * Maps environment variable names (CFN_REDIS_HOST) to their specs\r\n *\r\n * @param nested - Nested contract from YAML\r\n * @returns Flattened mapping with both simple keys and spec metadata\r\n */\r\nfunction flattenContract(nested: Record<string, any>): Record<string, ContractSpec & { _cfnVarName?: string }> {\r\n const flattened: Record<string, ContractSpec & { _cfnVarName?: string }> = {};\r\n\r\n // Process each top-level category (redis, agent, task, etc.)\r\n for (const [category, variables] of Object.entries(nested)) {\r\n // Skip metadata fields\r\n if (category === 'version' || category === 'last_updated') {\r\n continue;\r\n }\r\n\r\n if (typeof variables !== 'object' || variables === null) {\r\n continue;\r\n }\r\n\r\n // Process each variable in category\r\n for (const [envVarName, spec] of Object.entries(variables)) {\r\n if (typeof spec === 'object' && spec !== null) {\r\n // Create a simple key from env var name (e.g., CFN_REDIS_HOST -> redis_host)\r\n const simpleKey = envVarName\r\n .replace(/^CFN_/, '')\r\n .toLowerCase()\r\n .replace(/_/g, '_');\r\n\r\n const specWithCfnName = {\r\n ...(spec as ContractSpec),\r\n _cfnVarName: envVarName, // Store the CFN variable name for later lookup\r\n };\r\n\r\n flattened[simpleKey] = specWithCfnName;\r\n }\r\n }\r\n }\r\n\r\n return flattened;\r\n}\r\n\r\n/**\r\n * Gets environment value with mode-specific override support\r\n *\r\n * Resolution order:\r\n * 1. Mode-specific override from contract (if defined)\r\n * 2. CFN_-prefixed environment variable\r\n * 3. Legacy environment variable (with warning)\r\n * 4. Default from contract\r\n * 5. Error if required and no value found\r\n *\r\n * @param key - Contract key (e.g., 'redis_host')\r\n * @param mode - Execution mode ('cli' or 'trigger')\r\n * @returns Resolved environment variable value as string\r\n * @throws Error if key not found in contract or required value missing\r\n */\r\nexport function getEnvValue(key: string, mode: 'cli' | 'trigger'): string {\r\n const contract = loadContract();\r\n const spec = contract[key] as (ContractSpec & { _cfnVarName?: string }) | undefined;\r\n\r\n if (!spec) {\r\n throw new Error(\r\n `Unknown contract key: '${key}'. ` +\r\n `Available keys: ${Object.keys(contract).filter(k => !k.startsWith('_')).join(', ')}`\r\n );\r\n }\r\n\r\n // Get the CFN variable name for this spec\r\n const cfnVarName = spec._cfnVarName || 'CFN_VAR';\r\n\r\n // Step 1: Check CFN_ prefixed environment variable (highest priority explicit env var)\r\n if (process.env[cfnVarName]) {\r\n return process.env[cfnVarName];\r\n }\r\n\r\n // Step 2: Check legacy environment variables\r\n if (spec.legacy_aliases && spec.legacy_aliases.length > 0) {\r\n for (const legacy of spec.legacy_aliases) {\r\n if (process.env[legacy]) {\r\n console.warn(\r\n `[ENV DEPRECATION] Using legacy environment variable '${legacy}', ` +\r\n `migrate to '${cfnVarName}' (see docker/runtime/cfn-runtime.contract.yml)`\r\n );\r\n return process.env[legacy];\r\n }\r\n }\r\n }\r\n\r\n // Step 3: Use mode-specific override if no explicit env var was set\r\n if (spec.modes?.[mode]?.override !== undefined) {\r\n return String(spec.modes[mode].override);\r\n }\r\n\r\n // Step 4: Use default value\r\n if (spec.default !== null && spec.default !== undefined) {\r\n return String(spec.default);\r\n }\r\n\r\n // Step 5: Error if required\r\n if (spec.required) {\r\n throw new Error(\r\n `Required environment variable '${cfnVarName}' not set. ` +\r\n `See docker/runtime/cfn-runtime.contract.yml for configuration.`\r\n );\r\n }\r\n\r\n // No value found and not required - return empty string\r\n return '';\r\n}\r\n\r\n/**\r\n * Gets mode-specific network name from contract\r\n *\r\n * @param mode - Execution mode ('cli' or 'trigger')\r\n * @returns Network name for the mode\r\n */\r\nexport function getNetworkName(mode: 'cli' | 'trigger'): string {\r\n const contract = loadContract();\r\n const spec = contract['network_name'];\r\n\r\n if (!spec) {\r\n // Fallback to default if not in contract\r\n return mode === 'cli' ? 'mcp-network' : 'trigger-cfn-network';\r\n }\r\n\r\n return getEnvValue('network_name', mode);\r\n}\r\n\r\n/**\r\n * Gets all environment variables for a specific mode\r\n * Useful for Docker environment setup\r\n *\r\n * @param mode - Execution mode ('cli' or 'trigger')\r\n * @returns Object with resolved environment variables\r\n */\r\nexport function getAllEnvValues(mode: 'cli' | 'trigger'): Record<string, string> {\r\n const contract = loadContract();\r\n const envVars: Record<string, string> = {};\r\n\r\n for (const [key, spec] of Object.entries(contract)) {\r\n if (spec && typeof spec === 'object' && 'description' in spec) {\r\n try {\r\n const value = getEnvValue(key, mode);\r\n if (value) {\r\n envVars[key] = value;\r\n }\r\n } catch {\r\n // Skip variables that can't be resolved (optional)\r\n }\r\n }\r\n }\r\n\r\n return envVars;\r\n}\r\n\r\n/**\r\n * Validates an environment variable against contract rules\r\n *\r\n * @param key - Contract key\r\n * @param value - Value to validate\r\n * @returns Validation result with optional error message\r\n */\r\nexport function validateEnvValue(key: string, value: string): { valid: boolean; error?: string } {\r\n const contract = loadContract();\r\n const spec = contract[key];\r\n\r\n if (!spec) {\r\n return { valid: false, error: `Unknown contract key: '${key}'` };\r\n }\r\n\r\n const rules = spec.validation;\r\n if (!rules) {\r\n return { valid: true };\r\n }\r\n\r\n // Pattern validation\r\n if (rules.pattern) {\r\n const regex = new RegExp(rules.pattern);\r\n if (!regex.test(value)) {\r\n return {\r\n valid: false,\r\n error: `Value '${value}' does not match pattern '${rules.pattern}'`,\r\n };\r\n }\r\n }\r\n\r\n // Numeric validations\r\n if (spec.type === 'integer' || spec.type === 'float') {\r\n const numValue = spec.type === 'integer' ? parseInt(value, 10) : parseFloat(value);\r\n\r\n if (isNaN(numValue)) {\r\n return {\r\n valid: false,\r\n error: `Value '${value}' is not a valid ${spec.type}`,\r\n };\r\n }\r\n\r\n if (rules.min !== undefined && numValue < rules.min) {\r\n return {\r\n valid: false,\r\n error: `Value ${numValue} is less than minimum ${rules.min}`,\r\n };\r\n }\r\n\r\n if (rules.max !== undefined && numValue > rules.max) {\r\n return {\r\n valid: false,\r\n error: `Value ${numValue} is greater than maximum ${rules.max}`,\r\n };\r\n }\r\n }\r\n\r\n // Allowed values\r\n if (rules.allowed_values && !rules.allowed_values.includes(value)) {\r\n return {\r\n valid: false,\r\n error: `Value '${value}' is not in allowed values: ${rules.allowed_values.join(', ')}`,\r\n };\r\n }\r\n\r\n return { valid: true };\r\n}\r\n\r\n\r\n/**\r\n * Exports for barrel import\r\n */\r\nexport default {\r\n getEnvValue,\r\n getNetworkName,\r\n getAllEnvValues,\r\n validateEnvValue,\r\n};\r\n"],"names":["fs","path","fileURLToPath","yaml","__filename","url","__dirname","dirname","contractCache","_clearContractCache","loadContract","projectRoot","process","env","PROJECT_ROOT","resolve","contractPath","existsSync","Error","contractYaml","readFileSync","contractData","load","flattenContract","nested","flattened","category","variables","Object","entries","envVarName","spec","simpleKey","replace","toLowerCase","specWithCfnName","_cfnVarName","getEnvValue","key","mode","contract","keys","filter","k","startsWith","join","cfnVarName","legacy_aliases","length","legacy","console","warn","modes","override","undefined","String","default","required","getNetworkName","getAllEnvValues","envVars","value","validateEnvValue","valid","error","rules","validation","pattern","regex","RegExp","test","type","numValue","parseInt","parseFloat","isNaN","min","max","allowed_values","includes"],"mappings":"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"}
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Optimizer
|
|
3
|
+
*
|
|
4
|
+
* Implements database query optimizations including:
|
|
5
|
+
* - Index management for agents table
|
|
6
|
+
* - Materialized views for cost aggregation
|
|
7
|
+
* - Query pattern optimization
|
|
8
|
+
* - Expected: 10-20x query speedup
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Automated index creation
|
|
12
|
+
* - Materialized view management
|
|
13
|
+
* - Query rewriting for optimal execution
|
|
14
|
+
* - Performance monitoring
|
|
15
|
+
*/ export class QueryOptimizer {
|
|
16
|
+
pool;
|
|
17
|
+
refreshInterval;
|
|
18
|
+
refreshTimer = null;
|
|
19
|
+
constructor(config){
|
|
20
|
+
this.pool = config.pool;
|
|
21
|
+
this.refreshInterval = config.refreshInterval || 3600000; // 1 hour default
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Initialize all query optimizations
|
|
25
|
+
*/ async initialize() {
|
|
26
|
+
console.log('Initializing query optimizer...');
|
|
27
|
+
await this.createIndexes();
|
|
28
|
+
await this.createMaterializedViews();
|
|
29
|
+
await this.startMaterializedViewRefresh();
|
|
30
|
+
console.log('Query optimizer initialized successfully');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Create indexes on agents table for performance
|
|
34
|
+
* Indexes: team_id, status, spawned_at
|
|
35
|
+
*/ async createIndexes() {
|
|
36
|
+
const indexes = [
|
|
37
|
+
{
|
|
38
|
+
name: 'idx_agents_team_id',
|
|
39
|
+
table: 'agents',
|
|
40
|
+
columns: [
|
|
41
|
+
'team_id'
|
|
42
|
+
],
|
|
43
|
+
description: 'Index for team-based queries'
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'idx_agents_status',
|
|
47
|
+
table: 'agents',
|
|
48
|
+
columns: [
|
|
49
|
+
'status'
|
|
50
|
+
],
|
|
51
|
+
description: 'Index for status filtering'
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'idx_agents_spawned_at',
|
|
55
|
+
table: 'agents',
|
|
56
|
+
columns: [
|
|
57
|
+
'spawned_at'
|
|
58
|
+
],
|
|
59
|
+
description: 'Index for time-based queries'
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'idx_agents_team_status',
|
|
63
|
+
table: 'agents',
|
|
64
|
+
columns: [
|
|
65
|
+
'team_id',
|
|
66
|
+
'status'
|
|
67
|
+
],
|
|
68
|
+
description: 'Composite index for team + status queries'
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'idx_agents_status_spawned',
|
|
72
|
+
table: 'agents',
|
|
73
|
+
columns: [
|
|
74
|
+
'status',
|
|
75
|
+
'spawned_at'
|
|
76
|
+
],
|
|
77
|
+
description: 'Composite index for status + time queries'
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'idx_agents_cost_query',
|
|
81
|
+
table: 'agents',
|
|
82
|
+
columns: [
|
|
83
|
+
'team_id',
|
|
84
|
+
'spawned_at',
|
|
85
|
+
'status'
|
|
86
|
+
],
|
|
87
|
+
description: 'Composite index for cost aggregation queries'
|
|
88
|
+
}
|
|
89
|
+
];
|
|
90
|
+
const client = await this.pool.connect();
|
|
91
|
+
try {
|
|
92
|
+
for (const index of indexes){
|
|
93
|
+
const query = `
|
|
94
|
+
CREATE INDEX IF NOT EXISTS ${index.name}
|
|
95
|
+
ON ${index.table} (${index.columns.join(', ')})
|
|
96
|
+
`;
|
|
97
|
+
await client.query(query);
|
|
98
|
+
console.log(`Created index: ${index.name} - ${index.description}`);
|
|
99
|
+
}
|
|
100
|
+
} finally{
|
|
101
|
+
client.release();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Create materialized views for cost aggregation queries
|
|
106
|
+
*/ async createMaterializedViews() {
|
|
107
|
+
const client = await this.pool.connect();
|
|
108
|
+
try {
|
|
109
|
+
// Drop existing views if they exist
|
|
110
|
+
await client.query('DROP MATERIALIZED VIEW IF EXISTS mv_cost_by_team CASCADE');
|
|
111
|
+
await client.query('DROP MATERIALIZED VIEW IF EXISTS mv_cost_by_agent_type CASCADE');
|
|
112
|
+
await client.query('DROP MATERIALIZED VIEW IF EXISTS mv_daily_cost_summary CASCADE');
|
|
113
|
+
// Create materialized view for cost by team
|
|
114
|
+
await client.query(`
|
|
115
|
+
CREATE MATERIALIZED VIEW mv_cost_by_team AS
|
|
116
|
+
SELECT
|
|
117
|
+
team_id,
|
|
118
|
+
COUNT(*) as agent_count,
|
|
119
|
+
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed_count,
|
|
120
|
+
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed_count,
|
|
121
|
+
AVG(confidence) as avg_confidence,
|
|
122
|
+
SUM(COALESCE(metadata::json->>'cost', '0')::numeric) as total_cost,
|
|
123
|
+
MIN(spawned_at) as first_spawn,
|
|
124
|
+
MAX(spawned_at) as last_spawn
|
|
125
|
+
FROM agents
|
|
126
|
+
WHERE team_id IS NOT NULL
|
|
127
|
+
GROUP BY team_id
|
|
128
|
+
`);
|
|
129
|
+
console.log('Created materialized view: mv_cost_by_team');
|
|
130
|
+
// Create materialized view for cost by agent type
|
|
131
|
+
await client.query(`
|
|
132
|
+
CREATE MATERIALIZED VIEW mv_cost_by_agent_type AS
|
|
133
|
+
SELECT
|
|
134
|
+
type as agent_type,
|
|
135
|
+
COUNT(*) as agent_count,
|
|
136
|
+
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed_count,
|
|
137
|
+
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed_count,
|
|
138
|
+
AVG(confidence) as avg_confidence,
|
|
139
|
+
SUM(COALESCE(metadata::json->>'cost', '0')::numeric) as total_cost,
|
|
140
|
+
AVG(EXTRACT(EPOCH FROM (completed_at - spawned_at))) as avg_duration_seconds
|
|
141
|
+
FROM agents
|
|
142
|
+
WHERE type IS NOT NULL
|
|
143
|
+
GROUP BY type
|
|
144
|
+
`);
|
|
145
|
+
console.log('Created materialized view: mv_cost_by_agent_type');
|
|
146
|
+
// Create materialized view for daily cost summary
|
|
147
|
+
await client.query(`
|
|
148
|
+
CREATE MATERIALIZED VIEW mv_daily_cost_summary AS
|
|
149
|
+
SELECT
|
|
150
|
+
DATE(spawned_at) as date,
|
|
151
|
+
COUNT(*) as total_agents,
|
|
152
|
+
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed_count,
|
|
153
|
+
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed_count,
|
|
154
|
+
SUM(COALESCE(metadata::json->>'cost', '0')::numeric) as total_cost,
|
|
155
|
+
AVG(confidence) as avg_confidence
|
|
156
|
+
FROM agents
|
|
157
|
+
WHERE spawned_at IS NOT NULL
|
|
158
|
+
GROUP BY DATE(spawned_at)
|
|
159
|
+
ORDER BY date DESC
|
|
160
|
+
`);
|
|
161
|
+
console.log('Created materialized view: mv_daily_cost_summary');
|
|
162
|
+
// Create UNIQUE indexes on materialized views (required for CONCURRENT refresh)
|
|
163
|
+
await client.query('CREATE UNIQUE INDEX idx_mv_cost_by_team_team_id ON mv_cost_by_team (team_id)');
|
|
164
|
+
await client.query('CREATE UNIQUE INDEX idx_mv_cost_by_agent_type_type ON mv_cost_by_agent_type (agent_type)');
|
|
165
|
+
await client.query('CREATE UNIQUE INDEX idx_mv_daily_cost_summary_date ON mv_daily_cost_summary (date)');
|
|
166
|
+
console.log('Created UNIQUE indexes on materialized views for concurrent refresh');
|
|
167
|
+
} finally{
|
|
168
|
+
client.release();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Refresh materialized views
|
|
173
|
+
*/ async refreshMaterializedViews() {
|
|
174
|
+
const client = await this.pool.connect();
|
|
175
|
+
try {
|
|
176
|
+
console.log('Refreshing materialized views...');
|
|
177
|
+
await client.query('REFRESH MATERIALIZED VIEW CONCURRENTLY mv_cost_by_team');
|
|
178
|
+
await client.query('REFRESH MATERIALIZED VIEW CONCURRENTLY mv_cost_by_agent_type');
|
|
179
|
+
await client.query('REFRESH MATERIALIZED VIEW CONCURRENTLY mv_daily_cost_summary');
|
|
180
|
+
console.log('Materialized views refreshed successfully');
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.error('Error refreshing materialized views:', err);
|
|
183
|
+
throw err;
|
|
184
|
+
} finally{
|
|
185
|
+
client.release();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Start automatic materialized view refresh
|
|
190
|
+
*/ startMaterializedViewRefresh() {
|
|
191
|
+
if (this.refreshTimer) {
|
|
192
|
+
clearInterval(this.refreshTimer);
|
|
193
|
+
}
|
|
194
|
+
// Initial refresh
|
|
195
|
+
this.refreshMaterializedViews().catch(console.error);
|
|
196
|
+
// Schedule periodic refresh
|
|
197
|
+
this.refreshTimer = setInterval(()=>{
|
|
198
|
+
this.refreshMaterializedViews().catch(console.error);
|
|
199
|
+
}, this.refreshInterval);
|
|
200
|
+
console.log(`Started materialized view auto-refresh (interval: ${this.refreshInterval / 1000}s)`);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Stop automatic materialized view refresh
|
|
204
|
+
*/ stopMaterializedViewRefresh() {
|
|
205
|
+
if (this.refreshTimer) {
|
|
206
|
+
clearInterval(this.refreshTimer);
|
|
207
|
+
this.refreshTimer = null;
|
|
208
|
+
console.log('Stopped materialized view auto-refresh');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Optimized query: Get cost by team
|
|
213
|
+
*/ async getCostByTeam(teamId) {
|
|
214
|
+
const client = await this.pool.connect();
|
|
215
|
+
try {
|
|
216
|
+
let query = 'SELECT * FROM mv_cost_by_team';
|
|
217
|
+
const params = [];
|
|
218
|
+
if (teamId) {
|
|
219
|
+
query += ' WHERE team_id = $1';
|
|
220
|
+
params.push(teamId);
|
|
221
|
+
}
|
|
222
|
+
query += ' ORDER BY total_cost DESC';
|
|
223
|
+
const result = await client.query(query, params);
|
|
224
|
+
return result.rows;
|
|
225
|
+
} finally{
|
|
226
|
+
client.release();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Optimized query: Get cost by agent type
|
|
231
|
+
*/ async getCostByAgentType(agentType) {
|
|
232
|
+
const client = await this.pool.connect();
|
|
233
|
+
try {
|
|
234
|
+
let query = 'SELECT * FROM mv_cost_by_agent_type';
|
|
235
|
+
const params = [];
|
|
236
|
+
if (agentType) {
|
|
237
|
+
query += ' WHERE agent_type = $1';
|
|
238
|
+
params.push(agentType);
|
|
239
|
+
}
|
|
240
|
+
query += ' ORDER BY total_cost DESC';
|
|
241
|
+
const result = await client.query(query, params);
|
|
242
|
+
return result.rows;
|
|
243
|
+
} finally{
|
|
244
|
+
client.release();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Optimized query: Get daily cost summary
|
|
249
|
+
*/ async getDailyCostSummary(startDate, endDate) {
|
|
250
|
+
const client = await this.pool.connect();
|
|
251
|
+
try {
|
|
252
|
+
let query = 'SELECT * FROM mv_daily_cost_summary';
|
|
253
|
+
const params = [];
|
|
254
|
+
const conditions = [];
|
|
255
|
+
if (startDate) {
|
|
256
|
+
params.push(startDate);
|
|
257
|
+
conditions.push(`date >= $${params.length}`);
|
|
258
|
+
}
|
|
259
|
+
if (endDate) {
|
|
260
|
+
params.push(endDate);
|
|
261
|
+
conditions.push(`date <= $${params.length}`);
|
|
262
|
+
}
|
|
263
|
+
if (conditions.length > 0) {
|
|
264
|
+
query += ' WHERE ' + conditions.join(' AND ');
|
|
265
|
+
}
|
|
266
|
+
query += ' ORDER BY date DESC';
|
|
267
|
+
const result = await client.query(query, params);
|
|
268
|
+
return result.rows;
|
|
269
|
+
} finally{
|
|
270
|
+
client.release();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Optimized query: Get agents by team and status
|
|
275
|
+
* Uses composite index idx_agents_team_status
|
|
276
|
+
*/ async getAgentsByTeamAndStatus(teamId, status) {
|
|
277
|
+
const client = await this.pool.connect();
|
|
278
|
+
try {
|
|
279
|
+
const query = `
|
|
280
|
+
SELECT *
|
|
281
|
+
FROM agents
|
|
282
|
+
WHERE team_id = $1 AND status = $2
|
|
283
|
+
ORDER BY spawned_at DESC
|
|
284
|
+
`;
|
|
285
|
+
const result = await client.query(query, [
|
|
286
|
+
teamId,
|
|
287
|
+
status
|
|
288
|
+
]);
|
|
289
|
+
return result.rows;
|
|
290
|
+
} finally{
|
|
291
|
+
client.release();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Optimized query: Get agents by status and time range
|
|
296
|
+
* Uses composite index idx_agents_status_spawned
|
|
297
|
+
*/ async getAgentsByStatusAndTimeRange(status, startDate, endDate) {
|
|
298
|
+
const client = await this.pool.connect();
|
|
299
|
+
try {
|
|
300
|
+
const query = `
|
|
301
|
+
SELECT *
|
|
302
|
+
FROM agents
|
|
303
|
+
WHERE status = $1
|
|
304
|
+
AND spawned_at >= $2
|
|
305
|
+
AND spawned_at <= $3
|
|
306
|
+
ORDER BY spawned_at DESC
|
|
307
|
+
`;
|
|
308
|
+
const result = await client.query(query, [
|
|
309
|
+
status,
|
|
310
|
+
startDate,
|
|
311
|
+
endDate
|
|
312
|
+
]);
|
|
313
|
+
return result.rows;
|
|
314
|
+
} finally{
|
|
315
|
+
client.release();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Analyze query performance
|
|
320
|
+
*/ async analyzeQuery(query, params) {
|
|
321
|
+
const client = await this.pool.connect();
|
|
322
|
+
try {
|
|
323
|
+
const explainQuery = `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ${query}`;
|
|
324
|
+
const result = await client.query(explainQuery, params);
|
|
325
|
+
return result.rows[0]['QUERY PLAN'][0];
|
|
326
|
+
} finally{
|
|
327
|
+
client.release();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Get index usage statistics
|
|
332
|
+
*/ async getIndexUsageStats() {
|
|
333
|
+
const client = await this.pool.connect();
|
|
334
|
+
try {
|
|
335
|
+
const query = `
|
|
336
|
+
SELECT
|
|
337
|
+
schemaname,
|
|
338
|
+
tablename,
|
|
339
|
+
indexname,
|
|
340
|
+
idx_scan as index_scans,
|
|
341
|
+
idx_tup_read as tuples_read,
|
|
342
|
+
idx_tup_fetch as tuples_fetched
|
|
343
|
+
FROM pg_stat_user_indexes
|
|
344
|
+
WHERE schemaname = 'public'
|
|
345
|
+
ORDER BY idx_scan DESC
|
|
346
|
+
`;
|
|
347
|
+
const result = await client.query(query);
|
|
348
|
+
return result.rows;
|
|
349
|
+
} finally{
|
|
350
|
+
client.release();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Shutdown query optimizer
|
|
355
|
+
*/ async shutdown() {
|
|
356
|
+
this.stopMaterializedViewRefresh();
|
|
357
|
+
console.log('Query optimizer shutdown complete');
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// Singleton instance
|
|
361
|
+
let queryOptimizerInstance = null;
|
|
362
|
+
/**
|
|
363
|
+
* Initialize singleton query optimizer
|
|
364
|
+
*/ export async function initQueryOptimizer(config) {
|
|
365
|
+
if (!queryOptimizerInstance) {
|
|
366
|
+
queryOptimizerInstance = new QueryOptimizer(config);
|
|
367
|
+
await queryOptimizerInstance.initialize();
|
|
368
|
+
}
|
|
369
|
+
return queryOptimizerInstance;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Get singleton query optimizer instance
|
|
373
|
+
*/ export function getQueryOptimizer() {
|
|
374
|
+
if (!queryOptimizerInstance) {
|
|
375
|
+
throw new Error('Query optimizer not initialized. Call initQueryOptimizer first.');
|
|
376
|
+
}
|
|
377
|
+
return queryOptimizerInstance;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Shutdown singleton query optimizer
|
|
381
|
+
*/ export async function shutdownQueryOptimizer() {
|
|
382
|
+
if (queryOptimizerInstance) {
|
|
383
|
+
await queryOptimizerInstance.shutdown();
|
|
384
|
+
queryOptimizerInstance = null;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
//# sourceMappingURL=query-optimizer.js.map
|