@cleocode/lafs 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +235 -0
  3. package/dist/schemas/v1/conformance-profiles.json +39 -0
  4. package/dist/schemas/v1/envelope.schema.json +306 -0
  5. package/dist/schemas/v1/error-registry.json +162 -0
  6. package/dist/src/a2a/bindings/grpc.d.ts +67 -0
  7. package/dist/src/a2a/bindings/grpc.js +148 -0
  8. package/dist/src/a2a/bindings/http.d.ts +102 -0
  9. package/dist/src/a2a/bindings/http.js +120 -0
  10. package/dist/src/a2a/bindings/index.d.ts +35 -0
  11. package/dist/src/a2a/bindings/index.js +79 -0
  12. package/dist/src/a2a/bindings/jsonrpc.d.ts +77 -0
  13. package/dist/src/a2a/bindings/jsonrpc.js +114 -0
  14. package/dist/src/a2a/bridge.d.ts +175 -0
  15. package/dist/src/a2a/bridge.js +286 -0
  16. package/dist/src/a2a/extensions.d.ts +121 -0
  17. package/dist/src/a2a/extensions.js +205 -0
  18. package/dist/src/a2a/index.d.ts +40 -0
  19. package/dist/src/a2a/index.js +76 -0
  20. package/dist/src/a2a/streaming.d.ts +74 -0
  21. package/dist/src/a2a/streaming.js +265 -0
  22. package/dist/src/a2a/task-lifecycle.d.ts +109 -0
  23. package/dist/src/a2a/task-lifecycle.js +313 -0
  24. package/dist/src/budgetEnforcement.d.ts +84 -0
  25. package/dist/src/budgetEnforcement.js +328 -0
  26. package/dist/src/circuit-breaker/index.d.ts +121 -0
  27. package/dist/src/circuit-breaker/index.js +249 -0
  28. package/dist/src/cli.d.ts +16 -0
  29. package/dist/src/cli.js +63 -0
  30. package/dist/src/compliance.d.ts +31 -0
  31. package/dist/src/compliance.js +89 -0
  32. package/dist/src/conformance.d.ts +7 -0
  33. package/dist/src/conformance.js +248 -0
  34. package/dist/src/conformanceProfiles.d.ts +11 -0
  35. package/dist/src/conformanceProfiles.js +34 -0
  36. package/dist/src/deprecationRegistry.d.ts +13 -0
  37. package/dist/src/deprecationRegistry.js +39 -0
  38. package/dist/src/discovery.d.ts +286 -0
  39. package/dist/src/discovery.js +350 -0
  40. package/dist/src/envelope.d.ts +60 -0
  41. package/dist/src/envelope.js +136 -0
  42. package/dist/src/errorRegistry.d.ts +28 -0
  43. package/dist/src/errorRegistry.js +36 -0
  44. package/dist/src/fieldExtraction.d.ts +67 -0
  45. package/dist/src/fieldExtraction.js +133 -0
  46. package/dist/src/flagResolver.d.ts +46 -0
  47. package/dist/src/flagResolver.js +47 -0
  48. package/dist/src/flagSemantics.d.ts +16 -0
  49. package/dist/src/flagSemantics.js +45 -0
  50. package/dist/src/health/index.d.ts +105 -0
  51. package/dist/src/health/index.js +220 -0
  52. package/dist/src/index.d.ts +24 -0
  53. package/dist/src/index.js +34 -0
  54. package/dist/src/mcpAdapter.d.ts +28 -0
  55. package/dist/src/mcpAdapter.js +281 -0
  56. package/dist/src/mviProjection.d.ts +19 -0
  57. package/dist/src/mviProjection.js +116 -0
  58. package/dist/src/problemDetails.d.ts +34 -0
  59. package/dist/src/problemDetails.js +45 -0
  60. package/dist/src/shutdown/index.d.ts +69 -0
  61. package/dist/src/shutdown/index.js +160 -0
  62. package/dist/src/tokenEstimator.d.ts +87 -0
  63. package/dist/src/tokenEstimator.js +238 -0
  64. package/dist/src/types.d.ts +135 -0
  65. package/dist/src/types.js +12 -0
  66. package/dist/src/validateEnvelope.d.ts +15 -0
  67. package/dist/src/validateEnvelope.js +31 -0
  68. package/lafs.md +819 -0
  69. package/package.json +88 -0
  70. package/schemas/v1/agent-card.schema.json +230 -0
  71. package/schemas/v1/conformance-profiles.json +39 -0
  72. package/schemas/v1/context-ledger.schema.json +70 -0
  73. package/schemas/v1/discovery.schema.json +132 -0
  74. package/schemas/v1/envelope.schema.json +306 -0
  75. package/schemas/v1/error-registry.json +162 -0
