@metasession.co/devaudit-cli 0.1.24 → 0.1.26

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.1.24",
3
+ "version": "0.1.26",
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.1.24",
36
+ "@metasession.co/devaudit-plugin-sdk": "^0.1.26",
37
37
  "commander": "^12.1.0",
38
38
  "consola": "^3.2.3",
39
39
  "env-paths": "^3.0.0",
@@ -117,11 +117,31 @@ echo "Ticket Status -> RELEASED."
117
117
 
118
118
  # ── Flip the RTM row -> RELEASED (preserve any parenthetical note) ───────────
119
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`.
120
132
  awk -v req="$REQ_ID" '
121
133
  BEGIN { FS="|"; OFS="|"; statuscol=0 }
122
- # Locate the "Status" column from the first header row that has one.
123
- statuscol==0 {
124
- for (i=1; i<=NF; i++) { c=$i; gsub(/^[[:space:]]+|[[:space:]]+$/, "", c); if (c=="Status") statuscol=i }
134
+ # Header detection: scan every row; require both a "Status" header cell
135
+ # AND an ID-like header cell in the same row before locking statuscol to
136
+ # this row''s column index.
137
+ {
138
+ cand=0; idseen=0
139
+ for (i=1; i<=NF; i++) {
140
+ c=$i; gsub(/^[[:space:]]+|[[:space:]]+$/, "", c)
141
+ if (c=="Status") cand=i
142
+ if (c=="ID" || c=="REQ-ID" || c=="REQ ID" || c ~ /^Requirement/) idseen=1
143
+ }
144
+ if (cand>0 && idseen) statuscol=cand
125
145
  }
126
146
  $0 ~ ("^\\| " req " ") && statuscol>0 {
127
147
  cell=$statuscol
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env bash
2
+ # close-out-release.test.sh — Tests for the RTM-flip awk in
3
+ # close-out-release.sh, specifically that it locates the correct
4
+ # Status column when the RTM has multiple markdown tables with
5
+ # different shapes (#72 regression: the prior implementation locked
6
+ # `statuscol` on the FIRST header containing "Status", mangling the
7
+ # wrong column when a legend table appeared above the main RTM).
8
+ #
9
+ # Usage:
10
+ # ./scripts/close-out-release.test.sh
11
+
12
+ set -euo pipefail
13
+
14
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
15
+ HELPER="$SCRIPT_DIR/close-out-release.sh"
16
+ [ -x "$HELPER" ] || chmod +x "$HELPER"
17
+
18
+ PASS=0
19
+ FAIL=0
20
+
21
+ assert_eq() {
22
+ local desc="$1" want="$2" got="$3"
23
+ if [ "$got" = "$want" ]; then
24
+ echo " PASS: $desc"
25
+ PASS=$((PASS + 1))
26
+ else
27
+ echo " FAIL: $desc"
28
+ echo " want: $want"
29
+ echo " got: $got"
30
+ FAIL=$((FAIL + 1))
31
+ fi
32
+ }
33
+
34
+ # Build a self-contained fixture under $1 with:
35
+ # - compliance/RTM.md containing the legend table + the main RTM
36
+ # - compliance/pending-releases/RELEASE-TICKET-REQ-050.md (so the script
37
+ # can stage the move + flip; close-out aborts early without it)
38
+ # - a `.git` so the `git mv` step doesn't break
39
+ make_fixture() {
40
+ local dir="$1"
41
+ rm -rf "$dir"
42
+ mkdir -p "$dir/compliance/pending-releases" "$dir/compliance/approved-releases"
43
+ cd "$dir"
44
+ git init -q --initial-branch=main
45
+ git config user.email "test@example.com"
46
+ git config user.name "test"
47
+
48
+ # Multi-table RTM: legend table first (Status | Description, 2 cols, the
49
+ # shape that mangled WGB on REQ-048/050), then the main RTM (REQ-ID, …,
50
+ # Status, …, 7 cols).
51
+ cat > compliance/RTM.md <<'EOF'
52
+ # Requirements Traceability Matrix
53
+
54
+ ## Status Legend
55
+
56
+ | Status | Description |
57
+ | ---------------------------- | ------------------------------------------ |
58
+ | DRAFT | Requirement captured, planning underway |
59
+ | TESTED - PENDING SIGN-OFF | Implementation merged, awaiting close-out |
60
+ | RELEASED | Promoted to main + portal-released |
61
+
62
+ ## Main RTM
63
+
64
+ | REQ-ID | Source | Risk | Evidence | Status | Owner | Date |
65
+ | ------- | ------ | ------ | ------------------------------ | ------------------------- | ------- | ---------- |
66
+ | REQ-049 | #155 | LOW | compliance/evidence/REQ-049/ | RELEASED | thomp@. | 2026-05-24 |
67
+ | REQ-050 | #180 | HIGH | compliance/evidence/REQ-050/ | TESTED - PENDING SIGN-OFF | thomp@. | 2026-05-28 |
68
+ EOF
69
+
70
+ # A minimal release ticket — just enough for the script's pre-checks to pass.
71
+ cat > compliance/pending-releases/RELEASE-TICKET-REQ-050.md <<'EOF'
72
+ # Release Ticket: REQ-050
73
+
74
+ **Status:** TESTED - PENDING SIGN-OFF
75
+ **DevAudit Release:** REQ-050
76
+ EOF
77
+
78
+ git add -A
79
+ git commit -q -m "fixture: pre-close-out state"
80
+ }
81
+
82
+ # ── Case 1: multi-table RTM, status-column lock disambiguated by ID column ─
83
+ {
84
+ dir="$(mktemp -d)/cli-close-out-fixture-1"
85
+ make_fixture "$dir"
86
+ # Skip the portal probe + the ticket-move so we isolate the RTM-flip path.
87
+ unset DEVAUDIT_API_KEY DEVAUDIT_BASE_URL || true
88
+ # Run the script; tolerate exit on the warnings — the RTM flip should still
89
+ # have happened before any non-fatal warning.
90
+ bash "$HELPER" REQ-050 >/dev/null 2>&1 || true
91
+ # Assert: col-1 stays REQ-050 (NOT overwritten with RELEASED); col-5 flips.
92
+ row=$(grep -m1 -E "^\| REQ-050 " compliance/RTM.md || true)
93
+ col1=$(echo "$row" | awk -F '|' '{gsub(/^[[:space:]]+|[[:space:]]+$/,"",$2); print $2}')
94
+ col5=$(echo "$row" | awk -F '|' '{gsub(/^[[:space:]]+|[[:space:]]+$/,"",$6); print $6}')
95
+ assert_eq "REQ-050 row: col-1 unchanged" "REQ-050" "$col1"
96
+ assert_eq "REQ-050 row: col-5 flipped" "RELEASED" "$col5"
97
+ # And the unrelated REQ-049 row stays untouched (it was already RELEASED
98
+ # before this run; if the awk picked up the wrong table or column, col-1
99
+ # would say RELEASED instead of REQ-049).
100
+ row49=$(grep -m1 -E "^\| REQ-049 " compliance/RTM.md || true)
101
+ col1_49=$(echo "$row49" | awk -F '|' '{gsub(/^[[:space:]]+|[[:space:]]+$/,"",$2); print $2}')
102
+ assert_eq "REQ-049 row: col-1 untouched" "REQ-049" "$col1_49"
103
+ rm -rf "$(dirname "$dir")"
104
+ }
105
+
106
+ # ── Case 2: single-table RTM (the simple shape) — behaviour unchanged ──────
107
+ {
108
+ dir="$(mktemp -d)/cli-close-out-fixture-2"
109
+ mkdir -p "$dir/compliance/pending-releases" "$dir/compliance/approved-releases"
110
+ cd "$dir"
111
+ git init -q --initial-branch=main >/dev/null
112
+ git config user.email "test@example.com"
113
+ git config user.name "test"
114
+ cat > compliance/RTM.md <<'EOF'
115
+ # Requirements Traceability Matrix
116
+
117
+ | REQ-ID | Source | Risk | Evidence | Status | Owner | Date |
118
+ | ------- | ------ | ------ | ------------------------------ | ------------------------- | ------- | ---------- |
119
+ | REQ-050 | #180 | HIGH | compliance/evidence/REQ-050/ | TESTED - PENDING SIGN-OFF | thomp@. | 2026-05-28 |
120
+ EOF
121
+ cat > compliance/pending-releases/RELEASE-TICKET-REQ-050.md <<'EOF'
122
+ # Release Ticket: REQ-050
123
+
124
+ **Status:** TESTED - PENDING SIGN-OFF
125
+ **DevAudit Release:** REQ-050
126
+ EOF
127
+ git add -A
128
+ git commit -q -m "fixture: single-table"
129
+ unset DEVAUDIT_API_KEY DEVAUDIT_BASE_URL || true
130
+ bash "$HELPER" REQ-050 >/dev/null 2>&1 || true
131
+ row=$(grep -m1 -E "^\| REQ-050 " compliance/RTM.md || true)
132
+ col1=$(echo "$row" | awk -F '|' '{gsub(/^[[:space:]]+|[[:space:]]+$/,"",$2); print $2}')
133
+ col5=$(echo "$row" | awk -F '|' '{gsub(/^[[:space:]]+|[[:space:]]+$/,"",$6); print $6}')
134
+ assert_eq "single-table: col-1 unchanged" "REQ-050" "$col1"
135
+ assert_eq "single-table: col-5 flipped" "RELEASED" "$col5"
136
+ rm -rf "$(dirname "$dir")"
137
+ }
138
+
139
+ echo
140
+ echo "Result: $PASS passed, $FAIL failed"
141
+ [ "$FAIL" = "0" ]
@@ -34,6 +34,17 @@ jobs:
34
34
 
35
35
  steps:
36
36
  - uses: actions/checkout@v4
37
+ with:
38
+ # The default `pull_request` checkout is a synthetic merge commit
39
+ # with an empty body, so `derive-release-version.sh` can't see the
40
+ # `[REQ-XXX]` tag on the integration-side commit. Pull the head
41
+ # commit directly + full history so the same derivation script the
42
+ # uploaders use (compliance-evidence.yml, ci.yml's register-release)
43
+ # produces the same release identity here (#81 — release-identity
44
+ # coherence; was hardcoded to `v$(date +%Y.%m.%d)` which never
45
+ # matched the `REQ-XXX` records the uploaders wrote).
46
+ ref: ${{ github.event.pull_request.head.sha }}
47
+ fetch-depth: 0
37
48
 
38
49
  - name: Resolve DevAudit base URL
39
50
  run: |
@@ -138,8 +149,15 @@ jobs:
138
149
  id: release
139
150
  if: env.BOOTSTRAP_MODE != 'true'
140
151
  run: |
141
- DATE_PREFIX="v$(date +%Y.%m.%d)"
142
- RESOLVE_URL="${BASE}/api/ci/releases/resolve?projectSlug=${PROJECT_SLUG}&versionPrefix=${DATE_PREFIX}"
152
+ # Derive the same release prefix the uploaders write to. The script
153
+ # priority is: subject `[REQ-XXX]` → body `Ref: REQ-XXX` → body
154
+ # `[REQ-XXX]` (merge-commit case) → bare-date fallback. Reusing it
155
+ # here makes the gate and the uploaders agree on release identity
156
+ # (#81). The 8 scenarios are covered by derive-release-version.test.sh.
157
+ chmod +x scripts/derive-release-version.sh 2>/dev/null || true
158
+ LOOKUP_PREFIX=$(bash scripts/derive-release-version.sh)
159
+ echo "Resolving release for prefix: ${LOOKUP_PREFIX}"
160
+ RESOLVE_URL="${BASE}/api/ci/releases/resolve?projectSlug=${PROJECT_SLUG}&versionPrefix=${LOOKUP_PREFIX}"
143
161
  # Retry: register-release in ci.yml may still be racing with us.
144
162
  MAX_ATTEMPTS=6
145
163
  WAIT_SECONDS=10
@@ -151,13 +169,13 @@ jobs:
151
169
  break
152
170
  fi
153
171
  if [ "$ATTEMPT" -lt "$MAX_ATTEMPTS" ]; then
154
- echo "Attempt ${ATTEMPT}/${MAX_ATTEMPTS}: no release for ${DATE_PREFIX} yet — waiting ${WAIT_SECONDS}s..."
172
+ echo "Attempt ${ATTEMPT}/${MAX_ATTEMPTS}: no release for ${LOOKUP_PREFIX} yet — waiting ${WAIT_SECONDS}s..."
155
173
  sleep $WAIT_SECONDS
156
174
  fi
157
175
  done
158
176
  VERSION=$(echo "$RESP" | jq -r '.latest.version // empty')
159
177
  if [ -z "$VERSION" ]; then
160
- echo "::error::No release found for ${DATE_PREFIX} after ${MAX_ATTEMPTS} attempts. Push to develop first to create the release."
178
+ echo "::error::No release found for ${LOOKUP_PREFIX} after ${MAX_ATTEMPTS} attempts. Push to develop first to create the release."
161
179
  exit 1
162
180
  fi
163
181
  RELEASE_ID=$(echo "$RESP" | jq -r '.latest.id')
@@ -329,17 +329,22 @@ jobs:
329
329
  npm audit --json > ci-evidence/dependency-audit.json 2>/dev/null || echo '{"vulnerabilities":{}}' > ci-evidence/dependency-audit.json
330
330
  fi
331
331
 
332
- # Upload SAST results (security_scan category)
332
+ # Upload SAST results — precise evidence_type=sast_report (Phase 3a /
333
+ # devaudit#370). Pre-3a uploads used `audit_log` + category alone,
334
+ # which made the portal's SAST and Dependency Audit gates show
335
+ # identical content (devaudit#387). Tagging the precise type keeps
336
+ # the two panels distinct + matches the ISO 27001 A.8.28 clause.
333
337
  if [ -f ci-evidence/sast-results.json ]; then
334
338
  upload sast-results.json \
335
- {{PROJECT_SLUG}} _compliance-docs audit_log ci-evidence/sast-results.json \
339
+ {{PROJECT_SLUG}} _compliance-docs sast_report ci-evidence/sast-results.json \
336
340
  --category security_scan ${FLAGS}
337
341
  fi
338
342
 
339
- # Upload dependency audit (security_scan category)
343
+ # Upload dependency audit precise evidence_type=dependency_audit
344
+ # (same rationale as SAST above).
340
345
  if [ -f ci-evidence/dependency-audit.json ]; then
341
346
  upload dependency-audit.json \
342
- {{PROJECT_SLUG}} _compliance-docs audit_log ci-evidence/dependency-audit.json \
347
+ {{PROJECT_SLUG}} _compliance-docs dependency_audit ci-evidence/dependency-audit.json \
343
348
  --category security_scan ${FLAGS}
344
349
  fi
345
350
 
@@ -137,6 +137,36 @@ jobs:
137
137
  fi
138
138
  done
139
139
 
140
+ # Project-level governance docs (devaudit#370 Phase 3a). When the
141
+ # operator commits any of these markdown files, upload with the
142
+ # precise evidence_type so the portal's framework-coverage matrix
143
+ # auto-closes the matching clauses (GDPR.Art-30 for ropa, GDPR.Art-35
144
+ # for dpia, EUAIA.Art-13 for ai_disclosure, SOC2.CC4.1 + ISO27001.A.12.1
145
+ # for periodic_review, etc.). Each path is optional — skipped silently
146
+ # when the file is absent.
147
+ upload_governance() {
148
+ local FILE="$1" TYPE="$2"
149
+ if [ ! -f "$FILE" ]; then return 0; fi
150
+ echo "Uploading governance: $(basename "$FILE") (type=${TYPE})"
151
+ bash scripts/upload-evidence.sh \
152
+ {{PROJECT_SLUG}} _compliance-docs "$TYPE" "$FILE" \
153
+ --category planning ${FLAGS} --release "${DERIVED_RELEASE}" \
154
+ "${DERIVED_META[@]}" \
155
+ || echo "Warning: Failed to upload $(basename "$FILE")"
156
+ }
157
+ # Recognise governance docs at top-level OR under compliance/governance/
158
+ # (operator's choice — both layouts are common).
159
+ upload_governance compliance/ropa.md ropa
160
+ upload_governance compliance/governance/ropa.md ropa
161
+ upload_governance compliance/dpia.md dpia
162
+ upload_governance compliance/governance/dpia.md dpia
163
+ upload_governance compliance/ai-disclosure.md ai_disclosure
164
+ upload_governance compliance/governance/ai-disclosure.md ai_disclosure
165
+ upload_governance compliance/periodic-review.md periodic_review
166
+ upload_governance compliance/governance/periodic-review.md periodic_review
167
+ upload_governance compliance/incident-report.md incident_report
168
+ upload_governance compliance/governance/incident-report.md incident_report
169
+
140
170
  # Helper: emit a `--release-title …` `--change-type …` pair for a given
141
171
  # REQ, derived from its pending release-ticket H1 and the most recent
142
172
  # commit attributed to that REQ. Empty pair when neither is available.