Skip to content

Commit 5c3d55f

Browse files
committed
fix(sdk-review): FP-A-01/B-01/B-02/C-01/C-02/D-01/E-01/G-01 — reduce FP rate from 84% to <15%
Wires lib/hunk-filter.sh into 8 check-*.sh scripts so line-scoped findings are suppressed when the reported line is not in the PR's added-lines set. Adds config-level fixes and a peer-consistency check surfaced by the empirical dry-run against the 26-PR consolidated corpus. See docs/plans 09-FP-REMEDIATION.md. FP-A-01 — hunk attribution (systemic, ~80% of Python FPs) * lib/hunk-filter.sh: is_line_touched / is_range_touched / is_meta_finding / emit_finding_if_touched helpers. Permissive when ADDED_LINES_FILE unset. * Wired into: check-hardcode, check-http-hygiene, check-errors-logging, check-disclosure, check-secrets, check-telemetry, check-concurrency, check-constants, check-patterns. * AST-based emissions (PT-08, EL-01/02, TEL-02, CON-01) filtered post-emit via ADDED_LINES_FILE, eliminating whole-file scans. FP-B-01 — HC-01 POM / XML namespace allowlist * check-hardcode.sh: ignore *.md, *.xml, pom.xml, *.yml, *.yaml, *.properties * URL allowlist: maven.apache.org/POM/*, w3.org/*/XMLSchema*, xmlsoap.org/* FP-B-02 — HTTP-01 markdown exclusion * check-http-hygiene.sh: skip *.md/*.rst/*.txt files and docs/examples/ dirs. FP-C-01 — BDD-01 glob-based feature detection * check-bdd.sh: BDD-01 now passes when ANY .feature file exists under tests/<mod>/integration/, not just <mod>.feature. FP-C-02 — PY-PT-04 AST-based exception detection * ast_python_checks.py check_pt_04(module_dir): walks module for classes subclassing Exception / *Error / *Exception. * check-patterns.sh: PY-PT-04 uses AST helper, not exceptions.py existence. FP-D-01 — PY-CON-01 exclusions + per-file cap * ast_python_checks.py check_con_01: skip _models.py, _generated.py, _*_api.py generated files; min_len ≥ 3; cap at 3 findings/file. FP-E-01 — PY-PT-08 line-level baseline * lib/baseline.sh: is_in_line_baseline / bootstrap_pt_08 helpers. baseline.json extended with 'line_baseline' array. Bootstrap via: bash .claude/scripts/lib/baseline.sh bootstrap PY-PT-08 <repo_root> * check-patterns.sh: PY-PT-08 consults baseline before emit. FP-G-01 — peer-consistency for PT-01 (replaces 'factory required' law) * lib/peer-consistency.sh: peer_modules, has_element, peer_element_fraction, should_flag_peer_divergence. * check-patterns.sh: PY-PT-01/JV-PT-01 now emit FLAG (not BLOCK) only when the new module diverges from an element adopted by ≥80% of peer modules. Elements checked: factory, client, config, user-guide.md, py.typed. * rules.yaml: PT-01 tier downgraded BLOCK → FLAG. Regression coverage (tests/sdk-review/test_fp_remediation.bats): 14 new tests pinning every fix above. Full suite: 71 tests, all green. DEL-01 kept as-is (synthetic src/:1 path, not a per-line finding). DIS-07/08 kept as-is (PR_BODY metadata, not filtered). Ref: docs/plans 09-FP-REMEDIATION.md §Pattern A..G.
1 parent 58ede70 commit 5c3d55f

17 files changed

Lines changed: 863 additions & 46 deletions

.claude/config/rules.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ rules:
7171
BDD-02: { tier: BLOCK }
7272

7373
# -------- PATTERNS --------
74-
PY-PT-01: { tier: BLOCK, predicates: { module_shape: client } }
75-
JV-PT-01: { tier: BLOCK, predicates: { module_shape: client } }
74+
PY-PT-01: { tier: FLAG, predicates: { module_shape: client } }
75+
JV-PT-01: { tier: FLAG, predicates: { module_shape: client } }
7676
PY-PT-03: { tier: FLAG }
7777
PY-PT-04: { tier: FLAG }
7878
PY-PT-08: { tier: FLAG }

