@metasession.co/devaudit-cli 0.1.0 → 0.1.2

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 (70) hide show
  1. package/README.md +23 -11
  2. package/dist/index.js +21 -5
  3. package/dist/index.js.map +1 -1
  4. package/package.json +9 -5
  5. package/scripts/upload-evidence.sh +225 -0
  6. package/sdlc/.claude/settings.local.json +11 -0
  7. package/sdlc/CLAUDE.md +73 -0
  8. package/sdlc/HOST_ADAPTER.md +127 -0
  9. package/sdlc/SKILLS.md +137 -0
  10. package/sdlc/STACK_ADAPTER.md +130 -0
  11. package/sdlc/ai-rules/INSTRUCTIONS-SDLC.md +172 -0
  12. package/sdlc/ai-rules/README.md +103 -0
  13. package/sdlc/ai-rules/SDLC_RULES.md +584 -0
  14. package/sdlc/ai-rules/claude/CLAUDE.md +192 -0
  15. package/sdlc/ai-rules/cursor/.cursorrules +167 -0
  16. package/sdlc/ai-rules/windsurf/.windsurfrules +167 -0
  17. package/sdlc/article.md +219 -0
  18. package/sdlc/files/_common/0-project-setup.md +410 -0
  19. package/sdlc/files/_common/1-plan-requirement.md +381 -0
  20. package/sdlc/files/_common/2-implement-and-test.md +276 -0
  21. package/sdlc/files/_common/3-compile-evidence.md +603 -0
  22. package/sdlc/files/_common/4-submit-for-review.md +362 -0
  23. package/sdlc/files/_common/5-deploy-main.md +251 -0
  24. package/sdlc/files/_common/Periodic_Security_Review_Schedule.md +169 -0
  25. package/sdlc/files/_common/README_TEMPLATE.md +441 -0
  26. package/sdlc/files/_common/Test_Architecture.md +461 -0
  27. package/sdlc/files/_common/Test_Plan_TEMPLATE.md +311 -0
  28. package/sdlc/files/_common/Test_Policy.md +277 -0
  29. package/sdlc/files/_common/Test_Strategy.md +359 -0
  30. package/sdlc/files/_common/github/ISSUE_TEMPLATE/bug.yml +75 -0
  31. package/sdlc/files/_common/github/ISSUE_TEMPLATE/config.yml +11 -0
  32. package/sdlc/files/_common/github/ISSUE_TEMPLATE/requirement.yml +75 -0
  33. package/sdlc/files/_common/github/ISSUE_TEMPLATE/task.yml +48 -0
  34. package/sdlc/files/_common/github/pull_request_template.md +69 -0
  35. package/sdlc/files/_common/implementing-an-sdlc-issue.md +413 -0
  36. package/sdlc/files/_common/scripts/derive-release-version.sh +40 -0
  37. package/sdlc/files/_common/scripts/derive-release-version.test.sh +98 -0
  38. package/sdlc/files/_common/scripts/submit-for-uat-review.sh +162 -0
  39. package/sdlc/files/_common/scripts/validate-commits.sh +83 -0
  40. package/sdlc/files/_common/scripts/validate-compliance-artifacts.sh +202 -0
  41. package/sdlc/files/_common/scripts/validate-compliance-artifacts.test.sh +202 -0
  42. package/sdlc/files/_common/skills/_schema/skill.schema.json +36 -0
  43. package/sdlc/files/_common/skills/e2e-test-engineer/SKILL.md +254 -0
  44. package/sdlc/files/_common/skills/e2e-test-engineer/references/bootstrap.md +244 -0
  45. package/sdlc/files/_common/skills/e2e-test-engineer/references/evidence.ts +40 -0
  46. package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +189 -0
  47. package/sdlc/files/_common/skills/sdlc-implementer/references/call-graph.md +64 -0
  48. package/sdlc/files/_common/skills/sdlc-implementer/references/change-request-loop.md +192 -0
  49. package/sdlc/files/_common/skills/sdlc-implementer/references/compliance-constraints.md +81 -0
  50. package/sdlc/files/ci/check-release-approval.yml.template +201 -0
  51. package/sdlc/files/ci/ci-status-fallback.yml.template +41 -0
  52. package/sdlc/files/ci/ci.yml.template +390 -0
  53. package/sdlc/files/ci/compliance-evidence.yml.template +161 -0
  54. package/sdlc/files/ci/compliance-validation.yml.template +34 -0
  55. package/sdlc/files/ci/post-deploy-prod.yml.template +159 -0
  56. package/sdlc/files/ci/python/ci.yml.template +335 -0
  57. package/sdlc/files/hosts/_schema/adapter.schema.json +103 -0
  58. package/sdlc/files/hosts/railway/adapter.json +32 -0
  59. package/sdlc/files/sdlc-config.example.json +74 -0
  60. package/sdlc/files/stacks/_schema/adapter.schema.json +151 -0
  61. package/sdlc/files/stacks/node/adapter.json +54 -0
  62. package/sdlc/files/stacks/node/hooks/.prettierrc.json +9 -0
  63. package/sdlc/files/stacks/node/hooks/commit-msg +7 -0
  64. package/sdlc/files/stacks/node/hooks/commitlint.config.mjs +64 -0
  65. package/sdlc/files/stacks/node/hooks/lint-staged.config.mjs +16 -0
  66. package/sdlc/files/stacks/node/hooks/pre-commit +13 -0
  67. package/sdlc/files/stacks/node/hooks/pre-push +15 -0
  68. package/sdlc/files/stacks/node/scripts/check-requirement-jsdoc.sh +54 -0
  69. package/sdlc/files/stacks/python/adapter.json +36 -0
  70. package/sdlc/files/stacks/python/hooks/.pre-commit-config.yaml +51 -0
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env bash
2
+ # submit-for-uat-review.sh — Run readiness checks, then submit a release for UAT review.
3
+ #
4
+ # Usage:
5
+ # ./scripts/submit-for-uat-review.sh <project-slug> <version-prefix>
6
+ #
7
+ # Example:
8
+ # ./scripts/submit-for-uat-review.sh wawagardenbar-app v2026.05.14
9
+ #
10
+ # Required environment:
11
+ # DEVAUDIT_USER_TOKEN — Personal Access Token (mctok_…) issued from
12
+ # /settings/tokens. Attributes the submission
13
+ # to the issuing user so the four-eyes approval
14
+ # control is preserved (the submitter cannot
15
+ # approve their own release).
16
+ # DEVAUDIT_API_KEY — project-scoped API key (existing) used to
17
+ # resolve the release id and read its current
18
+ # status. Distinct from the PAT.
19
+ # DEVAUDIT_BASE_URL — DevAudit base URL (e.g. https://devaudit.metasession.co).
20
+ #
21
+ # What it does:
22
+ # 1. Verify working tree is clean and develop is up-to-date with origin.
23
+ # 2. Verify the release ticket exists in compliance/pending-releases/.
24
+ # 3. Verify CI gates green on the current develop HEAD via gh CLI.
25
+ # 4. Resolve the release id from (projectSlug, versionPrefix) via /api/ci/releases/resolve.
26
+ # 5. If status is draft → POST /api/releases/<id>/submit-review with the PAT.
27
+ # If status is uat_review → idempotent no-op (exit 0, "already submitted").
28
+ # If status is uat_approved → idempotent no-op (exit 0, "already approved").
29
+ # Any other status → refuse with an explanatory message.
30
+
31
+ set -euo pipefail
32
+
33
+ PROJECT_SLUG="${1:-}"
34
+ VERSION_PREFIX="${2:-}"
35
+
36
+ if [ -z "$PROJECT_SLUG" ] || [ -z "$VERSION_PREFIX" ]; then
37
+ echo "Usage: $0 <project-slug> <version-prefix>" >&2
38
+ echo "Example: $0 wawagardenbar-app v2026.05.14" >&2
39
+ exit 1
40
+ fi
41
+
42
+ : "${DEVAUDIT_USER_TOKEN:?DEVAUDIT_USER_TOKEN must be set (issue from /settings/tokens)}"
43
+ : "${DEVAUDIT_API_KEY:?DEVAUDIT_API_KEY must be set (project API key)}"
44
+ : "${DEVAUDIT_BASE_URL:?DEVAUDIT_BASE_URL must be set (e.g. https://devaudit.metasession.co)}"
45
+
46
+ BASE_URL="${DEVAUDIT_BASE_URL%/}"
47
+
48
+ FAILED=0
49
+ note() { printf ' - %s\n' "$*"; }
50
+ fail() { printf ' ✗ %s\n' "$*"; FAILED=$((FAILED + 1)); }
51
+ ok() { printf ' ✓ %s\n' "$*"; }
52
+
53
+ echo "Readiness checks for ${PROJECT_SLUG} ${VERSION_PREFIX}"
54
+
55
+ # 1. Working tree clean
56
+ if [ -n "$(git status --porcelain)" ]; then
57
+ fail "Working tree has uncommitted changes — commit or stash before submitting."
58
+ else
59
+ ok "Working tree clean."
60
+ fi
61
+
62
+ # 2. develop up-to-date with origin
63
+ git fetch origin develop --quiet
64
+ LOCAL_SHA="$(git rev-parse develop 2>/dev/null || echo '')"
65
+ REMOTE_SHA="$(git rev-parse origin/develop 2>/dev/null || echo '')"
66
+ if [ -z "$LOCAL_SHA" ] || [ -z "$REMOTE_SHA" ]; then
67
+ fail "Cannot resolve develop SHA — is the develop branch present?"
68
+ elif [ "$LOCAL_SHA" != "$REMOTE_SHA" ]; then
69
+ fail "Local develop (${LOCAL_SHA:0:7}) does not match origin/develop (${REMOTE_SHA:0:7}) — push first."
70
+ else
71
+ ok "develop is up-to-date with origin (${LOCAL_SHA:0:7})."
72
+ fi
73
+
74
+ # 3. Release ticket exists somewhere under compliance/pending-releases/
75
+ TICKETS=$(find compliance/pending-releases -maxdepth 1 -name 'RELEASE-TICKET-*.md' 2>/dev/null | head -5 || true)
76
+ if [ -z "$TICKETS" ]; then
77
+ fail "No RELEASE-TICKET-*.md found in compliance/pending-releases/."
78
+ else
79
+ ok "Release ticket present:$(echo "$TICKETS" | sed 's|.*/| |g' | tr '\n' ' ')"
80
+ fi
81
+
82
+ # 4. CI gates green on current develop HEAD (if gh CLI available)
83
+ if command -v gh >/dev/null 2>&1; then
84
+ CI_CONCLUSIONS=$(gh run list --branch develop --commit "$REMOTE_SHA" --limit 10 --json conclusion,status,name 2>/dev/null || echo '[]')
85
+ if [ "$CI_CONCLUSIONS" = '[]' ]; then
86
+ fail "No CI runs found for ${REMOTE_SHA:0:7} — has the push reached GitHub Actions yet?"
87
+ else
88
+ INCOMPLETE=$(echo "$CI_CONCLUSIONS" | jq -r '[.[] | select(.status != "completed")] | length')
89
+ FAILED_RUNS=$(echo "$CI_CONCLUSIONS" | jq -r '[.[] | select(.conclusion == "failure" or .conclusion == "cancelled")] | length')
90
+ if [ "$INCOMPLETE" -gt 0 ]; then
91
+ fail "CI still running on ${REMOTE_SHA:0:7} (${INCOMPLETE} in-flight)."
92
+ elif [ "$FAILED_RUNS" -gt 0 ]; then
93
+ fail "CI failed on ${REMOTE_SHA:0:7} (${FAILED_RUNS} runs failed)."
94
+ else
95
+ ok "CI gates green on ${REMOTE_SHA:0:7}."
96
+ fi
97
+ fi
98
+ else
99
+ note "gh CLI not available — skipping CI-green check. Verify in GitHub UI."
100
+ fi
101
+
102
+ # 5. Resolve release id from (projectSlug, versionPrefix)
103
+ RESOLVE_URL="${BASE_URL}/api/ci/releases/resolve?projectSlug=${PROJECT_SLUG}&versionPrefix=${VERSION_PREFIX}"
104
+ RESOLVE=$(curl -s -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" "$RESOLVE_URL")
105
+ RELEASE_ID=$(echo "$RESOLVE" | jq -r '.latest.id // empty')
106
+ RELEASE_STATUS=$(echo "$RESOLVE" | jq -r '.latest.status // empty')
107
+ RELEASE_VERSION=$(echo "$RESOLVE" | jq -r '.latest.version // empty')
108
+
109
+ if [ -z "$RELEASE_ID" ]; then
110
+ fail "No release found in DevAudit for ${PROJECT_SLUG} ${VERSION_PREFIX} (has CI uploaded evidence yet?)."
111
+ else
112
+ ok "Release found: ${RELEASE_VERSION} (id ${RELEASE_ID:0:8}…, status ${RELEASE_STATUS})."
113
+ fi
114
+
115
+ if [ "$FAILED" -gt 0 ]; then
116
+ echo ""
117
+ echo "Refusing to submit — ${FAILED} readiness check(s) failed."
118
+ exit 1
119
+ fi
120
+
121
+ # 6. Idempotent dispatch on release status
122
+ case "$RELEASE_STATUS" in
123
+ draft)
124
+ echo ""
125
+ echo "Submitting ${RELEASE_VERSION} for UAT review…"
126
+ SUBMIT_URL="${BASE_URL}/api/releases/${RELEASE_ID}/submit-review"
127
+ RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
128
+ -H "X-DevAudit-Token: ${DEVAUDIT_USER_TOKEN}" \
129
+ -H "Content-Type: application/json" \
130
+ "$SUBMIT_URL")
131
+ HTTP_CODE=$(echo "$RESPONSE" | tail -1)
132
+ BODY=$(echo "$RESPONSE" | sed '$d')
133
+ if [ "$HTTP_CODE" = "200" ]; then
134
+ NEW_STATUS=$(echo "$BODY" | jq -r '.status')
135
+ echo " Status: draft → ${NEW_STATUS}"
136
+ echo " Review page: ${BASE_URL}/projects/${PROJECT_SLUG}/releases/${RELEASE_ID}?env=uat"
137
+ else
138
+ echo " Submission failed (HTTP ${HTTP_CODE}): $(echo "$BODY" | jq -r '.error // .')" >&2
139
+ exit 1
140
+ fi
141
+ ;;
142
+ uat_review)
143
+ echo ""
144
+ echo "Release already submitted for UAT review (status: uat_review). No action taken."
145
+ echo "Review page: ${BASE_URL}/projects/${PROJECT_SLUG}/releases/${RELEASE_ID}?env=uat"
146
+ ;;
147
+ uat_approved | prod_review | prod_approved | released)
148
+ echo ""
149
+ echo "Release has progressed past UAT review (status: ${RELEASE_STATUS}). No action taken."
150
+ echo "Release page: ${BASE_URL}/projects/${PROJECT_SLUG}/releases/${RELEASE_ID}"
151
+ ;;
152
+ uat_rejected | prod_rejected)
153
+ echo ""
154
+ echo "Release is in a rejected state (status: ${RELEASE_STATUS}). Address review comments and push a fix before resubmitting." >&2
155
+ exit 2
156
+ ;;
157
+ *)
158
+ echo ""
159
+ echo "Release in unexpected status: ${RELEASE_STATUS}. Refusing to submit." >&2
160
+ exit 2
161
+ ;;
162
+ esac
@@ -0,0 +1,83 @@
1
+ #!/bin/bash
2
+ # validate-commits.sh — Validates commit conventions on PRs.
3
+ #
4
+ # Usage:
5
+ # ./scripts/validate-commits.sh [base-branch]
6
+ #
7
+ # Checks all commits in the PR for:
8
+ # - Conventional Commits format (type: description or type(scope): description)
9
+ # - Co-Authored-By tag on commits touching code files (warning)
10
+ # - Ref: REQ-XXX on commits related to tracked requirements (warning)
11
+ #
12
+ # Install: cp this file to your project's scripts/ directory && chmod +x scripts/validate-commits.sh
13
+
14
+ set -euo pipefail
15
+
16
+ BASE_BRANCH="${1:-origin/main}"
17
+ EXIT_CODE=0
18
+ WARN_COUNT=0
19
+
20
+ echo "=== Commit Convention Validation ==="
21
+ echo "Comparing: $BASE_BRANCH...HEAD"
22
+ echo ""
23
+
24
+ # Conventional Commit regex: type(optional-scope): description
25
+ CC_REGEX='^(feat|fix|docs|test|refactor|chore|compliance|security|perf|ci|build|revert)(\([a-zA-Z0-9_-]+\))?!?: .+'
26
+
27
+ COMMITS=$(git log "$BASE_BRANCH"..HEAD --format='%H' || true)
28
+
29
+ if [ -z "$COMMITS" ]; then
30
+ echo "No commits found between $BASE_BRANCH and HEAD."
31
+ exit 0
32
+ fi
33
+
34
+ TOTAL=0
35
+ FAILED=0
36
+
37
+ while IFS= read -r sha; do
38
+ TOTAL=$((TOTAL + 1))
39
+ SUBJECT=$(git log -1 --format='%s' "$sha")
40
+ BODY=$(git log -1 --format='%B' "$sha")
41
+ SHORT=$(git log -1 --format='%h' "$sha")
42
+
43
+ # Check Conventional Commits format
44
+ if ! echo "$SUBJECT" | grep -qE "$CC_REGEX"; then
45
+ # Allow merge commits
46
+ if echo "$SUBJECT" | grep -q '^Merge '; then
47
+ continue
48
+ fi
49
+ echo "ERROR [$SHORT]: Not Conventional Commits format: \"$SUBJECT\""
50
+ FAILED=$((FAILED + 1))
51
+ EXIT_CODE=1
52
+ continue
53
+ fi
54
+
55
+ # Check Co-Authored-By on commits that touch code files
56
+ CODE_FILES=$(git diff-tree --no-commit-id --name-only -r "$sha" -- '*.ts' '*.tsx' '*.js' '*.jsx' '*.py' 2>/dev/null || true)
57
+ if [ -n "$CODE_FILES" ]; then
58
+ if ! echo "$BODY" | grep -qi 'Co-Authored-By:'; then
59
+ echo "WARNING [$SHORT]: Touches code files but missing Co-Authored-By tag"
60
+ WARN_COUNT=$((WARN_COUNT + 1))
61
+ fi
62
+ fi
63
+
64
+ # Check Ref: REQ-XXX on requirement-related commits
65
+ if echo "$BODY" | grep -qi 'compliance\|requirement\|REQ-'; then
66
+ if ! echo "$BODY" | grep -qP 'Ref:\s*REQ-\d+'; then
67
+ echo "WARNING [$SHORT]: Appears requirement-related but missing Ref: REQ-XXX"
68
+ WARN_COUNT=$((WARN_COUNT + 1))
69
+ fi
70
+ fi
71
+
72
+ done <<< "$COMMITS"
73
+
74
+ echo ""
75
+ echo "Checked $TOTAL commits: $FAILED errors, $WARN_COUNT warnings."
76
+
77
+ if [ $EXIT_CODE -eq 0 ]; then
78
+ echo "=== Commit convention check passed ==="
79
+ else
80
+ echo "=== FAILED: $FAILED commits do not follow Conventional Commits format ==="
81
+ fi
82
+
83
+ exit $EXIT_CODE
@@ -0,0 +1,202 @@
1
+ #!/bin/bash
2
+ # validate-compliance-artifacts.sh — Validates compliance artifacts exist on PRs to main.
3
+ #
4
+ # Usage:
5
+ # ./scripts/validate-compliance-artifacts.sh [base-branch]
6
+ #
7
+ # Checks that PRs containing tracked requirement commits have the required
8
+ # compliance artifacts (test-scope, RTM entry, release ticket, etc.).
9
+ # Designed to run as a CI job on PRs to main.
10
+ #
11
+ # Install: cp this file to your project's scripts/ directory && chmod +x scripts/validate-compliance-artifacts.sh
12
+
13
+ set -euo pipefail
14
+
15
+ BASE_BRANCH="${1:-origin/main}"
16
+ EXIT_CODE=0
17
+
18
+ echo "=== Compliance Artifact Validation ==="
19
+ echo "Comparing: $BASE_BRANCH...HEAD"
20
+ echo ""
21
+
22
+ # Extract REQ-XXX references from commits in this PR.
23
+ #
24
+ # Requires ≥3 digits so placeholder patterns like `REQ-0XX` (used in
25
+ # commit-body templates referring to a batch of sub-REQs) don't create
26
+ # phantom IDs that block CI — the loose `\d+` would match `REQ-0` from
27
+ # `REQ-0XX` and then ERROR on a missing evidence dir for the phantom
28
+ # `REQ-0`. The project's stable id format is REQ-001 onwards (#232).
29
+ REQUIREMENTS=$(git log "$BASE_BRANCH"..HEAD --format='%B' | grep -oP 'REQ-\d{3,}' | sort -u || true)
30
+
31
+ if [ -z "$REQUIREMENTS" ]; then
32
+ echo "No REQ-XXX references found in PR commits — skipping artifact validation."
33
+ echo "If this PR contains tracked requirements, ensure commits include 'Ref: REQ-XXX'."
34
+ exit 0
35
+ fi
36
+
37
+ echo "Requirements found in PR commits: $REQUIREMENTS"
38
+ echo ""
39
+
40
+ for REQ in $REQUIREMENTS; do
41
+ echo "--- Checking $REQ ---"
42
+
43
+ # Skip REQs that have no RTM row — they're forward-references or
44
+ # design-discussion mentions in commit bodies, not tracked
45
+ # requirements for this release. Two practical cases (#232):
46
+ # - "REQ-033 is a prereq for REQ-034" — REQ-034 hasn't started.
47
+ # - REQ scaffolded then descoped — RTM row removed but old
48
+ # `Ref: REQ-XXX` commits still grep-match.
49
+ # The `| $REQ ` pattern (leading pipe + trailing space) matches the
50
+ # markdown table row used in RTM, so a substring elsewhere doesn't
51
+ # accidentally satisfy it.
52
+ if ! grep -q "| $REQ " compliance/RTM.md 2>/dev/null; then
53
+ echo " INFO: $REQ is referenced in commits but has no RTM row — skipping (forward-reference, not a tracked requirement for this release)"
54
+ continue
55
+ fi
56
+
57
+ # Check evidence directory exists
58
+ if [ ! -d "compliance/evidence/$REQ" ]; then
59
+ echo " ERROR: Evidence directory missing: compliance/evidence/$REQ/"
60
+ EXIT_CODE=1
61
+ continue
62
+ fi
63
+ echo " OK: Evidence directory exists"
64
+
65
+ # Check test-scope.md exists
66
+ if [ ! -f "compliance/evidence/$REQ/test-scope.md" ]; then
67
+ echo " ERROR: Test scope missing: compliance/evidence/$REQ/test-scope.md"
68
+ EXIT_CODE=1
69
+ else
70
+ echo " OK: test-scope.md exists"
71
+ fi
72
+
73
+ # Check test-plan.md exists
74
+ if [ ! -f "compliance/evidence/$REQ/test-plan.md" ]; then
75
+ echo " ERROR: Test plan missing: compliance/evidence/$REQ/test-plan.md"
76
+ EXIT_CODE=1
77
+ else
78
+ echo " OK: test-plan.md exists"
79
+
80
+ # Verify test files referenced in test-plan.md exist in the tree.
81
+ # Two alternatives: directory-prefixed paths (captured whole), and bare
82
+ # filenames containing .test. / .spec. (captured from the stem, not the
83
+ # dot — without the leading [\w./-]+ the match would start at `.test.`
84
+ # and drop the filename entirely). See DevAudit #133.
85
+ #
86
+ # mapfile + quoted expansion: without this, tokens like `__tests__/**`
87
+ # get pathname-expanded by the for-loop and produce phantom "missing"
88
+ # errors for every real subdirectory. See DevAudit #137.
89
+ mapfile -t TEST_FILES < <(
90
+ grep -oP '(?:__tests__/|tests?/|e2e/|spec/)\S+|[\w./-]+\.(?:test|spec)\.\S+' \
91
+ "compliance/evidence/$REQ/test-plan.md" 2>/dev/null \
92
+ | sed 's/[`),.;:]*$//' | grep -v '/$' | sort -u || true
93
+ )
94
+ if [ "${#TEST_FILES[@]}" -gt 0 ]; then
95
+ MISSING_TESTS=0
96
+ for TF in "${TEST_FILES[@]}"; do
97
+ # Prose glob references like `__tests__/**` describe a directory
98
+ # set, not a specific file — skip instead of treating as missing.
99
+ if [[ "$TF" == *[*?]* ]]; then
100
+ echo " INFO: Skipping glob reference (not a file path): $TF"
101
+ continue
102
+ fi
103
+ # Try exact path; otherwise search the repo by basename, skipping
104
+ # node_modules and build/coverage outputs. The previous
105
+ # `compgen -G "**/X"` form relied on bash globstar to recurse,
106
+ # but globstar is OFF by default — `**` collapsed to a single
107
+ # `*` (depth-1 wildcard), so any bare-filename reference whose
108
+ # actual file lived at depth ≥2 was falsely reported as missing.
109
+ if [ ! -f "$TF" ] && ! find . \
110
+ -type d \( -name node_modules -o -name .next -o -name .git \
111
+ -o -name playwright-report -o -name coverage \
112
+ -o -name dist -o -name build -o -name 'test-results' \) -prune \
113
+ -o -type f -name "$(basename "$TF")" -print 2>/dev/null \
114
+ | grep -q .; then
115
+ echo " ERROR: Test file referenced in test-plan.md not found: $TF"
116
+ MISSING_TESTS=$((MISSING_TESTS + 1))
117
+ fi
118
+ done
119
+ if [ "$MISSING_TESTS" -gt 0 ]; then
120
+ echo " ERROR: $MISSING_TESTS test file(s) from test-plan.md missing — tests must be written before merge"
121
+ EXIT_CODE=1
122
+ else
123
+ echo " OK: All test files referenced in test-plan.md exist"
124
+ fi
125
+ fi
126
+ fi
127
+
128
+ # Check test-execution-summary.md exists
129
+ if [ ! -f "compliance/evidence/$REQ/test-execution-summary.md" ]; then
130
+ echo " WARNING: Test execution summary missing: compliance/evidence/$REQ/test-execution-summary.md"
131
+ else
132
+ echo " OK: test-execution-summary.md exists"
133
+ fi
134
+
135
+ # Check RTM entry exists and has an accepted terminal status. Accepts
136
+ # SUPERSEDED as a valid terminal state — used when a REQ is replaced
137
+ # by a successor (e.g. REQ-030 superseded by REQ-031). Without this
138
+ # branch every PR with a commit referencing a SUPERSEDED REQ blocks
139
+ # CI (#232).
140
+ if grep -q "$REQ" compliance/RTM.md 2>/dev/null; then
141
+ if grep "$REQ" compliance/RTM.md | grep -q "TESTED - PENDING SIGN-OFF"; then
142
+ echo " OK: RTM status is TESTED - PENDING SIGN-OFF"
143
+ elif grep "$REQ" compliance/RTM.md | grep -q "APPROVED"; then
144
+ echo " OK: RTM status is APPROVED"
145
+ elif grep "$REQ" compliance/RTM.md | grep -q "SUPERSEDED"; then
146
+ echo " OK: RTM status is SUPERSEDED"
147
+ else
148
+ echo " WARNING: RTM entry exists but status is not TESTED - PENDING SIGN-OFF"
149
+ fi
150
+ else
151
+ echo " ERROR: No RTM entry found for $REQ in compliance/RTM.md"
152
+ EXIT_CODE=1
153
+ fi
154
+
155
+ # Check release ticket exists. Accepts the superseded-releases/
156
+ # location as a valid terminal home (mirrors pending- and
157
+ # approved-releases/) so SUPERSEDED REQs don't block CI (#232).
158
+ TICKET_PATTERN="compliance/pending-releases/RELEASE-TICKET-${REQ}*"
159
+ APPROVED_PATTERN="compliance/approved-releases/RELEASE-TICKET-${REQ}*"
160
+ SUPERSEDED_PATTERN="compliance/superseded-releases/RELEASE-TICKET-${REQ}*"
161
+ if compgen -G "$TICKET_PATTERN" > /dev/null 2>&1 \
162
+ || compgen -G "$APPROVED_PATTERN" > /dev/null 2>&1 \
163
+ || compgen -G "$SUPERSEDED_PATTERN" > /dev/null 2>&1; then
164
+ echo " OK: Release ticket exists"
165
+ else
166
+ echo " ERROR: Release ticket missing: compliance/pending-releases/RELEASE-TICKET-${REQ}.md"
167
+ EXIT_CODE=1
168
+ fi
169
+
170
+ # Check implementation-plan.md for MEDIUM/HIGH risk (check RTM for risk level)
171
+ if grep "$REQ" compliance/RTM.md 2>/dev/null | grep -qiE 'MEDIUM|HIGH'; then
172
+ if [ ! -f "compliance/evidence/$REQ/implementation-plan.md" ]; then
173
+ echo " ERROR: Implementation plan missing for MEDIUM/HIGH risk: compliance/evidence/$REQ/implementation-plan.md"
174
+ EXIT_CODE=1
175
+ else
176
+ echo " OK: implementation-plan.md exists (MEDIUM/HIGH risk)"
177
+ fi
178
+
179
+ # Check AI prompt log for MEDIUM/HIGH with AI involvement
180
+ if [ -f "compliance/evidence/$REQ/ai-use-note.md" ] && [ ! -f "compliance/evidence/$REQ/ai-prompts.md" ]; then
181
+ echo " WARNING: AI use noted but ai-prompts.md missing for MEDIUM/HIGH risk"
182
+ fi
183
+ fi
184
+
185
+ echo ""
186
+ done
187
+
188
+ # Check that RTM was updated in this PR
189
+ if git diff --name-only "$BASE_BRANCH"...HEAD | grep -q 'compliance/RTM.md'; then
190
+ echo "OK: compliance/RTM.md was updated in this PR"
191
+ else
192
+ echo "WARNING: compliance/RTM.md was not modified in this PR"
193
+ fi
194
+
195
+ echo ""
196
+ if [ $EXIT_CODE -eq 0 ]; then
197
+ echo "=== All compliance artifact checks passed ==="
198
+ else
199
+ echo "=== FAILED: Missing required compliance artifacts ==="
200
+ fi
201
+
202
+ exit $EXIT_CODE
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env bash
2
+ # validate-compliance-artifacts.test.sh — fixture-based tests for #232
3
+ # hardenings (regex tightening, no-RTM-row skip, SUPERSEDED handling).
4
+ #
5
+ # Builds a throwaway git repo per case, populates compliance/ files,
6
+ # runs validate-compliance-artifacts.sh against it, asserts on the exit
7
+ # code and selected log lines.
8
+ #
9
+ # Usage:
10
+ # ./scripts/validate-compliance-artifacts.test.sh
11
+ #
12
+ # The script is hermetic: it does NOT touch the host repo; everything
13
+ # runs inside a mktemp'd directory that's torn down at the end. Suitable
14
+ # for CI invocation, no extra deps beyond bash + git + grep.
15
+
16
+ set -euo pipefail
17
+
18
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
19
+ VALIDATOR="$SCRIPT_DIR/validate-compliance-artifacts.sh"
20
+ [ -x "$VALIDATOR" ] || chmod +x "$VALIDATOR"
21
+
22
+ PASS=0
23
+ FAIL=0
24
+
25
+ # --- helpers ---
26
+
27
+ # Build a fresh git fixture under $1 and cd into it. Initial commit on
28
+ # a `base` branch, then a single feature commit on `feature` whose body
29
+ # the caller seeds via the second argument.
30
+ make_fixture() {
31
+ local dir="$1" feat_msg="$2"
32
+ rm -rf "$dir"
33
+ mkdir -p "$dir"
34
+ cd "$dir"
35
+ git init -q --initial-branch=base
36
+ git config user.email "test@example.com"
37
+ git config user.name "test"
38
+ mkdir -p compliance/pending-releases compliance/approved-releases compliance/superseded-releases compliance/evidence
39
+ printf '# RTM\n\n| ID | Description | Status |\n| --- | --- | --- |\n' > compliance/RTM.md
40
+ git add . && git commit -q -m "init"
41
+ git checkout -q -b feature
42
+ echo "feature change" > feature.txt
43
+ git add . && git commit -q -m "feat: feature commit
44
+
45
+ $feat_msg"
46
+ }
47
+
48
+ # Run the validator against the current fixture's HEAD vs base. Captures
49
+ # stdout to $OUT_FILE and exit code to $EXIT_CODE_VAR.
50
+ run_validator() {
51
+ set +e
52
+ OUT_FILE=$(mktemp)
53
+ bash "$VALIDATOR" base > "$OUT_FILE" 2>&1
54
+ LAST_EXIT=$?
55
+ set -e
56
+ }
57
+
58
+ assert_grep() {
59
+ local desc="$1" pattern="$2" want_match="$3"
60
+ if grep -qE "$pattern" "$OUT_FILE"; then
61
+ found=1
62
+ else
63
+ found=0
64
+ fi
65
+ if [ "$found" = "$want_match" ]; then
66
+ echo " PASS: $desc"
67
+ PASS=$((PASS + 1))
68
+ else
69
+ echo " FAIL: $desc (want match=$want_match, got=$found, pattern=$pattern)"
70
+ echo " --- output ---"
71
+ sed 's/^/ /' "$OUT_FILE"
72
+ echo " ---"
73
+ FAIL=$((FAIL + 1))
74
+ fi
75
+ }
76
+
77
+ assert_exit() {
78
+ local desc="$1" want="$2"
79
+ if [ "$LAST_EXIT" = "$want" ]; then
80
+ echo " PASS: $desc (exit=$LAST_EXIT)"
81
+ PASS=$((PASS + 1))
82
+ else
83
+ echo " FAIL: $desc (want=$want, got=$LAST_EXIT)"
84
+ echo " --- output ---"
85
+ sed 's/^/ /' "$OUT_FILE"
86
+ echo " ---"
87
+ FAIL=$((FAIL + 1))
88
+ fi
89
+ }
90
+
91
+ WORKDIR=$(mktemp -d -t validate-compliance-test-XXXX)
92
+ trap 'rm -rf "$WORKDIR"' EXIT
93
+
94
+ # --- case 1: REQ-0XX placeholder is ignored (\d{3,} regex) ---
95
+
96
+ echo "Case 1: REQ-0XX placeholder doesn't create phantom REQ-0"
97
+ make_fixture "$WORKDIR/case1" "Ref: REQ-0XX/test-scope.md (placeholder for sub-REQs)"
98
+ run_validator
99
+ assert_grep "no phantom REQ-0 surfaced" '^Requirements found in PR commits: REQ-0\b' 0
100
+ assert_grep "skip-validation message present" 'No REQ-XXX references found' 1
101
+ assert_exit "validator exits 0 on placeholder-only commit body" 0
102
+ cd "$WORKDIR"
103
+
104
+ # --- case 2: REQ-099 mentioned but absent from RTM → INFO + skip ---
105
+
106
+ echo "Case 2: forward-reference REQ skipped when RTM row absent"
107
+ make_fixture "$WORKDIR/case2" "Ref: REQ-099 (this is a forward-reference, no RTM row)"
108
+ run_validator
109
+ assert_grep "INFO line emitted for forward-reference" 'INFO: REQ-099 is referenced.*no RTM row' 1
110
+ assert_grep "no ERROR for missing evidence dir" 'ERROR: Evidence directory missing.*REQ-099' 0
111
+ assert_exit "validator exits 0 when only forward-references present" 0
112
+ cd "$WORKDIR"
113
+
114
+ # --- case 3: SUPERSEDED RTM status is accepted ---
115
+
116
+ echo "Case 3: SUPERSEDED RTM status passes"
117
+ make_fixture "$WORKDIR/case3" "Ref: REQ-030"
118
+ # RTM with SUPERSEDED status; full evidence + ticket present
119
+ {
120
+ echo '# RTM'
121
+ echo
122
+ echo '| ID | Description | Status |'
123
+ echo '| --- | --- | --- |'
124
+ echo '| REQ-030 | Replaced by REQ-031 | SUPERSEDED by REQ-031 |'
125
+ } > compliance/RTM.md
126
+ mkdir -p compliance/evidence/REQ-030
127
+ echo "scope" > compliance/evidence/REQ-030/test-scope.md
128
+ echo "plan" > compliance/evidence/REQ-030/test-plan.md
129
+ touch compliance/pending-releases/RELEASE-TICKET-REQ-030.md
130
+ git add . && git commit -q --amend --no-edit
131
+ run_validator
132
+ assert_grep "RTM SUPERSEDED accepted" 'OK: RTM status is SUPERSEDED' 1
133
+ assert_exit "validator exits 0 with SUPERSEDED RTM status" 0
134
+ cd "$WORKDIR"
135
+
136
+ # --- case 4: SUPERSEDED ticket location is accepted ---
137
+
138
+ echo "Case 4: superseded-releases/ ticket location accepted"
139
+ make_fixture "$WORKDIR/case4" "Ref: REQ-031"
140
+ {
141
+ echo '# RTM'
142
+ echo
143
+ echo '| ID | Description | Status |'
144
+ echo '| --- | --- | --- |'
145
+ echo '| REQ-031 | Successor of REQ-030 | SUPERSEDED by REQ-032 |'
146
+ } > compliance/RTM.md
147
+ mkdir -p compliance/evidence/REQ-031
148
+ echo "scope" > compliance/evidence/REQ-031/test-scope.md
149
+ echo "plan" > compliance/evidence/REQ-031/test-plan.md
150
+ # Only the superseded location — neither pending- nor approved-releases.
151
+ touch compliance/superseded-releases/RELEASE-TICKET-REQ-031.md
152
+ git add . && git commit -q --amend --no-edit
153
+ run_validator
154
+ assert_grep "ticket in superseded-releases/ accepted" 'OK: Release ticket exists' 1
155
+ assert_grep "no missing-ticket ERROR" 'ERROR: Release ticket missing' 0
156
+ assert_exit "validator exits 0 with SUPERSEDED ticket location" 0
157
+ cd "$WORKDIR"
158
+
159
+ # --- case 5: bare-filename reference resolves to file at depth ≥2 ---
160
+ #
161
+ # Regression for the broken `compgen -G "**/$TF"` search: bash globstar
162
+ # is off by default, so `**` only matched depth-1 paths. A test plan
163
+ # referencing a bare filename (e.g. `inventory-service.list-by-kind.test.ts`)
164
+ # whose actual file lived at `__tests__/services/...` was reported as
165
+ # missing even though it existed. Fix uses `find -name` instead.
166
+
167
+ echo "Case 5: bare-filename reference resolves to depth-2 test file"
168
+ make_fixture "$WORKDIR/case5" "Ref: REQ-200"
169
+ {
170
+ echo '# RTM'
171
+ echo
172
+ echo '| ID | Description | Status |'
173
+ echo '| --- | --- | --- |'
174
+ echo '| REQ-200 | Bare-filename ref test | IN PROGRESS |'
175
+ } > compliance/RTM.md
176
+ mkdir -p compliance/evidence/REQ-200
177
+ echo "scope" > compliance/evidence/REQ-200/test-scope.md
178
+ # Plan references the file by BARE filename (no directory prefix).
179
+ {
180
+ echo '# Test Plan — REQ-200'
181
+ echo
182
+ echo '| AC | Test | Notes |'
183
+ echo '| --- | --- | --- |'
184
+ echo '| AC1 | `foo-service.list-by-kind.test.ts` | unit test |'
185
+ } > compliance/evidence/REQ-200/test-plan.md
186
+ # Actual test file lives at depth 2.
187
+ mkdir -p __tests__/services
188
+ echo "// stub" > __tests__/services/foo-service.list-by-kind.test.ts
189
+ echo "summary" > compliance/evidence/REQ-200/test-execution-summary.md
190
+ touch compliance/pending-releases/RELEASE-TICKET-REQ-200.md
191
+ git add . && git commit -q --amend --no-edit
192
+ run_validator
193
+ assert_grep "depth-2 bare filename resolved" 'OK: All test files referenced in test-plan.md exist' 1
194
+ assert_grep "no missing-test ERROR" 'ERROR: Test file referenced in test-plan.md not found' 0
195
+ assert_exit "validator exits 0 with depth-2 bare-filename reference" 0
196
+ cd "$WORKDIR"
197
+
198
+ # --- summary ---
199
+
200
+ echo
201
+ echo "=== validate-compliance-artifacts.test.sh: $PASS passed, $FAIL failed ==="
202
+ [ "$FAIL" -eq 0 ]