edsger 0.19.18 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,14 @@
1
1
  import { FeatureInfo } from '../../types/features.js';
2
+ /**
3
+ * Claim the next available ready_for_ai feature for processing.
4
+ * Uses database-level locking (FOR UPDATE SKIP LOCKED) to prevent
5
+ * race conditions between multiple workers.
6
+ *
7
+ * @param productId - The product ID to claim a feature from
8
+ * @param verbose - Whether to log verbose output
9
+ * @returns The claimed feature or null if no features available
10
+ */
11
+ export declare function claimNextFeature(productId: string, verbose?: boolean): Promise<FeatureInfo | null>;
2
12
  /**
3
13
  * Filter features by status
4
14
  */
@@ -1,5 +1,39 @@
1
1
  import { logInfo, logError } from '../../utils/logger.js';
2
2
  import { callMcpEndpoint } from '../mcp-client.js';
3
+ /**
4
+ * Claim the next available ready_for_ai feature for processing.
5
+ * Uses database-level locking (FOR UPDATE SKIP LOCKED) to prevent
6
+ * race conditions between multiple workers.
7
+ *
8
+ * @param productId - The product ID to claim a feature from
9
+ * @param verbose - Whether to log verbose output
10
+ * @returns The claimed feature or null if no features available
11
+ */
12
+ export async function claimNextFeature(productId, verbose) {
13
+ if (verbose) {
14
+ logInfo(`Attempting to claim next ready_for_ai feature for product: ${productId}`);
15
+ }
16
+ try {
17
+ const result = (await callMcpEndpoint('features/claim', {
18
+ product_id: productId,
19
+ }));
20
+ if (result.feature) {
21
+ if (verbose) {
22
+ logInfo(`✅ Claimed feature: ${result.feature.name} (${result.feature.id})`);
23
+ }
24
+ return result.feature;
25
+ }
26
+ if (verbose) {
27
+ logInfo('No features available for processing');
28
+ }
29
+ return null;
30
+ }
31
+ catch (error) {
32
+ const errorMessage = error instanceof Error ? error.message : String(error);
33
+ logError(`Failed to claim feature: ${errorMessage}`);
34
+ throw error;
35
+ }
36
+ }
3
37
  /**
4
38
  * Filter features by status
5
39
  */
@@ -32,7 +32,8 @@ export declare class WorkflowProcessor {
32
32
  */
33
33
  stop(): void;
34
34
  /**
35
- * Process the next available feature using functional composition
35
+ * Process the next available feature using atomic claim mechanism
36
+ * Uses database-level locking to prevent race conditions between workers
36
37
  */
37
38
  private processNextFeature;
38
39
  /**
@@ -3,14 +3,13 @@
3
3
  * Monitors for ready_for_ai features and processes them through the complete pipeline
4
4
  * Uses functional programming principles
5
5
  */
6
- import { getReadyForAIFeatures } from '../../api/features/index.js';
6
+ import { claimNextFeature } from '../../api/features/index.js';
7
7
  import { runFeatureWorkflow } from './feature-coordinator.js';
8
8
  import { logInfo } from '../../utils/logger.js';
9
9
  // Import core modules
10
10
  import { createInitialState, updateFeatureState, createProcessingState, createCompletedState, createFailedState, calculateStats, } from './core/state-manager.js';
11
- import { findNextFeature } from './core/feature-filter.js';
12
11
  import { evaluatePipelineResults } from './core/pipeline-evaluator.js';
13
- import { logProcessingStart, logRetryInfo, logPipelineResults, logProcessorStart, logProcessorReady, logProcessorStop, logFeatureSuccess, logFeatureFailed, logFeatureError, logNoFeaturesFound, logAllFeaturesProcessed, logSkippingProcessing, logPollingError, logProcessNextFeatureError, } from './core/workflow-logger.js';
12
+ import { logProcessingStart, logRetryInfo, logPipelineResults, logProcessorStart, logProcessorReady, logProcessorStop, logFeatureSuccess, logFeatureFailed, logFeatureError, logNoFeaturesFound, logSkippingProcessing, logPollingError, logProcessNextFeatureError, } from './core/workflow-logger.js';
14
13
  /**
15
14
  * Workflow processor using functional programming principles
16
15
  */
@@ -64,7 +63,8 @@ export class WorkflowProcessor {
64
63
  logProcessorStop();
65
64
  }
66
65
  /**
67
- * Process the next available feature using functional composition
66
+ * Process the next available feature using atomic claim mechanism
67
+ * Uses database-level locking to prevent race conditions between workers
68
68
  */
69
69
  async processNextFeature() {
70
70
  try {
@@ -76,24 +76,17 @@ export class WorkflowProcessor {
76
76
  }
77
77
  return;
78
78
  }
79
- // Fetch features using unified API
80
- const features = await getReadyForAIFeatures(this.options.productId, this.options.verbose);
81
- if (features.length === 0) {
79
+ // Atomically claim the next available feature
80
+ // This uses FOR UPDATE SKIP LOCKED to prevent multiple workers from claiming the same feature
81
+ const claimedFeature = await claimNextFeature(this.options.productId, this.options.verbose);
82
+ if (!claimedFeature) {
82
83
  if (this.options.verbose) {
83
84
  logNoFeaturesFound();
84
85
  }
85
86
  return;
86
87
  }
87
- // Find next feature to process using pure functions
88
- const nextFeature = findNextFeature(features, this.processedFeatures, this.options.maxRetries);
89
- if (!nextFeature) {
90
- if (this.options.verbose) {
91
- logAllFeaturesProcessed();
92
- }
93
- return;
94
- }
95
- // Process the feature
96
- await this.processFeature(nextFeature);
88
+ // Process the claimed feature
89
+ await this.processFeature(claimedFeature);
97
90
  }
98
91
  catch (error) {
99
92
  logProcessNextFeatureError(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edsger",
3
- "version": "0.19.18",
3
+ "version": "0.20.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "edsger": "dist/index.js"