Skip to content

Commit ab90a3c

Browse files
committed
feat(sdk-review): batch 4/6 — structural checks + BREAKING family
Adds 5 checks that enforce structural conventions: ## Checks (.claude/scripts/) - check-docs.sh — DC-01..16 (user-guide.md sections; BTP dependency documentation for Destination/Fragments/Certificates/Regional Availability) - check-bdd.sh — BDD-01..05 (feature files + cross-language parity via module-aliases.yaml) - check-patterns.sh — PT-01..11 (factory pattern, py.typed, exception hierarchy, type hints). Scope-gated to client modules. - check-versioning.sh — VER-01..07 + BREAKING-01..04 family. Uses breaking-detector.py (from batch 1) for AST-based diff. BREAKING-01/02 BLOCK_LOCKED — cannot be suppressed. - check-commits.sh — COM-01 Conventional Commits. Merge commits skipped via parent-count detection (robust regardless of message shape). Part 4 of 6. Ref: AFSDK-3937
1 parent 1284315 commit ab90a3c

7 files changed

Lines changed: 446 additions & 0 deletions

File tree

.claude/scripts/check-bdd.sh

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#!/usr/bin/env bash
2+
# check-bdd.sh — feature file existence + cross-language parity via alias map.
3+
set -euo pipefail
4+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
source "$SCRIPT_DIR/lib/json-emit.sh"
6+
7+
LANGUAGE="${LANGUAGE:-python}"
8+
REPO_ROOT="${REPO_ROOT:-.}"
9+
DIFF_FILE="${DIFF_FILE:-/dev/stdin}"
10+
SDK_SIBLING_PATH="${SDK_SIBLING_PATH:-}"
11+
CONFIG_DIR="${CONFIG_DIR:-$REPO_ROOT/.claude/config}"
12+
13+
STARTED=$(now_iso)
14+
findings=$(mktemp); trap 'rm -f "$findings"' EXIT
15+
16+
diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "")
17+
18+
# Extract new/modified modules from diff
19+
if [ "$LANGUAGE" = "python" ]; then
20+
modules=$(echo "$diff_content" | grep -oE 'src/sap_cloud_sdk/[a-z_]+/' 2>/dev/null | sed 's|src/sap_cloud_sdk/||; s|/$||' | grep -v '^core$' | sort -u || true)
21+
else
22+
modules=$(echo "$diff_content" | grep -oE 'src/main/java/com/sap/cloud/sdk/[a-z_]+/' 2>/dev/null | sed 's|src/main/java/com/sap/cloud/sdk/||; s|/$||' | grep -v '^core$' | sort -u || true)
23+
fi
24+
25+
# resolve alias mapping (python name → java name and vice versa)
26+
resolve_sibling_name() {
27+
local mod="$1" dir="$LANGUAGE"
28+
local aliases="$CONFIG_DIR/module-aliases.yaml"
29+
if [ ! -f "$aliases" ]; then echo "$mod"; return; fi
30+
# Simple parser: "- python: X\n java: Y" pairs
31+
if [ "$dir" = "python" ]; then
32+
# look for "python: $mod" then get "java: <name>"
33+
awk -v mod="$mod" '
34+
/python:/ { p_name=$2 }
35+
/java:/ { j_name=$2; if (p_name == mod) { print j_name; exit } }
36+
' "$aliases" | head -1
37+
return
38+
else
39+
awk -v mod="$mod" '
40+
/python:/ { p_name=$2 }
41+
/java:/ { j_name=$2; if (j_name == mod) { print p_name; exit } }
42+
' "$aliases" | head -1
43+
return
44+
fi
45+
}
46+
47+
while IFS= read -r mod; do
48+
[ -z "$mod" ] && continue
49+
50+
# Path for THIS repo's feature file
51+
if [ "$LANGUAGE" = "python" ]; then
52+
feature_path="$REPO_ROOT/tests/$mod/integration/$mod.feature"
53+
mod_dir="$REPO_ROOT/src/sap_cloud_sdk/$mod"
54+
else
55+
feature_path="$REPO_ROOT/src/test/resources/com/sap/applicationfoundation/$mod/integration/$mod.feature"
56+
mod_dir="$REPO_ROOT/src/main/java/com/sap/cloud/sdk/$mod"
57+
fi
58+
59+
# BDD-01: feature file exists (only fire if module has any source files)
60+
if [ ! -f "$feature_path" ] && [ -d "$mod_dir" ]; then
61+
# check if it's a "new" module (created in this PR)
62+
if echo "$diff_content" | grep -qE "new file mode.*($mod/)"; then
63+
emit_finding "BDD-01" "BLOCK" "tests/$mod/integration/$mod.feature" 1 \
64+
"New module '$mod' has no BDD feature file" \
65+
"Create $feature_path with cross-language-consistent scenarios" >> "$findings"
66+
fi
67+
fi
68+
69+
# BDD-02: sibling repo parity
70+
sibling_name=$(resolve_sibling_name "$mod")
71+
[ -z "$sibling_name" ] && sibling_name="$mod"
72+
73+
if [ -n "$SDK_SIBLING_PATH" ] && [ -d "$SDK_SIBLING_PATH" ]; then
74+
if [ "$LANGUAGE" = "python" ]; then
75+
# Python repo — sibling is Java
76+
sibling_feature="$SDK_SIBLING_PATH/src/test/resources/com/sap/applicationfoundation/$sibling_name/integration/$sibling_name.feature"
77+
sibling_module_dir="$SDK_SIBLING_PATH/src/main/java/com/sap/cloud/sdk/$sibling_name"
78+
else
79+
sibling_feature="$SDK_SIBLING_PATH/tests/$sibling_name/integration/$sibling_name.feature"
80+
sibling_module_dir="$SDK_SIBLING_PATH/src/sap_cloud_sdk/$sibling_name"
81+
fi
82+
# If the sibling module dir exists, and sibling has no feature but we do (or vice versa)
83+
if [ -d "$sibling_module_dir" ] && [ ! -f "$sibling_feature" ] && [ -f "$feature_path" ]; then
84+
emit_finding "BDD-02" "FLAG" "$feature_path" 1 \
85+
"Sibling SDK ($sibling_name) has module but no BDD feature — parity broken" "" >> "$findings"
86+
fi
87+
if [ -d "$sibling_module_dir" ] && [ -f "$sibling_feature" ] && [ ! -f "$feature_path" ] && [ -d "$mod_dir" ]; then
88+
emit_finding "BDD-02" "BLOCK" "tests/.../$mod.feature" 1 \
89+
"Module '$mod' exists in sibling SDK ($sibling_name) with BDD feature — this repo must have equivalent" "" >> "$findings"
90+
fi
91+
fi
92+
done <<< "$modules"
93+
94+
status=$(status_from_findings < "$findings")
95+
emit_report "bdd" "$LANGUAGE" "$status" "$STARTED" < "$findings"

