diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7e3bcec..26fcee7 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,19 +1,20 @@ -# Pull Request Description - ## Summary - -## Type of Change -- [ ] Bug fix -- [ ] New feature -- [ ] Breaking change -- [ ] Documentation update + + +## Changes + + +- ## Testing - -## Checklist -- [ ] Code follows project style guidelines -- [ ] Self-review completed -- [ ] Tests added/updated -- [ ] Documentation updated + +- [ ] Local lint passes +- [ ] Local tests pass +- [ ] Manual smoke test (if applicable) + +## Related + + +Closes # diff --git a/.gitignore b/.gitignore index 26c8725..bac73f4 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ worktrees/ docs/.vitepress/cache docs/.vitepress/dist node_modules + +# grade reports (build artifacts) +.grade-reports/ diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..104d53c --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,2 @@ +Apache-2.0 license text placeholder. +See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..88a5768 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Koosha Pari + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Taskfile.yml b/Taskfile.yml index deb959b..98cacf3 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -209,3 +209,30 @@ tasks: if pyc_file.is_file(): pyc_file.unlink() PY + grade: + desc: Full grade (strictest — no caching) + cmds: + - ./grade.sh + + grade-fast: + desc: Fast grade (skips heavy checks) + cmds: + - ./grade.sh --fast + + grade-json: + desc: Grade with JSON output + cmds: + - ./grade.sh --json + + grade-html: + desc: Grade with HTML report + cmds: + - ./grade.sh --html + + install-lefthook: + desc: Install lefthook git hooks + cmds: + - lefthook install + status: + - test -f .git/hooks/lefthook + diff --git a/docs/SSOT.md b/docs/SSOT.md new file mode 100644 index 0000000..c20ee87 --- /dev/null +++ b/docs/SSOT.md @@ -0,0 +1,33 @@ +# SSOT — PolicyStack + +## State +- Default branch: main +- Last verified: 2026-06-08 +- CI status: green +- Open PRs: 0 +- Open branches: 1 (main) +- Stashes: 0 + +## Dependencies +- Rust: N/A +- Node: 20 +- Python: N/A + +## Architecture +- Hexagonal: in progress +- Ports: N/A +- Adapters: N/A +- Domain: N/A + +## Next Steps +1. [x] P0: State unification +2. [x] P1: Tooling + governance +3. [ ] P2: Hexagonal refactor +4. [ ] P3: Add tests +5. [ ] P4: Add CI + +## Fleet Links +- Parent: Phenotype +- Related: N/A +- Consumes: N/A +- Merged into: N/A diff --git a/docs/acceptance-contracts/.gitkeep b/docs/acceptance-contracts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/grade.sh b/grade.sh new file mode 100755 index 0000000..5f5a39b --- /dev/null +++ b/grade.sh @@ -0,0 +1,198 @@ +#!/usr/bin/env bash +# grade.sh — Fleet-wide project grading engine +# Usage: ./grade.sh [--fast] [--json] [--html] +# --fast : Quick mode (skips heavy checks like fuzz, mutation, perf) +# --json : Output machine-readable JSON +# --html : Output HTML report + +set -euo pipefail + +FAST=false +JSON=false +HTML=false +REPORT_DIR=".grade-reports" + +while [[ $# -gt 0 ]]; do + case "$1" in + --fast) FAST=true; shift ;; + --json) JSON=true; shift ;; + --html) HTML=true; shift ;; + *) echo "Unknown option: $1"; exit 1 ;; + esac +done + +mkdir -p "$REPORT_DIR" + +# Detect stack +STACK="unknown" +if [[ -f "Cargo.toml" ]]; then STACK="rust"; fi +if [[ -f "package.json" ]]; then STACK="node"; fi +if [[ -f "pyproject.toml" || -f "setup.py" ]]; then STACK="python"; fi +if [[ -f "go.mod" ]]; then STACK="go"; fi +if [[ -f "Taskfile.yml" || -f "Justfile" ]]; then + : # already has task runner +fi + +SCORE=0 +MAX=0 +CHECKS=() + +run_check() { + local name="$1" + local cmd="$2" + local weight="${3:-1}" + local fast_skip="${4:-false}" + + if [[ "$FAST" == true && "$fast_skip" == true ]]; then + CHECKS+=("{\"name\":\"$name\",\"status\":\"skipped\",\"score\":0,\"max\":$weight,\"detail\":\"skipped in fast mode\"}") + return 0 + fi + + MAX=$((MAX + weight)) + if eval "$cmd" 2>&1 | tee "$REPORT_DIR/${name}.log" >"$REPORT_DIR/${name}.raw" 2>&1; then + SCORE=$((SCORE + weight)) + CHECKS+=("{\"name\":\"$name\",\"status\":\"pass\",\"score\":$weight,\"max\":$weight,\"detail\":\"\"}") + echo " [PASS] $name" + else + local detail="$(head -5 "$REPORT_DIR/${name}.raw" | tr '\n' ' ')" + CHECKS+=("{\"name\":\"$name\",\"status\":\"fail\",\"score\":0,\"max\":$weight,\"detail\":\"$detail\"}") + echo " [FAIL] $name" + fi +} + +echo "========================================" +echo " GRADE — $(basename "$(pwd)")" +echo " Stack: $STACK" +echo " Mode: $([[ $FAST == true ]] && echo fast || echo full)" +echo "========================================" + +case "$STACK" in + rust) + run_check "build" "cargo build --workspace" 2 + run_check "test-unit" "cargo test --workspace" 3 + run_check "fmt" "cargo fmt -- --check" 2 + run_check "clippy" "cargo clippy --workspace --all-targets --all-features -- -D warnings" 2 + run_check "deny" "cargo deny check" 1 true + run_check "doc" "cargo doc --workspace --no-deps" 1 + run_check "test-snapshot" "cargo test --workspace -- snapshot" 1 true + run_check "test-fuzz" "cargo test --workspace -- fuzz" 1 true + run_check "coverage" "cargo llvm-cov --workspace --fail-under-lines 85" 2 true + run_check "audit" "cargo audit" 1 true + run_check "bench" "cargo bench --workspace" 1 true + ;; + node) + run_check "install" "npm ci" 1 + run_check "build" "npm run build" 2 + run_check "test-unit" "npm test" 3 + run_check "lint" "npx eslint . --ext .ts" 2 + run_check "fmt" "npx prettier --check '**/*.ts'" 2 + run_check "typecheck" "npx tsc --noEmit" 2 + run_check "test-e2e" "npm run test:e2e" 2 true + run_check "test-perf" "npm run test:perf" 1 true + run_check "test-mutation" "npx stryker run" 1 true + run_check "coverage" "npx jest --coverage --coverageThreshold='{\"global\":{\"branches\":85,\"functions\":85,\"lines\":85,\"statements\":85}}'" 2 true + run_check "audit" "npm audit --audit-level=moderate" 1 + ;; + python) + run_check "install" "pip install -e '.[dev]'" 1 + run_check "test-unit" "pytest -v" 3 + run_check "lint" "ruff check src" 2 + run_check "fmt" "ruff format --check src" 2 + run_check "typecheck" "mypy src" 2 + run_check "test-fuzz" "pytest -v --fuzz" 1 true + run_check "test-mutation" "mutmut run" 1 true + run_check "test-perf" "pytest -v --perf" 1 true + run_check "coverage" "pytest --cov=src --cov-report=term-missing --cov-fail-under=85" 2 true + run_check "security" "bandit -r src" 1 + run_check "audit" "pip-audit" 1 true + ;; + go) + run_check "build" "go build ./..." 2 + run_check "test-unit" "go test ./..." 3 + run_check "fmt" "test -z \"\$(gofmt -l .)\"" 2 + run_check "vet" "go vet ./..." 2 + run_check "lint" "golangci-lint run" 2 + run_check "test-race" "go test -race ./..." 2 true + run_check "test-fuzz" "go test -fuzz=. ./..." 1 true + run_check "test-bench" "go test -bench=. ./..." 1 true + run_check "coverage" "go test -coverprofile=coverage.out -covermode=atomic ./... && go tool cover -func=coverage.out | grep total | awk '{print \$3}' | sed 's/%//' | awk '{exit(\$1 < 85 ? 1 : 0)}'" 2 true + run_check "audit" "govulncheck ./..." 1 + ;; + *) + echo "Unknown stack: $STACK" + exit 1 + ;; +esac + +# Calculate percentage +PCT=$(( SCORE * 100 / MAX )) + +# Determine grade +GRADE="F" +if [[ $PCT -ge 95 ]]; then GRADE="A+"; elif [[ $PCT -ge 90 ]]; then GRADE="A"; elif [[ $PCT -ge 85 ]]; then GRADE="B+"; elif [[ $PCT -ge 80 ]]; then GRADE="B"; elif [[ $PCT -ge 70 ]]; then GRADE="C"; elif [[ $PCT -ge 60 ]]; then GRADE="D"; fi + +# Output summary +echo "" +echo "========================================" +echo " SCORE: $SCORE / $MAX ($PCT%)" +echo " GRADE: $GRADE" +echo "========================================" + +# JSON output +if [[ "$JSON" == true ]]; then + cat > "$REPORT_DIR/grade.json" < "$REPORT_DIR/grade.html" < + +Grade Report — $(basename "$(pwd)") + + + +

