@prism-d1/cli 1.0.79 → 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 (38) 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 +1 -0
  7. package/dist/assets/infra/lib/constructs/otel-collector-construct.ts +4 -0
  8. package/dist/assets/infra/lib/dashboard-stack.ts +378 -1235
  9. package/dist/assets/infra/lib/lambda/event-schema.ts +144 -0
  10. package/dist/assets/infra/lib/lambda/metrics-processor.ts +124 -36
  11. package/dist/assets/infra/lib/lambda/otel-receiver.ts +249 -35
  12. package/dist/assets/infra/lib/lambda/security-remediation-tracker.ts +17 -2
  13. package/dist/assets/infra/lib/lambda/velocity-widget.ts +1483 -0
  14. package/dist/assets/infra/lib/metrics-pipeline-stack.ts +110 -119
  15. package/dist/assets/infra/package.json +2 -1
  16. package/dist/assets/infra/tsconfig.json +9 -2
  17. package/dist/src/commands/bootstrapper/install-eval-harness.d.ts +8 -2
  18. package/dist/src/commands/bootstrapper/install-eval-harness.d.ts.map +1 -1
  19. package/dist/src/commands/bootstrapper/install-eval-harness.js +32 -32
  20. package/dist/src/commands/bootstrapper/install-eval-harness.js.map +1 -1
  21. package/dist/src/commands/bootstrapper/install-github-workflows.d.ts.map +1 -1
  22. package/dist/src/commands/bootstrapper/install-github-workflows.js +7 -2
  23. package/dist/src/commands/bootstrapper/install-github-workflows.js.map +1 -1
  24. package/dist/src/commands/bootstrapper/install-gitlab-workflows.d.ts.map +1 -1
  25. package/dist/src/commands/bootstrapper/install-gitlab-workflows.js +7 -1
  26. package/dist/src/commands/bootstrapper/install-gitlab-workflows.js.map +1 -1
  27. package/dist/src/utils/region.d.ts +50 -0
  28. package/dist/src/utils/region.d.ts.map +1 -0
  29. package/dist/src/utils/region.js +62 -0
  30. package/dist/src/utils/region.js.map +1 -0
  31. package/package.json +2 -2
  32. package/dist/assets/eval-harness/README.md +0 -144
  33. package/dist/assets/github-workflows/README.md +0 -110
  34. package/dist/assets/github-workflows/prism-dora-weekly.yml +0 -334
  35. package/dist/assets/gitlab-workflows/prism-dora-weekly.yml +0 -181
  36. package/dist/assets/infra/lib/lambda/defect-correlator.ts +0 -142
  37. package/dist/assets/infra/lib/lambda/spec-to-code-calculator.ts +0 -123
  38. package/dist/assets/metric-hooks/README.md +0 -107
@@ -9,6 +9,8 @@ const DEFAULT_PERIOD = cdk.Duration.hours(1);
9
9
  export interface DashboardStackProps extends cdk.StackProps {
10
10
  /** ARN of the productivity custom-widget Lambda (from the OTEL collector construct). */
11
11
  readonly productivityWidgetArn?: string;
12
+ /** ARN of the Team Velocity custom-widget Lambda (DDB-backed panels). */
13
+ readonly velocityWidgetArn?: string;
12
14
  }
13
15
 
14
16
  export class DashboardStack extends cdk.Stack {
@@ -36,1243 +38,433 @@ export class DashboardStack extends cdk.Stack {
36
38
  defaultInterval: cdk.Duration.days(7),
37
39
  });
38
40
 
39
- // --- AI Acceptance Rate (time series) ---
40
- const aiAcceptanceMetric = new cloudwatch.Metric({
41
- namespace: METRIC_NAMESPACE,
42
- metricName: 'AIAcceptanceRate',
43
- statistic: 'Average',
44
- period: DEFAULT_PERIOD,
45
- label: 'AI Acceptance Rate (%)',
46
- });
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,
53
+ width: 24,
54
+ height,
55
+ params: { view },
56
+ updateOnRefresh: true,
57
+ updateOnResize: false,
58
+ updateOnTimeRangeChange: true,
59
+ });
47
60
 
48
61
  teamDashboard.addWidgets(
49
62
  new cloudwatch.TextWidget({
50
- markdown: '# PRISM D1 - Team Velocity Dashboard\nReal-time AI-enhanced DORA metrics for engineering teams.',
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**.',
51
67
  width: 24,
52
- height: 1,
68
+ height: 2,
53
69
  }),
54
70
  );
55
71
 
56
- teamDashboard.addWidgets(
57
- new cloudwatch.GraphWidget({
58
- title: 'AI Acceptance Rate',
59
- left: [aiAcceptanceMetric],
60
- width: 12,
61
- height: 6,
62
- leftYAxis: { min: 0, max: 100, label: 'Percent' },
63
- statistic: 'Average',
64
- period: DEFAULT_PERIOD,
65
- }),
66
- new cloudwatch.GraphWidget({
67
- title: 'Deployment Frequency',
68
- left: [
69
- new cloudwatch.Metric({
70
- namespace: METRIC_NAMESPACE,
71
- metricName: 'DeploymentFrequency',
72
- statistic: 'Sum',
73
- period: cdk.Duration.days(1),
74
- label: 'Deploys / Day',
75
- }),
76
- ],
77
- width: 12,
78
- height: 6,
79
- view: cloudwatch.GraphWidgetView.BAR,
80
- leftYAxis: { min: 0, label: 'Deployments' },
81
- }),
82
- );
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
+ }
83
78
 
84
- teamDashboard.addWidgets(
85
- new cloudwatch.GraphWidget({
86
- title: 'Lead Time for Changes',
87
- left: [
88
- new cloudwatch.Metric({
89
- namespace: METRIC_NAMESPACE,
90
- metricName: 'LeadTimeForChanges',
91
- statistic: 'Average',
92
- period: DEFAULT_PERIOD,
93
- label: 'Lead Time (seconds)',
94
- }),
95
- ],
96
- width: 8,
97
- height: 6,
98
- leftYAxis: { min: 0, label: 'Seconds' },
99
- }),
100
- new cloudwatch.GaugeWidget({
101
- title: 'Eval Gate Pass Rate',
102
- metrics: [
103
- new cloudwatch.Metric({
104
- namespace: METRIC_NAMESPACE,
105
- metricName: 'EvalGatePassRate',
106
- statistic: 'Average',
107
- period: cdk.Duration.days(1),
108
- label: 'Eval Pass Rate (%)',
109
- }),
110
- ],
111
- width: 8,
112
- height: 6,
113
- leftYAxis: { min: 0, max: 100 },
114
- }),
115
- new cloudwatch.GraphWidget({
116
- title: 'AI Test Coverage Delta',
117
- left: [
118
- new cloudwatch.Metric({
119
- namespace: METRIC_NAMESPACE,
120
- metricName: 'AITestCoverageDelta',
121
- statistic: 'Average',
122
- period: DEFAULT_PERIOD,
123
- label: 'Coverage Delta (%)',
124
- }),
125
- ],
126
- width: 8,
127
- height: 6,
128
- leftYAxis: { label: 'Percent' },
129
- }),
130
- );
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
+ });
131
93
 
