@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
@@ -1,99 +1,337 @@
1
1
  #!/usr/bin/env bash
2
2
  # generate-bundled-changes.sh
3
3
  #
4
- # Scans commits since a given ref (tag, SHA, or branch) and filters for
5
- # housekeeping commit types (chore/docs/ci/build/test/revert) that were
6
- # skipped via [skip ci] or otherwise not individually released. Outputs
7
- # a markdown summary suitable for upload as `bundled_changes` evidence
8
- # against a REQ-tagged release.
4
+ # Emits:
5
+ # 1. human-readable bundled-changes markdown on stdout
6
+ # 2. optional machine-readable bundle manifest JSON via --json-out
9
7
  #
10
- # Synced into the consumer's `scripts/` by `devaudit update`; invoked
11
- # from `ci.yml` register-release job when the derived version is a
12
- # REQ-tagged release (not bare-date).
8
+ # Source of truth:
9
+ # - explicit predecessor release membership comes from release tickets
10
+ # - commit scanning contributes only non-release housekeeping work items
13
11
  #
14
12
  # Usage:
15
- # bash scripts/generate-bundled-changes.sh <since-ref> [<version>]
16
- #
17
- # Where:
18
- # <since-ref> — git ref to scan from (tag, SHA, or branch name).
19
- # Typically the previous release tag or the merge-base
20
- # of the current branch and the previous release.
21
- # <version> — (optional) the release version for the header.
22
- # Defaults to "current release".
23
- #
24
- # Output (stdout): markdown document listing housekeeping commits
25
- # absorbed into this release.
26
- #
27
- # DevAudit-Installer#220.
13
+ # bash scripts/generate-bundled-changes.sh <since-ref> [<version>] [--json-out <path>]
28
14
 
29
15
  set -euo pipefail
30
16
 
31
17
  SINCE_REF="${1:-}"
32
- VERSION="${2:-current release}"
18
+ VERSION="current release"
19
+ if [ "${2:-}" != "" ] && [[ "${2:-}" != --* ]]; then
20
+ VERSION="$2"
21
+ shift 2
22
+ else
23
+ shift $(( $# > 0 ? 1 : 0 ))
24
+ fi
25
+
26
+ JSON_OUT=""
27
+ while [ "$#" -gt 0 ]; do
28
+ case "$1" in
29
+ --json-out) JSON_OUT="$2"; shift 2 ;;
30
+ *) echo "Unknown argument: $1" >&2; exit 2 ;;
31
+ esac
32
+ done
33
33
 
34
34
  if [ -z "$SINCE_REF" ]; then
35
- echo "Usage: bash scripts/generate-bundled-changes.sh <since-ref> [<version>]" >&2
35
+ echo "Usage: bash scripts/generate-bundled-changes.sh <since-ref> [<version>] [--json-out <path>]" >&2
36
36
  exit 1
37
37
  fi
38
38
 
39
- # Verify the ref exists before proceeding.
40
39
  if ! git rev-parse --verify "$SINCE_REF" >/dev/null 2>&1; then
41
40
  echo "Error: ref '$SINCE_REF' not found in git history." >&2
42
41
  exit 1
43
42
  fi
44
43
 
45
- # Housekeeping commit type prefixes (Conventional Commits).
46
- # feat/fix are tracked changes — excluded. Anything else is housekeeping.
47
- # The git log format is "<short-sha>\t<subject>", so the regex matches
48
- # the commit type after the tab character.
49
- HOUSEKEEPING_TYPES='^[0-9a-f]+ (chore|docs|ci|build|test|revert|style|perf|refactor)(\(.+\))?!?:'
50
-
51
- # Collect commits since the ref, filtering for housekeeping types.
52
- # Format: <short-sha>\t<subject>
53
- COMMITS=$(git log "$SINCE_REF"..HEAD --format='%h %s' 2>/dev/null || true)
54
-
55
- if [ -z "$COMMITS" ]; then
56
- # No commits since ref — output an empty summary.
57
- echo "## Bundled Changes"
58
- echo ""
59
- echo "No housekeeping commits found since \`${SINCE_REF}\`."
60
- echo ""
61
- exit 0
44
+ if ! command -v jq >/dev/null 2>&1; then
45
+ echo "Error: jq is required." >&2
46
+ exit 1
62
47
  fi
63
48
 
