@prism-d1/cli 1.0.77 → 1.0.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/github-workflows/prism-ai-metrics.yml +83 -122
- package/dist/assets/github-workflows/prism-eval-gate-kiro.yml +66 -0
- package/dist/assets/github-workflows/prism-eval-gate.yml +1 -1
- package/dist/assets/gitlab-workflows/.gitlab-ci.yml +0 -3
- package/dist/assets/gitlab-workflows/prism-ai-metrics.yml +53 -56
- package/dist/assets/infra/bin/app.ts +2 -0
- package/dist/assets/infra/lib/constructs/otel-collector-construct.ts +62 -0
- package/dist/assets/infra/lib/dashboard-stack.ts +541 -1144
- package/dist/assets/infra/lib/lambda/attribution-metrics-publisher.ts +248 -0
- package/dist/assets/infra/lib/lambda/event-schema.ts +144 -0
- package/dist/assets/infra/lib/lambda/metrics-processor.ts +124 -36
- package/dist/assets/infra/lib/lambda/otel-receiver.ts +927 -16
- package/dist/assets/infra/lib/lambda/productivity-widget.ts +203 -0
- package/dist/assets/infra/lib/lambda/security-remediation-tracker.ts +17 -2
- package/dist/assets/infra/lib/lambda/velocity-widget.ts +1483 -0
- package/dist/assets/infra/lib/metrics-pipeline-stack.ts +191 -119
- package/dist/assets/infra/package.json +2 -1
- package/dist/assets/infra/tsconfig.json +9 -2
- package/dist/src/commands/bootstrapper/install-eval-harness.d.ts +8 -2
- package/dist/src/commands/bootstrapper/install-eval-harness.d.ts.map +1 -1
- package/dist/src/commands/bootstrapper/install-eval-harness.js +32 -32
- package/dist/src/commands/bootstrapper/install-eval-harness.js.map +1 -1
- package/dist/src/commands/bootstrapper/install-github-workflows.d.ts.map +1 -1
- package/dist/src/commands/bootstrapper/install-github-workflows.js +7 -2
- package/dist/src/commands/bootstrapper/install-github-workflows.js.map +1 -1
- package/dist/src/commands/bootstrapper/install-gitlab-workflows.d.ts.map +1 -1
- package/dist/src/commands/bootstrapper/install-gitlab-workflows.js +7 -1
- package/dist/src/commands/bootstrapper/install-gitlab-workflows.js.map +1 -1
- package/dist/src/commands/bootstrapper/setup-otel-sync.d.ts.map +1 -1
- package/dist/src/commands/bootstrapper/setup-otel-sync.js +3 -2
- package/dist/src/commands/bootstrapper/setup-otel-sync.js.map +1 -1
- package/dist/src/utils/region.d.ts +50 -0
- package/dist/src/utils/region.d.ts.map +1 -0
- package/dist/src/utils/region.js +62 -0
- package/dist/src/utils/region.js.map +1 -0
- package/package.json +2 -2
- package/dist/assets/eval-harness/README.md +0 -144
- package/dist/assets/github-workflows/README.md +0 -110
- package/dist/assets/github-workflows/prism-dora-weekly.yml +0 -334
- package/dist/assets/gitlab-workflows/prism-dora-weekly.yml +0 -181
- package/dist/assets/infra/lib/lambda/defect-correlator.ts +0 -142
- package/dist/assets/infra/lib/lambda/spec-to-code-calculator.ts +0 -123
- package/dist/assets/metric-hooks/README.md +0 -107
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CloudWatch Custom Widget: Developer Productivity.
|
|
3
|
+
*
|
|
4
|
+
* Invoked by CloudWatch dashboards (with the VIEWING USER's IAM
|
|
5
|
+
* credentials — access control is lambda:InvokeFunction on this function).
|
|
6
|
+
* Delegates aggregation to the otel-receiver's GET /v1/productivity
|
|
7
|
+
* handler via direct Lambda invoke (single implementation, no drift),
|
|
8
|
+
* then renders themed HTML.
|
|
9
|
+
*
|
|
10
|
+
* Widget params:
|
|
11
|
+
* view — 'table' (team comparison, always all users) | 'detail'
|
|
12
|
+
* (by-tool/by-model breakdown + ratio card for the current scope)
|
|
13
|
+
* user — email or 'all' (default 'all'); scopes the detail view.
|
|
14
|
+
* Time range follows the dashboard (widgetContext.timeRange).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda';
|
|
18
|
+
|
|
19
|
+
const lambdaClient = new LambdaClient({});
|
|
20
|
+
const RECEIVER_FUNCTION = process.env.RECEIVER_FUNCTION || 'prism-d1-otel-receiver';
|
|
21
|
+
|
|
22
|
+
interface WidgetEvent {
|
|
23
|
+
describe?: boolean;
|
|
24
|
+
view?: string;
|
|
25
|
+
user?: string;
|
|
26
|
+
widgetContext?: {
|
|
27
|
+
timeRange?: { start: number; end: number };
|
|
28
|
+
theme?: string;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const DOCS = `## Developer Productivity
|
|
33
|
+
Per-developer usage and commit outcomes from the PRISM attribution store —
|
|
34
|
+
full history, real commit timestamps.
|
|
35
|
+
|
|
36
|
+
### Parameters
|
|
37
|
+
| Name | Type | Default | Description |
|
|
38
|
+
|------|------|---------|-------------|
|
|
39
|
+
| view | string | table | 'table' (team comparison) or 'detail' (by-tool/by-model + ratios) |
|
|
40
|
+
| user | string | all | Developer email, or 'all' — scopes the detail view |
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
type Breakdown = Record<string, { costUsd: number; calls: number }>;
|
|
44
|
+
type UserRow = {
|
|
45
|
+
user: string;
|
|
46
|
+
usage: { inputTokens: number; outputTokens: number; costUsd: number; calls: number; byTool: Breakdown; byModel: Breakdown };
|
|
47
|
+
commits: { total: number; ai: number; human: number; mergedAi: number; revertedAi: number };
|
|
48
|
+
ratios: { aiSharePct: number | null; mergeRatePct: number | null; defectRatePct: number | null; costPerAiCommit: number | null; costPerShippedCommit: number | null };
|
|
49
|
+
};
|
|
50
|
+
type Report = { range: { from: string; to: string }; users: UserRow[]; totals: UserRow };
|
|
51
|
+
|
|
52
|
+
// Theme palette resolved once per invocation
|
|
53
|
+
type Palette = { fg: string; mut: string; bord: string; accent: string; ok: string; warn: string };
|
|
54
|
+
function palette(dark: boolean): Palette {
|
|
55
|
+
return {
|
|
56
|
+
fg: dark ? '#d1d5db' : '#16191f',
|
|
57
|
+
mut: dark ? '#8d99a8' : '#687078',
|
|
58
|
+
bord: dark ? '#414750' : '#e9ebed',
|
|
59
|
+
accent: dark ? '#44b9d6' : '#0073bb',
|
|
60
|
+
ok: dark ? '#5fd38d' : '#1d8102',
|
|
61
|
+
warn: dark ? '#e0b13e' : '#906806',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const money = (n: number): string => `$${n.toFixed(2)}`;
|
|
66
|
+
const pct = (n: number | null): string => (n === null ? '—' : `${n}%`);
|
|
67
|
+
const num = (n: number): string => n.toLocaleString('en-US');
|
|
68
|
+
const esc = (s: string): string => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
69
|
+
|
|
70
|
+
/** Horizontal CSS bar scaled against a maximum. */
|
|
71
|
+
function bar(value: number, max: number, color: string): string {
|
|
72
|
+
const w = max > 0 ? Math.max(1, Math.round((value / max) * 100)) : 0;
|
|
73
|
+
return `<div style="background:${color};height:9px;width:${w}%;border-radius:2px;min-width:2px"></div>`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function fetchReport(user: string, from: string, to: string): Promise<Report> {
|
|
77
|
+
const receiverEvent = {
|
|
78
|
+
rawPath: '/v1/productivity',
|
|
79
|
+
rawQueryString: `user=${encodeURIComponent(user)}&from=${from}&to=${to}`,
|
|
80
|
+
requestContext: {
|
|
81
|
+
http: { method: 'GET', path: '/v1/productivity' },
|
|
82
|
+
// Direct invoke bypasses the API GW JWT authorizer by design: reaching
|
|
83
|
+
// this code path already required lambda:InvokeFunction IAM on the
|
|
84
|
+
// widget, which gates dashboard viewers.
|
|
85
|
+
authorizer: { jwt: { claims: { email: 'cloudwatch-dashboard-widget' } } },
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
return lambdaClient.send(new InvokeCommand({
|
|
89
|
+
FunctionName: RECEIVER_FUNCTION,
|
|
90
|
+
Payload: Buffer.from(JSON.stringify(receiverEvent)),
|
|
91
|
+
})).then(resp => {
|
|
92
|
+
const raw = JSON.parse(Buffer.from(resp.Payload ?? new Uint8Array()).toString() || '{}');
|
|
93
|
+
if (raw.statusCode !== 200) throw new Error(`productivity query HTTP ${raw.statusCode ?? '?'}`);
|
|
94
|
+
return JSON.parse(raw.body) as Report;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---- Table view: team comparison with spend bars ----
|
|
99
|
+
function renderTable(report: Report, p: Palette): string {
|
|
100
|
+
const maxCost = Math.max(...report.users.map(u => u.usage.costUsd), 0.01);
|
|
101
|
+
const header = ['Developer', 'AI Spend', 'Calls', 'AI / Total Commits', 'Shipped (AI)', 'Merge Rate', 'Defect Rate', '$ / Shipped']
|
|
102
|
+
.map((h, i) => `<th style="padding:6px 10px;color:${p.mut};font-weight:normal;text-align:${i <= 1 ? 'left' : 'right'}">${h}</th>`)
|
|
103
|
+
.join('');
|
|
104
|
+
|
|
105
|
+
const row = (u: UserRow, bold = false, withBar = true): string => `
|
|
106
|
+
<tr style="${bold ? 'font-weight:bold;' : ''}border-top:1px solid ${p.bord}">
|
|
107
|
+
<td style="padding:6px 10px;color:${bold ? p.fg : p.accent};white-space:nowrap">${esc(u.user)}</td>
|
|
108
|
+
<td style="padding:6px 10px;min-width:140px">${withBar ? bar(u.usage.costUsd, maxCost, p.accent) : ''}<span style="color:${p.mut}">${money(u.usage.costUsd)}</span></td>
|
|
109
|
+
<td style="padding:6px 10px;text-align:right">${num(u.usage.calls)}</td>
|
|
110
|
+
<td style="padding:6px 10px;text-align:right">${num(u.commits.ai)} / ${num(u.commits.total)}</td>
|
|
111
|
+
<td style="padding:6px 10px;text-align:right">${num(u.commits.mergedAi)}</td>
|
|
112
|
+
<td style="padding:6px 10px;text-align:right;color:${(u.ratios.mergeRatePct ?? 100) >= 85 ? p.ok : p.warn}">${pct(u.ratios.mergeRatePct)}</td>
|
|
113
|
+
<td style="padding:6px 10px;text-align:right">${pct(u.ratios.defectRatePct)}</td>
|
|
114
|
+
<td style="padding:6px 10px;text-align:right">${u.ratios.costPerShippedCommit === null ? '—' : money(u.ratios.costPerShippedCommit)}</td>
|
|
115
|
+
</tr>`;
|
|
116
|
+
|
|
117
|
+
return `
|
|
118
|
+
<table style="border-collapse:collapse;width:100%">
|
|
119
|
+
<tr>${header}</tr>
|
|
120
|
+
${report.users.map(u => row(u)).join('')}
|
|
121
|
+
${report.users.length > 1 ? row({ ...report.totals, user: 'ORG TOTAL' }, true, false) : ''}
|
|
122
|
+
</table>`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---- Detail view: by-tool + by-model bars and ratio card ----
|
|
126
|
+
function renderDetail(report: Report, user: string, p: Palette): string {
|
|
127
|
+
const scope = user === 'all'
|
|
128
|
+
? report.totals
|
|
129
|
+
: report.users.find(u => u.user === user) ?? report.totals;
|
|
130
|
+
const title = user === 'all' ? 'Organization' : esc(scope.user);
|
|
131
|
+
|
|
132
|
+
const breakdownTable = (label: string, data: Breakdown, color: string): string => {
|
|
133
|
+
const entries = Object.entries(data).sort((a, b) => b[1].costUsd - a[1].costUsd).slice(0, 8);
|
|
134
|
+
const max = Math.max(...entries.map(([, v]) => v.costUsd), 0.01);
|
|
135
|
+
if (entries.length === 0) return `<div style="color:${p.mut}">no ${label.toLowerCase()} data</div>`;
|
|
136
|
+
return `
|
|
137
|
+
<table style="border-collapse:collapse;width:100%">
|
|
138
|
+
<tr style="color:${p.mut}"><th align="left" style="padding:4px 8px;font-weight:normal">${label}</th><th align="left" style="padding:4px 8px;font-weight:normal;width:40%">Spend</th><th align="right" style="padding:4px 8px;font-weight:normal">Calls</th></tr>
|
|
139
|
+
${entries.map(([k, v]) => `
|
|
140
|
+
<tr style="border-top:1px solid ${p.bord}">
|
|
141
|
+
<td style="padding:4px 8px;white-space:nowrap">${esc(k)}</td>
|
|
142
|
+
<td style="padding:4px 8px">${bar(v.costUsd, max, color)}<span style="color:${p.mut}">${money(v.costUsd)}</span></td>
|
|
143
|
+
<td style="padding:4px 8px;text-align:right">${num(v.calls)}</td>
|
|
144
|
+
</tr>`).join('')}
|
|
145
|
+
</table>`;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const stat = (label: string, value: string, color = p.fg): string =>
|
|
149
|
+
`<div style="display:flex;justify-content:space-between;padding:5px 8px;border-bottom:1px solid ${p.bord}">
|
|
150
|
+
<span style="color:${p.mut}">${label}</span><b style="color:${color}">${value}</b>
|
|
151
|
+
</div>`;
|
|
152
|
+
|
|
153
|
+
const r = scope.ratios;
|
|
154
|
+
const c = scope.commits;
|
|
155
|
+
return `
|
|
156
|
+
<div style="margin-bottom:6px"><b style="color:${p.accent}">${title}</b>
|
|
157
|
+
${user === 'all' ? `<span style="color:${p.mut}"> · set the Developer variable to an email to drill into one person</span>` : ''}
|
|
158
|
+
</div>
|
|
159
|
+
<div style="display:flex;gap:16px;flex-wrap:wrap">
|
|
160
|
+
<div style="flex:2;min-width:280px">${breakdownTable('By Tool', scope.usage.byTool, p.accent)}</div>
|
|
161
|
+
<div style="flex:2;min-width:280px">${breakdownTable('By Model', scope.usage.byModel, p.warn)}</div>
|
|
162
|
+
<div style="flex:1;min-width:200px">
|
|
163
|
+
${stat('AI share of commits', pct(r.aiSharePct))}
|
|
164
|
+
${stat('Merge rate', pct(r.mergeRatePct), (r.mergeRatePct ?? 100) >= 85 ? p.ok : p.warn)}
|
|
165
|
+
${stat('Defect rate', pct(r.defectRatePct))}
|
|
166
|
+
${stat('$ / AI commit', r.costPerAiCommit === null ? '—' : money(r.costPerAiCommit))}
|
|
167
|
+
${stat('$ / shipped commit', r.costPerShippedCommit === null ? '—' : money(r.costPerShippedCommit))}
|
|
168
|
+
${stat('Commits (AI / human)', `${num(c.ai)} / ${num(c.human)}`)}
|
|
169
|
+
</div>
|
|
170
|
+
</div>`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function handler(event: WidgetEvent): Promise<string> {
|
|
174
|
+
if (event.describe) return DOCS;
|
|
175
|
+
|
|
176
|
+
const view = (event.view ?? 'table').trim();
|
|
177
|
+
const user = (event.user ?? 'all').trim() || 'all';
|
|
178
|
+
const tr = event.widgetContext?.timeRange;
|
|
179
|
+
const from = tr ? new Date(tr.start).toISOString().slice(0, 10) : new Date(Date.now() - 30 * 86400_000).toISOString().slice(0, 10);
|
|
180
|
+
const to = tr ? new Date(tr.end).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10);
|
|
181
|
+
const p = palette(event.widgetContext?.theme === 'dark');
|
|
182
|
+
|
|
183
|
+
let report: Report;
|
|
184
|
+
try {
|
|
185
|
+
// The table always compares the whole team; the detail view scopes to
|
|
186
|
+
// the selected user (fetching 'all' also covers user-scope via totals
|
|
187
|
+
// when the email doesn't match — cheap at team scale).
|
|
188
|
+
report = await fetchReport('all', from, to);
|
|
189
|
+
} catch (e) {
|
|
190
|
+
return `<p style="color:#d13212">${esc((e as Error).message)}</p>`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const body = view === 'detail' ? renderDetail(report, user, p) : renderTable(report, p);
|
|
194
|
+
return `
|
|
195
|
+
<div style="color:${p.fg};font-size:13px">
|
|
196
|
+
<div style="color:${p.mut};margin-bottom:6px">
|
|
197
|
+
${report.range.from} → ${report.range.to}
|
|
198
|
+
· ${report.users.length} developer(s)
|
|
199
|
+
· attribution store (real commit times, full history)
|
|
200
|
+
</div>
|
|
201
|
+
${body}
|
|
202
|
+
</div>`;
|
|
203
|
+
}
|
|
@@ -21,7 +21,7 @@ interface PrEvent {
|
|
|
21
21
|
repo: string;
|
|
22
22
|
timestamp: string;
|
|
23
23
|
ai_context?: { origin: string };
|
|
24
|
-
pr?: { merged_at?: string; number?: number };
|
|
24
|
+
pr?: { merged_at?: string; number?: number; commit_shas?: string[] };
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -90,7 +90,20 @@ export async function handler(event: PrEvent): Promise<void> {
|
|
|
90
90
|
finding_id: finding.finding_id,
|
|
91
91
|
severity: finding.severity,
|
|
92
92
|
remediation_time_hours: Math.round(remediationHours * 100) / 100,
|
|
93
|
+
// WHO FIXED IT — resolved by deferred attribution join at render
|
|
94
|
+
// time, not here. These are the FIX PR's commits (not the
|
|
95
|
+
// offending commits on the finding), which is the correct signal
|
|
96
|
+
// for "who remediated this". SHAs are immutable facts; a verdict
|
|
97
|
+
// computed now can be permanently wrong because `codeburn sync
|
|
98
|
+
// --attribution` runs on a ~12h schedule and has not necessarily
|
|
99
|
+
// covered these commits yet.
|
|
100
|
+
fix_commit_shas: detail.pr?.commit_shas ?? [],
|
|
101
|
+
// Legacy trailer-derived verdict, retained for the Phase 1
|
|
102
|
+
// parallel run. Reads 'human' for everything once git hooks are
|
|
103
|
+
// removed (ai_context.origin <- ai_ratio <- trailer counting), so
|
|
104
|
+
// dashboards prefer the join above and only fall back to this.
|
|
93
105
|
remediated_by_origin: detail.ai_context?.origin ?? 'unknown',
|
|
106
|
+
remediated_by_origin_source: 'git-trailers',
|
|
94
107
|
fix_pr_number: detail.pr?.number ?? null,
|
|
95
108
|
finding_phase: finding.phase,
|
|
96
109
|
},
|
|
@@ -110,7 +123,9 @@ export async function handler(event: PrEvent): Promise<void> {
|
|
|
110
123
|
);
|
|
111
124
|
|
|
112
125
|
console.log(
|
|
113
|
-
`Remediation event: finding ${finding.finding_id} (${finding.severity}) fixed in ${remediationHours.toFixed(1)}h
|
|
126
|
+
`Remediation event: finding ${finding.finding_id} (${finding.severity}) fixed in ${remediationHours.toFixed(1)}h ` +
|
|
127
|
+
`in PR #${detail.pr?.number ?? '?'} (${(detail.pr?.commit_shas ?? []).length} commit SHAs for deferred origin join; ` +
|
|
128
|
+
`trailer said ${detail.ai_context?.origin ?? 'unknown'})`,
|
|
114
129
|
);
|
|
115
130
|
}
|
|
116
131
|
}
|