.claude/scripts/check-bdd.sh

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,28 @@ while IFS= read -r mod; do
6464
[ -z "$mod" ] && continue
6565

6666
# Path for THIS repo's feature file
67+
# FP-C-01: BDD-01 must accept ANY .feature file under the module's integration
68+
# dir, not just `<module>.feature`.
6769
if [ "$LANGUAGE" = "python" ]; then
68-
feature_path="$REPO_ROOT/tests/$mod/integration/$mod.feature"
70+
feature_dir="$REPO_ROOT/tests/$mod/integration"
71+
feature_path="$feature_dir/$mod.feature" # kept for BDD-02 sibling comparison
6972
mod_dir="$REPO_ROOT/src/sap_cloud_sdk/$mod"
7073
else
71-
feature_path="$REPO_ROOT/src/test/resources/com/sap/applicationfoundation/$mod/integration/$mod.feature"
74+
feature_dir="$REPO_ROOT/src/test/resources/com/sap/applicationfoundation/$mod/integration"
75+
feature_path="$feature_dir/$mod.feature" # kept for BDD-02 sibling comparison
7276
mod_dir="$REPO_ROOT/src/main/java/com/sap/cloud/sdk/$mod"
7377
fi
7478

75-
# BDD-01: feature file exists (only fire if module has any source files)
76-
if [ ! -f "$feature_path" ] && [ -d "$mod_dir" ]; then
79+
# BDD-01: at least one .feature file exists in the module's integration dir
80+
# (only fire if module has any source files and PR creates the module).
81+
has_any_feature=false
82+
if [ -d "$feature_dir" ]; then
83+
if compgen -G "$feature_dir/*.feature" > /dev/null 2>&1; then
84+
has_any_feature=true
85+
fi
86+
fi
87+
88+
if [ "$has_any_feature" = "false" ] && [ -d "$mod_dir" ]; then
7789
# Detect if this PR creates any new file INSIDE the module.
7890
# `new file mode` is on its own line before the `+++ b/<path>` header,
7991
# so we scan block-by-block to link them.
@@ -89,9 +101,9 @@ while IFS= read -r mod; do
89101
}
90102
')
91103
if [ "$is_new_module" = "true" ]; then
92-
emit_finding "BDD-01" "BLOCK" "tests/$mod/integration/$mod.feature" 1 \
93-
"New module '$mod' has no BDD feature file" \
94-
"Create $feature_path with cross-language-consistent scenarios" >> "$findings"
104+
emit_finding "BDD-01" "BLOCK" "tests/$mod/integration/" 1 \
105+
"New module '$mod' has no .feature files under integration/" \
106+
"Create at least one $feature_dir/*.feature with cross-language-consistent scenarios" >> "$findings"
95107
fi
96108
fi
97109

.claude/scripts/check-concurrency.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
set -euo pipefail
55
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
66
source "$SCRIPT_DIR/lib/json-emit.sh"
7+
source "$SCRIPT_DIR/lib/hunk-filter.sh"
78
source "$SCRIPT_DIR/lib/skill-self-skip.sh"
89

910
LANGUAGE="${LANGUAGE:-python}"
@@ -27,7 +28,7 @@ echo "$diff_content" | awk '
2728
if echo "$content" | grep -qE 'asyncio\.Queue\('; then
2829
# Check if same file has a Lock or set() nearby
2930
if [ -f "$REPO_ROOT/$file" ] && ! grep -qE 'asyncio\.Lock|set\(\)' "$REPO_ROOT/$file" 2>/dev/null; then
30-
emit_finding "CC-01" "FLAG" "$file" "$line_num" \
31+
emit_finding_if_touched "CC-01" "FLAG" "$file" "$line_num" \
3132
"asyncio.Queue without dedup — if items must be unique, add a set() + asyncio.Lock" "" >> "$findings"
3233
fi
3334
fi