64
- # Filter for housekeeping commit types.
65
- BUNDLED=$(echo "$COMMITS" | grep -E "$HOUSEKEEPING_TYPES" || true)
49
+ HOUSEKEEPING_TYPES='^[0-9a-f]+[[:space:]]+(chore|docs|ci|build|test|revert|style|perf|refactor)(\([^)]+\))?!?:'
50
+ DATE_VERSION_RE='^v[0-9]{4}\.[0-9]{2}\.[0-9]{2}([.][0-9]+)?$'
51
+
52
+ find_release_ticket() {
53
+ local version="$1"
54
+ local candidate=""
55
+ for candidate in \
56
+ "compliance/pending-releases/RELEASE-TICKET-${version}.md" \
57
+ "compliance/approved-releases/RELEASE-TICKET-${version}.md" \
58
+ "compliance/superseded-releases/RELEASE-TICKET-${version}.md"; do
59
+ if [ -f "$candidate" ]; then
60
+ printf '%s' "$candidate"
61
+ return 0
62
+ fi
63
+ done
64
+ return 1
65
+ }
66
+
67
+ trim() {
68
+ printf '%s' "$1" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//'
69
+ }
70
+
71
+ extract_ticket_title() {
72
+ local file="$1"
73
+ local line=""
74
+ line=$(grep -m1 '^\*\*Requirement:\*\*' "$file" 2>/dev/null || true)
75
+ if [ -n "$line" ]; then
76
+ printf '%s' "$line" \
77
+ | sed -E 's/^\*\*Requirement:\*\*[[:space:]]*//' \
78
+ | sed -E 's/^REQ-[0-9]+[[:space:]]*[—–:-][[:space:]]*//' \
79
+ | sed -E 's/[[:space:]]*$//'
80
+ return 0
81
+ fi
82
+ line=$(grep -m1 '^# ' "$file" 2>/dev/null || true)
83
+ if [ -n "$line" ]; then
84
+ printf '%s' "$line" \
85
+ | sed -E 's/^# *//' \
86
+ | sed -E 's/^Release Ticket[[:space:]]*[—:-][[:space:]]*//' \
87
+ | sed -E 's/^REQ-[0-9]+[[:space:]]*[—:-][[:space:]]*//' \
88
+ | sed -E 's/[[:space:]]*$//'
89
+ fi
90
+ }
91
+
92
+ extract_ticket_summary() {
93
+ local file="$1"
94
+ awk '
95
+ /^## Summary/ { found=1; next }
96
+ /^## / { if (found) exit }
97
+ found { print }
98
+ ' "$file" 2>/dev/null \
99
+ | sed -E 's/[[:space:]]+$//' \
100
+ | awk 'NF { p=1 } p { print }' \
101
+ | awk '{ lines[NR]=$0 } END { for (i=NR; i>=1; i--) { if (lines[i] ~ /[^[:space:]]/) { last=i; break } } for (i=1; i<=last; i++) print lines[i] }'
102
+ }
103
+
104
+ extract_ticket_pr() {
105
+ local file="$1" line pr_num pr_url
106
+ line=$(grep -m1 '^\*\*PR:\*\*' "$file" 2>/dev/null || true)
107
+ pr_num=$(printf '%s' "$line" | grep -oE '#[0-9]+' | head -1 | tr -d '#' || true)
108
+ pr_url=$(printf '%s' "$line" | grep -oE 'https://[^ )]+' | head -1 || true)
109
+ printf '%s\t%s' "$pr_num" "$pr_url"
110
+ }
66
111
 