.claude/scripts/check-commits.sh

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env bash
2+
# check-commits.sh — Conventional Commits enforcement.
3+
set -euo pipefail
4+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
source "$SCRIPT_DIR/lib/json-emit.sh"
6+
7+
LANGUAGE="${LANGUAGE:-python}"
8+
BASE_SHA="${BASE_SHA:-HEAD~10}"
9+
HEAD_SHA="${HEAD_SHA:-HEAD}"
10+
11+
STARTED=$(now_iso)
12+
findings=$(mktemp); trap 'rm -f "$findings"' EXIT
13+
14+
# List commit subjects
15+
commits=$(git log "${BASE_SHA}..${HEAD_SHA}" --format='%H %s' 2>/dev/null || echo "")
16+
prefix_regex='^(feat|fix|refactor|chore|docs|test|ci|build|perf|style|revert)(\([a-z0-9_/-]+\))?!?:[[:space:]]+.+'
17+
18+
echo "$commits" | while IFS= read -r line; do
19+
[ -z "$line" ] && continue
20+
sha=$(echo "$line" | cut -d' ' -f1)
21+
subject=$(echo "$line" | cut -d' ' -f2-)
22+
# Skip merge commits (identified by 2+ parents, robust regardless of message shape)
23+
parents=$(git rev-list --parents -n 1 "$sha" 2>/dev/null | awk '{print NF-1}')
24+
if [ "${parents:-0}" -gt 1 ]; then continue; fi
25+
# Also skip legacy "Merge branch/pull/etc" defaults
26+
if echo "$subject" | grep -qiE '^Merge '; then continue; fi
27+
if ! echo "$subject" | grep -qE "$prefix_regex"; then
28+
emit_finding "COM-01" "BLOCK" "COMMIT:$sha" 1 \
29+
"Commit '$subject' does not follow Conventional Commits" \
30+
"Prefix with feat:/fix:/chore:/docs:/test:/refactor:/ci:/build:/perf:/style:/revert:" >> "$findings"
31+
fi
32+
done
33+
34+
status=$(status_from_findings < "$findings")
35+
emit_report "commits" "$LANGUAGE" "$status" "$STARTED" < "$findings"