@@ -0,0 +1,328 @@
1
+ /**
2
+ * LAFS Budget Enforcement
3
+ *
4
+ * Middleware for enforcing MVI (Minimal Viable Interface) token budgets on LAFS envelopes.
5
+ * Provides budget checking, truncation, and error generation for exceeded budgets.
6
+ */
7
+ import { TokenEstimator } from "./tokenEstimator.js";
8
+ /**
9
+ * Budget exceeded error code from LAFS error registry
10
+ */
11
+ const BUDGET_EXCEEDED_CODE = "E_MVI_BUDGET_EXCEEDED";
12
+ /**
13
+ * Default category for budget exceeded errors
14
+ */
15
+ const BUDGET_ERROR_CATEGORY = "VALIDATION";
16
+ /**
17
+ * Create a budget exceeded error object
18
+ */
19
+ function createBudgetExceededError(estimated, budget) {
20
+ return {
21
+ code: BUDGET_EXCEEDED_CODE,
22
+ message: `Response exceeds declared MVI budget: estimated ${estimated} tokens, budget ${budget} tokens`,
23
+ category: BUDGET_ERROR_CATEGORY,
24
+ retryable: false,
25
+ retryAfterMs: null,
26
+ details: {
27
+ estimatedTokens: estimated,
28
+ budgetTokens: budget,
29
+ exceededBy: estimated - budget,
30
+ exceededByPercent: Math.round(((estimated - budget) / budget) * 100),
31
+ },
32
+ };
33
+ }
34
+ /**
35
+ * Truncate a result to fit within budget.
36
+ * Returns the truncated result and whether truncation occurred.
37
+ */
38
+ function truncateResult(result, targetTokens, estimator) {
39
+ if (result === null) {
40
+ return { result: null, wasTruncated: false };
41
+ }
42
+ const currentEstimate = estimator.estimate(result);
43
+ // If already within budget, no truncation needed
44
+ if (currentEstimate <= targetTokens) {
45
+ return { result, wasTruncated: false };
46
+ }
47
+ // Calculate target size (conservative: assume 10% overhead)
48
+ const targetChars = Math.floor(targetTokens * 4 * 0.9);
49
+ if (Array.isArray(result)) {
50
+ return truncateArray(result, targetChars, targetTokens, estimator);
51
+ }
52
+ return truncateObject(result, targetChars, targetTokens, estimator);
53
+ }
54
+ /**
55
+ * Truncate an array to fit within budget.
56
+ */
57
+ function truncateArray(arr, targetChars, targetTokens, estimator) {
58
+ if (arr.length === 0) {
59
+ return { result: arr, wasTruncated: false };
60
+ }
61
+ // Binary search to find how many items fit
62
+ let left = 0;
63
+ let right = arr.length;
64
+ let bestFit = 0;
65
+ while (left <= right) {
66
+ const mid = Math.floor((left + right) / 2);
67
+ const subset = arr.slice(0, mid);
68
+ const estimate = estimator.estimate(subset);
69
+ if (estimate <= targetTokens) {
70
+ bestFit = mid;
71
+ left = mid + 1;
72
+ }
73
+ else {
74
+ right = mid - 1;
75
+ }
76
+ }
77
+ // If we can fit all items, no truncation needed
78
+ if (bestFit >= arr.length) {
79
+ return { result: arr, wasTruncated: false };
80
+ }
81
+ // Create truncated result
82
+ const truncated = arr.slice(0, bestFit);
83
+ // If we couldn't fit any items, return minimal response
84
+ if (bestFit === 0 && arr.length > 0) {
85
+ return {
86
+ result: [{ _truncated: true, reason: "budget_exceeded" }],
87
+ wasTruncated: true
88
+ };
89
+ }
90
+ // Add truncation indicator to last element if it's an object
91
+ if (bestFit > 0 && typeof truncated[bestFit - 1] === 'object' && truncated[bestFit - 1] !== null) {
92
+ const lastItem = truncated[bestFit - 1];
93
+ truncated[bestFit - 1] = {
94
+ ...lastItem,
95
+ _truncated: true,
96
+ remainingItems: arr.length - bestFit,
97
+ };
98
+ }
99
+ return { result: truncated, wasTruncated: true };
100
+ }
101
+ /**
102
+ * Truncate an object to fit within budget.
103
+ */
104
+ function truncateObject(obj, targetChars, targetTokens, estimator) {
105
+ const keys = Object.keys(obj);
106
+ if (keys.length === 0) {
107
+ return { result: obj, wasTruncated: false };
108
+ }
109
+ // Try to fit as many top-level properties as possible
110
+ let left = 0;
111
+ let right = keys.length;
112
+ let bestFit = 0;
113
+ while (left <= right) {
114
+ const mid = Math.floor((left + right) / 2);
115
+ const subsetKeys = keys.slice(0, mid);
116
+ const subset = {};
117
+ for (const key of subsetKeys) {
118
+ subset[key] = obj[key];
119
+ }
120
+ const estimate = estimator.estimate(subset);
121
+ if (estimate <= targetTokens) {
122
+ bestFit = mid;
123
+ left = mid + 1;
124
+ }
125
+ else {
126
+ right = mid - 1;
127
+ }
128
+ }
129
+ // If we can fit all properties, no truncation needed
130
+ if (bestFit >= keys.length) {
131
+ return { result: obj, wasTruncated: false };
132
+ }
133
+ // Create truncated result
134
+ const subsetKeys = keys.slice(0, bestFit);
135
+ const truncated = {};
136
+ for (const key of subsetKeys) {
137
+ truncated[key] = obj[key];
138
+ }
139
+ // If we couldn't fit any properties, return minimal response
140
+ if (bestFit === 0) {
141
+ return {
142
+ result: { _truncated: true, reason: "budget_exceeded" },
143
+ wasTruncated: true
144
+ };
145
+ }
146
+ // Add truncation metadata
147
+ truncated._truncated = true;
148
+ truncated._truncatedFields = keys.slice(bestFit);
149
+ return { result: truncated, wasTruncated: true };
150
+ }
151
+ /**
152
+ * Apply budget enforcement to an envelope.
153
+ *
154
+ * @param envelope - The LAFS envelope to check
155
+ * @param budget - Maximum allowed tokens
156
+ * @param options - Budget enforcement options
157
+ * @returns Enforce result with potentially modified envelope
158
+ */
159
+ export function applyBudgetEnforcement(envelope, budget, options = {}) {
160
+ const { truncateOnExceed = false, onBudgetExceeded } = options;
161
+ const estimator = new TokenEstimator();
162
+ // Estimate the result payload
163
+ const estimatedTokens = estimator.estimate(envelope.result);
164
+ // Add estimate to metadata
165
+ const tokenEstimate = {
166
+ estimated: estimatedTokens,
167
+ };
168
+ // Check if within budget
169
+ const withinBudget = estimatedTokens <= budget;
170
+ // If within budget, just add the estimate to metadata
171
+ if (withinBudget) {
172
+ return {
173
+ envelope: {
174
+ ...envelope,
175
+ _meta: {
176
+ ...envelope._meta,
177
+ _tokenEstimate: tokenEstimate,
178
+ },
179
+ },
180
+ withinBudget: true,
181
+ estimatedTokens,
182
+ budget,
183
+ truncated: false,
184
+ };
185
+ }
186
+ // Budget exceeded - call callback if provided
187
+ if (onBudgetExceeded) {
188
+ onBudgetExceeded(estimatedTokens, budget);
189
+ }
190
+ // If truncation is enabled, try to truncate
191
+ if (truncateOnExceed) {
192
+ const { result, wasTruncated } = truncateResult(envelope.result, budget, estimator);
193
+ const truncatedEstimate = estimator.estimate(result);
194
+ if (truncatedEstimate <= budget) {
195
+ return {
196
+ envelope: {
197
+ ...envelope,
198
+ result,
199
+ _meta: {
200
+ ...envelope._meta,
201
+ _tokenEstimate: {
202
+ estimated: truncatedEstimate,
203
+ truncated: true,
204
+ originalEstimate: estimatedTokens,
205
+ },
206
+ },
207
+ },
208
+ withinBudget: true,
209
+ estimatedTokens: truncatedEstimate,
210
+ budget,
211
+ truncated: true,
212
+ };
213
+ }
214
+ }
215
+ // Return budget exceeded error
216
+ return {
217
+ envelope: {
218
+ ...envelope,
219
+ success: false,
220
+ result: null,
221
+ error: createBudgetExceededError(estimatedTokens, budget),
222
+ _meta: {
223
+ ...envelope._meta,
224
+ _tokenEstimate: tokenEstimate,
225
+ },
226
+ },
227
+ withinBudget: false,
228
+ estimatedTokens,
229
+ budget,
230
+ truncated: false,
231
+ };
232
+ }
233
+ /**
234
+ * Create a budget enforcement middleware function.
235
+ *
236
+ * @param budget - Maximum allowed tokens for response
237
+ * @param options - Budget enforcement options
238
+ * @returns Middleware function that enforces budget
239
+ *
240
+ * @example
241
+ * ```typescript
242
+ * const middleware = withBudget(1000, { truncateOnExceed: true });
243
+ * const result = await middleware(envelope, async () => nextEnvelope);
244
+ * ```
245
+ */
246
+ export function withBudget(budget, options = {}) {
247
+ return async (envelope, next) => {
248
+ // Execute next middleware/handler
249
+ const result = await next();
250
+ // Apply budget enforcement to the result
251
+ const enforcement = applyBudgetEnforcement(result, budget, options);
252
+ return enforcement.envelope;
253
+ };
254
+ }
255
+ /**
256
+ * Check if an envelope has exceeded its budget without modifying it.
257
+ *
258
+ * @param envelope - The LAFS envelope to check
259
+ * @param budget - Maximum allowed tokens
260
+ * @returns Budget check result
261
+ */
262
+ export function checkBudget(envelope, budget) {
263
+ const estimator = new TokenEstimator();
264
+ const estimated = estimator.estimate(envelope.result);
265
+ return {
266
+ exceeded: estimated > budget,
267
+ estimated,
268
+ remaining: Math.max(0, budget - estimated),
269
+ };
270
+ }
271
+ /**
272
+ * Synchronous version of withBudget for non-async contexts.
273
+ *
274
+ * @param budget - Maximum allowed tokens for response
275
+ * @param options - Budget enforcement options
276
+ * @returns Middleware function that enforces budget synchronously
277
+ */
278
+ export function withBudgetSync(budget, options = {}) {
279
+ return (envelope, next) => {
280
+ const result = next();
281
+ const enforcement = applyBudgetEnforcement(result, budget, options);
282
+ return enforcement.envelope;
283
+ };
284
+ }
285
+ /**
286
+ * Higher-order function that wraps a handler with budget enforcement.
287
+ *
288
+ * @param handler - The handler function to wrap
289
+ * @param budget - Maximum allowed tokens
290
+ * @param options - Budget enforcement options
291
+ * @returns Wrapped handler with budget enforcement
292
+ *
293
+ * @example
294
+ * ```typescript
295
+ * const myHandler = async (request: Request) => ({ success: true, result: { data } });
296
+ * const budgetedHandler = wrapWithBudget(myHandler, 1000, { truncateOnExceed: true });
297
+ * const result = await budgetedHandler(request);
298
+ * ```
299
+ */
300
+ export function wrapWithBudget(handler, budget, options = {}) {
301
+ return async (...args) => {
302
+ const result = await handler(...args);
303
+ const enforcement = applyBudgetEnforcement(result, budget, options);
304
+ return enforcement.envelope;
305
+ };
306
+ }
307
+ /**
308
+ * Compose multiple middleware functions into a single middleware.
309
+ * Middleware is executed in order (left to right).
310
+ */
311
+ export function composeMiddleware(...middlewares) {
312
+ return async (envelope, next) => {
313
+ let index = 0;
314
+ async function dispatch(i) {
315
+ if (i >= middlewares.length) {
316
+ return next();
317
+ }
318
+ const middleware = middlewares[i];
319
+ if (!middleware) {
320
+ return dispatch(i + 1);
321
+ }
322
+ return middleware(envelope, () => dispatch(i + 1));
323
+ }
324
+ return dispatch(0);
325
+ };
326
+ }
327
+ export { TokenEstimator };
328
+ export { BUDGET_EXCEEDED_CODE };
@@ -0,0 +1,121 @@
1
+ /**
2
+ * LAFS Circuit Breaker Module
3
+ *
4
+ * Provides circuit breaker pattern for resilient service calls
5
+ */
6
+ export type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
7
+ export interface CircuitBreakerConfig {
8
+ name: string;
9
+ failureThreshold?: number;
10
+ resetTimeout?: number;
11
+ halfOpenMaxCalls?: number;
12
+ successThreshold?: number;
13
+ }
14
+ export interface CircuitBreakerMetrics {
15
+ state: CircuitState;
16
+ failures: number;
17
+ successes: number;
18
+ lastFailureTime?: Date;
19
+ consecutiveSuccesses: number;
20
+ totalCalls: number;
21
+ }
22
+ export declare class CircuitBreakerError extends Error {
23
+ constructor(message: string);
24
+ }
25
+ /**
26
+ * Circuit breaker for protecting against cascading failures
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * import { CircuitBreaker } from '@cleocode/lafs/circuit-breaker';
31
+ *
32
+ * const breaker = new CircuitBreaker({
33
+ * name: 'external-api',
34
+ * failureThreshold: 5,
35
+ * resetTimeout: 30000
36
+ * });
37
+ *
38
+ * try {
39
+ * const result = await breaker.execute(async () => {
40
+ * return await externalApi.call();
41
+ * });
42
+ * } catch (error) {
43
+ * if (error instanceof CircuitBreakerError) {
44
+ * console.log('Circuit breaker is open');
45
+ * }
46
+ * }
47
+ * ```
48
+ */
49
+ export declare class CircuitBreaker {
50
+ private config;
51
+ private state;
52
+ private failures;
53
+ private successes;
54
+ private lastFailureTime?;
55
+ private consecutiveSuccesses;
56
+ private totalCalls;
57
+ private halfOpenCalls;
58
+ private resetTimer?;
59
+ constructor(config: CircuitBreakerConfig);
60
+ /**
61
+ * Execute a function with circuit breaker protection
62
+ */
63
+ execute<T>(fn: () => Promise<T>): Promise<T>;
64
+ /**
65
+ * Get current circuit breaker state
66
+ */
67
+ getState(): CircuitState;
68
+ /**
69
+ * Get circuit breaker metrics
70
+ */
71
+ getMetrics(): CircuitBreakerMetrics;
72
+ /**
73
+ * Manually open the circuit breaker
74
+ */
75
+ forceOpen(): void;
76
+ /**
77
+ * Manually close the circuit breaker
78
+ */
79
+ forceClose(): void;
80
+ private onSuccess;
81
+ private onFailure;
82
+ private transitionTo;
83
+ private shouldAttemptReset;
84
+ private scheduleReset;
85
+ private reset;
86
+ }
87
+ /**
88
+ * Circuit breaker registry for managing multiple breakers
89
+ *
90
+ * @example
91
+ * ```typescript
92
+ * const registry = new CircuitBreakerRegistry();
93
+ *
94
+ * registry.add('payment-api', {
95
+ * failureThreshold: 3,
96
+ * resetTimeout: 60000
97
+ * });
98
+ *
99
+ * const paymentBreaker = registry.get('payment-api');
100
+ * ```
101
+ */
102
+ export declare class CircuitBreakerRegistry {
103
+ private breakers;
104
+ add(name: string, config: Omit<CircuitBreakerConfig, 'name'>): CircuitBreaker;
105
+ get(name: string): CircuitBreaker | undefined;
106
+ getOrCreate(name: string, config: Omit<CircuitBreakerConfig, 'name'>): CircuitBreaker;
107
+ getAllMetrics(): Record<string, CircuitBreakerMetrics>;
108
+ resetAll(): void;
109
+ }
110
+ /**
111
+ * Create a circuit breaker middleware for Express
112
+ *
113
+ * @example
114
+ * ```typescript
115
+ * app.use('/external-api', circuitBreakerMiddleware({
116
+ * name: 'external-api',
117
+ * failureThreshold: 5
118
+ * }));
119
+ * ```
120
+ */
121
+ export declare function circuitBreakerMiddleware(config: CircuitBreakerConfig): (req: any, res: any, next: any) => Promise<void>;
@@ -0,0 +1,249 @@
1
+ /**
2
+ * LAFS Circuit Breaker Module
3
+ *
4
+ * Provides circuit breaker pattern for resilient service calls
5
+ */
6
+ export class CircuitBreakerError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = 'CircuitBreakerError';
10
+ }
11
+ }
12
+ /**
13
+ * Circuit breaker for protecting against cascading failures
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { CircuitBreaker } from '@cleocode/lafs/circuit-breaker';
18
+ *
19
+ * const breaker = new CircuitBreaker({
20
+ * name: 'external-api',
21
+ * failureThreshold: 5,
22
+ * resetTimeout: 30000
23
+ * });
24
+ *
25
+ * try {
26
+ * const result = await breaker.execute(async () => {
27
+ * return await externalApi.call();
28
+ * });
29
+ * } catch (error) {
30
+ * if (error instanceof CircuitBreakerError) {
31
+ * console.log('Circuit breaker is open');
32
+ * }
33
+ * }
34
+ * ```
35
+ */
36
+ export class CircuitBreaker {
37
+ config;
38
+ state = 'CLOSED';
39
+ failures = 0;
40
+ successes = 0;
41
+ lastFailureTime;
42
+ consecutiveSuccesses = 0;
43
+ totalCalls = 0;
44
+ halfOpenCalls = 0;
45
+ resetTimer;
46
+ constructor(config) {
47
+ this.config = config;
48
+ this.config = {
49
+ failureThreshold: 5,
50
+ resetTimeout: 30000,
51
+ halfOpenMaxCalls: 3,
52
+ successThreshold: 2,
53
+ ...config
54
+ };
55
+ }
56
+ /**
57
+ * Execute a function with circuit breaker protection
58
+ */
59
+ async execute(fn) {
60
+ this.totalCalls++;
61
+ if (this.state === 'OPEN') {
62
+ if (this.shouldAttemptReset()) {
63
+ this.transitionTo('HALF_OPEN');
64
+ }
65
+ else {
66
+ throw new CircuitBreakerError(`Circuit breaker '${this.config.name}' is OPEN`);
67
+ }
68
+ }
69
+ if (this.state === 'HALF_OPEN') {
70
+ if (this.halfOpenCalls >= (this.config.halfOpenMaxCalls || 3)) {
71
+ throw new CircuitBreakerError(`Circuit breaker '${this.config.name}' is HALF_OPEN (max calls reached)`);
72
+ }
73
+ this.halfOpenCalls++;
74
+ }
75
+ try {
76
+ const result = await fn();
77
+ this.onSuccess();
78
+ return result;
79
+ }
80
+ catch (error) {
81
+ this.onFailure();
82
+ throw error;
83
+ }
84
+ }
85
+ /**
86
+ * Get current circuit breaker state
87
+ */
88
+ getState() {
89
+ return this.state;
90
+ }
91
+ /**
92
+ * Get circuit breaker metrics
93
+ */
94
+ getMetrics() {
95
+ return {
96
+ state: this.state,
97
+ failures: this.failures,
98
+ successes: this.successes,
99
+ lastFailureTime: this.lastFailureTime,
100
+ consecutiveSuccesses: this.consecutiveSuccesses,
101
+ totalCalls: this.totalCalls
102
+ };
103
+ }
104
+ /**
105
+ * Manually open the circuit breaker
106
+ */
107
+ forceOpen() {
108
+ this.transitionTo('OPEN');
109
+ }
110
+ /**
111
+ * Manually close the circuit breaker
112
+ */
113
+ forceClose() {
114
+ this.transitionTo('CLOSED');
115
+ this.reset();
116
+ }
117
+ onSuccess() {
118
+ this.successes++;
119
+ this.consecutiveSuccesses++;
120
+ if (this.state === 'HALF_OPEN') {
121
+ if (this.consecutiveSuccesses >= (this.config.successThreshold || 2)) {
122
+ this.transitionTo('CLOSED');
123
+ this.reset();
124
+ }
125
+ }
126
+ }
127
+ onFailure() {
128
+ this.failures++;
129
+ this.consecutiveSuccesses = 0;
130
+ this.lastFailureTime = new Date();
131
+ if (this.state === 'HALF_OPEN') {
132
+ this.transitionTo('OPEN');
133
+ this.scheduleReset();
134
+ }
135
+ else if (this.state === 'CLOSED') {
136
+ if (this.failures >= (this.config.failureThreshold || 5)) {
137
+ this.transitionTo('OPEN');
138
+ this.scheduleReset();
139
+ }
140
+ }
141
+ }
142
+ transitionTo(newState) {
143
+ console.log(`Circuit breaker '${this.config.name}': ${this.state} -> ${newState}`);
144
+ this.state = newState;
145
+ if (newState === 'HALF_OPEN') {
146
+ this.halfOpenCalls = 0;
147
+ }
148
+ }
149
+ shouldAttemptReset() {
150
+ if (!this.lastFailureTime)
151
+ return true;
152
+ const elapsed = Date.now() - this.lastFailureTime.getTime();
153
+ return elapsed >= (this.config.resetTimeout || 30000);
154
+ }
155
+ scheduleReset() {
156
+ if (this.resetTimer) {
157
+ clearTimeout(this.resetTimer);
158
+ }
159
+ this.resetTimer = setTimeout(() => {
160
+ if (this.state === 'OPEN') {
161
+ this.transitionTo('HALF_OPEN');
162
+ }
163
+ }, this.config.resetTimeout || 30000);
164
+ }
165
+ reset() {
166
+ this.failures = 0;
167
+ this.consecutiveSuccesses = 0;
168
+ this.halfOpenCalls = 0;
169
+ if (this.resetTimer) {
170
+ clearTimeout(this.resetTimer);
171
+ this.resetTimer = undefined;
172
+ }
173
+ }
174
+ }
175
+ /**
176
+ * Circuit breaker registry for managing multiple breakers
177
+ *
178
+ * @example
179
+ * ```typescript
180
+ * const registry = new CircuitBreakerRegistry();
181
+ *
182
+ * registry.add('payment-api', {
183
+ * failureThreshold: 3,
184
+ * resetTimeout: 60000
185
+ * });
186
+ *
187
+ * const paymentBreaker = registry.get('payment-api');
188
+ * ```
189
+ */
190
+ export class CircuitBreakerRegistry {
191
+ breakers = new Map();
192
+ add(name, config) {
193
+ const breaker = new CircuitBreaker({ ...config, name });
194
+ this.breakers.set(name, breaker);
195
+ return breaker;
196
+ }
197
+ get(name) {
198
+ return this.breakers.get(name);
199
+ }
200
+ getOrCreate(name, config) {
201
+ let breaker = this.breakers.get(name);
202
+ if (!breaker) {
203
+ breaker = this.add(name, config);
204
+ }
205
+ return breaker;
206
+ }
207
+ getAllMetrics() {
208
+ const metrics = {};
209
+ this.breakers.forEach((breaker, name) => {
210
+ metrics[name] = breaker.getMetrics();
211
+ });
212
+ return metrics;
213
+ }
214
+ resetAll() {
215
+ this.breakers.forEach(breaker => breaker.forceClose());
216
+ }
217
+ }
218
+ /**
219
+ * Create a circuit breaker middleware for Express
220
+ *
221
+ * @example
222
+ * ```typescript
223
+ * app.use('/external-api', circuitBreakerMiddleware({
224
+ * name: 'external-api',
225
+ * failureThreshold: 5
226
+ * }));
227
+ * ```
228
+ */
229
+ export function circuitBreakerMiddleware(config) {
230
+ const breaker = new CircuitBreaker(config);
231
+ return async (req, res, next) => {
232
+ try {
233
+ await breaker.execute(async () => {
234
+ next();
235
+ });
236
+ }
237
+ catch (error) {
238
+ if (error instanceof CircuitBreakerError) {
239
+ res.status(503).json({
240
+ error: 'Service temporarily unavailable',
241
+ reason: 'Circuit breaker is open'
242
+ });
243
+ }
244
+ else {
245
+ throw error;
246
+ }
247
+ }
248
+ };
249
+ }
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * LAFS Conformance CLI — diagnostic/human-readable tool.
4
+ *
5
+ * This CLI is a **diagnostic utility** that validates envelopes and flags
6
+ * against the LAFS schema and conformance checks. It is NOT itself a
7
+ * LAFS-conformant envelope producer. Its output is for human consumption
8
+ * and CI pipelines, not for machine-to-machine chaining.
9
+ *
10
+ * Exemption: The CLI is exempt from LAFS envelope conformance requirements.
11
+ * Its output format is not a LAFS envelope and MUST NOT be validated as one.
12
+ *
13
+ * @task T042
14
+ * @epic T034
15
+ */
16
+ export {};