reviewflow 3.8.1 → 3.10.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/CHANGELOG.md +16 -0
- package/README.md +2 -0
- package/dist/config/projectConfig.d.ts +3 -5
- package/dist/config/projectConfig.d.ts.map +1 -1
- package/dist/config/projectConfig.js +19 -1
- package/dist/config/projectConfig.js.map +1 -1
- package/dist/entities/modelRouting/modelRouting.gateway.d.ts +5 -0
- package/dist/entities/modelRouting/modelRouting.gateway.d.ts.map +1 -0
- package/dist/entities/modelRouting/modelRouting.gateway.js +2 -0
- package/dist/entities/modelRouting/modelRouting.gateway.js.map +1 -0
- package/dist/entities/modelRouting/modelRouting.schema.d.ts +13 -0
- package/dist/entities/modelRouting/modelRouting.schema.d.ts.map +1 -0
- package/dist/entities/modelRouting/modelRouting.schema.js +7 -0
- package/dist/entities/modelRouting/modelRouting.schema.js.map +1 -0
- package/dist/entities/tokenUsage/tokenUsage.gateway.d.ts +6 -0
- package/dist/entities/tokenUsage/tokenUsage.gateway.d.ts.map +1 -0
- package/dist/entities/tokenUsage/tokenUsage.gateway.js +2 -0
- package/dist/entities/tokenUsage/tokenUsage.gateway.js.map +1 -0
- package/dist/entities/tokenUsage/tokenUsage.schema.d.ts +30 -0
- package/dist/entities/tokenUsage/tokenUsage.schema.d.ts.map +1 -0
- package/dist/entities/tokenUsage/tokenUsage.schema.js +19 -0
- package/dist/entities/tokenUsage/tokenUsage.schema.js.map +1 -0
- package/dist/frameworks/claude/claudeInvoker.d.ts +4 -0
- package/dist/frameworks/claude/claudeInvoker.d.ts.map +1 -1
- package/dist/frameworks/claude/claudeInvoker.js +86 -27
- package/dist/frameworks/claude/claudeInvoker.js.map +1 -1
- package/dist/frameworks/claude/streamJsonParser.d.ts +44 -0
- package/dist/frameworks/claude/streamJsonParser.d.ts.map +1 -0
- package/dist/frameworks/claude/streamJsonParser.js +96 -0
- package/dist/frameworks/claude/streamJsonParser.js.map +1 -0
- package/dist/frameworks/queue/pQueueAdapter.d.ts +2 -0
- package/dist/frameworks/queue/pQueueAdapter.d.ts.map +1 -1
- package/dist/frameworks/queue/pQueueAdapter.js.map +1 -1
- package/dist/frameworks/settings/runtimeSettings.d.ts +1 -1
- package/dist/frameworks/settings/runtimeSettings.d.ts.map +1 -1
- package/dist/frameworks/settings/runtimeSettings.js +1 -1
- package/dist/frameworks/settings/runtimeSettings.js.map +1 -1
- package/dist/interface-adapters/gateways/projectConfig/routingPolicy.projectConfig.gateway.d.ts +6 -0
- package/dist/interface-adapters/gateways/projectConfig/routingPolicy.projectConfig.gateway.d.ts.map +1 -0
- package/dist/interface-adapters/gateways/projectConfig/routingPolicy.projectConfig.gateway.js +8 -0
- package/dist/interface-adapters/gateways/projectConfig/routingPolicy.projectConfig.gateway.js.map +1 -0
- package/dist/interface-adapters/gateways/tokenUsage/tokenUsage.filesystem.gateway.d.ts +7 -0
- package/dist/interface-adapters/gateways/tokenUsage/tokenUsage.filesystem.gateway.d.ts.map +1 -0
- package/dist/interface-adapters/gateways/tokenUsage/tokenUsage.filesystem.gateway.js +37 -0
- package/dist/interface-adapters/gateways/tokenUsage/tokenUsage.filesystem.gateway.js.map +1 -0
- package/dist/tests/factories/routingPolicy.factory.d.ts +5 -0
- package/dist/tests/factories/routingPolicy.factory.d.ts.map +1 -0
- package/dist/tests/factories/routingPolicy.factory.js +10 -0
- package/dist/tests/factories/routingPolicy.factory.js.map +1 -0
- package/dist/tests/factories/tokenUsage.factory.d.ts +8 -0
- package/dist/tests/factories/tokenUsage.factory.d.ts.map +1 -0
- package/dist/tests/factories/tokenUsage.factory.js +28 -0
- package/dist/tests/factories/tokenUsage.factory.js.map +1 -0
- package/dist/tests/stubs/tokenUsage.stub.d.ts +11 -0
- package/dist/tests/stubs/tokenUsage.stub.d.ts.map +1 -0
- package/dist/tests/stubs/tokenUsage.stub.js +19 -0
- package/dist/tests/stubs/tokenUsage.stub.js.map +1 -0
- package/dist/tests/units/config/projectConfig.test.js +47 -0
- package/dist/tests/units/config/projectConfig.test.js.map +1 -1
- package/dist/tests/units/frameworks/claude/streamJsonParser.test.d.ts +2 -0
- package/dist/tests/units/frameworks/claude/streamJsonParser.test.d.ts.map +1 -0
- package/dist/tests/units/frameworks/claude/streamJsonParser.test.js +83 -0
- package/dist/tests/units/frameworks/claude/streamJsonParser.test.js.map +1 -0
- package/dist/tests/units/interface-adapters/gateways/projectConfig/routingPolicy.projectConfig.gateway.test.d.ts +2 -0
- package/dist/tests/units/interface-adapters/gateways/projectConfig/routingPolicy.projectConfig.gateway.test.d.ts.map +1 -0
- package/dist/tests/units/interface-adapters/gateways/projectConfig/routingPolicy.projectConfig.gateway.test.js +44 -0
- package/dist/tests/units/interface-adapters/gateways/projectConfig/routingPolicy.projectConfig.gateway.test.js.map +1 -0
- package/dist/tests/units/interface-adapters/gateways/tokenUsage/tokenUsage.filesystem.gateway.test.d.ts +2 -0
- package/dist/tests/units/interface-adapters/gateways/tokenUsage/tokenUsage.filesystem.gateway.test.d.ts.map +1 -0
- package/dist/tests/units/interface-adapters/gateways/tokenUsage/tokenUsage.filesystem.gateway.test.js +57 -0
- package/dist/tests/units/interface-adapters/gateways/tokenUsage/tokenUsage.filesystem.gateway.test.js.map +1 -0
- package/dist/tests/units/usecases/selectModelForReview/selectModelForReview.usecase.test.d.ts +2 -0
- package/dist/tests/units/usecases/selectModelForReview/selectModelForReview.usecase.test.d.ts.map +1 -0
- package/dist/tests/units/usecases/selectModelForReview/selectModelForReview.usecase.test.js +55 -0
- package/dist/tests/units/usecases/selectModelForReview/selectModelForReview.usecase.test.js.map +1 -0
- package/dist/tests/units/usecases/summarizeTokenUsage/summarizeTokenUsage.usecase.test.d.ts +2 -0
- package/dist/tests/units/usecases/summarizeTokenUsage/summarizeTokenUsage.usecase.test.d.ts.map +1 -0
- package/dist/tests/units/usecases/summarizeTokenUsage/summarizeTokenUsage.usecase.test.js +64 -0
- package/dist/tests/units/usecases/summarizeTokenUsage/summarizeTokenUsage.usecase.test.js.map +1 -0
- package/dist/tests/units/usecases/trackTokenUsage/trackTokenUsage.usecase.test.d.ts +2 -0
- package/dist/tests/units/usecases/trackTokenUsage/trackTokenUsage.usecase.test.d.ts.map +1 -0
- package/dist/tests/units/usecases/trackTokenUsage/trackTokenUsage.usecase.test.js +26 -0
- package/dist/tests/units/usecases/trackTokenUsage/trackTokenUsage.usecase.test.js.map +1 -0
- package/dist/usecases/selectModelForReview/selectModelForReview.usecase.d.ts +15 -0
- package/dist/usecases/selectModelForReview/selectModelForReview.usecase.d.ts.map +1 -0
- package/dist/usecases/selectModelForReview/selectModelForReview.usecase.js +16 -0
- package/dist/usecases/selectModelForReview/selectModelForReview.usecase.js.map +1 -0
- package/dist/usecases/summarizeTokenUsage/summarizeTokenUsage.usecase.d.ts +23 -0
- package/dist/usecases/summarizeTokenUsage/summarizeTokenUsage.usecase.d.ts.map +1 -0
- package/dist/usecases/summarizeTokenUsage/summarizeTokenUsage.usecase.js +38 -0
- package/dist/usecases/summarizeTokenUsage/summarizeTokenUsage.usecase.js.map +1 -0
- package/dist/usecases/trackTokenUsage/trackTokenUsage.usecase.d.ts +8 -0
- package/dist/usecases/trackTokenUsage/trackTokenUsage.usecase.d.ts.map +1 -0
- package/dist/usecases/trackTokenUsage/trackTokenUsage.usecase.js +10 -0
- package/dist/usecases/trackTokenUsage/trackTokenUsage.usecase.js.map +1 -0
- package/package.json +3 -3
- package/scripts/hooks/enforce-dependency-rule.sh +61 -0
- package/scripts/hooks/enforce-gateway-port-purity.sh +35 -0
- package/scripts/hooks/enforce-presenter-class.sh +34 -0
- package/scripts/hooks/no-barrel-exports.sh +23 -0
- package/scripts/hooks/parse-json.sh +17 -0
- package/scripts/hooks/pre-commit-gate.sh +29 -0
- package/scripts/hooks/protect-main-branch.sh +28 -0
- package/scripts/hooks/protect-main-push.sh +38 -0
- package/scripts/hooks/require-spec.sh +58 -0
- package/scripts/hooks/session-context.sh +47 -0
- package/scripts/hooks/tests/run-tests.sh +189 -0
- package/scripts/hooks/tests/test-architecture-hooks.sh +163 -0
- package/scripts/hooks/verify-spec-updated.sh +75 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Blocks creation or editing of barrel export files (index.ts).
|
|
5
|
+
# Used as PreToolUse hook on Write|Edit.
|
|
6
|
+
# Exit 0 = allow, Exit 2 = block with feedback to Claude.
|
|
7
|
+
|
|
8
|
+
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
INPUT=$(cat)
|
|
10
|
+
FILE_PATH=$(echo "$INPUT" | "$HOOK_DIR/parse-json.sh" tool_input.file_path)
|
|
11
|
+
|
|
12
|
+
is_barrel_export() {
|
|
13
|
+
local filename
|
|
14
|
+
filename=$(basename "$FILE_PATH")
|
|
15
|
+
[[ "$filename" == "index.ts" || "$filename" == "index.js" ]]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if is_barrel_export; then
|
|
19
|
+
echo "Barrel exports are forbidden. Import directly from the source file instead of using index.ts." >&2
|
|
20
|
+
exit 2
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
exit 0
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# Extracts a value from JSON on stdin using a dot-separated path.
|
|
4
|
+
# Usage: echo '{"a":{"b":"c"}}' | parse-json.sh a.b
|
|
5
|
+
# Returns empty string if path not found. No external dependencies beyond python3.
|
|
6
|
+
|
|
7
|
+
python3 -c "
|
|
8
|
+
import json, sys
|
|
9
|
+
data = json.load(sys.stdin)
|
|
10
|
+
path = sys.argv[1].split('.')
|
|
11
|
+
for key in path:
|
|
12
|
+
if isinstance(data, dict) and key in data:
|
|
13
|
+
data = data[key]
|
|
14
|
+
else:
|
|
15
|
+
sys.exit(0)
|
|
16
|
+
print(data if data is not None else '')
|
|
17
|
+
" "$1"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Blocks git commit if tests fail.
|
|
5
|
+
# Used as PreToolUse hook on Bash(git commit *).
|
|
6
|
+
# Exit 0 = allow, Exit 2 = block with feedback to Claude.
|
|
7
|
+
|
|
8
|
+
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
INPUT=$(cat)
|
|
10
|
+
COMMAND=$(echo "$INPUT" | "$HOOK_DIR/parse-json.sh" tool_input.command)
|
|
11
|
+
|
|
12
|
+
is_commit_command() {
|
|
13
|
+
echo "$COMMAND" | grep -qE "git commit"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if ! is_commit_command; then
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$HOOK_DIR/../.." && pwd)}"
|
|
21
|
+
|
|
22
|
+
cd "$PROJECT_DIR"
|
|
23
|
+
|
|
24
|
+
if yarn test:ci 2>&1; then
|
|
25
|
+
exit 0
|
|
26
|
+
else
|
|
27
|
+
echo "Tests failed. Fix them before committing." >&2
|
|
28
|
+
exit 2
|
|
29
|
+
fi
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Blocks git commit on main/master branch.
|
|
5
|
+
# Used as PreToolUse hook on Bash(git commit *).
|
|
6
|
+
# Exit 0 = allow, Exit 2 = block with feedback to Claude.
|
|
7
|
+
|
|
8
|
+
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
INPUT=$(cat)
|
|
10
|
+
COMMAND=$(echo "$INPUT" | "$HOOK_DIR/parse-json.sh" tool_input.command)
|
|
11
|
+
|
|
12
|
+
is_commit_command() {
|
|
13
|
+
echo "$COMMAND" | grep -qE "git commit"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if ! is_commit_command; then
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$HOOK_DIR/../.." && pwd)}"
|
|
21
|
+
CURRENT_BRANCH=$(cd "$PROJECT_DIR" && git branch --show-current 2>/dev/null || true)
|
|
22
|
+
|
|
23
|
+
if [[ "$CURRENT_BRANCH" == "main" || "$CURRENT_BRANCH" == "master" ]]; then
|
|
24
|
+
echo "You are on '$CURRENT_BRANCH'. Create a feature branch or use /worktree add <name> first." >&2
|
|
25
|
+
exit 2
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
exit 0
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Blocks git push to main/master branch and force pushes.
|
|
5
|
+
# Used as PreToolUse hook on Bash(git push *).
|
|
6
|
+
# Exit 0 = allow, Exit 2 = block with feedback to Claude.
|
|
7
|
+
|
|
8
|
+
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
INPUT=$(cat)
|
|
10
|
+
COMMAND=$(echo "$INPUT" | "$HOOK_DIR/parse-json.sh" tool_input.command)
|
|
11
|
+
|
|
12
|
+
is_push_command() {
|
|
13
|
+
echo "$COMMAND" | grep -qE "git push"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if ! is_push_command; then
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
is_force_push() {
|
|
21
|
+
echo "$COMMAND" | grep -qE "\-\-force|\-f"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if is_force_push; then
|
|
25
|
+
echo "Force push is forbidden. Push normally to your feature branch instead." >&2
|
|
26
|
+
exit 2
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
pushes_to_protected_branch() {
|
|
30
|
+
echo "$COMMAND" | grep -qE "git push.*(origin\s+(main|master)|origin/main|origin/master)"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if pushes_to_protected_branch; then
|
|
34
|
+
echo "Push to main/master is forbidden. Push to your feature branch instead." >&2
|
|
35
|
+
exit 2
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
exit 0
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Blocks feature-implementer agent if no spec file exists or spec is incomplete.
|
|
5
|
+
# Used as PreToolUse hook on Agent.
|
|
6
|
+
# Exit 0 = allow, Exit 2 = block with feedback to Claude.
|
|
7
|
+
|
|
8
|
+
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
INPUT=$(cat)
|
|
10
|
+
PROMPT=$(echo "$INPUT" | "$HOOK_DIR/parse-json.sh" tool_input.prompt)
|
|
11
|
+
|
|
12
|
+
is_feature_agent() {
|
|
13
|
+
echo "$PROMPT" | grep -qi "feature-implementer\|feature-planner"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if ! is_feature_agent; then
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$HOOK_DIR/../.." && pwd)}"
|
|
21
|
+
|
|
22
|
+
spec_file_referenced() {
|
|
23
|
+
echo "$PROMPT" | grep -oP 'docs/specs/[a-z0-9-]+\.md' | head -1 || true
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
SPEC_REF=$(spec_file_referenced || true)
|
|
27
|
+
|
|
28
|
+
if [[ -z "$SPEC_REF" ]]; then
|
|
29
|
+
echo "No spec file referenced in the prompt. Create a spec first with /product-manager." >&2
|
|
30
|
+
exit 2
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
if [[ ! -f "$PROJECT_DIR/$SPEC_REF" ]]; then
|
|
34
|
+
echo "Spec file '$SPEC_REF' does not exist. Create it first with /product-manager." >&2
|
|
35
|
+
exit 2
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
SPEC_CONTENT=$(cat "$PROJECT_DIR/$SPEC_REF")
|
|
39
|
+
|
|
40
|
+
has_rules_section() {
|
|
41
|
+
echo "$SPEC_CONTENT" | grep -qE "^## (Rules|Business Rules|Remaining Scope|Functional Requirements)"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
has_scenarios_section() {
|
|
45
|
+
echo "$SPEC_CONTENT" | grep -qE "^## (Scenarios|Acceptance Criteria|Gherkin Scenarios)"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if ! has_rules_section; then
|
|
49
|
+
echo "Spec '$SPEC_REF' is missing the '## Rules' (or '## Business Rules') section. Fix it before implementing." >&2
|
|
50
|
+
exit 2
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
if ! has_scenarios_section; then
|
|
54
|
+
echo "Spec '$SPEC_REF' is missing the '## Scenarios' (or '## Acceptance Criteria') section. Fix it before implementing." >&2
|
|
55
|
+
exit 2
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
exit 0
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Injects feature tracker status into session context on startup.
|
|
5
|
+
# Used as SessionStart hook.
|
|
6
|
+
# Outputs JSON with additionalContext field.
|
|
7
|
+
|
|
8
|
+
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$HOOK_DIR/../.." && pwd)}"
|
|
10
|
+
TRACKER="$PROJECT_DIR/docs/feature-tracker.md"
|
|
11
|
+
|
|
12
|
+
if [[ ! -f "$TRACKER" ]]; then
|
|
13
|
+
exit 0
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
FEATURE_COUNT=$(tail -n +3 "$TRACKER" | grep -c '|' || true)
|
|
17
|
+
|
|
18
|
+
if [[ "$FEATURE_COUNT" -eq 0 ]]; then
|
|
19
|
+
exit 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
IMPLEMENTING=$(tail -n +3 "$TRACKER" | grep -c "| implementing |" || true)
|
|
23
|
+
IMPLEMENTED=$(tail -n +3 "$TRACKER" | grep -c "| implemented |" || true)
|
|
24
|
+
DRAFTED=$(tail -n +3 "$TRACKER" | grep -c "| drafted |" || true)
|
|
25
|
+
PLANNED=$(tail -n +3 "$TRACKER" | grep -c "| planned |" || true)
|
|
26
|
+
|
|
27
|
+
CONTEXT="Feature tracker: ${FEATURE_COUNT} features total"
|
|
28
|
+
|
|
29
|
+
if [[ "$IMPLEMENTING" -gt 0 ]]; then
|
|
30
|
+
CONTEXT="$CONTEXT, ${IMPLEMENTING} in progress"
|
|
31
|
+
fi
|
|
32
|
+
if [[ "$PLANNED" -gt 0 ]]; then
|
|
33
|
+
CONTEXT="$CONTEXT, ${PLANNED} planned"
|
|
34
|
+
fi
|
|
35
|
+
if [[ "$DRAFTED" -gt 0 ]]; then
|
|
36
|
+
CONTEXT="$CONTEXT, ${DRAFTED} drafted"
|
|
37
|
+
fi
|
|
38
|
+
if [[ "$IMPLEMENTED" -gt 0 ]]; then
|
|
39
|
+
CONTEXT="$CONTEXT, ${IMPLEMENTED} implemented"
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
CONTEXT="$CONTEXT. See docs/feature-tracker.md for details."
|
|
43
|
+
|
|
44
|
+
python3 -c "
|
|
45
|
+
import json, sys
|
|
46
|
+
print(json.dumps({'additionalContext': sys.argv[1]}))
|
|
47
|
+
" "$CONTEXT"
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Test runner for Claude Code hook scripts.
|
|
5
|
+
# Run: bash scripts/hooks/tests/run-tests.sh
|
|
6
|
+
|
|
7
|
+
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
8
|
+
TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
PASSED=0
|
|
10
|
+
FAILED=0
|
|
11
|
+
TOTAL=0
|
|
12
|
+
|
|
13
|
+
green() { printf "\033[32m%s\033[0m\n" "$1"; }
|
|
14
|
+
red() { printf "\033[31m%s\033[0m\n" "$1"; }
|
|
15
|
+
bold() { printf "\033[1m%s\033[0m\n" "$1"; }
|
|
16
|
+
|
|
17
|
+
assert_exit() {
|
|
18
|
+
local test_name="$1"
|
|
19
|
+
local expected_exit="$2"
|
|
20
|
+
local actual_exit="$3"
|
|
21
|
+
TOTAL=$((TOTAL + 1))
|
|
22
|
+
|
|
23
|
+
if [[ "$actual_exit" -eq "$expected_exit" ]]; then
|
|
24
|
+
green " PASS: $test_name (exit $actual_exit)"
|
|
25
|
+
PASSED=$((PASSED + 1))
|
|
26
|
+
else
|
|
27
|
+
red " FAIL: $test_name (expected exit $expected_exit, got $actual_exit)"
|
|
28
|
+
FAILED=$((FAILED + 1))
|
|
29
|
+
fi
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
assert_stderr_contains() {
|
|
33
|
+
local test_name="$1"
|
|
34
|
+
local pattern="$2"
|
|
35
|
+
local stderr_output="$3"
|
|
36
|
+
TOTAL=$((TOTAL + 1))
|
|
37
|
+
|
|
38
|
+
if echo "$stderr_output" | grep -q "$pattern"; then
|
|
39
|
+
green " PASS: $test_name (stderr contains '$pattern')"
|
|
40
|
+
PASSED=$((PASSED + 1))
|
|
41
|
+
else
|
|
42
|
+
red " FAIL: $test_name (stderr missing '$pattern')"
|
|
43
|
+
red " actual stderr: $stderr_output"
|
|
44
|
+
FAILED=$((FAILED + 1))
|
|
45
|
+
fi
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# ─────────────────────────────────────────────
|
|
49
|
+
bold "=== parse-json.sh ==="
|
|
50
|
+
|
|
51
|
+
OUTPUT=$(echo '{"tool_input":{"command":"git commit -m test"}}' | "$HOOK_DIR/parse-json.sh" tool_input.command)
|
|
52
|
+
TOTAL=$((TOTAL + 1))
|
|
53
|
+
if [[ "$OUTPUT" == "git commit -m test" ]]; then
|
|
54
|
+
green " PASS: extracts nested value"
|
|
55
|
+
PASSED=$((PASSED + 1))
|
|
56
|
+
else
|
|
57
|
+
red " FAIL: extracts nested value (got: $OUTPUT)"
|
|
58
|
+
FAILED=$((FAILED + 1))
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
OUTPUT=$(echo '{"tool_input":{"command":"test"}}' | "$HOOK_DIR/parse-json.sh" tool_input.missing_key)
|
|
62
|
+
TOTAL=$((TOTAL + 1))
|
|
63
|
+
if [[ -z "$OUTPUT" ]]; then
|
|
64
|
+
green " PASS: returns empty for missing key"
|
|
65
|
+
PASSED=$((PASSED + 1))
|
|
66
|
+
else
|
|
67
|
+
red " FAIL: returns empty for missing key (got: $OUTPUT)"
|
|
68
|
+
FAILED=$((FAILED + 1))
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# ─────────────────────────────────────────────
|
|
72
|
+
bold "=== no-barrel-exports.sh ==="
|
|
73
|
+
|
|
74
|
+
echo '{"tool_input":{"file_path":"/src/entities/index.ts"}}' | "$HOOK_DIR/no-barrel-exports.sh" > /dev/null 2>&1 || true
|
|
75
|
+
EXIT_CODE=$(echo '{"tool_input":{"file_path":"/src/entities/index.ts"}}' | "$HOOK_DIR/no-barrel-exports.sh" > /dev/null 2>&1; echo $?) || true
|
|
76
|
+
assert_exit "blocks index.ts" 2 "$EXIT_CODE"
|
|
77
|
+
|
|
78
|
+
STDERR=$(echo '{"tool_input":{"file_path":"/src/entities/index.ts"}}' | "$HOOK_DIR/no-barrel-exports.sh" 2>&1 > /dev/null || true)
|
|
79
|
+
assert_stderr_contains "error message mentions barrel" "Barrel exports" "$STDERR"
|
|
80
|
+
|
|
81
|
+
EXIT_CODE=$(echo '{"tool_input":{"file_path":"/src/entities/review.ts"}}' | "$HOOK_DIR/no-barrel-exports.sh" > /dev/null 2>&1; echo $?) || true
|
|
82
|
+
assert_exit "allows normal .ts file" 0 "$EXIT_CODE"
|
|
83
|
+
|
|
84
|
+
EXIT_CODE=$(echo '{"tool_input":{"file_path":"/src/entities/index.js"}}' | "$HOOK_DIR/no-barrel-exports.sh" > /dev/null 2>&1; echo $?) || true
|
|
85
|
+
assert_exit "blocks index.js" 2 "$EXIT_CODE"
|
|
86
|
+
|
|
87
|
+
# ─────────────────────────────────────────────
|
|
88
|
+
bold "=== protect-main-branch.sh ==="
|
|
89
|
+
|
|
90
|
+
# Create temp git repo to test
|
|
91
|
+
TEMP_REPO=$(mktemp -d)
|
|
92
|
+
cd "$TEMP_REPO"
|
|
93
|
+
git init -q
|
|
94
|
+
git checkout -q -b master
|
|
95
|
+
|
|
96
|
+
EXIT_CODE=$(echo '{"tool_input":{"command":"git commit -m test"}}' | CLAUDE_PROJECT_DIR="$TEMP_REPO" "$HOOK_DIR/protect-main-branch.sh" > /dev/null 2>&1; echo $?) || true
|
|
97
|
+
assert_exit "blocks commit on master" 2 "$EXIT_CODE"
|
|
98
|
+
|
|
99
|
+
STDERR=$(echo '{"tool_input":{"command":"git commit -m test"}}' | CLAUDE_PROJECT_DIR="$TEMP_REPO" "$HOOK_DIR/protect-main-branch.sh" 2>&1 > /dev/null || true)
|
|
100
|
+
assert_stderr_contains "mentions master" "master" "$STDERR"
|
|
101
|
+
|
|
102
|
+
git checkout -q -b feat/test
|
|
103
|
+
EXIT_CODE=$(echo '{"tool_input":{"command":"git commit -m test"}}' | CLAUDE_PROJECT_DIR="$TEMP_REPO" "$HOOK_DIR/protect-main-branch.sh" > /dev/null 2>&1; echo $?) || true
|
|
104
|
+
assert_exit "allows commit on feature branch" 0 "$EXIT_CODE"
|
|
105
|
+
|
|
106
|
+
EXIT_CODE=$(echo '{"tool_input":{"command":"ls -la"}}' | CLAUDE_PROJECT_DIR="$TEMP_REPO" "$HOOK_DIR/protect-main-branch.sh" > /dev/null 2>&1; echo $?) || true
|
|
107
|
+
assert_exit "ignores non-commit commands" 0 "$EXIT_CODE"
|
|
108
|
+
|
|
109
|
+
rm -rf "$TEMP_REPO"
|
|
110
|
+
|
|
111
|
+
# ─────────────────────────────────────────────
|
|
112
|
+
bold "=== protect-main-push.sh ==="
|
|
113
|
+
|
|
114
|
+
EXIT_CODE=$(echo '{"tool_input":{"command":"git push origin master"}}' | "$HOOK_DIR/protect-main-push.sh" > /dev/null 2>&1; echo $?) || true
|
|
115
|
+
assert_exit "blocks push to master" 2 "$EXIT_CODE"
|
|
116
|
+
|
|
117
|
+
EXIT_CODE=$(echo '{"tool_input":{"command":"git push origin main"}}' | "$HOOK_DIR/protect-main-push.sh" > /dev/null 2>&1; echo $?) || true
|
|
118
|
+
assert_exit "blocks push to main" 2 "$EXIT_CODE"
|
|
119
|
+
|
|
120
|
+
EXIT_CODE=$(echo '{"tool_input":{"command":"git push --force origin feat/test"}}' | "$HOOK_DIR/protect-main-push.sh" > /dev/null 2>&1; echo $?) || true
|
|
121
|
+
assert_exit "blocks force push" 2 "$EXIT_CODE"
|
|
122
|
+
|
|
123
|
+
EXIT_CODE=$(echo '{"tool_input":{"command":"git push origin feat/test"}}' | "$HOOK_DIR/protect-main-push.sh" > /dev/null 2>&1; echo $?) || true
|
|
124
|
+
assert_exit "allows push to feature branch" 0 "$EXIT_CODE"
|
|
125
|
+
|
|
126
|
+
EXIT_CODE=$(echo '{"tool_input":{"command":"ls -la"}}' | "$HOOK_DIR/protect-main-push.sh" > /dev/null 2>&1; echo $?) || true
|
|
127
|
+
assert_exit "ignores non-push commands" 0 "$EXIT_CODE"
|
|
128
|
+
|
|
129
|
+
# ─────────────────────────────────────────────
|
|
130
|
+
bold "=== require-spec.sh ==="
|
|
131
|
+
|
|
132
|
+
PROJECT_DIR="$HOOK_DIR/../.."
|
|
133
|
+
|
|
134
|
+
# Test: no spec reference → block
|
|
135
|
+
EXIT_CODE=$(echo '{"tool_input":{"prompt":"implement the feature-implementer for login"}}' | CLAUDE_PROJECT_DIR="$PROJECT_DIR" "$HOOK_DIR/require-spec.sh" > /dev/null 2>&1; echo $?) || true
|
|
136
|
+
assert_exit "blocks agent without spec reference" 2 "$EXIT_CODE"
|
|
137
|
+
|
|
138
|
+
STDERR=$(echo '{"tool_input":{"prompt":"implement the feature-implementer for login"}}' | CLAUDE_PROJECT_DIR="$PROJECT_DIR" "$HOOK_DIR/require-spec.sh" 2>&1 > /dev/null || true)
|
|
139
|
+
assert_stderr_contains "suggests /product-manager" "product-manager" "$STDERR"
|
|
140
|
+
|
|
141
|
+
# Test: non-feature agent → allow
|
|
142
|
+
EXIT_CODE=$(echo '{"tool_input":{"prompt":"review the code in src/entities"}}' | CLAUDE_PROJECT_DIR="$PROJECT_DIR" "$HOOK_DIR/require-spec.sh" > /dev/null 2>&1; echo $?) || true
|
|
143
|
+
assert_exit "allows non-feature agents" 0 "$EXIT_CODE"
|
|
144
|
+
|
|
145
|
+
# Test: valid spec reference with existing file
|
|
146
|
+
EXIT_CODE=$(echo '{"tool_input":{"prompt":"run feature-planner with docs/specs/44-zod-guard-gitlab-webhook.md"}}' | CLAUDE_PROJECT_DIR="$PROJECT_DIR" "$HOOK_DIR/require-spec.sh" > /dev/null 2>&1; echo $?) || true
|
|
147
|
+
assert_exit "allows with valid spec reference" 0 "$EXIT_CODE"
|
|
148
|
+
|
|
149
|
+
# Test: spec file does not exist
|
|
150
|
+
EXIT_CODE=$(echo '{"tool_input":{"prompt":"run feature-implementer with docs/specs/999-nonexistent.md"}}' | CLAUDE_PROJECT_DIR="$PROJECT_DIR" "$HOOK_DIR/require-spec.sh" > /dev/null 2>&1; echo $?) || true
|
|
151
|
+
assert_exit "blocks with nonexistent spec" 2 "$EXIT_CODE"
|
|
152
|
+
|
|
153
|
+
# ─────────────────────────────────────────────
|
|
154
|
+
bold "=== session-context.sh ==="
|
|
155
|
+
|
|
156
|
+
EXIT_CODE=$(CLAUDE_PROJECT_DIR="$PROJECT_DIR" "$HOOK_DIR/session-context.sh" > /dev/null 2>&1; echo $?) || true
|
|
157
|
+
assert_exit "runs without error" 0 "$EXIT_CODE"
|
|
158
|
+
|
|
159
|
+
OUTPUT=$(CLAUDE_PROJECT_DIR="$PROJECT_DIR" "$HOOK_DIR/session-context.sh" 2>/dev/null || true)
|
|
160
|
+
TOTAL=$((TOTAL + 1))
|
|
161
|
+
if echo "$OUTPUT" | python3 -c "import json,sys; json.load(sys.stdin)" 2>/dev/null; then
|
|
162
|
+
green " PASS: outputs valid JSON"
|
|
163
|
+
PASSED=$((PASSED + 1))
|
|
164
|
+
else
|
|
165
|
+
red " FAIL: outputs valid JSON (got: $OUTPUT)"
|
|
166
|
+
FAILED=$((FAILED + 1))
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
TOTAL=$((TOTAL + 1))
|
|
170
|
+
if echo "$OUTPUT" | python3 -c "import json,sys; d=json.load(sys.stdin); assert 'additionalContext' in d" 2>/dev/null; then
|
|
171
|
+
green " PASS: JSON contains additionalContext"
|
|
172
|
+
PASSED=$((PASSED + 1))
|
|
173
|
+
else
|
|
174
|
+
red " FAIL: JSON contains additionalContext (got: $OUTPUT)"
|
|
175
|
+
FAILED=$((FAILED + 1))
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
# ─────────────────────────────────────────────
|
|
179
|
+
echo ""
|
|
180
|
+
bold "=== RESULTS ==="
|
|
181
|
+
echo "Total: $TOTAL | Passed: $PASSED | Failed: $FAILED"
|
|
182
|
+
|
|
183
|
+
if [[ "$FAILED" -gt 0 ]]; then
|
|
184
|
+
red "SOME TESTS FAILED"
|
|
185
|
+
exit 1
|
|
186
|
+
else
|
|
187
|
+
green "ALL TESTS PASSED"
|
|
188
|
+
exit 0
|
|
189
|
+
fi
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Tests for the 3 Clean Architecture enforcement hooks.
|
|
5
|
+
# Run: bash scripts/hooks/tests/test-architecture-hooks.sh
|
|
6
|
+
|
|
7
|
+
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
8
|
+
PASSED=0
|
|
9
|
+
FAILED=0
|
|
10
|
+
TOTAL=0
|
|
11
|
+
|
|
12
|
+
green() { printf "\033[32m%s\033[0m\n" "$1"; }
|
|
13
|
+
red() { printf "\033[31m%s\033[0m\n" "$1"; }
|
|
14
|
+
bold() { printf "\033[1m%s\033[0m\n" "$1"; }
|
|
15
|
+
|
|
16
|
+
assert_exit() {
|
|
17
|
+
local name="$1" expected="$2" actual="$3"
|
|
18
|
+
TOTAL=$((TOTAL + 1))
|
|
19
|
+
if [[ "$actual" -eq "$expected" ]]; then
|
|
20
|
+
green " PASS: $name (exit $actual)"
|
|
21
|
+
PASSED=$((PASSED + 1))
|
|
22
|
+
else
|
|
23
|
+
red " FAIL: $name (expected exit $expected, got $actual)"
|
|
24
|
+
FAILED=$((FAILED + 1))
|
|
25
|
+
fi
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
payload() {
|
|
29
|
+
python3 -c "
|
|
30
|
+
import json, sys
|
|
31
|
+
print(json.dumps({'tool_input': {'file_path': sys.argv[1], 'content': sys.argv[2], 'new_string': ''}}))
|
|
32
|
+
" "$1" "$2"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# ─────────────────────────────────────────────────────────────
|
|
36
|
+
bold "=== enforce-dependency-rule.sh ==="
|
|
37
|
+
|
|
38
|
+
# Entity importing interface-adapters → blocked
|
|
39
|
+
EXIT=$(payload "/project/src/entities/foo/foo.gateway.ts" \
|
|
40
|
+
"import type { Bar } from '@/interface-adapters/gateways/bar.js'" \
|
|
41
|
+
| "$HOOK_DIR/enforce-dependency-rule.sh" > /dev/null 2>&1; echo $?) || true
|
|
42
|
+
assert_exit "entity cannot import interface-adapters" 2 "$EXIT"
|
|
43
|
+
|
|
44
|
+
# Entity importing usecases → blocked
|
|
45
|
+
EXIT=$(payload "/project/src/entities/foo/foo.ts" \
|
|
46
|
+
"import type { Usecase } from '@/usecases/something.usecase.js'" \
|
|
47
|
+
| "$HOOK_DIR/enforce-dependency-rule.sh" > /dev/null 2>&1; echo $?) || true
|
|
48
|
+
assert_exit "entity cannot import usecases" 2 "$EXIT"
|
|
49
|
+
|
|
50
|
+
# Entity importing frameworks → blocked
|
|
51
|
+
EXIT=$(payload "/project/src/entities/foo/foo.ts" \
|
|
52
|
+
"import type { Queue } from '@/frameworks/queue/pQueueAdapter.js'" \
|
|
53
|
+
| "$HOOK_DIR/enforce-dependency-rule.sh" > /dev/null 2>&1; echo $?) || true
|
|
54
|
+
assert_exit "entity cannot import frameworks" 2 "$EXIT"
|
|
55
|
+
|
|
56
|
+
# Entity importing within entities → allowed
|
|
57
|
+
EXIT=$(payload "/project/src/entities/foo/foo.ts" \
|
|
58
|
+
"import type { Bar } from '@/entities/bar/bar.js'" \
|
|
59
|
+
| "$HOOK_DIR/enforce-dependency-rule.sh" > /dev/null 2>&1; echo $?) || true
|
|
60
|
+
assert_exit "entity can import other entities" 0 "$EXIT"
|
|
61
|
+
|
|
62
|
+
# Usecase importing interface-adapters → blocked
|
|
63
|
+
EXIT=$(payload "/project/src/usecases/doSomething.usecase.ts" \
|
|
64
|
+
"import type { SomeGateway } from '@/interface-adapters/gateways/some.gateway.js'" \
|
|
65
|
+
| "$HOOK_DIR/enforce-dependency-rule.sh" > /dev/null 2>&1; echo $?) || true
|
|
66
|
+
assert_exit "usecase cannot import interface-adapters" 2 "$EXIT"
|
|
67
|
+
|
|
68
|
+
# Usecase importing frameworks → blocked
|
|
69
|
+
EXIT=$(payload "/project/src/usecases/doSomething.usecase.ts" \
|
|
70
|
+
"import type { ReviewJob } from '@/frameworks/queue/pQueueAdapter.js'" \
|
|
71
|
+
| "$HOOK_DIR/enforce-dependency-rule.sh" > /dev/null 2>&1; echo $?) || true
|
|
72
|
+
assert_exit "usecase cannot import frameworks" 2 "$EXIT"
|
|
73
|
+
|
|
74
|
+
# Usecase importing entities (valid) → allowed
|
|
75
|
+
EXIT=$(payload "/project/src/usecases/doSomething.usecase.ts" \
|
|
76
|
+
"import type { ReviewContextGateway } from '@/entities/reviewContext/reviewContext.gateway.js'" \
|
|
77
|
+
| "$HOOK_DIR/enforce-dependency-rule.sh" > /dev/null 2>&1; echo $?) || true
|
|
78
|
+
assert_exit "usecase can import entities" 0 "$EXIT"
|
|
79
|
+
|
|
80
|
+
# File outside src/ → always allowed
|
|
81
|
+
EXIT=$(payload "/project/scripts/build.ts" \
|
|
82
|
+
"import type { Foo } from '@/interface-adapters/gateways/foo.js'" \
|
|
83
|
+
| "$HOOK_DIR/enforce-dependency-rule.sh" > /dev/null 2>&1; echo $?) || true
|
|
84
|
+
assert_exit "non-src file always allowed" 0 "$EXIT"
|
|
85
|
+
|
|
86
|
+
# ─────────────────────────────────────────────────────────────
|
|
87
|
+
bold "=== enforce-gateway-port-purity.sh ==="
|
|
88
|
+
|
|
89
|
+
# Entity gateway with interface → allowed
|
|
90
|
+
EXIT=$(payload "/project/src/entities/review/reviewContext.gateway.ts" \
|
|
91
|
+
"export interface ReviewContextGateway { create(): void }" \
|
|
92
|
+
| "$HOOK_DIR/enforce-gateway-port-purity.sh" > /dev/null 2>&1; echo $?) || true
|
|
93
|
+
assert_exit "entity gateway with interface is allowed" 0 "$EXIT"
|
|
94
|
+
|
|
95
|
+
# Entity gateway with abstract class → allowed
|
|
96
|
+
EXIT=$(payload "/project/src/entities/review/reviewContext.gateway.ts" \
|
|
97
|
+
"export abstract class ReviewContextGateway { abstract create(): void }" \
|
|
98
|
+
| "$HOOK_DIR/enforce-gateway-port-purity.sh" > /dev/null 2>&1; echo $?) || true
|
|
99
|
+
assert_exit "entity gateway with abstract class is allowed" 0 "$EXIT"
|
|
100
|
+
|
|
101
|
+
# Entity gateway with plain class → blocked
|
|
102
|
+
EXIT=$(payload "/project/src/entities/review/reviewContext.gateway.ts" \
|
|
103
|
+
"export class ReviewContextGateway { create(): void {} }" \
|
|
104
|
+
| "$HOOK_DIR/enforce-gateway-port-purity.sh" > /dev/null 2>&1; echo $?) || true
|
|
105
|
+
assert_exit "entity gateway with plain class is blocked" 2 "$EXIT"
|
|
106
|
+
|
|
107
|
+
# Implementation in interface-adapters with plain class → allowed (not in entities/)
|
|
108
|
+
EXIT=$(payload "/project/src/interface-adapters/gateways/reviewContext.fileSystem.gateway.ts" \
|
|
109
|
+
"export class ReviewContextFileSystemGateway implements ReviewContextGateway {}" \
|
|
110
|
+
| "$HOOK_DIR/enforce-gateway-port-purity.sh" > /dev/null 2>&1; echo $?) || true
|
|
111
|
+
assert_exit "gateway impl in interface-adapters with plain class is allowed" 0 "$EXIT"
|
|
112
|
+
|
|
113
|
+
# Non-gateway entity file → not checked
|
|
114
|
+
EXIT=$(payload "/project/src/entities/review/reviewContext.ts" \
|
|
115
|
+
"export class ReviewContext { private constructor() {} }" \
|
|
116
|
+
| "$HOOK_DIR/enforce-gateway-port-purity.sh" > /dev/null 2>&1; echo $?) || true
|
|
117
|
+
assert_exit "non-gateway entity file not checked" 0 "$EXIT"
|
|
118
|
+
|
|
119
|
+
# ─────────────────────────────────────────────────────────────
|
|
120
|
+
bold "=== enforce-presenter-class.sh ==="
|
|
121
|
+
|
|
122
|
+
# Presenter with class ending in Presenter → allowed
|
|
123
|
+
EXIT=$(payload "/project/src/interface-adapters/presenters/jobStatus.presenter.ts" \
|
|
124
|
+
"export class JobStatusPresenter { present(input: unknown) { return {} } }" \
|
|
125
|
+
| "$HOOK_DIR/enforce-presenter-class.sh" > /dev/null 2>&1; echo $?) || true
|
|
126
|
+
assert_exit "presenter class *Presenter is allowed" 0 "$EXIT"
|
|
127
|
+
|
|
128
|
+
# Presenter with class ending in Calculator → allowed
|
|
129
|
+
EXIT=$(payload "/project/src/interface-adapters/presenters/score.presenter.ts" \
|
|
130
|
+
"export class ScoreCalculator { compute(input: unknown) { return 0 } }" \
|
|
131
|
+
| "$HOOK_DIR/enforce-presenter-class.sh" > /dev/null 2>&1; echo $?) || true
|
|
132
|
+
assert_exit "presenter class *Calculator is allowed" 0 "$EXIT"
|
|
133
|
+
|
|
134
|
+
# Presenter with function export → blocked
|
|
135
|
+
EXIT=$(payload "/project/src/interface-adapters/presenters/jobStatus.presenter.ts" \
|
|
136
|
+
"export function presentJobStatus(input: unknown) { return {} }" \
|
|
137
|
+
| "$HOOK_DIR/enforce-presenter-class.sh" > /dev/null 2>&1; echo $?) || true
|
|
138
|
+
assert_exit "presenter function export is blocked" 2 "$EXIT"
|
|
139
|
+
|
|
140
|
+
# Non-presenter .ts file → not checked
|
|
141
|
+
EXIT=$(payload "/project/src/interface-adapters/controllers/webhook.controller.ts" \
|
|
142
|
+
"export function handleWebhook() {}" \
|
|
143
|
+
| "$HOOK_DIR/enforce-presenter-class.sh" > /dev/null 2>&1; echo $?) || true
|
|
144
|
+
assert_exit "non-presenter file not checked" 0 "$EXIT"
|
|
145
|
+
|
|
146
|
+
# presenter.ts outside interface-adapters/presenters/ → not checked
|
|
147
|
+
EXIT=$(payload "/project/src/entities/foo/foo.presenter.ts" \
|
|
148
|
+
"export function fooPresenter() {}" \
|
|
149
|
+
| "$HOOK_DIR/enforce-presenter-class.sh" > /dev/null 2>&1; echo $?) || true
|
|
150
|
+
assert_exit "presenter outside interface-adapters/presenters/ not checked" 0 "$EXIT"
|
|
151
|
+
|
|
152
|
+
# ─────────────────────────────────────────────────────────────
|
|
153
|
+
echo ""
|
|
154
|
+
bold "=== RESULTS ==="
|
|
155
|
+
echo "Total: $TOTAL | Passed: $PASSED | Failed: $FAILED"
|
|
156
|
+
|
|
157
|
+
if [[ "$FAILED" -gt 0 ]]; then
|
|
158
|
+
red "SOME TESTS FAILED"
|
|
159
|
+
exit 1
|
|
160
|
+
else
|
|
161
|
+
green "ALL TESTS PASSED"
|
|
162
|
+
exit 0
|
|
163
|
+
fi
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Blocks git commit if staged files include src/ code but the corresponding
|
|
5
|
+
# spec does not have "## Status: implemented" and the feature-tracker is stale.
|
|
6
|
+
# Used as PreToolUse hook on Bash(git commit *).
|
|
7
|
+
# Exit 0 = allow, Exit 2 = block with feedback to Claude.
|
|
8
|
+
|
|
9
|
+
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
INPUT=$(cat)
|
|
11
|
+
COMMAND=$(echo "$INPUT" | "$HOOK_DIR/parse-json.sh" tool_input.command)
|
|
12
|
+
|
|
13
|
+
is_commit_command() {
|
|
14
|
+
echo "$COMMAND" | grep -qE "git commit"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if ! is_commit_command; then
|
|
18
|
+
exit 0
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$HOOK_DIR/../.." && pwd)}"
|
|
22
|
+
cd "$PROJECT_DIR"
|
|
23
|
+
|
|
24
|
+
STAGED_FILES=$(git diff --cached --name-only 2>/dev/null || true)
|
|
25
|
+
|
|
26
|
+
if [[ -z "$STAGED_FILES" ]]; then
|
|
27
|
+
exit 0
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
has_source_code() {
|
|
31
|
+
echo "$STAGED_FILES" | grep -q "^src/" 2>/dev/null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if ! has_source_code; then
|
|
35
|
+
exit 0
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
TRACKER="$PROJECT_DIR/docs/feature-tracker.md"
|
|
39
|
+
SPECS_DIR="$PROJECT_DIR/docs/specs"
|
|
40
|
+
|
|
41
|
+
ERRORS=""
|
|
42
|
+
|
|
43
|
+
if [[ -f "$TRACKER" ]]; then
|
|
44
|
+
DRAFTS_IN_TRACKER=$(grep -c "| drafted |" "$TRACKER" || true)
|
|
45
|
+
STAGED_HAS_TRACKER=$(echo "$STAGED_FILES" | grep -c "docs/feature-tracker.md" || true)
|
|
46
|
+
|
|
47
|
+
if [[ "$DRAFTS_IN_TRACKER" -gt 0 && "$STAGED_HAS_TRACKER" -eq 0 ]]; then
|
|
48
|
+
IMPLEMENTING=$(grep -c "| implementing |" "$TRACKER" || true)
|
|
49
|
+
if [[ "$IMPLEMENTING" -gt 0 ]]; then
|
|
50
|
+
ERRORS="${ERRORS}Feature tracker has features in 'implementing' status. Update docs/feature-tracker.md before committing.\n"
|
|
51
|
+
fi
|
|
52
|
+
fi
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
if [[ -d "$SPECS_DIR" ]]; then
|
|
56
|
+
for SPEC_FILE in "$SPECS_DIR"/*.md; do
|
|
57
|
+
[[ -f "$SPEC_FILE" ]] || continue
|
|
58
|
+
SPEC_NAME=$(basename "$SPEC_FILE")
|
|
59
|
+
|
|
60
|
+
if grep -q "^## Status: implemented" "$SPEC_FILE" 2>/dev/null; then
|
|
61
|
+
continue
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
if echo "$STAGED_FILES" | grep -q "$SPEC_NAME" 2>/dev/null; then
|
|
65
|
+
continue
|
|
66
|
+
fi
|
|
67
|
+
done
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
if [[ -n "$ERRORS" ]]; then
|
|
71
|
+
echo -e "$ERRORS" >&2
|
|
72
|
+
exit 2
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
exit 0
|