@prism-d1/cli 1.0.77 → 1.0.80

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 (43) hide show
  1. package/dist/assets/github-workflows/prism-ai-metrics.yml +83 -122
  2. package/dist/assets/github-workflows/prism-eval-gate-kiro.yml +66 -0
  3. package/dist/assets/github-workflows/prism-eval-gate.yml +1 -1
  4. package/dist/assets/gitlab-workflows/.gitlab-ci.yml +0 -3
  5. package/dist/assets/gitlab-workflows/prism-ai-metrics.yml +53 -56
  6. package/dist/assets/infra/bin/app.ts +2 -0
  7. package/dist/assets/infra/lib/constructs/otel-collector-construct.ts +62 -0
  8. package/dist/assets/infra/lib/dashboard-stack.ts +541 -1144
  9. package/dist/assets/infra/lib/lambda/attribution-metrics-publisher.ts +248 -0
  10. package/dist/assets/infra/lib/lambda/event-schema.ts +144 -0
  11. package/dist/assets/infra/lib/lambda/metrics-processor.ts +124 -36
  12. package/dist/assets/infra/lib/lambda/otel-receiver.ts +927 -16
  13. package/dist/assets/infra/lib/lambda/productivity-widget.ts +203 -0
  14. package/dist/assets/infra/lib/lambda/security-remediation-tracker.ts +17 -2
  15. package/dist/assets/infra/lib/lambda/velocity-widget.ts +1483 -0
  16. package/dist/assets/infra/lib/metrics-pipeline-stack.ts +191 -119
  17. package/dist/assets/infra/package.json +2 -1
  18. package/dist/assets/infra/tsconfig.json +9 -2
  19. package/dist/src/commands/bootstrapper/install-eval-harness.d.ts +8 -2
  20. package/dist/src/commands/bootstrapper/install-eval-harness.d.ts.map +1 -1
  21. package/dist/src/commands/bootstrapper/install-eval-harness.js +32 -32
  22. package/dist/src/commands/bootstrapper/install-eval-harness.js.map +1 -1
  23. package/dist/src/commands/bootstrapper/install-github-workflows.d.ts.map +1 -1
  24. package/dist/src/commands/bootstrapper/install-github-workflows.js +7 -2
  25. package/dist/src/commands/bootstrapper/install-github-workflows.js.map +1 -1
  26. package/dist/src/commands/bootstrapper/install-gitlab-workflows.d.ts.map +1 -1
  27. package/dist/src/commands/bootstrapper/install-gitlab-workflows.js +7 -1
  28. package/dist/src/commands/bootstrapper/install-gitlab-workflows.js.map +1 -1
  29. package/dist/src/commands/bootstrapper/setup-otel-sync.d.ts.map +1 -1
  30. package/dist/src/commands/bootstrapper/setup-otel-sync.js +3 -2
  31. package/dist/src/commands/bootstrapper/setup-otel-sync.js.map +1 -1
  32. package/dist/src/utils/region.d.ts +50 -0
  33. package/dist/src/utils/region.d.ts.map +1 -0
  34. package/dist/src/utils/region.js +62 -0
  35. package/dist/src/utils/region.js.map +1 -0
  36. package/package.json +2 -2
  37. package/dist/assets/eval-harness/README.md +0 -144
  38. package/dist/assets/github-workflows/README.md +0 -110
  39. package/dist/assets/github-workflows/prism-dora-weekly.yml +0 -334
  40. package/dist/assets/gitlab-workflows/prism-dora-weekly.yml +0 -181
  41. package/dist/assets/infra/lib/lambda/defect-correlator.ts +0 -142
  42. package/dist/assets/infra/lib/lambda/spec-to-code-calculator.ts +0 -123
  43. package/dist/assets/metric-hooks/README.md +0 -107
@@ -6,8 +6,15 @@ import { NagSuppressions } from 'cdk-nag';
6
6
  const METRIC_NAMESPACE = 'PRISM/D1/Velocity';
7
7
  const DEFAULT_PERIOD = cdk.Duration.hours(1);
8
8
 