.claude/scripts/check-constants.sh

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
set -euo pipefail
55
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
66
source "$SCRIPT_DIR/lib/json-emit.sh"
7+
source "$SCRIPT_DIR/lib/hunk-filter.sh"
78
source "$SCRIPT_DIR/lib/skill-self-skip.sh"
89

910
LANGUAGE="${LANGUAGE:-python}"
@@ -25,8 +26,20 @@ if [ "$LANGUAGE" = "python" ]; then
2526
filtered="$filtered $f"
2627
done <<< "$changed"
2728
if [ -n "$filtered" ]; then
29+
raw_con=$(mktemp); trap 'rm -f "$raw_con"' EXIT
2830
# shellcheck disable=SC2086
29-
python3 "$SCRIPT_DIR/lib/ast_python_checks.py" con-01 $filtered 2>/dev/null >> "$findings" || true
31+
python3 "$SCRIPT_DIR/lib/ast_python_checks.py" con-01 $filtered 2>/dev/null > "$raw_con" || true
32+
# FP-A-01: filter to touched lines only
33+
while IFS= read -r finding; do
34+
[ -z "$finding" ] && continue
35+
f=$(echo "$finding" | python3 -c "import json,sys;o=json.loads(sys.stdin.read());print(o.get('file',''))")
36+
ln=$(echo "$finding" | python3 -c "import json,sys;o=json.loads(sys.stdin.read());print(o.get('line',0))")
37+
[ -z "$f" ] && continue
38+
if is_line_touched "$f" "$ln"; then
39+
echo "$finding" >> "$findings"
40+
fi
41+
done < "$raw_con"
42+
rm -f "$raw_con"
3043
fi
3144
fi
3245

.claude/scripts/check-deletion-hygiene.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
set -euo pipefail
55
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
66
source "$SCRIPT_DIR/lib/json-emit.sh"
7+
# DEL-01 uses synthetic path "src/:1" — hunk-filter is sourced for
8+
# emit_finding_if_touched (via is_meta_finding, which treats non-file paths
9+
# as metadata and passes them through). No filtering applied here.
10+
source "$SCRIPT_DIR/lib/hunk-filter.sh"
711

812
LANGUAGE="${LANGUAGE:-python}"
913
DIFF_FILE="${DIFF_FILE:-/dev/stdin}"

.claude/scripts/check-disclosure.sh

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ set -euo pipefail
66
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
77
# shellcheck source=lib/json-emit.sh
88
source "$SCRIPT_DIR/lib/json-emit.sh"
9+
# shellcheck source=lib/hunk-filter.sh
10+
source "$SCRIPT_DIR/lib/hunk-filter.sh"
911
# shellcheck source=lib/skill-self-skip.sh
1012
source "$SCRIPT_DIR/lib/skill-self-skip.sh"
1113

@@ -39,16 +41,16 @@ echo "$diff_content" | awk '
3941
# DIS-02: Internal Jira URL (checked first — more specific)
4042
dis02_fired=false
4143
if echo "$content" | grep -qEi 'jira\.tools\.sap|jira\.wdf\.sap\.corp'; then
42-
emit_finding "DIS-02" "$(sev_public)" "$file" "$line_num" "Internal Jira URL — remove or move to internal-only docs" "" >> "$findings"
44+
emit_finding_if_touched "DIS-02" "$(sev_public)" "$file" "$line_num" "Internal Jira URL — remove or move to internal-only docs" "" >> "$findings"
4345
dis02_fired=true
4446
fi
4547
# DIS-01: SAP-internal hostnames (skip if DIS-02 already covers the same line)
4648
if [ "$dis02_fired" = "false" ] && echo "$content" | grep -qEi '(int\.repositories\.cloud\.sap|\.tools\.sap|\.wdf\.sap\.corp|\.mo\.sap\.corp)'; then
47-
emit_finding "DIS-01" "$(sev_public)" "$file" "$line_num" "SAP-internal hostname detected in code" "" >> "$findings"
49+
emit_finding_if_touched "DIS-01" "$(sev_public)" "$file" "$line_num" "SAP-internal hostname detected in code" "" >> "$findings"
4850
fi
4951
# DIS-06: Internal artifactory index-url
5052
if echo "$content" | grep -qEi '\-\-index-url[[:space:]]+https?://int\.repositories\.cloud\.sap'; then
51-
emit_finding "DIS-06" "BLOCK" "$file" "$line_num" "Internal artifactory --index-url — must not appear in public/shared configs" "" >> "$findings"
53+
emit_finding_if_touched "DIS-06" "BLOCK" "$file" "$line_num" "Internal artifactory --index-url — must not appear in public/shared configs" "" >> "$findings"
5254
fi
5355
done
5456

