aios-core 3.7.0 → 3.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.aios-core/core/session/context-detector.js +3 -0
  2. package/.aios-core/core/session/context-loader.js +154 -0
  3. package/.aios-core/data/learned-patterns.yaml +3 -0
  4. package/.aios-core/data/workflow-patterns.yaml +347 -3
  5. package/.aios-core/development/agents/dev.md +6 -0
  6. package/.aios-core/development/agents/squad-creator.md +30 -0
  7. package/.aios-core/development/scripts/squad/squad-analyzer.js +638 -0
  8. package/.aios-core/development/scripts/squad/squad-extender.js +871 -0
  9. package/.aios-core/development/scripts/squad/squad-generator.js +107 -19
  10. package/.aios-core/development/scripts/squad/squad-migrator.js +3 -5
  11. package/.aios-core/development/scripts/squad/squad-validator.js +98 -0
  12. package/.aios-core/development/tasks/next.md +294 -0
  13. package/.aios-core/development/tasks/patterns.md +334 -0
  14. package/.aios-core/development/tasks/squad-creator-analyze.md +315 -0
  15. package/.aios-core/development/tasks/squad-creator-create.md +26 -3
  16. package/.aios-core/development/tasks/squad-creator-extend.md +411 -0
  17. package/.aios-core/development/tasks/squad-creator-validate.md +9 -1
  18. package/.aios-core/development/tasks/waves.md +205 -0
  19. package/.aios-core/development/templates/squad/agent-template.md +69 -0
  20. package/.aios-core/development/templates/squad/checklist-template.md +82 -0
  21. package/.aios-core/development/templates/squad/data-template.yaml +105 -0
  22. package/.aios-core/development/templates/squad/script-template.js +179 -0
  23. package/.aios-core/development/templates/squad/task-template.md +125 -0
  24. package/.aios-core/development/templates/squad/template-template.md +97 -0
  25. package/.aios-core/development/templates/squad/tool-template.js +103 -0
  26. package/.aios-core/development/templates/squad/workflow-template.yaml +108 -0
  27. package/.aios-core/infrastructure/scripts/test-generator.js +8 -8
  28. package/.aios-core/infrastructure/scripts/test-quality-assessment.js +5 -5
  29. package/.aios-core/infrastructure/scripts/test-utilities.js +3 -3
  30. package/.aios-core/install-manifest.yaml +97 -33
  31. package/.aios-core/quality/metrics-collector.js +27 -0
  32. package/.aios-core/scripts/session-context-loader.js +13 -254
  33. package/.aios-core/scripts/test-template-system.js +6 -6
  34. package/.aios-core/utils/aios-validator.js +25 -0
  35. package/.aios-core/workflow-intelligence/__tests__/confidence-scorer.test.js +334 -0
  36. package/.aios-core/workflow-intelligence/__tests__/integration.test.js +337 -0
  37. package/.aios-core/workflow-intelligence/__tests__/suggestion-engine.test.js +431 -0
  38. package/.aios-core/workflow-intelligence/__tests__/wave-analyzer.test.js +458 -0
  39. package/.aios-core/workflow-intelligence/__tests__/workflow-registry.test.js +302 -0
  40. package/.aios-core/workflow-intelligence/engine/confidence-scorer.js +305 -0
  41. package/.aios-core/workflow-intelligence/engine/output-formatter.js +285 -0
  42. package/.aios-core/workflow-intelligence/engine/suggestion-engine.js +603 -0
  43. package/.aios-core/workflow-intelligence/engine/wave-analyzer.js +676 -0
  44. package/.aios-core/workflow-intelligence/index.js +327 -0
  45. package/.aios-core/workflow-intelligence/learning/capture-hook.js +147 -0
  46. package/.aios-core/workflow-intelligence/learning/index.js +230 -0
  47. package/.aios-core/workflow-intelligence/learning/pattern-capture.js +340 -0
  48. package/.aios-core/workflow-intelligence/learning/pattern-store.js +498 -0
  49. package/.aios-core/workflow-intelligence/learning/pattern-validator.js +309 -0
  50. package/.aios-core/workflow-intelligence/registry/workflow-registry.js +358 -0
  51. package/package.json +1 -1
  52. package/src/installer/brownfield-upgrader.js +1 -1
  53. package/bin/aios-init.backup-v1.1.4.js +0 -352