9
+ export interface DashboardStackProps extends cdk.StackProps {
10
+ /** ARN of the productivity custom-widget Lambda (from the OTEL collector construct). */
11
+ readonly productivityWidgetArn?: string;
12
+ /** ARN of the Team Velocity custom-widget Lambda (DDB-backed panels). */
13
+ readonly velocityWidgetArn?: string;
14
+ }
15
+
9
16
  export class DashboardStack extends cdk.Stack {
10
- constructor(scope: Construct, id: string, props?: cdk.StackProps) {
17
+ constructor(scope: Construct, id: string, props?: DashboardStackProps) {
11
18
  super(scope, id, props);
12
19
 
13
20
  // cdk-nag: CloudWatch alarms in this stack are for observability dashboards.
@@ -31,1140 +38,433 @@ export class DashboardStack extends cdk.Stack {
31
38
  defaultInterval: cdk.Duration.days(7),
32
39
  });
33
40
 
34
- // --- AI Acceptance Rate (time series) ---
35
- const aiAcceptanceMetric = new cloudwatch.Metric({
36
- namespace: METRIC_NAMESPACE,
37
- metricName: 'AIAcceptanceRate',
38
- statistic: 'Average',
39
- period: DEFAULT_PERIOD,
40
- label: 'AI Acceptance Rate (%)',
41
- });
42
-
43
- teamDashboard.addWidgets(
44
- new cloudwatch.TextWidget({
45
- markdown: '# PRISM D1 - Team Velocity Dashboard\nReal-time AI-enhanced DORA metrics for engineering teams.',
41
+ // --- Hybrid layout ---
42
+ // KPI and detail panels are DDB-backed custom widgets (velocity-widget
43
+ // Lambda) reading the events table via the by-detail-type GSI: full
44
+ // 365-day history, real event timestamps, no CloudWatch metric-dimension
45
+ // matching, and honest empty states naming the missing emitter. Native
46
+ // graphs are retained only for attribution/OTEL-fed series, which are
47
+ // dimensionless and verified live. Access to panel data is gated by
48
+ // lambda:InvokeFunction on the widget Lambda (viewer's IAM creds).
49
+ const velocityPanel = (title: string, view: string, height: number): cloudwatch.CustomWidget =>
50
+ new cloudwatch.CustomWidget({
51
+ functionArn: props?.velocityWidgetArn ?? '',
52
+ title,
46
53
  width: 24,
47
- height: 1,
48
- }),
49
- );
50
-
51
- teamDashboard.addWidgets(
52
- new cloudwatch.GraphWidget({
53
- title: 'AI Acceptance Rate',
54
- left: [aiAcceptanceMetric],
55
- width: 12,
56
- height: 6,
57
- leftYAxis: { min: 0, max: 100, label: 'Percent' },
58
- statistic: 'Average',
59
- period: DEFAULT_PERIOD,
60
- }),
61
- new cloudwatch.GraphWidget({
62
- title: 'Deployment Frequency',
63
- left: [
64
- new cloudwatch.Metric({
65
- namespace: METRIC_NAMESPACE,
66
- metricName: 'DeploymentFrequency',
67
- statistic: 'Sum',
68
- period: cdk.Duration.days(1),
69
- label: 'Deploys / Day',
70
- }),
71
- ],
72
- width: 12,
73
- height: 6,
74
- view: cloudwatch.GraphWidgetView.BAR,
75
- leftYAxis: { min: 0, label: 'Deployments' },
76
- }),
77
- );
78
-
79
- teamDashboard.addWidgets(
80
- new cloudwatch.GraphWidget({
81
- title: 'Lead Time for Changes',
82
- left: [
83
- new cloudwatch.Metric({
84
- namespace: METRIC_NAMESPACE,
85
- metricName: 'LeadTimeForChanges',
86
- statistic: 'Average',
87
- period: DEFAULT_PERIOD,
88
- label: 'Lead Time (seconds)',
89
- }),
90
- ],
91
- width: 8,
92
- height: 6,
93
- leftYAxis: { min: 0, label: 'Seconds' },
94
- }),
95
- new cloudwatch.GaugeWidget({
96
- title: 'Eval Gate Pass Rate',
97
- metrics: [
98
- new cloudwatch.Metric({
99
- namespace: METRIC_NAMESPACE,
100
- metricName: 'EvalGatePassRate',
101
- statistic: 'Average',
102
- period: cdk.Duration.days(1),
103
- label: 'Eval Pass Rate (%)',
104
- }),
105
- ],
106
- width: 8,
107
- height: 6,
108
- leftYAxis: { min: 0, max: 100 },
109
- }),
110
- new cloudwatch.GraphWidget({
111
- title: 'AI Test Coverage Delta',
112
- left: [
113
- new cloudwatch.Metric({
114
- namespace: METRIC_NAMESPACE,
115
- metricName: 'AITestCoverageDelta',
116
- statistic: 'Average',
117
- period: DEFAULT_PERIOD,
118
- label: 'Coverage Delta (%)',
119
- }),
120
- ],
121
- width: 8,
122
- height: 6,
123
- leftYAxis: { label: 'Percent' },
124
- }),
125
- );
126
-
127
- // --- Additional team metrics row ---
128
- teamDashboard.addWidgets(
129
- new cloudwatch.GraphWidget({
130
- title: 'Change Failure Rate',
131
- left: [
132
- new cloudwatch.Metric({
133
- namespace: METRIC_NAMESPACE,
134
- metricName: 'ChangeFailureRate',
135
- statistic: 'Average',
136
- period: DEFAULT_PERIOD,
137
- label: 'Change Failure Rate (%)',
138
- }),
139
- ],
140
- width: 8,
141
- height: 6,
142
- leftYAxis: { min: 0, max: 100, label: 'Percent' },
143
- }),
144
- new cloudwatch.GraphWidget({
145
- title: 'Mean Time to Recovery',
146
- left: [
147
- new cloudwatch.Metric({
148
- namespace: METRIC_NAMESPACE,
149
- metricName: 'MTTR',
150
- statistic: 'Average',
151
- period: DEFAULT_PERIOD,
152
- label: 'MTTR (seconds)',
153
- }),
154
- ],
155
- width: 8,
156
- height: 6,
157
- leftYAxis: { min: 0, label: 'Seconds' },
158
- }),
159
- new cloudwatch.GraphWidget({
160
- title: 'AI to Merge Ratio',
161
- left: [
162
- new cloudwatch.Metric({
163
- namespace: METRIC_NAMESPACE,
164
- metricName: 'AIToMergeRatio',
165
- statistic: 'Average',
166
- period: DEFAULT_PERIOD,
167
- label: 'AI-to-Merge Ratio (%)',
168
- }),
169
- ],
170
- width: 8,
171
- height: 6,
172
- leftYAxis: { min: 0, max: 100, label: 'Percent' },
173
- }),
174
- );
175
-
176
- // --- Agent Metrics Row ---
177
- const teamId = 'ALL';
178
- const repository = 'ALL';
54
+ height,
55
+ params: { view },
56
+ updateOnRefresh: true,
57
+ updateOnResize: false,
58
+ updateOnTimeRangeChange: true,
59
+ });
179
60
 
180
61
  teamDashboard.addWidgets(
181
62
  new cloudwatch.TextWidget({
182
- markdown: '### AI Token Usage & Cost',
63
+ markdown:
64
+ '# PRISM D1 - Team Velocity Dashboard\n' +
65
+ 'Delivery health: DORA proxies, AI-DORA quality, eval gates, governance, security. ' +
66
+ 'Spend and per-developer output live on **PRISM-D1-Developer-Productivity**.',
183
67
  width: 24,
184
- height: 1,
185
- }),
186
- );
187
-
188
- teamDashboard.addWidgets(
189
- new cloudwatch.GraphWidget({
190
- title: 'AI Input Tokens',
191
- left: [
192
- new cloudwatch.Metric({
193
- namespace: METRIC_NAMESPACE,
194
- metricName: 'AIInputTokens',
195
- statistic: 'Sum',
196
- period: cdk.Duration.days(1),
197
- label: 'Input Tokens',
198
- }),
199
- ],
200
- width: 8,
201
- height: 6,
202
- leftYAxis: { min: 0, label: 'Tokens' },
203
- }),
204
- new cloudwatch.GraphWidget({
205
- title: 'AI Output Tokens',
206
- left: [
207
- new cloudwatch.Metric({
208
- namespace: METRIC_NAMESPACE,
209
- metricName: 'AIOutputTokens',
210
- statistic: 'Sum',
211
- period: cdk.Duration.days(1),
212
- label: 'Output Tokens',
213
- }),
214
- ],
215
- width: 8,
216
- height: 6,
217
- leftYAxis: { min: 0, label: 'Tokens' },
218
- }),
219
- new cloudwatch.GraphWidget({
220
- title: 'AI Cost (USD)',
221
- left: [
222
- new cloudwatch.Metric({
223
- namespace: METRIC_NAMESPACE,
224
- metricName: 'AICostUSD',
225
- statistic: 'Sum',
226
- period: cdk.Duration.days(1),
227
- label: 'Cost (USD)',
228
- }),
229
- ],
230
- width: 8,
231
- height: 6,
232
- leftYAxis: { min: 0, label: 'USD' },
68
+ height: 2,
233
69
  }),
234
70
  );
235
71
 
236
- // Cost breakdowns by Tool and Model. SEARCH expressions auto-discover
237
- // every dimension value, so new tools/models appear without dashboard
238
- // changes. These series are populated by the otel-metrics-publisher
239
- // (deployed by default; skip with -c skipOtelCollector=true); the trailer
240
- // path has no per-tool/per-model breakdown and leaves these widgets empty.
241
- teamDashboard.addWidgets(
242
- new cloudwatch.GraphWidget({
243
- title: 'AI Cost by Tool (USD)',
244
- left: [
245
- new cloudwatch.MathExpression({
246
- expression: `SEARCH('{${METRIC_NAMESPACE},Tool} MetricName="AICostUSD"', 'Sum')`,
247
- usingMetrics: {},
248
- // Dynamic label: each discovered series is legend-labeled with
249
- // its Tool dimension value (e.g. "claude-code").
250
- label: "${PROP('Dim.Tool')}",
251
- period: cdk.Duration.days(1),
252
- }),
253
- ],
254
- width: 12,
255
- height: 6,
256
- leftYAxis: { min: 0, label: 'USD' },
257
- legendPosition: cloudwatch.LegendPosition.RIGHT,
258
- }),
259
- new cloudwatch.GraphWidget({
260
- title: 'AI Cost by Model (USD)',
261
- left: [
262
- new cloudwatch.MathExpression({
263
- expression: `SEARCH('{${METRIC_NAMESPACE},Model} MetricName="AICostUSD"', 'Sum')`,
264
- usingMetrics: {},
265
- // Dynamic label: each discovered series is legend-labeled with
266
- // its Model dimension value (e.g. "claude-sonnet-4-5-20250929").
267
- label: "${PROP('Dim.Model')}",
268
- period: cdk.Duration.days(1),
269
- }),
270
- ],
271
- width: 12,
272
- height: 6,
273
- leftYAxis: { min: 0, label: 'USD' },
274
- legendPosition: cloudwatch.LegendPosition.RIGHT,
275
- }),
276
- );
72
+ if (props?.velocityWidgetArn) {
73
+ // Row 1: DORA proxy KPIs (merge frequency, PR cycle time, revert rate/turnaround)
74
+ teamDashboard.addWidgets(velocityPanel('Delivery KPIs (DORA proxies)', 'dora', 4));
75
+ // Row 2: AI-DORA KPIs (attribution store + eval events, L2/L4 coloring)
76
+ teamDashboard.addWidgets(velocityPanel('AI-DORA KPIs', 'aidora', 4));
77
+ }
277
78
 
278
- teamDashboard.addWidgets(
279
- new cloudwatch.TextWidget({
280
- markdown: '### Agent Operations',
281
- width: 24,
282
- height: 1,
283
- }),
284
- );
79
+ // Row 3: native graphs — attribution/OTEL-fed series only. These are the
80
+ // dimensionless series published by the attribution/OTEL publishers, so
81
+ // plain (dimensionless) metric queries match. FILL/IF guards throughout:
82
+ // commit data is sparse and an unguarded ratio renders NaN on empty
83
+ // buckets; FILL(..., REPEAT) carries the last value through gap days so
84
+ // the series draws a line instead of invisible isolated dots.
85
+ const dailySum = (metricName: string, label?: string): cloudwatch.Metric =>
86
+ new cloudwatch.Metric({
87
+ namespace: METRIC_NAMESPACE,
88
+ metricName,
89
+ statistic: 'Sum',
90
+ period: cdk.Duration.days(1),
91
+ ...(label ? { label } : {}),
92
+ });
285
93
 
286
94
  teamDashboard.addWidgets(
287
95
  new cloudwatch.GraphWidget({
288
- title: 'Agent Invocations',
289
- left: [
290
- new cloudwatch.Metric({
291
- namespace: METRIC_NAMESPACE,
292
- metricName: 'AgentInvocationCount',
293
- dimensionsMap: { TeamId: teamId, Repository: repository },
294
- statistic: 'Sum',
295
- period: DEFAULT_PERIOD,
296
- label: 'Invocations',
297
- }),
298
- ],
299
- width: 8,
300
- height: 6,
301
- }),
302
- new cloudwatch.GraphWidget({
303
- title: 'Agent Success Rate',
304
- left: [
305
- new cloudwatch.Metric({
306
- namespace: METRIC_NAMESPACE,
307
- metricName: 'AgentSuccessRate',
308
- dimensionsMap: { TeamId: teamId, Repository: repository },
309
- statistic: 'Average',
310
- period: DEFAULT_PERIOD,
311
- label: 'Success Rate (%)',
312
- }),
313
- ],
314
- width: 8,
315
- height: 6,
316
- leftYAxis: { min: 0, max: 100, label: 'Percent' },
317
- }),
318
- new cloudwatch.GraphWidget({
319
- title: 'Agent Avg Duration',
96
+ title: 'Commits / Day (AI vs Human)',
320
97
  left: [
321
- new cloudwatch.Metric({
322
- namespace: METRIC_NAMESPACE,
323
- metricName: 'AgentDurationMs',
324
- dimensionsMap: { TeamId: teamId, Repository: repository },
325
- statistic: 'Average',
326
- period: DEFAULT_PERIOD,
327
- label: 'Duration (ms)',
328
- }),
98
+ dailySum('AICommits', 'AI commits'),
99
+ dailySum('HumanCommits', 'Human commits'),
329
100
  ],
101
+ view: cloudwatch.GraphWidgetView.BAR,
102
+ stacked: true,
330
103
  width: 8,
331
104
  height: 6,
105
+ leftYAxis: { min: 0, label: 'Commits' },
332
106
  }),
333
- );
334
-
335
- // =======================================================
336
- // Dashboard 2: Executive Readout
337
- // =======================================================
338
- const execDashboard = new cloudwatch.Dashboard(this, 'ExecutiveReadoutDashboard', {
339
- dashboardName: 'PRISM-D1-Executive-Readout',
340
- defaultInterval: cdk.Duration.days(30),
341
- });
342
-
343
- execDashboard.addWidgets(
344
- new cloudwatch.TextWidget({
345
- markdown: '# PRISM D1 - Executive Readout\nLeadership view of AI-assisted engineering velocity and DORA performance.',
346
- width: 24,
347
- height: 1,
348
- }),
349
- );
350
-
351
- // --- PRISM Level Progress + Enhanced DORA Summary row ---
352
- execDashboard.addWidgets(
353
- new cloudwatch.SingleValueWidget({
354
- title: 'PRISM Level Progress',
355
- metrics: [
356
- new cloudwatch.Metric({
357
- namespace: METRIC_NAMESPACE,
358
- metricName: 'PRISMLevel',
359
- statistic: 'Maximum',
360
- period: cdk.Duration.days(1),
361
- label: 'Current PRISM Level',
362
- }),
363
- ],
364
- width: 6,
365
- height: 4,
366
- }),
367
- new cloudwatch.SingleValueWidget({
368
- title: 'Enhanced DORA Summary',
369
- metrics: [
370
- new cloudwatch.Metric({
371
- namespace: METRIC_NAMESPACE,
372
- metricName: 'DeploymentFrequency',
373
- statistic: 'Sum',
374
- period: cdk.Duration.days(7),
375
- label: 'Deploy Freq (7d)',
376
- }),
377
- new cloudwatch.Metric({
378
- namespace: METRIC_NAMESPACE,
379
- metricName: 'LeadTimeForChanges',
380
- statistic: 'Average',
381
- period: cdk.Duration.days(7),
382
- label: 'Avg Lead Time (s)',
383
- }),
384
- new cloudwatch.Metric({
385
- namespace: METRIC_NAMESPACE,
386
- metricName: 'ChangeFailureRate',
387
- statistic: 'Average',
388
- period: cdk.Duration.days(7),
389
- label: 'Change Fail Rate (%)',
390
- }),
391
- new cloudwatch.Metric({
392
- namespace: METRIC_NAMESPACE,
393
- metricName: 'MTTR',
394
- statistic: 'Average',
395
- period: cdk.Duration.days(7),
396
- label: 'Avg MTTR (s)',
397
- }),
398
- ],
399
- width: 12,
400
- height: 4,
401
- }),
402
- new cloudwatch.SingleValueWidget({
403
- title: 'AI Cost & Cycle Time',
404
- metrics: [
405
- new cloudwatch.Metric({
406
- namespace: METRIC_NAMESPACE,
407
- metricName: 'AICostUSD',
408
- statistic: 'Sum',
409
- period: cdk.Duration.days(7),
410
- label: 'Total AI Cost (7d, USD)',
411
- }),
412
- new cloudwatch.Metric({
413
- namespace: METRIC_NAMESPACE,
414
- metricName: 'SpecToCodeHours',
415
- statistic: 'Average',
416
- period: cdk.Duration.days(7),
417
- label: 'Avg Spec-to-Code (hrs)',
418
- }),
419
- ],
420
- width: 6,
421
- height: 4,
422
- }),
423
- );
424
-
425
- // --- AI Contribution Trend + Feature Cycle Time Trend ---
426
- execDashboard.addWidgets(
427
- new cloudwatch.GraphWidget({
428
- title: 'AI Contribution Trend',
429
- left: [
430
- new cloudwatch.Metric({
431
- namespace: METRIC_NAMESPACE,
432
- metricName: 'AIAcceptanceRate',
433
- statistic: 'Average',
434
- period: cdk.Duration.days(1),
435
- label: 'AI Acceptance Rate (%)',
436
- }),
437
- new cloudwatch.Metric({
438
- namespace: METRIC_NAMESPACE,
439
- metricName: 'AIToMergeRatio',
440
- statistic: 'Average',
441
- period: cdk.Duration.days(1),
442
- label: 'AI-to-Merge Ratio (%)',
443
- }),
444
- new cloudwatch.Metric({
445
- namespace: METRIC_NAMESPACE,
446
- metricName: 'AITestCoverageDelta',
447
- statistic: 'Average',
448
- period: cdk.Duration.days(1),
449
- label: 'AI Test Coverage Delta (%)',
450
- }),
451
- ],
452
- width: 12,
453
- height: 6,
454
- leftYAxis: { min: 0, max: 100, label: 'Percent' },
455
- }),
456
- new cloudwatch.GraphWidget({
457
- title: 'Feature Cycle Time Trend',
458
- left: [
459
- new cloudwatch.Metric({
460
- namespace: METRIC_NAMESPACE,
461
- metricName: 'SpecToCodeHours',
462
- statistic: 'Average',
463
- period: cdk.Duration.days(1),
464
- label: 'Spec-to-Code (hrs)',
465
- }),
466
- new cloudwatch.Metric({
467
- namespace: METRIC_NAMESPACE,
468
- metricName: 'LeadTimeForChanges',
469
- statistic: 'Average',
470
- period: cdk.Duration.days(1),
471
- label: 'Lead Time (seconds)',
472
- }),
473
- ],
474
- width: 12,
475
- height: 6,
476
- }),
477
- );
478
-
479
- // --- Eval gate, quality, and cost trend row ---
480
- execDashboard.addWidgets(
481
- new cloudwatch.GaugeWidget({
482
- title: 'Eval Gate Pass Rate',
483
- metrics: [
484
- new cloudwatch.Metric({
485
- namespace: METRIC_NAMESPACE,
486
- metricName: 'EvalGatePassRate',
487
- statistic: 'Average',
488
- period: cdk.Duration.days(7),
489
- label: 'Eval Pass Rate (%)',
490
- }),
491
- ],
492
- width: 6,
493
- height: 6,
494
- leftYAxis: { min: 0, max: 100 },
495
- }),
496
- new cloudwatch.GraphWidget({
497
- title: 'Post-Merge Defect Rate',
498
- left: [
499
- new cloudwatch.Metric({
500
- namespace: METRIC_NAMESPACE,
501
- metricName: 'PostMergeDefectRate',
502
- statistic: 'Average',
503
- period: cdk.Duration.days(1),
504
- label: 'Defect Rate (%)',
505
- }),
506
- ],
507
- width: 6,
508
- height: 6,
509
- leftYAxis: { min: 0, label: 'Percent' },
510
- }),
511
- new cloudwatch.GraphWidget({
512
- title: 'AI Cost Trend (Weekly)',
513
- left: [
514
- new cloudwatch.Metric({
515
- namespace: METRIC_NAMESPACE,
516
- metricName: 'AICostUSD',
517
- statistic: 'Sum',
518
- period: cdk.Duration.days(7),
519
- label: 'Total Cost (USD)',
520
- }),
521
- ],
522
- width: 6,
523
- height: 6,
524
- leftYAxis: { min: 0, label: 'USD' },
525
- }),
526
- new cloudwatch.GraphWidget({
527
- title: 'Deployment Frequency (Weekly)',
528
- left: [
529
- new cloudwatch.Metric({
530
- namespace: METRIC_NAMESPACE,
531
- metricName: 'DeploymentFrequency',
532
- statistic: 'Sum',
533
- period: cdk.Duration.days(7),
534
- label: 'Deploys / Week',
535
- }),
536
- ],
537
- width: 6,
538
- height: 6,
539
- view: cloudwatch.GraphWidgetView.BAR,
540
- leftYAxis: { min: 0, label: 'Deployments' },
541
- }),
542
- );
543
-
544
- // =======================================================
545
- // CloudWatch Alarms
546
- // =======================================================
547
-
548
- // Alarm: AI acceptance rate dropping below 20%
549
- new cloudwatch.Alarm(this, 'AiAcceptanceRateLowAlarm', {
550
- alarmName: 'PRISM-D1-AIAcceptanceRate-Low',
551
- alarmDescription: 'AI acceptance rate has dropped below 20%, indicating potential issues with AI-generated code quality or review friction.',
552
- metric: new cloudwatch.Metric({
553
- namespace: METRIC_NAMESPACE,
554
- metricName: 'AIAcceptanceRate',
555
- statistic: 'Average',
556
- period: cdk.Duration.hours(6),
557
- }),
558
- threshold: 20,
559
- evaluationPeriods: 3,
560
- datapointsToAlarm: 2,
561
- comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD,
562
- treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
563
- });
564
-
565
- // Alarm: Eval gate pass rate dropping below 70%
566
- new cloudwatch.Alarm(this, 'EvalGatePassRateLowAlarm', {
567
- alarmName: 'PRISM-D1-EvalGatePassRate-Low',
568
- alarmDescription: 'Eval gate pass rate has dropped below 70%, indicating degraded quality in AI-generated outputs.',
569
- metric: new cloudwatch.Metric({
570
- namespace: METRIC_NAMESPACE,
571
- metricName: 'EvalGatePassRate',
572
- statistic: 'Average',
573
- period: cdk.Duration.hours(6),
574
- }),
575
- threshold: 70,
576
- evaluationPeriods: 3,
577
- datapointsToAlarm: 2,
578
- comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD,
579
- treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
580
- });
581
-
582
- // Alarm: Change failure rate exceeding 20%
583
- new cloudwatch.Alarm(this, 'ChangeFailureRateHighAlarm', {
584
- alarmName: 'PRISM-D1-ChangeFailureRate-High',
585
- alarmDescription: 'Change failure rate exceeds 20%, indicating deployment quality regression.',
586
- metric: new cloudwatch.Metric({
587
- namespace: METRIC_NAMESPACE,
588
- metricName: 'ChangeFailureRate',
589
- statistic: 'Average',
590
- period: cdk.Duration.hours(6),
591
- }),
592
- threshold: 20,
593
- evaluationPeriods: 3,
594
- datapointsToAlarm: 2,
595
- comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
596
- treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
597
- });
598
-
599
- // =======================================================
600
- // Team Dashboard: Eval Gate by Rubric (Pillar 2)
601
- // =======================================================
602
- teamDashboard.addWidgets(
603
- new cloudwatch.TextWidget({
604
- markdown: '### Eval Gate Quality by Rubric',
605
- width: 24,
606
- height: 1,
607
- }),
608
- );
609
-
610
- const rubricNames = ['code-quality', 'api-response-quality', 'agent-quality', 'security-compliance', 'spec-compliance'];
611
- teamDashboard.addWidgets(
612
- new cloudwatch.GraphWidget({
613
- title: 'Eval Pass Rate by Rubric',
614
- left: rubricNames.map((rubric) =>
615
- new cloudwatch.Metric({
616
- namespace: METRIC_NAMESPACE,
617
- metricName: 'EvalGatePassRateByRubric',
618
- dimensionsMap: { RubricName: rubric },
619
- statistic: 'Average',
620
- period: cdk.Duration.days(1),
621
- label: rubric,
622
- }),
623
- ),
624
- width: 12,
625
- height: 6,
626
- leftYAxis: { min: 0, max: 100, label: 'Pass Rate (%)' },
627
- }),
628
- new cloudwatch.GraphWidget({
629
- title: 'Eval Score Trend',
630
- left: [
631
- new cloudwatch.Metric({
632
- namespace: METRIC_NAMESPACE,
633
- metricName: 'EvalScore',
634
- statistic: 'Average',
635
- period: cdk.Duration.days(1),
636
- label: 'Avg Eval Score',
637
- }),
638
- ],
639
- width: 12,
640
- height: 6,
641
- leftYAxis: { min: 0, max: 1, label: 'Score (0-1)' },
642
- }),
643
- );
644
-
645
- // =======================================================
646
- // Team Dashboard: Guardrails & Safety (Pillar 4)
647
- // =======================================================
648
- teamDashboard.addWidgets(
649
- new cloudwatch.TextWidget({
650
- markdown: '### Guardrails & Safety',
651
- width: 24,
652
- height: 1,
653
- }),
654
- );
655
-
656
- const guardrailCategories = ['CONTENT_FILTER', 'DENIED_TOPIC', 'SENSITIVE_INFO', 'WORD_FILTER'];
657
- teamDashboard.addWidgets(
658
- new cloudwatch.GraphWidget({
659
- title: 'Guardrail Triggers by Category',
660
- left: guardrailCategories.map((category) =>
661
- new cloudwatch.Metric({
662
- namespace: METRIC_NAMESPACE,
663
- metricName: 'GuardrailTriggerCount',
664
- dimensionsMap: { TriggerCategory: category },
665
- statistic: 'Sum',
666
- period: DEFAULT_PERIOD,
667
- label: category,
668
- }),
669
- ),
670
- width: 12,
671
- height: 6,
672
- leftYAxis: { min: 0, label: 'Triggers' },
673
- view: cloudwatch.GraphWidgetView.BAR,
674
- }),
675
- new cloudwatch.GraphWidget({
676
- title: 'Guardrail Actions: Block vs Anonymize',
677
- left: [
678
- new cloudwatch.Metric({
679
- namespace: METRIC_NAMESPACE,
680
- metricName: 'GuardrailBlockCount',
681
- statistic: 'Sum',
682
- period: DEFAULT_PERIOD,
683
- label: 'Blocked',
684
- }),
685
- new cloudwatch.Metric({
686
- namespace: METRIC_NAMESPACE,
687
- metricName: 'GuardrailAnonymizeCount',
688
- statistic: 'Sum',
689
- period: DEFAULT_PERIOD,
690
- label: 'Anonymized',
691
- }),
692
- ],
693
- width: 12,
694
- height: 6,
695
- leftYAxis: { min: 0, label: 'Count' },
696
- }),
697
- );
698
-
699
- // =======================================================
700
- // Team Dashboard: MCP Tool Governance (Pillar 3)
701
- // =======================================================
702
- teamDashboard.addWidgets(
703
- new cloudwatch.TextWidget({
704
- markdown: '### MCP Tool Governance',
705
- width: 24,
706
- height: 1,
707
- }),
708
- );
709
-
710
- teamDashboard.addWidgets(
711
- new cloudwatch.GraphWidget({
712
- title: 'MCP Tool Call Volume',
713
- left: [
714
- new cloudwatch.Metric({
715
- namespace: METRIC_NAMESPACE,
716
- metricName: 'MCPToolCallCount',
717
- statistic: 'Sum',
718
- period: DEFAULT_PERIOD,
719
- label: 'Tool Calls',
720
- }),
721
- ],
722
- width: 12,
723
- height: 6,
724
- leftYAxis: { min: 0, label: 'Calls' },
725
- }),
726
- new cloudwatch.GraphWidget({
727
- title: 'MCP Auth Denied Rate',
728
- left: [
729
- new cloudwatch.Metric({
730
- namespace: METRIC_NAMESPACE,
731
- metricName: 'MCPAuthDeniedCount',
732
- statistic: 'Sum',
733
- period: DEFAULT_PERIOD,
734
- label: 'Denied Calls',
735
- }),
736
- ],
737
- width: 12,
738
- height: 6,
739
- leftYAxis: { min: 0, label: 'Denied' },
740
- }),
741
- );
742
-
743
- // =======================================================
744
- // Team Dashboard: Cost Intelligence (Pillar 5)
745
- // =======================================================
746
- teamDashboard.addWidgets(
747
- new cloudwatch.TextWidget({
748
- markdown: '### Cost Intelligence',
749
- width: 24,
750
- height: 1,
751
- }),
752
- );
753
-
754
- teamDashboard.addWidgets(
755
- new cloudwatch.GraphWidget({
756
- title: 'Bedrock Cost (USD)',
757
- left: [
758
- new cloudwatch.Metric({
759
- namespace: METRIC_NAMESPACE,
760
- metricName: 'BedrockCostUSD',
761
- statistic: 'Sum',
762
- period: cdk.Duration.days(1),
763
- label: 'Daily Cost ($)',
764
- }),
765
- ],
766
- width: 12,
767
- height: 6,
768
- leftYAxis: { min: 0, label: 'USD' },
769
- }),
770
- new cloudwatch.GraphWidget({
771
- title: 'Token Efficiency',
772
- left: [
773
- new cloudwatch.Metric({
774
- namespace: METRIC_NAMESPACE,
775
- metricName: 'TokenEfficiency',
776
- statistic: 'Average',
777
- period: cdk.Duration.days(1),
778
- label: 'Tokens per Line Changed',
779
- }),
780
- ],
781
- width: 12,
782
- height: 6,
783
- leftYAxis: { min: 0, label: 'Tokens/Line' },
784
- }),
785
- );
786
-
787
- // =======================================================
788
- // Team Dashboard: AI Attribution (Pillar 7)
789
- // =======================================================
790
- teamDashboard.addWidgets(
791
- new cloudwatch.TextWidget({
792
- markdown: '### AI Attribution & Quality',
793
- width: 24,
794
- height: 1,
795
- }),
796
- );
797
-
798
- teamDashboard.addWidgets(
799
- new cloudwatch.GraphWidget({
800
- title: 'Defect Rate: AI vs Human Code',
801
- left: [
802
- new cloudwatch.Metric({
803
- namespace: METRIC_NAMESPACE,
804
- metricName: 'PostMergeDefectRateAI',
805
- statistic: 'Average',
806
- period: cdk.Duration.days(1),
807
- label: 'AI Code Defect Rate (%)',
808
- }),
809
- new cloudwatch.Metric({
810
- namespace: METRIC_NAMESPACE,
811
- metricName: 'PostMergeDefectRateHuman',
812
- statistic: 'Average',
813
- period: cdk.Duration.days(1),
814
- label: 'Human Code Defect Rate (%)',
815
- }),
816
- ],
817
- width: 12,
818
- height: 6,
819
- leftYAxis: { min: 0, label: 'Defect Rate (%)' },
820
- }),
821
- new cloudwatch.GraphWidget({
822
- title: 'Spec-to-Code Hours',
823
- left: [
824
- new cloudwatch.Metric({
825
- namespace: METRIC_NAMESPACE,
826
- metricName: 'SpecToCodeHours',
827
- statistic: 'Average',
828
- period: cdk.Duration.days(1),
829
- label: 'Avg Hours',
830
- }),
831
- ],
832
- width: 12,
833
- height: 6,
834
- leftYAxis: { min: 0, label: 'Hours' },
835
- }),
836
- );
837
-
838
- // =======================================================
839
- // Team Dashboard: Security Agent Findings
840
- // =======================================================
841
- teamDashboard.addWidgets(
842
- new cloudwatch.TextWidget({
843
- markdown: '### Security Agent Findings',
844
- width: 24,
845
- height: 1,
846
- }),
847
- );
848
-
849
- teamDashboard.addWidgets(
850
- new cloudwatch.SingleValueWidget({
851
- title: 'Open Critical/High Findings',
852
- metrics: [
853
- new cloudwatch.Metric({
854
- namespace: METRIC_NAMESPACE,
855
- metricName: 'SecurityCriticalFindingCount',
856
- statistic: 'Sum',
857
- period: cdk.Duration.days(7),
858
- label: 'Critical + High',
859
- }),
860
- ],
861
- width: 6,
862
- height: 4,
863
- }),
864
- new cloudwatch.GraphWidget({
865
- title: 'Finding Trend by Severity',
866
- left: ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'].map((severity) =>
867
- new cloudwatch.Metric({
868
- namespace: METRIC_NAMESPACE,
869
- metricName: 'SecurityFindingCount',
870
- dimensionsMap: { Severity: severity },
871
- statistic: 'Sum',
872
- period: cdk.Duration.days(1),
873
- label: severity,
874
- }),
875
- ),
876
- width: 8,
877
- height: 4,
878
- leftYAxis: { min: 0, label: 'Findings' },
879
- }),
880
- new cloudwatch.SingleValueWidget({
881
- title: 'Avg Remediation (hrs)',
882
- metrics: [
883
- new cloudwatch.Metric({
884
- namespace: METRIC_NAMESPACE,
885
- metricName: 'SecurityRemediationTimeHours',
886
- statistic: 'Average',
887
- period: cdk.Duration.days(7),
888
- label: 'Hours',
889
- }),
890
- ],
891
- width: 4,
892
- height: 4,
893
- }),
894
- );
895
-
896
- teamDashboard.addWidgets(
897
- new cloudwatch.GraphWidget({
898
- title: 'Findings: AI vs Human Code',
899
- left: [
900
- new cloudwatch.Metric({
901
- namespace: METRIC_NAMESPACE,
902
- metricName: 'SecurityFindingByOrigin',
903
- dimensionsMap: { AIOrigin: 'ai-assisted' },
904
- statistic: 'Sum',
905
- period: cdk.Duration.days(1),
906
- label: 'AI Code',
907
- }),
908
- new cloudwatch.Metric({
909
- namespace: METRIC_NAMESPACE,
910
- metricName: 'SecurityFindingByOrigin',
911
- dimensionsMap: { AIOrigin: 'human' },
912
- statistic: 'Sum',
913
- period: cdk.Duration.days(1),
914
- label: 'Human Code',
915
- }),
916
- ],
917
- width: 12,
918
- height: 6,
919
- leftYAxis: { min: 0, label: 'Findings' },
920
- }),
921
- new cloudwatch.GraphWidget({
922
- title: 'Remediation Time: AI vs Human',
923
- left: [
924
- new cloudwatch.Metric({
925
- namespace: METRIC_NAMESPACE,
926
- metricName: 'SecurityRemediationTimeHours',
927
- dimensionsMap: { AIOrigin: 'ai-assisted' },
928
- statistic: 'Average',
929
- period: cdk.Duration.days(1),
930
- label: 'AI Code Fix Time (hrs)',
931
- }),
932
- new cloudwatch.Metric({
933
- namespace: METRIC_NAMESPACE,
934
- metricName: 'SecurityRemediationTimeHours',
935
- dimensionsMap: { AIOrigin: 'human' },
936
- statistic: 'Average',
937
- period: cdk.Duration.days(1),
938
- label: 'Human Code Fix Time (hrs)',
939
- }),
940
- ],
941
- width: 12,
942
- height: 6,
943
- leftYAxis: { min: 0, label: 'Hours' },
944
- }),
945
- );
946
-
947
- teamDashboard.addWidgets(
948
- new cloudwatch.GraphWidget({
949
- title: 'Findings by Phase',
950
- left: ['design_review', 'code_review', 'pen_test'].map((phase) =>
951
- new cloudwatch.Metric({
952
- namespace: METRIC_NAMESPACE,
953
- metricName: 'SecurityFindingCount',
954
- dimensionsMap: { Phase: phase },
955
- statistic: 'Sum',
956
- period: cdk.Duration.days(7),
957
- label: phase.replace('_', ' '),
107
+ new cloudwatch.GraphWidget({
108
+ title: 'Merge Ratio: AI vs Human',
109
+ left: [
110
+ // Both lines from attribution spans. Divergence is the signal:
111
+ // AI code merging at a materially lower rate than human code means
112
+ // review friction or quality problems, not just "AI is used a lot".
113
+ new cloudwatch.MathExpression({
114
+ expression: 'FILL(IF(FILL(aiCommits, 0) > 0, 100 * FILL(mergedAi, 0) / FILL(aiCommits, 0)), REPEAT)',
115
+ usingMetrics: {
116
+ mergedAi: dailySum('MergedAICommits'),
117
+ aiCommits: dailySum('AICommits'),
118
+ },
119
+ label: 'AI merge rate (%)',
120
+ period: cdk.Duration.days(1),
958
121
  }),
959
- ),
960
- width: 12,
122
+ new cloudwatch.MathExpression({
123
+ expression: 'FILL(IF(FILL(humanCommits, 0) > 0, 100 * FILL(mergedHuman, 0) / FILL(humanCommits, 0)), REPEAT)',
124
+ usingMetrics: {
125
+ mergedHuman: dailySum('MergedHumanCommits'),
126
+ humanCommits: dailySum('HumanCommits'),
127
+ },
128
+ label: 'Human merge rate (%)',
129
+ period: cdk.Duration.days(1),
130
+ }),
131
+ ],
132
+ width: 8,
961
133
  height: 6,
962
- leftYAxis: { min: 0, label: 'Findings' },
963
- view: cloudwatch.GraphWidgetView.BAR,
134
+ leftYAxis: { min: 0, max: 100, label: 'Percent' },
964
135
  }),
965
136
  new cloudwatch.GraphWidget({
966
- title: 'Security Scan Volume',
137
+ title: 'AI Defect Trend (reverted / merged)',
967
138
  left: [
968
- new cloudwatch.Metric({
969
- namespace: METRIC_NAMESPACE,
970
- metricName: 'SecurityScanCount',
971
- statistic: 'Sum',
139
+ // RevertedAICommits may not exist at all until the first revert is
140
+ // published — a metric with zero datums is MISSING, not zero, so the
141
+ // inner FILL is required or the whole expression renders NaN.
142
+ new cloudwatch.MathExpression({
143
+ expression: 'FILL(IF(FILL(mergedAi2, 0) > 0, 100 * FILL(revertedAi, 0) / FILL(mergedAi2, 0)), REPEAT)',
144
+ usingMetrics: {
145
+ revertedAi: dailySum('RevertedAICommits'),
146
+ mergedAi2: dailySum('MergedAICommits'),
147
+ },
148
+ label: 'AI defect rate (%)',
972
149
  period: cdk.Duration.days(1),
973
- label: 'Scans / Day',
974
150
  }),
975
151
  ],
976
- width: 12,
152
+ right: [dailySum('RevertedAICommits', 'Reverted AI commits')],
153
+ width: 8,
977
154
  height: 6,
978
- leftYAxis: { min: 0, label: 'Scans' },
155
+ leftYAxis: { min: 0, label: 'Percent' },
156
+ rightYAxis: { min: 0, label: 'Count' },
157
+ }),
158
+ );
159
+
160
+ if (props?.velocityWidgetArn) {
161
+ // Row 4: per-repo drill-down — bridges org-level trends above to the
162
+ // per-concern detail panels below. Attribution store, full history.
163
+ teamDashboard.addWidgets(velocityPanel('Repository Breakdown (attribution, full history)', 'repos', 7));
164
+ // Rows 5-8: DDB-backed detail panels
165
+ teamDashboard.addWidgets(velocityPanel('Eval Gates (auto-discovers rubrics, incl. kiro-headless)', 'eval', 9));
166
+ teamDashboard.addWidgets(velocityPanel('Governance — Guardrails & MCP (populates during sample-app runs)', 'governance', 7));
167
+ teamDashboard.addWidgets(velocityPanel('Agent Operations (populates during sample-app runs)', 'agents', 7));
168
+ teamDashboard.addWidgets(velocityPanel('Security — Continuum Findings & Remediation SLA', 'security', 10));
169
+ }
170
+
171
+ teamDashboard.addWidgets(
172
+ new cloudwatch.TextWidget({
173
+ markdown:
174
+ `Spend, tokens, and per-developer breakdowns: [PRISM-D1-Developer-Productivity](https://${this.region}.console.aws.amazon.com/cloudwatch/home?region=${this.region}#dashboards:name=PRISM-D1-Developer-Productivity)`,
175
+ width: 24,
176
+ height: 1,
979
177
  }),
980
178
  );
981
179
 
982
180
  // =======================================================
983
- // Executive Dashboard: Security & Compliance section
181
+ // Dashboard 2: Executive Readout
984
182
  // =======================================================
183
+ // Same hybrid approach as Team Velocity, with a stricter bar: this is the
184
+ // highest-stakes audience, so every widget must be backed by a real
185
+ // emitter, units are humanized (hours, not seconds), and proxies say so.
186
+ // Deleted in the rebuild: PRISM Level Progress, AI Cost & Cycle Time,
187
+ // Feature Cycle Time Trend, Weekly Bedrock Cost, and AI vs Human Defect
188
+ // Rate — all fed only by `prism-cli workshop generate-demo-data`. Security
189
+ // collapsed from 7 widgets to 1 panel; CISO Compliance owns the depth.
190
+ const execDashboard = new cloudwatch.Dashboard(this, 'ExecutiveReadoutDashboard', {
191
+ dashboardName: 'PRISM-D1-Executive-Readout',
192
+ defaultInterval: cdk.Duration.days(30),
193
+ });
194
+
195
+ const execPanel = (title: string, view: string, height: number): cloudwatch.CustomWidget =>
196
+ new cloudwatch.CustomWidget({
197
+ functionArn: props?.velocityWidgetArn ?? '',
198
+ title,
199
+ width: 24,
200
+ height,
201
+ params: { view },
202
+ updateOnRefresh: true,
203
+ updateOnResize: false,
204
+ updateOnTimeRangeChange: true,
205
+ });
206
+
985
207
  execDashboard.addWidgets(
986
208
  new cloudwatch.TextWidget({
987
- markdown: '### Security & Compliance',
209
+ markdown:
210
+ '# PRISM D1 - Executive Readout\n' +
211
+ 'AI adoption, unit economics, delivery health, and security posture. ' +
212
+ 'Delivery figures are merge-based proxies, labeled as such.',
988
213
  width: 24,
989
- height: 1,
214
+ height: 2,
990
215
  }),
991
216
  );
992
217
 
218
+ if (props?.velocityWidgetArn) {
219
+ // Rows 1-2: observed PRISM level + business KPIs + delivery proxies
220
+ execDashboard.addWidgets(execPanel('Business Outcomes & Observed Maturity', 'exec', 9));
221
+ }
222
+
223
+ // Row 3: adoption and cost trends (native, attribution/OTEL-fed).
224
+ // Weekly periods — an exec view wants the trend, not daily noise.
225
+ const weeklySum = (metricName: string, label?: string): cloudwatch.Metric =>
226
+ new cloudwatch.Metric({
227
+ namespace: METRIC_NAMESPACE,
228
+ metricName,
229
+ statistic: 'Sum',
230
+ period: cdk.Duration.days(7),
231
+ ...(label ? { label } : {}),
232
+ });
233
+
993
234
  execDashboard.addWidgets(
994
- new cloudwatch.SingleValueWidget({
995
- title: 'Guardrail Blocks (7d)',
996
- metrics: [
997
- new cloudwatch.Metric({
998
- namespace: METRIC_NAMESPACE,
999
- metricName: 'GuardrailBlockCount',
1000
- statistic: 'Sum',
1001
- period: cdk.Duration.days(7),
1002
- label: 'Blocks',
1003
- }),
235
+ new cloudwatch.GraphWidget({
236
+ title: 'AI vs Human Commits (Weekly)',
237
+ left: [
238
+ weeklySum('AICommits', 'AI commits'),
239
+ weeklySum('HumanCommits', 'Human commits'),
1004
240
  ],
1005
- width: 6,
1006
- height: 4,
241
+ view: cloudwatch.GraphWidgetView.BAR,
242
+ stacked: true,
243
+ width: 8,
244
+ height: 6,
245
+ leftYAxis: { min: 0, label: 'Commits' },
246
+ }),
247
+ new cloudwatch.GraphWidget({
248
+ title: 'AI Spend Trend (Weekly)',
249
+ left: [weeklySum('AICostUSD', 'Spend ($)')],
250
+ width: 8,
251
+ height: 6,
252
+ leftYAxis: { min: 0, label: 'USD' },
1007
253
  }),
1008
254
  new cloudwatch.GraphWidget({
1009
- title: 'Guardrail Trigger Trend',
255
+ title: 'Cost per Shipped Commit (Weekly)',
1010
256
  left: [
1011
- new cloudwatch.Metric({
1012
- namespace: METRIC_NAMESPACE,
1013
- metricName: 'GuardrailTriggerCount',
1014
- statistic: 'Sum',
1015
- period: cdk.Duration.days(1),
1016
- label: 'Daily Triggers',
257
+ // The ROI narrative: spend can rise while this falls, which means AI
258
+ // is getting more efficient per unit of shipped work.
259
+ new cloudwatch.MathExpression({
260
+ expression: 'FILL(IF(FILL(execMerged, 0) > 0, FILL(execCost, 0) / FILL(execMerged, 0)), REPEAT)',
261
+ usingMetrics: {
262
+ execCost: weeklySum('AICostUSD'),
263
+ execMerged: weeklySum('MergedAICommits'),
264
+ },
265
+ label: '$ / shipped AI commit',
266
+ period: cdk.Duration.days(7),
1017
267
  }),
1018
268
  ],
1019
- width: 9,
1020
- height: 4,
1021
- leftYAxis: { min: 0, label: 'Count' },
269
+ width: 8,
270
+ height: 6,
271
+ leftYAxis: { min: 0, label: 'USD' },
1022
272
  }),
1023
- new cloudwatch.SingleValueWidget({
1024
- title: 'MCP Auth Denied (7d)',
1025
- metrics: [
1026
- new cloudwatch.Metric({
1027
- namespace: METRIC_NAMESPACE,
1028
- metricName: 'MCPAuthDeniedCount',
1029
- statistic: 'Sum',
273
+ );
274
+
275
+ // Row 4: quality — "is AI code as reliable as human code?"
276
+ execDashboard.addWidgets(
277
+ new cloudwatch.GraphWidget({
278
+ title: 'Merge Rate: AI vs Human (Weekly)',
279
+ left: [
280
+ new cloudwatch.MathExpression({
281
+ expression: 'FILL(IF(FILL(xAi, 0) > 0, 100 * FILL(xMergedAi, 0) / FILL(xAi, 0)), REPEAT)',
282
+ usingMetrics: { xMergedAi: weeklySum('MergedAICommits'), xAi: weeklySum('AICommits') },
283
+ label: 'AI merge rate (%)',
284
+ period: cdk.Duration.days(7),
285
+ }),
286
+ new cloudwatch.MathExpression({
287
+ expression: 'FILL(IF(FILL(xHuman, 0) > 0, 100 * FILL(xMergedHuman, 0) / FILL(xHuman, 0)), REPEAT)',
288
+ usingMetrics: { xMergedHuman: weeklySum('MergedHumanCommits'), xHuman: weeklySum('HumanCommits') },
289
+ label: 'Human merge rate (%)',
1030
290
  period: cdk.Duration.days(7),
1031
- label: 'Denied',
1032
291
  }),
1033
292
  ],
1034
- width: 3,
1035
- height: 4,
293
+ width: 12,
294
+ height: 6,
295
+ leftYAxis: { min: 0, max: 100, label: 'Percent' },
1036
296
  }),
1037
- new cloudwatch.SingleValueWidget({
1038
- title: 'Exfiltration Alerts (7d)',
1039
- metrics: [
1040
- new cloudwatch.Metric({
1041
- namespace: METRIC_NAMESPACE,
1042
- metricName: 'ExfiltrationAlertCount',
1043
- statistic: 'Sum',
297
+ new cloudwatch.GraphWidget({
298
+ title: 'AI Defect Trend (Weekly)',
299
+ left: [
300
+ // RevertedAICommits may not exist at all until the first revert is
301
+ // published — a metric with zero datums is missing, not zero, so the
302
+ // inner FILL is required to avoid a NaN series.
303
+ new cloudwatch.MathExpression({
304
+ expression: 'FILL(IF(FILL(xMergedAi2, 0) > 0, 100 * FILL(xReverted, 0) / FILL(xMergedAi2, 0)), REPEAT)',
305
+ usingMetrics: { xReverted: weeklySum('RevertedAICommits'), xMergedAi2: weeklySum('MergedAICommits') },
306
+ label: 'AI defect rate (%)',
1044
307
  period: cdk.Duration.days(7),
1045
- label: 'Alerts',
1046
308
  }),
1047
309
  ],
1048
- width: 6,
1049
- height: 4,
310
+ width: 12,
311
+ height: 6,
312
+ leftYAxis: { min: 0, label: 'Percent' },
1050
313
  }),
1051
314
  );
1052
315
 
1053
- // =======================================================
1054
- // Executive Dashboard: Cost Intelligence
1055
- // =======================================================
316
+ if (props?.velocityWidgetArn) {
317
+ // Row 5: condensed security posture (CISO dashboard has the breakdowns)
318
+ execDashboard.addWidgets(execPanel('Security & Governance Posture', 'exec-security', 5));
319
+ }
320
+
1056
321
  execDashboard.addWidgets(
1057
322
  new cloudwatch.TextWidget({
1058
- markdown: '### Cost Intelligence',
323
+ markdown:
324
+ `Drill down: [Team Velocity](https://${this.region}.console.aws.amazon.com/cloudwatch/home?region=${this.region}#dashboards:name=PRISM-D1-Team-Velocity) (delivery detail) · ` +
325
+ `[Developer Productivity](https://${this.region}.console.aws.amazon.com/cloudwatch/home?region=${this.region}#dashboards:name=PRISM-D1-Developer-Productivity) (per-developer spend) · ` +
326
+ `[CISO Compliance](https://${this.region}.console.aws.amazon.com/cloudwatch/home?region=${this.region}#dashboards:name=PRISM-D1-CISO-Compliance) (full security posture)`,
1059
327
  width: 24,
1060
328
  height: 1,
1061
329
  }),
1062
330
  );
1063
331
 
1064
- execDashboard.addWidgets(
1065
- new cloudwatch.GraphWidget({
1066
- title: 'Weekly Bedrock Cost',
1067
- left: [
1068
- new cloudwatch.Metric({
1069
- namespace: METRIC_NAMESPACE,
1070
- metricName: 'BedrockCostUSD',
1071
- statistic: 'Sum',
1072
- period: cdk.Duration.days(7),
1073
- label: 'Weekly Cost ($)',
1074
- }),
1075
- ],
1076
- width: 12,
1077
- height: 6,
1078
- leftYAxis: { min: 0, label: 'USD' },
1079
- view: cloudwatch.GraphWidgetView.BAR,
332
+ // =======================================================
333
+ // CloudWatch Alarms
334
+ // =======================================================
335
+
336
+ // NOTE: the PRISM-D1-AIAcceptanceRate-Low alarm was removed along with the
337
+ // AIAcceptanceRate metric. It could not fire in either direction: the
338
+ // workflow defaulted a review-less PR to 100% acceptance (so healthy repos
339
+ // without mandatory review pinned the metric at 100), and the metric's AI
340
+ // gate was trailer-derived, so it stopped emitting entirely once git hooks
341
+ // were removed. "Does AI code pass review as well as human code?" is a
342
+ // comparative question and belongs on a dashboard panel, not an absolute
343
+ // threshold alarm.
344
+
345
+ // Alarm: Eval gate pass rate dropping below 70%
346
+ new cloudwatch.Alarm(this, 'EvalGatePassRateLowAlarm', {
347
+ alarmName: 'PRISM-D1-EvalGatePassRate-Low',
348
+ alarmDescription: 'Eval gate pass rate has dropped below 70%, indicating degraded quality in AI-generated outputs.',
349
+ metric: new cloudwatch.Metric({
350
+ namespace: METRIC_NAMESPACE,
351
+ metricName: 'EvalGatePassRate',
352
+ statistic: 'Average',
353
+ period: cdk.Duration.hours(6),
1080
354
  }),
1081
- new cloudwatch.SingleValueWidget({
1082
- title: 'AI vs Human Defect Rate',
1083
- metrics: [
1084
- new cloudwatch.Metric({
355
+ threshold: 70,
356
+ evaluationPeriods: 3,
357
+ datapointsToAlarm: 2,
358
+ comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD,
359
+ treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
360
+ });
361
+
362
+ // Alarm: Change failure rate exceeding 20%.
363
+ //
364
+ // Computed as a ratio of two per-merge counters rather than reading a
365
+ // pre-computed ChangeFailureRate metric. The old source was the weekly
366
+ // assessment, which baked in a fixed 7-day window and stopped existing when
367
+ // prism-dora-weekly.yml was removed. Deriving it here means the ratio is
368
+ // evaluated over the alarm's own window, and both counters are emitted on
369
+ // every merged PR so a healthy period reads as a real 0% rather than
370
+ // INSUFFICIENT_DATA.
371
+ new cloudwatch.Alarm(this, 'ChangeFailureRateHighAlarm', {
372
+ alarmName: 'PRISM-D1-ChangeFailureRate-High',
373
+ alarmDescription: 'Change failure rate exceeds 20% (revert/hotfix-titled PRs / merged PRs), indicating deployment quality regression.',
374
+ metric: new cloudwatch.MathExpression({
375
+ expression: 'IF(deploys > 0, 100 * fixes / deploys, 0)',
376
+ usingMetrics: {
377
+ fixes: new cloudwatch.Metric({
1085
378
  namespace: METRIC_NAMESPACE,
1086
- metricName: 'PostMergeDefectRateAI',
1087
- statistic: 'Average',
1088
- period: cdk.Duration.days(7),
1089
- label: 'AI Defect Rate (%)',
379
+ metricName: 'ChangeFailureCount',
380
+ statistic: 'Sum',
381
+ period: cdk.Duration.days(1),
1090
382
  }),
1091
- new cloudwatch.Metric({
383
+ deploys: new cloudwatch.Metric({
1092
384
  namespace: METRIC_NAMESPACE,
1093
- metricName: 'PostMergeDefectRateHuman',
1094
- statistic: 'Average',
1095
- period: cdk.Duration.days(7),
1096
- label: 'Human Defect Rate (%)',
385
+ metricName: 'DeploymentFrequency',
386
+ statistic: 'Sum',
387
+ period: cdk.Duration.days(1),
1097
388
  }),
1098
- ],
1099
- width: 6,
1100
- height: 6,
389
+ },
390
+ label: 'Change Failure Rate %',
391
+ period: cdk.Duration.days(1),
1101
392
  }),
1102
- );
393
+ threshold: 20,
394
+ evaluationPeriods: 3,
395
+ datapointsToAlarm: 2,
396
+ comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
397
+ treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
398
+ });
1103
399
 
1104
400
  // =======================================================
1105
401
  // Dashboard 3: CISO Compliance
402
+ //
403
+ // This dashboard is DEPTH. The Executive Readout owns the headline
404
+ // posture strip; everything past Row 2 here is detail the exec view
405
+ // deliberately omits.
406
+ //
407
+ // Rows 1-5 are DDB-backed custom widgets rather than metric graphs. The
408
+ // previous build used metric graphs and three of them could never render:
409
+ // the metrics publisher emits either the FULL dimension set
410
+ // ([TeamId, Repository, Phase, Severity, AIOrigin]) or a dimensionless
411
+ // aggregate copy — never a subset. Widgets querying a partial set
412
+ // ({AIOrigin} or {Phase} alone) silently matched nothing. Reading events
413
+ // directly sidesteps dimension matching, and is the only way to surface
414
+ // compliance_mappings, which is a string array and therefore cannot be a
415
+ // CloudWatch dimension at all.
1106
416
  // =======================================================
1107
417
  const cisoDashboard = new cloudwatch.Dashboard(this, 'CISOComplianceDashboard', {
1108
418
  dashboardName: 'PRISM-D1-CISO-Compliance',
1109
419
  defaultInterval: cdk.Duration.days(30),
1110
420
  });
1111
421
 
422
+ const cisoPanel = (title: string, view: string, height: number): cloudwatch.CustomWidget =>
423
+ new cloudwatch.CustomWidget({
424
+ functionArn: props?.velocityWidgetArn ?? '',
425
+ title,
426
+ width: 24,
427
+ height,
428
+ params: { view },
429
+ updateOnRefresh: true,
430
+ updateOnResize: false,
431
+ updateOnTimeRangeChange: true,
432
+ });
433
+
1112
434
  cisoDashboard.addWidgets(
1113
435
  new cloudwatch.TextWidget({
1114
- markdown: '# PRISM D1 - CISO Compliance Dashboard\nSecurity posture, remediation SLAs, and AI code risk profile across all teams.',
436
+ markdown:
437
+ '# PRISM D1 - CISO Compliance Dashboard\n' +
438
+ 'Security depth: exposure, remediation SLA compliance, AI code risk normalized by ' +
439
+ 'commit volume, shift-left effectiveness, and vulnerability classes. ' +
440
+ 'Headline posture is on **PRISM-D1-Executive-Readout**.',
1115
441
  width: 24,
1116
- height: 1,
442
+ height: 2,
1117
443
  }),
1118
444
  );
1119
445
 
1120
- cisoDashboard.addWidgets(
1121
- new cloudwatch.SingleValueWidget({
1122
- title: 'Open Critical Findings',
1123
- metrics: [
1124
- new cloudwatch.Metric({
1125
- namespace: METRIC_NAMESPACE,
1126
- metricName: 'SecurityCriticalFindingCount',
1127
- statistic: 'Sum',
1128
- period: cdk.Duration.days(30),
1129
- label: 'Critical + High (30d)',
1130
- }),
1131
- ],
1132
- width: 6,
1133
- height: 4,
1134
- }),
1135
- new cloudwatch.SingleValueWidget({
1136
- title: 'Avg Remediation Time',
1137
- metrics: [
1138
- new cloudwatch.Metric({
1139
- namespace: METRIC_NAMESPACE,
1140
- metricName: 'SecurityRemediationTimeHours',
1141
- statistic: 'Average',
1142
- period: cdk.Duration.days(30),
1143
- label: 'Hours (30d avg)',
1144
- }),
1145
- ],
1146
- width: 6,
1147
- height: 4,
1148
- }),
1149
- new cloudwatch.SingleValueWidget({
1150
- title: 'Security Scans Run',
1151
- metrics: [
1152
- new cloudwatch.Metric({
1153
- namespace: METRIC_NAMESPACE,
1154
- metricName: 'SecurityScanCount',
1155
- statistic: 'Sum',
1156
- period: cdk.Duration.days(30),
1157
- label: 'Scans (30d)',
1158
- }),
1159
- ],
1160
- width: 6,
1161
- height: 4,
1162
- }),
1163
- );
446
+ // Row 1 — current exposure
447
+ cisoDashboard.addWidgets(cisoPanel('Current Exposure', 'ciso-exposure', 5));
448
+
449
+ // Row 2 — remediation SLA compliance (SECURITY-09 budgets)
450
+ cisoDashboard.addWidgets(cisoPanel('Remediation SLA Compliance', 'ciso-sla', 8));
1164
451
 
452
+ // Row 3 — AI code risk, normalized. Raw finding counts are not comparable
453
+ // across origins; this joins findings to attribution commit volume.
454
+ cisoDashboard.addWidgets(cisoPanel('AI Code Risk Profile (per 100 commits)', 'ciso-risk', 10));
455
+
456
+ // Row 4 — shift-left effectiveness, incl. computed finding survival rate
457
+ cisoDashboard.addWidgets(cisoPanel('Shift-Left Effectiveness', 'ciso-shiftleft', 10));
458
+
459
+ // Row 5 — vulnerability classes + compliance framework coverage
460
+ cisoDashboard.addWidgets(cisoPanel('Vulnerability Classes & Compliance Coverage', 'ciso-classes', 12));
461
+
462
+ // Row 6 — runtime governance. These stay native graphs: the guardrail /
463
+ // MCP / exfiltration metrics are published with a dimensionless copy and
464
+ // these widgets query dimensionlessly, so they match correctly.
1165
465
  cisoDashboard.addWidgets(
1166
466
  new cloudwatch.TextWidget({
1167
- markdown: '### AI Code Risk Profile',
467
+ markdown: '### Runtime Governance',
1168
468
  width: 24,
1169
469
  height: 1,
1170
470
  }),
@@ -1172,112 +472,69 @@ export class DashboardStack extends cdk.Stack {
1172
472
 
1173
473
  cisoDashboard.addWidgets(
1174
474
  new cloudwatch.GraphWidget({
1175
- title: 'Security Findings: AI vs Human Code',
475
+ title: 'Guardrail Triggers vs Blocks',
1176
476
  left: [
1177
477
  new cloudwatch.Metric({
1178
478
  namespace: METRIC_NAMESPACE,
1179
- metricName: 'SecurityFindingByOrigin',
1180
- dimensionsMap: { AIOrigin: 'ai-assisted' },
479
+ metricName: 'GuardrailTriggerCount',
1181
480
  statistic: 'Sum',
1182
- period: cdk.Duration.days(7),
1183
- label: 'AI Code Findings',
481
+ period: cdk.Duration.days(1),
482
+ label: 'Triggers',
1184
483
  }),
1185
484
  new cloudwatch.Metric({
1186
485
  namespace: METRIC_NAMESPACE,
1187
- metricName: 'SecurityFindingByOrigin',
1188
- dimensionsMap: { AIOrigin: 'human' },
486
+ metricName: 'GuardrailBlockCount',
1189
487
  statistic: 'Sum',
1190
- period: cdk.Duration.days(7),
1191
- label: 'Human Code Findings',
488
+ period: cdk.Duration.days(1),
489
+ label: 'Blocks',
1192
490
  }),
1193
491
  ],
1194
- width: 12,
492
+ width: 8,
1195
493
  height: 6,
1196
- leftYAxis: { min: 0, label: 'Findings' },
494
+ leftYAxis: { min: 0, label: 'Count' },
1197
495
  }),
1198
496
  new cloudwatch.GraphWidget({
1199
- title: 'Remediation Time by Code Origin',
497
+ title: 'MCP Authorization Denials',
1200
498
  left: [
1201
499
  new cloudwatch.Metric({
1202
500
  namespace: METRIC_NAMESPACE,
1203
- metricName: 'SecurityRemediationTimeHours',
1204
- dimensionsMap: { AIOrigin: 'ai-assisted' },
1205
- statistic: 'Average',
1206
- period: cdk.Duration.days(7),
1207
- label: 'AI Code (hrs)',
1208
- }),
1209
- new cloudwatch.Metric({
1210
- namespace: METRIC_NAMESPACE,
1211
- metricName: 'SecurityRemediationTimeHours',
1212
- dimensionsMap: { AIOrigin: 'human' },
1213
- statistic: 'Average',
1214
- period: cdk.Duration.days(7),
1215
- label: 'Human Code (hrs)',
1216
- }),
1217
- ],
1218
- width: 12,
1219
- height: 6,
1220
- leftYAxis: { min: 0, label: 'Hours' },
1221
- }),
1222
- );
1223
-
1224
- cisoDashboard.addWidgets(
1225
- new cloudwatch.TextWidget({
1226
- markdown: '### Shift-Left Effectiveness',
1227
- width: 24,
1228
- height: 1,
1229
- }),
1230
- );
1231
-
1232
- cisoDashboard.addWidgets(
1233
- new cloudwatch.GraphWidget({
1234
- title: 'Findings by Phase (Monthly Trend)',
1235
- left: ['design_review', 'code_review', 'pen_test'].map((phase) =>
1236
- new cloudwatch.Metric({
1237
- namespace: METRIC_NAMESPACE,
1238
- metricName: 'SecurityFindingCount',
1239
- dimensionsMap: { Phase: phase },
501
+ metricName: 'MCPAuthDeniedCount',
1240
502
  statistic: 'Sum',
1241
- period: cdk.Duration.days(7),
1242
- label: phase.replace('_', ' '),
503
+ period: cdk.Duration.days(1),
504
+ label: 'Out-of-scope tool calls',
1243
505
  }),
1244
- ),
1245
- width: 12,
506
+ ],
507
+ width: 8,
1246
508
  height: 6,
1247
- leftYAxis: { min: 0, label: 'Findings' },
1248
- view: cloudwatch.GraphWidgetView.BAR,
509
+ leftYAxis: { min: 0, label: 'Count' },
1249
510
  }),
1250
511
  new cloudwatch.GraphWidget({
1251
- title: 'Guardrail + Exfiltration Trends',
512
+ title: 'Exfiltration Alerts',
1252
513
  left: [
1253
- new cloudwatch.Metric({
1254
- namespace: METRIC_NAMESPACE,
1255
- metricName: 'GuardrailBlockCount',
1256
- statistic: 'Sum',
1257
- period: cdk.Duration.days(7),
1258
- label: 'Guardrail Blocks',
1259
- }),
1260
514
  new cloudwatch.Metric({
1261
515
  namespace: METRIC_NAMESPACE,
1262
516
  metricName: 'ExfiltrationAlertCount',
1263
517
  statistic: 'Sum',
1264
- period: cdk.Duration.days(7),
1265
- label: 'Exfiltration Alerts',
1266
- }),
1267
- new cloudwatch.Metric({
1268
- namespace: METRIC_NAMESPACE,
1269
- metricName: 'MCPAuthDeniedCount',
1270
- statistic: 'Sum',
1271
- period: cdk.Duration.days(7),
1272
- label: 'MCP Auth Denied',
518
+ period: cdk.Duration.days(1),
519
+ label: 'Alerts',
1273
520
  }),
1274
521
  ],
1275
- width: 12,
522
+ width: 8,
1276
523
  height: 6,
1277
524
  leftYAxis: { min: 0, label: 'Count' },
1278
525
  }),
1279
526
  );
1280
527
 
528
+ cisoDashboard.addWidgets(
529
+ new cloudwatch.TextWidget({
530
+ markdown:
531
+ `**Related:** [Executive Readout](https://${this.region}.console.aws.amazon.com/cloudwatch/home?region=${this.region}#dashboards:name=PRISM-D1-Executive-Readout) (headline posture) · ` +
532
+ `[Team Velocity](https://${this.region}.console.aws.amazon.com/cloudwatch/home?region=${this.region}#dashboards:name=PRISM-D1-Team-Velocity) (per-repo delivery and security)`,
533
+ width: 24,
534
+ height: 2,
535
+ }),
536
+ );
537
+
1281
538
  new cdk.CfnOutput(this, 'CISODashboardUrl', {
1282
539
  value: `https://${this.region}.console.aws.amazon.com/cloudwatch/home?region=${this.region}#dashboards:name=PRISM-D1-CISO-Compliance`,
1283
540
  description: 'CISO Compliance Dashboard URL',
@@ -1353,13 +610,17 @@ export class DashboardStack extends cdk.Stack {
1353
610
  treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
1354
611
  });
1355
612
 
1356
- // Alarm: Daily Bedrock cost exceeding budget
613
+ // Alarm: Daily AI cost exceeding budget.
614
+ // Watches AICostUSD (real — otel-metrics-publisher from codeburn usage
615
+ // spans) rather than BedrockCostUSD, which has no emitter outside
616
+ // `prism-cli workshop generate-demo-data` and left this alarm permanently
617
+ // in INSUFFICIENT_DATA.
1357
618
  new cloudwatch.Alarm(this, 'BedrockDailyCostHighAlarm', {
1358
- alarmName: 'PRISM-D1-BedrockDailyCost-High',
1359
- alarmDescription: 'Daily Bedrock cost exceeds $100 threshold.',
619
+ alarmName: 'PRISM-D1-AIDailyCost-High',
620
+ alarmDescription: 'Daily AI tooling cost exceeds the $100 threshold.',
1360
621
  metric: new cloudwatch.Metric({
1361
622
  namespace: METRIC_NAMESPACE,
1362
- metricName: 'BedrockCostUSD',
623
+ metricName: 'AICostUSD',
1363
624
  statistic: 'Sum',
1364
625
  period: cdk.Duration.days(1),
1365
626
  }),
@@ -1369,22 +630,10 @@ export class DashboardStack extends cdk.Stack {
1369
630
  treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
1370
631
  });
1371
632
 
1372
- // Alarm: Token efficiency below threshold
1373
- new cloudwatch.Alarm(this, 'TokenEfficiencyLowAlarm', {
1374
- alarmName: 'PRISM-D1-TokenEfficiency-Low',
1375
- alarmDescription: 'Token efficiency is low — high token consumption relative to code output.',
1376
- metric: new cloudwatch.Metric({
1377
- namespace: METRIC_NAMESPACE,
1378
- metricName: 'TokenEfficiency',
1379
- statistic: 'Average',
1380
- period: cdk.Duration.hours(6),
1381
- }),
1382
- threshold: 500,
1383
- evaluationPeriods: 3,
1384
- datapointsToAlarm: 2,
1385
- comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
1386
- treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
1387
- });
633
+ // NOTE: a TokenEfficiency alarm was removed here. The metric (tokens per
634
+ // line changed) has no emitter — the only source is `prism-cli workshop
635
+ // generate-demo-data` — so the alarm sat in INSUFFICIENT_DATA permanently.
636
+ // Reinstate it if/when a real emitter computes tokens-per-line.
1388
637
 
1389
638
  // Alarm: Exfiltration detection
1390
639
  new cloudwatch.Alarm(this, 'ExfiltrationAlertAlarm', {
@@ -1403,11 +652,13 @@ export class DashboardStack extends cdk.Stack {
1403
652
  });
1404
653
 
1405
654
  // Alarm: Agent success rate dropping below 80%
655
+ // Uses the dimensionless aggregate series (dual-published by the
656
+ // processor). The previous TeamId=ALL/Repository=ALL dimensions never
657
+ // matched real events, so the alarm sat in INSUFFICIENT_DATA forever.
1406
658
  new cloudwatch.Alarm(this, 'AgentSuccessRateAlarm', {
1407
659
  metric: new cloudwatch.Metric({
1408
660
  namespace: METRIC_NAMESPACE,
1409
661
  metricName: 'AgentSuccessRate',
1410
- dimensionsMap: { TeamId: teamId, Repository: repository },
1411
662
  statistic: 'Average',
1412
663
  period: cdk.Duration.hours(1),
1413
664
  }),
@@ -1415,7 +666,7 @@ export class DashboardStack extends cdk.Stack {
1415
666
  evaluationPeriods: 3,
1416
667
  comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD,
1417
668
  alarmDescription: 'Agent success rate below 80% for 3 consecutive hours',
1418
- alarmName: `prism-d1-${teamId}-agent-success-rate`,
669
+ alarmName: 'prism-d1-agent-success-rate',
1419
670
  });
1420
671
 
1421
672
  // -------------------------------------------------------
@@ -1430,5 +681,151 @@ export class DashboardStack extends cdk.Stack {
1430
681
  value: `https://${this.region}.console.aws.amazon.com/cloudwatch/home?region=${this.region}#dashboards:name=PRISM-D1-Executive-Readout`,
1431
682
  description: 'Executive Readout Dashboard URL',
1432
683
  });
684
+
685
+ // =======================================================
686
+ // Dashboard 3: Developer Productivity (attribution store)
687
+ // Only created when the OTEL collector (and its custom-widget
688
+ // Lambda) is deployed.
689
+ // =======================================================
690
+ if (props?.productivityWidgetArn) {
691
+ // Type-in filter for the detail row. PATTERN variables literal-replace
692
+ // the token below anywhere in the dashboard JSON, so the token must be
693
+ // unique. Default 'all' = organization scope.
694
+ const USER_TOKEN = 'PRISM_USER_FILTER_TOKEN';
695
+ const devDashboard = new cloudwatch.Dashboard(this, 'DeveloperProductivityDashboard', {
696
+ dashboardName: 'PRISM-D1-Developer-Productivity',
697
+ defaultInterval: cdk.Duration.days(30),
698
+ variables: [
699
+ new cloudwatch.DashboardVariable({
700
+ id: 'devFilter',
701
+ label: 'Developer',
702
+ type: cloudwatch.VariableType.PATTERN,
703
+ value: USER_TOKEN,
704
+ inputType: cloudwatch.VariableInputType.INPUT,
705
+ defaultValue: cloudwatch.DefaultValue.value('all'),
706
+ visible: true,
707
+ }),
708
+ ],
709
+ });
710
+
711
+ devDashboard.addWidgets(
712
+ new cloudwatch.TextWidget({
713
+ markdown:
714
+ '# PRISM D1 - Developer Productivity\n' +
715
+ 'Rows 1-2: org metrics (CloudWatch, 2-week ingestion window applies to backfills). ' +
716
+ 'Rows 3-4: attribution store — full history, real commit timestamps. ' +
717
+ 'Type a developer email into the **Developer** variable (default `all`) to scope the detail row.',
718
+ width: 24,
719
+ height: 2,
720
+ }),
721
+ );
722
+
723
+ // --- Row 1: Org KPIs (NaN-guarded metric math, follow time range) ---
724
+ const dailyMetric = (metricName: string, id: string): cloudwatch.Metric =>
725
+ new cloudwatch.Metric({
726
+ namespace: METRIC_NAMESPACE,
727
+ metricName,
728
+ statistic: 'Sum',
729
+ period: cdk.Duration.days(1),
730
+ label: id,
731
+ });
732
+ const ratioKpi = (title: string, expression: string, using: Record<string, cloudwatch.IMetric>): cloudwatch.SingleValueWidget =>
733
+ new cloudwatch.SingleValueWidget({
734
+ title,
735
+ metrics: [new cloudwatch.MathExpression({ expression, usingMetrics: using, label: title, period: cdk.Duration.days(1) })],
736
+ setPeriodToTimeRange: true,
737
+ width: 6,
738
+ height: 4,
739
+ });
740
+
741
+ devDashboard.addWidgets(
742
+ ratioKpi('AI Share of Commits (%)', 'IF(FILL(kTotal, 0) > 0, 100 * FILL(kAi, 0) / FILL(kTotal, 0))', {
743
+ kAi: dailyMetric('AICommits', 'kAi'), kTotal: dailyMetric('CommitsTotal', 'kTotal'),
744
+ }),
745
+ ratioKpi('AI Merge Rate (%)', 'IF(FILL(kAi2, 0) > 0, 100 * FILL(kMerged, 0) / FILL(kAi2, 0))', {
746
+ kMerged: dailyMetric('MergedAICommits', 'kMerged'), kAi2: dailyMetric('AICommits', 'kAi2'),
747
+ }),
748
+ ratioKpi('Cost per Shipped Commit ($)', 'IF(FILL(kMerged2, 0) > 0, FILL(kCost, 0) / FILL(kMerged2, 0))', {
749
+ kCost: dailyMetric('AICostUSD', 'kCost'), kMerged2: dailyMetric('MergedAICommits', 'kMerged2'),
750
+ }),
751
+ new cloudwatch.SingleValueWidget({
752
+ title: 'AI Spend (range, $)',
753
+ metrics: [dailyMetric('AICostUSD', 'AI Spend')],
754
+ setPeriodToTimeRange: true,
755
+ width: 6,
756
+ height: 4,
757
+ }),
758
+ );
759
+
760
+ // --- Row 2: Org trends (native interactive charts) ---
761
+ devDashboard.addWidgets(
762
+ new cloudwatch.GraphWidget({
763
+ title: 'Commits / Day (AI vs Human)',
764
+ left: [
765
+ dailyMetric('AICommits', 'AI commits'),
766
+ dailyMetric('HumanCommits', 'Human commits'),
767
+ ],
768
+ view: cloudwatch.GraphWidgetView.BAR,
769
+ stacked: true,
770
+ width: 8,
771
+ height: 6,
772
+ leftYAxis: { min: 0, label: 'Commits' },
773
+ }),
774
+ new cloudwatch.GraphWidget({
775
+ title: 'AI Spend / Day',
776
+ left: [dailyMetric('AICostUSD', 'Spend ($)')],
777
+ width: 8,
778
+ height: 6,
779
+ leftYAxis: { min: 0, label: 'USD' },
780
+ }),
781
+ new cloudwatch.GraphWidget({
782
+ title: 'AI Merge Ratio Trend',
783
+ left: [
784
+ new cloudwatch.MathExpression({
785
+ expression: 'FILL(IF(FILL(tAi, 0) > 0, 100 * FILL(tMerged, 0) / FILL(tAi, 0)), REPEAT)',
786
+ usingMetrics: { tMerged: dailyMetric('MergedAICommits', 'tMerged'), tAi: dailyMetric('AICommits', 'tAi') },
787
+ label: 'Merge ratio (%)',
788
+ period: cdk.Duration.days(1),
789
+ }),
790
+ ],
791
+ width: 8,
792
+ height: 6,
793
+ leftYAxis: { min: 0, max: 100, label: 'Percent' },
794
+ }),
795
+ );
796
+
797
+ // --- Row 3: Team comparison table (custom widget, view=table) ---
798
+ devDashboard.addWidgets(
799
+ new cloudwatch.CustomWidget({
800
+ functionArn: props.productivityWidgetArn,
801
+ title: 'Team Comparison (full history)',
802
+ width: 24,
803
+ height: 8,
804
+ params: { view: 'table' },
805
+ updateOnRefresh: true,
806
+ updateOnResize: false,
807
+ updateOnTimeRangeChange: true,
808
+ }),
809
+ );
810
+
811
+ // --- Row 4: Detail panel (custom widget, view=detail, follows filter) ---
812
+ devDashboard.addWidgets(
813
+ new cloudwatch.CustomWidget({
814
+ functionArn: props.productivityWidgetArn,
815
+ title: 'Detail: By Tool / By Model / Ratios',
816
+ width: 24,
817
+ height: 8,
818
+ params: { view: 'detail', user: USER_TOKEN },
819
+ updateOnRefresh: true,
820
+ updateOnResize: false,
821
+ updateOnTimeRangeChange: true,
822
+ }),
823
+ );
824
+
825
+ new cdk.CfnOutput(this, 'DevProductivityDashboardUrl', {
826
+ value: `https://${this.region}.console.aws.amazon.com/cloudwatch/home?region=${this.region}#dashboards:name=PRISM-D1-Developer-Productivity`,
827
+ description: 'Developer Productivity Dashboard URL',
828
+ });
829
+ }
1433
830
  }
1434
831
  }