.claude/scripts/check-errors-logging.sh

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
set -euo pipefail
55
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
66
source "$SCRIPT_DIR/lib/json-emit.sh"
7+
source "$SCRIPT_DIR/lib/hunk-filter.sh"
78
source "$SCRIPT_DIR/lib/skill-self-skip.sh"
89

910
LANGUAGE="${LANGUAGE:-python}"
@@ -19,10 +20,22 @@ if [ "$LANGUAGE" = "python" ]; then
1920
changed=$(echo "$diff_content" | grep -oE '^\+\+\+ b/src/.*\.py' | sed 's|^+++ b/||' | sort -u)
2021
if [ -n "$changed" ]; then
2122
# AST-based chaining / swallow — only reports FLAGs when body ends non-Raise
23+
# FP-A-01: filter AST hits through hunk attribution
24+
raw_el=$(mktemp); trap 'rm -f "$raw_el"' EXIT
2225
# shellcheck disable=SC2086
23-
python3 "$SCRIPT_DIR/lib/ast_python_checks.py" el-01 $changed 2>/dev/null >> "$findings" || true
26+
python3 "$SCRIPT_DIR/lib/ast_python_checks.py" el-01 $changed 2>/dev/null > "$raw_el" || true
2427
# shellcheck disable=SC2086
25-
python3 "$SCRIPT_DIR/lib/ast_python_checks.py" el-02 $changed 2>/dev/null >> "$findings" || true
28+
python3 "$SCRIPT_DIR/lib/ast_python_checks.py" el-02 $changed 2>/dev/null >> "$raw_el" || true
29+
while IFS= read -r finding; do
30+
[ -z "$finding" ] && continue
31+
f=$(echo "$finding" | python3 -c "import json,sys;o=json.loads(sys.stdin.read());print(o.get('file',''))")
32+
ln=$(echo "$finding" | python3 -c "import json,sys;o=json.loads(sys.stdin.read());print(o.get('line',0))")
33+
[ -z "$f" ] && continue
34+
if is_line_touched "$f" "$ln"; then
35+
echo "$finding" >> "$findings"
36+
fi
37+
done < "$raw_el"
38+
rm -f "$raw_el"
2639
fi
2740

2841
# EL-04: secret-like variable name in raise args (grep-based, added lines only)
@@ -37,7 +50,7 @@ if [ "$LANGUAGE" = "python" ]; then
3750
if [ "$(is_skill_file "$file")" = "true" ]; then continue; fi
3851
# match raise ...({...token|secret|password|api_key|client_secret...})
3952
if echo "$content" | grep -qE 'raise[[:space:]].*[fF]?"[^"]*\{[^}]*(token|secret|password|api_key|client_secret)[^}]*\}'; then
40-
emit_finding "EL-04" "BLOCK" "$file" "$line_num" \
53+
emit_finding_if_touched "EL-04" "BLOCK" "$file" "$line_num" \
4154
"Exception message includes secret-like variable — leaks sensitive data" "" >> "$findings"
4255
fi
4356
done

.claude/scripts/check-hardcode.sh

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ set -euo pipefail
55
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
66
# shellcheck source=lib/json-emit.sh
77
source "$SCRIPT_DIR/lib/json-emit.sh"
8+
# shellcheck source=lib/hunk-filter.sh
9+
source "$SCRIPT_DIR/lib/hunk-filter.sh"
810
# shellcheck source=lib/skill-self-skip.sh
911
source "$SCRIPT_DIR/lib/skill-self-skip.sh"
1012

