@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
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical PRISM event schema — the contract between emitters and the pipeline.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS
|
|
5
|
+
* ---------------
|
|
6
|
+
* The event payload is a wide union of optional sections that producers fill
|
|
7
|
+
* selectively. Nothing validated it, and every silent failure in this pipeline
|
|
8
|
+
* traced back to that gap:
|
|
9
|
+
*
|
|
10
|
+
* - prism-dora-weekly.yml emitted `dora.lead_time_hours` while the processor
|
|
11
|
+
* read `lead_time_seconds`. The `!= null` guard skipped it, so the
|
|
12
|
+
* LeadTimeForChanges metric was never published and nothing errored.
|
|
13
|
+
* - It emitted `dora.mttr_hours` against `mttr_seconds`, so the MTTR metric
|
|
14
|
+
* was never published at all.
|
|
15
|
+
* - `ai_dora.ai_adoption_rate` matched no key the processor consumed.
|
|
16
|
+
* - The `pr` section was emitted by producers for months but missing from the
|
|
17
|
+
* processor's persisted-section list, so every field was dropped at write.
|
|
18
|
+
* - GitHub emitted `pr` while GitLab emitted `mr` for the same concept.
|
|
19
|
+
*
|
|
20
|
+
* None of those threw. They produced plausible-looking dashboards with missing
|
|
21
|
+
* data. Declaring the shape here and validating against it converts that class
|
|
22
|
+
* of failure from silent to loud.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** Sections persisted to the events table. Anything not listed is DROPPED. */
|
|
26
|
+
export const PERSISTED_SECTIONS = [
|
|
27
|
+
'metric',
|
|
28
|
+
'ai_context',
|
|
29
|
+
'dora',
|
|
30
|
+
'ai_dora',
|
|
31
|
+
'eval',
|
|
32
|
+
'guardrail',
|
|
33
|
+
'mcp_tool_call',
|
|
34
|
+
'agent',
|
|
35
|
+
'security',
|
|
36
|
+
'security_agent_finding',
|
|
37
|
+
'security_remediation',
|
|
38
|
+
'pr',
|
|
39
|
+
] as const;
|
|
40
|
+
|
|
41
|
+
export type PersistedSection = (typeof PERSISTED_SECTIONS)[number];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Known field names per section. Used by validateEventShape to flag fields a
|
|
45
|
+
* producer emits that no consumer reads — the `lead_time_hours` class of bug.
|
|
46
|
+
*
|
|
47
|
+
* This is intentionally a denylist-by-omission: unknown fields are WARNED about,
|
|
48
|
+
* not rejected, so adding a field is never a hard outage. The CI guard
|
|
49
|
+
* (scripts/check-metric-coverage.ts) is what fails the build.
|
|
50
|
+
*/
|
|
51
|
+
export const SECTION_FIELDS: Record<PersistedSection, readonly string[]> = {
|
|
52
|
+
metric: ['name', 'value', 'unit'],
|
|
53
|
+
ai_context: ['tool', 'model', 'origin', 'origin_source'],
|
|
54
|
+
dora: [
|
|
55
|
+
'deployment_frequency',
|
|
56
|
+
'lead_time_seconds',
|
|
57
|
+
'change_failure_rate',
|
|
58
|
+
'mttr_seconds',
|
|
59
|
+
'is_failure_fix',
|
|
60
|
+
],
|
|
61
|
+
ai_dora: ['eval_gate_pass_rate'],
|
|
62
|
+
eval: ['eval_id', 'rubric', 'result', 'score', 'findings', 'high_findings', 'pr_number', 'criterion_scores'],
|
|
63
|
+
guardrail: ['guardrail_id', 'trigger_category', 'trigger_type', 'action_taken', 'agent_name'],
|
|
64
|
+
mcp_tool_call: ['session_id', 'client_id', 'tool_name', 'scopes', 'authorized', 'risk_level', 'result_status', 'duration_ms'],
|
|
65
|
+
agent: ['agent_name', 'status', 'duration_ms', 'steps_taken', 'tools_invoked', 'tokens_used', 'guardrails_triggered'],
|
|
66
|
+
security: ['alert_type', 'table_name', 'principal_arn', 'read_count'],
|
|
67
|
+
security_agent_finding: [
|
|
68
|
+
'finding_id', 'phase', 'severity', 'cvss_score', 'title', 'description',
|
|
69
|
+
'category', 'cwe_id', 'exploit_validated', 'remediation_guidance',
|
|
70
|
+
'compliance_mappings', 'ai_origin', 'commit_shas', 'pr_number',
|
|
71
|
+
'commit_sha', 'spec_ref', 'environment', 'found_at', 'remediated_at',
|
|
72
|
+
],
|
|
73
|
+
security_remediation: [
|
|
74
|
+
'finding_id', 'severity', 'remediation_time_hours', 'remediated_by_origin',
|
|
75
|
+
'remediated_by_origin_source', 'fix_commit_shas', 'fix_pr_number', 'finding_phase',
|
|
76
|
+
],
|
|
77
|
+
pr: [
|
|
78
|
+
'number', 'author', 'reviews_approved', 'reviews_changes_requested',
|
|
79
|
+
'total_commits', 'commit_shas',
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Units matter: these fields have caused real bugs by carrying the wrong unit
|
|
85
|
+
* under a plausible name. A producer emitting `lead_time_hours` is emitting a
|
|
86
|
+
* field this pipeline does not read — the suffix is load-bearing.
|
|
87
|
+
*/
|
|
88
|
+
export const UNIT_SUFFIXED_FIELDS = [
|
|
89
|
+
'lead_time_seconds',
|
|
90
|
+
'mttr_seconds',
|
|
91
|
+
'remediation_time_hours',
|
|
92
|
+
'duration_ms',
|
|
93
|
+
] as const;
|
|
94
|
+
|
|
95
|
+
export interface ShapeWarning {
|
|
96
|
+
section: string;
|
|
97
|
+
field?: string;
|
|
98
|
+
message: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Non-throwing shape validation. Returns warnings for the processor to log so
|
|
103
|
+
* field drift shows up in CloudWatch Logs Insights instead of vanishing.
|
|
104
|
+
*
|
|
105
|
+
* Deliberately does NOT reject: dropping a real event because a producer added
|
|
106
|
+
* a field would be a worse failure than logging it.
|
|
107
|
+
*/
|
|
108
|
+
export function validateEventShape(detail: Record<string, unknown>): ShapeWarning[] {
|
|
109
|
+
const warnings: ShapeWarning[] = [];
|
|
110
|
+
const known = new Set<string>(PERSISTED_SECTIONS);
|
|
111
|
+
|
|
112
|
+
for (const [key, value] of Object.entries(detail)) {
|
|
113
|
+
// Envelope fields live alongside sections and are handled separately.
|
|
114
|
+
if (['team_id', 'repo', 'timestamp', 'prism_level'].includes(key)) continue;
|
|
115
|
+
|
|
116
|
+
if (!known.has(key)) {
|
|
117
|
+
warnings.push({
|
|
118
|
+
section: key,
|
|
119
|
+
message: `section '${key}' is not in PERSISTED_SECTIONS and will be DROPPED at write time`,
|
|
120
|
+
});
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
125
|
+
const allowed = SECTION_FIELDS[key as PersistedSection];
|
|
126
|
+
for (const field of Object.keys(value as Record<string, unknown>)) {
|
|
127
|
+
if (!allowed.includes(field)) {
|
|
128
|
+
// Near-miss on a unit suffix is the highest-signal case.
|
|
129
|
+
const nearMiss = UNIT_SUFFIXED_FIELDS.find(
|
|
130
|
+
f => f.split('_').slice(0, -1).join('_') === field.split('_').slice(0, -1).join('_'),
|
|
131
|
+
);
|
|
132
|
+
warnings.push({
|
|
133
|
+
section: key,
|
|
134
|
+
field,
|
|
135
|
+
message: nearMiss
|
|
136
|
+
? `'${key}.${field}' is not consumed — did you mean '${nearMiss}'? Unit suffixes are load-bearing.`
|
|
137
|
+
: `'${key}.${field}' is not consumed by any reader`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return warnings;
|
|
144
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PERSISTED_SECTIONS, validateEventShape } from './event-schema';
|
|
1
2
|
import {
|
|
2
3
|
DynamoDBClient,
|
|
3
4
|
PutItemCommand,
|
|
@@ -22,6 +23,28 @@ interface DoraMetrics {
|
|
|
22
23
|
lead_time_seconds: number | null;
|
|
23
24
|
change_failure_rate: number | null;
|
|
24
25
|
mttr_seconds: number | null;
|
|
26
|
+
/**
|
|
27
|
+
* Per-PR failure-fix label (title matched revert|hotfix|rollback). This is the
|
|
28
|
+
* change-failure-rate NUMERATOR as a fact; the rate and MTTR are computed at
|
|
29
|
+
* query time by the dashboard widgets over the selected window. Emitting a
|
|
30
|
+
* pre-computed rate per PR was meaningless (0% or 100%) and a pre-computed
|
|
31
|
+
* weekly rate hardcoded a 7-day window the dashboard could not override.
|
|
32
|
+
*/
|
|
33
|
+
is_failure_fix?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Per-PR facts from prism-ai-metrics.yml. Persisted so the dashboard can
|
|
38
|
+
* aggregate at query time and resolve AI origin by joining commit_shas against
|
|
39
|
+
* the attribution store.
|
|
40
|
+
*/
|
|
41
|
+
interface PrDetail {
|
|
42
|
+
number: number;
|
|
43
|
+
author: string;
|
|
44
|
+
reviews_approved: number;
|
|
45
|
+
reviews_changes_requested: number;
|
|
46
|
+
total_commits?: number;
|
|
47
|
+
commit_shas?: string[];
|
|
25
48
|
}
|
|
26
49
|
|
|
27
50
|
interface AiDoraMetrics {
|
|
@@ -95,7 +118,19 @@ interface SecurityAgentFinding {
|
|
|
95
118
|
cwe_id: string | null;
|
|
96
119
|
exploit_validated: boolean;
|
|
97
120
|
compliance_mappings: string[];
|
|
98
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Trailer-derived origin. Legacy — emitters now send `commit_shas` instead
|
|
123
|
+
* and dashboards resolve origin at render time against the attribution
|
|
124
|
+
* store. Retained so events emitted before the cutover still parse.
|
|
125
|
+
*/
|
|
126
|
+
ai_origin?: string;
|
|
127
|
+
/**
|
|
128
|
+
* Commit SHAs the finding's PR introduced. Immutable facts — the deferred
|
|
129
|
+
* join reads these at query time, which is correct even when the PR merged
|
|
130
|
+
* before `codeburn sync --attribution` had run for those commits.
|
|
131
|
+
*/
|
|
132
|
+
commit_shas?: string[];
|
|
133
|
+
pr_number?: number;
|
|
99
134
|
spec_ref: string | null;
|
|
100
135
|
found_at: string;
|
|
101
136
|
remediated_at: string | null;
|
|
@@ -105,7 +140,17 @@ interface SecurityRemediationDetail {
|
|
|
105
140
|
finding_id: string;
|
|
106
141
|
severity: string;
|
|
107
142
|
remediation_time_hours: number;
|
|
143
|
+
/**
|
|
144
|
+
* Trailer-derived verdict for who fixed the finding. Legacy — reads 'human'
|
|
145
|
+
* for everything once git hooks are removed. Dashboards prefer
|
|
146
|
+
* `fix_commit_shas` and only fall back to this. See the deferred-join note
|
|
147
|
+
* on SecurityAgentFinding.commit_shas.
|
|
148
|
+
*/
|
|
108
149
|
remediated_by_origin: string;
|
|
150
|
+
remediated_by_origin_source?: string;
|
|
151
|
+
/** Fix PR's commit SHAs — joined against the attribution store at render time. */
|
|
152
|
+
fix_commit_shas?: string[];
|
|
153
|
+
fix_pr_number?: number | null;
|
|
109
154
|
finding_phase: string;
|
|
110
155
|
}
|
|
111
156
|
|
|
@@ -134,6 +179,7 @@ interface MetricDetail {
|
|
|
134
179
|
security?: SecurityDetail;
|
|
135
180
|
security_agent_finding?: SecurityAgentFinding;
|
|
136
181
|
security_remediation?: SecurityRemediationDetail;
|
|
182
|
+
pr?: PrDetail;
|
|
137
183
|
}
|
|
138
184
|
|
|
139
185
|
interface EventBridgeEvent {
|
|
@@ -155,13 +201,10 @@ const METRIC_NAMESPACE = process.env.METRIC_NAMESPACE ?? 'PRISM/D1/Velocity';
|
|
|
155
201
|
// ---- Handler ----
|
|
156
202
|
|
|
157
203
|
export async function handler(event: EventBridgeEvent): Promise<void> {
|
|
158
|
-
console.log('[metrics-processor] Received event:', JSON.stringify(event, null, 2));
|
|
159
|
-
|
|
160
204
|
const detailType = event['detail-type'];
|
|
161
205
|
const detail = event.detail;
|
|
162
206
|
|
|
163
207
|
console.log(`[metrics-processor] detail-type=${detailType} team_id=${detail?.team_id} repo=${detail?.repo} timestamp=${detail?.timestamp}`);
|
|
164
|
-
console.log(`[metrics-processor] dora=${JSON.stringify(detail?.dora)} ai_dora=${JSON.stringify(detail?.ai_dora)} metric=${JSON.stringify(detail?.metric)}`);
|
|
165
208
|
|
|
166
209
|
if (!detail.team_id) {
|
|
167
210
|
console.log('[metrics-processor] No team_id provided, defaulting to "no_team"');
|
|
@@ -179,21 +222,27 @@ export async function handler(event: EventBridgeEvent): Promise<void> {
|
|
|
179
222
|
publishCloudWatchMetrics(detailType, detail),
|
|
180
223
|
]);
|
|
181
224
|
|
|
225
|
+
const labels = ['writeEventToDynamo', 'writeMetadataToDynamo', 'publishCloudWatchMetrics'];
|
|
182
226
|
results.forEach((result, idx) => {
|
|
183
|
-
|
|
184
|
-
if (result.status === 'fulfilled') {
|
|
185
|
-
console.log(`[metrics-processor] ${labels[idx]} succeeded`);
|
|
186
|
-
} else {
|
|
227
|
+
if (result.status === 'rejected') {
|
|
187
228
|
console.error(`[metrics-processor] ${labels[idx]} FAILED:`, result.reason);
|
|
188
229
|
}
|
|
189
230
|
});
|
|
190
231
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
232
|
+
// Only DynamoDB write failures are retryable — the events table is the
|
|
233
|
+
// source of truth and DDB writes are idempotent (same pk/sk overwrites).
|
|
234
|
+
// CloudWatch metric publishing is best-effort: retrying a whole invocation
|
|
235
|
+
// because PutMetricData throttled creates a self-amplifying retry storm
|
|
236
|
+
// (more retries -> more PutMetricData calls -> more throttling). This
|
|
237
|
+
// exact loop ran at ~108M invocations/day in July 2026 and wrote 445 GB
|
|
238
|
+
// of logs before stopping. Metrics lost to throttling are an acceptable
|
|
239
|
+
// gap; events lost from DDB are not.
|
|
240
|
+
const ddbFailures = results.slice(0, 2).filter((r) => r.status === 'rejected');
|
|
241
|
+
if (ddbFailures.length > 0) {
|
|
242
|
+
throw new Error(`${ddbFailures.length} DynamoDB write(s) failed — retrying event`);
|
|
194
243
|
}
|
|
195
244
|
|
|
196
|
-
console.log(`[metrics-processor]
|
|
245
|
+
console.log(`[metrics-processor] Processed ${detailType} for ${detail.team_id}/${detail.repo}`);
|
|
197
246
|
}
|
|
198
247
|
|
|
199
248
|
// ---- DynamoDB events ----
|
|
@@ -211,22 +260,42 @@ async function writeEventToDynamo(
|
|
|
211
260
|
prism_level: detail.prism_level ?? '1',
|
|
212
261
|
};
|
|
213
262
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
263
|
+
// Persist every payload section — the events table is the source of truth
|
|
264
|
+
// for DDB-backed dashboard panels and replay. (Previously only the first
|
|
265
|
+
// four were stored, silently dropping eval/guardrail/mcp/agent/security
|
|
266
|
+
// payloads from the durable record.)
|
|
267
|
+
// Shape validation: logs field drift instead of letting it vanish. See
|
|
268
|
+
// event-schema.ts for the failures that motivated this.
|
|
269
|
+
for (const w of validateEventShape(detail as unknown as Record<string, unknown>)) {
|
|
270
|
+
console.warn(`[schema] ${w.message}`);
|
|
219
271
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
272
|
+
|
|
273
|
+
const sections = PERSISTED_SECTIONS;
|
|
274
|
+
for (const key of sections) {
|
|
275
|
+
if ((detail as Record<string, unknown>)[key]) {
|
|
276
|
+
data[key] = (detail as Record<string, unknown>)[key];
|
|
277
|
+
}
|
|
225
278
|
}
|
|
226
279
|
|
|
280
|
+
// Sort key. Events that fan out PER FINDING (security findings, and one
|
|
281
|
+
// remediation per finding resolved by a merged PR) all carry the SAME
|
|
282
|
+
// timestamp — the scan time or the PR merge time. With sk = timestamp alone
|
|
283
|
+
// they collide on (pk, sk) and silently overwrite each other: a PR fixing
|
|
284
|
+
// three findings stored exactly one remediation event. Appending the
|
|
285
|
+
// finding id disambiguates them while preserving the timestamp prefix, so
|
|
286
|
+
// `sk BETWEEN :from AND :to` range queries and lexicographic time ordering
|
|
287
|
+
// still work. Follows the existing `SPAN#${ts}#${spanId}` convention in
|
|
288
|
+
// otel-receiver.ts. Pre-existing items keep bare-timestamp sks; readers
|
|
289
|
+
// split on '#', which is a no-op for those.
|
|
290
|
+
const findingDiscriminator =
|
|
291
|
+
detail.security_agent_finding?.finding_id ?? detail.security_remediation?.finding_id;
|
|
292
|
+
const sortKey = findingDiscriminator
|
|
293
|
+
? `${detail.timestamp}#${findingDiscriminator}`
|
|
294
|
+
: detail.timestamp;
|
|
295
|
+
|
|
227
296
|
const item: Record<string, { S?: string; N?: string }> = {
|
|
228
297
|
pk: { S: `${detail.team_id}#${detail.repo}` },
|
|
229
|
-
sk: { S:
|
|
298
|
+
sk: { S: sortKey },
|
|
230
299
|
detail_type: { S: detailType },
|
|
231
300
|
data: { S: JSON.stringify(data) },
|
|
232
301
|
ttl: { N: ttl.toString() },
|
|
@@ -328,9 +397,6 @@ async function publishCloudWatchMetrics(
|
|
|
328
397
|
detailType: string,
|
|
329
398
|
detail: MetricDetail,
|
|
330
399
|
): Promise<void> {
|
|
331
|
-
console.log(`[publishCloudWatchMetrics] Starting for ${detailType}, namespace=${METRIC_NAMESPACE}`);
|
|
332
|
-
console.log(`[publishCloudWatchMetrics] dora fields: deployment_frequency=${detail.dora?.deployment_frequency} lead_time_seconds=${detail.dora?.lead_time_seconds} change_failure_rate=${detail.dora?.change_failure_rate} mttr_seconds=${detail.dora?.mttr_seconds}`);
|
|
333
|
-
console.log(`[publishCloudWatchMetrics] ai_dora fields: ai_acceptance_rate=${detail.ai_dora?.ai_acceptance_rate} ai_to_merge_ratio=${detail.ai_dora?.ai_to_merge_ratio} eval_gate_pass_rate=${detail.ai_dora?.eval_gate_pass_rate}`);
|
|
334
400
|
|
|
335
401
|
const sharedDimensions = [
|
|
336
402
|
{ Name: 'TeamId', Value: detail.team_id },
|
|
@@ -430,17 +496,37 @@ async function publishCloudWatchMetrics(
|
|
|
430
496
|
Timestamp: metricTimestamp,
|
|
431
497
|
});
|
|
432
498
|
}
|
|
499
|
+
// Change-failure NUMERATOR as a counter. The CFR alarm divides this by
|
|
500
|
+
// DeploymentFrequency via metric math, so the ratio is evaluated over the
|
|
501
|
+
// alarm's own window instead of a window baked in at emit time. Emitted on
|
|
502
|
+
// every merged PR (0 or 1) so the alarm sees a real zero during healthy
|
|
503
|
+
// periods rather than sitting in INSUFFICIENT_DATA.
|
|
504
|
+
if (detail.dora.is_failure_fix != null) {
|
|
505
|
+
metricData.push({
|
|
506
|
+
MetricName: 'ChangeFailureCount',
|
|
507
|
+
Value: detail.dora.is_failure_fix ? 1 : 0,
|
|
508
|
+
Unit: StandardUnit.Count,
|
|
509
|
+
Dimensions: sharedDimensions,
|
|
510
|
+
Timestamp: metricTimestamp,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
433
513
|
}
|
|
434
514
|
|
|
435
515
|
// AI-DORA metrics — scale 0–1 ratios to 0–100 for CloudWatch Percent unit
|
|
436
516
|
if (detail.ai_dora) {
|
|
517
|
+
// AIAcceptanceRate and AIToMergeRatio were removed: neither had any
|
|
518
|
+
// consumer. No widget read them, and the dashboards' AI-vs-human graphs are
|
|
519
|
+
// driven by attribution metrics (AICommits / MergedAICommits / ...) via
|
|
520
|
+
// metric math instead. AIAcceptanceRate was additionally unsalvageable —
|
|
521
|
+
// gated on a trailer-derived ai_ratio, and defaulting to 100% whenever a PR
|
|
522
|
+
// had no reviews, which rewarded skipping review.
|
|
437
523
|
const aiDoraMap: Array<[string, number | null, StandardUnit, boolean]> = [
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
524
|
+
// EvalGatePassRate is the only survivor: it has a live producer
|
|
525
|
+
// (prism-agent-eval.yml) and a live consumer (the eval-gate alarm).
|
|
526
|
+
// SpecToCodeHours and PostMergeDefectRate were emitted by the
|
|
527
|
+
// spec-to-code-calculator and defect-correlator Lambdas, both now deleted
|
|
528
|
+
// as permanent no-ops. AITestCoverageDelta never had a producer at all.
|
|
442
529
|
['EvalGatePassRate', detail.ai_dora.eval_gate_pass_rate, StandardUnit.Percent, true],
|
|
443
|
-
['AITestCoverageDelta', detail.ai_dora.ai_test_coverage_delta, StandardUnit.Percent, true],
|
|
444
530
|
];
|
|
445
531
|
|
|
446
532
|
// Token & cost metrics come from git-trailer PR sums by default. When the
|
|
@@ -691,6 +777,13 @@ async function publishCloudWatchMetrics(
|
|
|
691
777
|
// Security remediation metrics
|
|
692
778
|
if (detail.security_remediation) {
|
|
693
779
|
const remediation = detail.security_remediation;
|
|
780
|
+
// NOTE: the AIOrigin dimension here is trailer-derived and therefore
|
|
781
|
+
// legacy — dashboards resolve "who fixed it" from fix_commit_shas via the
|
|
782
|
+
// deferred attribution join instead. It is kept, and kept UNCONDITIONAL,
|
|
783
|
+
// on purpose: emitting this metric sometimes with AIOrigin and sometimes
|
|
784
|
+
// without would create a partial dimension set, which is exactly the
|
|
785
|
+
// failure that left three CISO widgets permanently empty. Full set or
|
|
786
|
+
// nothing. The dimensionless copy below is what the SLA panels read.
|
|
694
787
|
metricData.push({
|
|
695
788
|
MetricName: 'SecurityRemediationTimeHours',
|
|
696
789
|
Value: remediation.remediation_time_hours,
|
|
@@ -721,15 +814,11 @@ async function publishCloudWatchMetrics(
|
|
|
721
814
|
}
|
|
722
815
|
|
|
723
816
|
console.log(`[publishCloudWatchMetrics] Publishing ${metricData.length} metric data points`);
|
|
724
|
-
metricData.forEach((m, i) => {
|
|
725
|
-
console.log(`[publishCloudWatchMetrics] [${i}] ${m.MetricName}=${m.Value} unit=${m.Unit} dims=${JSON.stringify(m.Dimensions)}`);
|
|
726
|
-
});
|
|
727
817
|
|
|
728
818
|
// CloudWatch accepts max 1000 metric data points per call; batch in chunks of 25
|
|
729
819
|
const batchSize = 25;
|
|
730
820
|
for (let i = 0; i < metricData.length; i += batchSize) {
|
|
731
821
|
const batch = metricData.slice(i, i + batchSize);
|
|
732
|
-
console.log(`[publishCloudWatchMetrics] Sending batch ${Math.floor(i / batchSize) + 1} with ${batch.length} metrics`);
|
|
733
822
|
try {
|
|
734
823
|
await cloudwatchClient.send(
|
|
735
824
|
new PutMetricDataCommand({
|
|
@@ -737,7 +826,6 @@ async function publishCloudWatchMetrics(
|
|
|
737
826
|
MetricData: batch,
|
|
738
827
|
}),
|
|
739
828
|
);
|
|
740
|
-
console.log(`[publishCloudWatchMetrics] Batch ${Math.floor(i / batchSize) + 1} sent successfully`);
|
|
741
829
|
} catch (err) {
|
|
742
830
|
console.error(`[publishCloudWatchMetrics] Batch ${Math.floor(i / batchSize) + 1} FAILED:`, err);
|
|
743
831
|
throw err;
|