Skip to content
Closed
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
19 changes: 19 additions & 0 deletions .claude/commands/review-new-module.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# /review-new-module — orchestrator slash command
Run the SDK Module Review skill against a PR.

## Usage
- `/review-new-module <PR_NUMBER>` — full review, posts findings
- `/review-new-module <PR_NUMBER> --dry-run` — analysis only, no posting

## What it does
1. Detects language (Python vs Java) via `pyproject.toml` / `pom.xml`
2. Fetches PR diff + body
3. Runs 20 deterministic checks in parallel (secrets, license, disclosure, hardcode, telemetry, docs, bdd, patterns, versioning, commits, errors-logging, testing-depth, http-hygiene, concurrency, deps-supply, deletion-hygiene, constants, binding-shape, quality-gate-parity, pr-size)
4. Applies baseline exemptions + scope predicates + tier gating
5. Detects breaking changes via AST diff
6. Posts 4 signals: inline comments, summary comment, check-run, label

## Implementation
Run: `bash .claude/scripts/orchestrate.sh <PR_NUMBER>`

The orchestrator is 100% deterministic — no LLM calls in CI. When run inside Claude Code, the LLM can enrich the summary comment before posting (this happens naturally in the session).
25 changes: 25 additions & 0 deletions .claude/config/baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"created_at": "2026-07-15T00:00:00Z",
"at_commit": "TBD",
"schema_version": 1,
"exemptions": {
"PY-LIC-01": {
"reason": "Repo uses REUSE.toml aggregate — pre-existing files exempt",
"detector": "REUSE.toml exists with path='**' aggregate annotation",
"scope": "repo",
"files_exempted_glob": "src/**/*.py"
},
"PY-LIC-02": {
"reason": "Same as PY-LIC-01",
"scope": "repo"
},
"JV-LIC-01": {
"reason": "cloud-sdk-java has no REUSE.toml AND no per-file SPDX baseline — rule kept SHADOW pending team decision on Wave-4 baseline PR",
"scope": "repo"
},
"JV-LIC-02": {
"reason": "Same as JV-LIC-01",
"scope": "repo"
}
}
}
5 changes: 5 additions & 0 deletions .claude/config/module-aliases.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Module name aliases across languages.
# Used by check-bdd.sh to resolve cross-repo feature file location.
aliases:
- python: dms
java: documentmanagement
131 changes: 131 additions & 0 deletions .claude/config/rules.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
version: 2

# =====================================================
# rules.yaml — SDK Module Review rule catalog
# All rules default to their tier here; may be overridden per-repo.
# BLOCK_LOCKED rules cannot be softened or suppressed.
# =====================================================

baseline:
file: .claude/baseline.json
regenerate_with: orchestrate.sh --baseline

tiers:
SHADOW: { posts: false, blocks: false }
FLAG: { posts: true, blocks: false }
BLOCK: { posts: true, blocks: true }
BLOCK_LOCKED: { posts: true, blocks: true, suppressible: false, downgradable: false }

rules:
# -------- BREAKING FAMILY (all locked) --------
BREAKING-01: { tier: BLOCK_LOCKED }
BREAKING-02: { tier: BLOCK_LOCKED }
BREAKING-03: { tier: FLAG }
BREAKING-04: { tier: FLAG }

# -------- SECRETS (all locked) --------
SEC-01: { tier: BLOCK_LOCKED }
SEC-02: { tier: BLOCK_LOCKED }
SEC-03: { tier: BLOCK_LOCKED }
SEC-04: { tier: BLOCK_LOCKED }
SEC-05: { tier: BLOCK_LOCKED }
SEC-06: { tier: BLOCK_LOCKED }
SEC-07: { tier: BLOCK_LOCKED }
SEC-08: { tier: BLOCK_LOCKED }
SEC-10: { tier: BLOCK_LOCKED }

# -------- LICENSE --------
LIC-01: { tier: BLOCK, predicates: { not: reuse_toml_aggregate_present, diff_scope: file_in_added_set } }
LIC-02: { tier: BLOCK, predicates: { not: reuse_toml_aggregate_present, diff_scope: file_in_added_set } }

