@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.
Files changed (43) hide show
  1. package/dist/assets/github-workflows/prism-ai-metrics.yml +83 -122
  2. package/dist/assets/github-workflows/prism-eval-gate-kiro.yml +66 -0
  3. package/dist/assets/github-workflows/prism-eval-gate.yml +1 -1
  4. package/dist/assets/gitlab-workflows/.gitlab-ci.yml +0 -3
  5. package/dist/assets/gitlab-workflows/prism-ai-metrics.yml +53 -56
  6. package/dist/assets/infra/bin/app.ts +2 -0
  7. package/dist/assets/infra/lib/constructs/otel-collector-construct.ts +62 -0
  8. package/dist/assets/infra/lib/dashboard-stack.ts +541 -1144
  9. package/dist/assets/infra/lib/lambda/attribution-metrics-publisher.ts +248 -0
  10. package/dist/assets/infra/lib/lambda/event-schema.ts +144 -0
  11. package/dist/assets/infra/lib/lambda/metrics-processor.ts +124 -36
  12. package/dist/assets/infra/lib/lambda/otel-receiver.ts +927 -16
  13. package/dist/assets/infra/lib/lambda/productivity-widget.ts +203 -0
  14. package/dist/assets/infra/lib/lambda/security-remediation-tracker.ts +17 -2
  15. package/dist/assets/infra/lib/lambda/velocity-widget.ts +1483 -0
  16. package/dist/assets/infra/lib/metrics-pipeline-stack.ts +191 -119
  17. package/dist/assets/infra/package.json +2 -1
  18. package/dist/assets/infra/tsconfig.json +9 -2
  19. package/dist/src/commands/bootstrapper/install-eval-harness.d.ts +8 -2
  20. package/dist/src/commands/bootstrapper/install-eval-harness.d.ts.map +1 -1
  21. package/dist/src/commands/bootstrapper/install-eval-harness.js +32 -32
  22. package/dist/src/commands/bootstrapper/install-eval-harness.js.map +1 -1
  23. package/dist/src/commands/bootstrapper/install-github-workflows.d.ts.map +1 -1
  24. package/dist/src/commands/bootstrapper/install-github-workflows.js +7 -2
  25. package/dist/src/commands/bootstrapper/install-github-workflows.js.map +1 -1
  26. package/dist/src/commands/bootstrapper/install-gitlab-workflows.d.ts.map +1 -1
  27. package/dist/src/commands/bootstrapper/install-gitlab-workflows.js +7 -1
  28. package/dist/src/commands/bootstrapper/install-gitlab-workflows.js.map +1 -1
  29. package/dist/src/commands/bootstrapper/setup-otel-sync.d.ts.map +1 -1
  30. package/dist/src/commands/bootstrapper/setup-otel-sync.js +3 -2
  31. package/dist/src/commands/bootstrapper/setup-otel-sync.js.map +1 -1
  32. package/dist/src/utils/region.d.ts +50 -0
  33. package/dist/src/utils/region.d.ts.map +1 -0
  34. package/dist/src/utils/region.js +62 -0
  35. package/dist/src/utils/region.js.map +1 -0
  36. package/package.json +2 -2
  37. package/dist/assets/eval-harness/README.md +0 -144
  38. package/dist/assets/github-workflows/README.md +0 -110
  39. package/dist/assets/github-workflows/prism-dora-weekly.yml +0 -334
  40. package/dist/assets/gitlab-workflows/prism-dora-weekly.yml +0 -181
  41. package/dist/assets/infra/lib/lambda/defect-correlator.ts +0 -142
  42. package/dist/assets/infra/lib/lambda/spec-to-code-calculator.ts +0 -123
  43. 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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 by ${detail.ai_context?.origin ?? 'unknown'}`,
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
  }