@metasession.co/devaudit-cli 0.3.10 → 0.3.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metasession.co/devaudit-cli",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "description": "DevAudit CLI — installs, syncs, and operates the Metasession SDLC across consumer projects.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@clack/prompts": "^0.8.2",
36
- "@metasession.co/devaudit-plugin-sdk": "^0.3.10",
36
+ "@metasession.co/devaudit-plugin-sdk": "^0.3.12",
37
37
  "ajv": "^8.20.0",
38
38
  "commander": "^12.1.0",
39
39
  "consola": "^3.2.3",
@@ -76,6 +76,17 @@ Sync may warn if these are absent; it's a soft check (the CI workflow is the har
76
76
  | `config_keys.optional` | string[] | Keys consumers may define (e.g. an optional database service). |
77
77
  | `config_keys.defaults` | object | Fallback values when a key is absent. |
78
78
 
79
+ ### Runtime contract
80
+
81
+ | Field | Type | Purpose |
82
+ | ----------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------- |
83
+ | `runtime_contract.preferred_web_runtime` | string | Operator-facing guidance for the expected production web runtime shape on this host. |
84
+ | `runtime_contract.forbid_typescript_runtime` | boolean | Whether long-lived production web processes on this host should forbid `tsx`/`ts-node` style execution. |
85
+ | `runtime_contract.prefer_standalone_output` | boolean | Whether frameworks with a standalone/minimal runtime output should prefer it on this host. |
86
+ | `runtime_contract.scheduler_placement` | string | Operator-facing guidance for where scheduled/background jobs should run on this host. |
87
+
88
+ This field is not consumed by templates yet. It exists so host-level runtime-efficiency rules are versioned and schema-validated instead of living as tribal knowledge in issues and comments.
89
+
79
90
  ### Notes
80
91
 
81
92
  | Field | Type | Purpose |
