@prism-d1/cli 1.0.79 → 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 (38) 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 +1 -0
  7. package/dist/assets/infra/lib/constructs/otel-collector-construct.ts +4 -0
  8. package/dist/assets/infra/lib/dashboard-stack.ts +378 -1235
  9. package/dist/assets/infra/lib/lambda/event-schema.ts +144 -0
  10. package/dist/assets/infra/lib/lambda/metrics-processor.ts +124 -36
  11. package/dist/assets/infra/lib/lambda/otel-receiver.ts +249 -35
  12. package/dist/assets/infra/lib/lambda/security-remediation-tracker.ts +17 -2
  13. package/dist/assets/infra/lib/lambda/velocity-widget.ts +1483 -0
  14. package/dist/assets/infra/lib/metrics-pipeline-stack.ts +110 -119
  15. package/dist/assets/infra/package.json +2 -1
  16. package/dist/assets/infra/tsconfig.json +9 -2
  17. package/dist/src/commands/bootstrapper/install-eval-harness.d.ts +8 -2
  18. package/dist/src/commands/bootstrapper/install-eval-harness.d.ts.map +1 -1
  19. package/dist/src/commands/bootstrapper/install-eval-harness.js +32 -32
  20. package/dist/src/commands/bootstrapper/install-eval-harness.js.map +1 -1
  21. package/dist/src/commands/bootstrapper/install-github-workflows.d.ts.map +1 -1
  22. package/dist/src/commands/bootstrapper/install-github-workflows.js +7 -2
  23. package/dist/src/commands/bootstrapper/install-github-workflows.js.map +1 -1
  24. package/dist/src/commands/bootstrapper/install-gitlab-workflows.d.ts.map +1 -1
  25. package/dist/src/commands/bootstrapper/install-gitlab-workflows.js +7 -1
  26. package/dist/src/commands/bootstrapper/install-gitlab-workflows.js.map +1 -1
  27. package/dist/src/utils/region.d.ts +50 -0
  28. package/dist/src/utils/region.d.ts.map +1 -0
  29. package/dist/src/utils/region.js +62 -0
  30. package/dist/src/utils/region.js.map +1 -0
  31. package/package.json +2 -2
  32. package/dist/assets/eval-harness/README.md +0 -144
  33. package/dist/assets/github-workflows/README.md +0 -110
  34. package/dist/assets/github-workflows/prism-dora-weekly.yml +0 -334
  35. package/dist/assets/gitlab-workflows/prism-dora-weekly.yml +0 -181
  36. package/dist/assets/infra/lib/lambda/defect-correlator.ts +0 -142
  37. package/dist/assets/infra/lib/lambda/spec-to-code-calculator.ts +0 -123
  38. package/dist/assets/metric-hooks/README.md +0 -107
@@ -1,12 +1,30 @@
1
- # prism-ai-metrics.yml — Emit AI-assisted development metrics on PR merge.
1
+ # prism-ai-metrics.yml — Per-merge delivery telemetry.
2
2
  #
3
3
  # Copy this file to .github/workflows/prism-ai-metrics.yml in your repo.
4
4
  #
5
+ # DESIGN: this workflow emits immutable per-PR FACTS and computes no rates.
6
+ # Every aggregation (frequency, averages, change-failure rate, MTTR) happens at
7
+ # query time in the dashboard widgets. That means:
8
+ # - the dashboard's time-range picker applies to every KPI uniformly
9
+ # - no metric bakes in a fixed window (the old weekly job hardcoded 7 days)
10
+ # - lead-time distribution survives, so p50/p95 stay computable
11
+ #
12
+ # AI origin is deliberately NOT computed here. It is resolved at render time by
13
+ # joining `pr.commit_shas` against the codeburn attribution store. Origin
14
+ # verdicts computed at merge time are frequently wrong, because attribution
15
+ # syncs on a ~12h schedule and may not yet cover these commits. See
16
+ # docs/data-architecture.md → "AI Origin Resolution: Deferred Attribution Join".
17
+ #
18
+ # Replaces the former prism-dora-weekly.yml, which computed a parallel 7-day
19
+ # aggregate from the GitHub API. That duplicated deployment-frequency
20
+ # measurement, disagreed with this workflow on `master`-based repos, and
21
+ # published six CloudWatch metrics no dashboard read.
22
+ #
5
23
  # Required setup:
6
24
  # - OIDC provider configured for GitHub Actions in your AWS account
7
25
  # - IAM role with events:PutEvents on the prism-d1-metrics bus
8
26
  # - Repository secret PRISM_METRICS_ROLE_ARN set to the OIDC role ARN
9
- # - .prism/config.json committed to the repo (created by: bash prism-cli bootstrapper install-git-hooks)
27
+ # - .prism/config.json committed to the repo (optional; defaults to no_team)
10
28
 
11
29
  name: PRISM AI Metrics
12
30
 
@@ -28,6 +46,8 @@ jobs:
28
46
  - name: Checkout
29
47
  uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
30
48
  with:
49
+ # Full history is required: `git rev-list BASE..HEAD` below needs the
50
+ # base commit present to enumerate the PR's commit SHAs.
31
51
  fetch-depth: 0
32
52
 
33
53
  - name: Configure AWS credentials (OIDC)
@@ -44,178 +64,111 @@ jobs:
44
64
  else
45
65
  TEAM_ID=""
46
66
  fi
