@contractspec/lib.evolution 3.7.16 → 3.7.18

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 (3) hide show
  1. package/dist/index.js +11 -1073
  2. package/dist/node/index.js +11 -1073
  3. package/package.json +14 -14
@@ -1,1084 +1,22 @@
1
- // src/analyzer/posthog-telemetry-reader.ts
2
- class PosthogTelemetryReader {
3
- reader;
4
- eventPrefix;
5
- constructor(reader, options = {}) {
6
- this.reader = reader;
7
- this.eventPrefix = options.eventPrefix ?? "observability";
8
- }
9
- async readOperationSamples(input) {
10
- const result = await this.queryHogQL({
11
- query: [
12
- "select",
13
- " properties.operation as operationKey,",
14
- " properties.version as version,",
15
- " properties.durationMs as durationMs,",
16
- " properties.success as success,",
17
- " properties.errorCode as errorCode,",
18
- " properties.tenantId as tenantId,",
19
- " properties.traceId as traceId,",
20
- " properties.metadata as metadata,",
21
- " timestamp as timestamp",
22
- "from events",
23
- `where ${buildOperationWhereClause(this.eventPrefix, input)}`,
24
- "order by timestamp desc",
25
- `limit ${input.limit ?? 1000}`
26
- ].join(`
27
- `),
28
- values: buildOperationValues(input)
29
- });
30
- return mapOperationSamples(result);
31
- }
32
- async readAnomalyBaseline(operation, windowDays = 7) {
33
- const dateRange = buildWindowRange(windowDays);
34
- const baseResult = await this.queryHogQL({
35
- query: [
36
- "select",
37
- " count() as totalCalls,",
38
- " avg(properties.durationMs) as averageLatencyMs,",
39
- " quantile(0.95)(properties.durationMs) as p95LatencyMs,",
40
- " quantile(0.99)(properties.durationMs) as p99LatencyMs,",
41
- " max(properties.durationMs) as maxLatencyMs,",
42
- " sum(if(properties.success = 1, 1, 0)) as successCount,",
43
- " sum(if(properties.success = 0, 1, 0)) as errorCount",
44
- "from events",
45
- `where ${buildOperationWhereClause(this.eventPrefix, {
46
- operations: [operation],
47
- dateRange
48
- })}`
49
- ].join(`
50
- `),
51
- values: buildOperationValues({
52
- operations: [operation],
53
- dateRange
54
- })
55
- });
56
- const stats = mapBaselineStats(baseResult, operation, dateRange);
57
- if (!stats)
58
- return null;
59
- const topErrors = await this.readTopErrors(operation, dateRange);
60
- return {
61
- ...stats,
62
- topErrors
63
- };
64
- }
65
- async readTopErrors(operation, dateRange) {
66
- const result = await this.queryHogQL({
67
- query: [
68
- "select",
69
- " properties.errorCode as errorCode,",
70
- " count() as errorCount",
71
- "from events",
72
- `where ${buildOperationWhereClause(this.eventPrefix, {
73
- operations: [operation],
74
- dateRange
75
- })} and properties.success = 0`,
76
- "group by errorCode",
77
- "order by errorCount desc",
78
- "limit 5"
79
- ].join(`
80
- `),
81
- values: buildOperationValues({
82
- operations: [operation],
83
- dateRange
84
- })
85
- });
86
- const rows = mapRows(result);
87
- return rows.reduce((acc, row) => {
88
- const code = asString(row.errorCode);
89
- if (!code)
90
- return acc;
91
- acc[code] = asNumber(row.errorCount);
92
- return acc;
93
- }, {});
94
- }
95
- async queryHogQL(input) {
96
- if (!this.reader.queryHogQL) {
97
- throw new Error("Analytics reader does not support HogQL queries.");
98
- }
99
- return this.reader.queryHogQL(input);
100
- }
101
- }
102
- function buildOperationWhereClause(eventPrefix, input) {
103
- const clauses = [`event = '${eventPrefix}.operation'`];
104
- if (input.operations?.length) {
105
- clauses.push(`(${buildOperationFilters(input.operations)})`);
106
- }
107
- if (input.dateRange?.from) {
108
- clauses.push("timestamp >= {dateFrom}");
109
- }
110
- if (input.dateRange?.to) {
111
- clauses.push("timestamp < {dateTo}");
112
- }
113
- return clauses.join(" and ");
114
- }
115
- function buildOperationValues(input) {
116
- const values = {
117
- dateFrom: toIsoString(input.dateRange?.from),
118
- dateTo: toIsoString(input.dateRange?.to)
119
- };
120
- input.operations?.forEach((op, index) => {
121
- values[`operationKey${index}`] = op.key;
122
- values[`operationVersion${index}`] = op.version;
123
- if (op.tenantId) {
124
- values[`operationTenant${index}`] = op.tenantId;
125
- }
126
- });
127
- return values;
128
- }
129
- function buildOperationFilters(operations) {
130
- return operations.map((op, index) => {
131
- const clauses = [
132
- `properties.operation = {operationKey${index}}`,
133
- `properties.version = {operationVersion${index}}`
134
- ];
135
- if (op.tenantId) {
136
- clauses.push(`properties.tenantId = {operationTenant${index}}`);
137
- }
138
- return `(${clauses.join(" and ")})`;
139
- }).join(" or ");
140
- }
141
- function mapOperationSamples(result) {
142
- const rows = mapRows(result);
143
- return rows.flatMap((row) => {
144
- const operationKey = asString(row.operationKey);
145
- const version = asString(row.version);
146
- const timestamp = asDate(row.timestamp);
147
- if (!operationKey || !version || !timestamp) {
148
- return [];
149
- }
150
- return [
151
- {
152
- operation: {
153
- key: operationKey,
154
- version,
155
- tenantId: asOptionalString(row.tenantId) ?? undefined
156
- },
157
- durationMs: asNumber(row.durationMs),
158
- success: asBoolean(row.success),
159
- timestamp,
160
- errorCode: asOptionalString(row.errorCode) ?? undefined,
161
- traceId: asOptionalString(row.traceId) ?? undefined,
162
- metadata: isRecord(row.metadata) ? row.metadata : undefined
163
- }
164
- ];
165
- });
166
- }
167
- function mapBaselineStats(result, operation, dateRange) {
168
- const rows = mapRows(result);
169
- const row = rows[0];
170
- if (!row)
171
- return null;
172
- const totalCalls = asNumber(row.totalCalls);
173
- if (!totalCalls)
174
- return null;
175
- const successCount = asNumber(row.successCount);
176
- const errorCount = asNumber(row.errorCount);
177
- return {
178
- operation,
179
- totalCalls,
180
- successRate: totalCalls ? successCount / totalCalls : 0,
181
- errorRate: totalCalls ? errorCount / totalCalls : 0,
182
- averageLatencyMs: asNumber(row.averageLatencyMs),
183
- p95LatencyMs: asNumber(row.p95LatencyMs),
184
- p99LatencyMs: asNumber(row.p99LatencyMs),
185
- maxLatencyMs: asNumber(row.maxLatencyMs),
186
- lastSeenAt: new Date,
187
- windowStart: toDate(dateRange.from) ?? new Date,
188
- windowEnd: toDate(dateRange.to) ?? new Date,
189
- topErrors: {}
190
- };
191
- }
192
- function mapRows(result) {
193
- if (!Array.isArray(result.results) || !Array.isArray(result.columns)) {
194
- return [];
195
- }
196
- const columns = result.columns;
197
- return result.results.flatMap((row) => {
198
- if (!Array.isArray(row))
199
- return [];
200
- const record = {};
201
- columns.forEach((column, index) => {
202
- record[column] = row[index];
203
- });
204
- return [record];
205
- });
206
- }
207
- function buildWindowRange(windowDays) {
208
- const windowEnd = new Date;
209
- const windowStart = new Date(windowEnd.getTime() - windowDays * 24 * 60 * 60 * 1000);
210
- return {
211
- from: windowStart,
212
- to: windowEnd
213
- };
214
- }
215
- function asString(value) {
216
- if (typeof value === "string" && value.trim())
217
- return value;
218
- if (typeof value === "number")
219
- return String(value);
220
- return null;
221
- }
222
- function asOptionalString(value) {
223
- if (typeof value === "string")
224
- return value;
225
- if (typeof value === "number")
226
- return String(value);
227
- return null;
228
- }
229
- function asNumber(value) {
230
- if (typeof value === "number" && Number.isFinite(value))
231
- return value;
232
- if (typeof value === "string" && value.trim()) {
233
- const parsed = Number(value);
234
- if (Number.isFinite(parsed))
235
- return parsed;
236
- }
237
- return 0;
238
- }
239
- function asBoolean(value) {
240
- if (typeof value === "boolean")
241
- return value;
242
- if (typeof value === "number")
243
- return value !== 0;
244
- if (typeof value === "string")
245
- return value.toLowerCase() === "true";
246
- return false;
247
- }
248
- function asDate(value) {
249
- if (value instanceof Date)
250
- return value;
251
- if (typeof value === "string" || typeof value === "number") {
252
- const date = new Date(value);
253
- if (!Number.isNaN(date.getTime()))
254
- return date;
255
- }
256
- return null;
257
- }
258
- function toIsoString(value) {
259
- if (!value)
260
- return;
261
- return typeof value === "string" ? value : value.toISOString();
262
- }
263
- function toDate(value) {
264
- if (!value)
265
- return null;
266
- return value instanceof Date ? value : new Date(value);
267
- }
268
- function isRecord(value) {
269
- return typeof value === "object" && value !== null;
270
- }
271
- // src/analyzer/spec-analyzer.ts
272
- import { LifecycleStage } from "@contractspec/lib.lifecycle";
273
- import { randomUUID } from "crypto";
274
- var DEFAULT_OPTIONS = {
275
- minSampleSize: 50,
276
- errorRateThreshold: 0.05,
277
- latencyP99ThresholdMs: 750
278
- };
279
-
280
- class SpecAnalyzer {
281
- logger;
282
- minSampleSize;
283
- errorRateThreshold;
284
- latencyP99ThresholdMs;
285
- throughputDropThreshold;
286
- constructor(options = {}) {
287
- this.logger = options.logger;
288
- this.minSampleSize = options.minSampleSize ?? DEFAULT_OPTIONS.minSampleSize;
289
- this.errorRateThreshold = options.errorRateThreshold ?? DEFAULT_OPTIONS.errorRateThreshold;
290
- this.latencyP99ThresholdMs = options.latencyP99ThresholdMs ?? DEFAULT_OPTIONS.latencyP99ThresholdMs;
291
- this.throughputDropThreshold = options.throughputDropThreshold ?? 0.2;
292
- }
293
- analyzeSpecUsage(samples) {
294
- if (!samples.length) {
295
- this.logger?.debug("SpecAnalyzer.analyzeSpecUsage.skip", {
296
- reason: "no-samples"
297
- });
298
- return [];
299
- }
300
- const groups = new Map;
301
- for (const sample of samples) {
302
- const key = this.operationKey(sample);
303
- const arr = groups.get(key) ?? [];
304
- arr.push(sample);
305
- groups.set(key, arr);
306
- }
307
- const entries = [...groups.values()];
308
- const usable = entries.filter((samplesForOp) => {
309
- const valid = samplesForOp.length >= this.minSampleSize;
310
- if (!valid) {
311
- this.logger?.debug("SpecAnalyzer.analyzeSpecUsage.skipOperation", {
312
- operation: this.operationKey(samplesForOp[0]),
313
- sampleSize: samplesForOp.length,
314
- minSampleSize: this.minSampleSize
315
- });
316
- }
317
- return valid;
318
- });
319
- return usable.map((operationSamples) => this.buildUsageStats(operationSamples));
320
- }
321
- detectAnomalies(stats, baseline) {
322
- const anomalies = [];
323
- if (!stats.length) {
324
- this.logger?.debug("SpecAnalyzer.detectAnomalies.skip", {
325
- reason: "no-stats"
326
- });
327
- return anomalies;
328
- }
329
- const baselineByOp = new Map((baseline ?? []).map((item) => [this.operationKey(item.operation), item]));
330
- for (const stat of stats) {
331
- const evidence = [];
332
- if (stat.errorRate >= this.errorRateThreshold) {
333
- evidence.push({
334
- type: "telemetry",
335
- description: `Error rate ${stat.errorRate.toFixed(2)} exceeded threshold ${this.errorRateThreshold}`,
336
- data: { errorRate: stat.errorRate }
337
- });
338
- anomalies.push({
339
- operation: stat.operation,
340
- severity: this.toSeverity(stat.errorRate / this.errorRateThreshold),
341
- metric: "error-rate",
342
- description: "Error rate spike",
343
- detectedAt: new Date,
344
- threshold: this.errorRateThreshold,
345
- observedValue: stat.errorRate,
346
- evidence
347
- });
348
- continue;
349
- }
350
- if (stat.p99LatencyMs >= this.latencyP99ThresholdMs) {
351
- evidence.push({
352
- type: "telemetry",
353
- description: `P99 latency ${stat.p99LatencyMs}ms exceeded threshold ${this.latencyP99ThresholdMs}ms`,
354
- data: { p99LatencyMs: stat.p99LatencyMs }
355
- });
356
- anomalies.push({
357
- operation: stat.operation,
358
- severity: this.toSeverity(stat.p99LatencyMs / this.latencyP99ThresholdMs),
359
- metric: "latency",
360
- description: "Latency regression detected",
361
- detectedAt: new Date,
362
- threshold: this.latencyP99ThresholdMs,
363
- observedValue: stat.p99LatencyMs,
364
- evidence
365
- });
366
- continue;
367
- }
368
- const baselineStat = baselineByOp.get(this.operationKey(stat.operation));
369
- if (baselineStat) {
370
- const drop = (baselineStat.totalCalls - stat.totalCalls) / baselineStat.totalCalls;
371
- if (drop >= this.throughputDropThreshold) {
372
- evidence.push({
373
- type: "telemetry",
374
- description: `Throughput dropped by ${(drop * 100).toFixed(1)}% compared to baseline`,
375
- data: {
376
- baselineCalls: baselineStat.totalCalls,
377
- currentCalls: stat.totalCalls
378
- }
379
- });
380
- anomalies.push({
381
- operation: stat.operation,
382
- severity: this.toSeverity(drop / this.throughputDropThreshold),
383
- metric: "throughput",
384
- description: "Usage drop detected",
385
- detectedAt: new Date,
386
- threshold: this.throughputDropThreshold,
387
- observedValue: drop,
388
- evidence
389
- });
390
- }
391
- }
392
- }
393
- return anomalies;
394
- }
395
- toIntentPatterns(anomalies, stats) {
396
- const statsByOp = new Map(stats.map((item) => [this.operationKey(item.operation), item]));
397
- return anomalies.map((anomaly) => {
398
- const stat = statsByOp.get(this.operationKey(anomaly.operation));
399
- const confidence = {
400
- score: Math.min(1, (anomaly.observedValue ?? 0) / (anomaly.threshold ?? 1)),
401
- sampleSize: stat?.totalCalls ?? 0,
402
- pValue: undefined
403
- };
404
- return {
405
- id: randomUUID(),
406
- type: this.mapMetricToIntent(anomaly.metric),
407
- description: anomaly.description,
408
- operation: anomaly.operation,
409
- confidence,
410
- metadata: {
411
- observedValue: anomaly.observedValue,
412
- threshold: anomaly.threshold
413
- },
414
- evidence: anomaly.evidence
415
- };
416
- });
417
- }
418
- suggestOptimizations(stats, anomalies, lifecycleContext) {
419
- const anomaliesByOp = new Map(this.groupByOperation(anomalies));
420
- const hints = [];
421
- for (const stat of stats) {
422
- const opKey = this.operationKey(stat.operation);
423
- const opAnomalies = anomaliesByOp.get(opKey) ?? [];
424
- for (const anomaly of opAnomalies) {
425
- if (anomaly.metric === "latency") {
426
- hints.push(this.applyLifecycleContext({
427
- operation: stat.operation,
428
- category: "performance",
429
- summary: "Latency regression detected",
430
- justification: `P99 latency at ${stat.p99LatencyMs}ms`,
431
- recommendedActions: [
432
- "Add batching or caching layer",
433
- "Replay golden tests to capture slow inputs"
434
- ]
435
- }, lifecycleContext?.stage));
436
- } else if (anomaly.metric === "error-rate") {
437
- const topError = Object.entries(stat.topErrors).sort((a, b) => b[1] - a[1])[0]?.[0];
438
- hints.push(this.applyLifecycleContext({
439
- operation: stat.operation,
440
- category: "error-handling",
441
- summary: "Error spike detected",
442
- justification: topError ? `Dominant error code ${topError}` : "Increase in failures",
443
- recommendedActions: [
444
- "Generate regression spec from failing payloads",
445
- "Add policy guardrails before rollout"
446
- ]
447
- }, lifecycleContext?.stage));
448
- } else if (anomaly.metric === "throughput") {
449
- hints.push(this.applyLifecycleContext({
450
- operation: stat.operation,
451
- category: "performance",
452
- summary: "Throughput drop detected",
453
- justification: "Significant traffic reduction relative to baseline",
454
- recommendedActions: [
455
- "Validate routing + feature flag bucketing",
456
- "Backfill spec variant to rehydrate demand"
457
- ]
458
- }, lifecycleContext?.stage));
459
- }
460
- }
461
- }
462
- return hints;
463
- }
464
- operationKey(op) {
465
- const coordinate = "operation" in op ? op.operation : op;
466
- return `${coordinate.key}.v${coordinate.version}${coordinate.tenantId ? `@${coordinate.tenantId}` : ""}`;
467
- }
468
- buildUsageStats(samples) {
469
- const durations = samples.map((s) => s.durationMs).sort((a, b) => a - b);
470
- const errors = samples.filter((s) => !s.success);
471
- const totalCalls = samples.length;
472
- const successRate = (totalCalls - errors.length) / totalCalls;
473
- const errorRate = errors.length / totalCalls;
474
- const averageLatencyMs = durations.reduce((sum, value) => sum + value, 0) / totalCalls;
475
- const topErrors = errors.reduce((acc, sample) => {
476
- if (!sample.errorCode)
477
- return acc;
478
- acc[sample.errorCode] = (acc[sample.errorCode] ?? 0) + 1;
479
- return acc;
480
- }, {});
481
- const timestamps = samples.map((s) => s.timestamp.getTime());
482
- const windowStart = new Date(Math.min(...timestamps));
483
- const windowEnd = new Date(Math.max(...timestamps));
484
- return {
485
- operation: samples[0].operation,
486
- totalCalls,
487
- successRate,
488
- errorRate,
489
- averageLatencyMs,
490
- p95LatencyMs: percentile(durations, 0.95),
491
- p99LatencyMs: percentile(durations, 0.99),
492
- maxLatencyMs: Math.max(...durations),
493
- lastSeenAt: windowEnd,
494
- windowStart,
495
- windowEnd,
496
- topErrors
497
- };
498
- }
499
- toSeverity(ratio) {
500
- if (ratio >= 2)
501
- return "high";
502
- if (ratio >= 1.3)
503
- return "medium";
504
- return "low";
505
- }
506
- mapMetricToIntent(metric) {
507
- switch (metric) {
508
- case "error-rate":
509
- return "error-spike";
510
- case "latency":
511
- return "latency-regression";
512
- case "throughput":
513
- return "throughput-drop";
514
- default:
515
- return "schema-mismatch";
516
- }
517
- }
518
- groupByOperation(items) {
519
- const map = new Map;
520
- for (const item of items) {
521
- const key = this.operationKey(item.operation);
522
- const arr = map.get(key) ?? [];
523
- arr.push(item);
524
- map.set(key, arr);
525
- }
526
- return map;
527
- }
528
- applyLifecycleContext(hint, stage) {
529
- if (stage === undefined)
530
- return hint;
531
- const band = mapStageBand(stage);
532
- const advice = LIFECYCLE_HINTS[band]?.[hint.category];
533
- if (!advice) {
534
- return { ...hint, lifecycleStage: stage };
535
- }
536
- return {
537
- ...hint,
538
- lifecycleStage: stage,
539
- lifecycleNotes: advice.message,
540
- recommendedActions: dedupeActions([
541
- ...hint.recommendedActions,
542
- ...advice.supplementalActions
543
- ])
544
- };
545
- }
546
- }
547
- function percentile(values, p) {
548
- if (!values.length)
549
- return 0;
550
- if (values.length === 1)
551
- return values[0];
552
- const idx = Math.min(values.length - 1, Math.floor(p * values.length));
553
- return values[idx];
554
- }
555
- var mapStageBand = (stage) => {
556
- if (stage <= 2)
557
- return "early";
558
- if (stage === LifecycleStage.ProductMarketFit)
559
- return "pmf";
560
- if (stage === LifecycleStage.GrowthScaleUp || stage === LifecycleStage.ExpansionPlatform) {
561
- return "scale";
562
- }
563
- return "mature";
564
- };
565
- var LIFECYCLE_HINTS = {
566
- early: {
567
- performance: {
568
- message: "Favor guardrails that protect learning velocity before heavy rewrites.",
569
- supplementalActions: [
570
- "Wrap risky changes behind progressive delivery flags"
571
- ]
572
- },
573
- "error-handling": {
574
- message: "Make failures loud and recoverable so you can learn faster.",
575
- supplementalActions: ["Add auto-rollbacks or manual kill switches"]
576
- }
577
- },
578
- pmf: {
579
- performance: {
580
- message: "Stabilize the core use case to avoid regressions while demand grows.",
581
- supplementalActions: ["Instrument regression tests on critical specs"]
582
- }
583
- },
584
- scale: {
585
- performance: {
586
- message: "Prioritize resilience and multi-tenant safety as volumes expand.",
587
- supplementalActions: [
588
- "Introduce workload partitioning or isolation per tenant"
589
- ]
590
- },
591
- "error-handling": {
592
- message: "Contain blast radius with policy fallbacks and circuit breakers.",
593
- supplementalActions: ["Add circuit breakers to high-risk operations"]
594
- }
595
- },
596
- mature: {
597
- performance: {
598
- message: "Optimize for margins and predictable SLAs.",
599
- supplementalActions: [
600
- "Capture unit-cost impacts alongside latency fixes"
601
- ]
602
- },
603
- "error-handling": {
604
- message: "Prevent regressions with automated regression specs before deploy.",
605
- supplementalActions: [
606
- "Run auto-evolution simulations on renewal scenarios"
607
- ]
608
- }
609
- }
610
- };
611
- var dedupeActions = (actions) => {
612
- const seen = new Set;
613
- const ordered = [];
614
- for (const action of actions) {
615
- if (seen.has(action))
616
- continue;
617
- seen.add(action);
618
- ordered.push(action);
619
- }
620
- return ordered;
621
- };
622
- // src/approval/integration.ts
623
- import { mkdir, writeFile } from "node:fs/promises";
624
- import { join } from "node:path";
625
-
626
- class SpecSuggestionOrchestrator {
627
- options;
628
- constructor(options) {
629
- this.options = options;
630
- }
631
- async submit(suggestion, session, approvalReason) {
632
- await this.options.repository.create(suggestion);
633
- if (session && this.options.approval) {
634
- await this.options.approval.requestApproval({
635
- sessionId: session.sessionId,
636
- agentId: session.agentId,
637
- tenantId: session.tenantId,
638
- toolName: "evolution_apply_suggestion",
639
- toolCallId: suggestion.id,
640
- toolArgs: { suggestionId: suggestion.id },
641
- reason: approvalReason ?? suggestion.proposal.summary,
642
- payload: { suggestionId: suggestion.id }
643
- });
644
- }
645
- return suggestion;
646
- }
647
- async approve(id, reviewer, notes) {
648
- const suggestion = await this.ensureSuggestion(id);
649
- await this.options.repository.updateStatus(id, "approved", {
650
- reviewer,
651
- notes,
652
- decidedAt: new Date
653
- });
654
- if (this.options.writer) {
655
- await this.options.writer.write({
656
- ...suggestion,
657
- status: "approved",
658
- approvals: {
659
- reviewer,
660
- notes,
661
- decidedAt: new Date,
662
- status: "approved"
663
- }
664
- });
665
- }
666
- }
667
- async reject(id, reviewer, notes) {
668
- await this.options.repository.updateStatus(id, "rejected", {
669
- reviewer,
670
- notes,
671
- decidedAt: new Date
672
- });
673
- }
674
- list(filters) {
675
- return this.options.repository.list(filters);
676
- }
677
- async ensureSuggestion(id) {
678
- const suggestion = await this.options.repository.getById(id);
679
- if (!suggestion)
680
- throw new Error(`Spec suggestion ${id} not found`);
681
- return suggestion;
682
- }
683
- }
684
-
685
- class FileSystemSuggestionWriter {
686
- outputDir;
687
- filenameTemplate;
688
- constructor(options = {}) {
689
- this.outputDir = options.outputDir ?? join(process.cwd(), "packages/libs/contracts-spec/src/generated");
690
- this.filenameTemplate = options.filenameTemplate ?? ((suggestion) => `${suggestion.target?.key ?? suggestion.intent.id}.v${suggestion.target?.version ?? "next"}.suggestion.json`);
691
- }
692
- async write(suggestion) {
693
- await mkdir(this.outputDir, { recursive: true });
694
- const filename = this.filenameTemplate(suggestion);
695
- const filepath = join(this.outputDir, filename);
696
- const payload = serializeSuggestion(suggestion);
697
- await writeFile(filepath, JSON.stringify(payload, null, 2));
698
- return filepath;
699
- }
700
- }
701
-
702
- class InMemorySpecSuggestionRepository {
703
- items = new Map;
704
- async create(suggestion) {
705
- this.items.set(suggestion.id, suggestion);
706
- }
707
- async getById(id) {
708
- return this.items.get(id);
709
- }
710
- async updateStatus(id, status, metadata) {
711
- const suggestion = await this.getById(id);
712
- if (!suggestion)
713
- return;
714
- this.items.set(id, {
715
- ...suggestion,
716
- status,
717
- approvals: {
718
- reviewer: metadata?.reviewer,
719
- notes: metadata?.notes,
720
- decidedAt: metadata?.decidedAt,
721
- status
722
- }
723
- });
724
- }
725
- async list(filters) {
726
- const values = [...this.items.values()];
727
- if (!filters)
728
- return values;
729
- return values.filter((item) => {
730
- if (filters.status && item.status !== filters.status)
731
- return false;
732
- if (filters.operationKey && item.target?.key !== filters.operationKey)
733
- return false;
734
- return true;
735
- });
736
- }
737
- }
738
- function serializeSuggestion(suggestion) {
739
- const { proposal, ...rest } = suggestion;
740
- const { spec, ...proposalRest } = proposal;
741
- return {
742
- ...rest,
743
- proposal: {
744
- ...proposalRest,
745
- specMeta: spec?.meta
746
- },
747
- createdAt: suggestion.createdAt.toISOString(),
748
- intent: {
749
- ...suggestion.intent,
750
- confidence: { ...suggestion.intent.confidence },
751
- evidence: suggestion.intent.evidence
752
- }
753
- };
754
- }
755
- // src/generator/ai-spec-generator.ts
756
- import { generateText, Output } from "ai";
757
- import { randomUUID as randomUUID2 } from "crypto";
758
- import * as z from "zod";
759
- var SpecSuggestionProposalSchema = z.object({
760
- summary: z.string().describe("Brief summary of the proposed change"),
761
- rationale: z.string().describe("Detailed explanation of why this change is needed"),
762
- changeType: z.enum(["new-spec", "revision", "policy-update", "schema-update"]).describe("Type of change being proposed"),
763
- recommendedActions: z.array(z.string()).describe("List of specific actions to implement the change"),
764
- estimatedImpact: z.enum(["low", "medium", "high"]).describe("Estimated impact of implementing this change"),
765
- riskLevel: z.enum(["low", "medium", "high"]).describe("Risk level associated with this change"),
766
- diff: z.string().optional().describe("Optional diff or code snippet showing the change")
767
- });
768
-
769
- class AISpecGenerator {
770
- model;
771
- config;
772
- systemPrompt;
773
- modelSelector;
774
- selectionContext;
775
- constructor(options) {
776
- this.model = options.model;
777
- this.modelSelector = options.modelSelector;
778
- this.selectionContext = options.selectionContext;
779
- this.config = options.evolutionConfig ?? {};
780
- this.systemPrompt = options.systemPrompt ?? `You are a ContractSpec evolution expert. Your role is to analyze telemetry data, anomalies, and usage patterns to suggest improvements to API contracts and specifications.
1
+ class L{reader;eventPrefix;constructor(e,r={}){this.reader=e,this.eventPrefix=r.eventPrefix??"observability"}async readOperationSamples(e){let r=await this.queryHogQL({query:["select"," properties.operation as operationKey,"," properties.version as version,"," properties.durationMs as durationMs,"," properties.success as success,"," properties.errorCode as errorCode,"," properties.tenantId as tenantId,"," properties.traceId as traceId,"," properties.metadata as metadata,"," timestamp as timestamp","from events",`where ${m(this.eventPrefix,e)}`,"order by timestamp desc",`limit ${e.limit??1000}`].join(`
2
+ `),values:g(e)});return O(r)}async readAnomalyBaseline(e,r=7){let n=v(r),t=await this.queryHogQL({query:["select"," count() as totalCalls,"," avg(properties.durationMs) as averageLatencyMs,"," quantile(0.95)(properties.durationMs) as p95LatencyMs,"," quantile(0.99)(properties.durationMs) as p99LatencyMs,"," max(properties.durationMs) as maxLatencyMs,"," sum(if(properties.success = 1, 1, 0)) as successCount,"," sum(if(properties.success = 0, 1, 0)) as errorCount","from events",`where ${m(this.eventPrefix,{operations:[e],dateRange:n})}`].join(`
3
+ `),values:g({operations:[e],dateRange:n})}),o=q(t,e,n);if(!o)return null;let c=await this.readTopErrors(e,n);return{...o,topErrors:c}}async readTopErrors(e,r){let n=await this.queryHogQL({query:["select"," properties.errorCode as errorCode,"," count() as errorCount","from events",`where ${m(this.eventPrefix,{operations:[e],dateRange:r})} and properties.success = 0`,"group by errorCode","order by errorCount desc","limit 5"].join(`
4
+ `),values:g({operations:[e],dateRange:r})});return b(n).reduce((o,c)=>{let i=R(c.errorCode);if(!i)return o;return o[i]=u(c.errorCount),o},{})}async queryHogQL(e){if(!this.reader.queryHogQL)throw Error("Analytics reader does not support HogQL queries.");return this.reader.queryHogQL(e)}}function m(e,r){let n=[`event = '${e}.operation'`];if(r.operations?.length)n.push(`(${$(r.operations)})`);if(r.dateRange?.from)n.push("timestamp >= {dateFrom}");if(r.dateRange?.to)n.push("timestamp < {dateTo}");return n.join(" and ")}function g(e){let r={dateFrom:P(e.dateRange?.from),dateTo:P(e.dateRange?.to)};return e.operations?.forEach((n,t)=>{if(r[`operationKey${t}`]=n.key,r[`operationVersion${t}`]=n.version,n.tenantId)r[`operationTenant${t}`]=n.tenantId}),r}function $(e){return e.map((r,n)=>{let t=[`properties.operation = {operationKey${n}}`,`properties.version = {operationVersion${n}}`];if(r.tenantId)t.push(`properties.tenantId = {operationTenant${n}}`);return`(${t.join(" and ")})`}).join(" or ")}function O(e){return b(e).flatMap((n)=>{let t=R(n.operationKey),o=R(n.version),c=T(n.timestamp);if(!t||!o||!c)return[];return[{operation:{key:t,version:o,tenantId:h(n.tenantId)??void 0},durationMs:u(n.durationMs),success:E(n.success),timestamp:c,errorCode:h(n.errorCode)??void 0,traceId:h(n.traceId)??void 0,metadata:Q(n.metadata)?n.metadata:void 0}]})}function q(e,r,n){let o=b(e)[0];if(!o)return null;let c=u(o.totalCalls);if(!c)return null;let i=u(o.successCount),d=u(o.errorCount);return{operation:r,totalCalls:c,successRate:c?i/c:0,errorRate:c?d/c:0,averageLatencyMs:u(o.averageLatencyMs),p95LatencyMs:u(o.p95LatencyMs),p99LatencyMs:u(o.p99LatencyMs),maxLatencyMs:u(o.maxLatencyMs),lastSeenAt:new Date,windowStart:k(n.from)??new Date,windowEnd:k(n.to)??new Date,topErrors:{}}}function b(e){if(!Array.isArray(e.results)||!Array.isArray(e.columns))return[];let r=e.columns;return e.results.flatMap((n)=>{if(!Array.isArray(n))return[];let t={};return r.forEach((o,c)=>{t[o]=n[c]}),[t]})}function v(e){let r=new Date;return{from:new Date(r.getTime()-e*24*60*60*1000),to:r}}function R(e){if(typeof e==="string"&&e.trim())return e;if(typeof e==="number")return String(e);return null}function h(e){if(typeof e==="string")return e;if(typeof e==="number")return String(e);return null}function u(e){if(typeof e==="number"&&Number.isFinite(e))return e;if(typeof e==="string"&&e.trim()){let r=Number(e);if(Number.isFinite(r))return r}return 0}function E(e){if(typeof e==="boolean")return e;if(typeof e==="number")return e!==0;if(typeof e==="string")return e.toLowerCase()==="true";return!1}function T(e){if(e instanceof Date)return e;if(typeof e==="string"||typeof e==="number"){let r=new Date(e);if(!Number.isNaN(r.getTime()))return r}return null}function P(e){if(!e)return;return typeof e==="string"?e:e.toISOString()}function k(e){if(!e)return null;return e instanceof Date?e:new Date(e)}function Q(e){return typeof e==="object"&&e!==null}import{LifecycleStage as C}from"@contractspec/lib.lifecycle";import{randomUUID as V}from"crypto";var w={minSampleSize:50,errorRateThreshold:0.05,latencyP99ThresholdMs:750};class j{logger;minSampleSize;errorRateThreshold;latencyP99ThresholdMs;throughputDropThreshold;constructor(e={}){this.logger=e.logger,this.minSampleSize=e.minSampleSize??w.minSampleSize,this.errorRateThreshold=e.errorRateThreshold??w.errorRateThreshold,this.latencyP99ThresholdMs=e.latencyP99ThresholdMs??w.latencyP99ThresholdMs,this.throughputDropThreshold=e.throughputDropThreshold??0.2}analyzeSpecUsage(e){if(!e.length)return this.logger?.debug("SpecAnalyzer.analyzeSpecUsage.skip",{reason:"no-samples"}),[];let r=new Map;for(let o of e){let c=this.operationKey(o),i=r.get(c)??[];i.push(o),r.set(c,i)}return[...r.values()].filter((o)=>{let c=o.length>=this.minSampleSize;if(!c)this.logger?.debug("SpecAnalyzer.analyzeSpecUsage.skipOperation",{operation:this.operationKey(o[0]),sampleSize:o.length,minSampleSize:this.minSampleSize});return c}).map((o)=>this.buildUsageStats(o))}detectAnomalies(e,r){let n=[];if(!e.length)return this.logger?.debug("SpecAnalyzer.detectAnomalies.skip",{reason:"no-stats"}),n;let t=new Map((r??[]).map((o)=>[this.operationKey(o.operation),o]));for(let o of e){let c=[];if(o.errorRate>=this.errorRateThreshold){c.push({type:"telemetry",description:`Error rate ${o.errorRate.toFixed(2)} exceeded threshold ${this.errorRateThreshold}`,data:{errorRate:o.errorRate}}),n.push({operation:o.operation,severity:this.toSeverity(o.errorRate/this.errorRateThreshold),metric:"error-rate",description:"Error rate spike",detectedAt:new Date,threshold:this.errorRateThreshold,observedValue:o.errorRate,evidence:c});continue}if(o.p99LatencyMs>=this.latencyP99ThresholdMs){c.push({type:"telemetry",description:`P99 latency ${o.p99LatencyMs}ms exceeded threshold ${this.latencyP99ThresholdMs}ms`,data:{p99LatencyMs:o.p99LatencyMs}}),n.push({operation:o.operation,severity:this.toSeverity(o.p99LatencyMs/this.latencyP99ThresholdMs),metric:"latency",description:"Latency regression detected",detectedAt:new Date,threshold:this.latencyP99ThresholdMs,observedValue:o.p99LatencyMs,evidence:c});continue}let i=t.get(this.operationKey(o.operation));if(i){let d=(i.totalCalls-o.totalCalls)/i.totalCalls;if(d>=this.throughputDropThreshold)c.push({type:"telemetry",description:`Throughput dropped by ${(d*100).toFixed(1)}% compared to baseline`,data:{baselineCalls:i.totalCalls,currentCalls:o.totalCalls}}),n.push({operation:o.operation,severity:this.toSeverity(d/this.throughputDropThreshold),metric:"throughput",description:"Usage drop detected",detectedAt:new Date,threshold:this.throughputDropThreshold,observedValue:d,evidence:c})}}return n}toIntentPatterns(e,r){let n=new Map(r.map((t)=>[this.operationKey(t.operation),t]));return e.map((t)=>{let o=n.get(this.operationKey(t.operation)),c={score:Math.min(1,(t.observedValue??0)/(t.threshold??1)),sampleSize:o?.totalCalls??0,pValue:void 0};return{id:V(),type:this.mapMetricToIntent(t.metric),description:t.description,operation:t.operation,confidence:c,metadata:{observedValue:t.observedValue,threshold:t.threshold},evidence:t.evidence}})}suggestOptimizations(e,r,n){let t=new Map(this.groupByOperation(r)),o=[];for(let c of e){let i=this.operationKey(c.operation),d=t.get(i)??[];for(let s of d)if(s.metric==="latency")o.push(this.applyLifecycleContext({operation:c.operation,category:"performance",summary:"Latency regression detected",justification:`P99 latency at ${c.p99LatencyMs}ms`,recommendedActions:["Add batching or caching layer","Replay golden tests to capture slow inputs"]},n?.stage));else if(s.metric==="error-rate"){let y=Object.entries(c.topErrors).sort((S,a)=>a[1]-S[1])[0]?.[0];o.push(this.applyLifecycleContext({operation:c.operation,category:"error-handling",summary:"Error spike detected",justification:y?`Dominant error code ${y}`:"Increase in failures",recommendedActions:["Generate regression spec from failing payloads","Add policy guardrails before rollout"]},n?.stage))}else if(s.metric==="throughput")o.push(this.applyLifecycleContext({operation:c.operation,category:"performance",summary:"Throughput drop detected",justification:"Significant traffic reduction relative to baseline",recommendedActions:["Validate routing + feature flag bucketing","Backfill spec variant to rehydrate demand"]},n?.stage))}return o}operationKey(e){let r="operation"in e?e.operation:e;return`${r.key}.v${r.version}${r.tenantId?`@${r.tenantId}`:""}`}buildUsageStats(e){let r=e.map((a)=>a.durationMs).sort((a,f)=>a-f),n=e.filter((a)=>!a.success),t=e.length,o=(t-n.length)/t,c=n.length/t,i=r.reduce((a,f)=>a+f,0)/t,d=n.reduce((a,f)=>{if(!f.errorCode)return a;return a[f.errorCode]=(a[f.errorCode]??0)+1,a},{}),s=e.map((a)=>a.timestamp.getTime()),y=new Date(Math.min(...s)),S=new Date(Math.max(...s));return{operation:e[0].operation,totalCalls:t,successRate:o,errorRate:c,averageLatencyMs:i,p95LatencyMs:M(r,0.95),p99LatencyMs:M(r,0.99),maxLatencyMs:Math.max(...r),lastSeenAt:S,windowStart:y,windowEnd:S,topErrors:d}}toSeverity(e){if(e>=2)return"high";if(e>=1.3)return"medium";return"low"}mapMetricToIntent(e){switch(e){case"error-rate":return"error-spike";case"latency":return"latency-regression";case"throughput":return"throughput-drop";default:return"schema-mismatch"}}groupByOperation(e){let r=new Map;for(let n of e){let t=this.operationKey(n.operation),o=r.get(t)??[];o.push(n),r.set(t,o)}return r}applyLifecycleContext(e,r){if(r===void 0)return e;let n=G(r),t=H[n]?.[e.category];if(!t)return{...e,lifecycleStage:r};return{...e,lifecycleStage:r,lifecycleNotes:t.message,recommendedActions:K([...e.recommendedActions,...t.supplementalActions])}}}function M(e,r){if(!e.length)return 0;if(e.length===1)return e[0];let n=Math.min(e.length-1,Math.floor(r*e.length));return e[n]}var G=(e)=>{if(e<=2)return"early";if(e===C.ProductMarketFit)return"pmf";if(e===C.GrowthScaleUp||e===C.ExpansionPlatform)return"scale";return"mature"},H={early:{performance:{message:"Favor guardrails that protect learning velocity before heavy rewrites.",supplementalActions:["Wrap risky changes behind progressive delivery flags"]},"error-handling":{message:"Make failures loud and recoverable so you can learn faster.",supplementalActions:["Add auto-rollbacks or manual kill switches"]}},pmf:{performance:{message:"Stabilize the core use case to avoid regressions while demand grows.",supplementalActions:["Instrument regression tests on critical specs"]}},scale:{performance:{message:"Prioritize resilience and multi-tenant safety as volumes expand.",supplementalActions:["Introduce workload partitioning or isolation per tenant"]},"error-handling":{message:"Contain blast radius with policy fallbacks and circuit breakers.",supplementalActions:["Add circuit breakers to high-risk operations"]}},mature:{performance:{message:"Optimize for margins and predictable SLAs.",supplementalActions:["Capture unit-cost impacts alongside latency fixes"]},"error-handling":{message:"Prevent regressions with automated regression specs before deploy.",supplementalActions:["Run auto-evolution simulations on renewal scenarios"]}}},K=(e)=>{let r=new Set,n=[];for(let t of e){if(r.has(t))continue;r.add(t),n.push(t)}return n};import{mkdir as B,writeFile as N}from"node:fs/promises";import{join as A}from"node:path";class W{options;constructor(e){this.options=e}async submit(e,r,n){if(await this.options.repository.create(e),r&&this.options.approval)await this.options.approval.requestApproval({sessionId:r.sessionId,agentId:r.agentId,tenantId:r.tenantId,toolName:"evolution_apply_suggestion",toolCallId:e.id,toolArgs:{suggestionId:e.id},reason:n??e.proposal.summary,payload:{suggestionId:e.id}});return e}async approve(e,r,n){let t=await this.ensureSuggestion(e);if(await this.options.repository.updateStatus(e,"approved",{reviewer:r,notes:n,decidedAt:new Date}),this.options.writer)await this.options.writer.write({...t,status:"approved",approvals:{reviewer:r,notes:n,decidedAt:new Date,status:"approved"}})}async reject(e,r,n){await this.options.repository.updateStatus(e,"rejected",{reviewer:r,notes:n,decidedAt:new Date})}list(e){return this.options.repository.list(e)}async ensureSuggestion(e){let r=await this.options.repository.getById(e);if(!r)throw Error(`Spec suggestion ${e} not found`);return r}}class z{outputDir;filenameTemplate;constructor(e={}){this.outputDir=e.outputDir??A(process.cwd(),"packages/libs/contracts-spec/src/generated"),this.filenameTemplate=e.filenameTemplate??((r)=>`${r.target?.key??r.intent.id}.v${r.target?.version??"next"}.suggestion.json`)}async write(e){await B(this.outputDir,{recursive:!0});let r=this.filenameTemplate(e),n=A(this.outputDir,r),t=X(e);return await N(n,JSON.stringify(t,null,2)),n}}class J{items=new Map;async create(e){this.items.set(e.id,e)}async getById(e){return this.items.get(e)}async updateStatus(e,r,n){let t=await this.getById(e);if(!t)return;this.items.set(e,{...t,status:r,approvals:{reviewer:n?.reviewer,notes:n?.notes,decidedAt:n?.decidedAt,status:r}})}async list(e){let r=[...this.items.values()];if(!e)return r;return r.filter((n)=>{if(e.status&&n.status!==e.status)return!1;if(e.operationKey&&n.target?.key!==e.operationKey)return!1;return!0})}}function X(e){let{proposal:r,...n}=e,{spec:t,...o}=r;return{...n,proposal:{...o,specMeta:t?.meta},createdAt:e.createdAt.toISOString(),intent:{...e.intent,confidence:{...e.intent.confidence},evidence:e.intent.evidence}}}import{generateText as D,Output as I}from"ai";import{randomUUID as Z}from"crypto";import*as p from"zod";var l=p.object({summary:p.string().describe("Brief summary of the proposed change"),rationale:p.string().describe("Detailed explanation of why this change is needed"),changeType:p.enum(["new-spec","revision","policy-update","schema-update"]).describe("Type of change being proposed"),recommendedActions:p.array(p.string()).describe("List of specific actions to implement the change"),estimatedImpact:p.enum(["low","medium","high"]).describe("Estimated impact of implementing this change"),riskLevel:p.enum(["low","medium","high"]).describe("Risk level associated with this change"),diff:p.string().optional().describe("Optional diff or code snippet showing the change")});class x{model;config;systemPrompt;modelSelector;selectionContext;constructor(e){this.model=e.model,this.modelSelector=e.modelSelector,this.selectionContext=e.selectionContext,this.config=e.evolutionConfig??{},this.systemPrompt=e.systemPrompt??`You are a ContractSpec evolution expert. Your role is to analyze telemetry data, anomalies, and usage patterns to suggest improvements to API contracts and specifications.
781
5
 
782
6
  When generating suggestions:
783
7
  1. Be specific and actionable
784
8
  2. Consider backwards compatibility
785
9
  3. Prioritize stability and reliability
786
10
  4. Explain the rationale clearly
787
- 5. Estimate impact and risk accurately`;
788
- }
789
- async resolveModel() {
790
- if (this.modelSelector) {
791
- const ctx = this.selectionContext ?? {
792
- taskDimension: "reasoning"
793
- };
794
- const { model } = await this.modelSelector.selectAndCreate(ctx);
795
- return model;
796
- }
797
- return this.model;
798
- }
799
- async generateFromIntent(intent, options = {}) {
800
- const prompt = this.buildPrompt(intent, options);
801
- const model = await this.resolveModel();
802
- const { output } = await generateText({
803
- model,
804
- system: this.systemPrompt,
805
- prompt,
806
- output: Output.object({
807
- schema: SpecSuggestionProposalSchema
808
- })
809
- });
810
- return this.buildSuggestion(intent, output);
811
- }
812
- async generateBatch(intents, options = {}) {
813
- const maxConcurrent = options.maxConcurrent ?? 3;
814
- const results = [];
815
- for (let i = 0;i < intents.length; i += maxConcurrent) {
816
- const batch = intents.slice(i, i + maxConcurrent);
817
- const batchResults = await Promise.all(batch.map((intent) => this.generateFromIntent(intent)));
818
- results.push(...batchResults);
819
- }
820
- return results;
821
- }
822
- async enhanceSuggestion(suggestion) {
823
- const prompt = `Review and enhance this spec suggestion:
11
+ 5. Estimate impact and risk accurately`}async resolveModel(){if(this.modelSelector){let e=this.selectionContext??{taskDimension:"reasoning"},{model:r}=await this.modelSelector.selectAndCreate(e);return r}return this.model}async generateFromIntent(e,r={}){let n=this.buildPrompt(e,r),t=await this.resolveModel(),{output:o}=await D({model:t,system:this.systemPrompt,prompt:n,output:I.object({schema:l})});return this.buildSuggestion(e,o)}async generateBatch(e,r={}){let n=r.maxConcurrent??3,t=[];for(let o=0;o<e.length;o+=n){let c=e.slice(o,o+n),i=await Promise.all(c.map((d)=>this.generateFromIntent(d)));t.push(...i)}return t}async enhanceSuggestion(e){let r=`Review and enhance this spec suggestion:
824
12
 
825
- Intent: ${suggestion.intent.type} - ${suggestion.intent.description}
826
- Current Summary: ${suggestion.proposal.summary}
827
- Current Rationale: ${suggestion.proposal.rationale}
13
+ Intent: ${e.intent.type} - ${e.intent.description}
14
+ Current Summary: ${e.proposal.summary}
15
+ Current Rationale: ${e.proposal.rationale}
828
16
 
829
17
  Evidence:
830
- ${suggestion.evidence.map((e) => `- ${e.type}: ${e.description}`).join(`
18
+ ${e.evidence.map((o)=>`- ${o.type}: ${o.description}`).join(`
831
19
  `)}
832
20
 
833
- Please provide an improved version with more specific recommendations.`;
834
- const model = await this.resolveModel();
835
- const { output } = await generateText({
836
- model,
837
- system: this.systemPrompt,
838
- prompt,
839
- output: Output.object({
840
- schema: SpecSuggestionProposalSchema
841
- })
842
- });
843
- return {
844
- ...suggestion,
845
- proposal: {
846
- ...suggestion.proposal,
847
- summary: output.summary,
848
- rationale: output.rationale,
849
- changeType: output.changeType,
850
- diff: output.diff,
851
- metadata: {
852
- ...suggestion.proposal.metadata,
853
- aiEnhanced: true,
854
- recommendedActions: output.recommendedActions,
855
- estimatedImpact: output.estimatedImpact,
856
- riskLevel: output.riskLevel
857
- }
858
- }
859
- };
860
- }
861
- buildPrompt(intent, options) {
862
- const parts = [
863
- `Analyze this intent pattern and generate a spec suggestion:`,
864
- ``,
865
- `Intent Type: ${intent.type}`,
866
- `Description: ${intent.description}`,
867
- `Confidence: ${(intent.confidence.score * 100).toFixed(0)}% (sample size: ${intent.confidence.sampleSize})`
868
- ];
869
- if (intent.operation) {
870
- parts.push(`Operation: ${intent.operation.key} v${intent.operation.version}`);
871
- }
872
- if (intent.evidence.length > 0) {
873
- parts.push(``, `Evidence:`);
874
- for (const evidence of intent.evidence) {
875
- parts.push(`- [${evidence.type}] ${evidence.description}`);
876
- }
877
- }
878
- if (intent.metadata) {
879
- parts.push(``, `Metadata: ${JSON.stringify(intent.metadata, null, 2)}`);
880
- }
881
- if (options.existingSpec) {
882
- parts.push(``, `Existing Spec:`, "```json", JSON.stringify(options.existingSpec, null, 2), "```");
883
- }
884
- if (options.additionalContext) {
885
- parts.push(``, `Additional Context:`, options.additionalContext);
886
- }
887
- return parts.join(`
888
- `);
889
- }
890
- buildSuggestion(intent, aiOutput) {
891
- const now = new Date;
892
- const proposal = {
893
- summary: aiOutput.summary,
894
- rationale: aiOutput.rationale,
895
- changeType: aiOutput.changeType,
896
- diff: aiOutput.diff,
897
- metadata: {
898
- aiGenerated: true,
899
- recommendedActions: aiOutput.recommendedActions,
900
- estimatedImpact: aiOutput.estimatedImpact,
901
- riskLevel: aiOutput.riskLevel
902
- }
903
- };
904
- return {
905
- id: randomUUID2(),
906
- intent,
907
- target: intent.operation,
908
- proposal,
909
- confidence: intent.confidence.score,
910
- priority: this.calculatePriority(intent, aiOutput),
911
- createdAt: now,
912
- createdBy: "ai-spec-generator",
913
- status: this.determineInitialStatus(intent),
914
- evidence: intent.evidence,
915
- tags: ["ai-generated", intent.type]
916
- };
917
- }
918
- calculatePriority(intent, aiOutput) {
919
- const impactScore = aiOutput.estimatedImpact === "high" ? 1 : aiOutput.estimatedImpact === "medium" ? 0.5 : 0.25;
920
- const intentScore = intent.confidence.score;
921
- const urgency = intent.type === "error-spike" ? 0.3 : intent.type === "latency-regression" ? 0.2 : 0;
922
- const combined = impactScore * 0.4 + intentScore * 0.4 + urgency;
923
- if (combined >= 0.7)
924
- return "high";
925
- if (combined >= 0.4)
926
- return "medium";
927
- return "low";
928
- }
929
- determineInitialStatus(intent) {
930
- if (this.config.autoApproveThreshold && intent.confidence.score >= this.config.autoApproveThreshold && !this.config.requireApproval) {
931
- return "approved";
932
- }
933
- return "pending";
934
- }
935
- }
936
- function createAISpecGenerator(config) {
937
- return new AISpecGenerator(config);
938
- }
939
- // src/generator/spec-generator.ts
940
- import { randomUUID as randomUUID3 } from "crypto";
941
-
942
- class SpecGenerator {
943
- config;
944
- logger;
945
- clock;
946
- getSpec;
947
- constructor(options = {}) {
948
- this.config = options.config ?? {};
949
- this.logger = options.logger;
950
- this.clock = options.clock ?? (() => new Date);
951
- this.getSpec = options.getSpec;
952
- }
953
- generateFromIntent(intent, options = {}) {
954
- const now = this.clock();
955
- const summary = options.summary ?? `${this.intentToVerb(intent.type)} ${intent.operation?.key ?? "operation"}`;
956
- const rationale = options.rationale ?? [
957
- intent.description,
958
- intent.metadata?.observedValue ? `Observed ${intent.metadata.observedValue}` : undefined
959
- ].filter(Boolean).join(" — ");
960
- const suggestion = {
961
- id: randomUUID3(),
962
- intent,
963
- target: intent.operation,
964
- proposal: {
965
- summary,
966
- rationale,
967
- changeType: options.changeType ?? this.inferChangeType(intent),
968
- kind: options.kind,
969
- spec: options.spec,
970
- diff: options.diff,
971
- metadata: options.metadata
972
- },
973
- confidence: intent.confidence.score,
974
- priority: this.intentToPriority(intent),
975
- createdAt: now,
976
- createdBy: options.createdBy ?? "auto-evolution",
977
- status: options.status ?? "pending",
978
- evidence: intent.evidence,
979
- tags: options.tags
980
- };
981
- return suggestion;
982
- }
983
- generateVariant(operation, patch, intent, options = {}) {
984
- if (!this.getSpec) {
985
- throw new Error("SpecGenerator requires getSpec() to generate variants");
986
- }
987
- const base = this.getSpec(operation.key, operation.version);
988
- if (!base) {
989
- throw new Error(`Cannot generate variant; spec ${operation.key}.v${operation.version} not found`);
990
- }
991
- const merged = mergeContract(base, patch);
992
- return this.generateFromIntent(intent, { ...options, spec: merged });
993
- }
994
- validateSuggestion(suggestion, config = this.config) {
995
- const reasons = [];
996
- if (config.minConfidence != null && suggestion.confidence < config.minConfidence) {
997
- reasons.push(`Confidence ${suggestion.confidence.toFixed(2)} below minimum ${config.minConfidence}`);
998
- }
999
- if (config.requireApproval && suggestion.status === "approved") {
1000
- reasons.push("Suggestion cannot be auto-approved when approval is required");
1001
- }
1002
- if (suggestion.proposal.spec && !suggestion.proposal.spec.meta?.key) {
1003
- reasons.push("Proposal spec must include meta.key");
1004
- }
1005
- if (!suggestion.proposal.summary) {
1006
- reasons.push("Proposal summary is required");
1007
- }
1008
- const ok = reasons.length === 0;
1009
- if (!ok) {
1010
- this.logger?.warn("SpecGenerator.validateSuggestion.failed", {
1011
- suggestionId: suggestion.id,
1012
- reasons
1013
- });
1014
- }
1015
- return { ok, reasons };
1016
- }
1017
- intentToVerb(intent) {
1018
- switch (intent) {
1019
- case "error-spike":
1020
- return "Stabilize";
1021
- case "latency-regression":
1022
- return "Optimize";
1023
- case "missing-operation":
1024
- return "Introduce";
1025
- case "throughput-drop":
1026
- return "Rebalance";
1027
- default:
1028
- return "Adjust";
1029
- }
1030
- }
1031
- intentToPriority(intent) {
1032
- const severity = intent.confidence.score;
1033
- if (intent.type === "error-spike" || severity >= 0.8)
1034
- return "high";
1035
- if (severity >= 0.5)
1036
- return "medium";
1037
- return "low";
1038
- }
1039
- inferChangeType(intent) {
1040
- switch (intent.type) {
1041
- case "missing-operation":
1042
- return "new-spec";
1043
- case "schema-mismatch":
1044
- return "schema-update";
1045
- case "error-spike":
1046
- return "policy-update";
1047
- default:
1048
- return "revision";
1049
- }
1050
- }
1051
- }
1052
- function mergeContract(base, patch) {
1053
- return {
1054
- ...base,
1055
- ...patch,
1056
- meta: { ...base.meta, ...patch.meta },
1057
- io: {
1058
- ...base.io,
1059
- ...patch.io
1060
- },
1061
- policy: {
1062
- ...base.policy,
1063
- ...patch.policy
1064
- },
1065
- telemetry: {
1066
- ...base.telemetry,
1067
- ...patch.telemetry
1068
- },
1069
- sideEffects: {
1070
- ...base.sideEffects,
1071
- ...patch.sideEffects
1072
- }
1073
- };
1074
- }
1075
- export {
1076
- createAISpecGenerator,
1077
- SpecSuggestionOrchestrator,
1078
- SpecGenerator,
1079
- SpecAnalyzer,
1080
- PosthogTelemetryReader,
1081
- InMemorySpecSuggestionRepository,
1082
- FileSystemSuggestionWriter,
1083
- AISpecGenerator
1084
- };
21
+ Please provide an improved version with more specific recommendations.`,n=await this.resolveModel(),{output:t}=await D({model:n,system:this.systemPrompt,prompt:r,output:I.object({schema:l})});return{...e,proposal:{...e.proposal,summary:t.summary,rationale:t.rationale,changeType:t.changeType,diff:t.diff,metadata:{...e.proposal.metadata,aiEnhanced:!0,recommendedActions:t.recommendedActions,estimatedImpact:t.estimatedImpact,riskLevel:t.riskLevel}}}}buildPrompt(e,r){let n=["Analyze this intent pattern and generate a spec suggestion:","",`Intent Type: ${e.type}`,`Description: ${e.description}`,`Confidence: ${(e.confidence.score*100).toFixed(0)}% (sample size: ${e.confidence.sampleSize})`];if(e.operation)n.push(`Operation: ${e.operation.key} v${e.operation.version}`);if(e.evidence.length>0){n.push("","Evidence:");for(let t of e.evidence)n.push(`- [${t.type}] ${t.description}`)}if(e.metadata)n.push("",`Metadata: ${JSON.stringify(e.metadata,null,2)}`);if(r.existingSpec)n.push("","Existing Spec:","```json",JSON.stringify(r.existingSpec,null,2),"```");if(r.additionalContext)n.push("","Additional Context:",r.additionalContext);return n.join(`
22
+ `)}buildSuggestion(e,r){let n=new Date,t={summary:r.summary,rationale:r.rationale,changeType:r.changeType,diff:r.diff,metadata:{aiGenerated:!0,recommendedActions:r.recommendedActions,estimatedImpact:r.estimatedImpact,riskLevel:r.riskLevel}};return{id:Z(),intent:e,target:e.operation,proposal:t,confidence:e.confidence.score,priority:this.calculatePriority(e,r),createdAt:n,createdBy:"ai-spec-generator",status:this.determineInitialStatus(e),evidence:e.evidence,tags:["ai-generated",e.type]}}calculatePriority(e,r){let n=r.estimatedImpact==="high"?1:r.estimatedImpact==="medium"?0.5:0.25,t=e.confidence.score,o=e.type==="error-spike"?0.3:e.type==="latency-regression"?0.2:0,c=n*0.4+t*0.4+o;if(c>=0.7)return"high";if(c>=0.4)return"medium";return"low"}determineInitialStatus(e){if(this.config.autoApproveThreshold&&e.confidence.score>=this.config.autoApproveThreshold&&!this.config.requireApproval)return"approved";return"pending"}}function pe(e){return new x(e)}import{randomUUID as F}from"crypto";class U{config;logger;clock;getSpec;constructor(e={}){this.config=e.config??{},this.logger=e.logger,this.clock=e.clock??(()=>new Date),this.getSpec=e.getSpec}generateFromIntent(e,r={}){let n=this.clock(),t=r.summary??`${this.intentToVerb(e.type)} ${e.operation?.key??"operation"}`,o=r.rationale??[e.description,e.metadata?.observedValue?`Observed ${e.metadata.observedValue}`:void 0].filter(Boolean).join(" — ");return{id:F(),intent:e,target:e.operation,proposal:{summary:t,rationale:o,changeType:r.changeType??this.inferChangeType(e),kind:r.kind,spec:r.spec,diff:r.diff,metadata:r.metadata},confidence:e.confidence.score,priority:this.intentToPriority(e),createdAt:n,createdBy:r.createdBy??"auto-evolution",status:r.status??"pending",evidence:e.evidence,tags:r.tags}}generateVariant(e,r,n,t={}){if(!this.getSpec)throw Error("SpecGenerator requires getSpec() to generate variants");let o=this.getSpec(e.key,e.version);if(!o)throw Error(`Cannot generate variant; spec ${e.key}.v${e.version} not found`);let c=Y(o,r);return this.generateFromIntent(n,{...t,spec:c})}validateSuggestion(e,r=this.config){let n=[];if(r.minConfidence!=null&&e.confidence<r.minConfidence)n.push(`Confidence ${e.confidence.toFixed(2)} below minimum ${r.minConfidence}`);if(r.requireApproval&&e.status==="approved")n.push("Suggestion cannot be auto-approved when approval is required");if(e.proposal.spec&&!e.proposal.spec.meta?.key)n.push("Proposal spec must include meta.key");if(!e.proposal.summary)n.push("Proposal summary is required");let t=n.length===0;if(!t)this.logger?.warn("SpecGenerator.validateSuggestion.failed",{suggestionId:e.id,reasons:n});return{ok:t,reasons:n}}intentToVerb(e){switch(e){case"error-spike":return"Stabilize";case"latency-regression":return"Optimize";case"missing-operation":return"Introduce";case"throughput-drop":return"Rebalance";default:return"Adjust"}}intentToPriority(e){let r=e.confidence.score;if(e.type==="error-spike"||r>=0.8)return"high";if(r>=0.5)return"medium";return"low"}inferChangeType(e){switch(e.type){case"missing-operation":return"new-spec";case"schema-mismatch":return"schema-update";case"error-spike":return"policy-update";default:return"revision"}}}function Y(e,r){return{...e,...r,meta:{...e.meta,...r.meta},io:{...e.io,...r.io},policy:{...e.policy,...r.policy},telemetry:{...e.telemetry,...r.telemetry},sideEffects:{...e.sideEffects,...r.sideEffects}}}export{pe as createAISpecGenerator,W as SpecSuggestionOrchestrator,U as SpecGenerator,j as SpecAnalyzer,L as PosthogTelemetryReader,J as InMemorySpecSuggestionRepository,z as FileSystemSuggestionWriter,x as AISpecGenerator};