# -------- DISCLOSURE --------
DIS-01: { tier: BLOCK, profile: { public: BLOCK, internal: FLAG } }
DIS-02: { tier: BLOCK, profile: { public: BLOCK, internal: PASS } }
DIS-06: { tier: BLOCK_LOCKED }
DIS-07: { tier: BLOCK }
DIS-08: { tier: SHADOW }

# -------- HARDCODE --------
HC-01: { tier: BLOCK }
HC-02: { tier: BLOCK }
HC-03: { tier: BLOCK_LOCKED }
HC-04: { tier: FLAG }
HC-06: { tier: FLAG }

# -------- TELEMETRY --------
PY-TEL-02: { tier: BLOCK }
JV-TEL-02: { tier: BLOCK }
PY-TEL-06: { tier: BLOCK, predicates: { diff_scope: new_decorator_added } }
PY-TEL-08: { tier: SHADOW }

# -------- DOCS --------
DC-01: { tier: BLOCK }
DC-02: { tier: BLOCK }
DC-11: { tier: BLOCK }
DC-12: { tier: BLOCK }
DC-13: { tier: BLOCK }
DC-14: { tier: BLOCK }

# -------- BDD --------
BDD-01: { tier: BLOCK }
BDD-02: { tier: BLOCK }

# -------- PATTERNS --------
PY-PT-01: { tier: BLOCK, predicates: { module_shape: client } }
JV-PT-01: { tier: BLOCK, predicates: { module_shape: client } }
PY-PT-03: { tier: FLAG }
PY-PT-04: { tier: FLAG }
PY-PT-08: { tier: FLAG }
JV-PT-05: { tier: FLAG }

# -------- VERSIONING --------
VER-01: { tier: BLOCK, predicates: { commit_types_or_breaking: [feat, feat!, breaking] } }
VER-07: { tier: FLAG }
PY-VER-08: { tier: SHADOW }
PY-VER-09: { tier: SHADOW }

# -------- ERRORS/LOGGING --------
EL-01: { tier: FLAG }
EL-02: { tier: FLAG }
EL-04: { tier: BLOCK }
PY-EL-11: { tier: SHADOW }

# -------- TESTING --------
TD-01: { tier: FLAG }
TD-10: { tier: BLOCK }
TD-checkbox: { tier: FLAG }

# -------- HTTP --------
HTTP-01: { tier: FLAG, predicates: { diff_scope: line_in_added_set } }

# -------- CONCURRENCY --------
CC-01: { tier: FLAG }
JV-CC-05: { tier: SHADOW }

# -------- DEPS --------
DEP-03: { tier: FLAG }
DEP-04: { tier: BLOCK_LOCKED }

# -------- COMMITS --------
COM-01: { tier: BLOCK }

# -------- DELETION --------
DEL-01: { tier: BLOCK }

# -------- CONSTANTS --------
PY-CON-01: { tier: FLAG }

# -------- BINDING --------
BND-02: { tier: BLOCK_LOCKED }
BND-04: { tier: BLOCK }
BND-05: { tier: BLOCK_LOCKED }

# -------- QUALITY GATE --------
QG-01: { tier: FLAG }
QG-02: { tier: FLAG }

# -------- PR SIZE (advisory, SHADOW during rollout) --------
PR-SIZE-01: { tier: SHADOW }
PR-SIZE-02: { tier: SHADOW }
PR-SIZE-03: { tier: SHADOW }
PR-SIZE-05: { tier: SHADOW }
104 changes: 104 additions & 0 deletions .claude/scripts/aggregate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#!/usr/bin/env bash
# aggregate.sh — merge N check reports into a single summary, applying rule tiers.
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TMPDIR_RUN="$1"
RULES_YAML="${RULES_YAML:-$SCRIPT_DIR/../config/rules.yaml}"

# get_tier <rule> — reads rules.yaml, returns tier or empty
get_tier() {
local rule="$1"
# rules.yaml format: " RULE-ID: { tier: X, ... }"
awk -v rule="$rule" '
match($0, "^ " rule ":") {
if (match($0, /tier:[[:space:]]*[A-Z_]+/)) {
t = substr($0, RSTART, RLENGTH)
sub(/^tier:[[:space:]]*/, "", t)
print t
exit
}
}
' "$RULES_YAML" 2>/dev/null
}