67
+ # Missing config is not fatal — emit under no_team rather than failing
68
+ # the run and losing this PR's facts permanently.
47
69
  if [[ -z "${TEAM_ID}" ]]; then
48
70
  TEAM_ID="no_team"
49
71
  fi
50
72
  echo "team_id=${TEAM_ID}" >> "$GITHUB_OUTPUT"
51
73
 
52
- - name: Calculate AI-to-merge ratio
53
- id: ai-ratio
74
+ - name: Collect PR facts
75
+ id: facts
76
+ env:
77
+ # PR titles are attacker-controlled. Bind to env and match with
78
+ # printf rather than interpolating into the shell (script injection).
79
+ PR_TITLE: ${{ github.event.pull_request.title }}
80
+ CREATED_AT: ${{ github.event.pull_request.created_at }}
81
+ MERGED_AT: ${{ github.event.pull_request.merged_at }}
54
82
  run: |
83
+ set -uo pipefail
55
84
  BASE_SHA="${{ github.event.pull_request.base.sha }}"
56
85
  HEAD_SHA="${{ github.event.pull_request.merge_commit_sha }}"
57
86
 
58
- TOTAL_COMMITS=$(git rev-list --count "${BASE_SHA}..${HEAD_SHA}")
59
-
60
- # Count AI origin per-commit (not per-line) to avoid overcounting
61
- AI_GENERATED=0
62
- AI_ASSISTED=0
63
- HUMAN_ONLY=0
64
- TOTAL_INPUT_TOKENS=0
65
- TOTAL_OUTPUT_TOKENS=0
66
- TOTAL_COST="0"
67
- for SHA in $(git rev-list "${BASE_SHA}..${HEAD_SHA}"); do
68
- MSG=$(git log -1 --format='%B' "${SHA}")
69
- if echo "${MSG}" | grep -q '^AI-Origin: ai-generated'; then
70
- AI_GENERATED=$((AI_GENERATED + 1))
71
- elif echo "${MSG}" | grep -q '^AI-Origin: ai-assisted'; then
72
- AI_ASSISTED=$((AI_ASSISTED + 1))
73
- elif echo "${MSG}" | grep -q '^AI-Origin: human'; then
74
- HUMAN_ONLY=$((HUMAN_ONLY + 1))
75
- fi
76
-
77
- # Accumulate token usage trailers (clamped to prevent injection)
78
- INPUT_T=$(echo "${MSG}" | grep -oP '(?<=^AI-Input-Tokens: )\d+' || echo "0")
79
- OUTPUT_T=$(echo "${MSG}" | grep -oP '(?<=^AI-Output-Tokens: )\d+' || echo "0")
80
- COST_T=$(echo "${MSG}" | grep -oP '(?<=^AI-Cost: )[0-9.]+' || echo "0")
81
- [[ ${INPUT_T} -gt 1000000 ]] && INPUT_T=0
82
- [[ ${OUTPUT_T} -gt 1000000 ]] && OUTPUT_T=0
83
- (( $(echo "${COST_T} > 100" | bc -l) )) && COST_T=0
84
- TOTAL_INPUT_TOKENS=$((TOTAL_INPUT_TOKENS + INPUT_T))
85
- TOTAL_OUTPUT_TOKENS=$((TOTAL_OUTPUT_TOKENS + OUTPUT_T))
86
- TOTAL_COST=$(echo "${TOTAL_COST} + ${COST_T}" | bc -l)
87
- done
88
-
89
- AI_COMMITS=$((AI_GENERATED + AI_ASSISTED))
90
- if [[ "${TOTAL_COMMITS}" -gt 0 ]]; then
91
- AI_RATIO=$(echo "scale=4; ${AI_COMMITS} / ${TOTAL_COMMITS}" | bc -l | sed 's/^\./0./')
92
- else
93
- AI_RATIO="0"
94
- fi
95
-
96
- # Detect tools used
97
- TOOLS_USED=$(git log "${BASE_SHA}..${HEAD_SHA}" --format='%B' | grep '^AI-Origin:' | sort -u | head -5 | tr '\n' ',' | sed 's/,$//')
98
-
99
- echo "total_commits=${TOTAL_COMMITS}" >> "$GITHUB_OUTPUT"
100
- echo "ai_commits=${AI_COMMITS}" >> "$GITHUB_OUTPUT"
101
- echo "ai_ratio=${AI_RATIO}" >> "$GITHUB_OUTPUT"
102
- echo "ai_generated=${AI_GENERATED}" >> "$GITHUB_OUTPUT"
103
- echo "ai_assisted=${AI_ASSISTED}" >> "$GITHUB_OUTPUT"
104
- echo "human_only=${HUMAN_ONLY}" >> "$GITHUB_OUTPUT"
105
- echo "total_input_tokens=${TOTAL_INPUT_TOKENS}" >> "$GITHUB_OUTPUT"
106
- echo "total_output_tokens=${TOTAL_OUTPUT_TOKENS}" >> "$GITHUB_OUTPUT"
107
- echo "total_cost=${TOTAL_COST}" >> "$GITHUB_OUTPUT"
108
-
109
- - name: Calculate lead time
110
- id: lead-time
111
- run: |
112
- CREATED_AT="${{ github.event.pull_request.created_at }}"
113
- MERGED_AT="${{ github.event.pull_request.merged_at }}"
87
+ # --- Commit SHAs: immutable facts for deferred origin resolution.
88
+ # Capped at 50 to bound the event size and the render-time join.
89
+ COMMIT_SHAS=$(git rev-list "${BASE_SHA}..${HEAD_SHA}" 2>/dev/null | head -50 | jq -R . | jq -sc . || echo '[]')
90
+ TOTAL_COMMITS=$(git rev-list --count "${BASE_SHA}..${HEAD_SHA}" 2>/dev/null || echo 0)
114
91
 
