Add BUILD.bazel dependency linting (#5437)

* Add BUILD.bazel dependency linting

Adds tooling to enforce architectural boundaries in the codebase:

1. Shell script `scripts/check_build_deps.sh` with multiple modes:
   - Default: runs all checks and shows dependency counts
   - --ci: fails only on hard violations (src/main depends on src/test)
   - --strict: also fails if proto deps in library/ increase from baseline
   - --count: just show current dependency counts
   - --update-baseline: update the baseline file for tracking

2. Bazel test `//:build_deps_test` for integration with test suite

3. Baseline file `scripts/build_deps_baseline.txt` tracking proto dep count

Current enforced rules:
- src/main must not depend on src/test (enforced now)
- library/ proto dependencies tracked (167 currently, deproto in progress)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Update DEPROTO_PLAN.md with accurate proto import inventory

The previous status was inaccurate. This update:

- Corrects summary table: Availability, Command Choice Helpers,
  and other utilities still have Hostility proto imports
- Adds Phase 8b documenting Hostility proto migration (4 files)
- Updates detailed inventory showing actual 13 files with direct
  proto imports (vs 167 transitive deps from bazel query)
- Corrects success criteria checkboxes to reflect actual status
- Documents the grep command used to verify imports

The 167 proto deps from bazel query are transitive dependencies.
Only 13 files have direct proto imports:
- 5 boundary files (expected to keep proto)
- 4 files using Hostility proto (Phase 8b)
- 4 LLM prompt generator files (Phase 8)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-19 07:28:58 -08:00
committed by GitHub
co-authored by Claude Opus 4.5
parent a0bf6050b4
commit 7ab8fc796b
4 changed files with 274 additions and 85 deletions
+12
View File
@@ -32,3 +32,15 @@ nogo(
vet = True,
visibility = ["//visibility:public"],
)
# Dependency constraint tests
# These verify architectural boundaries are maintained
sh_test(
name = "build_deps_test",
srcs = ["scripts/check_build_deps.sh"],
args = ["--ci"],
tags = [
"local", # Needs bazel query access
"no-sandbox",
],
)
+101 -85
View File
@@ -42,16 +42,17 @@
| AI (`/ai/`) | **0** | ✅ **Complete** |
| Actions (`/library/actions/impl/action/`) | **0** | ✅ **Complete** |
| Commands (`/library/actions/impl/command/`) | **0** | ✅ **Complete** |
| Availability (`/library/actions/availability/`) | **0** | **Complete** |
| Command Choice Helpers (`/library/util/command_choice_helpers/`) | **0** | **Complete** |
| View Filters (`/library/util/view_filters/`) | **0** | **Complete** (except boundary code) |
| Other Utilities (`/library/util/`) | **0** | **Complete** |
| Availability (`/library/actions/availability/`) | **2** | ⏳ Uses `Hostility` proto |
| Command Choice Helpers (`/library/util/command_choice_helpers/`) | **1** | ⏳ Uses `Hostility` proto |
| View Filters (`/library/util/view_filters/`) | **1** | Keep (boundary code for battles) |
| Other Utilities (`/library/util/`) | **1** | `FactionUtils` uses `Hostility` proto |
| NewGameCreation (`/service/new_game_creation/`) | **0** | ✅ **Complete** (except input GameParameters proto) |
| LLM Prompt Generators (`/library/actions/llm_prompt_generators/`) | **0** | **Complete** |
| LLM Request Generators (`/library/actions/llm_request_generators/`) | **0** | **Complete** |
| Root Library (`/library/`) | **7** | Boundary code (4 files) |
| LLM Prompt Generators (`/library/actions/llm_prompt_generators/`) | **~8** | ⏳ Remaining work (4 files) |
| Root Library (`/library/`) | **~7** | Boundary code (4 files) |
**Total: ~7 proto imports remaining** (down from 149) - all in boundary code
**Total: 13 files with proto imports in library/** (verified via `bazel query`)
**Note:** The `bazel query` for proto dependencies returns 167 targets because it includes transitive dependencies. The actual direct proto imports are in only 13 files.
### Completed Phases
@@ -68,7 +69,7 @@
| Phase 6c: AI Clients | **Complete** | AIClient, command choosers all protoless |
| Phase 6d: CommandSelection | **Complete** | Renamed ScalaCommandSelection → CommandSelection |
### Recent Completions (PRs #5326-#5431)
### Recent Completions (PRs #5326-#5390)
1. **CommandFactory protoless** - Now accepts/returns Scala `SelectedCommand` types
2. **AI clients protoless** - `AIClient`, `MidGameAIClient`, all command choosers use Scala types
@@ -87,8 +88,6 @@
15. **Remove proto ActionResult from Engine layer** (PR #5378) - Engine uses Scala `ActionResultT`
16. **NewGameCreation protoless** (PR #5381) - Uses Scala types throughout, no proto conversion
17. **PersistedHistory Scala API** (PR #5390) - New `apply` overload accepts `Vector[ActionResultT]` directly
18. **All LLM prompt generators protoless** (PR #5429) - All 34 files converted to use `LlmRequestT` Scala types
19. **All LLM request generators protoless** (PRs #5419-#5428) - Converted to use Scala `LlmRequestT` types
---
@@ -107,18 +106,31 @@
**Pattern established**: Create Scala view type → Create converter → Update filter to return Scala type → Convert at edge.
### Phase 8: LLM Prompt Generators ✅ Complete
### Phase 8: LLM Prompt Generators ⏳ In Progress
All 34 LLM prompt generator files have been converted to use Scala `LlmRequestT` types instead of proto types.
**Status**: 4 files still have proto imports
| File Category | Files | Status |
|---------------|-------|--------|
| Diplomacy prompts | 12 | ✅ Complete |
| Quest prompts | 4 | ✅ Complete |
| Chronicle prompts | 3 | ✅ Complete |
| Other prompts | 15 | ✅ Complete |
| File | Proto Imports | Notes |
|------|---------------|-------|
| `BattalionDescriptions.scala` | `BattalionTypeId`, `Battalion`, `BattalionView` | Used for battalion text generation |
| `ChronicleEventTextGenerator.scala` | `EventForChronicle.*` | Converts chronicle events to text |
| `GeneratorUtilities.scala` | `Date` | Date formatting |
| `QuestEndedGeneratorUtilities.scala` | `BattalionTypeId`, `UnaffiliatedHeroQuest.*` | Quest text generation |
**Migration completed in PR #5429**: All prompt generators now accept `LlmRequestT.*` Scala types (e.g., `LlmRequestT.DivineMessage`, `LlmRequestT.AllianceOfferMessage`). Converters are used at the boundary in `LlmResolver.scala` where needed.
**Migration strategy**: Create Scala equivalents for these types or accept Scala types at the API boundary.
### Phase 8b: Hostility Proto Migration ⏳ In Progress
**Status**: 4 files use `net.eagle0.common.hostility.Hostility` proto
| File | Usage |
|------|-------|
| `AvailableAttackDecisionCommandFactory.scala` | Checks hostility for attack decisions |
| `AvailableFreeForAllDecisionCommandFactory.scala` | Checks hostility for FFA decisions |
| `AttackDecisionCommandChooser.scala` | AI uses hostility for decisions |
| `FactionUtils.scala` | `Hostility` conversion utility |
**Migration strategy**: Create a Scala `Hostility` enum and migrate these 4 files. The enum is simple (ALLIED, NEUTRAL, HOSTILE).
### Phase 9: Remaining Utilities ✅ Complete
@@ -163,12 +175,12 @@ All 34 LLM prompt generator files have been converted to use Scala `LlmRequestT`
- `*Converter.scala` - Explicit conversion utilities
- `PersistedHistory.scala` - Disk persistence
### Remaining Proto Usage (Boundary Only) ✅
### Remaining Proto Usage
All remaining proto usage is at system boundaries, which is expected and appropriate:
- BattleFilter (1 import) - Keep (boundary for Shardok battle views)
- Root library boundary code (7 imports in 4 files) - Keep (gRPC/persistence boundaries)
- LLM prompt generators (56 imports in 34 files) - Medium priority
- LLM request generators (2 imports in 1 file) - Medium priority
- BattleFilter (1 import) - Keep (boundary)
- Root library boundary code (7 imports in 4 files) - Keep (boundary)
---
@@ -179,12 +191,10 @@ All remaining proto usage is at system boundaries, which is expected and appropr
| View Filters | 0 | 0 | ✅ Complete |
| Utility files | 0 | 0 | ✅ Complete |
| History APIs | 0 | 0 | ✅ Complete |
| LLM Prompt Generators | 0 | 0 | ✅ Complete |
| LLM Request Generators | 0 | 0 | ✅ Complete |
| Root Library (boundary) | 4 | 7 | Keep (boundary code) |
| **Total** | **4** | **~7** | **All boundary code** |
🎉 **All business logic is now protoless!** The remaining proto usage is exclusively at system boundaries (gRPC, persistence).
| LLM Prompt Generators | 34 | 56 | Medium |
| LLM Request Generators | 1 | 2 | Medium |
| Root Library (boundary) | 4 | 7 | Keep |
| **Total** | **~35** | **~65** | |
---
@@ -193,13 +203,13 @@ All remaining proto usage is at system boundaries, which is expected and appropr
### Code Quality
- [x] Zero proto imports in `/ai/`
- [x] Zero proto imports in `/library/actions/impl/command/`
- [x] Zero proto imports in `/library/actions/availability/`
- [x] Zero proto imports in `/library/util/command_choice_helpers/`
- [ ] Zero proto imports in `/library/actions/availability/` (2 files use Hostility)
- [ ] Zero proto imports in `/library/util/command_choice_helpers/` (1 file uses Hostility)
- [x] Zero proto imports in `/library/actions/impl/action/`
- [x] Zero proto imports in `/library/util/view_filters/` (except BattleFilter boundary code)
- [x] Zero proto imports in `/library/util/` (except view filters)
- [ ] Zero proto imports in `/library/util/` (FactionUtils uses Hostility)
- [x] Zero proto imports in `/service/new_game_creation/` (except input GameParameters)
- [x] Zero proto imports in `/library/actions/llm_prompt_generators/`
- [ ] Zero proto imports in `/library/actions/llm_prompt_generators/` (4 files)
- [x] Zero proto imports in `/library/actions/llm_request_generators/`
### Architecture
@@ -213,72 +223,78 @@ All remaining proto usage is at system boundaries, which is expected and appropr
- [x] All view filters return Scala types
- [x] NewGameCreation uses Scala types
- [x] PersistedHistory accepts Scala types for new games
- [x] LLM layer uses Scala types
- [x] No "proto creep" into business logic
**All success criteria met!**
- [ ] LLM layer uses Scala types (4 files remaining)
- [ ] Hostility enum migrated to Scala (4 files remaining)
- [ ] No "proto creep" into business logic
---
## Open Questions
1. ~~**LLM Prompt Generators**: Should these accept Scala types directly, or is it acceptable to have proto usage here since they're generating text (not core game logic)?~~ **Resolved**: All LLM prompt generators now use Scala `LlmRequestT` types directly (PR #5429).
1. **LLM Prompt Generators**: Should these accept Scala types directly, or is it acceptable to have proto usage here since they're generating text (not core game logic)?
2. ~~**GameStateViewDiffer**: This works with view protos for client updates. Should it remain proto-based since it's generating client-facing data?~~ **Resolved**: GameStateViewDiffer now uses Scala types internally (`GameStateViewDiff`, `ProvinceViewDiff`, etc.) and converts to proto at the boundary in `ActionResultFilter`.
3. **History Serialization**: Keep proto for persistence (good for schema evolution) or consider alternatives? **Decision**: Keep proto for persistence - it's appropriate for disk serialization and schema evolution.
3. **History Serialization**: Keep proto for persistence (good for schema evolution) or consider alternatives?
---
## Proto Import Inventory (Detailed)
**Current: ~7 proto imports across 4 files** (as of 2026-01-18) - all boundary code
**Current: 13 files with direct proto imports in library/** (as of 2026-01-18)
The `bazel query` command reports 167 proto dependencies, but this includes transitive dependencies. The actual files with direct proto imports are listed below.
### Files with Proto Imports
```bash
# Verified with:
grep -rl "import net.eagle0.eagle\.\(internal\|common\|api\|views\)\.\|import net.eagle0.shardok\.\|import net.eagle0.common.hostility" \
src/main/scala/net/eagle0/eagle/library/ --include="*.scala"
```
### Boundary Code (Keep Proto) - 5 files
| File | Proto Imports | Notes |
|------|---------------|-------|
| `GameHistory.scala` | `ShardokActionResultView`, `ShardokAvailableCommands`, `ShardokActionResult` | Shardok integration boundary |
| `Engine.scala` | `ActionResultView` | Returns proto for client |
| `EngineImpl.scala` | `ActionResultView` | Returns proto for client |
| `ActionResultFilter.scala` | `ActionResultTypeProto`, `ActionResultView`, `GameStateViewDiffProto` | View conversion boundary |
| `BattleFilter.scala` | `Hostility` | Battle view boundary |
### Hostility Proto - 4 files (Phase 8b)
| File | Proto Imports | Notes |
|------|---------------|-------|
| `AvailableAttackDecisionCommandFactory.scala` | `Hostility` | Attack decision logic |
| `AvailableFreeForAllDecisionCommandFactory.scala` | `Hostility` | FFA decision logic |
| `AttackDecisionCommandChooser.scala` | `Hostility` | AI decision logic |
| `FactionUtils.scala` | `Hostility` | Utility function |
### LLM Prompt Generators - 4 files (Phase 8)
| File | Proto Imports | Notes |
|------|---------------|-------|
| `BattalionDescriptions.scala` | `BattalionTypeId`, `Battalion`, `BattalionView` | Battalion text |
| `ChronicleEventTextGenerator.scala` | `EventForChronicle.*` | Chronicle text |
| `GeneratorUtilities.scala` | `Date` | Date formatting |
| `QuestEndedGeneratorUtilities.scala` | `BattalionTypeId`, `UnaffiliatedHeroQuest.*` | Quest text |
### Summary by Directory
| Directory | Files | Imports | Status |
|-----------|-------|---------|--------|
| `ai/` | 0 | 0 | ✅ Clean |
| `actions/availability/` | 0 | 0 | ✅ Clean |
| `actions/impl/action/` | 0 | 0 | ✅ Clean |
| `actions/impl/command/` | 0 | 0 | ✅ Clean |
| `actions/llm_prompt_generators/` | 0 | 0 | ✅ Clean |
| `actions/llm_request_generators/` | 0 | 0 | ✅ Clean |
| `util/command_choice_helpers/` | 0 | 0 | ✅ Clean |
| `util/view_filters/` | 0 | 0 | ✅ Clean (except boundary) |
| `util/` (other) | 0 | 0 | ✅ Clean |
| Root (`library/`) | 4 | 7 | Boundary code (expected) |
### util/view_filters/ (0 imports, except boundary)
| File | Import | Notes |
|------|--------|-------|
| `GameStateViewFilter.scala` | ✅ **0** | Returns Scala `GameStateView`, converts at edge |
| `GameStateViewDiffer.scala` | ✅ **0** | Uses Scala diff types, converts at edge |
| `BattleFilter.scala` | 1 | Battle view boundary (keep proto) |
### util/ other files ✅ Complete
| File | Imports | Notes |
|------|---------|-------|
| `MapGenerator.scala` | ✅ **0** | Returns Scala types (PR #5373) |
### Root library/ (7 imports, 4 files)
| File | Imports | Notes |
|------|---------|-------|
| `ActionResultFilter.scala` | 3 | Action result filtering (boundary, uses Scala types internally) |
| `Engine.scala` | 1 | Trait interface (returns `ActionResultView` proto for client) |
| `EngineImpl.scala` | 1 | Returns proto for persistence (boundary) |
| `ActionWithResultingState.scala` | 2 | Stores both proto and Scala (boundary for persistence) |
### actions/llm_prompt_generators/ ✅ Complete (0 imports)
All 34 files now use Scala `LlmRequestT` types (e.g., `LlmRequestT.DivineMessage`, `LlmRequestT.AllianceOfferMessage`). Converted in PR #5429.
### actions/llm_request_generators/ ✅ Complete (0 imports)
All LLM request generators now use Scala types. The `LlmRequestT` enum provides all request types, and `Status` sealed trait provides diplomacy offer statuses. Converted in PRs #5419-#5428.
| Directory | Files with Proto | Status |
|-----------|------------------|--------|
| `ai/` | 0 | ✅ Clean |
| `actions/availability/` | 2 | ⏳ Hostility proto |
| `actions/impl/action/` | 0 | ✅ Clean |
| `actions/impl/command/` | 0 | ✅ Clean |
| `actions/llm_prompt_generators/` | 4 | ⏳ LLM types |
| `util/command_choice_helpers/` | 1 | ⏳ Hostility proto |
| `util/view_filters/` | 1 | Keep (boundary) |
| `util/faction_utils/` | 1 | ⏳ Hostility proto |
| Root (`library/`) | 4 | Keep (boundary) |
| **Total** | **13** | |
---
+3
View File
@@ -0,0 +1,3 @@
# BUILD dependency baseline - updated Sun Jan 18 15:45:56 PST 2026
# Do not increase these numbers - only decrease!
library_proto_deps=167
+158
View File
@@ -0,0 +1,158 @@
#!/bin/bash
# Check BUILD.bazel dependency constraints
# This script enforces architectural boundaries in the codebase.
#
# Usage:
# ./scripts/check_build_deps.sh # Check all rules
# ./scripts/check_build_deps.sh --ci # CI mode (fail on any violation)
# ./scripts/check_build_deps.sh --count # Just count current violations (for tracking progress)
# ./scripts/check_build_deps.sh --strict # Fail if proto deps in library/ exceed baseline
#
# The baseline for proto deps is stored in scripts/build_deps_baseline.txt
# Update it with: ./scripts/check_build_deps.sh --update-baseline
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
BASELINE_FILE="$SCRIPT_DIR/build_deps_baseline.txt"
cd "$REPO_ROOT"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
MODE="${1:-check}"
EXIT_CODE=0
# Get baseline proto count (default to a high number if no baseline exists)
get_baseline() {
if [ -f "$BASELINE_FILE" ]; then
grep "^library_proto_deps=" "$BASELINE_FILE" | cut -d= -f2
else
echo "999999" # No baseline = don't fail
fi
}
# Rule 1: src/main should not depend on src/test
check_main_depends_on_test() {
echo -e "${YELLOW}Checking: src/main should not depend on src/test...${NC}"
violations=$(bazel query 'deps(//src/main/...) intersect //src/test/...' 2>/dev/null || true)
if [ -n "$violations" ]; then
echo -e "${RED}VIOLATION: src/main depends on src/test:${NC}"
echo "$violations"
return 1
else
echo -e "${GREEN}✓ No violations${NC}"
return 0
fi
}
# Rule 2: library/ should not depend on src/main/protobuf
# Note: This rule is aspirational - there are currently many violations as part of ongoing deproto effort
check_library_depends_on_proto() {
echo -e "${YELLOW}Checking: library/ should not depend on src/main/protobuf...${NC}"
violations=$(bazel query 'deps(//src/main/scala/net/eagle0/eagle/library/...) intersect //src/main/protobuf/...' 2>/dev/null || true)
count=$(echo "$violations" | grep -c "^//" || echo "0")
if [ "$count" -gt 0 ]; then
echo -e "${YELLOW}Found $count proto dependencies in library/ (deproto in progress)${NC}"
if [ "$MODE" == "--count" ]; then
echo "$violations" | head -20
if [ "$count" -gt 20 ]; then
echo "... and $((count - 20)) more"
fi
fi
# Don't fail on this rule yet - it's aspirational
return 0
else
echo -e "${GREEN}✓ No proto dependencies in library/${NC}"
return 0
fi
}
# Count proto deps for tracking deproto progress
count_proto_deps() {
echo -e "${YELLOW}=== Proto dependency counts ===${NC}"
library_count=$(bazel query 'deps(//src/main/scala/net/eagle0/eagle/library/...) intersect //src/main/protobuf/...' 2>/dev/null | grep -c "^//" || echo "0")
echo "library/: $library_count proto dependencies"
baseline=$(get_baseline)
if [ "$baseline" != "999999" ]; then
echo "baseline: $baseline"
if [ "$library_count" -lt "$baseline" ]; then
echo -e "${GREEN}↓ Reduced by $((baseline - library_count)) from baseline${NC}"
elif [ "$library_count" -gt "$baseline" ]; then
echo -e "${RED}↑ Increased by $((library_count - baseline)) from baseline${NC}"
fi
fi
}
# Check if proto deps exceed baseline (for strict mode)
check_proto_baseline() {
library_count=$(bazel query 'deps(//src/main/scala/net/eagle0/eagle/library/...) intersect //src/main/protobuf/...' 2>/dev/null | grep -c "^//" || echo "0")
baseline=$(get_baseline)
echo -e "${YELLOW}Checking: proto deps in library/ should not exceed baseline...${NC}"
echo "Current: $library_count, Baseline: $baseline"
if [ "$library_count" -gt "$baseline" ]; then
echo -e "${RED}VIOLATION: Proto dependencies increased from $baseline to $library_count${NC}"
echo "Run './scripts/check_build_deps.sh --count' to see which protos are used"
return 1
else
echo -e "${GREEN}✓ Proto deps at or below baseline${NC}"
return 0
fi
}
# Update baseline file
update_baseline() {
library_count=$(bazel query 'deps(//src/main/scala/net/eagle0/eagle/library/...) intersect //src/main/protobuf/...' 2>/dev/null | grep -c "^//" || echo "0")
echo "# BUILD dependency baseline - updated $(date)" > "$BASELINE_FILE"
echo "# Do not increase these numbers - only decrease!" >> "$BASELINE_FILE"
echo "library_proto_deps=$library_count" >> "$BASELINE_FILE"
echo -e "${GREEN}Updated baseline: library_proto_deps=$library_count${NC}"
}
echo "=== BUILD.bazel Dependency Check ==="
echo ""
case "$MODE" in
--count)
count_proto_deps
;;
--ci)
check_main_depends_on_test || EXIT_CODE=1
check_library_depends_on_proto || EXIT_CODE=1
;;
--strict)
check_main_depends_on_test || EXIT_CODE=1
check_proto_baseline || EXIT_CODE=1
;;
--update-baseline)
update_baseline
;;
*)
check_main_depends_on_test || EXIT_CODE=1
check_library_depends_on_proto
echo ""
count_proto_deps
;;
esac
echo ""
if [ $EXIT_CODE -eq 0 ]; then
echo -e "${GREEN}=== All checks passed ===${NC}"
else
echo -e "${RED}=== Some checks failed ===${NC}"
fi
exit $EXIT_CODE