@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.
- package/dist/assets/github-workflows/prism-ai-metrics.yml +83 -122
- package/dist/assets/github-workflows/prism-eval-gate-kiro.yml +66 -0
- package/dist/assets/github-workflows/prism-eval-gate.yml +1 -1
- package/dist/assets/gitlab-workflows/.gitlab-ci.yml +0 -3
- package/dist/assets/gitlab-workflows/prism-ai-metrics.yml +53 -56
- package/dist/assets/infra/bin/app.ts +2 -0
- package/dist/assets/infra/lib/constructs/otel-collector-construct.ts +62 -0
- package/dist/assets/infra/lib/dashboard-stack.ts +541 -1144
- package/dist/assets/infra/lib/lambda/attribution-metrics-publisher.ts +248 -0
- package/dist/assets/infra/lib/lambda/event-schema.ts +144 -0
- package/dist/assets/infra/lib/lambda/metrics-processor.ts +124 -36
- package/dist/assets/infra/lib/lambda/otel-receiver.ts +927 -16
- package/dist/assets/infra/lib/lambda/productivity-widget.ts +203 -0
- package/dist/assets/infra/lib/lambda/security-remediation-tracker.ts +17 -2
- package/dist/assets/infra/lib/lambda/velocity-widget.ts +1483 -0
- package/dist/assets/infra/lib/metrics-pipeline-stack.ts +191 -119
- package/dist/assets/infra/package.json +2 -1
- package/dist/assets/infra/tsconfig.json +9 -2
- package/dist/src/commands/bootstrapper/install-eval-harness.d.ts +8 -2
- package/dist/src/commands/bootstrapper/install-eval-harness.d.ts.map +1 -1
- package/dist/src/commands/bootstrapper/install-eval-harness.js +32 -32
- package/dist/src/commands/bootstrapper/install-eval-harness.js.map +1 -1
- package/dist/src/commands/bootstrapper/install-github-workflows.d.ts.map +1 -1
- package/dist/src/commands/bootstrapper/install-github-workflows.js +7 -2
- package/dist/src/commands/bootstrapper/install-github-workflows.js.map +1 -1
- package/dist/src/commands/bootstrapper/install-gitlab-workflows.d.ts.map +1 -1
- package/dist/src/commands/bootstrapper/install-gitlab-workflows.js +7 -1
- package/dist/src/commands/bootstrapper/install-gitlab-workflows.js.map +1 -1
- package/dist/src/commands/bootstrapper/setup-otel-sync.d.ts.map +1 -1
- package/dist/src/commands/bootstrapper/setup-otel-sync.js +3 -2
- package/dist/src/commands/bootstrapper/setup-otel-sync.js.map +1 -1
- package/dist/src/utils/region.d.ts +50 -0
- package/dist/src/utils/region.d.ts.map +1 -0
- package/dist/src/utils/region.js +62 -0
- package/dist/src/utils/region.js.map +1 -0
- package/package.json +2 -2
- package/dist/assets/eval-harness/README.md +0 -144
- package/dist/assets/github-workflows/README.md +0 -110
- package/dist/assets/github-workflows/prism-dora-weekly.yml +0 -334
- package/dist/assets/gitlab-workflows/prism-dora-weekly.yml +0 -181
- package/dist/assets/infra/lib/lambda/defect-correlator.ts +0 -142
- package/dist/assets/infra/lib/lambda/spec-to-code-calculator.ts +0 -123
- 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?:
|
|
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
|
-
// ---
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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: '
|
|
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
|
-
|
|
322
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
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: '
|
|
963
|
-
view: cloudwatch.GraphWidgetView.BAR,
|
|
134
|
+
leftYAxis: { min: 0, max: 100, label: 'Percent' },
|
|
964
135
|
}),
|
|
965
136
|
new cloudwatch.GraphWidget({
|
|
966
|
-
title: '
|
|
137
|
+
title: 'AI Defect Trend (reverted / merged)',
|
|
967
138
|
left: [
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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
|
-
|
|
152
|
+
right: [dailySum('RevertedAICommits', 'Reverted AI commits')],
|
|
153
|
+
width: 8,
|
|
977
154
|
height: 6,
|
|
978
|
-
leftYAxis: { min: 0, label: '
|
|
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
|
-
//
|
|
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:
|
|
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:
|
|
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.
|
|
995
|
-
title: '
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
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
|
-
|
|
1006
|
-
|
|
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: '
|
|
255
|
+
title: 'Cost per Shipped Commit (Weekly)',
|
|
1010
256
|
left: [
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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:
|
|
1020
|
-
height:
|
|
1021
|
-
leftYAxis: { min: 0, label: '
|
|
269
|
+
width: 8,
|
|
270
|
+
height: 6,
|
|
271
|
+
leftYAxis: { min: 0, label: 'USD' },
|
|
1022
272
|
}),
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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:
|
|
1035
|
-
height:
|
|
293
|
+
width: 12,
|
|
294
|
+
height: 6,
|
|
295
|
+
leftYAxis: { min: 0, max: 100, label: 'Percent' },
|
|
1036
296
|
}),
|
|
1037
|
-
new cloudwatch.
|
|
1038
|
-
title: '
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
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:
|
|
1049
|
-
height:
|
|
310
|
+
width: 12,
|
|
311
|
+
height: 6,
|
|
312
|
+
leftYAxis: { min: 0, label: 'Percent' },
|
|
1050
313
|
}),
|
|
1051
314
|
);
|
|
1052
315
|
|
|
1053
|
-
|
|
1054
|
-
|
|
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:
|
|
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
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
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
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
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: '
|
|
1087
|
-
statistic: '
|
|
1088
|
-
period: cdk.Duration.days(
|
|
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: '
|
|
1094
|
-
statistic: '
|
|
1095
|
-
period: cdk.Duration.days(
|
|
1096
|
-
label: 'Human Defect Rate (%)',
|
|
385
|
+
metricName: 'DeploymentFrequency',
|
|
386
|
+
statistic: 'Sum',
|
|
387
|
+
period: cdk.Duration.days(1),
|
|
1097
388
|
}),
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
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:
|
|
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:
|
|
442
|
+
height: 2,
|
|
1117
443
|
}),
|
|
1118
444
|
);
|
|
1119
445
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
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: '###
|
|
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: '
|
|
475
|
+
title: 'Guardrail Triggers vs Blocks',
|
|
1176
476
|
left: [
|
|
1177
477
|
new cloudwatch.Metric({
|
|
1178
478
|
namespace: METRIC_NAMESPACE,
|
|
1179
|
-
metricName: '
|
|
1180
|
-
dimensionsMap: { AIOrigin: 'ai-assisted' },
|
|
479
|
+
metricName: 'GuardrailTriggerCount',
|
|
1181
480
|
statistic: 'Sum',
|
|
1182
|
-
period: cdk.Duration.days(
|
|
1183
|
-
label: '
|
|
481
|
+
period: cdk.Duration.days(1),
|
|
482
|
+
label: 'Triggers',
|
|
1184
483
|
}),
|
|
1185
484
|
new cloudwatch.Metric({
|
|
1186
485
|
namespace: METRIC_NAMESPACE,
|
|
1187
|
-
metricName: '
|
|
1188
|
-
dimensionsMap: { AIOrigin: 'human' },
|
|
486
|
+
metricName: 'GuardrailBlockCount',
|
|
1189
487
|
statistic: 'Sum',
|
|
1190
|
-
period: cdk.Duration.days(
|
|
1191
|
-
label: '
|
|
488
|
+
period: cdk.Duration.days(1),
|
|
489
|
+
label: 'Blocks',
|
|
1192
490
|
}),
|
|
1193
491
|
],
|
|
1194
|
-
width:
|
|
492
|
+
width: 8,
|
|
1195
493
|
height: 6,
|
|
1196
|
-
leftYAxis: { min: 0, label: '
|
|
494
|
+
leftYAxis: { min: 0, label: 'Count' },
|
|
1197
495
|
}),
|
|
1198
496
|
new cloudwatch.GraphWidget({
|
|
1199
|
-
title: '
|
|
497
|
+
title: 'MCP Authorization Denials',
|
|
1200
498
|
left: [
|
|
1201
499
|
new cloudwatch.Metric({
|
|
1202
500
|
namespace: METRIC_NAMESPACE,
|
|
1203
|
-
metricName: '
|
|
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(
|
|
1242
|
-
label:
|
|
503
|
+
period: cdk.Duration.days(1),
|
|
504
|
+
label: 'Out-of-scope tool calls',
|
|
1243
505
|
}),
|
|
1244
|
-
|
|
1245
|
-
width:
|
|
506
|
+
],
|
|
507
|
+
width: 8,
|
|
1246
508
|
height: 6,
|
|
1247
|
-
leftYAxis: { min: 0, label: '
|
|
1248
|
-
view: cloudwatch.GraphWidgetView.BAR,
|
|
509
|
+
leftYAxis: { min: 0, label: 'Count' },
|
|
1249
510
|
}),
|
|
1250
511
|
new cloudwatch.GraphWidget({
|
|
1251
|
-
title: '
|
|
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(
|
|
1265
|
-
label: '
|
|
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:
|
|
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
|
|
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-
|
|
1359
|
-
alarmDescription: 'Daily
|
|
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: '
|
|
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
|
-
//
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
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:
|
|
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
|
}
|