Skip to content

Commit f88fb3b

Browse files
committed
feat(sdk-review): batch 1/6 — shared libs + config
Adds the foundation for the SDK Module Review skill: ## Libs (.claude/scripts/lib/) - json-emit.sh — findings JSON emitter + status derivation - diff-added-lines.sh — extract file:line pairs from diff (BSD-safe) - baseline.sh — apply baseline.json exemptions to reports - predicates.sh — scope predicates (commit_types, module_shape, REUSE.toml) - suppression.sh — parse # sdk-review: ignore[<check>] comments - apply-suppression.sh — filter reports by suppression tuples - github-api.sh — gh CLI wrapper (auth, comments, check-run, labels) - detect-language.sh — Python vs Java detection - detect-modules.sh — module extraction from diff - tier-manager.sh — apply SHADOW/FLAG/BLOCK/BLOCK_LOCKED tiers - skill-self-skip.sh — self-review protection (skip skill's own files) - ast_python_checks.py — Python AST checks (used by errors, telemetry, patterns, constants) - breaking-detector.py — API-diff detector for BREAKING family ## Config (.claude/config/) - rules.yaml — 70+ rules with tiers per rule - module-aliases.yaml — cross-language module name aliases (dms↔documentmanagement) - baseline.json — per-repo exemptions (REUSE.toml here → LIC-01/02 exempt) **Standalone testable.** No checks or orchestrator yet — comes in later batches. Part 1 of 6. Ref: AFSDK-3937
1 parent 4dfc723 commit f88fb3b

24 files changed

Lines changed: 2543 additions & 0 deletions

.claude/config/baseline.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"created_at": "2026-07-15T00:00:00Z",
3+
"at_commit": "TBD",
4+
"schema_version": 1,
5+
"exemptions": {
6+
"PY-LIC-01": {
7+
"reason": "Repo uses REUSE.toml aggregate — pre-existing files exempt",
8+
"detector": "REUSE.toml exists with path='**' aggregate annotation",
9+
"scope": "repo",
10+
"files_exempted_glob": "src/**/*.py"
11+
},
12+
"PY-LIC-02": {
13+
"reason": "Same as PY-LIC-01",
14+
"scope": "repo"
15+
},
16+
"JV-LIC-01": {
17+
"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",
18+
"scope": "repo"
19+
},
20+
"JV-LIC-02": {
21+
"reason": "Same as JV-LIC-01",
22+
"scope": "repo"
23+
}
24+
}
25+
}

.claude/config/module-aliases.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Module name aliases across languages.
2+
# Used by check-bdd.sh to resolve cross-repo feature file location.
3+
aliases:
4+
- python: dms
5+
java: documentmanagement