92
+ # --- Lead time: PR open → merge. From the event payload, not git, so
93
+ # it is unaffected by rebases and squashes.
115
94
  CREATED_TS=$(date -d "${CREATED_AT}" +%s 2>/dev/null || date -jf "%Y-%m-%dT%H:%M:%SZ" "${CREATED_AT}" +%s)
116
95
  MERGED_TS=$(date -d "${MERGED_AT}" +%s 2>/dev/null || date -jf "%Y-%m-%dT%H:%M:%SZ" "${MERGED_AT}" +%s)
117
-
118
96
  LEAD_TIME_SECONDS=$((MERGED_TS - CREATED_TS))
119
- LEAD_TIME_HOURS=$(echo "scale=2; ${LEAD_TIME_SECONDS} / 3600" | bc -l | sed 's/^\./0./')
97
+ [[ "${LEAD_TIME_SECONDS}" -lt 0 ]] && LEAD_TIME_SECONDS=0
98
+
99
+ # --- Failure-fix label: the change-failure-rate NUMERATOR, emitted as
100
+ # a per-PR boolean. The rate itself (and MTTR, the mean lead time of
101
+ # labelled PRs) is computed at query time over the dashboard's window.
102
+ # Still a title heuristic — it misses failures that aren't named.
103
+ if printf '%s' "${PR_TITLE}" | grep -qiE 'revert|hotfix|rollback'; then
104
+ IS_FAILURE_FIX=true
105
+ else
106
+ IS_FAILURE_FIX=false
107
+ fi
120
108
 
109
+ echo "commit_shas=${COMMIT_SHAS}" >> "$GITHUB_OUTPUT"
110
+ echo "total_commits=${TOTAL_COMMITS}" >> "$GITHUB_OUTPUT"
121
111
  echo "lead_time_seconds=${LEAD_TIME_SECONDS}" >> "$GITHUB_OUTPUT"
122
- echo "lead_time_hours=${LEAD_TIME_HOURS}" >> "$GITHUB_OUTPUT"
112
+ echo "is_failure_fix=${IS_FAILURE_FIX}" >> "$GITHUB_OUTPUT"
123
113
 
124
- - name: Calculate AI acceptance rate
125
- id: acceptance
114
+ - name: Collect review verdicts
115
+ id: reviews
126
116
  env:
127
117
  GH_TOKEN: ${{ github.token }}
128
118
  run: |
119
+ set -uo pipefail
129
120
  PR_NUMBER="${{ github.event.pull_request.number }}"
130
121
  REPO="${{ github.repository }}"
131
122
 
132
- # Get review decisions for this PR
123
+ # Raw counts only. The approval RATE is derived at query time, where it
124
+ # can be compared against the human-origin baseline — an absolute
125
+ # approval rate answers no useful question on its own.
133
126
  REVIEWS=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/reviews" \
134
127
  --jq '[.[] | select(.state == "APPROVED" or .state == "CHANGES_REQUESTED")]' 2>/dev/null || echo "[]")
135
128
 
136
129
  APPROVED=$(echo "${REVIEWS}" | jq '[.[] | select(.state == "APPROVED")] | length')
137
130
  CHANGES_REQUESTED=$(echo "${REVIEWS}" | jq '[.[] | select(.state == "CHANGES_REQUESTED")] | length')
138
- TOTAL_REVIEWS=$((APPROVED + CHANGES_REQUESTED))
139
131
 
140
- if [[ "${TOTAL_REVIEWS}" -gt 0 ]]; then
141
- ACCEPTANCE_RATE=$(echo "scale=4; ${APPROVED} / ${TOTAL_REVIEWS}" | bc -l | sed 's/^\./0./')
142
- else
143
- ACCEPTANCE_RATE="1" # No reviews = accepted by default
144
- fi
145
-
146
- # Only meaningful for AI PRs
147
- AI_RATIO="${{ steps.ai-ratio.outputs.ai_ratio }}"
148
- if [[ $(echo "${AI_RATIO} > 0" | bc -l) -eq 1 ]]; then
149
- AI_ACCEPTANCE_RATE="${ACCEPTANCE_RATE}"
150
- else
151
- AI_ACCEPTANCE_RATE=""
152
- fi
153
-
154
- echo "acceptance_rate=${ACCEPTANCE_RATE}" >> "$GITHUB_OUTPUT"
155
- echo "ai_acceptance_rate=${AI_ACCEPTANCE_RATE}" >> "$GITHUB_OUTPUT"
156
132
  echo "approved=${APPROVED}" >> "$GITHUB_OUTPUT"
157
133
  echo "changes_requested=${CHANGES_REQUESTED}" >> "$GITHUB_OUTPUT"
158
134
 
159
135
  - name: Emit prism.d1.pr event
160
136
  run: |
161
137
  TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
162
- TEAM_ID="${{ steps.prism-config.outputs.team_id }}"
163
- REPO="${{ github.repository }}"
164
138
 