@@ -18,10 +20,12 @@ trap 'rm -f "$findings"' EXIT
1820
diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "")
1921

2022
# Determine ignore patterns per language
23+
# FP-B-01: HC-01 must not fire on XML/YAML/POM files where URL-like strings
24+
# are namespace declarations (http://maven.apache.org/POM/4.0.0 etc.).
2125
if [ "$LANGUAGE" = "python" ]; then
22-
ignore_files='^(tests?/|mocks?/|docs?/|.*/spec/|.*/constants\.py|.*/user-guide\.md|README\.md)'
26+
ignore_files='^(tests?/|mocks?/|docs?/|.*/spec/|.*/constants\.py|.*/user-guide\.md|README\.md|.*\.md$|.*\.ya?ml$|.*\.xml$|.*\.properties$|.*pom\.xml$)'
2327
else
24-
ignore_files='^(src/test/|mocks?/|docs?/|.*Constants\.java|.*/constants/|.*/user-guide\.md|README\.md)'
28+
ignore_files='^(src/test/|mocks?/|docs?/|.*Constants\.java|.*/constants/|.*/user-guide\.md|README\.md|.*\.md$|.*\.ya?ml$|.*\.xml$|.*\.properties$|.*pom\.xml$)'
2529
fi
2630

2731
echo "$diff_content" | awk '
@@ -43,30 +47,35 @@ echo "$diff_content" | awk '
4347
[ -z "$url" ] && continue
4448
# allow-list: only IANA-reserved test/example TLDs and localhost
4549
# `.example` must be the terminal label (RFC 2606) — anchor at path/port/end.
50+
# FP-B-01: also allowlist standard XML/POM/W3C namespace URLs which appear
51+
# as identifiers in build files, not as network endpoints.
4652
if echo "$url" | grep -qE '^https?://(localhost|127\.0\.0\.1|example\.(com|org|net)|[^/]+\.example(/|:|$)|reserved\.)'; then
4753
continue
4854
fi
49-
emit_finding "HC-01" "BLOCK" "$file" "$line_num" "Hardcoded URL '$url' in implementation — externalize to config" "" >> "$findings"
55+
if echo "$url" | grep -qE '^https?://(maven\.apache\.org/POM/|www\.w3\.org/[0-9]+/XMLSchema|schemas\.xmlsoap\.org/)'; then
56+
continue
57+
fi
58+
emit_finding_if_touched "HC-01" "BLOCK" "$file" "$line_num" "Hardcoded URL '$url' in implementation — externalize to config" "" >> "$findings"
5059
break # only one finding per line to avoid duplicate reports
5160
done < <(echo "$content" | grep -oE 'https?://[A-Za-z0-9][A-Za-z0-9._~:/?#@!$&*+,;=%-]*' || true)
5261
# HC-02: Authorization Bearer
5362
if echo "$content" | grep -qE 'Authorization[[:space:]]*:[[:space:]]*Bearer[[:space:]]+[A-Za-z0-9]'; then
54-
emit_finding "HC-02" "BLOCK" "$file" "$line_num" "Hardcoded Authorization header value" "" >> "$findings"
63+
emit_finding_if_touched "HC-02" "BLOCK" "$file" "$line_num" "Hardcoded Authorization header value" "" >> "$findings"
5564
fi
5665
# HC-04: direct os.environ / System.getenv
5766
if [ "$LANGUAGE" = "python" ]; then
5867
if echo "$content" | grep -qE 'os\.environ\["[A-Z]'; then
59-
emit_finding "HC-04" "FLAG" "$file" "$line_num" "Direct os.environ access — prefer secret_resolver / config layer" "" >> "$findings"
68+
emit_finding_if_touched "HC-04" "FLAG" "$file" "$line_num" "Direct os.environ access — prefer secret_resolver / config layer" "" >> "$findings"
6069
fi
6170
else
6271
if echo "$content" | grep -qE 'System\.getenv\('; then
63-
emit_finding "HC-04" "FLAG" "$file" "$line_num" "Direct System.getenv() — prefer SecretResolver" "" >> "$findings"
72+
emit_finding_if_touched "HC-04" "FLAG" "$file" "$line_num" "Direct System.getenv() — prefer SecretResolver" "" >> "$findings"
6473
fi
6574
fi
6675
# HC-06: hardcoded timeout numeric literal
6776
# Use word boundary via (^|[^A-Za-z0-9_]) so 'default_timeout' or 'my_timeout' don't match
6877
if echo "$content" | grep -qiE '(^|[^A-Za-z0-9_])(timeout|retries|max_retries)[[:space:]]*=[[:space:]]*[0-9]+'; then
69-
emit_finding "HC-06" "FLAG" "$file" "$line_num" "Hardcoded timeout/retry number — externalize to config" "" >> "$findings"
78+
emit_finding_if_touched "HC-06" "FLAG" "$file" "$line_num" "Hardcoded timeout/retry number — externalize to config" "" >> "$findings"
7079
fi
7180
done
7281