.claude/config/rules.yaml

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
version: 2
2+
3+
# =====================================================
4+
# rules.yaml — SDK Module Review rule catalog
5+
# All rules default to their tier here; may be overridden per-repo.
6+
# BLOCK_LOCKED rules cannot be softened or suppressed.
7+
# =====================================================
8+
9+
baseline:
10+
file: .claude/baseline.json
11+
regenerate_with: orchestrate.sh --baseline
12+
13+
tiers:
14+
SHADOW: { posts: false, blocks: false }
15+
FLAG: { posts: true, blocks: false }
16+
BLOCK: { posts: true, blocks: true }
17+
BLOCK_LOCKED: { posts: true, blocks: true, suppressible: false, downgradable: false }
18+
19+
rules:
20+
# -------- BREAKING FAMILY (all locked) --------
21+
BREAKING-01: { tier: BLOCK_LOCKED }
22+
BREAKING-02: { tier: BLOCK_LOCKED }
23+
BREAKING-03: { tier: FLAG }
24+
BREAKING-04: { tier: FLAG }
25+
26+
# -------- SECRETS (all locked) --------
27+
SEC-01: { tier: BLOCK_LOCKED }
28+
SEC-02: { tier: BLOCK_LOCKED }
29+
SEC-03: { tier: BLOCK_LOCKED }
30+
SEC-04: { tier: BLOCK_LOCKED }
31+
SEC-05: { tier: BLOCK_LOCKED }
32+
SEC-06: { tier: BLOCK_LOCKED }
33+
SEC-07: { tier: BLOCK_LOCKED }
34+
SEC-08: { tier: BLOCK_LOCKED }
35+
SEC-10: { tier: BLOCK_LOCKED }
36+
37+
# -------- LICENSE --------
38+
LIC-01: { tier: BLOCK, predicates: { not: reuse_toml_aggregate_present, diff_scope: file_in_added_set } }
39+
LIC-02: { tier: BLOCK, predicates: { not: reuse_toml_aggregate_present, diff_scope: file_in_added_set } }
40+
41+
# -------- DISCLOSURE --------
42+
DIS-01: { tier: BLOCK, profile: { public: BLOCK, internal: FLAG } }
43+
DIS-02: { tier: BLOCK, profile: { public: BLOCK, internal: PASS } }
44+
DIS-06: { tier: BLOCK_LOCKED }
45+
DIS-07: { tier: BLOCK }
46+
DIS-08: { tier: SHADOW }
47+
48+
# -------- HARDCODE --------
49+
HC-01: { tier: BLOCK }
50+
HC-02: { tier: BLOCK }
51+
HC-03: { tier: BLOCK_LOCKED }
52+
HC-04: { tier: FLAG }
53+
HC-06: { tier: FLAG }
54+
55+
# -------- TELEMETRY --------
56+
PY-TEL-02: { tier: BLOCK }
57+
JV-TEL-02: { tier: BLOCK }
58+
PY-TEL-06: { tier: BLOCK, predicates: { diff_scope: new_decorator_added } }
59+
PY-TEL-08: { tier: SHADOW }
60+
61+
# -------- DOCS --------
62+
DC-01: { tier: BLOCK }
63+
DC-02: { tier: BLOCK }
64+
DC-11: { tier: BLOCK }
65+
DC-12: { tier: BLOCK }
66+
DC-13: { tier: BLOCK }
67+
DC-14: { tier: BLOCK }
68+
69+
# -------- BDD --------
70+
BDD-01: { tier: BLOCK }
71+
BDD-02: { tier: BLOCK }
72+
73+
# -------- PATTERNS --------
74+
PY-PT-01: { tier: BLOCK, predicates: { module_shape: client } }
75+
JV-PT-01: { tier: BLOCK, predicates: { module_shape: client } }
76+
PY-PT-03: { tier: FLAG }
77+
PY-PT-04: { tier: FLAG }
78+
PY-PT-08: { tier: FLAG }
79+
JV-PT-05: { tier: FLAG }
80+
81+
# -------- VERSIONING --------
82+
VER-01: { tier: BLOCK, predicates: { commit_types_or_breaking: [feat, feat!, breaking] } }
83+
VER-07: { tier: FLAG }
84+
PY-VER-08: { tier: SHADOW }
85+
PY-VER-09: { tier: SHADOW }
86+
87+
# -------- ERRORS/LOGGING --------
88+
EL-01: { tier: FLAG }
89+
EL-02: { tier: FLAG }
90+
EL-04: { tier: BLOCK }
91+
PY-EL-11: { tier: SHADOW }
92+
93+
# -------- TESTING --------
94+
TD-01: { tier: FLAG }
95+
TD-10: { tier: BLOCK }
96+
TD-checkbox: { tier: FLAG }
97+
98+
# -------- HTTP --------
99+
HTTP-01: { tier: FLAG, predicates: { diff_scope: line_in_added_set } }
100+
101+
# -------- CONCURRENCY --------
102+
CC-01: { tier: FLAG }
103+
JV-CC-05: { tier: SHADOW }
104+
105+
# -------- DEPS --------
106+
DEP-03: { tier: FLAG }
107+
DEP-04: { tier: BLOCK_LOCKED }
108+
109+
# -------- COMMITS --------
110+
COM-01: { tier: BLOCK }
111+
112+
# -------- DELETION --------
113+
DEL-01: { tier: BLOCK }
114+
115+
# -------- CONSTANTS --------
116+
PY-CON-01: { tier: FLAG }
117+
118+
# -------- BINDING --------
119+
BND-02: { tier: BLOCK_LOCKED }
120+
BND-04: { tier: BLOCK }
121+
BND-05: { tier: BLOCK_LOCKED }
122+
123+
# -------- QUALITY GATE --------
124+
QG-01: { tier: FLAG }
125+
QG-02: { tier: FLAG }
126+
127+
# -------- PR SIZE (advisory, SHADOW during rollout) --------
128+
PR-SIZE-01: { tier: SHADOW }
129+
PR-SIZE-02: { tier: SHADOW }
130+
PR-SIZE-03: { tier: SHADOW }
131+
PR-SIZE-05: { tier: SHADOW }
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#!/usr/bin/env bash
2+
# lib/apply-suppression.sh — filter findings against suppression comments in source.
3+
# Called after a check produces its JSON report; reads suppression tuples and drops findings.
4+
set -euo pipefail
5+
6+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7+
# Source suppression.sh so we don't fork bash+jq per finding.
8+
# shellcheck source=./suppression.sh
9+
source "$SCRIPT_DIR/suppression.sh"
10+
11+
# collect_suppressions_for_files <file1> <file2> ...
12+
# Emits "<file>:<line>:<rule>" tuples for all suppression comments found.
13+
collect_suppressions_for_files() {
14+
for f in "$@"; do
15+
[ -f "$f" ] || continue
16+
parse_line_suppressions "$f" 2>/dev/null || true
17+
parse_file_suppressions "$f" 2>/dev/null || true
18+
done
19+
}
20+
21+
# apply_to_report <report_json> <suppressions_file> → prints filtered report
22+
apply_to_report() {
23+
local report="$1" supp_file="$2"
24+
local suppressed_count=0
25+
local input; input=$(cat "$report")
26+
local n; n=$(echo "$input" | jq '.findings | length')
27+
if [ "$n" -eq 0 ]; then
28+
echo "$input"; return
29+
fi
30+
31+
# Extract all (rule, file, line) tuples in a single jq call — one fork total
32+
# instead of one per finding.
33+
local tuples; tuples=$(echo "$input" | jq -r '.findings[] | "\(.rule)|\(.file)|\(.line)"')
34+
35+
local keep_flags=()
36+
local i=0
37+
while IFS='|' read -r rule file line; do
38+
if [ "$(is_suppressed "$rule" "$file" "$line" "$supp_file")" = "true" ]; then
39+
keep_flags+=("0")
40+
suppressed_count=$((suppressed_count + 1))
41+
else
42+
keep_flags+=("1")
43+
fi
44+
i=$((i + 1))
45+
done <<< "$tuples"
46+
47+
# Build a JSON array of booleans and use jq to filter
48+
local keep_json
49+
if [ "${#keep_flags[@]}" -eq 0 ]; then
50+
keep_json="[]"
51+
else
52+
keep_json=$(printf '%s\n' "${keep_flags[@]}" | jq -s 'map(. == "1")')
53+
fi
54+
55+
echo "$input" | jq \
56+
--argjson keep "$keep_json" \
57+
--argjson suppressed "$suppressed_count" \
58+
'.findings = [.findings[] as $f | ($f | . as $x | (input_line_number - 1) as $i | select($keep[$i]))] // .findings
59+
| .findings = [range(.findings | length) as $i | .findings[$i] | select($keep[$i])]
60+
| .summary.suppressed_count = $suppressed' 2>/dev/null || {
61+
# Fallback: simple length-preserving filter
62+
echo "$input" | jq \
63+
--argjson keep "$keep_json" \
64+
--argjson suppressed "$suppressed_count" \
65+
'(.findings | length) as $n
66+
| .findings = [range($n) | . as $i | (($keep[$i] // true) | if . then $i else null end) | select(. != null)] as $indices
67+
| .findings = [$indices[] as $i | .findings[$i]]
68+
| .summary.suppressed_count = $suppressed'
69+
}
70+
}
71+
72+
# Simpler variant: build kept-list explicitly (safe fallback)
73+
apply_to_report_v2() {
74+
local report="$1" supp_file="$2"
75+
local suppressed_count=0
76+
local input; input=$(cat "$report")
77+
local n; n=$(echo "$input" | jq '.findings | length')
78+
if [ "$n" -eq 0 ]; then
79+
echo "$input"; return
80+
fi
81+
82+
local kept=()
83+
local i=0
84+
while [ "$i" -lt "$n" ]; do
85+
local finding rule file line
86+
finding=$(echo "$input" | jq -c ".findings[$i]")
87+
rule=$(echo "$finding" | jq -r '.rule')
88+
file=$(echo "$finding" | jq -r '.file')
89+
line=$(echo "$finding" | jq -r '.line')
90+
91+
if [ "$(is_suppressed "$rule" "$file" "$line" "$supp_file")" = "true" ]; then
92+
suppressed_count=$((suppressed_count + 1))
93+
else
94+
kept+=("$finding")
95+
fi
96+
i=$((i + 1))
97+
done
98+
99+
local kept_json
100+
if [ "${#kept[@]}" -eq 0 ]; then
101+
kept_json="[]"
102+
else
103+
kept_json=$(printf '%s\n' "${kept[@]}" | jq -s -c '.')
104+
fi
105+
106+
echo "$input" | jq \
107+
--argjson kept "$kept_json" \
108+
--argjson suppressed "$suppressed_count" \
109+
'.findings = $kept | .summary.suppressed_count = $suppressed'
110+
}
111+
112+
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
113+
cmd="${1:-}"; shift || true
114+
case "$cmd" in
115+
collect) collect_suppressions_for_files "$@" ;;
116+
apply) apply_to_report_v2 "$@" ;; # simpler + safer
117+
*) echo "Usage: apply-suppression.sh {collect|apply} args" >&2; exit 2 ;;
118+
esac
119+
fi

0 commit comments

Comments
 (0)