132
- // --- Additional team metrics row ---
133
94
  teamDashboard.addWidgets(
134
95
  new cloudwatch.GraphWidget({
135
- title: 'Change Failure Rate',
136
- left: [
137
- new cloudwatch.Metric({
138
- namespace: METRIC_NAMESPACE,
139
- metricName: 'ChangeFailureRate',
140
- statistic: 'Average',
141
- period: DEFAULT_PERIOD,
142
- label: 'Change Failure Rate (%)',
143
- }),
144
- ],
145
- width: 8,
146
- height: 6,
147
- leftYAxis: { min: 0, max: 100, label: 'Percent' },
148
- }),
149
- new cloudwatch.GraphWidget({
150
- title: 'Mean Time to Recovery',
96
+ title: 'Commits / Day (AI vs Human)',
151
97
  left: [
152
- new cloudwatch.Metric({
153
- namespace: METRIC_NAMESPACE,
154
- metricName: 'MTTR',
155
- statistic: 'Average',
156
- period: DEFAULT_PERIOD,
157
- label: 'MTTR (seconds)',
158
- }),
98
+ dailySum('AICommits', 'AI commits'),
99
+ dailySum('HumanCommits', 'Human commits'),
159
100
  ],
101
+ view: cloudwatch.GraphWidgetView.BAR,
102
+ stacked: true,
160
103
  width: 8,
161
104
  height: 6,
162
- leftYAxis: { min: 0, label: 'Seconds' },
105
+ leftYAxis: { min: 0, label: 'Commits' },
163
106
  }),
164
107
  new cloudwatch.GraphWidget({
165
- title: 'AI to Merge Ratio',
108
+ title: 'Merge Ratio: AI vs Human',
166
109
  left: [
167
- // Line 1: CI-fed metric (git trailers, computed per PR at merge time
168
- // by prism-ai-metrics.yml). Requires an instrumented repo.
169
- new cloudwatch.Metric({
170
- namespace: METRIC_NAMESPACE,
171
- metricName: 'AIToMergeRatio',
172
- statistic: 'Average',
173
- period: DEFAULT_PERIOD,
174
- label: 'CI (git trailers, per-PR)',
175
- }),
176
- // Line 2: attribution-derived (codeburn spans, fleet aggregate).
177
- // Populates for any user running `codeburn sync --attribution`.
178
- // Divergence from line 1 indicates attribution coverage gaps.
179
- // Inner FILL/IF: avoid NaN on empty buckets (gap, not 0). Outer
180
- // FILL(..., REPEAT): carry the last ratio forward through gap days —
181
- // commit data is sparse, and CloudWatch renders an isolated
182
- // datapoint between gaps as an invisible dot, not a line.
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".
183
113
  new cloudwatch.MathExpression({
184
114
  expression: 'FILL(IF(FILL(aiCommits, 0) > 0, 100 * FILL(mergedAi, 0) / FILL(aiCommits, 0)), REPEAT)',
185
115
  usingMetrics: {
186
- mergedAi: new cloudwatch.Metric({
187
- namespace: METRIC_NAMESPACE,
188
- metricName: 'MergedAICommits',
189
- statistic: 'Sum',
190
- period: cdk.Duration.days(1),
191
- }),
192
- aiCommits: new cloudwatch.Metric({
193
- namespace: METRIC_NAMESPACE,
194
- metricName: 'AICommits',
195
- statistic: 'Sum',
196
- period: cdk.Duration.days(1),
197
- }),
116
+ mergedAi: dailySum('MergedAICommits'),
117
+ aiCommits: dailySum('AICommits'),
198
118
  },
199
- label: 'Attribution (codeburn spans)',
119
+ label: 'AI merge rate (%)',
200
120
  period: cdk.Duration.days(1),
201
121
  }),
202
- ],
203
- width: 6,
204
- height: 6,
205
- leftYAxis: { min: 0, max: 100, label: 'Percent' },
206
- }),
207
- // KPI card: the aggregate ratio over the whole displayed time range.
208
- // Always shows a number regardless of how sparse the daily series is —
209
- // the graph next door can render near-empty when the range holds only
210
- // isolated datapoints.
211
- new cloudwatch.SingleValueWidget({
212
- title: 'AI Merge Rate (range)',
213
- metrics: [
214
122
  new cloudwatch.MathExpression({
215
- expression: 'IF(FILL(aiCommitsKpi, 0) > 0, 100 * FILL(mergedAiKpi, 0) / FILL(aiCommitsKpi, 0))',
123
+ expression: 'FILL(IF(FILL(humanCommits, 0) > 0, 100 * FILL(mergedHuman, 0) / FILL(humanCommits, 0)), REPEAT)',
216
124
  usingMetrics: {
217
- mergedAiKpi: new cloudwatch.Metric({
218
- namespace: METRIC_NAMESPACE,
219
- metricName: 'MergedAICommits',
220
- statistic: 'Sum',
221
- period: cdk.Duration.days(1),
222
- }),
223
- aiCommitsKpi: new cloudwatch.Metric({
224
- namespace: METRIC_NAMESPACE,
225
- metricName: 'AICommits',
226
- statistic: 'Sum',
227
- period: cdk.Duration.days(1),
228
- }),
125
+ mergedHuman: dailySum('MergedHumanCommits'),
126
+ humanCommits: dailySum('HumanCommits'),
229
127
  },
230
- label: 'Merged AI / AI commits (%)',
231
- period: cdk.Duration.days(1),
232
- }),
233
- ],
234
- setPeriodToTimeRange: true,
235
- width: 2,
236
- height: 6,
237
- }),
238
- );
239
-
240
- // --- Agent Metrics Row ---
241
- const teamId = 'ALL';
242
- const repository = 'ALL';
243
-
244
- teamDashboard.addWidgets(
245
- new cloudwatch.TextWidget({
246
- markdown: '### AI Token Usage & Cost',
247
- width: 24,
248
- height: 1,
249
- }),
250
- );
251
-
252
- teamDashboard.addWidgets(
253
- new cloudwatch.GraphWidget({
254
- title: 'AI Input Tokens',
255
- left: [
256
- new cloudwatch.Metric({
257
- namespace: METRIC_NAMESPACE,
258
- metricName: 'AIInputTokens',
259
- statistic: 'Sum',
260
- period: cdk.Duration.days(1),
261
- label: 'Input Tokens',
262
- }),
263
- ],
264
- width: 8,
265
- height: 6,
266
- leftYAxis: { min: 0, label: 'Tokens' },
267
- }),
268
- new cloudwatch.GraphWidget({
269
- title: 'AI Output Tokens',
270
- left: [
271
- new cloudwatch.Metric({
272
- namespace: METRIC_NAMESPACE,
273
- metricName: 'AIOutputTokens',
274
- statistic: 'Sum',
275
- period: cdk.Duration.days(1),
276
- label: 'Output Tokens',
277
- }),
278
- ],
279
- width: 8,
280
- height: 6,
281
- leftYAxis: { min: 0, label: 'Tokens' },
282
- }),
283
- new cloudwatch.GraphWidget({
284
- title: 'AI Cost (USD)',
285
- left: [
286
- new cloudwatch.Metric({
287
- namespace: METRIC_NAMESPACE,
288
- metricName: 'AICostUSD',
289
- statistic: 'Sum',
290
- period: cdk.Duration.days(1),
291
- label: 'Cost (USD)',
292
- }),
293
- ],
294
- width: 8,
295
- height: 6,
296
- leftYAxis: { min: 0, label: 'USD' },
297
- }),
298
- );
299
-
300
- // Cost breakdowns by Tool and Model. SEARCH expressions auto-discover
301
- // every dimension value, so new tools/models appear without dashboard
302
- // changes. These series are populated by the otel-metrics-publisher
303
- // (deployed by default; skip with -c skipOtelCollector=true); the trailer
304
- // path has no per-tool/per-model breakdown and leaves these widgets empty.
305
- teamDashboard.addWidgets(
306
- new cloudwatch.GraphWidget({
307
- title: 'AI Cost by Tool (USD)',
308
- left: [
309
- new cloudwatch.MathExpression({
310
- expression: `SEARCH('{${METRIC_NAMESPACE},Tool} MetricName="AICostUSD"', 'Sum')`,
311
- usingMetrics: {},
312
- // Dynamic label: each discovered series is legend-labeled with
313
- // its Tool dimension value (e.g. "claude-code").
314
- label: "${PROP('Dim.Tool')}",
315
- period: cdk.Duration.days(1),
316
- }),
317
- ],
318
- width: 12,
319
- height: 6,
320
- leftYAxis: { min: 0, label: 'USD' },
321
- legendPosition: cloudwatch.LegendPosition.RIGHT,
322
- }),
323
- new cloudwatch.GraphWidget({
324
- title: 'AI Cost by Model (USD)',
325
- left: [
326
- new cloudwatch.MathExpression({
327
- expression: `SEARCH('{${METRIC_NAMESPACE},Model} MetricName="AICostUSD"', 'Sum')`,
328
- usingMetrics: {},
329
- // Dynamic label: each discovered series is legend-labeled with
330
- // its Model dimension value (e.g. "claude-sonnet-4-5-20250929").
331
- label: "${PROP('Dim.Model')}",
332
- period: cdk.Duration.days(1),
333
- }),
334
- ],
335
- width: 12,
336
- height: 6,
337
- leftYAxis: { min: 0, label: 'USD' },
338
- legendPosition: cloudwatch.LegendPosition.RIGHT,
339
- }),
340
- );
341
-
342
- teamDashboard.addWidgets(
343
- new cloudwatch.TextWidget({
344
- markdown: '### Agent Operations',
345
- width: 24,
346
- height: 1,
347
- }),
348
- );
349
-
350
- teamDashboard.addWidgets(
351
- new cloudwatch.GraphWidget({
352
- title: 'Agent Invocations',
353
- left: [
354
- new cloudwatch.Metric({
355
- namespace: METRIC_NAMESPACE,
356
- metricName: 'AgentInvocationCount',
357
- dimensionsMap: { TeamId: teamId, Repository: repository },
358
- statistic: 'Sum',
359
- period: DEFAULT_PERIOD,
360
- label: 'Invocations',
361
- }),
362
- ],
363
- width: 8,
364
- height: 6,
365
- }),
366
- new cloudwatch.GraphWidget({
367
- title: 'Agent Success Rate',
368
- left: [
369
- new cloudwatch.Metric({
370
- namespace: METRIC_NAMESPACE,
371
- metricName: 'AgentSuccessRate',
372
- dimensionsMap: { TeamId: teamId, Repository: repository },
373
- statistic: 'Average',
374
- period: DEFAULT_PERIOD,
375
- label: 'Success Rate (%)',
376
- }),
377
- ],
378
- width: 8,
379
- height: 6,
380
- leftYAxis: { min: 0, max: 100, label: 'Percent' },
381
- }),
382
- new cloudwatch.GraphWidget({
383
- title: 'Agent Avg Duration',
384
- left: [
385
- new cloudwatch.Metric({
386
- namespace: METRIC_NAMESPACE,
387
- metricName: 'AgentDurationMs',
388
- dimensionsMap: { TeamId: teamId, Repository: repository },
389
- statistic: 'Average',
390
- period: DEFAULT_PERIOD,
391
- label: 'Duration (ms)',
392
- }),
393
- ],
394
- width: 8,
395
- height: 6,
396
- }),
397
- );
398
-
399
- // =======================================================
400
- // Dashboard 2: Executive Readout
401
- // =======================================================
402
- const execDashboard = new cloudwatch.Dashboard(this, 'ExecutiveReadoutDashboard', {
403
- dashboardName: 'PRISM-D1-Executive-Readout',
404
- defaultInterval: cdk.Duration.days(30),
405
- });
406
-
407
- execDashboard.addWidgets(
408
- new cloudwatch.TextWidget({
409
- markdown: '# PRISM D1 - Executive Readout\nLeadership view of AI-assisted engineering velocity and DORA performance.',
410
- width: 24,
411
- height: 1,
412
- }),
413
- );
414
-
415
- // --- PRISM Level Progress + Enhanced DORA Summary row ---
416
- execDashboard.addWidgets(
417
- new cloudwatch.SingleValueWidget({
418
- title: 'PRISM Level Progress',
419
- metrics: [
420
- new cloudwatch.Metric({
421
- namespace: METRIC_NAMESPACE,
422
- metricName: 'PRISMLevel',
423
- statistic: 'Maximum',
424
- period: cdk.Duration.days(1),
425
- label: 'Current PRISM Level',
426
- }),
427
- ],
428
- width: 6,
429
- height: 4,
430
- }),
431
- new cloudwatch.SingleValueWidget({
432
- title: 'Enhanced DORA Summary',
433
- metrics: [
434
- new cloudwatch.Metric({
435
- namespace: METRIC_NAMESPACE,
436
- metricName: 'DeploymentFrequency',
437
- statistic: 'Sum',
438
- period: cdk.Duration.days(7),
439
- label: 'Deploy Freq (7d)',
440
- }),
441
- new cloudwatch.Metric({
442
- namespace: METRIC_NAMESPACE,
443
- metricName: 'LeadTimeForChanges',
444
- statistic: 'Average',
445
- period: cdk.Duration.days(7),
446
- label: 'Avg Lead Time (s)',
447
- }),
448
- new cloudwatch.Metric({
449
- namespace: METRIC_NAMESPACE,
450
- metricName: 'ChangeFailureRate',
451
- statistic: 'Average',
452
- period: cdk.Duration.days(7),
453
- label: 'Change Fail Rate (%)',
454
- }),
455
- new cloudwatch.Metric({
456
- namespace: METRIC_NAMESPACE,
457
- metricName: 'MTTR',
458
- statistic: 'Average',
459
- period: cdk.Duration.days(7),
460
- label: 'Avg MTTR (s)',
461
- }),
462
- ],
463
- width: 12,
464
- height: 4,
465
- }),
466
- new cloudwatch.SingleValueWidget({
467
- title: 'AI Cost & Cycle Time',
468
- metrics: [
469
- new cloudwatch.Metric({
470
- namespace: METRIC_NAMESPACE,
471
- metricName: 'AICostUSD',
472
- statistic: 'Sum',
473
- period: cdk.Duration.days(7),
474
- label: 'Total AI Cost (7d, USD)',
475
- }),
476
- new cloudwatch.Metric({
477
- namespace: METRIC_NAMESPACE,
478
- metricName: 'SpecToCodeHours',
479
- statistic: 'Average',
480
- period: cdk.Duration.days(7),
481
- label: 'Avg Spec-to-Code (hrs)',
482
- }),
483
- ],
484
- width: 6,
485
- height: 4,
486
- }),
487
- );
488
-
489
- // --- AI Contribution Trend + Feature Cycle Time Trend ---
490
- execDashboard.addWidgets(
491
- new cloudwatch.GraphWidget({
492
- title: 'AI Contribution Trend',
493
- left: [
494
- new cloudwatch.Metric({
495
- namespace: METRIC_NAMESPACE,
496
- metricName: 'AIAcceptanceRate',
497
- statistic: 'Average',
128
+ label: 'Human merge rate (%)',
498
129
  period: cdk.Duration.days(1),
499
- label: 'AI Acceptance Rate (%)',
500
- }),
501
- new cloudwatch.Metric({
502
- namespace: METRIC_NAMESPACE,
503
- metricName: 'AIToMergeRatio',
504
- statistic: 'Average',
505
- period: cdk.Duration.days(1),
506
- label: 'AI-to-Merge Ratio (CI, %)',
507
- }),
508
- new cloudwatch.MathExpression({
509
- expression: 'IF(FILL(execAiCommits, 0) > 0, 100 * FILL(execMergedAi, 0) / FILL(execAiCommits, 0))',
510
- usingMetrics: {
511
- execMergedAi: new cloudwatch.Metric({
512
- namespace: METRIC_NAMESPACE,
513
- metricName: 'MergedAICommits',
514
- statistic: 'Sum',
515
- period: cdk.Duration.days(1),
516
- }),
517
- execAiCommits: new cloudwatch.Metric({
518
- namespace: METRIC_NAMESPACE,
519
- metricName: 'AICommits',
520
- statistic: 'Sum',
521
- period: cdk.Duration.days(1),
522
- }),
523
- },
524
- label: 'AI-to-Merge Ratio (Attribution, %)',
525
- period: cdk.Duration.days(1),
526
- }),
527
- new cloudwatch.Metric({
528
- namespace: METRIC_NAMESPACE,
529
- metricName: 'AITestCoverageDelta',
530
- statistic: 'Average',
531
- period: cdk.Duration.days(1),
532
- label: 'AI Test Coverage Delta (%)',
533
- }),
534
- ],
535
- width: 12,
536
- height: 6,
537
- leftYAxis: { min: 0, max: 100, label: 'Percent' },
538
- }),
539
- new cloudwatch.GraphWidget({
540
- title: 'Feature Cycle Time Trend',
541
- left: [
542
- new cloudwatch.Metric({
543
- namespace: METRIC_NAMESPACE,
544
- metricName: 'SpecToCodeHours',
545
- statistic: 'Average',
546
- period: cdk.Duration.days(1),
547
- label: 'Spec-to-Code (hrs)',
548
- }),
549
- new cloudwatch.Metric({
550
- namespace: METRIC_NAMESPACE,
551
- metricName: 'LeadTimeForChanges',
552
- statistic: 'Average',
553
- period: cdk.Duration.days(1),
554
- label: 'Lead Time (seconds)',
555
- }),
556
- ],
557
- width: 12,
558
- height: 6,
559
- }),
560
- );
561
-
562
- // --- Eval gate, quality, and cost trend row ---
563
- execDashboard.addWidgets(
564
- new cloudwatch.GaugeWidget({
565
- title: 'Eval Gate Pass Rate',
566
- metrics: [
567
- new cloudwatch.Metric({
568
- namespace: METRIC_NAMESPACE,
569
- metricName: 'EvalGatePassRate',
570
- statistic: 'Average',
571
- period: cdk.Duration.days(7),
572
- label: 'Eval Pass Rate (%)',
573
- }),
574
- ],
575
- width: 6,
576
- height: 6,
577
- leftYAxis: { min: 0, max: 100 },
578
- }),
579
- new cloudwatch.GraphWidget({
580
- title: 'Post-Merge Defect Rate',
581
- left: [
582
- // CI-fed (git trailers + revert scan in prism-ai-metrics.yml)
583
- new cloudwatch.Metric({
584
- namespace: METRIC_NAMESPACE,
585
- metricName: 'PostMergeDefectRate',
586
- statistic: 'Average',
587
- period: cdk.Duration.days(1),
588
- label: 'CI (per-PR)',
589
- }),
590
- // Attribution-derived: reverted AI commits over merged AI commits.
591
- // FILL guards both series: RevertedAICommits may not exist AT ALL
592
- // until the first revert is published (a metric with zero datums is
593
- // missing, not zero — unguarded math renders NaN). The IF guard
594
- // gaps buckets with no merged commits instead of dividing by zero.
595
- new cloudwatch.MathExpression({
596
- expression: 'IF(FILL(mergedAi, 0) > 0, 100 * FILL(revertedAi, 0) / FILL(mergedAi, 0))',
597
- usingMetrics: {
598
- revertedAi: new cloudwatch.Metric({
599
- namespace: METRIC_NAMESPACE,
600
- metricName: 'RevertedAICommits',
601
- statistic: 'Sum',
602
- period: cdk.Duration.days(1),
603
- }),
604
- mergedAi: new cloudwatch.Metric({
605
- namespace: METRIC_NAMESPACE,
606
- metricName: 'MergedAICommits',
607
- statistic: 'Sum',
608
- period: cdk.Duration.days(1),
609
- }),
610
- },
611
- label: 'Attribution (codeburn spans)',
612
- period: cdk.Duration.days(1),
613
- }),
614
- ],
615
- width: 6,
616
- height: 6,
617
- leftYAxis: { min: 0, label: 'Percent' },
618
- }),
619
- new cloudwatch.GraphWidget({
620
- title: 'AI Cost Trend (Weekly)',
621
- left: [
622
- new cloudwatch.Metric({
623
- namespace: METRIC_NAMESPACE,
624
- metricName: 'AICostUSD',
625
- statistic: 'Sum',
626
- period: cdk.Duration.days(7),
627
- label: 'Total Cost (USD)',
628
- }),
629
- ],
630
- width: 6,
631
- height: 6,
632
- leftYAxis: { min: 0, label: 'USD' },
633
- }),
634
- new cloudwatch.GraphWidget({
635
- title: 'Deployment Frequency (Weekly)',
636
- left: [
637
- new cloudwatch.Metric({
638
- namespace: METRIC_NAMESPACE,
639
- metricName: 'DeploymentFrequency',
640
- statistic: 'Sum',
641
- period: cdk.Duration.days(7),
642
- label: 'Deploys / Week',
643
- }),
644
- ],
645
- width: 6,
646
- height: 6,
647
- view: cloudwatch.GraphWidgetView.BAR,
648
- leftYAxis: { min: 0, label: 'Deployments' },
649
- }),
650
- );
651
-
652
- // =======================================================
653
- // CloudWatch Alarms
654
- // =======================================================
655
-
656
- // Alarm: AI acceptance rate dropping below 20%
657
- new cloudwatch.Alarm(this, 'AiAcceptanceRateLowAlarm', {
658
- alarmName: 'PRISM-D1-AIAcceptanceRate-Low',
659
- alarmDescription: 'AI acceptance rate has dropped below 20%, indicating potential issues with AI-generated code quality or review friction.',
660
- metric: new cloudwatch.Metric({
661
- namespace: METRIC_NAMESPACE,
662
- metricName: 'AIAcceptanceRate',
663
- statistic: 'Average',
664
- period: cdk.Duration.hours(6),
665
- }),
666
- threshold: 20,
667
- evaluationPeriods: 3,
668
- datapointsToAlarm: 2,
669
- comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD,
670
- treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
671
- });
672
-
673
- // Alarm: Eval gate pass rate dropping below 70%
674
- new cloudwatch.Alarm(this, 'EvalGatePassRateLowAlarm', {
675
- alarmName: 'PRISM-D1-EvalGatePassRate-Low',
676
- alarmDescription: 'Eval gate pass rate has dropped below 70%, indicating degraded quality in AI-generated outputs.',
677
- metric: new cloudwatch.Metric({
678
- namespace: METRIC_NAMESPACE,
679
- metricName: 'EvalGatePassRate',
680
- statistic: 'Average',
681
- period: cdk.Duration.hours(6),
682
- }),
683
- threshold: 70,
684
- evaluationPeriods: 3,
685
- datapointsToAlarm: 2,
686
- comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD,
687
- treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
688
- });
689
-
690
- // Alarm: Change failure rate exceeding 20%
691
- new cloudwatch.Alarm(this, 'ChangeFailureRateHighAlarm', {
692
- alarmName: 'PRISM-D1-ChangeFailureRate-High',
693
- alarmDescription: 'Change failure rate exceeds 20%, indicating deployment quality regression.',
694
- metric: new cloudwatch.Metric({
695
- namespace: METRIC_NAMESPACE,
696
- metricName: 'ChangeFailureRate',
697
- statistic: 'Average',
698
- period: cdk.Duration.hours(6),
699
- }),
700
- threshold: 20,
701
- evaluationPeriods: 3,
702
- datapointsToAlarm: 2,
703
- comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
704
- treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
705
- });
706
-
707
- // =======================================================
708
- // Team Dashboard: Eval Gate by Rubric (Pillar 2)
709
- // =======================================================
710
- teamDashboard.addWidgets(
711
- new cloudwatch.TextWidget({
712
- markdown: '### Eval Gate Quality by Rubric',
713
- width: 24,
714
- height: 1,
715
- }),
716
- );
717
-
718
- const rubricNames = ['code-quality', 'api-response-quality', 'agent-quality', 'security-compliance', 'spec-compliance'];
719
- teamDashboard.addWidgets(
720
- new cloudwatch.GraphWidget({
721
- title: 'Eval Pass Rate by Rubric',
722
- left: rubricNames.map((rubric) =>
723
- new cloudwatch.Metric({
724
- namespace: METRIC_NAMESPACE,
725
- metricName: 'EvalGatePassRateByRubric',
726
- dimensionsMap: { RubricName: rubric },
727
- statistic: 'Average',
728
- period: cdk.Duration.days(1),
729
- label: rubric,
730
- }),
731
- ),
732
- width: 12,
733
- height: 6,
734
- leftYAxis: { min: 0, max: 100, label: 'Pass Rate (%)' },
735
- }),
736
- new cloudwatch.GraphWidget({
737
- title: 'Eval Score Trend',
738
- left: [
739
- new cloudwatch.Metric({
740
- namespace: METRIC_NAMESPACE,
741
- metricName: 'EvalScore',
742
- statistic: 'Average',
743
- period: cdk.Duration.days(1),
744
- label: 'Avg Eval Score',
745
- }),
746
- ],
747
- width: 12,
748
- height: 6,
749
- leftYAxis: { min: 0, max: 1, label: 'Score (0-1)' },
750
- }),
751
- );
752
-
753
- // =======================================================
754
- // Team Dashboard: Guardrails & Safety (Pillar 4)
755
- // =======================================================
756
- teamDashboard.addWidgets(
757
- new cloudwatch.TextWidget({
758
- markdown: '### Guardrails & Safety',
759
- width: 24,
760
- height: 1,
761
- }),
762
- );
763
-
764
- const guardrailCategories = ['CONTENT_FILTER', 'DENIED_TOPIC', 'SENSITIVE_INFO', 'WORD_FILTER'];
765
- teamDashboard.addWidgets(
766
- new cloudwatch.GraphWidget({
767
- title: 'Guardrail Triggers by Category',
768
- left: guardrailCategories.map((category) =>
769
- new cloudwatch.Metric({
770
- namespace: METRIC_NAMESPACE,
771
- metricName: 'GuardrailTriggerCount',
772
- dimensionsMap: { TriggerCategory: category },
773
- statistic: 'Sum',
774
- period: DEFAULT_PERIOD,
775
- label: category,
776
- }),
777
- ),
778
- width: 12,
779
- height: 6,
780
- leftYAxis: { min: 0, label: 'Triggers' },
781
- view: cloudwatch.GraphWidgetView.BAR,
782
- }),
783
- new cloudwatch.GraphWidget({
784
- title: 'Guardrail Actions: Block vs Anonymize',
785
- left: [
786
- new cloudwatch.Metric({
787
- namespace: METRIC_NAMESPACE,
788
- metricName: 'GuardrailBlockCount',
789
- statistic: 'Sum',
790
- period: DEFAULT_PERIOD,
791
- label: 'Blocked',
792
- }),
793
- new cloudwatch.Metric({
794
- namespace: METRIC_NAMESPACE,
795
- metricName: 'GuardrailAnonymizeCount',
796
- statistic: 'Sum',
797
- period: DEFAULT_PERIOD,
798
- label: 'Anonymized',
799
- }),
800
- ],
801
- width: 12,
802
- height: 6,
803
- leftYAxis: { min: 0, label: 'Count' },
804
- }),
805
- );
806
-
807
- // =======================================================
808
- // Team Dashboard: MCP Tool Governance (Pillar 3)
809
- // =======================================================
810
- teamDashboard.addWidgets(
811
- new cloudwatch.TextWidget({
812
- markdown: '### MCP Tool Governance',
813
- width: 24,
814
- height: 1,
815
- }),
816
- );
817
-
818
- teamDashboard.addWidgets(
819
- new cloudwatch.GraphWidget({
820
- title: 'MCP Tool Call Volume',
821
- left: [
822
- new cloudwatch.Metric({
823
- namespace: METRIC_NAMESPACE,
824
- metricName: 'MCPToolCallCount',
825
- statistic: 'Sum',
826
- period: DEFAULT_PERIOD,
827
- label: 'Tool Calls',
828
- }),
829
- ],
830
- width: 12,
831
- height: 6,
832
- leftYAxis: { min: 0, label: 'Calls' },
833
- }),
834
- new cloudwatch.GraphWidget({
835
- title: 'MCP Auth Denied Rate',
836
- left: [
837
- new cloudwatch.Metric({
838
- namespace: METRIC_NAMESPACE,
839
- metricName: 'MCPAuthDeniedCount',
840
- statistic: 'Sum',
841
- period: DEFAULT_PERIOD,
842
- label: 'Denied Calls',
843
- }),
844
- ],
845
- width: 12,
846
- height: 6,
847
- leftYAxis: { min: 0, label: 'Denied' },
848
- }),
849
- );
850
-
851
- // =======================================================
852
- // Team Dashboard: Cost Intelligence (Pillar 5)
853
- // =======================================================
854
- teamDashboard.addWidgets(
855
- new cloudwatch.TextWidget({
856
- markdown: '### Cost Intelligence',
857
- width: 24,
858
- height: 1,
859
- }),
860
- );
861
-
862
- teamDashboard.addWidgets(
863
- new cloudwatch.GraphWidget({
864
- title: 'Bedrock Cost (USD)',
865
- left: [
866
- new cloudwatch.Metric({
867
- namespace: METRIC_NAMESPACE,
868
- metricName: 'BedrockCostUSD',
869
- statistic: 'Sum',
870
- period: cdk.Duration.days(1),
871
- label: 'Daily Cost ($)',
872
- }),
873
- ],
874
- width: 12,
875
- height: 6,
876
- leftYAxis: { min: 0, label: 'USD' },
877
- }),
878
- new cloudwatch.GraphWidget({
879
- title: 'Token Efficiency',
880
- left: [
881
- new cloudwatch.Metric({
882
- namespace: METRIC_NAMESPACE,
883
- metricName: 'TokenEfficiency',
884
- statistic: 'Average',
885
- period: cdk.Duration.days(1),
886
- label: 'Tokens per Line Changed',
887
- }),
888
- ],
889
- width: 12,
890
- height: 6,
891
- leftYAxis: { min: 0, label: 'Tokens/Line' },
892
- }),
893
- );
894
-
895
- // =======================================================
896
- // Team Dashboard: AI Attribution (Pillar 7)
897
- // =======================================================
898
- teamDashboard.addWidgets(
899
- new cloudwatch.TextWidget({
900
- markdown: '### AI Attribution & Quality',
901
- width: 24,
902
- height: 1,
903
- }),
904
- );
905
-
906
- teamDashboard.addWidgets(
907
- new cloudwatch.GraphWidget({
908
- title: 'Defect Rate: AI vs Human Code',
909
- left: [
910
- new cloudwatch.Metric({
911
- namespace: METRIC_NAMESPACE,
912
- metricName: 'PostMergeDefectRateAI',
913
- statistic: 'Average',
914
- period: cdk.Duration.days(1),
915
- label: 'AI Code Defect Rate (%)',
916
- }),
917
- new cloudwatch.Metric({
918
- namespace: METRIC_NAMESPACE,
919
- metricName: 'PostMergeDefectRateHuman',
920
- statistic: 'Average',
921
- period: cdk.Duration.days(1),
922
- label: 'Human Code Defect Rate (%)',
923
- }),
924
- ],
925
- width: 12,
926
- height: 6,
927
- leftYAxis: { min: 0, label: 'Defect Rate (%)' },
928
- }),
929
- new cloudwatch.GraphWidget({
930
- title: 'Spec-to-Code Hours',
931
- left: [
932
- new cloudwatch.Metric({
933
- namespace: METRIC_NAMESPACE,
934
- metricName: 'SpecToCodeHours',
935
- statistic: 'Average',
936
- period: cdk.Duration.days(1),
937
- label: 'Avg Hours',
938
- }),
939
- ],
940
- width: 12,
941
- height: 6,
942
- leftYAxis: { min: 0, label: 'Hours' },
943
- }),
944
- );
945
-
946
- // =======================================================
947
- // Team Dashboard: Security Agent Findings
948
- // =======================================================
949
- teamDashboard.addWidgets(
950
- new cloudwatch.TextWidget({
951
- markdown: '### Security Agent Findings',
952
- width: 24,
953
- height: 1,
954
- }),
955
- );
956
-
957
- teamDashboard.addWidgets(
958
- new cloudwatch.SingleValueWidget({
959
- title: 'Open Critical/High Findings',
960
- metrics: [
961
- new cloudwatch.Metric({
962
- namespace: METRIC_NAMESPACE,
963
- metricName: 'SecurityCriticalFindingCount',
964
- statistic: 'Sum',
965
- period: cdk.Duration.days(7),
966
- label: 'Critical + High',
967
- }),
968
- ],
969
- width: 6,
970
- height: 4,
971
- }),
972
- new cloudwatch.GraphWidget({
973
- title: 'Finding Trend by Severity',
974
- left: ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'].map((severity) =>
975
- new cloudwatch.Metric({
976
- namespace: METRIC_NAMESPACE,
977
- metricName: 'SecurityFindingCount',
978
- dimensionsMap: { Severity: severity },
979
- statistic: 'Sum',
980
- period: cdk.Duration.days(1),
981
- label: severity,
982
- }),
983
- ),
984
- width: 8,
985
- height: 4,
986
- leftYAxis: { min: 0, label: 'Findings' },
987
- }),
988
- new cloudwatch.SingleValueWidget({
989
- title: 'Avg Remediation (hrs)',
990
- metrics: [
991
- new cloudwatch.Metric({
992
- namespace: METRIC_NAMESPACE,
993
- metricName: 'SecurityRemediationTimeHours',
994
- statistic: 'Average',
995
- period: cdk.Duration.days(7),
996
- label: 'Hours',
997
- }),
998
- ],
999
- width: 4,
1000
- height: 4,
1001
- }),
1002
- );
1003
-
1004
- teamDashboard.addWidgets(
1005
- new cloudwatch.GraphWidget({
1006
- title: 'Findings: AI vs Human Code',
1007
- left: [
1008
- new cloudwatch.Metric({
1009
- namespace: METRIC_NAMESPACE,
1010
- metricName: 'SecurityFindingByOrigin',
1011
- dimensionsMap: { AIOrigin: 'ai-assisted' },
1012
- statistic: 'Sum',
1013
- period: cdk.Duration.days(1),
1014
- label: 'AI Code',
1015
- }),
1016
- new cloudwatch.Metric({
1017
- namespace: METRIC_NAMESPACE,
1018
- metricName: 'SecurityFindingByOrigin',
1019
- dimensionsMap: { AIOrigin: 'human' },
1020
- statistic: 'Sum',
1021
- period: cdk.Duration.days(1),
1022
- label: 'Human Code',
1023
- }),
1024
- ],
1025
- width: 12,
1026
- height: 6,
1027
- leftYAxis: { min: 0, label: 'Findings' },
1028
- }),
1029
- new cloudwatch.GraphWidget({
1030
- title: 'Remediation Time: AI vs Human',
1031
- left: [
1032
- new cloudwatch.Metric({
1033
- namespace: METRIC_NAMESPACE,
1034
- metricName: 'SecurityRemediationTimeHours',
1035
- dimensionsMap: { AIOrigin: 'ai-assisted' },
1036
- statistic: 'Average',
1037
- period: cdk.Duration.days(1),
1038
- label: 'AI Code Fix Time (hrs)',
1039
- }),
1040
- new cloudwatch.Metric({
1041
- namespace: METRIC_NAMESPACE,
1042
- metricName: 'SecurityRemediationTimeHours',
1043
- dimensionsMap: { AIOrigin: 'human' },
1044
- statistic: 'Average',
1045
- period: cdk.Duration.days(1),
1046
- label: 'Human Code Fix Time (hrs)',
1047
- }),
1048
- ],
1049
- width: 12,
1050
- height: 6,
1051
- leftYAxis: { min: 0, label: 'Hours' },
1052
- }),
1053
- );
1054
-
1055
- teamDashboard.addWidgets(
1056
- new cloudwatch.GraphWidget({
1057
- title: 'Findings by Phase',
1058
- left: ['design_review', 'code_review', 'pen_test'].map((phase) =>
1059
- new cloudwatch.Metric({
1060
- namespace: METRIC_NAMESPACE,
1061
- metricName: 'SecurityFindingCount',
1062
- dimensionsMap: { Phase: phase },
1063
- statistic: 'Sum',
1064
- period: cdk.Duration.days(7),
1065
- label: phase.replace('_', ' '),
1066
130
  }),
1067
- ),
1068
- width: 12,
131
+ ],
132
+ width: 8,
1069
133
  height: 6,
1070
- leftYAxis: { min: 0, label: 'Findings' },
1071
- view: cloudwatch.GraphWidgetView.BAR,
134
+ leftYAxis: { min: 0, max: 100, label: 'Percent' },
1072
135
  }),
