Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions .claude/scripts/check-bdd.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#!/usr/bin/env bash
# check-bdd.sh — feature file existence + cross-language parity via alias map.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/json-emit.sh"

LANGUAGE="${LANGUAGE:-python}"
REPO_ROOT="${REPO_ROOT:-.}"
DIFF_FILE="${DIFF_FILE:-/dev/stdin}"
SDK_SIBLING_PATH="${SDK_SIBLING_PATH:-}"
CONFIG_DIR="${CONFIG_DIR:-$REPO_ROOT/.claude/config}"

STARTED=$(now_iso)
findings=$(mktemp); trap 'rm -f "$findings"' EXIT

diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "")

# Extract new/modified modules from diff
if [ "$LANGUAGE" = "python" ]; then
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)
else
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)
fi

# resolve alias mapping (python name → java name and vice versa)
# The YAML is a simple list of pairs:
# aliases:
# - python: dms
# java: documentmanagement
# awk on `$NF` (last field) extracts the value cleanly even when the key
# column varies. We only pair a python: line with the very next java: line.
resolve_sibling_name() {
local mod="$1" dir="$LANGUAGE"
local aliases="$CONFIG_DIR/module-aliases.yaml"
if [ ! -f "$aliases" ]; then echo "$mod"; return; fi
if [ "$dir" = "python" ]; then
awk -v mod="$mod" '
/^[[:space:]]*-?[[:space:]]*python:[[:space:]]*/ {
sub(/^[^:]*:[[:space:]]*/, ""); p_name=$0; next
}
/^[[:space:]]*java:[[:space:]]*/ {
sub(/^[^:]*:[[:space:]]*/, ""); j_name=$0
if (p_name == mod) { print j_name; exit }
p_name=""
}
' "$aliases"
return
else
awk -v mod="$mod" '
/^[[:space:]]*-?[[:space:]]*python:[[:space:]]*/ {
sub(/^[^:]*:[[:space:]]*/, ""); p_name=$0; next
}
/^[[:space:]]*java:[[:space:]]*/ {
sub(/^[^:]*:[[:space:]]*/, ""); j_name=$0
if (j_name == mod) { print p_name; exit }
p_name=""
}
' "$aliases"
return
fi
}

while IFS= read -r mod; do
[ -z "$mod" ] && continue

# Path for THIS repo's feature file
if [ "$LANGUAGE" = "python" ]; then
feature_path="$REPO_ROOT/tests/$mod/integration/$mod.feature"
mod_dir="$REPO_ROOT/src/sap_cloud_sdk/$mod"
else
feature_path="$REPO_ROOT/src/test/resources/com/sap/applicationfoundation/$mod/integration/$mod.feature"
mod_dir="$REPO_ROOT/src/main/java/com/sap/cloud/sdk/$mod"
fi

# BDD-01: feature file exists (only fire if module has any source files)
if [ ! -f "$feature_path" ] && [ -d "$mod_dir" ]; then
# Detect if this PR creates any new file INSIDE the module.
# `new file mode` is on its own line before the `+++ b/<path>` header,
# so we scan block-by-block to link them.
is_new_module=$(echo "$diff_content" | awk -v mod="$mod" -v lang="$LANGUAGE" '
BEGIN {
if (lang == "python") pat = "src/sap_cloud_sdk/" mod "/"
else pat = "src/main/java/com/sap/cloud/sdk/" mod "/"
}
/^diff --git/ { is_new = 0; next }
/^new file mode/ { is_new = 1; next }
/^\+\+\+ b\// {
if (is_new && index($0, pat) > 0) { print "true"; exit }
}
')
if [ "$is_new_module" = "true" ]; then
emit_finding "BDD-01" "BLOCK" "tests/$mod/integration/$mod.feature" 1 \
"New module '$mod' has no BDD feature file" \
"Create $feature_path with cross-language-consistent scenarios" >> "$findings"
fi
fi

# BDD-02: sibling repo parity
sibling_name=$(resolve_sibling_name "$mod")
[ -z "$sibling_name" ] && sibling_name="$mod"