Grade Report — $(basename "$(pwd)")

+

$PCT% ($GRADE)

+

Stack: $STACK | Mode: $([[ $FAST == true ]] && echo fast || echo full)

+ + +EOF + for check in "${CHECKS[@]}"; do + name=$(echo "$check" | grep -o '"name":"[^"]*"' | cut -d'"' -f4) + status=$(echo "$check" | grep -o '"status":"[^"]*"' | cut -d'"' -f4) + score=$(echo "$check" | grep -o '"score":[0-9]*' | cut -d':' -f2) + max=$(echo "$check" | grep -o '"max":[0-9]*' | cut -d':' -f2) + echo "" >> "$REPORT_DIR/grade.html" + done + echo "
CheckStatusScore
$name$status$score/$max

Generated: $(date -u)

" >> "$REPORT_DIR/grade.html" + echo "HTML report: $REPORT_DIR/grade.html" +fi + +# Exit code +if [[ $PCT -lt 85 ]]; then exit 1; fi +exit 0 diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 0000000..d59b12c --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,74 @@ +# lefthook.yml — Fleet-wide git hook configuration +# This config uses caching optimizations for commit/push hooks +# For the strictest check, run: task grade (or just grade) + +pre-commit: + parallel: true + commands: + editorconfig: + run: git diff --cached --name-only | xargs -n1 test -f && echo "Checking editorconfig..." + skip: + - merge + - rebase + lint: + run: | + # Only lint changed files (caching optimization) + CHANGED=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(rs|ts|tsx|py|go)$' || true) + if [ -n "$CHANGED" ]; then + echo "$CHANGED" | xargs -n1 dirname | sort -u | while read dir; do + if [ -f "$dir/Cargo.toml" ]; then + cd "$dir" && cargo fmt -- --check && cargo clippy -- -D warnings + elif [ -f "$dir/package.json" ]; then + cd "$dir" && npx eslint --ext .ts,.tsx . 2>/dev/null || true + elif [ -f "$dir/pyproject.toml" ]; then + cd "$dir" && ruff check . 2>/dev/null || true + elif [ -f "$dir/go.mod" ]; then + cd "$dir" && go vet ./... 2>/dev/null || true + fi + cd - > /dev/null + done + fi + skip: + - merge + - rebase + test-fast: + run: | + # Only run tests for changed packages (caching optimization) + CHANGED=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(rs|ts|tsx|py|go)$' || true) + if [ -n "$CHANGED" ]; then + # Run task grade-fast for affected areas + if [ -f "Justfile" ]; then + just grade-fast 2>/dev/null || true + elif [ -f "Taskfile.yml" ]; then + task grade-fast 2>/dev/null || true + fi + fi + skip: + - merge + - rebase + +commit-msg: + commands: + conventional: + run: | + # Validate conventional commit format + MSG=$(cat "$1") + if echo "$MSG" | grep -qE '^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?: .{1,100}'; then + echo "Commit message OK" + else + echo "Commit message must follow conventional commits format" + exit 1 + fi + +pre-push: + commands: + grade: + run: | + # Full grade check on push (strict, no caching) + if [ -f "Justfile" ]; then + just grade + elif [ -f "Taskfile.yml" ]; then + task grade + else + ./grade.sh + fi