.claude/scripts/check-http-hygiene.sh

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
set -euo pipefail
55
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
66
source "$SCRIPT_DIR/lib/json-emit.sh"
7+
source "$SCRIPT_DIR/lib/hunk-filter.sh"
78
source "$SCRIPT_DIR/lib/skill-self-skip.sh"
89

910
LANGUAGE="${LANGUAGE:-python}"
@@ -24,10 +25,12 @@ echo "$diff_content" | awk '
2425
' | while IFS=$'\t' read -r file line_num content; do
2526
[ -z "$file" ] && continue
2627
if [ "$(is_skill_file "$file")" = "true" ]; then continue; fi
27-
# only for impl files (not tests/mocks)
28-
if echo "$file" | grep -qE '^(tests?/|mocks?/)'; then continue; fi
28+
# only for impl files (not tests/mocks/docs/examples/markdown)
29+
# FP-B-02: HTTP-01 must not fire on markdown code fences.
30+
if echo "$file" | grep -qE '^(tests?/|mocks?/|docs?/|examples?/)'; then continue; fi
31+
if echo "$file" | grep -qiE '\.(md|rst|txt)$'; then continue; fi
2932
if echo "$content" | grep -qE '(requests|httpx)\.(Session|AsyncClient)\(\)'; then
30-
emit_finding "HTTP-01" "FLAG" "$file" "$line_num" \
33+
emit_finding_if_touched "HTTP-01" "FLAG" "$file" "$line_num" \
3134
"HTTP session created per invocation — prefer single instance in __init__" "" >> "$findings"
3235
fi
3336
done

.claude/scripts/check-patterns.sh

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ set -euo pipefail
55
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
66
source "$SCRIPT_DIR/lib/json-emit.sh"
77
source "$SCRIPT_DIR/lib/predicates.sh"
8+
source "$SCRIPT_DIR/lib/hunk-filter.sh"
9+
source "$SCRIPT_DIR/lib/baseline.sh"
10+
source "$SCRIPT_DIR/lib/peer-consistency.sh"
811

912
LANGUAGE="${LANGUAGE:-python}"
1013
REPO_ROOT="${REPO_ROOT:-.}"
@@ -33,20 +36,31 @@ while IFS= read -r mod; do
3336

3437
[ -d "$mod_dir" ] || continue
3538