if [ -n "$SDK_SIBLING_PATH" ] && [ -d "$SDK_SIBLING_PATH" ]; then
if [ "$LANGUAGE" = "python" ]; then
# Python repo — sibling is Java
sibling_feature="$SDK_SIBLING_PATH/src/test/resources/com/sap/applicationfoundation/$sibling_name/integration/$sibling_name.feature"
sibling_module_dir="$SDK_SIBLING_PATH/src/main/java/com/sap/cloud/sdk/$sibling_name"
else
sibling_feature="$SDK_SIBLING_PATH/tests/$sibling_name/integration/$sibling_name.feature"
sibling_module_dir="$SDK_SIBLING_PATH/src/sap_cloud_sdk/$sibling_name"
fi
# If the sibling module dir exists, and sibling has no feature but we do (or vice versa)
if [ -d "$sibling_module_dir" ] && [ ! -f "$sibling_feature" ] && [ -f "$feature_path" ]; then
emit_finding "BDD-02" "FLAG" "$feature_path" 1 \
"Sibling SDK ($sibling_name) has module but no BDD feature — parity broken" "" >> "$findings"
fi
if [ -d "$sibling_module_dir" ] && [ -f "$sibling_feature" ] && [ ! -f "$feature_path" ] && [ -d "$mod_dir" ]; then
emit_finding "BDD-02" "BLOCK" "tests/.../$mod.feature" 1 \
"Module '$mod' exists in sibling SDK ($sibling_name) with BDD feature — this repo must have equivalent" "" >> "$findings"
fi
fi
done <<< "$modules"

status=$(status_from_findings < "$findings")
emit_report "bdd" "$LANGUAGE" "$status" "$STARTED" < "$findings"
47 changes: 47 additions & 0 deletions .claude/scripts/check-commits.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# check-commits.sh — Conventional Commits enforcement.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/json-emit.sh"

LANGUAGE="${LANGUAGE:-python}"
# Resolve BASE_SHA from GITHUB_BASE_REF merge-base when not explicitly set —
# HEAD~10 is a poor fallback (only reachable when the PR has ≥10 commits).
if [ -z "${BASE_SHA:-}" ]; then
base_ref="${GITHUB_BASE_REF:-main}"
# Try origin/<ref>, then <ref>, then finally HEAD~10 as last resort
if git rev-parse --verify "origin/${base_ref}" >/dev/null 2>&1; then
BASE_SHA=$(git merge-base HEAD "origin/${base_ref}" 2>/dev/null || echo "HEAD~10")
elif git rev-parse --verify "$base_ref" >/dev/null 2>&1; then
BASE_SHA=$(git merge-base HEAD "$base_ref" 2>/dev/null || echo "HEAD~10")
else
BASE_SHA="HEAD~10"
fi
fi
HEAD_SHA="${HEAD_SHA:-HEAD}"

STARTED=$(now_iso)
findings=$(mktemp); trap 'rm -f "$findings"' EXIT

# List commit subjects
commits=$(git log "${BASE_SHA}..${HEAD_SHA}" --format='%H %s' 2>/dev/null || echo "")
prefix_regex='^(feat|fix|refactor|chore|docs|test|ci|build|perf|style|revert)(\([a-z0-9_/-]+\))?!?:[[:space:]]+.+'

echo "$commits" | while IFS= read -r line; do
[ -z "$line" ] && continue
sha=$(echo "$line" | cut -d' ' -f1)
subject=$(echo "$line" | cut -d' ' -f2-)
# Skip merge commits (identified by 2+ parents, robust regardless of message shape)
parents=$(git rev-list --parents -n 1 "$sha" 2>/dev/null | awk '{print NF-1}')
if [ "${parents:-0}" -gt 1 ]; then continue; fi
# Also skip legacy "Merge branch/pull/etc" defaults
if echo "$subject" | grep -qiE '^Merge '; then continue; fi
if ! echo "$subject" | grep -qE "$prefix_regex"; then
emit_finding "COM-01" "BLOCK" "COMMIT:$sha" 1 \
"Commit '$subject' does not follow Conventional Commits" \
"Prefix with feat:/fix:/chore:/docs:/test:/refactor:/ci:/build:/perf:/style:/revert:" >> "$findings"
fi
done

status=$(status_from_findings < "$findings")
emit_report "commits" "$LANGUAGE" "$status" "$STARTED" < "$findings"
109 changes: 109 additions & 0 deletions .claude/scripts/check-docs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#!/usr/bin/env bash
# check-docs.sh — documentation completeness including BTP deps and regional availability.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/json-emit.sh"

LANGUAGE="${LANGUAGE:-python}"
REPO_ROOT="${REPO_ROOT:-.}"
DIFF_FILE="${DIFF_FILE:-/dev/stdin}"

STARTED=$(now_iso)
findings=$(mktemp); trap 'rm -f "$findings"' EXIT

diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "")

# Detect new module directories (module has new files at top-level)
if [ "$LANGUAGE" = "python" ]; then
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)
else
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)
fi

while IFS= read -r mod; do
[ -z "$mod" ] && continue
[ "$mod" = "core" ] && continue