67
- if [ -z "$BUNDLED" ]; then
68
- echo "## Bundled Changes"
69
- echo ""
70
- echo "No housekeeping commits found since \`${SINCE_REF}\`."
71
- echo ""
72
- exit 0
112
+ extract_explicit_predecessors() {
113
+ local file="$1" line=""
114
+ line=$(grep -m1 '^\- \*\*Absorbed predecessor releases:\*\*' "$file" 2>/dev/null || true)
115
+ if [ -z "$line" ]; then
116
+ return 0
117
+ fi
118
+ if printf '%s' "$line" | grep -qi '\bNone\b'; then
119
+ return 0
120
+ fi
121
+ printf '%s' "$line" | grep -oE 'REQ-[0-9]+|v[0-9]{4}\.[0-9]{2}\.[0-9]{2}([.][0-9]+)?' || true
122
+ }
123
+
124
+ derive_commit_range() {
125
+ local version="$1" file="$2"
126
+ local from="" to="" sha=""
127
+ if [[ "$version" =~ ^REQ-[0-9]+$ ]]; then
128
+ while IFS= read -r sha; do
129
+ [ -n "$sha" ] || continue
130
+ [ -n "$from" ] || from="${sha:0:7}"
131
+ to="${sha:0:7}"
132
+ done < <(git log --reverse --format='%H' --grep "\\[${version}\\]\\|Ref: ${version}" 2>/dev/null || true)
133
+ fi
134
+ printf '%s\t%s' "$from" "$to"
135
+ }
136
+
137
+ CURRENT_TICKET="$(find_release_ticket "$VERSION" 2>/dev/null || true)"
138
+ if [ -z "$CURRENT_TICKET" ] && [ -d "compliance/pending-releases" ]; then
139
+ echo "Error: release ticket for ${VERSION} not found; cannot build explicit bundle manifest." >&2
140
+ exit 1
73
141
  fi
74
142
 