165
139
  PR_EVENT=$(jq -n \
166
- --arg team_id "${TEAM_ID}" \
167
- --arg repo "${REPO}" \
140
+ --arg team_id "${{ steps.prism-config.outputs.team_id }}" \
141
+ --arg repo "${{ github.repository }}" \
168
142
  --arg timestamp "${TIMESTAMP}" \
169
- --argjson ai_ratio "${{ steps.ai-ratio.outputs.ai_ratio }}" \
170
- --argjson total_commits "${{ steps.ai-ratio.outputs.total_commits }}" \
171
- --argjson ai_commits "${{ steps.ai-ratio.outputs.ai_commits }}" \
172
- --argjson ai_generated "${{ steps.ai-ratio.outputs.ai_generated }}" \
173
- --argjson ai_assisted "${{ steps.ai-ratio.outputs.ai_assisted }}" \
174
- --argjson lead_time_seconds "${{ steps.lead-time.outputs.lead_time_seconds }}" \
143
+ --argjson lead_time_seconds "${{ steps.facts.outputs.lead_time_seconds }}" \
144
+ --argjson is_failure_fix "${{ steps.facts.outputs.is_failure_fix }}" \
145
+ --argjson total_commits "${{ steps.facts.outputs.total_commits }}" \
146
+ --argjson commit_shas '${{ steps.facts.outputs.commit_shas }}' \
175
147
  --arg pr_number "${{ github.event.pull_request.number }}" \
176
148
  --arg pr_author "${{ github.event.pull_request.user.login }}" \
177
- --argjson approved "${{ steps.acceptance.outputs.approved }}" \
178
- --argjson changes_requested "${{ steps.acceptance.outputs.changes_requested }}" \
179
- --arg acceptance_rate "${{ steps.acceptance.outputs.acceptance_rate }}" \
180
- --arg ai_acceptance_rate "${{ steps.acceptance.outputs.ai_acceptance_rate }}" \
181
- --argjson total_input_tokens "${{ steps.ai-ratio.outputs.total_input_tokens }}" \
182
- --argjson total_output_tokens "${{ steps.ai-ratio.outputs.total_output_tokens }}" \
183
- --arg total_cost "${{ steps.ai-ratio.outputs.total_cost }}" \
149
+ --argjson approved "${{ steps.reviews.outputs.approved }}" \
150
+ --argjson changes_requested "${{ steps.reviews.outputs.changes_requested }}" \
184
151
  '{
185
152
  "team_id": $team_id,
186
153
  "repo": $repo,
187
154
  "timestamp": $timestamp,
188
155
  "prism_level": 2,
189
156
  "metric": {
190
- "name": "ai_to_merge_ratio",
191
- "value": $ai_ratio,
192
- "unit": "ratio"
193
- },
194
- "ai_context": {
195
- "tool": "github-actions",
196
- "model": "n/a",
197
- "origin": (if $ai_ratio > 0.5 then "ai-assisted" elif $ai_ratio > 0 then "ai-assisted" else "human" end)
157
+ "name": "pr_merged",
158
+ "value": 1,
159
+ "unit": "count"
198
160
  },
199
161
  "dora": {
200
- "deployment_frequency": 1,
201
- "lead_time_seconds": $lead_time_seconds
202
- },
203
- "ai_dora": {
204
- "ai_to_merge_ratio": $ai_ratio,
205
- "ai_acceptance_rate": (if $ai_acceptance_rate != "" then ($ai_acceptance_rate | tonumber) else null end),
206
- "total_commits": $total_commits,
207
- "ai_commits": $ai_commits,
208
- "ai_generated_commits": $ai_generated,
209
- "ai_assisted_commits": $ai_assisted,
210
- "total_input_tokens": $total_input_tokens,
211
- "total_output_tokens": $total_output_tokens,
212
- "total_cost_usd": (if $total_cost != "0" and $total_cost != "" then ($total_cost | tonumber) else 0 end)
162
+ "lead_time_seconds": $lead_time_seconds,
163
+ "is_failure_fix": $is_failure_fix
213
164
  },
214
165
  "pr": {
215
166
  "number": ($pr_number | tonumber),
216
167
  "author": $pr_author,
217
168
  "reviews_approved": $approved,
218
- "reviews_changes_requested": $changes_requested
169
+ "reviews_changes_requested": $changes_requested,
170
+ "total_commits": $total_commits,
171
+ "commit_shas": $commit_shas
219
172
  }
220
173
  }')
221
174
 
@@ -228,10 +181,18 @@ jobs:
228
181
  \"Detail\": $(echo "${PR_EVENT}" | jq -c '.' | jq -Rs '.')
229
182
  }]"
230
183
 
184
+ echo "Emitted prism.d1.pr for #${{ github.event.pull_request.number }}"
185
+ echo " lead time: ${{ steps.facts.outputs.lead_time_seconds }}s"
186
+ echo " failure fix: ${{ steps.facts.outputs.is_failure_fix }}"
187
+ echo " commit SHAs: ${{ steps.facts.outputs.total_commits }} (origin resolved at render time)"
188
+
231
189
  - name: Emit prism.d1.deploy event
232
190
  run: |
233
191
  TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
234
192
 