1073
136
  new cloudwatch.GraphWidget({
1074
- title: 'Security Scan Volume',
137
+ title: 'AI Defect Trend (reverted / merged)',
1075
138
  left: [
1076
- new cloudwatch.Metric({
1077
- namespace: METRIC_NAMESPACE,
1078
- metricName: 'SecurityScanCount',
1079
- 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 (%)',
1080
149
  period: cdk.Duration.days(1),
1081
- label: 'Scans / Day',
1082
150
  }),
1083
151
  ],
1084
- width: 12,
152
+ right: [dailySum('RevertedAICommits', 'Reverted AI commits')],
153
+ width: 8,
1085
154
  height: 6,
1086
- 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,
1087
177
  }),
1088
178
  );
1089
179
 
1090
180
  // =======================================================
1091
- // Executive Dashboard: Security & Compliance section
181
+ // Dashboard 2: Executive Readout
1092
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
+
1093
207
  execDashboard.addWidgets(
1094
208
  new cloudwatch.TextWidget({
1095
- 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.',
1096
213
  width: 24,
1097
- height: 1,
214
+ height: 2,
1098
215
  }),
1099
216
  );
1100
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
+
1101
234
  execDashboard.addWidgets(
1102
- new cloudwatch.SingleValueWidget({
1103
- title: 'Guardrail Blocks (7d)',
1104
- metrics: [
1105
- new cloudwatch.Metric({
1106
- namespace: METRIC_NAMESPACE,
1107
- metricName: 'GuardrailBlockCount',
1108
- statistic: 'Sum',
1109
- period: cdk.Duration.days(7),
1110
- label: 'Blocks',
1111
- }),
235
+ new cloudwatch.GraphWidget({
236
+ title: 'AI vs Human Commits (Weekly)',
237
+ left: [
238
+ weeklySum('AICommits', 'AI commits'),
239
+ weeklySum('HumanCommits', 'Human commits'),
1112
240
  ],
1113
- width: 6,
1114
- 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' },
1115
253
  }),
1116
254
  new cloudwatch.GraphWidget({
1117
- title: 'Guardrail Trigger Trend',
255
+ title: 'Cost per Shipped Commit (Weekly)',
1118
256
  left: [
1119
- new cloudwatch.Metric({
1120
- namespace: METRIC_NAMESPACE,
1121
- metricName: 'GuardrailTriggerCount',
1122
- statistic: 'Sum',
1123
- period: cdk.Duration.days(1),
1124
- 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),
1125
267
  }),
1126
268
  ],
1127
- width: 9,
1128
- height: 4,
1129
- leftYAxis: { min: 0, label: 'Count' },
269
+ width: 8,
270
+ height: 6,
271
+ leftYAxis: { min: 0, label: 'USD' },
1130
272
  }),
1131
- new cloudwatch.SingleValueWidget({
1132
- title: 'MCP Auth Denied (7d)',
1133
- metrics: [
1134
- new cloudwatch.Metric({
1135
- namespace: METRIC_NAMESPACE,
1136
- metricName: 'MCPAuthDeniedCount',
1137
- 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 (%)',
1138
290
  period: cdk.Duration.days(7),
1139
- label: 'Denied',
1140
291
  }),
1141
292
  ],
1142
- width: 3,
1143
- height: 4,
293
+ width: 12,
294
+ height: 6,
295
+ leftYAxis: { min: 0, max: 100, label: 'Percent' },
1144
296
  }),
1145
- new cloudwatch.SingleValueWidget({
1146
- title: 'Exfiltration Alerts (7d)',
1147
- metrics: [
1148
- new cloudwatch.Metric({
1149
- namespace: METRIC_NAMESPACE,
1150
- metricName: 'ExfiltrationAlertCount',
1151
- 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 (%)',
1152
307
  period: cdk.Duration.days(7),
1153
- label: 'Alerts',
1154
308
  }),
1155
309
  ],
1156
- width: 6,
1157
- height: 4,
310
+ width: 12,
311
+ height: 6,
312
+ leftYAxis: { min: 0, label: 'Percent' },
1158
313
  }),
1159
314
  );
1160
315
 
1161
- // =======================================================
1162
- // Executive Dashboard: Cost Intelligence
1163
- // =======================================================
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
+
1164
321
  execDashboard.addWidgets(
1165
322
  new cloudwatch.TextWidget({
1166
- 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)`,
1167
327
  width: 24,
1168
328
  height: 1,
1169
329
  }),
1170
330
  );
1171
331
 
1172
- execDashboard.addWidgets(
1173
- new cloudwatch.GraphWidget({
1174
- title: 'Weekly Bedrock Cost',
1175
- left: [
1176
- new cloudwatch.Metric({
1177
- namespace: METRIC_NAMESPACE,
1178
- metricName: 'BedrockCostUSD',
1179
- statistic: 'Sum',
1180
- period: cdk.Duration.days(7),
1181
- label: 'Weekly Cost ($)',
1182
- }),
1183
- ],
1184
- width: 12,
1185
- height: 6,
1186
- leftYAxis: { min: 0, label: 'USD' },
1187
- 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),
1188
354
  }),
1189
- new cloudwatch.SingleValueWidget({
1190
- title: 'AI vs Human Defect Rate',
1191
- metrics: [
1192
- 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({
1193
378
  namespace: METRIC_NAMESPACE,
1194
- metricName: 'PostMergeDefectRateAI',
1195
- statistic: 'Average',
1196
- period: cdk.Duration.days(7),
1197
- label: 'AI Defect Rate (%)',
379
+ metricName: 'ChangeFailureCount',
380
+ statistic: 'Sum',
381
+ period: cdk.Duration.days(1),
1198
382
  }),
1199
- new cloudwatch.Metric({
383
+ deploys: new cloudwatch.Metric({
1200
384
  namespace: METRIC_NAMESPACE,
1201
- metricName: 'PostMergeDefectRateHuman',
1202
- statistic: 'Average',
1203
- period: cdk.Duration.days(7),
1204
- label: 'Human Defect Rate (%)',
385
+ metricName: 'DeploymentFrequency',
386
+ statistic: 'Sum',
387
+ period: cdk.Duration.days(1),
1205
388
  }),
1206
- ],
1207
- width: 6,
1208
- height: 6,
389
+ },
390
+ label: 'Change Failure Rate %',
391
+ period: cdk.Duration.days(1),
1209
392
  }),
1210
- );
393
+ threshold: 20,
394
+ evaluationPeriods: 3,
395
+ datapointsToAlarm: 2,
396
+ comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
397
+ treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
398
+ });
1211
399
 
1212
400
  // =======================================================
1213
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.
1214
416
  // =======================================================
1215
417
  const cisoDashboard = new cloudwatch.Dashboard(this, 'CISOComplianceDashboard', {
1216
418
  dashboardName: 'PRISM-D1-CISO-Compliance',
1217
419
  defaultInterval: cdk.Duration.days(30),
1218
420
  });
1219
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
+
1220
434
  cisoDashboard.addWidgets(
1221
435
  new cloudwatch.TextWidget({
1222
- 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**.',
1223
441
  width: 24,
1224
- height: 1,
442
+ height: 2,
1225
443
  }),
1226
444
  );
1227
445
 
1228
- cisoDashboard.addWidgets(
1229
- new cloudwatch.SingleValueWidget({
1230
- title: 'Open Critical Findings',
1231
- metrics: [
1232
- new cloudwatch.Metric({
1233
- namespace: METRIC_NAMESPACE,
1234
- metricName: 'SecurityCriticalFindingCount',
1235
- statistic: 'Sum',
1236
- period: cdk.Duration.days(30),
1237
- label: 'Critical + High (30d)',
1238
- }),
1239
- ],
1240
- width: 6,
1241
- height: 4,
1242
- }),
1243
- new cloudwatch.SingleValueWidget({
1244
- title: 'Avg Remediation Time',
1245
- metrics: [
1246
- new cloudwatch.Metric({
1247
- namespace: METRIC_NAMESPACE,
1248
- metricName: 'SecurityRemediationTimeHours',
1249
- statistic: 'Average',
1250
- period: cdk.Duration.days(30),
1251
- label: 'Hours (30d avg)',
1252
- }),
1253
- ],
1254
- width: 6,
1255
- height: 4,
1256
- }),
1257
- new cloudwatch.SingleValueWidget({
1258
- title: 'Security Scans Run',
1259
- metrics: [
1260
- new cloudwatch.Metric({
1261
- namespace: METRIC_NAMESPACE,
1262
- metricName: 'SecurityScanCount',
1263
- statistic: 'Sum',
1264
- period: cdk.Duration.days(30),
1265
- label: 'Scans (30d)',
1266
- }),
1267
- ],
1268
- width: 6,
1269
- height: 4,
1270
- }),
1271
- );
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));
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));
1272
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.
1273
465
  cisoDashboard.addWidgets(
1274
466
  new cloudwatch.TextWidget({
1275
- markdown: '### AI Code Risk Profile',
467
+ markdown: '### Runtime Governance',
1276
468
  width: 24,
1277
469
  height: 1,
1278
470
  }),
@@ -1280,112 +472,69 @@ export class DashboardStack extends cdk.Stack {
1280
472
 
1281
473
  cisoDashboard.addWidgets(
1282
474
  new cloudwatch.GraphWidget({
1283
- title: 'Security Findings: AI vs Human Code',
475
+ title: 'Guardrail Triggers vs Blocks',
1284
476
  left: [
1285
477
  new cloudwatch.Metric({
1286
478
  namespace: METRIC_NAMESPACE,
1287
- metricName: 'SecurityFindingByOrigin',
1288
- dimensionsMap: { AIOrigin: 'ai-assisted' },
479
+ metricName: 'GuardrailTriggerCount',
1289
480
  statistic: 'Sum',
1290
- period: cdk.Duration.days(7),
1291
- label: 'AI Code Findings',
481
+ period: cdk.Duration.days(1),
482
+ label: 'Triggers',
1292
483
  }),
1293
484
  new cloudwatch.Metric({
1294
485
  namespace: METRIC_NAMESPACE,
1295
- metricName: 'SecurityFindingByOrigin',
1296
- dimensionsMap: { AIOrigin: 'human' },
486
+ metricName: 'GuardrailBlockCount',
1297
487
  statistic: 'Sum',
1298
- period: cdk.Duration.days(7),
1299
- label: 'Human Code Findings',
488
+ period: cdk.Duration.days(1),
489
+ label: 'Blocks',
1300
490
  }),
1301
491
  ],
1302
- width: 12,
492
+ width: 8,
1303
493
  height: 6,
1304
- leftYAxis: { min: 0, label: 'Findings' },
494
+ leftYAxis: { min: 0, label: 'Count' },
1305
495
  }),
1306
496
  new cloudwatch.GraphWidget({
1307
- title: 'Remediation Time by Code Origin',
497
+ title: 'MCP Authorization Denials',
1308
498
  left: [
1309
499
  new cloudwatch.Metric({
1310
500
  namespace: METRIC_NAMESPACE,
1311
- metricName: 'SecurityRemediationTimeHours',
1312
- dimensionsMap: { AIOrigin: 'ai-assisted' },
1313
- statistic: 'Average',
1314
- period: cdk.Duration.days(7),
1315
- label: 'AI Code (hrs)',
1316
- }),
1317
- new cloudwatch.Metric({
1318
- namespace: METRIC_NAMESPACE,
1319
- metricName: 'SecurityRemediationTimeHours',
1320
- dimensionsMap: { AIOrigin: 'human' },
1321
- statistic: 'Average',
1322
- period: cdk.Duration.days(7),
1323
- label: 'Human Code (hrs)',
1324
- }),
1325
- ],
1326
- width: 12,
1327
- height: 6,
1328
- leftYAxis: { min: 0, label: 'Hours' },
1329
- }),
1330
- );
1331
-
1332
- cisoDashboard.addWidgets(
1333
- new cloudwatch.TextWidget({
1334
- markdown: '### Shift-Left Effectiveness',
1335
- width: 24,
1336
- height: 1,
1337
- }),
1338
- );
1339
-
1340
- cisoDashboard.addWidgets(
1341
- new cloudwatch.GraphWidget({
1342
- title: 'Findings by Phase (Monthly Trend)',
1343
- left: ['design_review', 'code_review', 'pen_test'].map((phase) =>
1344
- new cloudwatch.Metric({
1345
- namespace: METRIC_NAMESPACE,
1346
- metricName: 'SecurityFindingCount',
1347
- dimensionsMap: { Phase: phase },
501
+ metricName: 'MCPAuthDeniedCount',
1348
502
  statistic: 'Sum',
1349
- period: cdk.Duration.days(7),
1350
- label: phase.replace('_', ' '),
503
+ period: cdk.Duration.days(1),
504
+ label: 'Out-of-scope tool calls',
1351
505
  }),
1352
- ),
1353
- width: 12,
506
+ ],
507
+ width: 8,
1354
508
  height: 6,
1355
- leftYAxis: { min: 0, label: 'Findings' },
1356
- view: cloudwatch.GraphWidgetView.BAR,
509
+ leftYAxis: { min: 0, label: 'Count' },
1357
510
  }),
1358
511
  new cloudwatch.GraphWidget({
1359
- title: 'Guardrail + Exfiltration Trends',
512
+ title: 'Exfiltration Alerts',
1360
513
  left: [
1361
- new cloudwatch.Metric({
1362
- namespace: METRIC_NAMESPACE,
1363
- metricName: 'GuardrailBlockCount',
1364
- statistic: 'Sum',
1365
- period: cdk.Duration.days(7),
1366
- label: 'Guardrail Blocks',
1367
- }),
1368
514
  new cloudwatch.Metric({
1369
515
  namespace: METRIC_NAMESPACE,
1370
516
  metricName: 'ExfiltrationAlertCount',
1371
517
  statistic: 'Sum',
1372
- period: cdk.Duration.days(7),
1373
- label: 'Exfiltration Alerts',
1374
- }),
1375
- new cloudwatch.Metric({
1376
- namespace: METRIC_NAMESPACE,
1377
- metricName: 'MCPAuthDeniedCount',
1378
- statistic: 'Sum',
1379
- period: cdk.Duration.days(7),
1380
- label: 'MCP Auth Denied',
518
+ period: cdk.Duration.days(1),
519
+ label: 'Alerts',
1381
520
  }),
1382
521
  ],
1383
- width: 12,
522
+ width: 8,
1384
523
  height: 6,
1385
524
  leftYAxis: { min: 0, label: 'Count' },
1386
525
  }),
1387
526
  );
1388
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
+
1389
538
  new cdk.CfnOutput(this, 'CISODashboardUrl', {
1390
539
  value: `https://${this.region}.console.aws.amazon.com/cloudwatch/home?region=${this.region}#dashboards:name=PRISM-D1-CISO-Compliance`,
1391
540
  description: 'CISO Compliance Dashboard URL',
@@ -1461,13 +610,17 @@ export class DashboardStack extends cdk.Stack {
1461
610
  treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
1462
611
  });
1463
612
 
1464
- // 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.
1465
618
  new cloudwatch.Alarm(this, 'BedrockDailyCostHighAlarm', {
1466
- alarmName: 'PRISM-D1-BedrockDailyCost-High',
1467
- alarmDescription: 'Daily Bedrock cost exceeds $100 threshold.',
619
+ alarmName: 'PRISM-D1-AIDailyCost-High',
620
+ alarmDescription: 'Daily AI tooling cost exceeds the $100 threshold.',
1468
621
  metric: new cloudwatch.Metric({
1469
622
  namespace: METRIC_NAMESPACE,
1470
- metricName: 'BedrockCostUSD',
623
+ metricName: 'AICostUSD',
1471
624
  statistic: 'Sum',
1472
625
  period: cdk.Duration.days(1),
1473
626
  }),
@@ -1477,22 +630,10 @@ export class DashboardStack extends cdk.Stack {
1477
630
  treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
1478
631
  });
1479
632
 
1480
- // Alarm: Token efficiency below threshold
1481
- new cloudwatch.Alarm(this, 'TokenEfficiencyLowAlarm', {
1482
- alarmName: 'PRISM-D1-TokenEfficiency-Low',
1483
- alarmDescription: 'Token efficiency is low — high token consumption relative to code output.',
1484
- metric: new cloudwatch.Metric({
1485
- namespace: METRIC_NAMESPACE,
1486
- metricName: 'TokenEfficiency',
1487
- statistic: 'Average',
1488
- period: cdk.Duration.hours(6),
1489
- }),
1490
- threshold: 500,
1491
- evaluationPeriods: 3,
1492
- datapointsToAlarm: 2,
1493
- comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
1494
- treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
1495
- });
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.
1496
637
 
1497
638
  // Alarm: Exfiltration detection
1498
639
  new cloudwatch.Alarm(this, 'ExfiltrationAlertAlarm', {
@@ -1511,11 +652,13 @@ export class DashboardStack extends cdk.Stack {
1511
652
  });
1512
653
 
1513
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.
1514
658
  new cloudwatch.Alarm(this, 'AgentSuccessRateAlarm', {
1515
659
  metric: new cloudwatch.Metric({
1516
660
  namespace: METRIC_NAMESPACE,
1517
661
  metricName: 'AgentSuccessRate',
1518
- dimensionsMap: { TeamId: teamId, Repository: repository },
1519
662
  statistic: 'Average',
1520
663
  period: cdk.Duration.hours(1),
1521
664
  }),
@@ -1523,7 +666,7 @@ export class DashboardStack extends cdk.Stack {
1523
666
  evaluationPeriods: 3,
1524
667
  comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD,
1525
668
  alarmDescription: 'Agent success rate below 80% for 3 consecutive hours',
1526
- alarmName: `prism-d1-${teamId}-agent-success-rate`,
669
+ alarmName: 'prism-d1-agent-success-rate',
1527
670
  });
1528
671
 
1529
672
  // -------------------------------------------------------