.claude/scripts/check-docs.sh

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#!/usr/bin/env bash
2+
# check-docs.sh — documentation completeness including BTP deps and regional availability.
3+
set -euo pipefail
4+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
source "$SCRIPT_DIR/lib/json-emit.sh"
6+
7+
LANGUAGE="${LANGUAGE:-python}"
8+
REPO_ROOT="${REPO_ROOT:-.}"
9+
DIFF_FILE="${DIFF_FILE:-/dev/stdin}"
10+
11+
STARTED=$(now_iso)
12+
findings=$(mktemp); trap 'rm -f "$findings"' EXIT
13+
14+
diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "")
15+
16+
# Detect new module directories (module has new files at top-level)
17+
if [ "$LANGUAGE" = "python" ]; then
18+
new_modules=$(echo "$diff_content" | grep -oE '^\+\+\+ b/src/sap_cloud_sdk/[a-z_]+/[^/]+\.py' | sed 's|^+++ b/src/sap_cloud_sdk/||; s|/[^/]*\.py$||' | sort -u)
19+
else
20+
new_modules=$(echo "$diff_content" | grep -oE '^\+\+\+ b/src/main/java/com/sap/cloud/sdk/[a-z_]+/[^/]+\.java' | sed 's|^+++ b/src/main/java/com/sap/cloud/sdk/||; s|/[^/]*\.java$||' | sort -u)
21+
fi
22+
23+
while IFS= read -r mod; do
24+
[ -z "$mod" ] && continue
25+
[ "$mod" = "core" ] && continue
26+
27+
if [ "$LANGUAGE" = "python" ]; then
28+
user_guide="$REPO_ROOT/src/sap_cloud_sdk/$mod/user-guide.md"
29+
mod_dir="$REPO_ROOT/src/sap_cloud_sdk/$mod"
30+
else
31+
user_guide="$REPO_ROOT/src/main/java/com/sap/cloud/sdk/$mod/user-guide.md"
32+
mod_dir="$REPO_ROOT/src/main/java/com/sap/cloud/sdk/$mod"
33+
fi
34+
35+
# DC-01: user-guide.md exists
36+
if [ ! -f "$user_guide" ]; then
37+
# Only fire if module dir exists (module is new-ish)
38+
if [ -d "$mod_dir" ]; then
39+
emit_finding "DC-01" "BLOCK" "src/.../$mod/user-guide.md" 1 \
40+
"Module '$mod' missing user-guide.md" \
41+
"Create $user_guide with sections: ## Installation, ## Quick Start, ## Configuration" >> "$findings"
42+
fi
43+
continue
44+
fi
45+
46+
# DC-02: required sections
47+
guide_content=$(cat "$user_guide" 2>/dev/null || echo "")
48+
if ! echo "$guide_content" | grep -qE '^##[[:space:]]+(Installation|Import)'; then
49+
emit_finding "DC-02" "FLAG" "$user_guide" 1 "user-guide.md missing ## Installation section" "" >> "$findings"
50+
fi
51+
if ! echo "$guide_content" | grep -qE '^##[[:space:]]+Quick Start'; then
52+
emit_finding "DC-02" "BLOCK" "$user_guide" 1 "user-guide.md missing ## Quick Start section" "" >> "$findings"
53+
fi
54+
55+
# DC-11..DC-14 (BTP deps + regional)
56+
# Detect module imports/usages
57+
if [ "$LANGUAGE" = "python" ]; then
58+
has_dest=$(grep -rq "from sap_cloud_sdk\.destination" "$mod_dir" 2>/dev/null && echo yes || echo no)
59+
has_frag=$(grep -rq "FragmentClient\|Fragment[[:space:]]*[,)]" "$mod_dir" 2>/dev/null && echo yes || echo no)
60+
has_cert=$(grep -rq "CertificateClient\|Certificate[[:space:]]*[,)]" "$mod_dir" 2>/dev/null && echo yes || echo no)
61+
has_region=$(grep -rqE "REGION|region_id|SupportedRegion|available_regions" "$mod_dir" 2>/dev/null && echo yes || echo no)
62+
else
63+
has_dest=$(grep -rq "com\.sap\.cloud\.sdk\.destination" "$mod_dir" 2>/dev/null && echo yes || echo no)
64+
has_frag=$(grep -rq "FragmentClient" "$mod_dir" 2>/dev/null && echo yes || echo no)
65+
has_cert=$(grep -rq "CertificateClient" "$mod_dir" 2>/dev/null && echo yes || echo no)
66+
has_region=$(grep -rqE "Region\b|regionId|SupportedRegion" "$mod_dir" 2>/dev/null && echo yes || echo no)
67+
fi
68+
69+
# DC-11: destination dep must be documented
70+
if [ "$has_dest" = "yes" ]; then
71+
if ! echo "$guide_content" | grep -qEi 'destination service|## Dependencies|## Prerequisites'; then
72+
emit_finding "DC-11" "BLOCK" "$user_guide" 1 \
73+
"Module imports destination service — must document in ## Dependencies section" \
74+
"Add: ## Dependencies\\n- SAP BTP Destination Service instance" >> "$findings"
75+
fi
76+
fi
77+
# DC-12: fragments
78+
if [ "$has_frag" = "yes" ]; then
79+
if ! echo "$guide_content" | grep -qEi 'fragments'; then
80+
emit_finding "DC-12" "BLOCK" "$user_guide" 1 \
81+
"Module uses Fragments — must document Fragments prerequisite" "" >> "$findings"
82+
fi
83+
fi
84+
# DC-13: certificates
85+
if [ "$has_cert" = "yes" ]; then
86+
if ! echo "$guide_content" | grep -qEi 'certificate'; then
87+
emit_finding "DC-13" "BLOCK" "$user_guide" 1 \
88+
"Module uses Certificates — must document Certificate prerequisite" "" >> "$findings"
89+
fi
90+
fi
91+
# DC-14: regional
92+
if [ "$has_region" = "yes" ]; then
93+
if ! echo "$guide_content" | grep -qEi 'Regional Availability|## Limitations|available in|supported region'; then
94+
emit_finding "DC-14" "BLOCK" "$user_guide" 1 \
95+
"Module has region-specific constants — must document ## Regional Availability" "" >> "$findings"
96+
fi
97+
fi
98+
99+
done <<< "$new_modules"
100+
101+
status=$(status_from_findings < "$findings")
102+
emit_report "docs" "$LANGUAGE" "$status" "$STARTED" < "$findings"

