javi-forge 1.18.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/hooks/commit-msg +178 -20
- package/assets/hooks/manifest.json +38 -30
- package/assets/hooks/pre-push +32 -15
- package/package.json +3 -2
- package/ci-local/hooks/commit-msg.test.sh +0 -286
package/assets/hooks/commit-msg
CHANGED
|
@@ -1,30 +1,188 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# Commit-msg: block AI attribution in commit messages
|
|
3
2
|
set -e
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# COMMIT-MSG: Block accidental AI-attribution + enforce Conventional Commits
|
|
5
|
+
# =============================================================================
|
|
6
|
+
# Best-effort, NOT an adversarial-grade security control.
|
|
7
|
+
#
|
|
8
|
+
# Two INDEPENDENT guards run in order; both must pass:
|
|
9
|
+
# 1. AI-attribution guard (always-on raw-literal matching; best-effort NFKC
|
|
10
|
+
# normalization layered on top when perl + Unicode::Normalize exist).
|
|
11
|
+
# 2. Conventional-commit subject guard (raw first non-comment line).
|
|
12
|
+
#
|
|
13
|
+
# Scope of the attribution guard:
|
|
14
|
+
# Defends against: accidental copy-paste, whitespace tricks (double space,
|
|
15
|
+
# NBSP, ZWSP), markdown formatting, full-width latin, combining diacritics,
|
|
16
|
+
# dashes/underscores in model names, punctuation/emoji separators.
|
|
17
|
+
# Does NOT defend against: cross-script homoglyphs, tag characters, variation
|
|
18
|
+
# selectors, standalone provider names without verb/model, motivated
|
|
19
|
+
# adversaries with a Unicode table. If you need adversarial defense, use
|
|
20
|
+
# signed commits, not regex. The threat model is an HONEST committer.
|
|
21
|
+
#
|
|
22
|
+
# The test suite (commit-msg.test.sh) documents covered/accepted bypasses.
|
|
23
|
+
# =============================================================================
|
|
24
|
+
|
|
4
25
|
COMMIT_MSG_FILE="$1"
|
|
5
|
-
|
|
26
|
+
RAW_MSG=$(cat "$COMMIT_MSG_FILE")
|
|
27
|
+
|
|
28
|
+
RED='\033[0;31m'
|
|
29
|
+
GREEN='\033[0;32m'
|
|
30
|
+
YELLOW='\033[1;33m'
|
|
31
|
+
NC='\033[0m'
|
|
32
|
+
|
|
33
|
+
# ─── Normalization ──────────────────────────────────────────────────
|
|
34
|
+
# If perl fails (not installed, Unicode::Normalize module absent), normalize
|
|
35
|
+
# returns the raw message. The pattern loop still matches against RAW_MSG, so a
|
|
36
|
+
# broken perl degrades gracefully but does not open everything up.
|
|
37
|
+
normalize_msg() {
|
|
38
|
+
if ! command -v perl >/dev/null 2>&1; then
|
|
39
|
+
printf "%s" "$1"
|
|
40
|
+
return
|
|
41
|
+
fi
|
|
42
|
+
# 2>/dev/null suppresses perl warnings. If the script fails outright,
|
|
43
|
+
# || printf "%s" "$1" lets the raw through — the match against RAW_MSG
|
|
44
|
+
# below still covers the literal patterns.
|
|
45
|
+
printf "%s" "$1" | perl -CSAD -pe '
|
|
46
|
+
# 1. Replace Unicode formatting/invisible chars with a SPACE (not delete).
|
|
47
|
+
# Deleting them joins "by" + "Claude" -> "byClaude" and the regex loses the match.
|
|
48
|
+
# ZWSP/ZWNJ/ZWJ/LRM/RLM, NBSP, bidi controls, BOM, word-joiner.
|
|
49
|
+
s/[\x{00A0}\x{200B}-\x{200F}\x{2028}-\x{202F}\x{2060}-\x{206F}\x{FEFF}]/ /g;
|
|
50
|
+
# 2. Strip combining diacritics. This folds "Ćlaude" (C + U+0301) to "Claude".
|
|
51
|
+
s/[\x{0300}-\x{036F}]//g;
|
|
52
|
+
# 3. NFKC normalizes compatibility forms (full-width latin -> ascii)
|
|
53
|
+
use Unicode::Normalize qw(NFKC);
|
|
54
|
+
$_ = NFKC($_);
|
|
55
|
+
# 4. Markdown / punctuation / emoji -> space
|
|
56
|
+
# Markdown: * _ ` ~
|
|
57
|
+
# Punctuation: . , : ; ! ? ( ) [ ] { } < > / \ | " '\''
|
|
58
|
+
# Emoji: Misc Symbols (2600-27BF), Pictographs (1F300-1FAFF)
|
|
59
|
+
s/[*_`~.,:;!?()\[\]{}<>\\\/|"'\''\x{2600}-\x{27BF}\x{1F300}-\x{1FAFF}]/ /g;
|
|
60
|
+
# 5. Collapse whitespace (includes \n, \t, multiple spaces)
|
|
61
|
+
s/\s+/ /g;
|
|
62
|
+
' 2>/dev/null || printf "%s" "$1"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
NORMALIZED_MSG=$(normalize_msg "$RAW_MSG")
|
|
66
|
+
|
|
67
|
+
# ─── Patterns (case insensitive, grep -iE) ──────────────────────────
|
|
68
|
+
#
|
|
69
|
+
# Notes:
|
|
70
|
+
# \b = word boundary (GNU grep ext.)
|
|
71
|
+
# [[:space:]]+ = one or more spaces (POSIX, portable)
|
|
72
|
+
# [-_ ]? = optional separator between provider and model
|
|
73
|
+
#
|
|
74
|
+
# Known, accepted limitation: Unicode homoglyphs (Cyrillic С instead of
|
|
75
|
+
# Latin C) require transliteration (Text::Unidecode), a non-core perl module.
|
|
76
|
+
# Documented, not defended against.
|
|
77
|
+
PROVIDER_PAT='(claude|gpt|chatgpt|openai|anthropic|gemini|copilot|llm|cursor|windsurf|codeium|cody|aider)'
|
|
6
78
|
|
|
7
79
|
AI_PATTERNS=(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
80
|
+
# ── Co-authored-by trailer ────────────────────────────────────────
|
|
81
|
+
# Captures any provider in the same trailer.
|
|
82
|
+
"co-authored-by:.*\b${PROVIDER_PAT}\b"
|
|
83
|
+
# AI in trailer: AI alone OR AIAssistant / AIBot / AIAgent compounds
|
|
84
|
+
"co-authored-by:.*\bai(assistant|bot|agent|helper)?\b"
|
|
85
|
+
# Standalone Copilot/Codeium/Cursor in trailer (no vendor prefix)
|
|
86
|
+
"co-authored-by:.*\b(copilot|cursor|codeium|cody|aider|chatgpt)\b"
|
|
87
|
+
|
|
88
|
+
# ── Claude-Session trailer (Claude Code harness auto-appends this) ─
|
|
89
|
+
# The canonical harness trailer key + its session URL host. Anchored to
|
|
90
|
+
# the trailer key "claude-session:" and the "claude.ai" host, so it does
|
|
91
|
+
# NOT fire on a person named Claude or a bare "session" word (zero FP).
|
|
92
|
+
"claude-session:"
|
|
93
|
+
"claude\\.ai"
|
|
94
|
+
|
|
95
|
+
# ── Attribution verbs + by/with + provider ───────────────────────
|
|
96
|
+
# e.g.: "made by Claude", "generated with GPT", "built with OpenAI"
|
|
97
|
+
# Verbs expanded in audit-2 to cover "I used X", "X helped me",
|
|
98
|
+
# "wrote by AI", "prompted to write", "asked Claude to".
|
|
99
|
+
"(made|generated|written|created|assisted|built|developed|authored|produced|coded|crafted|helped|used|wrote|prompted|asked|fixed|refactored)[[:space:]]+(by|with|using|via)[[:space:]]+${PROVIDER_PAT}"
|
|
100
|
+
"(made|generated|written|created|assisted|built|developed|authored|produced|coded|crafted|helped|used|wrote|prompted|asked|fixed|refactored)[[:space:]]+(by|with|using|via)[[:space:]]+\bai(assistant|bot|agent|helper)?\b"
|
|
101
|
+
|
|
102
|
+
# ── "with help from / thanks to / powered by X" ──────────────────
|
|
103
|
+
"(with[[:space:]]+help[[:space:]]+from|thanks[[:space:]]+to|powered[[:space:]]+by|courtesy[[:space:]]+of)[[:space:]]+${PROVIDER_PAT}"
|
|
104
|
+
"(with[[:space:]]+help[[:space:]]+from|thanks[[:space:]]+to|powered[[:space:]]+by|courtesy[[:space:]]+of)[[:space:]]+\bai(assistant|bot|agent|helper)?\b"
|
|
105
|
+
|
|
106
|
+
# ── "X-assisted" / "X assisted" ──────────────────────────────────
|
|
107
|
+
# Covers: anthropic-assisted, openai assisted, ai_assisted, etc.
|
|
108
|
+
"${PROVIDER_PAT}[-_ ]?assisted"
|
|
109
|
+
"\bai[-_ ]?assisted"
|
|
110
|
+
|
|
111
|
+
# ── Standalone branding / tooling ────────────────────────────────
|
|
112
|
+
# ACCEPTED TRADEOFF (JDA-002): "cursor" is homonymous with the DB/UI term.
|
|
113
|
+
# Because "-" is a grep word boundary, ANY bare-cursor match also fires on
|
|
114
|
+
# "cursor-based" / "DB cursor"; blocking "built with Cursor" (an AI-tool
|
|
115
|
+
# attribution we must catch) necessarily catches those. No tightening
|
|
116
|
+
# separates them without overfitting a blocklist, so the FP is documented,
|
|
117
|
+
# not chased. Domain/IDE/AI forms are the reliable signal.
|
|
118
|
+
"claude[-_ ]?code"
|
|
119
|
+
"(github|microsoft)[-_ ]?copilot"
|
|
120
|
+
"\bchatgpt\b"
|
|
121
|
+
"\bcursor[-_ ]?(ai|ide)?\b"
|
|
122
|
+
"\bwindsurf\b"
|
|
123
|
+
"\bcodeium\b"
|
|
124
|
+
"\bcody\b"
|
|
125
|
+
"\baider\b"
|
|
126
|
+
|
|
127
|
+
# ── Claude + model variant ───────────────────────────────────────
|
|
128
|
+
# Covers: "claude opus", "claude-sonnet", "Claude3", "claude4.5", "Claude 4.7"
|
|
129
|
+
"claude[-_ ]?(opus|sonnet|haiku|[0-9])"
|
|
130
|
+
|
|
131
|
+
# ── GPT + version ────────────────────────────────────────────────
|
|
132
|
+
# Covers: gpt-4, gpt 3, gpt_5, gpt4.5
|
|
133
|
+
"gpt[-_ ]?[0-9]"
|
|
134
|
+
|
|
135
|
+
# ── Provider email domains ───────────────────────────────────────
|
|
136
|
+
"@(anthropic|openai|cursor|codeium)\\.(com|sh|ai)"
|
|
137
|
+
"noreply@(anthropic|openai)"
|
|
18
138
|
)
|
|
19
139
|
|
|
140
|
+
# Run pattern matching against the NORMALIZED message.
|
|
141
|
+
# Use both raw and normalized — raw catches anything normalization might over-strip.
|
|
20
142
|
for pattern in "${AI_PATTERNS[@]}"; do
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
143
|
+
if printf "%s" "$NORMALIZED_MSG" | grep -iqE "$pattern" \
|
|
144
|
+
|| printf "%s" "$RAW_MSG" | grep -iqE "$pattern"; then
|
|
145
|
+
echo -e ""
|
|
146
|
+
echo -e "${RED}COMMIT BLOCKED: AI Attribution Detected${NC}"
|
|
147
|
+
echo -e ""
|
|
148
|
+
echo -e "${RED}Pattern matched:${NC} ${YELLOW}$pattern${NC}"
|
|
149
|
+
echo -e ""
|
|
150
|
+
echo -e "${RED}Your commit message:${NC}"
|
|
151
|
+
echo -e "${YELLOW}$RAW_MSG${NC}"
|
|
152
|
+
echo -e ""
|
|
153
|
+
echo -e "${GREEN}Remove AI attribution. You are the sole author.${NC}"
|
|
154
|
+
echo -e ""
|
|
155
|
+
exit 1
|
|
156
|
+
fi
|
|
29
157
|
done
|
|
158
|
+
|
|
159
|
+
# ─── Conventional-commit subject guard ──────────────────────────────
|
|
160
|
+
# Independent from the attribution guard above. Validates the RAW subject
|
|
161
|
+
# (first non-blank, non-comment line — git has not stripped comments yet at
|
|
162
|
+
# commit-msg time). Exempt prefixes skip the regex; everything else must
|
|
163
|
+
# match the Conventional Commit grammar.
|
|
164
|
+
CONV_COMMIT_RE='^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9._-]+\))?!?: .+'
|
|
165
|
+
|
|
166
|
+
SUBJECT=$(printf '%s\n' "$RAW_MSG" | grep -vE '^[[:space:]]*(#|$)' | head -n1 || true)
|
|
167
|
+
|
|
168
|
+
case "$SUBJECT" in
|
|
169
|
+
"Merge "*|"fixup! "*|"squash! "*|"amend! "*|"reword! "*|"Revert "*)
|
|
170
|
+
# Exempt: merge/autosquash/revert generated subjects skip the check.
|
|
171
|
+
exit 0
|
|
172
|
+
;;
|
|
173
|
+
esac
|
|
174
|
+
|
|
175
|
+
if ! [[ "$SUBJECT" =~ $CONV_COMMIT_RE ]]; then
|
|
176
|
+
echo -e ""
|
|
177
|
+
echo -e "${RED}COMMIT BLOCKED: subject must be Conventional Commit${NC}"
|
|
178
|
+
echo -e ""
|
|
179
|
+
echo -e "${RED}Your subject:${NC} ${YELLOW}${SUBJECT}${NC}"
|
|
180
|
+
echo -e ""
|
|
181
|
+
echo -e "${GREEN}Expected:${NC} <type>[optional scope][!]: <description>"
|
|
182
|
+
echo -e " types: build chore ci docs feat fix perf refactor revert style test"
|
|
183
|
+
echo -e " e.g. feat(hooks): add native gate"
|
|
184
|
+
echo -e ""
|
|
185
|
+
exit 1
|
|
186
|
+
fi
|
|
187
|
+
|
|
30
188
|
exit 0
|
|
@@ -1,32 +1,40 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
2
|
+
"pre-commit": {
|
|
3
|
+
"version": 1,
|
|
4
|
+
"sha256": "811f34ce57517e129554bc9c09801a66c0207332cd3e7f2950db43a40580e914",
|
|
5
|
+
"historical": [
|
|
6
|
+
{
|
|
7
|
+
"sha256": "811f34ce57517e129554bc9c09801a66c0207332cd3e7f2950db43a40580e914",
|
|
8
|
+
"firstCommit": "5587b3b9b9ed3c86809c0a3d89a850dd2bce7e1a"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
"pre-push": {
|
|
13
|
+
"version": 2,
|
|
14
|
+
"sha256": "3dad58303d81fc513a35339083438a78aa483fcd28e3377f006bd26e8ce76a6d",
|
|
15
|
+
"historical": [
|
|
16
|
+
{
|
|
17
|
+
"sha256": "7de58640aeef33085a49f31f1d9d0c8bacde0069d6d3265ae41aa8d3cd14d7a5",
|
|
18
|
+
"firstCommit": "5587b3b9b9ed3c86809c0a3d89a850dd2bce7e1a"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"sha256": "3dad58303d81fc513a35339083438a78aa483fcd28e3377f006bd26e8ce76a6d",
|
|
22
|
+
"firstCommit": "a35e09d39a5394d6380499d1ed73fdf22f08c829"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"commit-msg": {
|
|
27
|
+
"version": 2,
|
|
28
|
+
"sha256": "127fb8bfebd81d6b06e6f04bdf1be0036a3a224268ac54f11784043f55796a18",
|
|
29
|
+
"historical": [
|
|
30
|
+
{
|
|
31
|
+
"sha256": "1c23a60cd4ba7f6bc666da400b5d2971c4294782c8d9ce41543e7815de11a1d6",
|
|
32
|
+
"firstCommit": "5587b3b9b9ed3c86809c0a3d89a850dd2bce7e1a"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"sha256": "127fb8bfebd81d6b06e6f04bdf1be0036a3a224268ac54f11784043f55796a18",
|
|
36
|
+
"firstCommit": "65cb02e7efc5581303e2b20727acf07e3d88de65"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
32
40
|
}
|
package/assets/hooks/pre-push
CHANGED
|
@@ -1,20 +1,37 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
#
|
|
3
|
-
#
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# PRE-PUSH: Native CI checks via javi-forge
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# Runs the quick native gate (validate + coverage) with:
|
|
6
|
+
# javi-forge ci --quick --no-docker --no-security --no-ci-ghagga
|
|
7
|
+
# This is deliberately NOT the full containerized run: the native path
|
|
8
|
+
# sidesteps the pre-push container EACCES failure while keeping fail-closed
|
|
9
|
+
# image-gate enforcement where it belongs — the ci gate layer REFUSES any
|
|
10
|
+
# blocking image gate under --no-docker (see src/commands/ci.ts runGates).
|
|
11
|
+
# Requires: npm install -g javi-forge (npx fallback used when absent).
|
|
12
|
+
# To skip: git push --no-verify
|
|
13
|
+
# Target: under ~180s. Elapsed time is reported below.
|
|
14
|
+
# =============================================================================
|
|
15
|
+
|
|
4
16
|
set -e
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if command -v javi-forge &>/dev/null; then
|
|
12
|
-
javi-forge ci
|
|
17
|
+
|
|
18
|
+
_start=$SECONDS
|
|
19
|
+
|
|
20
|
+
echo "PRE-PUSH: Running native CI checks (validate + coverage)..."
|
|
21
|
+
if command -v javi-forge >/dev/null 2>&1; then
|
|
22
|
+
javi-forge ci --quick --no-docker --no-security --no-ci-ghagga
|
|
13
23
|
else
|
|
14
|
-
|
|
24
|
+
npx javi-forge ci --quick --no-docker --no-security --no-ci-ghagga
|
|
15
25
|
fi || {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
_elapsed=$((SECONDS - _start))
|
|
27
|
+
echo ""
|
|
28
|
+
echo "CI FAILED after ${_elapsed}s — push aborted. Fix the issues above before pushing."
|
|
29
|
+
echo "To skip: git push --no-verify"
|
|
30
|
+
exit 1
|
|
20
31
|
}
|
|
32
|
+
|
|
33
|
+
_elapsed=$((SECONDS - _start))
|
|
34
|
+
echo "PRE-PUSH: passed in ${_elapsed}s (target: 180s)"
|
|
35
|
+
if [ "$_elapsed" -gt 300 ]; then
|
|
36
|
+
echo " WARNING: pre-push took ${_elapsed}s, well over the 180s target."
|
|
37
|
+
fi
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "javi-forge",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"description": "Project scaffolding and AI-ready CI bootstrap",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"test:watch": "vitest",
|
|
17
17
|
"test:coverage": "vitest run --coverage",
|
|
18
18
|
"test:mutation": "stryker run",
|
|
19
|
-
"test:hooks": "bash
|
|
19
|
+
"test:hooks": "bash assets/hooks/commit-msg.test.sh",
|
|
20
20
|
"lint": "biome check src/",
|
|
21
21
|
"lint:fix": "biome check --write src/",
|
|
22
22
|
"format": "biome format --write src/",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"!dist/__integration__/**",
|
|
30
30
|
"!dist/e2e/**",
|
|
31
31
|
"assets/",
|
|
32
|
+
"!assets/**/*.test.sh",
|
|
32
33
|
"ci-local/",
|
|
33
34
|
"modules/",
|
|
34
35
|
"templates/",
|
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# =============================================================================
|
|
3
|
-
# COMMIT-MSG TEST SUITE
|
|
4
|
-
# =============================================================================
|
|
5
|
-
# Valida que el hook commit-msg bloquea AI attribution en TODAS las variantes
|
|
6
|
-
# conocidas, incluyendo bypasses identificados en auditoría 2026-05-17.
|
|
7
|
-
#
|
|
8
|
-
# Uso:
|
|
9
|
-
# ./commit-msg.test.sh
|
|
10
|
-
#
|
|
11
|
-
# Exit code 0 = todos los tests pasan
|
|
12
|
-
# Exit code 1 = al menos un test falla
|
|
13
|
-
# =============================================================================
|
|
14
|
-
|
|
15
|
-
set -u
|
|
16
|
-
|
|
17
|
-
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
18
|
-
HOOK="$SCRIPT_DIR/commit-msg"
|
|
19
|
-
|
|
20
|
-
if [ ! -x "$HOOK" ]; then
|
|
21
|
-
echo "ERROR: commit-msg hook not found or not executable: $HOOK"
|
|
22
|
-
exit 1
|
|
23
|
-
fi
|
|
24
|
-
|
|
25
|
-
PASS=0
|
|
26
|
-
FAIL=0
|
|
27
|
-
FAILED_TESTS=()
|
|
28
|
-
|
|
29
|
-
# Garantiza limpieza de tmpfiles aún si el hook revienta con set -u, etc.
|
|
30
|
-
TMP_FILES=()
|
|
31
|
-
cleanup() {
|
|
32
|
-
if [ ${#TMP_FILES[@]} -gt 0 ]; then
|
|
33
|
-
rm -f "${TMP_FILES[@]}"
|
|
34
|
-
fi
|
|
35
|
-
}
|
|
36
|
-
trap cleanup EXIT INT TERM
|
|
37
|
-
|
|
38
|
-
make_tmp() {
|
|
39
|
-
local f
|
|
40
|
-
f=$(mktemp)
|
|
41
|
-
TMP_FILES+=("$f")
|
|
42
|
-
printf "%s" "$1" > "$f"
|
|
43
|
-
printf "%s" "$f"
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
# expect_block <description> <message>
|
|
47
|
-
# El hook debe BLOQUEAR este mensaje (exit != 0)
|
|
48
|
-
expect_block() {
|
|
49
|
-
local desc="$1"
|
|
50
|
-
local msg="$2"
|
|
51
|
-
local tmpfile
|
|
52
|
-
tmpfile=$(make_tmp "$msg")
|
|
53
|
-
if "$HOOK" "$tmpfile" >/dev/null 2>&1; then
|
|
54
|
-
FAIL=$((FAIL + 1))
|
|
55
|
-
FAILED_TESTS+=("[NOT BLOCKED] $desc | msg: '$msg'")
|
|
56
|
-
echo " FAIL: $desc (should block but passed): '$msg'"
|
|
57
|
-
else
|
|
58
|
-
PASS=$((PASS + 1))
|
|
59
|
-
fi
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
# expect_pass <description> <message>
|
|
63
|
-
# El hook debe PERMITIR este mensaje (exit 0)
|
|
64
|
-
expect_pass() {
|
|
65
|
-
local desc="$1"
|
|
66
|
-
local msg="$2"
|
|
67
|
-
local tmpfile
|
|
68
|
-
tmpfile=$(make_tmp "$msg")
|
|
69
|
-
if "$HOOK" "$tmpfile" >/dev/null 2>&1; then
|
|
70
|
-
PASS=$((PASS + 1))
|
|
71
|
-
else
|
|
72
|
-
FAIL=$((FAIL + 1))
|
|
73
|
-
FAILED_TESTS+=("[FALSE POSITIVE] $desc | msg: '$msg'")
|
|
74
|
-
echo " FAIL: $desc (should pass but blocked): '$msg'"
|
|
75
|
-
fi
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
echo "=== Running commit-msg hook test suite ==="
|
|
79
|
-
echo ""
|
|
80
|
-
|
|
81
|
-
# ─── Cases que DEBEN bloquear ────────────────────────────────────────
|
|
82
|
-
|
|
83
|
-
# Co-authored-by con cada proveedor
|
|
84
|
-
expect_block "Co-authored-by Claude" "feat: add foo
|
|
85
|
-
|
|
86
|
-
Co-authored-by: Claude <claude@anthropic.com>"
|
|
87
|
-
|
|
88
|
-
expect_block "Co-authored-by claude lowercase" "feat: bar
|
|
89
|
-
|
|
90
|
-
co-authored-by: claude <c@a.com>"
|
|
91
|
-
|
|
92
|
-
expect_block "Co-authored-by double space" "feat: baz
|
|
93
|
-
|
|
94
|
-
Co-authored-by: Claude <c@a.com>"
|
|
95
|
-
|
|
96
|
-
expect_block "Co-authored-by GPT" "feat: x
|
|
97
|
-
|
|
98
|
-
Co-authored-by: GPT-4 <g@o.com>"
|
|
99
|
-
|
|
100
|
-
expect_block "Co-authored-by Copilot" "feat: y
|
|
101
|
-
|
|
102
|
-
Co-authored-by: GitHub Copilot <c@gh.com>"
|
|
103
|
-
|
|
104
|
-
expect_block "Co-authored-by ai standalone" "feat: z
|
|
105
|
-
|
|
106
|
-
Co-authored-by: AI Assistant <a@b.com>"
|
|
107
|
-
|
|
108
|
-
# Made/Generated/Written/Created by/with X — single space
|
|
109
|
-
expect_block "Made by Claude" "feat: add Made by Claude"
|
|
110
|
-
expect_block "Made with Claude" "feat: Made with Claude"
|
|
111
|
-
expect_block "Generated by GPT" "feat: Generated by GPT"
|
|
112
|
-
expect_block "Written with OpenAI" "fix: Written with OpenAI"
|
|
113
|
-
expect_block "Created by Anthropic" "feat: Created by Anthropic"
|
|
114
|
-
expect_block "Assisted by AI" "feat: Assisted by AI"
|
|
115
|
-
expect_block "Built with Gemini" "feat: Built with Gemini"
|
|
116
|
-
expect_block "Developed with Copilot" "feat: Developed with Copilot"
|
|
117
|
-
expect_block "Authored by LLM" "feat: Authored by LLM"
|
|
118
|
-
|
|
119
|
-
# Made/Generated/etc — DOUBLE space (bypass identificado en audit)
|
|
120
|
-
expect_block "Made by Claude (double space)" "feat: Made by Claude"
|
|
121
|
-
expect_block "Generated with GPT (double space)" "fix: Generated with GPT"
|
|
122
|
-
expect_block "Made by\tClaude (tab)" "$(printf 'feat: Made by\tClaude')"
|
|
123
|
-
|
|
124
|
-
# With help from X
|
|
125
|
-
expect_block "With help from Claude" "feat: x
|
|
126
|
-
|
|
127
|
-
With help from Claude"
|
|
128
|
-
expect_block "with help from GPT-4" "feat: with help from GPT-4"
|
|
129
|
-
|
|
130
|
-
# X-assisted variants (bypass identificado)
|
|
131
|
-
expect_block "anthropic-assisted" "feat: anthropic-assisted refactor"
|
|
132
|
-
expect_block "openai-assisted" "feat: openai-assisted change"
|
|
133
|
-
expect_block "ai-assisted" "feat: ai-assisted commit"
|
|
134
|
-
expect_block "Claude assisted (space)" "feat: Claude assisted work"
|
|
135
|
-
expect_block "LLM_assisted (underscore)" "feat: llm_assisted feature"
|
|
136
|
-
|
|
137
|
-
# Tool branding
|
|
138
|
-
expect_block "claude code" "feat: built with claude code"
|
|
139
|
-
expect_block "claude-code" "feat: via claude-code"
|
|
140
|
-
expect_block "claude_code" "feat: claude_code generated"
|
|
141
|
-
expect_block "GitHub Copilot" "feat: GitHub Copilot helped"
|
|
142
|
-
expect_block "Microsoft Copilot" "feat: Microsoft Copilot did this"
|
|
143
|
-
|
|
144
|
-
# Claude model variants (bypass identificado)
|
|
145
|
-
expect_block "claude opus (space)" "feat: claude opus tested"
|
|
146
|
-
expect_block "claude-sonnet (dash)" "feat: claude-sonnet used"
|
|
147
|
-
expect_block "claude_haiku (underscore)" "feat: claude_haiku approach"
|
|
148
|
-
expect_block "Claude3.5" "feat: Claude3.5 wrote this"
|
|
149
|
-
expect_block "claude4.7" "feat: claude4.7 contributed"
|
|
150
|
-
expect_block "Claude 4" "feat: Claude 4 logic"
|
|
151
|
-
|
|
152
|
-
# GPT model variants
|
|
153
|
-
expect_block "gpt-4" "feat: gpt-4 reviewed"
|
|
154
|
-
expect_block "gpt 5" "feat: gpt 5 wrote"
|
|
155
|
-
expect_block "gpt_3" "feat: gpt_3 helped"
|
|
156
|
-
expect_block "GPT4" "feat: GPT4 was used"
|
|
157
|
-
|
|
158
|
-
# ChatGPT
|
|
159
|
-
expect_block "ChatGPT" "feat: ChatGPT generated"
|
|
160
|
-
expect_block "chatgpt" "feat: powered by chatgpt"
|
|
161
|
-
|
|
162
|
-
# Emails
|
|
163
|
-
expect_block "@anthropic.com" "feat: x
|
|
164
|
-
|
|
165
|
-
Author: foo@anthropic.com"
|
|
166
|
-
expect_block "@openai.com" "feat: y
|
|
167
|
-
|
|
168
|
-
Co-authored-by: bot@openai.com"
|
|
169
|
-
expect_block "noreply@anthropic" "feat: noreply@anthropic.example"
|
|
170
|
-
|
|
171
|
-
# ─── Bypasses identificados en review 2026-05-17 (post-hardening) ─────
|
|
172
|
-
|
|
173
|
-
# Markdown formatting alrededor del proveedor
|
|
174
|
-
expect_block "Made by **Claude** (markdown bold)" "feat: Made by **Claude**"
|
|
175
|
-
expect_block "Made by __GPT__ (markdown italic)" "feat: Made by __GPT__"
|
|
176
|
-
expect_block "Made by \`OpenAI\` (markdown code)" "feat: Made by \`OpenAI\`"
|
|
177
|
-
expect_block "Made by ~~Gemini~~ (strikethrough)" "feat: Made by ~~Gemini~~"
|
|
178
|
-
|
|
179
|
-
# Puntuación entre verbo y proveedor (no whitespace)
|
|
180
|
-
expect_block "Made by.Claude" "feat: Made by.Claude"
|
|
181
|
-
expect_block "Made by,Claude" "feat: Made by,Claude"
|
|
182
|
-
expect_block "Made by:Claude" "feat: Made by:Claude"
|
|
183
|
-
expect_block "Made by;Claude" "feat: Made by;Claude"
|
|
184
|
-
|
|
185
|
-
# Newline entre verbo y proveedor
|
|
186
|
-
expect_block "Made by newline Claude" "$(printf 'feat: Made by\nClaude')"
|
|
187
|
-
|
|
188
|
-
# Emoji separator
|
|
189
|
-
expect_block "Made by emoji Claude" "feat: Made by 🤖 Claude"
|
|
190
|
-
expect_block "Generated by emoji GPT" "feat: Generated by ✨ GPT"
|
|
191
|
-
|
|
192
|
-
# Unicode invisible chars (ZWSP, RLM, NBSP)
|
|
193
|
-
expect_block "Made by ZWSP Claude" "$(printf 'feat: Made byClaude')"
|
|
194
|
-
expect_block "Made by NBSP Claude" "$(printf 'feat: Made by Claude')"
|
|
195
|
-
expect_block "Made by RLM Claude" "$(printf 'feat: Made byClaude')"
|
|
196
|
-
|
|
197
|
-
# "thanks/powered by/courtesy of" alternativas
|
|
198
|
-
expect_block "thanks to Claude" "feat: thanks to Claude for help"
|
|
199
|
-
expect_block "powered by GPT" "feat: powered by GPT"
|
|
200
|
-
expect_block "courtesy of OpenAI" "feat: courtesy of OpenAI"
|
|
201
|
-
|
|
202
|
-
# AI IDEs / herramientas
|
|
203
|
-
expect_block "Cursor IDE" "feat: built with Cursor"
|
|
204
|
-
expect_block "Windsurf" "feat: written with Windsurf"
|
|
205
|
-
expect_block "Codeium" "feat: codeium suggested this"
|
|
206
|
-
expect_block "Cody" "feat: with Cody help"
|
|
207
|
-
expect_block "Aider" "feat: aider generated"
|
|
208
|
-
|
|
209
|
-
# Co-authored-by sin espacio antes de AI (bypass del review Sonnet)
|
|
210
|
-
expect_block "Co-authored-by AIAssistant" "feat: x
|
|
211
|
-
|
|
212
|
-
Co-authored-by: AIAssistant <a@b.com>"
|
|
213
|
-
|
|
214
|
-
# Co-authored-by Copilot standalone (sin GitHub/Microsoft prefix)
|
|
215
|
-
expect_block "Co-authored-by bare Copilot" "feat: x
|
|
216
|
-
|
|
217
|
-
Co-authored-by: Copilot <c@gh.com>"
|
|
218
|
-
|
|
219
|
-
# Verbos alternativos
|
|
220
|
-
expect_block "produced by Claude" "feat: produced by Claude"
|
|
221
|
-
expect_block "coded by GPT" "feat: coded by GPT"
|
|
222
|
-
expect_block "crafted with AI" "feat: crafted with AI"
|
|
223
|
-
|
|
224
|
-
# Using/via como alternativas a by/with
|
|
225
|
-
expect_block "made using Claude" "feat: made using Claude"
|
|
226
|
-
expect_block "generated via GPT" "feat: generated via GPT"
|
|
227
|
-
|
|
228
|
-
# Full-width characters (NFKC normalization)
|
|
229
|
-
expect_block "Made by Claude (fullwidth)" "feat: Made by Claude"
|
|
230
|
-
|
|
231
|
-
# ─── Bypasses adicionales identificados en audit-2 (2026-05-17 review B) ──
|
|
232
|
-
|
|
233
|
-
# Combining diacritics (U+0300-U+036F) — Ćlaude = C + U+0301
|
|
234
|
-
expect_block "Ćlaude (combining acute)" "$(printf 'feat: Made by Ćlaude')"
|
|
235
|
-
expect_block "Cláude (combining grave)" "$(printf 'feat: Made by Clàude')"
|
|
236
|
-
|
|
237
|
-
# Verbos expandidos — sólo aplican si hay (by|with|using|via) después del verbo.
|
|
238
|
-
# "asked Claude to ..." NO bloquea por diseño: el riesgo de false positive en
|
|
239
|
-
# nombres personales o conversaciones legítimas es alto. Si necesitás defensa
|
|
240
|
-
# adversarial usá signed commits.
|
|
241
|
-
expect_block "helped by Claude" "feat: helped by Claude"
|
|
242
|
-
expect_block "wrote with GPT" "feat: wrote with GPT-4"
|
|
243
|
-
expect_block "prompted via AI" "feat: prompted via AI"
|
|
244
|
-
expect_block "fixed by Copilot" "feat: fixed by Copilot"
|
|
245
|
-
expect_block "refactored with Cursor" "feat: refactored with Cursor"
|
|
246
|
-
|
|
247
|
-
# Cases que DOCUMENTADAMENTE NO bloqueamos (bare provider sin verb-prepositional):
|
|
248
|
-
# - "asked Claude to refactor" (FP risk: pidió a una persona llamada Claude)
|
|
249
|
-
# - "I used Claude to write" (FP risk: usé al usuario Claude para escribir)
|
|
250
|
-
# - "claude helped me debug" (FP risk: Claude la persona ayudó a debuggear)
|
|
251
|
-
# Estos son límites aceptados; ver header del hook.
|
|
252
|
-
expect_pass "asked Claude (person, not attribution)" "feat: asked Claude to review the PR"
|
|
253
|
-
expect_pass "user Claude in seed" "feat: configure user Claude in fixtures"
|
|
254
|
-
|
|
255
|
-
# ─── Cases que DEBEN PASAR (false positive checks) ────────────────────
|
|
256
|
-
|
|
257
|
-
expect_pass "Simple feat" "feat: add user profile endpoint"
|
|
258
|
-
expect_pass "Simple fix" "fix: handle empty array in parser"
|
|
259
|
-
expect_pass "Refactor" "refactor: extract auth into middleware"
|
|
260
|
-
expect_pass "Docs" "docs: clarify install instructions"
|
|
261
|
-
expect_pass "Multi-line valid commit" "feat: add search
|
|
262
|
-
|
|
263
|
-
Implements full-text search using PostgreSQL FTS.
|
|
264
|
-
Includes integration tests."
|
|
265
|
-
expect_pass "Author named Claude (not attribution)" "feat: add user named Claude in seed data"
|
|
266
|
-
expect_pass "AI as acronym in product name" "feat: integrate with PaiAi API gateway"
|
|
267
|
-
expect_pass "Word 'made' without AI context" "feat: changes I made last week to the parser"
|
|
268
|
-
|
|
269
|
-
# ─── Summary ──────────────────────────────────────────────────────────
|
|
270
|
-
|
|
271
|
-
echo ""
|
|
272
|
-
echo "=== Results ==="
|
|
273
|
-
echo " Passed: $PASS"
|
|
274
|
-
echo " Failed: $FAIL"
|
|
275
|
-
echo ""
|
|
276
|
-
|
|
277
|
-
if [ $FAIL -gt 0 ]; then
|
|
278
|
-
echo "Failed tests:"
|
|
279
|
-
for t in "${FAILED_TESTS[@]}"; do
|
|
280
|
-
echo " - $t"
|
|
281
|
-
done
|
|
282
|
-
exit 1
|
|
283
|
-
fi
|
|
284
|
-
|
|
285
|
-
echo "All tests passed."
|
|
286
|
-
exit 0
|