@@ -0,0 +1,302 @@
1
+ /**
2
+ * @fileoverview Unit tests for WorkflowRegistry
3
+ * @story WIS-2 - Workflow Registry Enhancement
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const path = require('path');
9
+ const {
10
+ WorkflowRegistry,
11
+ createWorkflowRegistry,
12
+ DEFAULT_CACHE_TTL
13
+ } = require('../registry/workflow-registry');
14
+
15
+ describe('WorkflowRegistry', () => {
16
+ let registry;
17
+
18
+ beforeEach(() => {
19
+ registry = createWorkflowRegistry();
20
+ });
21
+
22
+ afterEach(() => {
23
+ registry.invalidateCache();
24
+ });
25
+
26
+ describe('constructor', () => {
27
+ it('should create registry with default options', () => {
28
+ expect(registry.cacheTTL).toBe(DEFAULT_CACHE_TTL);
29
+ expect(registry.cache).toBeNull();
30
+ });
31
+
32
+ it('should accept custom cache TTL', () => {
33
+ const customRegistry = createWorkflowRegistry({ cacheTTL: 1000 });
34
+ expect(customRegistry.cacheTTL).toBe(1000);
35
+ });
36
+
37
+ it('should accept custom patterns path', () => {
38
+ const customPath = '/custom/path.yaml';
39
+ const customRegistry = createWorkflowRegistry({ patternsPath: customPath });
40
+ expect(customRegistry.patternsPath).toBe(customPath);
41
+ });
42
+ });
43
+
44
+ describe('loadWorkflows()', () => {
45
+ it('should load workflow patterns from file', () => {
46
+ const workflows = registry.loadWorkflows();
47
+ expect(workflows).toBeDefined();
48
+ expect(typeof workflows).toBe('object');
49
+ });
50
+
51
+ it('should return 10 workflows', () => {
52
+ const workflows = registry.loadWorkflows();
53
+ const names = Object.keys(workflows);
54
+ expect(names.length).toBe(10);
55
+ });
56
+
57
+ it('should include story_development workflow', () => {
58
+ const workflows = registry.loadWorkflows();
59
+ expect(workflows.story_development).toBeDefined();
60
+ });
61
+
62
+ it('should include epic_creation workflow', () => {
63
+ const workflows = registry.loadWorkflows();
64
+ expect(workflows.epic_creation).toBeDefined();
65
+ });
66
+
67
+ it('should cache loaded workflows', () => {
68
+ const workflows1 = registry.loadWorkflows();
69
+ const workflows2 = registry.loadWorkflows();
70
+ expect(workflows1).toBe(workflows2);
71
+ });
72
+
73
+ it('should throw error for invalid file path', () => {
74
+ const badRegistry = createWorkflowRegistry({
75
+ patternsPath: '/nonexistent/path.yaml'
76
+ });
77
+ expect(() => badRegistry.loadWorkflows()).toThrow('Workflow patterns file not found');
78
+ });
79
+ });
80
+
81
+ describe('isCacheValid()', () => {
82
+ it('should return false when cache is null', () => {
83
+ expect(registry.isCacheValid()).toBe(false);
84
+ });
85
+
86
+ it('should return true after loading workflows', () => {
87
+ registry.loadWorkflows();
88
+ expect(registry.isCacheValid()).toBe(true);
89
+ });
90
+
91
+ it('should return false after invalidation', () => {
92
+ registry.loadWorkflows();
93
+ registry.invalidateCache();
94
+ expect(registry.isCacheValid()).toBe(false);
95
+ });
96
+ });
97
+
98
+ describe('getWorkflowNames()', () => {
99
+ it('should return array of workflow names', () => {
100
+ const names = registry.getWorkflowNames();
101
+ expect(Array.isArray(names)).toBe(true);
102
+ expect(names.length).toBe(10);
103
+ });
104
+
105
+ it('should include expected workflows', () => {
106
+ const names = registry.getWorkflowNames();
107
+ expect(names).toContain('story_development');
108
+ expect(names).toContain('epic_creation');
109
+ expect(names).toContain('backlog_management');
110
+ expect(names).toContain('architecture_review');
111
+ expect(names).toContain('git_workflow');
112
+ expect(names).toContain('database_workflow');
113
+ expect(names).toContain('code_quality_workflow');
114
+ expect(names).toContain('documentation_workflow');
115
+ expect(names).toContain('ux_workflow');
116
+ expect(names).toContain('research_workflow');
117
+ });
118
+ });
119
+
120
+ describe('getWorkflow()', () => {
121
+ it('should return workflow by name', () => {
122
+ const workflow = registry.getWorkflow('epic_creation');
123
+ expect(workflow).toBeDefined();
124
+ expect(workflow.description).toBeDefined();
125
+ });
126
+
127
+ it('should return null for unknown workflow', () => {
128
+ const workflow = registry.getWorkflow('nonexistent');
129
+ expect(workflow).toBeNull();
130
+ });
131
+
132
+ it('should include transitions in workflow', () => {
133
+ const workflow = registry.getWorkflow('epic_creation');
134
+ expect(workflow.transitions).toBeDefined();
135
+ expect(typeof workflow.transitions).toBe('object');
136
+ });
137
+ });
138
+
139
+ describe('matchWorkflow()', () => {
140
+ it('should match workflow from command history', () => {
141
+ const commands = ['create-epic', 'create-story'];
142
+ const match = registry.matchWorkflow(commands);
143
+
144
+ expect(match).not.toBeNull();
145
+ expect(match.name).toBe('epic_creation');
146
+ });
147
+
148
+ it('should return best matching workflow', () => {
149
+ const commands = ['validate-story-draft', 'develop', 'review-qa'];
150
+ const match = registry.matchWorkflow(commands);
151
+
152
+ expect(match).not.toBeNull();
153
+ expect(match.name).toBe('story_development');
154
+ });
155
+
156
+ it('should return null for empty commands', () => {
157
+ const match = registry.matchWorkflow([]);
158
+ expect(match).toBeNull();
159
+ });
160
+
161
+ it('should return null for unmatched commands', () => {
162
+ const match = registry.matchWorkflow(['random-cmd', 'another-random']);
163
+ expect(match).toBeNull();
164
+ });
165
+
166
+ it('should include score in match result', () => {
167
+ const match = registry.matchWorkflow(['create-epic', 'create-story']);
168
+ expect(match.score).toBeGreaterThanOrEqual(2);
169
+ });
170
+
171
+ it('should include matched commands', () => {
172
+ const match = registry.matchWorkflow(['create-epic', 'create-story']);
173
+ expect(match.matchedCommands).toBeDefined();
174
+ expect(Array.isArray(match.matchedCommands)).toBe(true);
175
+ });
176
+ });
177
+
178
+ describe('getTransitions()', () => {
179
+ it('should return transitions for valid state', () => {
180
+ const transition = registry.getTransitions('epic_creation', 'epic_drafted');
181
+
182
+ expect(transition).not.toBeNull();
183
+ expect(transition.trigger).toBeDefined();
184
+ expect(transition.confidence).toBeDefined();
185
+ expect(transition.next_steps).toBeDefined();
186
+ });
187
+
188
+ it('should return null for unknown state', () => {
189
+ const transition = registry.getTransitions('epic_creation', 'unknown_state');
190
+ expect(transition).toBeNull();
191
+ });
192
+
193
+ it('should return null for unknown workflow', () => {
194
+ const transition = registry.getTransitions('unknown_workflow', 'any_state');
195
+ expect(transition).toBeNull();
196
+ });
197
+
198
+ it('should return empty array for workflow without transitions', () => {
199
+ const result = registry.getAllTransitions('nonexistent');
200
+ expect(result).toEqual({});
201
+ });
202
+ });
203
+
204
+ describe('getNextSteps()', () => {
205
+ it('should return sorted next steps', () => {
206
+ const steps = registry.getNextSteps('epic_creation', 'epic_drafted');
207
+
208
+ expect(Array.isArray(steps)).toBe(true);
209
+ expect(steps.length).toBeGreaterThan(0);
210
+ expect(steps[0].priority).toBeLessThanOrEqual(steps[1]?.priority || 99);
211
+ });
212
+
213
+ it('should include command and description', () => {
214
+ const steps = registry.getNextSteps('epic_creation', 'epic_drafted');
215
+
216
+ expect(steps[0].command).toBeDefined();
217
+ expect(steps[0].description).toBeDefined();
218
+ });
219
+
220
+ it('should return empty array for unknown state', () => {
221
+ const steps = registry.getNextSteps('epic_creation', 'unknown_state');
222
+ expect(steps).toEqual([]);
223
+ });
224
+ });
225
+
226
+ describe('findCurrentState()', () => {
227
+ it('should find state from command', () => {
228
+ const state = registry.findCurrentState('epic_creation', 'create-epic completed');
229
+ expect(state).toBe('epic_drafted');
230
+ });
231
+
232
+ it('should find state without "completed" suffix', () => {
233
+ const state = registry.findCurrentState('epic_creation', 'create-epic');
234
+ expect(state).toBe('epic_drafted');
235
+ });
236
+
237
+ it('should return null for unknown command', () => {
238
+ const state = registry.findCurrentState('epic_creation', 'random-command');
239
+ expect(state).toBeNull();
240
+ });
241
+
242
+ it('should return null for unknown workflow', () => {
243
+ const state = registry.findCurrentState('unknown', 'create-epic');
244
+ expect(state).toBeNull();
245
+ });
246
+ });
247
+
248
+ describe('getWorkflowsByAgent()', () => {
249
+ it('should return workflows for agent', () => {
250
+ const workflows = registry.getWorkflowsByAgent('@po');
251
+
252
+ expect(Array.isArray(workflows)).toBe(true);
253
+ expect(workflows.length).toBeGreaterThan(0);
254
+ });
255
+
256
+ it('should handle agent without @ prefix', () => {
257
+ const workflows = registry.getWorkflowsByAgent('po');
258
+ expect(workflows.length).toBeGreaterThan(0);
259
+ });
260
+
261
+ it('should return empty array for unknown agent', () => {
262
+ const workflows = registry.getWorkflowsByAgent('@unknown');
263
+ expect(workflows).toEqual([]);
264
+ });
265
+ });
266
+
267
+ describe('getStats()', () => {
268
+ it('should return registry statistics', () => {
269
+ const stats = registry.getStats();
270
+
271
+ expect(stats.totalWorkflows).toBe(10);
272
+ expect(stats.workflowsWithTransitions).toBe(10);
273
+ expect(stats.totalTransitions).toBeGreaterThan(0);
274
+ });
275
+
276
+ it('should report cache status', () => {
277
+ registry.loadWorkflows();
278
+ const stats = registry.getStats();
279
+
280
+ expect(stats.cacheValid).toBe(true);
281
+ expect(stats.cacheAge).toBeGreaterThanOrEqual(0);
282
+ });
283
+ });
284
+
285
+ describe('normalizeCommand()', () => {
286
+ it('should normalize command strings', () => {
287
+ expect(registry.normalizeCommand('CREATE-EPIC')).toBe('create-epic');
288
+ expect(registry.normalizeCommand('*create-epic')).toBe('create-epic');
289
+ expect(registry.normalizeCommand('create-epic completed')).toBe('create-epic');
290
+ });
291
+
292
+ it('should handle null input', () => {
293
+ expect(registry.normalizeCommand(null)).toBe('');
294
+ });
295
+ });
296
+
297
+ describe('DEFAULT_CACHE_TTL', () => {
298
+ it('should be 5 minutes', () => {
299
+ expect(DEFAULT_CACHE_TTL).toBe(5 * 60 * 1000);
300
+ });
301
+ });
302
+ });
@@ -0,0 +1,305 @@
1
+ /**
2
+ * @module ConfidenceScorer
3
+ * @description Confidence scoring algorithm for workflow suggestions
4
+ * @story WIS-2 - Workflow Registry Enhancement
5
+ * @version 1.0.0
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ /**
11
+ * Scoring weights for confidence calculation
12
+ * @type {Object}
13
+ */
14
+ const SCORING_WEIGHTS = {
15
+ COMMAND_MATCH: 0.40,
16
+ AGENT_MATCH: 0.25,
17
+ HISTORY_DEPTH: 0.20,
18
+ PROJECT_STATE: 0.15
19
+ };
20
+
21
+ /**
22
+ * ConfidenceScorer class for calculating workflow suggestion confidence
23
+ */
24
+ class ConfidenceScorer {
25
+ /**
26
+ * Create a ConfidenceScorer instance
27
+ * @param {Object} options - Configuration options
28
+ * @param {Object} options.weights - Custom scoring weights
29
+ */
30
+ constructor(options = {}) {
31
+ this.weights = { ...SCORING_WEIGHTS, ...options.weights };
32
+ this.validateWeights();
33
+ }
34
+
35
+ /**
36
+ * Validate that weights sum to 1.0
37
+ * @throws {Error} If weights don't sum to 1.0
38
+ */
39
+ validateWeights() {
40
+ const sum = Object.values(this.weights).reduce((a, b) => a + b, 0);
41
+ if (Math.abs(sum - 1.0) > 0.001) {
42
+ throw new Error(`Scoring weights must sum to 1.0, got ${sum}`);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Calculate confidence score for a suggestion given context
48
+ * @param {Object} suggestion - The workflow suggestion
49
+ * @param {string} suggestion.trigger - Command or condition that triggers transition
50
+ * @param {string[]} suggestion.agentSequence - Expected agent sequence
51
+ * @param {string[]} suggestion.keyCommands - Key commands for this workflow
52
+ * @param {Object} context - Current session context
53
+ * @param {string} context.lastCommand - Last executed command
54
+ * @param {string[]} context.lastCommands - Recent command history
55
+ * @param {string} context.agentId - Current active agent
56
+ * @param {Object} context.projectState - Current project state
57
+ * @returns {number} Normalized score between 0.0 and 1.0
58
+ */
59
+ score(suggestion, context) {
60
+ if (!suggestion || !context) {
61
+ return 0;
62
+ }
63
+
64
+ const commandMatch = this.matchCommand(suggestion.trigger, context.lastCommand);
65
+ const agentMatch = this.matchAgent(suggestion.agentSequence, context.agentId);
66
+ const historyDepth = this.matchHistory(suggestion.keyCommands, context.lastCommands);
67
+ const projectState = this.matchProjectState(suggestion, context.projectState);
68
+
69
+ const rawScore = (
70
+ commandMatch * this.weights.COMMAND_MATCH +
71
+ agentMatch * this.weights.AGENT_MATCH +
72
+ historyDepth * this.weights.HISTORY_DEPTH +
73
+ projectState * this.weights.PROJECT_STATE
74
+ );
75
+
76
+ return this.normalize(rawScore);
77
+ }
78
+
79
+ /**
80
+ * Match command trigger against last executed command
81
+ * @param {string} trigger - Expected trigger command
82
+ * @param {string} lastCommand - Last executed command
83
+ * @returns {number} Match score 0.0-1.0
84
+ */
85
+ matchCommand(trigger, lastCommand) {
86
+ if (!trigger || !lastCommand) {
87
+ return 0;
88
+ }
89
+
90
+ const normalizedTrigger = this.normalizeCommand(trigger);
91
+ const normalizedLast = this.normalizeCommand(lastCommand);
92
+
93
+ // Exact match
94
+ if (normalizedTrigger === normalizedLast) {
95
+ return 1.0;
96
+ }
97
+
98
+ // Trigger contains the command
99
+ if (normalizedTrigger.includes(normalizedLast)) {
100
+ return 0.9;
101
+ }
102
+
103
+ // Command contains trigger keyword
104
+ if (normalizedLast.includes(normalizedTrigger.split(' ')[0])) {
105
+ return 0.7;
106
+ }
107
+
108
+ // Partial word match
109
+ const triggerWords = normalizedTrigger.split(/[\s-_]+/);
110
+ const lastWords = normalizedLast.split(/[\s-_]+/);
111
+ const commonWords = triggerWords.filter(w => lastWords.includes(w));
112
+
113
+ if (commonWords.length > 0) {
114
+ return 0.5 * (commonWords.length / Math.max(triggerWords.length, 1));
115
+ }
116
+
117
+ return 0;
118
+ }
119
+
120
+ /**
121
+ * Match agent sequence against current agent
122
+ * @param {string[]} agentSequence - Expected agent sequence
123
+ * @param {string} currentAgent - Current active agent
124
+ * @returns {number} Match score 0.0-1.0
125
+ */
126
+ matchAgent(agentSequence, currentAgent) {
127
+ if (!agentSequence || !currentAgent) {
128
+ return 0;
129
+ }
130
+
131
+ const normalizedAgent = currentAgent.replace('@', '').toLowerCase();
132
+
133
+ // Check if current agent is in the sequence
134
+ const agentIndex = agentSequence.findIndex(
135
+ agent => agent.toLowerCase() === normalizedAgent
136
+ );
137
+
138
+ if (agentIndex === -1) {
139
+ return 0;
140
+ }
141
+
142
+ // Higher score for agents later in sequence (workflow is progressing)
143
+ const progressScore = (agentIndex + 1) / agentSequence.length;
144
+
145
+ // Bonus for being the expected next agent
146
+ if (agentIndex === 0) {
147
+ return 0.6 + (progressScore * 0.4);
148
+ }
149
+
150
+ return progressScore;
151
+ }
152
+
153
+ /**
154
+ * Match command history against key workflow commands
155
+ * @param {string[]} keyCommands - Key commands for the workflow
156
+ * @param {string[]} lastCommands - Recent command history
157
+ * @returns {number} Match score 0.0-1.0
158
+ */
159
+ matchHistory(keyCommands, lastCommands) {
160
+ if (!keyCommands || !lastCommands || keyCommands.length === 0) {
161
+ return 0;
162
+ }
163
+
164
+ const normalizedKeys = keyCommands.map(cmd => this.normalizeCommand(cmd));
165
+ const normalizedHistory = lastCommands.map(cmd => this.normalizeCommand(cmd));
166
+
167
+ let matchCount = 0;
168
+ let recentBonus = 0;
169
+
170
+ for (let i = 0; i < normalizedHistory.length; i++) {
171
+ const historyCmd = normalizedHistory[i];
172
+
173
+ for (const keyCmd of normalizedKeys) {
174
+ if (historyCmd.includes(keyCmd) || keyCmd.includes(historyCmd)) {
175
+ matchCount++;
176
+
177
+ // Recent commands get higher weight (decay factor)
178
+ const recency = 1 - (i / normalizedHistory.length);
179
+ recentBonus += recency * 0.1;
180
+ break;
181
+ }
182
+ }
183
+ }
184
+
185
+ const baseScore = Math.min(matchCount / keyCommands.length, 1.0);
186
+ return Math.min(baseScore + recentBonus, 1.0);
187
+ }
188
+
189
+ /**
190
+ * Match suggestion against project state
191
+ * @param {Object} suggestion - The workflow suggestion
192
+ * @param {Object} projectState - Current project state
193
+ * @returns {number} Match score 0.0-1.0
194
+ */
195
+ matchProjectState(suggestion, projectState) {
196
+ if (!suggestion || !projectState) {
197
+ return 0.5; // Neutral score when state unknown
198
+ }
199
+
200
+ let score = 0.5;
201
+ const factors = [];
202
+
203
+ // Check if story is in progress
204
+ if (projectState.activeStory) {
205
+ factors.push(0.2);
206
+ }
207
+
208
+ // Check if uncommitted changes exist
209
+ if (projectState.hasUncommittedChanges) {
210
+ // Git-related suggestions get higher score
211
+ if (suggestion.trigger?.includes('git') ||
212
+ suggestion.trigger?.includes('commit') ||
213
+ suggestion.trigger?.includes('push')) {
214
+ factors.push(0.3);
215
+ }
216
+ }
217
+
218
+ // Check if tests are failing
219
+ if (projectState.failingTests) {
220
+ // QA-related suggestions get higher score
221
+ if (suggestion.trigger?.includes('test') ||
222
+ suggestion.trigger?.includes('qa') ||
223
+ suggestion.trigger?.includes('fix')) {
224
+ factors.push(0.3);
225
+ }
226
+ }
227
+
228
+ // Check current workflow phase
229
+ if (projectState.workflowPhase && suggestion.phase) {
230
+ if (projectState.workflowPhase === suggestion.phase) {
231
+ factors.push(0.2);
232
+ }
233
+ }
234
+
235
+ return Math.min(score + factors.reduce((a, b) => a + b, 0), 1.0);
236
+ }
237
+
238
+ /**
239
+ * Normalize a command string for comparison
240
+ * @param {string} command - Command to normalize
241
+ * @returns {string} Normalized command
242
+ */
243
+ normalizeCommand(command) {
244
+ if (!command) return '';
245
+
246
+ return command
247
+ .toLowerCase()
248
+ .replace(/\s+completed\s*$/i, '')
249
+ .replace(/\s+successfully\s*$/i, '')
250
+ .replace(/^\*/, '')
251
+ .replace(/['"]/g, '')
252
+ .trim();
253
+ }
254
+
255
+ /**
256
+ * Normalize score to 0.0-1.0 range
257
+ * @param {number} score - Raw score
258
+ * @returns {number} Normalized score
259
+ */
260
+ normalize(score) {
261
+ return Math.max(0, Math.min(1, score));
262
+ }
263
+
264
+ /**
265
+ * Calculate scores for multiple suggestions and rank them
266
+ * @param {Object[]} suggestions - Array of suggestions
267
+ * @param {Object} context - Current session context
268
+ * @returns {Object[]} Sorted suggestions with scores
269
+ */
270
+ rankSuggestions(suggestions, context) {
271
+ if (!suggestions || suggestions.length === 0) {
272
+ return [];
273
+ }
274
+
275
+ return suggestions
276
+ .map(suggestion => ({
277
+ ...suggestion,
278
+ score: this.score(suggestion, context)
279
+ }))
280
+ .sort((a, b) => b.score - a.score);
281
+ }
282
+
283
+ /**
284
+ * Get scoring weights
285
+ * @returns {Object} Current scoring weights
286
+ */
287
+ getWeights() {
288
+ return { ...this.weights };
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Create a new ConfidenceScorer instance
294
+ * @param {Object} options - Configuration options
295
+ * @returns {ConfidenceScorer} New scorer instance
296
+ */
297
+ function createConfidenceScorer(options = {}) {
298
+ return new ConfidenceScorer(options);
299
+ }
300
+
301
+ module.exports = {
302
+ ConfidenceScorer,
303
+ createConfidenceScorer,
304
+ SCORING_WEIGHTS
305
+ };