# Collect all report-*.json files
reports=$(ls "$TMPDIR_RUN"/report-*.json 2>/dev/null | sort)
if [ -z "$reports" ]; then
echo '{"findings":[],"shadow_findings":[],"summary":{"block_count":0,"flag_count":0,"shadow_count":0,"locked_count":0},"per_check_summary":{}}'
exit 0
fi

# Merge raw findings, then re-classify each finding by tier from rules.yaml
merged=$(mktemp)
# shellcheck disable=SC2086
jq -s '.' $reports > "$merged"

# For each finding, look up its rule tier and adjust
retagged_findings="[]"
retagged_shadow="[]"
locked_count=0

n=$(jq '[.[] | .findings // [] | length] | add // 0' "$merged")
if [ "$n" -gt 0 ]; then
all_findings=$(jq '[.[] | .findings // []] | flatten' "$merged")
rule_ids=$(echo "$all_findings" | jq -r '.[] | .rule' | sort -u)

# Build lookup: rule -> tier
tier_map='{}'
while IFS= read -r rule; do
[ -z "$rule" ] && continue
tier=$(get_tier "$rule")
[ -z "$tier" ] && tier="FLAG" # default
tier_map=$(echo "$tier_map" | jq --arg r "$rule" --arg t "$tier" '. + {($r): $t}')
done <<< "$rule_ids"

# Split findings into posted vs shadow based on tier
retagged_findings=$(echo "$all_findings" | jq --argjson tiers "$tier_map" '
map(
. as $f
| ($tiers[$f.rule] // "FLAG") as $tier
| if $tier == "SHADOW" then empty
elif $tier == "BLOCK_LOCKED" then . + {severity: "BLOCK", tier: "BLOCK_LOCKED", locked: true}
elif $tier == "BLOCK" then . + {severity: "BLOCK", tier: "BLOCK"}
elif $tier == "FLAG" then . + {severity: "FLAG", tier: "FLAG"}
else . + {tier: $tier} end
)
')
retagged_shadow=$(echo "$all_findings" | jq --argjson tiers "$tier_map" '
map(
. as $f
| ($tiers[$f.rule] // "FLAG") as $tier
| if $tier == "SHADOW" then . + {tier: "SHADOW"} else empty end
)
')
locked_count=$(echo "$retagged_findings" | jq '[.[] | select(.locked == true)] | length')
fi

block_count=$(echo "$retagged_findings" | jq '[.[] | select(.severity == "BLOCK")] | length')
flag_count=$(echo "$retagged_findings" | jq '[.[] | select(.severity == "FLAG")] | length')
shadow_count=$(echo "$retagged_shadow" | jq 'length')
per_check=$(jq '[.[] | {(.check): {status: .status, count: ((.findings // []) | length)}}] | add' "$merged")

jq -n \
--argjson findings "$retagged_findings" \
--argjson shadow "$retagged_shadow" \
--argjson block "$block_count" \
--argjson flag "$flag_count" \
--argjson shadow_c "$shadow_count" \
--argjson locked "$locked_count" \
--argjson per_check "$per_check" \
'{
version: "1.0.0",
findings: $findings,
shadow_findings: $shadow,
summary: {
block_count: $block,
flag_count: $flag,
shadow_count: $shadow_c,
locked_count: $locked
},
per_check_summary: $per_check
}'

rm -f "$merged"
95 changes: 95 additions & 0 deletions .claude/scripts/check-bdd.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/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)
resolve_sibling_name() {
local mod="$1" dir="$LANGUAGE"
local aliases="$CONFIG_DIR/module-aliases.yaml"
if [ ! -f "$aliases" ]; then echo "$mod"; return; fi
# Simple parser: "- python: X\n java: Y" pairs
if [ "$dir" = "python" ]; then
# look for "python: $mod" then get "java: <name>"
awk -v mod="$mod" '
/python:/ { p_name=$2 }
/java:/ { j_name=$2; if (p_name == mod) { print j_name; exit } }
' "$aliases" | head -1
return
else
awk -v mod="$mod" '
/python:/ { p_name=$2 }
/java:/ { j_name=$2; if (j_name == mod) { print p_name; exit } }
' "$aliases" | head -1
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
# check if it's a "new" module (created in this PR)
if echo "$diff_content" | grep -qE "new file mode.*($mod/)"; 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"
Loading
Loading