@metasession.co/devaudit-cli 0.3.12 → 0.3.13

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 (65) hide show
  1. package/dist/index.js +53 -4
  2. package/dist/index.js.map +1 -1
  3. package/package.json +2 -2
  4. package/scripts/upload-evidence.sh +55 -1
  5. package/sdlc/files/_common/0-project-setup.md +10 -4
  6. package/sdlc/files/_common/Periodic_Security_Review_Schedule.md +5 -5
  7. package/sdlc/files/_common/README_TEMPLATE.md +8 -8
  8. package/sdlc/files/_common/Test_Architecture.md +15 -15
  9. package/sdlc/files/_common/Test_Plan_TEMPLATE.md +13 -13
  10. package/sdlc/files/_common/Test_Policy.md +2 -2
  11. package/sdlc/files/_common/Test_Strategy.md +10 -10
  12. package/sdlc/files/_common/governance/ai-disclosure.md.template +3 -3
  13. package/sdlc/files/_common/governance/dpia.md.template +3 -3
  14. package/sdlc/files/_common/governance/incident-report.md.template +3 -3
  15. package/sdlc/files/_common/governance/nil-incident-report.md.template +1 -1
  16. package/sdlc/files/_common/governance/periodic-review.md.template +3 -3
  17. package/sdlc/files/_common/governance/risk-register.md.template +1 -1
  18. package/sdlc/files/_common/governance/ropa.md.template +2 -2
  19. package/sdlc/files/_common/joining-an-existing-project.md +4 -4
  20. package/sdlc/files/_common/scripts/check-host-deployment.sh +107 -0
  21. package/sdlc/files/_common/scripts/check-host-deployment.test.sh +119 -0
  22. package/sdlc/files/_common/scripts/check-release-pr-scope.sh +95 -0
  23. package/sdlc/files/_common/scripts/check-release-pr-scope.test.sh +137 -0
  24. package/sdlc/files/_common/scripts/close-out-release.sh +175 -54
  25. package/sdlc/files/_common/scripts/close-out-release.test.sh +90 -0
  26. package/sdlc/files/_common/scripts/extract-release-metadata.sh +27 -0
  27. package/sdlc/files/_common/scripts/extract-release-metadata.test.sh +13 -1
  28. package/sdlc/files/_common/scripts/generate-bundled-changes.sh +302 -64
  29. package/sdlc/files/_common/scripts/generate-bundled-changes.test.sh +128 -82
  30. package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.sh +18 -9
  31. package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.test.sh +66 -0
  32. package/sdlc/files/_common/scripts/generate-security-summary.sh +14 -5
  33. package/sdlc/files/_common/scripts/generate-security-summary.test.sh +57 -0
  34. package/sdlc/files/_common/scripts/prepare-release-pr.sh +44 -4
  35. package/sdlc/files/_common/scripts/render-test-cycles.sh +227 -0
  36. package/sdlc/files/_common/scripts/render-test-cycles.test.sh +107 -0
  37. package/sdlc/files/_common/scripts/report-test-cycle.sh +344 -0
  38. package/sdlc/files/_common/scripts/submit-bundle-manifest.sh +100 -0
  39. package/sdlc/files/_common/scripts/submit-bundle-manifest.test.sh +138 -0
  40. package/sdlc/files/_common/scripts/upload-evidence.sh +24 -0
  41. package/sdlc/files/_common/scripts/validate-compliance-artifacts.sh +16 -0
  42. package/sdlc/files/_common/scripts/validate-compliance-artifacts.test.sh +14 -0
  43. package/sdlc/files/_common/scripts/validate-test-summary.test.sh +4 -4
  44. package/sdlc/files/_common/skills/adr-author/SKILL.md +2 -2
  45. package/sdlc/files/_common/skills/e2e-test-engineer/SKILL.md +2 -2
  46. package/sdlc/files/_common/skills/e2e-test-engineer/references/bootstrap.md +5 -5
  47. package/sdlc/files/_common/skills/governance-doc-author/references/incident-classification.md +1 -1
  48. package/sdlc/files/_common/skills/requirements-aligner/SKILL.md +4 -4
  49. package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +21 -2
  50. package/sdlc/files/_common/skills/sdlc-implementer/references/change-request-loop.md +1 -1
  51. package/sdlc/files/_common/skills/sdlc-implementer/references/compliance-constraints.md +7 -14
  52. package/sdlc/files/ci/ci.yml.template +82 -28
  53. package/sdlc/files/ci/close-out-release.yml.template +7 -4
  54. package/sdlc/files/ci/compliance-evidence.yml.template +204 -20
  55. package/sdlc/files/ci/feature-e2e.yml.template +84 -1
  56. package/sdlc/files/ci/post-deploy-prod.yml.template +59 -4
  57. package/sdlc/files/ci/python/ci.yml.template +60 -12
  58. package/sdlc/files/ci/quality-gates-provenance.yml.template +123 -28
  59. package/sdlc/package.json +1 -1
  60. package/sdlc/src/bin/devaudit-sdlc.js +44 -2
  61. package/sdlc/src/blueprints/1-plan-requirement.raw.md +6 -1
  62. package/sdlc/src/blueprints/2-implement-and-test.raw.md +1 -1
  63. package/sdlc/src/blueprints/3-compile-evidence.raw.md +34 -20
  64. package/sdlc/src/blueprints/4-submit-for-review.raw.md +2 -2
  65. package/sdlc/src/blueprints/5-deploy-main.raw.md +2 -2