193
+ # Sole deployment signal. `dora.deployment_frequency` lives ONLY here
194
+ # and not on prism.d1.pr — both events fire on the same merge, so
195
+ # carrying it on both double-counted the DeploymentFrequency metric.
235
196
  DEPLOY_EVENT=$(jq -n \
236
197
  --arg team_id "${{ steps.prism-config.outputs.team_id }}" \
237
198
  --arg repo "${{ github.repository }}" \
@@ -372,10 +372,76 @@ jobs:
372
372
  echo "critical=${CRITICAL}" >> "$GITHUB_OUTPUT"
373
373
  echo "high=${HIGH}" >> "$GITHUB_OUTPUT"
374
374
 
375
+ # Persist full findings for the EventBridge emission step. A file
376
+ # (not a step output) sidesteps GITHUB_OUTPUT size limits and avoids
377
+ # interpolating finding text into a later script (injection surface).
378
+ echo "$FINDINGS_JSON" > "$RUNNER_TEMP/continuum-findings.json"
379
+
375
380
  if [ "$TOTAL" -gt 0 ]; then
376
381
  echo "$FINDINGS_JSON" | jq -r '.findingsSummaries[] | "::warning::[\(.riskLevel)] \(.name)"'
377
382
  fi
378
383
 
384
+ - name: Emit Continuum findings to EventBridge
385
+ if: steps.security-gate.outputs.skipped != 'true' && steps.security-gate.outputs.findings != '0' && steps.security-gate.outputs.findings != ''
386
+ continue-on-error: true
387
+ env:
388
+ AWS_REGION: us-west-2
389
+ TEAM_ID: ${{ steps.prism-config.outputs.team_id }}
390
+ REPO: ${{ github.repository }}
391
+ PR_NUMBER: ${{ github.event.pull_request.number }}
392
+ BASE_SHA: ${{ github.event.pull_request.base.sha }}
393
+ HEAD_SHA: ${{ github.sha }}
394
+ run: |
395
+ # Feeds the Team Velocity / CISO dashboards: without this step,
396
+ # Continuum findings gate the PR but never reach the metrics
397
+ # pipeline (prism.d1.security.code_review -> metrics-processor).
398
+ FINDINGS_FILE="$RUNNER_TEMP/continuum-findings.json"
399
+ if [ ! -s "$FINDINGS_FILE" ]; then
400
+ echo "No findings file — nothing to emit"
401
+ exit 0
402
+ fi
403
+
404
+ # Emit the PR's commit SHAs, NOT a derived AI-origin verdict.
405
+ #
406
+ # Deferred join: dashboards resolve origin at render time by looking
407
+ # these SHAs up in the attribution store. Two reasons not to decide
408
+ # here: (1) trailer grep dies with the git hooks, and (2) attribution
409
+ # spans arrive via `codeburn sync push --attribution` on a ~12h
410
+ # schedule, so at merge time the data usually does not exist yet — a
411
+ # verdict computed now would be wrong and permanently baked into an
412
+ # immutable event. SHAs are facts that never expire, so a PR merged
413
+ # before its attribution syncs simply renders correctly the next day.
414
+ #
415
+ # Capped at 50 to bound event size; PRs larger than that are rare and
416
+ # a partial sample is enough to classify origin.
417
+ COMMIT_SHAS=$(git rev-list "${BASE_SHA}..${HEAD_SHA}" 2>/dev/null | head -50 | jq -R . | jq -sc . || echo '[]')
418
+ echo "Emitting findings with $(echo "${COMMIT_SHAS}" | jq 'length') commit SHAs for deferred origin resolution"
419
+
420
+ ENTRIES=$(jq --arg team_id "${TEAM_ID}" --arg repo "${REPO}" \
421
+ --argjson commit_shas "${COMMIT_SHAS}" --argjson pr_number "${PR_NUMBER}" '
422
+ .findingsSummaries | to_entries | map(.value as $f |
423
+ {Source:"prism.d1.velocity", DetailType:"prism.d1.security.code_review",
424
+ EventBusName:"prism-d1-metrics",
425
+ Detail:({team_id:$team_id, repo:$repo, timestamp:(now | todate), prism_level:3,
426
+ metric:{name:"security_finding",value:1,unit:"count"},
427
+ ai_context:{tool:"continuum",model:"aws-continuum"},
428
+ security_agent_finding:{finding_id:($f.findingId // "cf-pr\($pr_number)-\(.key)"), phase:"code_review",
429
+ severity:($f.riskLevel // "MEDIUM"),
430
+ cwe_id:($f.cweId // null),
431
+ commit_shas:$commit_shas, pr_number:$pr_number,
432
+ found_at:($f.createdAt // null)}
433
+ } | tostring)})' "$FINDINGS_FILE" 2>/dev/null || echo "[]")
434
+
435
+ TOTAL=$(echo "${ENTRIES}" | jq 'length')
436
+ if [ "$TOTAL" -gt 0 ]; then
437
+ # EventBridge accepts max 10 entries per call
438
+ for i in $(seq 0 10 $((TOTAL - 1))); do
439
+ aws events put-events --region us-west-2 \
440
+ --entries "$(echo "${ENTRIES}" | jq -c ".[$i:$((i+10))]")" || true
441
+ done
442
+ echo "Emitted ${TOTAL} Continuum finding events to EventBridge"
443
+ fi
444
+
379
445
  - name: Fail on eval gate failure
380
446
  if: ${{ steps.review.outputs.result == 'FAIL' || steps.review.outputs.result == 'ERROR' || (steps.security-gate.outputs.skipped != 'true' && (steps.security-gate.outputs.critical != '0' || steps.security-gate.outputs.high != '0')) }}
381
447
  env:
@@ -426,7 +426,7 @@ jobs:
426
426
  Detail:({team_id:$team_id, repo:$repo, timestamp:($f.createdAt // now | tostring), prism_level:3,
427
427
  metric:{name:"security_finding",value:1,unit:"count"},
428
428
  ai_context:{tool:"continuum",model:"aws-continuum",origin:$ai_origin},
429
- continuum_finding:{finding_id:($f.findingId // "cf-pr\($pr_number)-\(.key)"), phase:"code_review",
429
+ security_agent_finding:{finding_id:($f.findingId // "cf-pr\($pr_number)-\(.key)"), phase:"code_review",
430
430
  severity:($f.riskLevel // "MEDIUM"),
431
431
  cwe_id:($f.cweId // null),
432
432
  ai_origin:$ai_origin, found_at:($f.createdAt // null)}
@@ -8,17 +8,14 @@
8
8
  # 2. Add CI/CD variable PRISM_METRICS_ROLE_ARN with the output role ARN
9
9
  # 3. Copy gitlab-workflows/*.yml to .prism/gitlab-workflows/ in your repo
10
10
  # 4. (Optional) Set GITLAB_OIDC_AUDIENCE if not using gitlab.aws.dev
11
- # 5. (Optional) Create a pipeline schedule for prism-dora-weekly (Monday 09:00 UTC)
12
11
 
13
12
  workflow:
14
13
  rules:
15
14
  - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
16
15
  - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
17
- - if: '$CI_PIPELINE_SOURCE == "schedule"'
18
16
  - if: '$CI_PIPELINE_SOURCE == "web"'
19
17
 
20
18
  include:
21
19
  - local: '.prism/gitlab-workflows/prism-ai-metrics.yml'
22
- - local: '.prism/gitlab-workflows/prism-dora-weekly.yml'
23
20
  - local: '.prism/gitlab-workflows/prism-agent-eval.yml'
24
21
  - local: '.prism/gitlab-workflows/prism-eval-gate.yml'
@@ -1,14 +1,25 @@
1
- # prism-ai-metrics.yml — Emit AI-assisted development metrics on MR merge.
1
+ # prism-ai-metrics.yml — Per-merge delivery telemetry (GitLab).
2
2
  #
3
3
  # Include this file in your .gitlab-ci.yml:
4
4
  # include:
5
5
  # - local: '.prism/gitlab-workflows/prism-ai-metrics.yml'
6
6
  #
7
+ # DESIGN: emits immutable per-MR FACTS and computes no rates. All aggregation
8
+ # (frequency, lead-time percentiles, change-failure rate, MTTR) happens at query
9
+ # time in the dashboard widgets. See the GitHub twin at
10
+ # bootstrapper/github-workflows/prism-ai-metrics.yml for the full rationale.
11
+ #
12
+ # AI origin is NOT computed here — it is resolved at render time by joining
13
+ # pr.commit_shas against the codeburn attribution store. See
14
+ # docs/data-architecture.md → "AI Origin Resolution: Deferred Attribution Join".
15
+ #
16
+ # Replaces the former prism-dora-weekly.yml, which published a parallel 7-day
17
+ # aggregate that no dashboard consumed.
18
+ #
7
19
  # Required setup:
8
20
  # - CI/CD variable PRISM_METRICS_ROLE_ARN (IAM role ARN with events:PutEvents)
9
- # - CI/CD variable AWS_REGION (default: us-west-2)
10
- # - .prism/config.json committed to the repo
11
- # - AWS credentials configured via OIDC or CI/CD variables
21
+ # - .prism/config.json committed to the repo (optional; defaults to no_team)
22
+ # - AWS credentials configured via OIDC
12
23
 
13
24
  prism-ai-metrics:
14
25
  stage: test
@@ -23,7 +34,7 @@ prism-ai-metrics:
23
34
  variables:
24
35
  AWS_DEFAULT_REGION: us-west-2
25
36
  before_script:
26
- - yum install -y -q jq bc git
37
+ - yum install -y -q jq git
27
38
  - git fetch --unshallow || true
28
39
  - |
29
40
  # Assume AWS role via GitLab OIDC
@@ -47,87 +58,69 @@ prism-ai-metrics:
47
58
  fi
48
59
  script:
49
60
  - |
50
- # Skip if no AWS credentials available
51
61
  if ! aws sts get-caller-identity &>/dev/null; then
52
62
  echo "⚠️ No AWS credentials. Set PRISM_METRICS_ROLE_ARN or configure AWS access."
53
63
  exit 0
54
64
  fi
55
65
 
56
- # Read team ID
57
66
  TEAM_ID=$(jq -r '.team_id // "no_team"' .prism/config.json 2>/dev/null || echo "no_team")
58
67
 
59
- # Determine commit range
68
+ # Commit range for this merge
60
69
  if [[ -n "${CI_MERGE_REQUEST_DIFF_BASE_SHA}" ]]; then
61
70
  BASE_SHA="${CI_MERGE_REQUEST_DIFF_BASE_SHA}"
62
- HEAD_SHA="${CI_COMMIT_SHA}"
63
71
  else
64
72
  BASE_SHA=$(git rev-parse HEAD~1 2>/dev/null || git rev-parse HEAD)
65
- HEAD_SHA="${CI_COMMIT_SHA}"
66
73
  fi
74
+ HEAD_SHA="${CI_COMMIT_SHA}"
67
75
 
76
+ # --- Commit SHAs: immutable facts for deferred origin resolution.
77
+ COMMIT_SHAS=$(git rev-list "${BASE_SHA}..${HEAD_SHA}" 2>/dev/null | head -50 | jq -R . | jq -sc . || echo '[]')
68
78
  TOTAL_COMMITS=$(git rev-list --count "${BASE_SHA}..${HEAD_SHA}" 2>/dev/null || echo "1")
69
79
 
70
- # Count AI origin per-commit
71
- AI_GENERATED=0; AI_ASSISTED=0; HUMAN_ONLY=0
72
- TOTAL_INPUT_TOKENS=0; TOTAL_OUTPUT_TOKENS=0; TOTAL_COST="0"
73
-
74
- for SHA in $(git rev-list "${BASE_SHA}..${HEAD_SHA}" 2>/dev/null || echo "${HEAD_SHA}"); do
75
- MSG=$(git log -1 --format='%B' "${SHA}")
76
- echo "${MSG}" | grep -q '^AI-Origin: ai-generated' && AI_GENERATED=$((AI_GENERATED + 1))
77
- echo "${MSG}" | grep -q '^AI-Origin: ai-assisted' && AI_ASSISTED=$((AI_ASSISTED + 1))
78
- echo "${MSG}" | grep -q '^AI-Origin: human' && HUMAN_ONLY=$((HUMAN_ONLY + 1))
79
-
80
- INPUT_T=$(echo "${MSG}" | grep -oP '(?<=^AI-Input-Tokens: )\d+' || echo "0")
81
- OUTPUT_T=$(echo "${MSG}" | grep -oP '(?<=^AI-Output-Tokens: )\d+' || echo "0")
82
- COST_T=$(echo "${MSG}" | grep -oP '(?<=^AI-Cost: )[0-9.]+' || echo "0")
83
- [[ ${INPUT_T} -gt 1000000 ]] && INPUT_T=0
84
- [[ ${OUTPUT_T} -gt 1000000 ]] && OUTPUT_T=0
85
- TOTAL_INPUT_TOKENS=$((TOTAL_INPUT_TOKENS + INPUT_T))
86
- TOTAL_OUTPUT_TOKENS=$((TOTAL_OUTPUT_TOKENS + OUTPUT_T))
87
- TOTAL_COST=$(echo "${TOTAL_COST} + ${COST_T}" | bc -l)
88
- done
89
-
90
- AI_COMMITS=$((AI_GENERATED + AI_ASSISTED))
91
- [[ "${TOTAL_COMMITS}" -gt 0 ]] && AI_RATIO=$(echo "scale=4; ${AI_COMMITS} / ${TOTAL_COMMITS}" | bc -l | sed 's/^\./0./') || AI_RATIO="0"
92
-
93
- # Calculate lead time (MR created to merged)
80
+ # --- Lead time: MR open → now (this job runs on the post-merge push).
94
81
  LEAD_TIME_SECONDS=0
95
82
  if [[ -n "${CI_MERGE_REQUEST_CREATED_AT}" ]]; then
96
83
  CREATED_TS=$(date -d "${CI_MERGE_REQUEST_CREATED_AT}" +%s 2>/dev/null || echo "0")
97
- MERGED_TS=$(date +%s)
98
- LEAD_TIME_SECONDS=$((MERGED_TS - CREATED_TS))
84
+ if [[ "${CREATED_TS}" -gt 0 ]]; then
85
+ LEAD_TIME_SECONDS=$(( $(date +%s) - CREATED_TS ))
86
+ [[ "${LEAD_TIME_SECONDS}" -lt 0 ]] && LEAD_TIME_SECONDS=0
87
+ fi
88
+ fi
89
+
90
+ # --- Failure-fix label: change-failure-rate numerator as a per-MR fact.
91
+ # On the post-merge push pipeline CI_MERGE_REQUEST_TITLE is usually unset,
92
+ # so fall back to the merge commit title. Matched via printf against a
93
+ # variable rather than interpolated, since titles are user-controlled.
94
+ FIX_TITLE="${CI_MERGE_REQUEST_TITLE:-${CI_COMMIT_TITLE:-}}"
95
+ if printf '%s' "${FIX_TITLE}" | grep -qiE 'revert|hotfix|rollback'; then
96
+ IS_FAILURE_FIX=true
97
+ else
98
+ IS_FAILURE_FIX=false
99
99
  fi
100
100
 
101
101
  TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
102
102
 
103
+ # NOTE: no review verdicts here. GitLab approvals would need an API call
104
+ # with a project token; the GitHub twin gets them from the reviews API.
105
+ # Emitted under `pr` (not `mr`) so both CI providers land in the same
106
+ # shape — the processor only persists a known section list.
103
107
  PR_EVENT=$(jq -n \
104
108
  --arg team_id "${TEAM_ID}" \
105
109
  --arg repo "${CI_PROJECT_PATH}" \
106
110
  --arg timestamp "${TIMESTAMP}" \
107
- --argjson ai_ratio "${AI_RATIO}" \
108
- --argjson total_commits "${TOTAL_COMMITS}" \
109
- --argjson ai_commits "${AI_COMMITS}" \
110
- --argjson ai_generated "${AI_GENERATED}" \
111
- --argjson ai_assisted "${AI_ASSISTED}" \
112
111
  --argjson lead_time_seconds "${LEAD_TIME_SECONDS}" \
112
+ --argjson is_failure_fix "${IS_FAILURE_FIX}" \
113
+ --argjson total_commits "${TOTAL_COMMITS}" \
114
+ --argjson commit_shas "${COMMIT_SHAS}" \
113
115
  --arg mr_iid "${CI_MERGE_REQUEST_IID:-0}" \
114
116
  --arg mr_author "${GITLAB_USER_LOGIN}" \
115
- --argjson total_input_tokens "${TOTAL_INPUT_TOKENS}" \
116
- --argjson total_output_tokens "${TOTAL_OUTPUT_TOKENS}" \
117
- --arg total_cost "${TOTAL_COST}" \
118
117
  '{
119
118
  team_id: $team_id, repo: $repo, timestamp: $timestamp, prism_level: 2,
120
- metric: {name: "ai_to_merge_ratio", value: $ai_ratio, unit: "ratio"},
121
- ai_context: {tool: "gitlab-ci", model: "n/a",
122
- origin: (if $ai_ratio > 0 then "ai-assisted" else "human" end)},
123
- dora: {deployment_frequency: 1, lead_time_seconds: $lead_time_seconds},
124
- ai_dora: {ai_to_merge_ratio: $ai_ratio, total_commits: $total_commits,
125
- ai_commits: $ai_commits, ai_generated_commits: $ai_generated,
126
- ai_assisted_commits: $ai_assisted,
127
- total_input_tokens: $total_input_tokens,
128
- total_output_tokens: $total_output_tokens,
129
- total_cost_usd: (if $total_cost != "0" and $total_cost != "" then ($total_cost | tonumber) else 0 end)},
130
- mr: {iid: ($mr_iid | tonumber), author: $mr_author}
119
+ metric: {name: "pr_merged", value: 1, unit: "count"},
120
+ dora: {lead_time_seconds: $lead_time_seconds, is_failure_fix: $is_failure_fix},
121
+ pr: {number: ($mr_iid | tonumber), author: $mr_author,
122
+ reviews_approved: 0, reviews_changes_requested: 0,
123
+ total_commits: $total_commits, commit_shas: $commit_shas}
131
124
  }')
132
125
 
133
126
  aws events put-events --region "${AWS_DEFAULT_REGION}" \
@@ -138,7 +131,8 @@ prism-ai-metrics:
138
131
  \"Detail\": $(echo "${PR_EVENT}" | jq -c '.' | jq -Rs '.')
139
132
  }]"
140
133
 
141
- # Also emit deploy event
134
+ # Sole deployment signal — deployment_frequency lives ONLY on this event.
135
+ # Carrying it on prism.d1.pr as well double-counted the metric.
142
136
  DEPLOY_EVENT=$(jq -n --arg team_id "${TEAM_ID}" --arg repo "${CI_PROJECT_PATH}" --arg timestamp "${TIMESTAMP}" \
143
137
  '{team_id: $team_id, repo: $repo, timestamp: $timestamp, prism_level: 2,
144
138
  metric: {name: "deployment", value: 1, unit: "count"},
@@ -151,3 +145,6 @@ prism-ai-metrics:
151
145
  \"EventBusName\": \"prism-d1-metrics\",
152
146
  \"Detail\": $(echo "${DEPLOY_EVENT}" | jq -c '.' | jq -Rs '.')
153
147
  }]"
148
+
149
+ echo "Emitted prism.d1.pr + prism.d1.deploy"
150
+ echo " lead time: ${LEAD_TIME_SECONDS}s · failure fix: ${IS_FAILURE_FIX} · commits: ${TOTAL_COMMITS}"
@@ -42,6 +42,7 @@ const dashboardStack = new DashboardStack(app, 'PrismD1Dashboard', {
42
42
  env,
43
43
  description: 'PRISM D1 Velocity - CloudWatch dashboards and alarms',
44
44
  productivityWidgetArn: pipelineStack.otelCollector?.productivityWidgetArn,
45
+ velocityWidgetArn: pipelineStack.velocityWidgetArn,
45
46
  tags: {
46
47
  'prism:project': 'PRISM',
47
48
  'prism:domain': 'D1-Velocity',
@@ -56,6 +56,9 @@ export class OtelCollectorConstruct extends Construct {
56
56
  public readonly userPoolClient?: cognito.UserPoolClient;
57
57
  /** ARN of the CloudWatch custom-widget Lambda (Developer Productivity table). */
58
58
  public productivityWidgetArn?: string;
59
+ /** The otel-receiver Lambda — other constructs may grant themselves invoke
60
+ * for direct aggregation queries (single implementation, no drift). */
61
+ public receiverFunction!: lambda.Function;
59
62
 
60
63
  constructor(scope: Construct, id: string, props: OtelCollectorConstructProps) {
61
64
  super(scope, id);
@@ -198,6 +201,7 @@ export class OtelCollectorConstruct extends Construct {
198
201
  props.aiUsageTable.grantReadWriteData(receiver);
199
202
  this.archiveBucket.grantPut(receiver);
200
203
  props.kmsKey.grantEncryptDecrypt(receiver);
204
+ this.receiverFunction = receiver;
201
205
 
202
206
  // -------------------------------------------------------
203
207
  // CloudWatch Custom Widget: Developer Productivity