if [ "$LANGUAGE" = "python" ]; then
user_guide="$REPO_ROOT/src/sap_cloud_sdk/$mod/user-guide.md"
mod_dir="$REPO_ROOT/src/sap_cloud_sdk/$mod"
else
user_guide="$REPO_ROOT/src/main/java/com/sap/cloud/sdk/$mod/user-guide.md"
mod_dir="$REPO_ROOT/src/main/java/com/sap/cloud/sdk/$mod"
fi

# DC-01: user-guide.md exists
if [ ! -f "$user_guide" ]; then
# Only fire if module dir exists (module is new-ish)
if [ -d "$mod_dir" ]; then
emit_finding "DC-01" "BLOCK" "src/.../$mod/user-guide.md" 1 \
"Module '$mod' missing user-guide.md" \
"Create $user_guide with sections: ## Installation, ## Quick Start, ## Configuration" >> "$findings"
fi
continue
fi

# DC-02: required sections
guide_content=$(cat "$user_guide" 2>/dev/null || echo "")
if ! echo "$guide_content" | grep -qE '^##[[:space:]]+(Installation|Import)'; then
emit_finding "DC-02" "FLAG" "$user_guide" 1 "user-guide.md missing ## Installation section" "" >> "$findings"
fi
if ! echo "$guide_content" | grep -qE '^##[[:space:]]+Quick Start'; then
emit_finding "DC-02" "BLOCK" "$user_guide" 1 "user-guide.md missing ## Quick Start section" "" >> "$findings"
fi

# DC-11..DC-14 (BTP deps + regional)
# Detect module imports/usages. Use word boundaries and prefer explicit
# SAP-namespaced references to avoid false positives on DocumentFragment,
# AWSRegion, region_id inside docstrings, etc.
if [ "$LANGUAGE" = "python" ]; then
has_dest=$(grep -rq "from sap_cloud_sdk\.destination" "$mod_dir" 2>/dev/null && echo yes || echo no)
# Fragment/Certificate: require the SDK-provided client class specifically,
# or an import from the SDK's fragments/certificates subpackage.
has_frag=$(grep -rqE "\bFragmentClient\b|from[[:space:]]+sap_cloud_sdk\.fragments" "$mod_dir" 2>/dev/null && echo yes || echo no)
has_cert=$(grep -rqE "\bCertificateClient\b|from[[:space:]]+sap_cloud_sdk\.certificates" "$mod_dir" 2>/dev/null && echo yes || echo no)
# Region: constant naming or SDK-provided helper only — plain 'region_id'
# in docstrings should not fire.
has_region=$(grep -rqE "\bSUPPORTED_REGIONS\b|\bSupportedRegion\b|\bavailable_regions\b|from[[:space:]]+sap_cloud_sdk\.regions" "$mod_dir" 2>/dev/null && echo yes || echo no)
else
has_dest=$(grep -rq "com\.sap\.cloud\.sdk\.destination" "$mod_dir" 2>/dev/null && echo yes || echo no)
has_frag=$(grep -rqE "\bFragmentClient\b|com\.sap\.cloud\.sdk\.fragments" "$mod_dir" 2>/dev/null && echo yes || echo no)
has_cert=$(grep -rqE "\bCertificateClient\b|com\.sap\.cloud\.sdk\.certificates" "$mod_dir" 2>/dev/null && echo yes || echo no)
# Java word-boundary: require SAP SDK Region type; avoid AWSRegion etc.
has_region=$(grep -rqE "\bSupportedRegion\b|com\.sap\.cloud\.sdk\.regions|\bREGIONAL_AVAILABILITY\b" "$mod_dir" 2>/dev/null && echo yes || echo no)
fi

# DC-11: destination dep must be documented
if [ "$has_dest" = "yes" ]; then
if ! echo "$guide_content" | grep -qEi 'destination service|## Dependencies|## Prerequisites'; then
emit_finding "DC-11" "BLOCK" "$user_guide" 1 \
"Module imports destination service — must document in ## Dependencies section" \
"Add: ## Dependencies\\n- SAP BTP Destination Service instance" >> "$findings"
fi
fi
# DC-12: fragments
if [ "$has_frag" = "yes" ]; then
if ! echo "$guide_content" | grep -qEi 'fragments'; then
emit_finding "DC-12" "BLOCK" "$user_guide" 1 \
"Module uses Fragments — must document Fragments prerequisite" "" >> "$findings"
fi
fi
# DC-13: certificates
if [ "$has_cert" = "yes" ]; then
if ! echo "$guide_content" | grep -qEi 'certificate'; then
emit_finding "DC-13" "BLOCK" "$user_guide" 1 \
"Module uses Certificates — must document Certificate prerequisite" "" >> "$findings"
fi
fi
# DC-14: regional
if [ "$has_region" = "yes" ]; then
if ! echo "$guide_content" | grep -qEi 'Regional Availability|## Limitations|available in|supported region'; then
emit_finding "DC-14" "BLOCK" "$user_guide" 1 \
"Module has region-specific constants — must document ## Regional Availability" "" >> "$findings"
fi
fi

