@prism-d1/cli 1.0.79 → 1.0.81
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 +1 -0
- package/dist/assets/infra/lib/constructs/otel-collector-construct.ts +4 -0
- package/dist/assets/infra/lib/dashboard-stack.ts +378 -1235
- 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 +249 -35
- 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 +111 -119
- package/dist/assets/infra/package.json +2 -1
- package/dist/assets/infra/tsconfig.json +9 -2
- package/dist/src/commands/assessment/web.d.ts.map +1 -1
- package/dist/src/commands/assessment/web.js +25 -2
- package/dist/src/commands/assessment/web.js.map +1 -1
- 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 +46 -36
- package/dist/src/commands/bootstrapper/install-eval-harness.js.map +1 -1
- package/dist/src/commands/bootstrapper/install-git-hooks.d.ts.map +1 -1
- package/dist/src/commands/bootstrapper/install-git-hooks.js +10 -14
- package/dist/src/commands/bootstrapper/install-git-hooks.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-github-oidc.d.ts.map +1 -1
- package/dist/src/commands/bootstrapper/setup-github-oidc.js +50 -27
- package/dist/src/commands/bootstrapper/setup-github-oidc.js.map +1 -1
- package/dist/src/commands/bootstrapper/setup-gitlab-oidc.d.ts.map +1 -1
- package/dist/src/commands/bootstrapper/setup-gitlab-oidc.js +69 -25
- package/dist/src/commands/bootstrapper/setup-gitlab-oidc.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 +145 -53
- package/dist/src/commands/bootstrapper/setup-otel-sync.js.map +1 -1
- package/dist/src/commands/workshop/generate-demo-data.d.ts +41 -2
- package/dist/src/commands/workshop/generate-demo-data.d.ts.map +1 -1
- package/dist/src/commands/workshop/generate-demo-data.js +391 -167
- package/dist/src/commands/workshop/generate-demo-data.js.map +1 -1
- package/dist/src/commands/workshop/verify-setup.d.ts.map +1 -1
- package/dist/src/commands/workshop/verify-setup.js +75 -14
- package/dist/src/commands/workshop/verify-setup.js.map +1 -1
- package/dist/src/utils/exec.d.ts +56 -0
- package/dist/src/utils/exec.d.ts.map +1 -0
- package/dist/src/utils/exec.js +72 -0
- package/dist/src/utils/exec.js.map +1 -0
- 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/dist/src/utils/tempfile.d.ts +19 -0
- package/dist/src/utils/tempfile.d.ts.map +1 -0
- package/dist/src/utils/tempfile.js +32 -0
- package/dist/src/utils/tempfile.js.map +1 -0
- package/dist/src/utils/validate.d.ts +54 -0
- package/dist/src/utils/validate.d.ts.map +1 -0
- package/dist/src/utils/validate.js +143 -0
- package/dist/src/utils/validate.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
|
@@ -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
|
-
// ---
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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:
|
|
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:
|
|
68
|
+
height: 2,
|
|
53
69
|
}),
|
|
54
70
|
);
|
|
55
71
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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: '
|
|
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
|
-
|
|
153
|
-
|
|
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: '
|
|
105
|
+
leftYAxis: { min: 0, label: 'Commits' },
|
|
163
106
|
}),
|
|
164
107
|
new cloudwatch.GraphWidget({
|
|
165
|
-
title: 'AI
|
|
108
|
+
title: 'Merge Ratio: AI vs Human',
|
|
166
109
|
left: [
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
|
|
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:
|
|
187
|
-
|
|
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: '
|
|
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(
|
|
123
|
+
expression: 'FILL(IF(FILL(humanCommits, 0) > 0, 100 * FILL(mergedHuman, 0) / FILL(humanCommits, 0)), REPEAT)',
|
|
216
124
|
usingMetrics: {
|
|
217
|
-
|
|
218
|
-
|
|
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: '
|
|
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:
|
|
131
|
+
],
|
|
132
|
+
width: 8,
|
|
1069
133
|
height: 6,
|
|
1070
|
-
leftYAxis: { min: 0, label: '
|
|
1071
|
-
view: cloudwatch.GraphWidgetView.BAR,
|
|
134
|
+
leftYAxis: { min: 0, max: 100, label: 'Percent' },
|
|
1072
135
|
}),
|
|
1073
136
|
new cloudwatch.GraphWidget({
|
|
1074
|
-
title: '
|
|
137
|
+
title: 'AI Defect Trend (reverted / merged)',
|
|
1075
138
|
left: [
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
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
|
-
|
|
152
|
+
right: [dailySum('RevertedAICommits', 'Reverted AI commits')],
|
|
153
|
+
width: 8,
|
|
1085
154
|
height: 6,
|
|
1086
|
-
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,
|
|
1087
177
|
}),
|
|
1088
178
|
);
|
|
1089
179
|
|
|
1090
180
|
// =======================================================
|
|
1091
|
-
//
|
|
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:
|
|
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:
|
|
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.
|
|
1103
|
-
title: '
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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
|
-
|
|
1114
|
-
|
|
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: '
|
|
255
|
+
title: 'Cost per Shipped Commit (Weekly)',
|
|
1118
256
|
left: [
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
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:
|
|
1128
|
-
height:
|
|
1129
|
-
leftYAxis: { min: 0, label: '
|
|
269
|
+
width: 8,
|
|
270
|
+
height: 6,
|
|
271
|
+
leftYAxis: { min: 0, label: 'USD' },
|
|
1130
272
|
}),
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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:
|
|
1143
|
-
height:
|
|
293
|
+
width: 12,
|
|
294
|
+
height: 6,
|
|
295
|
+
leftYAxis: { min: 0, max: 100, label: 'Percent' },
|
|
1144
296
|
}),
|
|
1145
|
-
new cloudwatch.
|
|
1146
|
-
title: '
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
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:
|
|
1157
|
-
height:
|
|
310
|
+
width: 12,
|
|
311
|
+
height: 6,
|
|
312
|
+
leftYAxis: { min: 0, label: 'Percent' },
|
|
1158
313
|
}),
|
|
1159
314
|
);
|
|
1160
315
|
|
|
1161
|
-
|
|
1162
|
-
|
|
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:
|
|
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
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
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
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
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: '
|
|
1195
|
-
statistic: '
|
|
1196
|
-
period: cdk.Duration.days(
|
|
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: '
|
|
1202
|
-
statistic: '
|
|
1203
|
-
period: cdk.Duration.days(
|
|
1204
|
-
label: 'Human Defect Rate (%)',
|
|
385
|
+
metricName: 'DeploymentFrequency',
|
|
386
|
+
statistic: 'Sum',
|
|
387
|
+
period: cdk.Duration.days(1),
|
|
1205
388
|
}),
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
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:
|
|
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:
|
|
442
|
+
height: 2,
|
|
1225
443
|
}),
|
|
1226
444
|
);
|
|
1227
445
|
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
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: '###
|
|
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: '
|
|
475
|
+
title: 'Guardrail Triggers vs Blocks',
|
|
1284
476
|
left: [
|
|
1285
477
|
new cloudwatch.Metric({
|
|
1286
478
|
namespace: METRIC_NAMESPACE,
|
|
1287
|
-
metricName: '
|
|
1288
|
-
dimensionsMap: { AIOrigin: 'ai-assisted' },
|
|
479
|
+
metricName: 'GuardrailTriggerCount',
|
|
1289
480
|
statistic: 'Sum',
|
|
1290
|
-
period: cdk.Duration.days(
|
|
1291
|
-
label: '
|
|
481
|
+
period: cdk.Duration.days(1),
|
|
482
|
+
label: 'Triggers',
|
|
1292
483
|
}),
|
|
1293
484
|
new cloudwatch.Metric({
|
|
1294
485
|
namespace: METRIC_NAMESPACE,
|
|
1295
|
-
metricName: '
|
|
1296
|
-
dimensionsMap: { AIOrigin: 'human' },
|
|
486
|
+
metricName: 'GuardrailBlockCount',
|
|
1297
487
|
statistic: 'Sum',
|
|
1298
|
-
period: cdk.Duration.days(
|
|
1299
|
-
label: '
|
|
488
|
+
period: cdk.Duration.days(1),
|
|
489
|
+
label: 'Blocks',
|
|
1300
490
|
}),
|
|
1301
491
|
],
|
|
1302
|
-
width:
|
|
492
|
+
width: 8,
|
|
1303
493
|
height: 6,
|
|
1304
|
-
leftYAxis: { min: 0, label: '
|
|
494
|
+
leftYAxis: { min: 0, label: 'Count' },
|
|
1305
495
|
}),
|
|
1306
496
|
new cloudwatch.GraphWidget({
|
|
1307
|
-
title: '
|
|
497
|
+
title: 'MCP Authorization Denials',
|
|
1308
498
|
left: [
|
|
1309
499
|
new cloudwatch.Metric({
|
|
1310
500
|
namespace: METRIC_NAMESPACE,
|
|
1311
|
-
metricName: '
|
|
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(
|
|
1350
|
-
label:
|
|
503
|
+
period: cdk.Duration.days(1),
|
|
504
|
+
label: 'Out-of-scope tool calls',
|
|
1351
505
|
}),
|
|
1352
|
-
|
|
1353
|
-
width:
|
|
506
|
+
],
|
|
507
|
+
width: 8,
|
|
1354
508
|
height: 6,
|
|
1355
|
-
leftYAxis: { min: 0, label: '
|
|
1356
|
-
view: cloudwatch.GraphWidgetView.BAR,
|
|
509
|
+
leftYAxis: { min: 0, label: 'Count' },
|
|
1357
510
|
}),
|
|
1358
511
|
new cloudwatch.GraphWidget({
|
|
1359
|
-
title: '
|
|
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(
|
|
1373
|
-
label: '
|
|
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:
|
|
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
|
|
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-
|
|
1467
|
-
alarmDescription: 'Daily
|
|
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: '
|
|
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
|
-
//
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
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:
|
|
669
|
+
alarmName: 'prism-d1-agent-success-rate',
|
|
1527
670
|
});
|
|
1528
671
|
|
|
1529
672
|
// -------------------------------------------------------
|