feat(ci): add pre-commit FactId validation hook (#393)
Grep-based pre-commit check validating fact_id references in content YAML against canonical knowledge catalogs. Runs in advisory mode when catalogs are stubs (exit 0), switches to enforcing mode once populated (exit 1 on unknown fact_ids with file:line output). - tooling/check-fact-ids: core validation script (<2s runtime) - .config/hooks/pre-commit: hook dispatcher for modular checks - Makefile: check-fact-ids + setup-hooks targets, wired into setup - docs/DEVOPS.md: content validation and pre-commit hooks sections Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Executable
+28
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Pre-commit hook dispatcher. Runs modular checks from tooling/.
|
||||||
|
# Installed via: git config core.hooksPath .config/hooks
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
run_check() {
|
||||||
|
local script="$1"
|
||||||
|
local label="$2"
|
||||||
|
if [ -x "$REPO_ROOT/$script" ]; then
|
||||||
|
if ! "$REPO_ROOT/$script"; then
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "pre-commit: skipping $label ($script not found or not executable)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Checks ---
|
||||||
|
run_check "tooling/check-fact-ids" "fact_id validation"
|
||||||
|
|
||||||
|
if [ "$ERRORS" -gt 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "pre-commit: $ERRORS check(s) failed. Commit aborted."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -2,7 +2,7 @@ GODOT := $(shell command -v godot4 2>/dev/null || command -v godot 2>/dev/null)
|
|||||||
|
|
||||||
.PHONY: help setup build client server game stop test lint ci ci-client ci-server clean \
|
.PHONY: help setup build client server game stop test lint ci ci-client ci-server clean \
|
||||||
decisions-sync decisions-coverage decisions-active decisions-orphan \
|
decisions-sync decisions-coverage decisions-active decisions-orphan \
|
||||||
db-backup db-install validate-content content-ron
|
db-backup db-install validate-content content-ron check-fact-ids setup-hooks
|
||||||
|
|
||||||
# --- Configuration ---
|
# --- Configuration ---
|
||||||
|
|
||||||
@@ -32,14 +32,17 @@ help:
|
|||||||
@echo " make decisions-coverage Decision-to-ticket coverage by domain"
|
@echo " make decisions-coverage Decision-to-ticket coverage by domain"
|
||||||
@echo " make decisions-active List active decisions"
|
@echo " make decisions-active List active decisions"
|
||||||
@echo " make decisions-orphan Decisions without implementing tickets"
|
@echo " make decisions-orphan Decisions without implementing tickets"
|
||||||
@echo " make validate-content Validate content YAML against schemas"
|
@echo " make validate-content Validate content YAML against schemas"
|
||||||
|
@echo " make check-fact-ids Check fact_id references against knowledge catalogs"
|
||||||
@echo " make content-ron Convert content YAML to RON (build-time)"
|
@echo " make content-ron Convert content YAML to RON (build-time)"
|
||||||
@echo ""
|
@echo ""
|
||||||
|
@echo " make setup-hooks Install pre-commit hooks (included in setup)"
|
||||||
|
@echo ""
|
||||||
@echo " GODOT_VERSION=4.6 make setup Override Godot version"
|
@echo " GODOT_VERSION=4.6 make setup Override Godot version"
|
||||||
|
|
||||||
# --- Setup ---
|
# --- Setup ---
|
||||||
|
|
||||||
setup: setup-rust setup-godot setup-tooling decisions-sync
|
setup: setup-rust setup-godot setup-tooling setup-hooks decisions-sync
|
||||||
@echo "Dev environment ready."
|
@echo "Dev environment ready."
|
||||||
|
|
||||||
setup-rust:
|
setup-rust:
|
||||||
@@ -54,6 +57,10 @@ setup-tooling:
|
|||||||
@command -v curl >/dev/null 2>&1 || { echo "Install curl (required for Godot download)"; exit 1; }
|
@command -v curl >/dev/null 2>&1 || { echo "Install curl (required for Godot download)"; exit 1; }
|
||||||
@command -v unzip >/dev/null 2>&1 || { echo "Install unzip (required for Godot download)"; exit 1; }
|
@command -v unzip >/dev/null 2>&1 || { echo "Install unzip (required for Godot download)"; exit 1; }
|
||||||
|
|
||||||
|
setup-hooks:
|
||||||
|
@git config core.hooksPath .config/hooks
|
||||||
|
@echo "Git hooks path set to .config/hooks"
|
||||||
|
|
||||||
# --- Build ---
|
# --- Build ---
|
||||||
|
|
||||||
build: build-server build-client
|
build: build-server build-client
|
||||||
@@ -149,6 +156,9 @@ decisions-orphan:
|
|||||||
validate-content:
|
validate-content:
|
||||||
@tooling/validate-content
|
@tooling/validate-content
|
||||||
|
|
||||||
|
check-fact-ids:
|
||||||
|
@tooling/check-fact-ids
|
||||||
|
|
||||||
content-ron:
|
content-ron:
|
||||||
cd tooling/content-converter && cargo build --release
|
cd tooling/content-converter && cargo build --release
|
||||||
tooling/content-converter/target/release/content-converter --input content --output content-ron --verbose
|
tooling/content-converter/target/release/content-converter --input content --output content-ron --verbose
|
||||||
|
|||||||
@@ -95,6 +95,40 @@ CI targets chain lint → build → test sequentially. A failure in any stage st
|
|||||||
make clean # Remove build artifacts and .cache/ contents
|
make clean # Remove build artifacts and .cache/ contents
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Content Validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make validate-content # Validate content YAML against JSON schemas
|
||||||
|
make check-fact-ids # Check fact_id references against knowledge catalogs
|
||||||
|
```
|
||||||
|
|
||||||
|
`check-fact-ids` operates in two modes:
|
||||||
|
- **Advisory** — when knowledge catalogs (`content/global/knowledge/*.yaml`) have no fact definitions yet: lists referenced fact_ids and exits cleanly.
|
||||||
|
- **Enforcing** — when catalogs are populated: fails on any `fact_id` reference that doesn't match a canonical definition.
|
||||||
|
|
||||||
|
## Pre-commit Hooks
|
||||||
|
|
||||||
|
Git hooks are stored in `.config/hooks/` (version-controlled). Activate them with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make setup # Includes hook installation
|
||||||
|
make setup-hooks # Just hooks
|
||||||
|
```
|
||||||
|
|
||||||
|
Or manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config core.hooksPath .config/hooks
|
||||||
|
```
|
||||||
|
|
||||||
|
Active checks:
|
||||||
|
|
||||||
|
| Check | Script | Behavior |
|
||||||
|
|-------|--------|----------|
|
||||||
|
| fact_id validation | `tooling/check-fact-ids` | Warns if catalogs are stubs; fails on unknown fact_ids when populated |
|
||||||
|
|
||||||
|
The `core.hooksPath` setting uses a relative path (`.config/hooks`) that resolves per worktree, so it works correctly across all worktrees in the repository.
|
||||||
|
|
||||||
## Configuration Files
|
## Configuration Files
|
||||||
|
|
||||||
The `.config/` directory holds shared configuration for linters, formatters, and CI. Examples of what goes here:
|
The `.config/` directory holds shared configuration for linters, formatters, and CI. Examples of what goes here:
|
||||||
|
|||||||
Executable
+85
@@ -0,0 +1,85 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Validate fact_id references in content YAML against canonical knowledge catalogs.
|
||||||
|
# Part of pre-commit checks (ticket #393). Grep-based, targets <2s runtime.
|
||||||
|
#
|
||||||
|
# Modes:
|
||||||
|
# Advisory — when knowledge catalogs have no fact definitions yet (exit 0)
|
||||||
|
# Enforcing — when catalogs are populated; fails on unknown fact_ids (exit 1)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||||
|
|
||||||
|
KNOWLEDGE_DIR="$REPO_ROOT/content/global/knowledge"
|
||||||
|
CONTENT_DIR="$REPO_ROOT/content/campaigns"
|
||||||
|
|
||||||
|
# --- Extract canonical fact_ids from knowledge catalogs ---
|
||||||
|
# Matches YAML lines like: fact_id: some_value or fact_id: "some_value"
|
||||||
|
# Excludes entity-attributes.yaml (different schema: attribute keys, not fact_ids)
|
||||||
|
CANONICAL_IDS=""
|
||||||
|
if [ -d "$KNOWLEDGE_DIR" ]; then
|
||||||
|
CANONICAL_IDS=$(
|
||||||
|
grep -rh 'fact_id:\s*' "$KNOWLEDGE_DIR" \
|
||||||
|
--include='*.yaml' \
|
||||||
|
--exclude='entity-attributes.yaml' \
|
||||||
|
| sed 's/.*fact_id:\s*//' \
|
||||||
|
| sed 's/\s*#.*//' \
|
||||||
|
| sed "s/^[\"']\(.*\)[\"']$/\1/" \
|
||||||
|
| sort -u \
|
||||||
|
|| true
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
CANONICAL_COUNT=$(echo "$CANONICAL_IDS" | grep -c '\S' || true)
|
||||||
|
|
||||||
|
# --- Extract referenced fact_ids from campaign content ---
|
||||||
|
# Covers monologue prerequisites.facts[].fact_id and dialogue knowledge_grant.fact_id
|
||||||
|
REFERENCED=""
|
||||||
|
if [ -d "$CONTENT_DIR" ]; then
|
||||||
|
REFERENCED=$(
|
||||||
|
grep -rh 'fact_id:\s*' "$CONTENT_DIR" \
|
||||||
|
--include='*.yaml' \
|
||||||
|
| sed 's/.*fact_id:\s*//' \
|
||||||
|
| sed 's/\s*#.*//' \
|
||||||
|
| sed "s/^[\"']\(.*\)[\"']$/\1/" \
|
||||||
|
|| true
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
REFERENCED_UNIQUE=$(echo "$REFERENCED" | sort -u | grep '\S' || true)
|
||||||
|
REF_COUNT=$(echo "$REFERENCED_UNIQUE" | grep -c '\S' || true)
|
||||||
|
|
||||||
|
# --- Compare ---
|
||||||
|
if [ "$CANONICAL_COUNT" -eq 0 ]; then
|
||||||
|
echo "check-fact-ids: WARNING — no canonical fact_ids in knowledge catalogs"
|
||||||
|
echo " Catalogs not yet populated. Check is advisory only."
|
||||||
|
if [ "$REF_COUNT" -gt 0 ]; then
|
||||||
|
echo " $REF_COUNT unique fact_ids referenced in content:"
|
||||||
|
echo "$REFERENCED_UNIQUE" | sed 's/^/ /'
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Enforcing mode: catalogs have definitions
|
||||||
|
ERRORS=0
|
||||||
|
while IFS= read -r ref_id; do
|
||||||
|
[ -z "$ref_id" ] && continue
|
||||||
|
if ! echo "$CANONICAL_IDS" | grep -qxF "$ref_id"; then
|
||||||
|
echo "ERROR: unknown fact_id '$ref_id' — not in knowledge catalogs"
|
||||||
|
grep -rn "fact_id:\s*$ref_id" "$CONTENT_DIR" --include='*.yaml' \
|
||||||
|
| sed "s|$REPO_ROOT/||" \
|
||||||
|
| sed 's/^/ /'
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done <<< "$REFERENCED_UNIQUE"
|
||||||
|
|
||||||
|
if [ "$ERRORS" -gt 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "check-fact-ids: FAILED — $ERRORS unknown fact_id(s)"
|
||||||
|
echo " Canonical fact_ids defined in: content/global/knowledge/*.yaml"
|
||||||
|
echo " Run 'make check-fact-ids' to recheck."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "check-fact-ids: OK — $REF_COUNT references validated against $CANONICAL_COUNT canonical facts"
|
||||||
|
exit 0
|
||||||
Reference in New Issue
Block a user