@@ -2,11 +2,20 @@
2
2
  # close-out-release.sh — Reconcile the local compliance tree after a release
3
3
  # is marked `released` on the DevAudit portal.
4
4
  #
5
- # For one requirement it: flips the release ticket Status -> RELEASED (backlinks
6
- # the release PR + records the sign-off), flips the matching compliance/RTM.md
7
- # row -> RELEASED, and moves the ticket from compliance/pending-releases/ to
8
- # compliance/approved-releases/. It stages the changes but does NOT commit — the
9
- # caller (the close-out workflow, or a human) commits/opens the PR.
5
+ # For the governing requirement it: flips the release ticket Status -> RELEASED
6
+ # (backlinks the release PR + records the sign-off), flips the matching
7
+ # compliance/RTM.md row -> RELEASED, and moves the ticket from
8
+ # compliance/pending-releases/ to compliance/approved-releases/.
9
+ #
10
+ # If the release carries a bundle manifest, explicit predecessor members are
11
+ # reconciled too: their release tickets move to compliance/superseded-releases/,
12
+ # ticket status flips to SUPERSEDED, the successor + reason are recorded, and
13
+ # tracked REQ rows in compliance/RTM.md flip to SUPERSEDED. This keeps absorbed
14
+ # predecessor releases from looking abandoned after the successor approval
15
+ # envelope closes.
16
+ #
17
+ # The script stages the changes but does NOT commit — the caller (the close-out
18
+ # workflow, or a human) commits/opens the PR.
10
19
  #
11
20
  # Usage:
12
21
  # ./scripts/close-out-release.sh <REQ-ID> [--release-pr <url-or-number>]
@@ -42,10 +51,147 @@ fi
42
51
 
43
52
  PENDING="compliance/pending-releases/RELEASE-TICKET-${REQ_ID}.md"
44
53
  APPROVED_DIR="compliance/approved-releases"
54
+ SUPERSEDED_DIR="compliance/superseded-releases"
45
55
  APPROVED="${APPROVED_DIR}/RELEASE-TICKET-${REQ_ID}.md"
46
56
  RTM="compliance/RTM.md"
47
57
  TODAY="$(date +%Y-%m-%d)"
48
58
 