done <<< "$new_modules"

status=$(status_from_findings < "$findings")
emit_report "docs" "$LANGUAGE" "$status" "$STARTED" < "$findings"
85 changes: 85 additions & 0 deletions .claude/scripts/check-patterns.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# check-patterns.sh — idiomatic patterns (factory, exceptions, py.typed).
# Uses module_shape predicate to skip non-client modules.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/json-emit.sh"
source "$SCRIPT_DIR/lib/predicates.sh"

LANGUAGE="${LANGUAGE:-python}"
REPO_ROOT="${REPO_ROOT:-.}"
DIFF_FILE="${DIFF_FILE:-/dev/stdin}"

STARTED=$(now_iso)
findings=$(mktemp); trap 'rm -f "$findings"' EXIT

diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "")

# Detect module dirs touched
if [ "$LANGUAGE" = "python" ]; then
modules=$(echo "$diff_content" | grep -oE 'src/sap_cloud_sdk/[a-z_]+/' | sed 's|src/sap_cloud_sdk/||; s|/$||' | grep -v '^core$' | sort -u)
else
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)
fi

while IFS= read -r mod; do
[ -z "$mod" ] && continue

if [ "$LANGUAGE" = "python" ]; then
mod_dir="$REPO_ROOT/src/sap_cloud_sdk/$mod"
else
mod_dir="$REPO_ROOT/src/main/java/com/sap/cloud/sdk/$mod"
fi

[ -d "$mod_dir" ] || continue

# PY-PT-01 / JV-PT-01: factory exists — only fire on client modules
shape=$(module_shape "$mod_dir")
if [ "$shape" = "client" ]; then
if [ "$LANGUAGE" = "python" ]; then
if ! grep -rqE '^def create_[a-z_]*client\(' "$mod_dir" 2>/dev/null; then
emit_finding "PY-PT-01" "BLOCK" "src/sap_cloud_sdk/$mod/" 1 \
"Client module '$mod' missing create_client() factory function" "" >> "$findings"
fi
else
if ! grep -rqE 'class [A-Z][A-Za-z]*ClientFactory' "$mod_dir" 2>/dev/null; then
emit_finding "JV-PT-01" "BLOCK" "src/main/java/com/sap/cloud/sdk/$mod/" 1 \
"Client module '$mod' missing ClientFactory class" "" >> "$findings"
fi
fi
fi

# PY-PT-03: py.typed marker
if [ "$LANGUAGE" = "python" ]; then
if [ ! -f "$mod_dir/py.typed" ] && [ ! -f "$REPO_ROOT/src/sap_cloud_sdk/py.typed" ]; then
emit_finding "PY-PT-03" "FLAG" "src/sap_cloud_sdk/$mod/py.typed" 1 \
"Module missing py.typed marker (PEP 561)" "" >> "$findings"
fi
fi

# PY-PT-04 / JV-PT-05: module-specific exceptions
if [ "$LANGUAGE" = "python" ]; then
if [ ! -f "$mod_dir/exceptions.py" ]; then
emit_finding "PY-PT-04" "FLAG" "src/sap_cloud_sdk/$mod/exceptions.py" 1 \
"Module lacks exceptions.py — module-specific exception hierarchy recommended" "" >> "$findings"
fi
else
if [ ! -d "$mod_dir/exceptions" ]; then
emit_finding "JV-PT-05" "FLAG" "src/main/java/com/sap/cloud/sdk/$mod/exceptions/" 1 \
"Module lacks exceptions/ package" "" >> "$findings"
fi
fi

done <<< "$modules"

# PY-PT-08 via AST on changed Python files
if [ "$LANGUAGE" = "python" ]; then
changed_py=$(echo "$diff_content" | grep -oE '^\+\+\+ b/src/sap_cloud_sdk/.*\.py' | sed 's|^+++ b/||' | grep -v '__pycache__' | sort -u)
if [ -n "$changed_py" ]; then
# shellcheck disable=SC2086
python3 "$SCRIPT_DIR/lib/ast_python_checks.py" pt-08 $changed_py 2>/dev/null >> "$findings" || true
fi
fi

status=$(status_from_findings < "$findings")
emit_report "patterns" "$LANGUAGE" "$status" "$STARTED" < "$findings"
Loading
Loading