.claude/scripts/check-patterns.sh

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/usr/bin/env bash
2+
# check-patterns.sh — idiomatic patterns (factory, exceptions, py.typed).
3+
# Uses module_shape predicate to skip non-client modules.
4+
set -euo pipefail
5+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
source "$SCRIPT_DIR/lib/json-emit.sh"
7+
source "$SCRIPT_DIR/lib/predicates.sh"
8+
9+
LANGUAGE="${LANGUAGE:-python}"
10+
REPO_ROOT="${REPO_ROOT:-.}"
11+
DIFF_FILE="${DIFF_FILE:-/dev/stdin}"
12+
13+
STARTED=$(now_iso)
14+
findings=$(mktemp); trap 'rm -f "$findings"' EXIT
15+
16+
diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "")
17+
18+
# Detect module dirs touched
19+
if [ "$LANGUAGE" = "python" ]; then
20+
modules=$(echo "$diff_content" | grep -oE 'src/sap_cloud_sdk/[a-z_]+/' | sed 's|src/sap_cloud_sdk/||; s|/$||' | grep -v '^core$' | sort -u)
21+
else
22+
modules=$(echo "$diff_content" | grep -oE 'src/main/java/com/sap/cloud/sdk/[a-z_]+/' | sed 's|src/main/java/com/sap/cloud/sdk/||; s|/$||' | grep -v '^core$' | sort -u)
23+
fi
24+
25+
while IFS= read -r mod; do
26+
[ -z "$mod" ] && continue
27+
28+
if [ "$LANGUAGE" = "python" ]; then
29+
mod_dir="$REPO_ROOT/src/sap_cloud_sdk/$mod"
30+
else
31+
mod_dir="$REPO_ROOT/src/main/java/com/sap/cloud/sdk/$mod"
32+
fi
33+
34+
[ -d "$mod_dir" ] || continue
35+
36+
# PY-PT-01 / JV-PT-01: factory exists — only fire on client modules
37+
shape=$(module_shape "$mod_dir")
38+
if [ "$shape" = "client" ]; then
39+
if [ "$LANGUAGE" = "python" ]; then
40+
if ! grep -rqE '^def create_[a-z_]*client\(' "$mod_dir" 2>/dev/null; then
41+
emit_finding "PY-PT-01" "BLOCK" "src/sap_cloud_sdk/$mod/" 1 \
42+
"Client module '$mod' missing create_client() factory function" "" >> "$findings"
43+
fi
44+
else
45+
if ! grep -rqE 'class [A-Z][A-Za-z]*ClientFactory' "$mod_dir" 2>/dev/null; then
46+
emit_finding "JV-PT-01" "BLOCK" "src/main/java/com/sap/cloud/sdk/$mod/" 1 \
47+
"Client module '$mod' missing ClientFactory class" "" >> "$findings"
48+
fi
49+
fi
50+
fi
51+
52+
# PY-PT-03: py.typed marker
53+
if [ "$LANGUAGE" = "python" ]; then
54+
if [ ! -f "$mod_dir/py.typed" ] && [ ! -f "$REPO_ROOT/src/sap_cloud_sdk/py.typed" ]; then
55+
emit_finding "PY-PT-03" "FLAG" "src/sap_cloud_sdk/$mod/py.typed" 1 \
56+
"Module missing py.typed marker (PEP 561)" "" >> "$findings"
57+
fi
58+
fi
59+
60+
# PY-PT-04 / JV-PT-05: module-specific exceptions
61+
if [ "$LANGUAGE" = "python" ]; then
62+
if [ ! -f "$mod_dir/exceptions.py" ]; then
63+
emit_finding "PY-PT-04" "FLAG" "src/sap_cloud_sdk/$mod/exceptions.py" 1 \
64+
"Module lacks exceptions.py — module-specific exception hierarchy recommended" "" >> "$findings"
65+
fi
66+
else
67+
if [ ! -d "$mod_dir/exceptions" ]; then
68+
emit_finding "JV-PT-05" "FLAG" "src/main/java/com/sap/cloud/sdk/$mod/exceptions/" 1 \
69+
"Module lacks exceptions/ package" "" >> "$findings"
70+
fi
71+
fi
72+
73+
done <<< "$modules"
74+
75+
# PY-PT-08 via AST on changed Python files
76+
if [ "$LANGUAGE" = "python" ]; then
77+
changed_py=$(echo "$diff_content" | grep -oE '^\+\+\+ b/src/sap_cloud_sdk/.*\.py' | sed 's|^+++ b/||' | grep -v '__pycache__' | sort -u)
78+
if [ -n "$changed_py" ]; then
79+
# shellcheck disable=SC2086
80+
python3 "$SCRIPT_DIR/lib/ast_python_checks.py" pt-08 $changed_py 2>/dev/null >> "$findings" || true
81+
fi
82+
fi
83+
84+
status=$(status_from_findings < "$findings")
85+
emit_report "patterns" "$LANGUAGE" "$status" "$STARTED" < "$findings"

0 commit comments

Comments
 (0)