75
- # Count the bundled commits.
76
- COUNT=$(echo "$BUNDLED" | wc -l | tr -d ' ')
143
+ mapfile -t CANDIDATE_RELEASES < <(
144
+ if [ -d "compliance/pending-releases" ]; then
145
+ for ticket in compliance/pending-releases/RELEASE-TICKET-*.md; do
146
+ [ -f "$ticket" ] || continue
147
+ version="${ticket##*/RELEASE-TICKET-}"
148
+ version="${version%.md}"
149
+ [ "$version" = "$VERSION" ] && continue
150
+ printf '%s\n' "$version"
151
+ done
152
+ fi
153
+ )
154
+
155
+ mapfile -t EXPLICIT_PREDECESSORS < <(
156
+ if [ -n "$CURRENT_TICKET" ]; then
157
+ extract_explicit_predecessors "$CURRENT_TICKET"
158
+ fi
159
+ )
160
+
161
+ declare -A EXPLICIT_SET=()
162
+ for version in "${EXPLICIT_PREDECESSORS[@]}"; do
163
+ if [ "$version" = "$VERSION" ]; then
164
+ echo "Error: bundle manifest cannot self-supersede ${VERSION}." >&2
165
+ exit 1
166
+ fi
167
+ if [ -n "${EXPLICIT_SET[$version]:-}" ]; then
168
+ echo "Error: duplicate explicit predecessor '${version}'." >&2
169
+ exit 1
170
+ fi
171
+ EXPLICIT_SET["$version"]=1
172
+ if [ -z "$(find_release_ticket "$version" 2>/dev/null || true)" ]; then
173
+ echo "Error: explicit predecessor '${version}' has no release ticket on disk." >&2
174
+ exit 1
175
+ fi
176
+ done
177
+
178
+ if [ "${#CANDIDATE_RELEASES[@]}" -gt 0 ] && [ "${#EXPLICIT_PREDECESSORS[@]}" -eq 0 ]; then
179
+ echo "Error: ambiguous predecessor ownership for ${VERSION}. Pending release tickets exist but the release ticket does not explicitly list absorbed predecessor releases." >&2
180
+ printf 'Candidates: %s\n' "${CANDIDATE_RELEASES[*]}" >&2
181
+ exit 1
182
+ fi
183
+
184
+ UNACCOUNTED=()
185
+ for version in "${CANDIDATE_RELEASES[@]}"; do
186
+ if [ -z "${EXPLICIT_SET[$version]:-}" ]; then
187
+ UNACCOUNTED+=("$version")
188
+ fi
189
+ done
190
+ if [ "${#UNACCOUNTED[@]}" -gt 0 ]; then
191
+ echo "Error: ambiguous predecessor ownership for ${VERSION}. The following pending releases are not explicitly listed in the release ticket bundle section:" >&2
192
+ printf ' - %s\n' "${UNACCOUNTED[@]}" >&2
193
+ exit 1
194
+ fi
195
+
196
+ MEMBERS='[]'
197
+ PREDECESSOR_LINES=()
198
+ for version in "${EXPLICIT_PREDECESSORS[@]}"; do
199
+ ticket="$(find_release_ticket "$version")"
200
+ title="$(extract_ticket_title "$ticket")"
201
+ summary="$(extract_ticket_summary "$ticket")"
202
+ pr_meta="$(extract_ticket_pr "$ticket")"
203
+ pr_number="${pr_meta%%$'\t'*}"
204
+ pr_url="${pr_meta#*$'\t'}"
205
+ range_meta="$(derive_commit_range "$version" "$ticket")"
206
+ commit_from="${range_meta%%$'\t'*}"
207
+ commit_to="${range_meta#*$'\t'}"
208
+
209
+ if [[ "$version" =~ $DATE_VERSION_RE ]]; then
210
+ role="housekeeping"
211
+ relationship="absorbed"
212
+ reason="Explicit housekeeping release ticket carried forward into approval envelope ${VERSION}."
213
+ else
214
+ role="predecessor"
215
+ relationship="superseded"
216
+ reason="Explicit predecessor release absorbed into approval envelope ${VERSION}."
217
+ fi
218
+
219
+ MEMBERS="$(
220
+ jq -c \
221
+ --arg version "$version" \
222
+ --arg role "$role" \
223
+ --arg relationship "$relationship" \
224
+ --arg reason "$reason" \
225
+ --arg scopeSummary "$summary" \
226
+ --arg commitFrom "$commit_from" \
227
+ --arg commitTo "$commit_to" \
228
+ --arg prNumber "$pr_number" \
229
+ --arg prUrl "$pr_url" \
230
+ '. + [{
231
+ version: $version,
232
+ role: $role,
233
+ relationship: $relationship,
234
+ reason: $reason,
235
+ scopeSummary: (if $scopeSummary == "" then null else $scopeSummary end),
236
+ commitFrom: (if $commitFrom == "" then null else $commitFrom end),
237
+ commitTo: (if $commitTo == "" then null else $commitTo end),
238
+ prNumber: (if $prNumber == "" then null else ($prNumber | tonumber) end),
239
+ prUrl: (if $prUrl == "" then null else $prUrl end)
240
+ }]' <<<"$MEMBERS"
241
+ )"
242
+ PREDECESSOR_LINES+=("- \`${version}\` (${role}/${relationship}) — ${title:-Untitled release ticket}")
243
+ done
244
+
245
+ COMMITS="$(git log "$SINCE_REF"..HEAD --format='%h%x09%s' 2>/dev/null || true)"
246
+ BUNDLED="$(printf '%s\n' "$COMMITS" | grep -E "$HOUSEKEEPING_TYPES" || true)"
247
+ NON_RELEASE_ITEMS='[]'
248
+ NON_RELEASE_LINES=()
249
+ if [ -n "$BUNDLED" ]; then
250
+ while IFS=$'\t' read -r sha subject; do
251
+ [ -n "$sha" ] || continue
252
+ NON_RELEASE_ITEMS="$(
253
+ jq -c \
254
+ --arg kind "housekeeping_commit" \
255
+ --arg title "$subject" \
256
+ --arg reference "$sha" \
257
+ '. + [{
258
+ kind: $kind,
259
+ title: $title,
260
+ reference: $reference
261
+ }]' <<<"$NON_RELEASE_ITEMS"
262
+ )"
263
+ NON_RELEASE_LINES+=("- \`${sha}\` ${subject}")
264
+ done <<<"$BUNDLED"
265
+ fi
266
+
267
+ GENERATOR_VERSION="$(jq -r '.version' cli/package.json 2>/dev/null || echo '0.0.0')"
268
+ REPOSITORY_SLUG="$(git config --get remote.origin.url 2>/dev/null | sed -E 's#(git@github.com:|https://github.com/)##; s#\.git$##' || true)"
269
+ GENERATED_AT="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
270
+
271
+ MANIFEST_BASE="$(
272
+ jq -c -n \
273
+ --arg version "$VERSION" \
274
+ --arg members "$MEMBERS" \
275
+ --arg nonReleaseWorkItems "$NON_RELEASE_ITEMS" \
276
+ --arg generatorVersion "$GENERATOR_VERSION" \
277
+ --arg repository "$REPOSITORY_SLUG" \
278
+ --arg generatedAt "$GENERATED_AT" \
279
+ '{
280
+ schemaVersion: 1,
281
+ approvalRelease: { version: $version },
282
+ coreRelease: { version: $version },
283
+ members: ($members | fromjson),
284
+ nonReleaseWorkItems: ($nonReleaseWorkItems | fromjson),
285
+ generator: {
286
+ name: "devaudit-installer",
287
+ version: $generatorVersion,
288
+ repository: (if $repository == "" then null else $repository end),
289
+ generatedAt: $generatedAt
290
+ }
291
+ }'
292
+ )"
293
+
294
+ HASH_INPUT="$(jq -S 'del(.generator.generatedAt)' <<<"$MANIFEST_BASE")"
295
+ MANIFEST_HASH="sha256:$(printf '%s' "$HASH_INPUT" | sha256sum | awk '{print $1}')"
296
+ MANIFEST_JSON="$(jq --arg manifestHash "$MANIFEST_HASH" '. + { manifestHash: $manifestHash }' <<<"$MANIFEST_BASE")"
297
+
298
+ if [ -n "$JSON_OUT" ]; then
299
+ mkdir -p "$(dirname "$JSON_OUT")"
300
+ jq -S . <<<"$MANIFEST_JSON" > "$JSON_OUT"
301
+ fi
77
302
 