@@ -103,6 +114,12 @@ See the step-by-step walkthrough: **[docs/adding-a-host.md](../docs/adding-a-hos
103
114
  "wait_for_deploy": "for i in $(seq 1 30); do flyctl status --app \"${APP_NAME}\" --json | jq -e '.Deployment.Status == \"successful\"' && break; sleep 10; done",
104
115
  "required_secrets": ["FLY_API_TOKEN", "DEVAUDIT_API_KEY"],
105
116
  "required_env": ["APP_NAME"],
117
+ "runtime_contract": {
118
+ "preferred_web_runtime": "compiled JavaScript or framework standalone output",
119
+ "forbid_typescript_runtime": true,
120
+ "prefer_standalone_output": true,
121
+ "scheduler_placement": "scheduled jobs should run in a separate worker or platform cron, not in the web process"
122
+ },
106
123
  "notes": [
107
124
  "Production URL is resolved at deploy time by flyctl — no GitHub Secret to maintain.",
108
125
  "Deploy step runs `flyctl deploy --remote-only` in the post-deploy workflow; FLY_API_TOKEN is the auth."
package/sdlc/SKILLS.md CHANGED
@@ -112,6 +112,14 @@ node scripts/validate-adapter.cjs sdlc/files/_common/skills/<name>/SKILL.md
112
112
 
113
113
  These delegations are hard contracts — the orchestrator's SKILL.md fails review if it inlines a specialist skill's procedure. The invocation pattern is documented in [docs/adding-a-skill.md §Orchestrator skills](../docs/adding-a-skill.md#orchestrator-skills-calling-other-skills).
114
114
 
115
+ Phase 4 is now more than a one-shot handoff note. When a release PR is blocked or waiting, `sdlc-implementer` can hand execution to the bundled watcher:
116
+
117
+ ```bash
118
+ node SDLC/bin/devaudit-sdlc.js --watch-pr=<number> --repo <owner/name> --once
119
+ ```
120
+
121
+ or, without `--once`, a bounded poll loop that persists retry state in `.sdlc-pr-watch.json`, re-runs likely flaky workflows, and re-runs the Release Approval Gate when the portal is already approved but GitHub has not yet converged. That makes "blocked PR handling" an executable part of the orchestration contract rather than a prose-only reminder.
122
+
115
123
  `sdlc-implementer` is **not** used for trivial / housekeeping changes (docs, formatting, dependency bumps, CI tweaks) — those skip the requirement and the ceremony. See [the change-type matrix](../docs/change-workflows.md) and the [trivial-change walkthrough](./files/_common/implementing-an-sdlc-issue.md#trivial-change-walkthrough).
116
124
 
117
125
  ### The SoT-alignment skill family
@@ -412,6 +412,16 @@ When the user says implementation is done, or when all acceptance criteria from
412
412
 
413
413
  After gates pass, create `compliance/evidence/REQ-XXX/test-execution-summary.md` documenting gate results, test changes, and coverage against the test plan. Include links to evidence locations in DevAudit.
414
414
 
415
+ If `compliance/pending-releases/BUNDLED-CHANGES-REQ-XXX.md` exists, add a `## Bundled Release Context` section to `test-execution-summary.md` with this minimum structure:
416
+ - `**Core tracked release:**`
417
+ - `**Absorbed predecessor releases:**`
418
+ - `**Absorbed non-release work:**`
419
+ - `**Why bundled here:**`
420
+ - `**Evidence impact:**`
421
+ - `**Reviewer impact:**`
422
+ - `**Security / risk impact:**`
423
+ - `**Reference:**`
424
+
415
425
  Tell the user: **"All gates passed. Let me generate the test execution summary."**
416
426
 
417
427
  ### Step 2: Upload binary/JSON evidence to DevAudit
@@ -457,6 +467,8 @@ Dependency audit: 0 vulnerabilities
457
467
  Evidence uploaded to DevAudit project: [PROJECT_SLUG]
458
468
  ```
459
469
 
470
+ If `BUNDLED-CHANGES-REQ-XXX.md` exists, add the same structured bundled-release fields to `security-summary.md` and to `ai-use-note.md` (when present), and add `## Bundled Changes` or `## Absorbed Predecessor Releases` to the release ticket with the same minimum fields. The GitHub PR body is supporting context, not the primary audit record.
471
+
460
472
  Verify these also exist in git:
461
473
  - `compliance/evidence/REQ-XXX/test-scope.md` (from planning)
462
474
  - `compliance/evidence/REQ-XXX/implementation-plan.md` (MEDIUM/HIGH risk — from implementation plan step)
@@ -484,9 +496,9 @@ When creating the PR, include:
484
496
  - A **"Test Changes"** section listing test files added/modified, what they cover, and what's NOT covered
485
497
  - A **"Where to Find Test Results"** section pointing reviewers to: CI status icons on commits, automated E2E comment, DevAudit portal link, and compliance evidence files in the PR
486
498
 
487
- ### Step 6: Commit compliance markdown only (do NOT push yet)
499
+ ### Step 6: Commit compliance markdown and push immediately
488
500
 
489
- Commit locally but **do not push**. UAT verification runs against the prior deployment. We batch all compliance + UAT commits into a single push after Step 7 to avoid duplicate CI runs.
501
+ Commit the compliance markdown and **push immediately**. The compliance-only push is cheap, and it is the authoritative trigger for the Compliance Evidence Upload workflow. Pushing now surfaces destination/configuration failures before you waste time on later review steps.
490
502
 
491
503
  ```bash
492
504
  # ONLY commit markdown — binary/JSON evidence is in DevAudit
@@ -508,7 +520,15 @@ Co-Authored-By: [AI tool tag]"
508
520
 
509
521
  **NEVER `git add` JSON, TXT, HTML, PNG, or JPG evidence files. They belong in DevAudit.**
510
522
 
511
- ### Step 7: WAIT CHECKPOINT UAT Verification (if UAT configured)
523
+ ### Step 7: Wait for the Compliance Evidence Upload workflow
524
+
525
+ ```bash
526
+ gh run watch --workflow "Compliance Evidence Upload"
527
+ ```
528
+
529
+ If it fails, fix the upload/configuration problem before proceeding. The portal's Test Reports gate stays red until the authoritative `develop` upload lands, even if `test-execution-summary.md` already exists on a feature branch.
530
+
531
+ ### Step 8: WAIT CHECKPOINT — UAT Verification (if UAT configured)
512
532
 
513
533
  If the project has a UAT environment that auto-deploys from `develop`, verify the change works on UAT before creating a PR.
514
534
 
@@ -547,17 +567,17 @@ Co-Authored-By: [AI tool tag]"
547
567
 
548
568
  **If UAT fails:** Fix on `develop`, re-run local gates, push, and repeat. Do NOT create a PR until UAT is green.
549
569
 
550
- ### Step 8: Push all compliance commits
570
+ ### Step 9: Push any UAT-result follow-up commit
551
571
 
552
- Push all batched commits (evidence + UAT results) in a single push. This triggers one CI run instead of multiple.
572
+ If Step 8 added a UAT verification note to `security-summary.md`, push that follow-up commit now.
553
573
 
554
574
  ```bash
555
575
  git push origin develop
556
576
  ```
557
577
 
558
- Tell the user: **"UAT verification passed. Compliance commits pushed. Next step: create a PR from develop to main."**
578
+ Tell the user: **"UAT verification passed. Compliance evidence is on the portal. Next step: create a PR from develop to main."**
559
579
 
560
- ### Step 9: Verify release exists in DevAudit
580
+ ### Step 10: Verify release exists in DevAudit
561
581
 
562
582
  CI auto-creates releases when uploading evidence (using `--create-release-if-missing`). After pushing, verify the release appears in DevAudit:
563
583
 
@@ -22,11 +22,11 @@ This guide configures a new project so that the five pipeline workflows can run
22
22
 
23
23
  ## Fast path (recommended): `devaudit install`
24
24
 
25
- The DevAudit CLI (`@metasession.co/devaudit-cli`) automates almost all of this guide. It writes `sdlc-config.json`, creates the DevAudit project, issues an API key, sets the GitHub secrets and variables, installs the hook framework, configures branch protection, and syncs the framework templates (CI workflows, hooks, scripts) in roughly 30 seconds.
25
+ The DevAudit CLI (`@metasession.co/devaudit-cli`) automates almost all of this guide. It writes `sdlc-config.json`, creates the DevAudit project, issues an API key, sets the GitHub secrets and variables, installs the hook framework, configures branch protection, and syncs the framework templates (CI workflows, hooks, scripts). The command starts in seconds; the full operator onboarding flow usually takes about 5-10 minutes.
26
26
 
27
27
  ```bash
28
28
  npm install -g @metasession.co/devaudit-cli # requires Node >= 22
29
- devaudit auth login # paste your PAT (issued at https://devaudit.metasession.co/settings/tokens)
29
+ devaudit auth login # paste your PAT (issued at https://devaudit.ai/settings/tokens)
30
30
  devaudit install ../path/to/this-project
31
31
  ```
32
32
 
@@ -112,8 +112,8 @@ cat > compliance/RTM.md << 'EOF'
112
112
 
113
113
  ## Part B: Change Request Traceability
114
114
 
115
- | REQ-ID | Issue | Risk | Evidence | Status | Approver | Date |
116
- |---|---|---|---|---|---|---|
115
+ | REQ-ID | Issue | Risk | Evidence | Status | PR | Reviewer | AI-tool |
116
+ |--------|-------|------|----------|--------|-----|----------|---------|
117
117
  EOF
118
118
 
119
119
  # Commit
@@ -26,6 +26,21 @@
26
26
 
27
27
  set -euo pipefail
28
28
 
29
+ find_bundled_changes_file() {
30
+ local req_id="$1"
31
+ local candidate=""
32
+ for candidate in \
33
+ "compliance/pending-releases/BUNDLED-CHANGES-${req_id}.md" \
34
+ "compliance/approved-releases/BUNDLED-CHANGES-${req_id}.md" \
35
+ "compliance/superseded-releases/BUNDLED-CHANGES-${req_id}.md"; do
36
+ if [ -f "$candidate" ]; then
37
+ printf '%s' "$candidate"
38
+ return 0
39
+ fi
40
+ done
41
+ return 1
42
+ }
43
+
29
44
  extract_release_metadata() {
30
45
  local req_id="$1"
31
46
  RELEASE_TITLE=""
@@ -116,4 +131,27 @@ extract_release_metadata() {
116
131
  ;;
117
132
  esac
118
133
  fi
134
+
135
+ # If the tracked release has a generated bundled-changes artefact,
136
+ # surface that fact in the release summary so the portal release row and
137
+ # workflow metadata don't rely on the GitHub PR body as the only bundle
138
+ # narrative. DevAudit-Installer#344.
139
+ local bundled_file=""
140
+ bundled_file=$(find_bundled_changes_file "$req_id" 2>/dev/null || true)
141
+ if [ -n "$bundled_file" ]; then
142
+ local bundled_note
143
+ bundled_note="Bundled release context: see \`${bundled_file}\`."
144
+ if [ -n "$RELEASE_SUMMARY" ]; then
145
+ case "$RELEASE_SUMMARY" in
146
+ *"Bundled release context:"*) ;;
147
+ *)
148
+ RELEASE_SUMMARY="${RELEASE_SUMMARY}
149
+
150
+ ${bundled_note}"
151
+ ;;
152
+ esac
153
+ else
154
+ RELEASE_SUMMARY="$bundled_note"
155
+ fi
156
+ fi
119
157
  }
@@ -358,6 +358,32 @@ assert_eq "Title extracted" "Feature fifteen" "$RELEASE_TITLE"
358
358
  assert_empty "Whitespace-only summary cleared" "$RELEASE_SUMMARY"
359
359
  echo ""
360
360
 
361
+ # --- Test 17: Bundled changes file appends bundle note to summary ---
362
+ echo "--- Test 17: Bundled changes note appended ---"
363
+ make_fixture "$WORK/test17"
364
+ cat > "compliance/pending-releases/RELEASE-TICKET-REQ-017.md" <<'TICKET'
365
+ # Release Ticket — REQ-017
366
+
367
+ **Requirement:** REQ-017 — Feature seventeen
368
+
369
+ ## Summary
370
+ Core tracked change summary.
371
+ TICKET
372
+ cat > "compliance/pending-releases/BUNDLED-CHANGES-REQ-017.md" <<'BUNDLE'
373
+ ## Bundled Changes
374
+
375
+ - `abc1234` chore: sync templates
376
+ BUNDLE
377
+
378
+ source "$HELPER"
379
+ extract_release_metadata "REQ-017"
380
+ assert_eq "Bundled note appended to summary" \
381
+ "Core tracked change summary.
382
+
383
+ Bundled release context: see \`compliance/pending-releases/BUNDLED-CHANGES-REQ-017.md\`." \
384
+ "$RELEASE_SUMMARY"
385
+ echo ""
386
+
361
387
  echo ""
362
388
  echo "=== Results: $PASS passed, $FAIL failed ==="
363
389
  exit $FAIL
@@ -78,6 +78,17 @@ COUNT=$(echo "$BUNDLED" | wc -l | tr -d ' ')
78
78
  # Generate the markdown summary.
79
79
  echo "## Bundled Changes"
80
80
  echo ""
81
+ 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."
87
+ echo "- **Security / risk impact:** No additional security/risk impact identified automatically; reviewer must confirm in the canonical release artifacts."
88
+ echo "- **Reference:** commit range \`${SINCE_REF}..HEAD\`"
89
+ echo ""
90
+ echo "### Absorbed Non-Release Work"
91
+ echo ""
81
92
  echo "The following ${COUNT} housekeeping commit(s) were absorbed into release \`${VERSION}\` since \`${SINCE_REF}\`:"
82
93
  echo ""
83
94
  echo "$BUNDLED" | while IFS=$'\t' read -r SHA SUBJECT; do
@@ -89,6 +89,10 @@ assert_not_contains "feat commit excluded" "feat: add booking widget" "$OUTPUT"
89
89
  assert_not_contains "fix commit excluded" "fix: resolve null pointer" "$OUTPUT"
90
90
  assert_contains "header present" "## Bundled Changes" "$OUTPUT"
91
91
  assert_contains "version in header" "REQ-042" "$OUTPUT"
92
+ assert_contains "core tracked release field present" "**Core tracked release:**" "$OUTPUT"
93
+ assert_contains "absorbed non-release work field present" "**Absorbed non-release work:**" "$OUTPUT"
94
+ assert_contains "reviewer impact field present" "**Reviewer impact:**" "$OUTPUT"
95
+ assert_contains "reference field present" "**Reference:**" "$OUTPUT"
92
96
  echo
93
97
 
94
98
  # ─── Test 2: No housekeeping commits ────────────────────────────────
@@ -98,6 +98,8 @@ CHANGE_TYPE=""
98
98
  GATE_STATUS=""
99
99
  SDLC_STAGE=""
100
100
  TEST_CYCLE=""
101
+ SENTINEL_CONTENT=""
102
+ COMMIT_TIMESTAMP=""
101
103
  # Repeatable `--meta-key key=value` accumulator. Each pair gets merged
102
104
  # into the metadata JSON sent to the portal. Used by the screenshot
103
105
  # upload loop to pass `origin=feature|regression` from the per-PNG
@@ -202,6 +204,28 @@ probe_base_url_drift() {
202
204
  }
203
205
  probe_base_url_drift
204
206
 
207
+ is_tracked_change_type() {
208
+ case "${1:-}" in
209
+ feat|fix|refactor|perf|compliance|revert) return 0 ;;
210
+ *) return 1 ;;
211
+ esac
212
+ }
213
+
214
+ resolve_sentinel_context() {
215
+ if ! is_tracked_change_type "$CHANGE_TYPE"; then
216
+ return 0
217
+ fi
218
+ if [ -f ".sdlc-implementer-invoked" ]; then
219
+ SENTINEL_CONTENT=$(cat .sdlc-implementer-invoked)
220
+ fi
221
+ local git_target="HEAD"
222
+ if [ -n "$GIT_SHA" ]; then
223
+ git_target="$GIT_SHA"
224
+ fi
225
+ COMMIT_TIMESTAMP=$(git show -s --format=%cI "$git_target" 2>/dev/null || true)
226
+ }
227
+ resolve_sentinel_context
228
+
205
229
  # --- Build metadata JSON ---
206
230
  # Assemble entries first; only emit `{ ... }` if at least one field is
207
231
  # set. Each entry is a `"key":"value"` JSON pair with the value
@@ -213,6 +237,7 @@ META_ENTRIES=()
213
237
  [ -n "$GIT_SHA" ] && META_ENTRIES+=("\"gitSha\":\"$(json_escape "$GIT_SHA")\"")
214
238
  [ -n "$CI_RUN_ID" ] && META_ENTRIES+=("\"ciRunId\":\"$(json_escape "$CI_RUN_ID")\"")
215
239
  [ -n "$BRANCH" ] && META_ENTRIES+=("\"branch\":\"$(json_escape "$BRANCH")\"")
240
+ [ -n "$COMMIT_TIMESTAMP" ] && META_ENTRIES+=("\"commitTimestamp\":\"$(json_escape "$COMMIT_TIMESTAMP")\"")
216
241
  for KV in "${META_KEYS[@]}"; do
217
242
  KEY="${KV%%=*}"
218
243
  VAL="${KV#*=}"
@@ -337,8 +362,11 @@ upload_presigned() {
337
362
  \"evidenceCategory\": \"${EVIDENCE_CATEGORY}\",
338
363
  \"releaseTitle\": \"${RELEASE_TITLE}\",
339
364
  \"releaseSummary\": \"${RELEASE_SUMMARY}\",
365
+ \"changeType\": \"${CHANGE_TYPE}\",
340
366
  \"sdlcStage\": \"${SDLC_STAGE}\",
341
- \"testCycleId\": \"${TEST_CYCLE}\"
367
+ \"testCycleId\": \"${TEST_CYCLE}\",
368
+ \"sentinelContent\": $(jq -Rn --arg v "$SENTINEL_CONTENT" '$v'),
369
+ \"commitTimestamp\": \"${COMMIT_TIMESTAMP}\"
342
370
  }") || curl_exit=$?
343
371
  curl_exit=${curl_exit:-0}
344
372
  if [ "$curl_exit" -eq 0 ] && [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then
@@ -503,6 +531,8 @@ for FILE in "${FILES[@]}"; do
503
531
  [ -n "$GATE_STATUS" ] && CURL_ARGS+=(-F "gateStatus=${GATE_STATUS}")
504
532
  [ -n "$SDLC_STAGE" ] && CURL_ARGS+=(-F "sdlcStage=${SDLC_STAGE}")
505
533
  [ -n "$TEST_CYCLE" ] && CURL_ARGS+=(-F "testCycleId=${TEST_CYCLE}")
534
+ [ -n "$SENTINEL_CONTENT" ] && CURL_ARGS+=(-F "sentinelContent=${SENTINEL_CONTENT}")
535
+ [ -n "$COMMIT_TIMESTAMP" ] && CURL_ARGS+=(-F "commitTimestamp=${COMMIT_TIMESTAMP}")
506
536
 
507
537
  ATTEMPT=1
508
538
  BACKOFF=$INITIAL_BACKOFF_SECONDS
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env bash
2
+ # upload-evidence.test.sh — Focused regression tests for sentinel propagation.
3
+ #
4
+ # Verifies tracked uploads automatically forward `.sdlc-implementer-invoked`
5
+ # content and a commit timestamp to the portal-facing upload contract.
6
+
7
+ set -euo pipefail
8
+
9
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
10
+ HELPER="$SCRIPT_DIR/upload-evidence.sh"
11
+ [ -x "$HELPER" ] || chmod +x "$HELPER"
12
+
13
+ PASS=0
14
+ FAIL=0
15
+
16
+ WORK=$(mktemp -d)
17
+ trap 'rm -rf "$WORK"' EXIT
18
+
19
+ assert_contains() {
20
+ local desc="$1" needle="$2" file="$3"
21
+ if grep -Fq "$needle" "$file"; then
22
+ echo " PASS: $desc"
23
+ PASS=$((PASS + 1))
24
+ else
25
+ echo " FAIL: $desc"
26
+ echo " missing: $needle"
27
+ echo " file:"
28
+ sed 's/^/ /' "$file"
29
+ FAIL=$((FAIL + 1))
30
+ fi
31
+ }
32
+
33
+ make_fixture() {
34
+ local dir="$1"
35
+ rm -rf "$dir"
36
+ mkdir -p "$dir/bin"
37
+ cd "$dir"
38
+ git init -q --initial-branch=main
39
+ git config user.email "test@example.com"
40
+ git config user.name "test"
41
+ echo "evidence" > evidence.txt
42
+ printf '%s\n' '[{"currentPhase":"3","initializedBy":"skill","status":"active"}]' > .sdlc-implementer-invoked
43
+ git add evidence.txt .sdlc-implementer-invoked
44
+ git commit -q -m "feat: tracked upload fixture"
45
+
46
+ cat > bin/curl <<'EOF'
47
+ #!/usr/bin/env bash
48
+ set -euo pipefail
49
+ LOG_FILE="${UPLOAD_TEST_LOG:?}"
50
+ URL=""
51
+ OUT_FILE=""
52
+ HDR_FILE=""
53
+ WRITE_OUT=""
54
+ ARGS=("$@")
55
+ for ((i=0; i<${#ARGS[@]}; i++)); do
56
+ arg="${ARGS[$i]}"
57
+ case "$arg" in
58
+ -o) OUT_FILE="${ARGS[$((i+1))]}" ;;
59
+ -D) HDR_FILE="${ARGS[$((i+1))]}" ;;
60
+ -w) WRITE_OUT="${ARGS[$((i+1))]}" ;;
61
+ http://*|https://*) URL="$arg" ;;
62
+ -F)
63
+ echo "FORM:${ARGS[$((i+1))]}" >> "$LOG_FILE"
64
+ ;;
65
+ esac
66
+ done
67
+ echo "URL:${URL}" >> "$LOG_FILE"
68
+ if [[ "$URL" == *"/api/health" ]]; then
69
+ printf '200 '
70
+ exit 0
71
+ fi
72
+ if [ -n "$OUT_FILE" ]; then
73
+ printf '{"ok":true}' > "$OUT_FILE"
74
+ fi
75
+ if [ -n "$HDR_FILE" ]; then
76
+ : > "$HDR_FILE"
77
+ fi
78
+ if [ -n "$WRITE_OUT" ]; then
79
+ printf '201'
80
+ fi
81
+ EOF
82
+ chmod +x bin/curl
83
+ }
84
+
85
+ echo "=== upload-evidence.sh tests ==="
86
+
87
+ D1="$WORK/case1"
88
+ make_fixture "$D1"
89
+ LOG_FILE="$D1/upload.log"
90
+ touch "$LOG_FILE"
91
+ export PATH="$D1/bin:$PATH"
92
+ export UPLOAD_TEST_LOG="$LOG_FILE"
93
+ export DEVAUDIT_BASE_URL="https://devaudit.example.test"
94
+ export DEVAUDIT_API_KEY="mc_test_dummy"
95
+
96
+ bash "$HELPER" fixture-project REQ-091 test_report evidence.txt \
97
+ --release v2026.07.13 \
98
+ --environment uat \
99
+ --category test_report \
100
+ --change-type feat > "$D1/stdout.log" 2>&1
101
+
102
+ assert_contains "tracked upload forwards sentinelContent" \
103
+ 'FORM:sentinelContent=[{"currentPhase":"3","initializedBy":"skill","status":"active"}]' \
104
+ "$LOG_FILE"
105
+ assert_contains "tracked upload forwards commitTimestamp field" \
106
+ 'FORM:commitTimestamp=' \
107
+ "$LOG_FILE"
108
+ assert_contains "tracked upload forwards changeType" \
109
+ 'FORM:changeType=feat' \
110
+ "$LOG_FILE"
111
+ assert_contains "tracked upload stamps commitTimestamp into metadata" \
112
+ 'FORM:metadata={"commitTimestamp":"' \
113
+ "$LOG_FILE"
114
+
115
+ echo ""
116
+ echo "=== upload-evidence.test.sh: $PASS passed, $FAIL failed ==="
117
+ if [ "$FAIL" -gt 0 ]; then
118
+ exit 1
119
+ fi
@@ -48,6 +48,27 @@ fi
48
48
  echo "Requirements found in PR commits: $REQUIREMENTS"
49
49
  echo ""
50
50
 
51
+ find_first_match() {
52
+ local pattern="$1"
53
+ local match=""
54
+ match=$(compgen -G "$pattern" | head -1 || true)
55
+ [ -n "$match" ] && printf '%s' "$match"
56
+ }
57
+
58
+ require_bundled_fields() {
59
+ local file="$1"
60
+ local label="$2"
61
+ local missing=0
62
+ shift 2
63
+ for field in "$@"; do
64
+ if ! grep -qF "**${field}:**" "$file" 2>/dev/null; then
65
+ echo " ERROR: ${label} is missing bundled field '**${field}:**'"
66
+ missing=1
67
+ fi
68
+ done
69
+ return $missing
70
+ }
71
+
51
72
  for REQ in $REQUIREMENTS; do
52
73
  echo "--- Checking $REQ ---"
53
74
 
@@ -149,9 +170,14 @@ for REQ in $REQUIREMENTS; do
149
170
  fi
150
171
  fi
151
172
 
152
- # Check test-execution-summary.md exists
173
+ # Check test-execution-summary.md exists. For tracked REQs this is a
174
+ # hard requirement: the file is the per-release test report that
175
+ # satisfies the portal's Test Reports gate. Housekeeping flows never
176
+ # reach this loop because they have no REQ refs in the validated
177
+ # commits, so the stricter behavior remains scoped to tracked work.
153
178
  if [ ! -f "compliance/evidence/$REQ/test-execution-summary.md" ]; then
154
- echo " WARNING: Test execution summary missing: compliance/evidence/$REQ/test-execution-summary.md"
179
+ echo " ERROR: Test execution summary missing: compliance/evidence/$REQ/test-execution-summary.md"
180
+ EXIT_CODE=1
155
181
  else
156
182
  echo " OK: test-execution-summary.md exists"
157
183
  fi
@@ -193,16 +219,141 @@ for REQ in $REQUIREMENTS; do
193
219
  compgen -G "$TICKET_PATTERN" > /dev/null 2>&1 && LOCATIONS=$((LOCATIONS+1))
194
220
  compgen -G "$APPROVED_PATTERN" > /dev/null 2>&1 && LOCATIONS=$((LOCATIONS+1))
195
221
  compgen -G "$SUPERSEDED_PATTERN" > /dev/null 2>&1 && LOCATIONS=$((LOCATIONS+1))
222
+ TICKET_FILE=""
196
223
  if [ "$LOCATIONS" -gt 1 ]; then
197
224
  echo " ERROR: RELEASE-TICKET-${REQ} exists in more than one release directory (pending/approved/superseded). Remove the stale pending copy — it will break the evidence-completeness gate (#192)."
198
225
  EXIT_CODE=1
199
226
  elif [ "$LOCATIONS" -eq 1 ]; then
200
227
  echo " OK: Release ticket exists"
228
+ TICKET_FILE=$(find_first_match "$TICKET_PATTERN" || true)
229
+ [ -z "$TICKET_FILE" ] && TICKET_FILE=$(find_first_match "$APPROVED_PATTERN" || true)
230
+ [ -z "$TICKET_FILE" ] && TICKET_FILE=$(find_first_match "$SUPERSEDED_PATTERN" || true)
201
231
  else
202
232
  echo " ERROR: Release ticket missing: compliance/pending-releases/RELEASE-TICKET-${REQ}.md"
203
233
  EXIT_CODE=1
204
234
  fi
205
235
 
236
+ BUNDLED_FILE=""
237
+ for BUNDLED_PATTERN in \
238
+ "compliance/pending-releases/BUNDLED-CHANGES-${REQ}.md" \
239
+ "compliance/approved-releases/BUNDLED-CHANGES-${REQ}.md" \
240
+ "compliance/superseded-releases/BUNDLED-CHANGES-${REQ}.md"; do
241
+ BUNDLED_FILE=$(find_first_match "$BUNDLED_PATTERN" || true)
242
+ [ -n "$BUNDLED_FILE" ] && break
243
+ done
244
+ if [ -n "$BUNDLED_FILE" ]; then
245
+ echo " OK: Bundled release evidence present: ${BUNDLED_FILE}"
246
+ if grep -q '^## Bundled Changes' "$BUNDLED_FILE"; then
247
+ echo " OK: Bundled release evidence has canonical heading"
248
+ else
249
+ echo " ERROR: Bundled release evidence is missing '## Bundled Changes'"
250
+ EXIT_CODE=1
251
+ fi
252
+ if ! require_bundled_fields \
253
+ "$BUNDLED_FILE" \
254
+ "Bundled release evidence" \
255
+ "Core tracked release" \
256
+ "Absorbed predecessor releases" \
257
+ "Absorbed non-release work" \
258
+ "Why bundled here" \
259
+ "Evidence impact" \
260
+ "Reviewer impact" \
261
+ "Security / risk impact" \
262
+ "Reference"; then
263
+ EXIT_CODE=1
264
+ else
265
+ echo " OK: Bundled release evidence carries the required structured fields"
266
+ fi
267
+ if [ -n "$TICKET_FILE" ] && grep -qE '^## (Bundled Changes|Absorbed Predecessor Releases)' "$TICKET_FILE"; then
268
+ echo " OK: Release ticket documents bundled release context"
269
+ else
270
+ echo " ERROR: Bundled release evidence exists but the release ticket is missing '## Bundled Changes' or '## Absorbed Predecessor Releases'"
271
+ EXIT_CODE=1
272
+ fi
273
+ if [ -n "$TICKET_FILE" ]; then
274
+ if ! require_bundled_fields \
275
+ "$TICKET_FILE" \
276
+ "Release ticket" \
277
+ "Core tracked release" \
278
+ "Absorbed predecessor releases" \
279
+ "Absorbed non-release work" \
280
+ "Why bundled here" \
281
+ "Evidence impact" \
282
+ "Reviewer impact" \
283
+ "Security / risk impact" \
284
+ "Reference"; then
285
+ EXIT_CODE=1
286
+ else
287
+ echo " OK: Release ticket carries the required bundled fields"
288
+ fi
289
+ fi
290
+ if grep -q '^## Bundled Release Context' "compliance/evidence/$REQ/test-execution-summary.md" 2>/dev/null; then
291
+ echo " OK: test-execution-summary.md documents bundled release context"
292
+ else
293
+ echo " ERROR: Bundled release evidence exists but test-execution-summary.md is missing '## Bundled Release Context'"
294
+ EXIT_CODE=1
295
+ fi
296
+ if ! require_bundled_fields \
297
+ "compliance/evidence/$REQ/test-execution-summary.md" \
298
+ "test-execution-summary.md" \
299
+ "Core tracked release" \
300
+ "Absorbed predecessor releases" \
301
+ "Absorbed non-release work" \
302
+ "Why bundled here" \
303
+ "Evidence impact" \
304
+ "Reviewer impact" \
305
+ "Security / risk impact" \
306
+ "Reference"; then
307
+ EXIT_CODE=1
308
+ else
309
+ echo " OK: test-execution-summary.md carries the required bundled fields"
310
+ fi
311
+ if grep -q '^## Bundled Release Context' "compliance/evidence/$REQ/security-summary.md" 2>/dev/null; then
312
+ echo " OK: security-summary.md documents bundled release context"
313
+ else
314
+ echo " ERROR: Bundled release evidence exists but security-summary.md is missing '## Bundled Release Context'"
315
+ EXIT_CODE=1
316
+ fi
317
+ if ! require_bundled_fields \
318
+ "compliance/evidence/$REQ/security-summary.md" \
319
+ "security-summary.md" \
320
+ "Core tracked release" \
321
+ "Absorbed predecessor releases" \
322
+ "Absorbed non-release work" \
323
+ "Why bundled here" \
324
+ "Evidence impact" \
325
+ "Reviewer impact" \
326
+ "Security / risk impact" \
327
+ "Reference"; then
328
+ EXIT_CODE=1
329
+ else
330
+ echo " OK: security-summary.md carries the required bundled fields"
331
+ fi
332
+ if [ -f "compliance/evidence/$REQ/ai-use-note.md" ]; then
333
+ if grep -q '^## Bundled Release Context' "compliance/evidence/$REQ/ai-use-note.md"; then
334
+ echo " OK: ai-use-note.md documents bundled release context"
335
+ else
336
+ echo " ERROR: Bundled release evidence exists but ai-use-note.md is missing '## Bundled Release Context'"
337
+ EXIT_CODE=1
338
+ fi
339
+ if ! require_bundled_fields \
340
+ "compliance/evidence/$REQ/ai-use-note.md" \
341
+ "ai-use-note.md" \
342
+ "Core tracked release" \
343
+ "Absorbed predecessor releases" \
344
+ "Absorbed non-release work" \
345
+ "Why bundled here" \
346
+ "Evidence impact" \
347
+ "Reviewer impact" \
348
+ "Security / risk impact" \
349
+ "Reference"; then
350
+ EXIT_CODE=1
351
+ else
352
+ echo " OK: ai-use-note.md carries the required bundled fields"
353
+ fi
354
+ fi
355
+ fi
356
+
206
357
  # Check implementation-plan.md for MEDIUM/HIGH risk (check RTM for risk level)
207
358
  if grep "$REQ" compliance/RTM.md 2>/dev/null | grep -qiE 'MEDIUM|HIGH'; then
208
359
  if [ ! -f "compliance/evidence/$REQ/implementation-plan.md" ]; then