36-
# PY-PT-01 / JV-PT-01: factory exists — only fire on client modules
39+
# PY-PT-01 / JV-PT-01: peer-consistency check (FP-G-01).
40+
# Previously: BLOCK if client module missing create_client()/ClientFactory.
41+
# Empirically wrong — only `destination` follows that shape. Now: FLAG only
42+
# when the module diverges from an element that ≥80% of peers adopt.
43+
# Tier: FLAG (never BLOCK).
3744
shape=$(module_shape "$mod_dir")
3845
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"
46+
for element in factory client config user-guide.md exceptions py.typed; do
47+
# py.typed only applies to Python
48+
if [ "$element" = "py.typed" ] && [ "$LANGUAGE" != "python" ]; then continue; fi
49+
# exceptions is checked separately below with AST for accuracy
50+
if [ "$element" = "exceptions" ]; then continue; fi
51+
if should_flag_peer_divergence "$mod_dir" "$element" "$LANGUAGE" "0.80"; then
52+
if [ "$LANGUAGE" = "python" ]; then
53+
rule_id="PY-PT-01"
54+
rel="src/sap_cloud_sdk/$mod/"
55+
else
56+
rule_id="JV-PT-01"
57+
rel="src/main/java/com/sap/cloud/sdk/$mod/"
58+
fi
59+
emit_finding "$rule_id" "FLAG" "$rel" 1 \
60+
"Module '$mod' diverges from peer convention: missing '$element' (≥80% of peer modules have it)" \
61+
"Consider adding $element for consistency with sibling modules" >> "$findings"
4362
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
63+
done
5064
fi
5165

5266
# PY-PT-03: py.typed marker
@@ -58,10 +72,12 @@ while IFS= read -r mod; do
5872
fi
5973

6074
# PY-PT-04 / JV-PT-05: module-specific exceptions
75+
# FP-C-02: pass if module has ANY class subclassing Exception (or *Error/*Exception),
76+
# regardless of whether it lives in exceptions.py or __init__.py or elsewhere.
6177
if [ "$LANGUAGE" = "python" ]; then
62-
if [ ! -f "$mod_dir/exceptions.py" ]; then
78+
if ! python3 "$SCRIPT_DIR/lib/ast_python_checks.py" pt-04 "$mod_dir" 2>/dev/null; then
6379
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"
80+
"Module lacks Exception subclasses — define a module-specific exception hierarchy (in exceptions.py or __init__.py)" "" >> "$findings"
6581
fi
6682
else
6783
if [ ! -d "$mod_dir/exceptions" ]; then
@@ -73,11 +89,28 @@ while IFS= read -r mod; do
7389
done <<< "$modules"
7490

7591
# PY-PT-08 via AST on changed Python files
92+
# FP-A-01: filter by ADDED_LINES_FILE (only fire on functions declared on lines the PR touched)
93+
# FP-E-01: consult line-level baseline for pre-existing PT-08 debt
7694
if [ "$LANGUAGE" = "python" ]; then
7795
changed_py=$(echo "$diff_content" | grep -oE '^\+\+\+ b/src/sap_cloud_sdk/.*\.py' | sed 's|^+++ b/||' | grep -v '__pycache__' | sort -u)
7896
if [ -n "$changed_py" ]; then
97+
raw_pt08=$(mktemp); trap 'rm -f "$raw_pt08"' EXIT
7998
# shellcheck disable=SC2086
80-
python3 "$SCRIPT_DIR/lib/ast_python_checks.py" pt-08 $changed_py 2>/dev/null >> "$findings" || true
99+
python3 "$SCRIPT_DIR/lib/ast_python_checks.py" pt-08 $changed_py 2>/dev/null > "$raw_pt08" || true
100+
# Filter: keep only findings on lines touched by this PR AND not in baseline
101+
while IFS= read -r finding; do
102+
[ -z "$finding" ] && continue
103+
f=$(echo "$finding" | python3 -c "import json,sys;o=json.loads(sys.stdin.read());print(o.get('file',''))")
104+
ln=$(echo "$finding" | python3 -c "import json,sys;o=json.loads(sys.stdin.read());print(o.get('line',0))")
105+
[ -z "$f" ] && continue
106+
# Baseline check first (bypasses hunk filter — always suppressed)
107+
if is_in_line_baseline "PY-PT-08" "$f" "$ln"; then continue; fi
108+
# Hunk filter
109+
if is_line_touched "$f" "$ln"; then
110+
echo "$finding" >> "$findings"
111+
fi
112+
done < "$raw_pt08"
113+
rm -f "$raw_pt08"
81114
fi
82115
fi
83116

0 commit comments

Comments
 (0)