78
- # Generate the markdown summary.
79
303
  echo "## Bundled Changes"
80
304
  echo ""
81
305
  echo "- **Core tracked release:** \`${VERSION}\`"
82
- echo "- **Absorbed predecessor releases:** None detected automatically"
83
- echo "- **Absorbed non-release work:** Housekeeping commits since \`${SINCE_REF}\` listed below"
84
- echo "- **Why bundled here:** These housekeeping commits were not individually released; the next tracked release is the truthful approval record for the full state of develop."
85
- echo "- **Evidence impact:** Gate evidence for \`${VERSION}\` covers the full state of develop at CI time, including the absorbed housekeeping commits listed below."
86
- echo "- **Reviewer impact:** Approval scope includes the core tracked change plus the absorbed non-release work listed below."
306
+ echo "- **Bundle manifest:** \`BUNDLED-CHANGES-${VERSION}.json\`"
307
+ echo "- **Manifest hash:** \`${MANIFEST_HASH}\`"
308
+ echo "- **Absorbed predecessor releases:** $(if [ "${#PREDECESSOR_LINES[@]}" -gt 0 ]; then printf '%s' "${EXPLICIT_PREDECESSORS[*]}"; else printf 'None'; fi)"
309
+ if [ "${#NON_RELEASE_LINES[@]}" -gt 0 ]; then
310
+ echo "- **Absorbed non-release work:** housekeeping commits since \`${SINCE_REF}\`"
311
+ else
312
+ echo "- **Absorbed non-release work:** None"
313
+ fi
314
+ echo "- **Why bundled here:** Explicit predecessor release tickets and non-release work are consolidated under approval envelope \`${VERSION}\`."
315
+ echo "- **Evidence impact:** Evidence ownership remains on the source releases; the bundle manifest provides lineage and inherited visibility only."
316
+ echo "- **Reviewer impact:** Approval scope includes the core tracked release plus the explicit predecessor releases and non-release work listed below."
87
317
  echo "- **Security / risk impact:** No additional security/risk impact identified automatically; reviewer must confirm in the canonical release artifacts."
88
318
  echo "- **Reference:** commit range \`${SINCE_REF}..HEAD\`"
89
319
  echo ""
90
- echo "### Absorbed Non-Release Work"
320
+
321
+ echo "### Explicit Constituent Releases"
91
322
  echo ""
92
- echo "The following ${COUNT} housekeeping commit(s) were absorbed into release \`${VERSION}\` since \`${SINCE_REF}\`:"
323
+ if [ "${#PREDECESSOR_LINES[@]}" -gt 0 ]; then
324
+ printf '%s\n' "${PREDECESSOR_LINES[@]}"
325
+ else
326
+ echo "- None"
327
+ fi
93
328
  echo ""
94
- echo "$BUNDLED" | while IFS=$'\t' read -r SHA SUBJECT; do
95
- echo "- \`${SHA}\` ${SUBJECT}"
96
- done
329
+
330
+ echo "### Absorbed Non-Release Work"
331
+ echo ""
332
+ if [ "${#NON_RELEASE_LINES[@]}" -gt 0 ]; then
333
+ printf '%s\n' "${NON_RELEASE_LINES[@]}"
334
+ else
335
+ echo "- None"
336
+ fi
97
337
  echo ""
98
- echo "These commits were not individually released (tooling syncs / housekeeping)."
99
- echo "Gate evidence above covers the full state of develop at CI time, including these changes."