59
+ find_bundle_manifest_file() {
60
+ local req_id="$1"
61
+ local candidate=""
62
+ for candidate in \
63
+ "compliance/pending-releases/BUNDLED-CHANGES-${req_id}.json" \
64
+ "compliance/approved-releases/BUNDLED-CHANGES-${req_id}.json" \
65
+ "compliance/superseded-releases/BUNDLED-CHANGES-${req_id}.json"; do
66
+ if [ -f "$candidate" ]; then
67
+ printf '%s' "$candidate"
68
+ return 0
69
+ fi
70
+ done
71
+ return 1
72
+ }
73
+
74
+ find_release_ticket_file() {
75
+ local version="$1"
76
+ local candidate=""
77
+ for candidate in \
78
+ "compliance/pending-releases/RELEASE-TICKET-${version}.md" \
79
+ "compliance/approved-releases/RELEASE-TICKET-${version}.md" \
80
+ "compliance/superseded-releases/RELEASE-TICKET-${version}.md"; do
81
+ if [ -f "$candidate" ]; then
82
+ printf '%s' "$candidate"
83
+ return 0
84
+ fi
85
+ done
86
+ return 1
87
+ }
88
+
89
+ update_rtm_status() {
90
+ local req_id="$1"
91
+ local target_status="$2"
92
+ local tmp_file
93
+ tmp_file="$(mktemp)"
94
+ if [ -f "$RTM" ] && grep -qE "^\| ${req_id} " "$RTM"; then
95
+ awk -v req="$req_id" -v target_status="$target_status" '
96
+ BEGIN { FS="|"; OFS="|"; statuscol=0 }
97
+ /\\\|/ { gsub(/\\\|/, "\001", $0) }
98
+ {
99
+ cand=0; idseen=0
100
+ for (i=1; i<=NF; i++) {
101
+ c=$i; gsub(/^[[:space:]]+|[[:space:]]+$/, "", c)
102
+ if (c=="Status") cand=i
103
+ if (c=="ID" || c=="REQ-ID" || c=="REQ ID" || c ~ /^Requirement/) idseen=1
104
+ }
105
+ if (cand>0 && idseen) statuscol=cand
106
+ }
107
+ $0 ~ ("^\\| " req " ") && statuscol>0 {
108
+ cell=$statuscol
109
+ note=""
110
+ if (match(cell, /\(/)) note=substr(cell, RSTART)
111
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", note)
112
+ $statuscol = (note != "" ? " " target_status " " note " " : " " target_status " ")
113
+ gsub(/\001/, "\\|", $0)
114
+ print; next
115
+ }
116
+ { gsub(/\001/, "\\|", $0); print }
117
+ ' "$RTM" > "$tmp_file" && mv "$tmp_file" "$RTM"
118
+ git add "$RTM" 2>/dev/null || true
119
+ echo "RTM row ${req_id} -> ${target_status}."
120
+ else
121
+ rm -f "$tmp_file"
122
+ fi
123
+ }
124
+
125
+ mark_ticket_superseded() {
126
+ local version="$1"
127
+ local reason="$2"
128
+ local relationship="$3"
129
+ local source_path target_path tmp_file
130
+ source_path="$(find_release_ticket_file "$version" 2>/dev/null || true)"
131
+ [ -n "$source_path" ] || return 0
132
+
133
+ mkdir -p "$SUPERSEDED_DIR"
134
+ target_path="${SUPERSEDED_DIR}/RELEASE-TICKET-${version}.md"
135
+ if [ "$source_path" != "$target_path" ]; then
136
+ git mv "$source_path" "$target_path" 2>/dev/null || mv "$source_path" "$target_path"
137
+ echo "Moved predecessor ticket -> ${target_path}"
138
+ fi
139
+
140
+ tmp_file="$(mktemp)"
141
+ awk \
142
+ -v successor="$REQ_ID" \
143
+ -v superseded_on="$TODAY" \
144
+ -v superseded_reason="$reason" \
145
+ -v superseded_relationship="$relationship" '
146
+ BEGIN {
147
+ status_done=0
148
+ successor_seen=0
149
+ reason_seen=0
150
+ relationship_seen=0
151
+ date_seen=0
152
+ }
153
+ /^\*\*Status:\*\*/ && status_done==0 {
154
+ print "**Status:** SUPERSEDED"
155
+ status_done=1
156
+ next
157
+ }
158
+ /^\*\*Superseded by:\*\*/ {
159
+ print "**Superseded by:** " successor
160
+ successor_seen=1
161
+ next
162
+ }
163
+ /^\*\*Supersession reason:\*\*/ {
164
+ print "**Supersession reason:** " superseded_reason
165
+ reason_seen=1
166
+ next
167
+ }
168
+ /^\*\*Supersession relationship:\*\*/ {
169
+ print "**Supersession relationship:** " superseded_relationship
170
+ relationship_seen=1
171
+ next
172
+ }
173
+ /^\*\*Superseded on:\*\*/ {
174
+ print "**Superseded on:** " superseded_on
175
+ date_seen=1
176
+ next
177
+ }
178
+ { print }
179
+ /^\*\*DevAudit Release:\*\*/ {
180
+ if (!successor_seen) print "**Superseded by:** " successor
181
+ if (!reason_seen) print "**Supersession reason:** " superseded_reason
182
+ if (!relationship_seen) print "**Supersession relationship:** " superseded_relationship
183
+ if (!date_seen) print "**Superseded on:** " superseded_on
184
+ }
185
+ ' "$target_path" > "$tmp_file"
186
+ mv "$tmp_file" "$target_path"
187
+ git add "$target_path" 2>/dev/null || true
188
+ echo "Ticket ${version} -> SUPERSEDED."
189
+
190
+ if printf '%s' "$version" | grep -qE '^REQ-[0-9]{3,}$'; then
191
+ update_rtm_status "$version" "SUPERSEDED"
192
+ fi
193
+ }
194
+
49
195
  # ── Optional portal safety check ─────────────────────────────────────────────
50
196
  if [ -n "${DEVAUDIT_API_KEY:-}" ] && [ -n "${DEVAUDIT_BASE_URL:-}" ]; then
51
197
  BASE="${DEVAUDIT_BASE_URL%/}"
@@ -76,6 +222,7 @@ fi
76
222
 
77
223
  # ── Move ticket pending -> approved (if still pending) ───────────────────────
78
224
  mkdir -p "$APPROVED_DIR"
225
+ mkdir -p "$SUPERSEDED_DIR"
79
226
  if [ -f "$PENDING" ]; then
80
227
  git mv "$PENDING" "$APPROVED" 2>/dev/null || mv "$PENDING" "$APPROVED"
81
228
  echo "Moved ticket -> ${APPROVED}"
@@ -115,56 +262,30 @@ mv "$TMP" "$APPROVED"
115
262
  git add "$APPROVED" 2>/dev/null || true
116
263
  echo "Ticket Status -> RELEASED."
117
264
 
118
- # ── Flip the RTM row -> RELEASED (preserve any parenthetical note) ───────────
119
- if [ -f "$RTM" ] && grep -qE "^\| ${REQ_ID} " "$RTM"; then
120
- # Table-aware Status column resolution (#72): the previous version locked
121
- # `statuscol` on the FIRST header that contained "Status", which mangled the
122
- # wrong column when the RTM has a small legend table above the main matrix
123
- # (e.g. a 2-column legend with `Status | Description` columns → statuscol=2,
124
- # then the awk overwrote col-1 REQ-ID for every row).
125
- #
126
- # Fix: re-evaluate `statuscol` on EVERY header-shaped row (a row whose cells
127
- # carry the literal header text "Status" + an ID-like column header). The
128
- # legend has "Status" but no ID-like column → not locked; the main RTM has
129
- # both → locks correctly. Data rows don't carry the literal "Status" header
130
- # text in any cell, so they don't re-trigger the lock. Separator rows
131
- # (`|---|---|…`) are left intact and don't affect `statuscol`.
132
- awk -v req="$REQ_ID" '
133
- BEGIN { FS="|"; OFS="|"; statuscol=0 }
134
- # Protect escaped pipes (\|) from being treated as column delimiters.
135
- # Replacing \| with \001 in $0 forces awk to re-split on only unescaped
136
- # pipes, so literal pipes inside cell content (e.g. regex patterns like
137
- # stop\|unsubscribe, enum values open\|in_progress\|closed) do not create
138
- # phantom extra columns. Placeholders are restored to \| before output.
139
- /\\\|/ { gsub(/\\\|/, "\001", $0) }
140
- # Header detection: scan every row; require both a "Status" header cell
141
- # AND an ID-like header cell in the same row before locking statuscol to
142
- # this row''s column index.
143
- {
144
- cand=0; idseen=0
145
- for (i=1; i<=NF; i++) {
146
- c=$i; gsub(/^[[:space:]]+|[[:space:]]+$/, "", c)
147
- if (c=="Status") cand=i
148
- if (c=="ID" || c=="REQ-ID" || c=="REQ ID" || c ~ /^Requirement/) idseen=1
149
- }
150
- if (cand>0 && idseen) statuscol=cand
151
- }
152
- $0 ~ ("^\\| " req " ") && statuscol>0 {
153
- cell=$statuscol
154
- note=""
155
- if (match(cell, /\(/)) note=substr(cell, RSTART) # preserve any " (requirement note)"
156
- gsub(/^[[:space:]]+|[[:space:]]+$/, "", note)
157
- $statuscol = (note != "" ? " RELEASED " note " " : " RELEASED ")
158
- gsub(/\001/, "\\|", $0)
159
- print; next
160
- }
161
- { gsub(/\001/, "\\|", $0); print }
162
- ' "$RTM" > "$TMP" && mv "$TMP" "$RTM"
163
- git add "$RTM" 2>/dev/null || true
164
- echo "RTM row ${REQ_ID} -> RELEASED."
165
- else
265
+ update_rtm_status "$REQ_ID" "RELEASED" || true
266
+ if ! grep -qE "^\| ${REQ_ID} " "$RTM" 2>/dev/null; then
166
267
  echo "::warning::No RTM row for ${REQ_ID} in ${RTM} — skipped RTM flip."
167
- rm -f "$TMP"
268
+ fi
269
+
270
+ # ── Reconcile explicit predecessors from the bundle manifest ──────────────────
271
+ BUNDLE_MANIFEST="$(find_bundle_manifest_file "$REQ_ID" 2>/dev/null || true)"
272
+ if [ -n "$BUNDLE_MANIFEST" ] && command -v jq >/dev/null 2>&1; then
273
+ while IFS=$'\t' read -r member_version member_reason member_relationship; do
274
+ [ -n "$member_version" ] || continue
275
+ mark_ticket_superseded "$member_version" "$member_reason" "$member_relationship"
276
+ done < <(
277
+ jq -r --arg successor "$REQ_ID" '
278
+ (.members // [])
279
+ | map(select((.relationship // "") == "superseded" or (.relationship // "") == "absorbed"))
280
+ | .[]
281
+ | [
282
+ (.version // ""),
283
+ (.reason // ("Absorbed into successor release " + $successor + ".")),
284
+ (.relationship // "superseded")
285
+ ]
286
+ | @tsv
287
+ ' "$BUNDLE_MANIFEST" 2>/dev/null || true
288
+ )
168
289
  fi
169
290
 
170
291
  echo "Close-out staged for ${REQ_ID}. Commit + open a PR to develop to land it."
@@ -195,6 +195,96 @@ EOF
195
195
  rm -rf "$(dirname "$dir")"
196
196
  }
197
197
 
198
+ # ── Case 4: bundled close-out supersedes predecessor tickets ──────────────────
199
+ {
200
+ dir="$(mktemp -d)/cli-close-out-fixture-4"
201
+ mkdir -p "$dir/compliance/pending-releases" \
202
+ "$dir/compliance/approved-releases" \
203
+ "$dir/compliance/superseded-releases"
204
+ cd "$dir"
205
+ git init -q --initial-branch=main >/dev/null
206
+ git config user.email "test@example.com"
207
+ git config user.name "test"
208
+ cat > compliance/RTM.md <<'EOF'
209
+ # Requirements Traceability Matrix
210
+
211
+ | REQ-ID | Source | Risk | Evidence | Status | Owner | Date |
212
+ | ------- | ------ | ---- | -------- | ------ | ----- | ---- |
213
+ | REQ-090 | #500 | HIGH | compliance/evidence/REQ-090/ | TESTED - PENDING SIGN-OFF | test | 2026-07-16 |
214
+ | REQ-089 | #499 | LOW | compliance/evidence/REQ-089/ | TESTED - PENDING SIGN-OFF | test | 2026-07-15 |
215
+ EOF
216
+ cat > compliance/pending-releases/RELEASE-TICKET-REQ-090.md <<'EOF'
217
+ # Release Ticket: REQ-090
218
+
219
+ **Status:** TESTED - PENDING SIGN-OFF
220
+ **DevAudit Release:** REQ-090
221
+ EOF
222
+ cat > compliance/pending-releases/RELEASE-TICKET-REQ-089.md <<'EOF'
223
+ # Release Ticket: REQ-089
224
+
225
+ **Status:** TESTED - PENDING SIGN-OFF
226
+ **DevAudit Release:** REQ-089
227
+ EOF
228
+ cat > compliance/pending-releases/RELEASE-TICKET-v2026.07.10.md <<'EOF'
229
+ # Release Ticket: v2026.07.10
230
+
231
+ **Status:** TESTED - PENDING SIGN-OFF
232
+ **DevAudit Release:** v2026.07.10
233
+ EOF
234
+ cat > compliance/pending-releases/BUNDLED-CHANGES-REQ-090.json <<'EOF'
235
+ {
236
+ "schemaVersion": 1,
237
+ "approvalRelease": { "version": "REQ-090" },
238
+ "coreRelease": { "version": "REQ-090" },
239
+ "members": [
240
+ {
241
+ "version": "REQ-089",
242
+ "role": "predecessor",
243
+ "relationship": "superseded",
244
+ "reason": "Explicit predecessor release absorbed into approval envelope REQ-090."
245
+ },
246
+ {
247
+ "version": "v2026.07.10",
248
+ "role": "housekeeping",
249
+ "relationship": "absorbed",
250
+ "reason": "Explicit housekeeping release ticket carried forward into approval envelope REQ-090."
251
+ }
252
+ ],
253
+ "nonReleaseWorkItems": [],
254
+ "manifestHash": "sha256:test"
255
+ }
256
+ EOF
257
+ git add -A
258
+ git commit -q -m "fixture: bundled close-out"
259
+ unset DEVAUDIT_API_KEY DEVAUDIT_BASE_URL || true
260
+ bash "$HELPER" REQ-090 >/dev/null 2>&1 || true
261
+
262
+ [ -f compliance/approved-releases/RELEASE-TICKET-REQ-090.md ] \
263
+ && assert_eq "current release moved to approved-releases" "yes" "yes" \
264
+ || assert_eq "current release moved to approved-releases" "yes" "no"
265
+ [ -f compliance/superseded-releases/RELEASE-TICKET-REQ-089.md ] \
266
+ && assert_eq "predecessor moved to superseded-releases" "yes" "yes" \
267
+ || assert_eq "predecessor moved to superseded-releases" "yes" "no"
268
+ [ -f compliance/superseded-releases/RELEASE-TICKET-v2026.07.10.md ] \
269
+ && assert_eq "housekeeping predecessor moved to superseded-releases" "yes" "yes" \
270
+ || assert_eq "housekeeping predecessor moved to superseded-releases" "yes" "no"
271
+
272
+ grep -qF '**Status:** SUPERSEDED' compliance/superseded-releases/RELEASE-TICKET-REQ-089.md \
273
+ && assert_eq "predecessor status flipped" "yes" "yes" \
274
+ || assert_eq "predecessor status flipped" "yes" "no"
275
+ grep -qF '**Superseded by:** REQ-090' compliance/superseded-releases/RELEASE-TICKET-REQ-089.md \
276
+ && assert_eq "predecessor successor recorded" "yes" "yes" \
277
+ || assert_eq "predecessor successor recorded" "yes" "no"
278
+ grep -qF 'Explicit predecessor release absorbed into approval envelope REQ-090.' compliance/superseded-releases/RELEASE-TICKET-REQ-089.md \
279
+ && assert_eq "predecessor reason recorded" "yes" "yes" \
280
+ || assert_eq "predecessor reason recorded" "yes" "no"
281
+
282
+ row=$(grep -m1 -E "^\| REQ-089 " compliance/RTM.md || true)
283
+ col5=$(echo "$row" | awk -F '|' '{gsub(/^[[:space:]]+|[[:space:]]+$/,"",$6); print $6}')
284
+ assert_eq "predecessor RTM row flipped" "SUPERSEDED" "$col5"
285
+ rm -rf "$(dirname "$dir")"
286
+ }
287
+
198
288
  echo
199
289
  echo "Result: $PASS passed, $FAIL failed"
200
290
  [ "$FAIL" = "0" ]
@@ -41,6 +41,21 @@ find_bundled_changes_file() {
41
41
  return 1
42
42
  }
43
43
 
44
+ find_bundled_manifest_file() {
45
+ local req_id="$1"
46
+ local candidate=""
47
+ for candidate in \
48
+ "compliance/pending-releases/BUNDLED-CHANGES-${req_id}.json" \
49
+ "compliance/approved-releases/BUNDLED-CHANGES-${req_id}.json" \
50
+ "compliance/superseded-releases/BUNDLED-CHANGES-${req_id}.json"; do
51
+ if [ -f "$candidate" ]; then
52
+ printf '%s' "$candidate"
53
+ return 0
54
+ fi
55
+ done
56
+ return 1
57
+ }
58
+
44
59
  extract_release_metadata() {
45
60
  local req_id="$1"
46
61
  RELEASE_TITLE=""
@@ -140,7 +155,19 @@ extract_release_metadata() {
140
155
  bundled_file=$(find_bundled_changes_file "$req_id" 2>/dev/null || true)
141
156
  if [ -n "$bundled_file" ]; then
142
157
  local bundled_note
158
+ local bundled_manifest=""
159
+ local manifest_hash=""
160
+ bundled_manifest=$(find_bundled_manifest_file "$req_id" 2>/dev/null || true)
161
+ if [ -n "$bundled_manifest" ] && command -v jq >/dev/null 2>&1; then
162
+ manifest_hash=$(jq -r '.manifestHash // empty' "$bundled_manifest" 2>/dev/null || true)
163
+ fi
143
164
  bundled_note="Bundled release context: see \`${bundled_file}\`."
165
+ if [ -n "$bundled_manifest" ]; then
166
+ bundled_note="${bundled_note} Manifest: \`${bundled_manifest}\`."
167
+ fi
168
+ if [ -n "$manifest_hash" ]; then
169
+ bundled_note="${bundled_note} Hash: \`${manifest_hash}\`."
170
+ fi
144
171
  if [ -n "$RELEASE_SUMMARY" ]; then
145
172
  case "$RELEASE_SUMMARY" in
146
173
  *"Bundled release context:"*) ;;
@@ -374,13 +374,25 @@ cat > "compliance/pending-releases/BUNDLED-CHANGES-REQ-017.md" <<'BUNDLE'
374
374
 
375
375
  - `abc1234` chore: sync templates
376
376
  BUNDLE
377
+ cat > "compliance/pending-releases/BUNDLED-CHANGES-REQ-017.json" <<'BUNDLEJSON'
378
+ {
379
+ "schemaVersion": 1,
380
+ "approvalRelease": { "version": "REQ-017" },
381
+ "coreRelease": { "version": "REQ-017" },
382
+ "members": [],
383
+ "nonReleaseWorkItems": [
384
+ { "kind": "housekeeping_commit", "title": "chore: sync templates", "reference": "abc1234" }
385
+ ],
386
+ "manifestHash": "sha256:test-bundle-hash"
387
+ }
388
+ BUNDLEJSON
377
389
 
378
390
  source "$HELPER"
379
391
  extract_release_metadata "REQ-017"
380
392
  assert_eq "Bundled note appended to summary" \
381
393
  "Core tracked change summary.
382
394
 
383
- Bundled release context: see \`compliance/pending-releases/BUNDLED-CHANGES-REQ-017.md\`." \
395
+ Bundled release context: see \`compliance/pending-releases/BUNDLED-CHANGES-REQ-017.md\`. Manifest: \`compliance/pending-releases/BUNDLED-CHANGES-REQ-017.json\`. Hash: \`sha256:test-bundle-hash\`." \
384
396
  "$RELEASE_SUMMARY"
385
397
  echo ""
386
398