diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 000000000..b33aa3884
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,31 @@
+## Change summary
+
+- Area: research-core | research-standards | theory-history | book-writing | thai-policy | services-tools | repo-ops | other
+- Canonical workspace/path:
+- What changed:
+- Why this change is needed:
+
+## Evidence and validation
+
+- Checks actually run:
+- Result or artifact produced:
+- Claim/readiness impact:
+- Next blocker or follow-up:
+
+## Public boundary
+
+- [ ] No .env, credentials, private exports, raw media, build cache, binary, or generated database is included.
+- [ ] Any excluded raw/private input is named in a manifest or ledger entry when needed.
+- [ ] No duplicate semantic path or stale alias was created.
+- [ ] git diff --check was run.
+
+## Work history
+
+- [ ] A factual WORK_LEDGER/YYYY/YYYY-MM-DD.md entry was added.
+- [ ] The relevant topic/book UPDATE_LOG.md was updated, or this change does not require one.
+- [ ] The staged file list is limited to this coherent unit.
+
+## Merge readiness
+
+- [ ] This PR is ready to merge.
+- [ ] This PR should remain a draft.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
deleted file mode 100644
index 0bbc21b5f..000000000
--- a/.github/workflows/ci.yml
+++ /dev/null
@@ -1,219 +0,0 @@
-name: UET Research Corpus CI
-
-on:
- push:
- branches: [main]
- pull_request:
- branches: [main]
-
-env:
- CARGO_TERM_COLOR: always
-
-jobs:
- # ==================== Rust Backend ====================
- rust-check:
- name: Rust Check
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Detect Rust workspace
- id: rust-workspace
- run: |
- if [ -f Cargo.toml ]; then
- echo "present=true" >> "$GITHUB_OUTPUT"
- else
- echo "present=false" >> "$GITHUB_OUTPUT"
- echo "No root Cargo.toml found; skipping Rust checks for this corpus-only checkout."
- fi
-
- - name: Install Rust
- if: steps.rust-workspace.outputs.present == 'true'
- uses: dtolnay/rust-toolchain@stable
- with:
- components: clippy, rustfmt
-
- - name: Cache cargo
- if: steps.rust-workspace.outputs.present == 'true'
- uses: actions/cache@v4
- with:
- path: |
- ~/.cargo/registry
- ~/.cargo/git
- target
- key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- restore-keys: |
- ${{ runner.os }}-cargo-
-
- - name: Check formatting
- if: steps.rust-workspace.outputs.present == 'true'
- run: cargo fmt --all -- --check
-
- - name: Clippy
- if: steps.rust-workspace.outputs.present == 'true'
- run: cargo clippy --workspace --all-targets -- -D warnings
- continue-on-error: true
-
- - name: Build
- if: steps.rust-workspace.outputs.present == 'true'
- run: cargo build --release --workspace
-
- # ==================== Next.js Frontend ====================
- web-check:
- name: Next.js Check
- runs-on: ubuntu-latest
- defaults:
- run:
- working-directory: ./uet_web
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Detect Next.js app
- id: next-app
- working-directory: .
- run: |
- if [ -f uet_web/package-lock.json ]; then
- echo "present=true" >> "$GITHUB_OUTPUT"
- else
- echo "present=false" >> "$GITHUB_OUTPUT"
- echo "No uet_web/package-lock.json found; skipping Next.js checks for this corpus-only checkout."
- fi
-
- - name: Setup Node.js
- if: steps.next-app.outputs.present == 'true'
- uses: actions/setup-node@v4
- with:
- node-version: '20'
- cache: 'npm'
- cache-dependency-path: './uet_web/package-lock.json'
-
- - name: Install dependencies
- if: steps.next-app.outputs.present == 'true'
- run: npm ci --legacy-peer-deps
-
- - name: Lint
- if: steps.next-app.outputs.present == 'true'
- run: npm run lint
- continue-on-error: true
-
- - name: Build
- if: steps.next-app.outputs.present == 'true'
- run: npm run build
- env:
- NEXT_TELEMETRY_DISABLED: 1
- DATABASE_URL: "postgresql://fake:fake@localhost:5432/fake"
-
- # ==================== Optional Python Service Checks ====================
- python-test:
- name: Python Tests
- runs-on: ubuntu-latest
- strategy:
- matrix:
- python-version: ["3.10", "3.11", "3.12"]
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v5
- with:
- python-version: ${{ matrix.python-version }}
-
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install pytest
-
- - name: Run optional Python agent tests
- run: |
- if find services_and_experiments/uet_agents -type f \( -name 'test_*.py' -o -name '*_test.py' \) | grep -q .; then
- pytest services_and_experiments/uet_agents/ -v --tb=short
- else
- echo "No service tests found; skipping optional agent checks."
- fi
-
- # ==================== Docker Build ====================
- docker-build:
- name: Docker Build
- runs-on: ubuntu-latest
- needs: [rust-check, web-check]
- if: vars.UET_PLATFORM_DEPLOY == 'true' && github.event_name == 'push' && github.ref == 'refs/heads/main'
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Detect Dockerfiles
- id: dockerfiles
- run: |
- if [ -f Dockerfile.api ] || [ -f Dockerfile.web ] || [ -f Dockerfile.agents ]; then
- echo "present=true" >> "$GITHUB_OUTPUT"
- else
- echo "present=false" >> "$GITHUB_OUTPUT"
- echo "No platform Dockerfiles found; skipping Docker image builds for this corpus-only checkout."
- fi
-
- - name: Set up Docker Buildx
- if: steps.dockerfiles.outputs.present == 'true'
- uses: docker/setup-buildx-action@v3
-
- - name: Build API image
- if: steps.dockerfiles.outputs.present == 'true' && hashFiles('Dockerfile.api') != ''
- uses: docker/build-push-action@v5
- with:
- context: .
- file: ./Dockerfile.api
- push: false
- tags: uet_api:${{ github.sha }}
- cache-from: type=gha
- cache-to: type=gha,mode=max
-
- - name: Build Web image
- if: steps.dockerfiles.outputs.present == 'true' && hashFiles('Dockerfile.web') != ''
- uses: docker/build-push-action@v5
- with:
- context: .
- file: ./Dockerfile.web
- push: false
- tags: uet_web:${{ github.sha }}
- build-args: |
- DATABASE_URL=postgresql://fake:fake@localhost:5432/fake
- cache-from: type=gha
- cache-to: type=gha,mode=max
-
- - name: Build Agent image
- if: steps.dockerfiles.outputs.present == 'true' && hashFiles('Dockerfile.agents') != ''
- uses: docker/build-push-action@v5
- with:
- context: .
- file: ./Dockerfile.agents
- push: false
- tags: uet_agents:${{ github.sha }}
- cache-from: type=gha
- cache-to: type=gha,mode=max
-
- # ==================== Optional future-platform deployment ====================
- deploy:
- name: Deploy to Railway
- runs-on: ubuntu-latest
- needs: [docker-build]
- if: vars.UET_PLATFORM_DEPLOY == 'true' && github.event_name == 'push' && github.ref == 'refs/heads/main'
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Install Railway CLI
- run: npm install -g @railway/cli
-
- - name: Deploy API
- run: railway up --service uet-api
- env:
- RAILWAY_TOKEN: ${{ secrets.RAILWAY_API_TOKEN }}
- continue-on-error: true
-
- - name: Deploy Web
- run: railway up --service uet-web
- env:
- RAILWAY_TOKEN: ${{ secrets.RAILWAY_WEB_TOKEN }}
- continue-on-error: true
diff --git a/.github/workflows/main-validation.yml b/.github/workflows/main-validation.yml
new file mode 100644
index 000000000..0ccc8d7fd
--- /dev/null
+++ b/.github/workflows/main-validation.yml
@@ -0,0 +1,153 @@
+
+name: Main Validation
+
+on:
+ push:
+ branches: [main]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: main-validation-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ public-boundary:
+ name: Public boundary and JSON validation
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Check new public-boundary paths
+ env:
+ BEFORE_SHA: ${{ github.event.before }}
+ CURRENT_SHA: ${{ github.sha }}
+ run: |
+ python - <<'PY'
+ import os
+ import subprocess
+ from pathlib import Path
+
+ media = {".pdf", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".mp3", ".m4a", ".wav", ".mp4", ".pptx"}
+ before = os.environ.get("BEFORE_SHA", "")
+ current = os.environ.get("CURRENT_SHA", "HEAD")
+ if before and set(before) != {"0"}:
+ changed = subprocess.check_output(["git", "diff", "--name-only", before, current], text=True).splitlines()
+ else:
+ changed = []
+
+ def blocked(path):
+ lower = path.lower().replace("\\", "/")
+ parts = set(lower.split("/"))
+ suffix = Path(lower).suffix
+ name = Path(lower).name
+ return (
+ name.startswith(".env")
+ or suffix in {".pem", ".key", ".p12", ".pfx", ".exe", ".dll", ".so", ".zip", ".7z", ".sqlite", ".sqlite3", ".db"}
+ or bool(parts & {"target", "node_modules", ".next", "__pycache__", ".pytest_cache", "debug", "build", "dist"})
+ or any(f"/{marker}/" in f"/{lower}/" for marker in ("1_raw", "ch_drafts", "raw"))
+ or (lower.startswith("thailand_proposals/") and suffix in media)
+ )
+
+ tracked = subprocess.check_output(["git", "ls-files"], text=True).splitlines()
+ baseline = sorted({path for path in tracked if blocked(path)})
+ new = sorted({path for path in changed if blocked(path)})
+ Path("public-boundary-baseline.txt").write_text("\n".join(baseline) + "\n", encoding="utf-8")
+ print(f"Existing baseline violations: {len(baseline)}")
+ if baseline:
+ print(*baseline, sep="\n")
+ if new:
+ print("New public-boundary violations:")
+ print(*new, sep="\n")
+ raise SystemExit(1)
+ print("No new public-boundary violations in this push.")
+ - name: Upload public-boundary baseline
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: public-boundary-baseline
+ path: public-boundary-baseline.txt
+ if-no-files-found: warn
+ - name: Validate all tracked JSON
+ run: |
+ set -euo pipefail
+ git ls-files '*.json' -z | while IFS= read -r -d '' file; do
+ python -m json.tool "$file" >/dev/null
+ done
+ - name: Check whitespace
+ run: git diff --check
+
+ python-validation:
+ name: Repository Python validation
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+ - run: python -m compileall -q docs services_and_experiments
+
+ rust-validation:
+ name: Optional Rust validation
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@v4
+ - id: rust-project
+ name: Detect Rust project
+ run: |
+ if test -f services_and_experiments/uet_core/Cargo.toml; then
+ echo "present=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "present=false" >> "$GITHUB_OUTPUT"
+ fi
+ - uses: dtolnay/rust-toolchain@stable
+ if: steps.rust-project.outputs.present == 'true'
+ with:
+ toolchain: stable
+ components: rustfmt, clippy
+ - name: Check Rust format
+ if: steps.rust-project.outputs.present == 'true'
+ run: cargo fmt --manifest-path services_and_experiments/uet_core/Cargo.toml --all -- --check
+ - name: Check Rust compilation
+ if: steps.rust-project.outputs.present == 'true'
+ run: cargo check --manifest-path services_and_experiments/uet_core/Cargo.toml --all-targets
+ - name: Record absence
+ if: steps.rust-project.outputs.present != 'true'
+ run: echo "No services_and_experiments/uet_core/Cargo.toml found; Rust validation is not applicable."
+
+ research-full:
+ name: Full research audit
+ runs-on: ubuntu-latest
+ timeout-minutes: 45
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+ - run: python -m pip install --upgrade pytest
+ - name: Run repository research tests
+ run: |
+ set -euo pipefail
+ if find docs -type f \( -name 'test_*.py' -o -name '*_test.py' \) | grep -q .; then
+ python -m pytest docs/core -q --disable-warnings --maxfail=20
+ else
+ echo "No repository research tests found."
+ fi
+ - name: Diagnostic baseline without legacy collection
+ if: always()
+ run: |
+ echo "Diagnostic only: the full suite above remains authoritative."
+ python -m pytest docs/core -q --disable-warnings --maxfail=20 --ignore=docs/core/data/scripts/Legacy
+ - name: Upload research report
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: main-research-report
+ path: docs/**/Result/**/*
+ if-no-files-found: ignore
diff --git a/.github/workflows/nightly-research-audit.yml b/.github/workflows/nightly-research-audit.yml
new file mode 100644
index 000000000..e03081008
--- /dev/null
+++ b/.github/workflows/nightly-research-audit.yml
@@ -0,0 +1,60 @@
+
+name: Nightly Research Audit
+
+on:
+ schedule:
+ - cron: '17 19 * * *'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: nightly-research-audit
+ cancel-in-progress: true
+
+jobs:
+ audit:
+ name: Full scheduled research audit
+ runs-on: ubuntu-latest
+ timeout-minutes: 60
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+ - run: python -m pip install --upgrade pytest
+ - name: Compile repository sources
+ run: python -m compileall -q docs services_and_experiments
+ - name: Run full research tests
+ run: |
+ set -euo pipefail
+ if find docs -type f \( -name 'test_*.py' -o -name '*_test.py' \) | grep -q .; then
+ python -m pytest docs/core -q --disable-warnings --maxfail=20
+ else
+ echo "No repository research tests found."
+ fi
+ - name: Diagnostic baseline without legacy collection
+ if: always()
+ run: |
+ echo "Diagnostic only: the full suite above remains authoritative."
+ python -m pytest docs/core -q --disable-warnings --maxfail=20 --ignore=docs/core/data/scripts/Legacy
+ - name: Run available core audit
+ if: always()
+ run: |
+ set -euo pipefail
+ script="docs/scripts/audit/audit_core_research_hardening.py"
+ if test -f "$script"; then
+ python "$script"
+ else
+ echo "Core hardening audit script is not present; test audit remains the scheduled check."
+ fi
+ - name: Upload nightly reports
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: nightly-research-report
+ path: |
+ docs/**/Result/**/*
+ docs/**/_Logs/**/*
+ if-no-files-found: ignore
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
new file mode 100644
index 000000000..57837a0d2
--- /dev/null
+++ b/.github/workflows/pages.yml
@@ -0,0 +1,45 @@
+
+name: GitHub Pages
+
+on:
+ push:
+ branches: [main]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: pages
+ cancel-in-progress: true
+
+jobs:
+ build:
+ name: Build public documentation
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/configure-pages@v5
+ - name: Build with Jekyll
+ uses: actions/jekyll-build-pages@v1
+ with:
+ source: ./docs
+ destination: ./_site
+ - name: Upload Pages artifact
+ uses: actions/upload-pages-artifact@v3
+
+ deploy:
+ name: Deploy GitHub Pages
+ needs: build
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - name: Deploy Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.github/workflows/pr-scope.yml b/.github/workflows/pr-scope.yml
new file mode 100644
index 000000000..b142972a1
--- /dev/null
+++ b/.github/workflows/pr-scope.yml
@@ -0,0 +1,103 @@
+
+name: PR Scope and Safety
+
+on:
+ pull_request:
+ branches: [main]
+ types: [opened, synchronize, reopened, ready_for_review]
+
+permissions:
+ contents: read
+ pull-requests: read
+
+concurrency:
+ group: pr-scope-${{ github.event.pull_request.number }}
+ cancel-in-progress: true
+
+jobs:
+ scope-safety:
+ name: Scope and public boundary
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Validate branch naming
+ env:
+ HEAD_BRANCH: ${{ github.head_ref }}
+ run: |
+ set -euo pipefail
+ [[ "$HEAD_BRANCH" =~ ^codex/(research|book|history|policy|services|repo)/[^/[:space:]]+$ ]] || {
+ echo "Use codex//."
+ exit 1
+ }
+ - name: Collect changed files
+ env:
+ BASE_SHA: ${{ github.event.pull_request.base.sha }}
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
+ run: |
+ git diff --name-only "$BASE_SHA" "$HEAD_SHA" | sort -u > changed-files.txt
+ test -s changed-files.txt
+ - name: Check whitespace and JSON
+ run: |
+ set -euo pipefail
+ git diff --check ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }}
+ while IFS= read -r file; do
+ case "$file" in
+ *.json) python -m json.tool "$file" >/dev/null ;;
+ esac
+ done < changed-files.txt
+ - name: Check public boundary
+ run: |
+ python - <<'PY'
+ from pathlib import Path
+ blocked = []
+ for raw in Path("changed-files.txt").read_text().splitlines():
+ path = raw.replace("\\", "/")
+ lower = path.lower()
+ suffix = Path(lower).suffix
+ parts = set(lower.split("/"))
+ name = Path(lower).name
+ if name.startswith(".env") or suffix in {".pem", ".key", ".p12", ".pfx", ".exe", ".dll", ".so", ".zip", ".sqlite", ".sqlite3", ".db"}:
+ blocked.append(path)
+ if parts & {"target", "node_modules", ".next", "__pycache__", ".pytest_cache", "debug", "build", "dist"}:
+ blocked.append(path)
+ if any(f"/{marker}/" in f"/{lower}/" for marker in ("1_raw", "ch_drafts", "raw")):
+ blocked.append(path)
+ if lower.startswith("thailand_proposals/") and suffix in {".pdf", ".png", ".jpg", ".jpeg", ".mp3", ".m4a", ".wav"}:
+ blocked.append(path)
+ if blocked:
+ print(*sorted(set(blocked)), sep="\\n")
+ raise SystemExit("Blocked public-boundary files")
+ PY
+ - name: Check path drift and workflow side effects
+ run: |
+ set -euo pipefail
+ if grep -Eq '^(docs/topics/For_Work/|uet_history/3_publish/books/origin_of_wealth_and_economics/)' changed-files.txt; then
+ echo "Known non-canonical path changed."
+ exit 1
+ fi
+ python - <<'PY'
+ from pathlib import Path
+ changed = Path("changed-files.txt").read_text().splitlines()
+ forbidden = ("git commit", "git push", "gh pr merge")
+ violations = []
+ for raw in changed:
+ path = raw.replace("\\", "/")
+ if path.startswith(".github/workflows/") and path != ".github/workflows/pr-scope.yml" and Path(path).is_file():
+ text = Path(path).read_text(encoding="utf-8")
+ for token in forbidden:
+ if token in text:
+ violations.append(f"{path}: {token}")
+ if violations:
+ print(*violations, sep="\n")
+ raise SystemExit("Workflows must not commit or push source changes.")
+ PY
+ - name: Upload report
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: pr-scope-report
+ path: changed-files.txt
+ if-no-files-found: warn
diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml
new file mode 100644
index 000000000..5388b67af
--- /dev/null
+++ b/.github/workflows/pr-validation.yml
@@ -0,0 +1,170 @@
+
+name: PR Changed-Area Validation
+
+on:
+ pull_request:
+ branches: [main]
+ types: [opened, synchronize, reopened, ready_for_review]
+
+permissions:
+ contents: read
+
+concurrency:
+ group: pr-validation-${{ github.event.pull_request.number }}
+ cancel-in-progress: true
+
+jobs:
+ detect:
+ name: Detect changed areas
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ outputs:
+ research: ${{ steps.areas.outputs.research }}
+ books: ${{ steps.areas.outputs.books }}
+ services: ${{ steps.areas.outputs.services }}
+ policy: ${{ steps.areas.outputs.policy }}
+ docs: ${{ steps.areas.outputs.docs }}
+ python: ${{ steps.areas.outputs.python }}
+ rust: ${{ steps.areas.outputs.rust }}
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - id: areas
+ env:
+ BASE_SHA: ${{ github.event.pull_request.base.sha }}
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
+ run: |
+ set -euo pipefail
+ git diff --name-only "$BASE_SHA" "$HEAD_SHA" | sort -u > changed-files.txt
+ set_output() {
+ local key="$1"
+ local pattern="$2"
+ if grep -Eq "$pattern" changed-files.txt; then
+ echo "$key=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "$key=false" >> "$GITHUB_OUTPUT"
+ fi
+ }
+ set_output research '^(docs/(core|topics|meta|scripts)/|docs/UET_Documentation_Details/)'
+ set_output books '^(uet_history/BOOK_WORKFLOW\.md|uet_history/3_publish/books/)'
+ set_output services '^services_and_experiments/'
+ set_output policy '^thailand_proposals/'
+ set_output docs '\.(md|yml|yaml)$'
+ set_output python '\.py$'
+ set_output rust '(^|/)(Cargo\.toml|Cargo\.lock|.*\.rs)$'
+
+ docs-validation:
+ name: Documentation validation
+ needs: detect
+ if: needs.detect.outputs.docs == 'true'
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@v4
+ - run: git diff --check
+ - name: Check canonical path guardrails
+ run: |
+ test ! -d docs/topics/For_Work || {
+ echo "docs/topics/For_Work is a drifted path; use docs/topics/For Work/."
+ exit 1
+ }
+
+ python-validation:
+ name: Python changed-area validation
+ needs: detect
+ if: needs.detect.outputs.python == 'true'
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+ - run: python -m pip install --upgrade pytest
+ - run: python -m compileall -q docs services_and_experiments
+ - name: Run changed Python tests
+ env:
+ BASE_SHA: ${{ github.event.pull_request.base.sha }}
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
+ run: |
+ set -euo pipefail
+ git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -E '(^|/)(test_[^/]+\.py|[^/]+_test\.py)$' > changed-tests.txt || true
+ if test -s changed-tests.txt; then
+ tests="$(cat changed-tests.txt)"
+ pytest -q $tests
+ else
+ echo "No changed Python test files; compile validation completed."
+ fi
+
+ research-validation:
+ name: Research contract validation
+ needs: detect
+ if: needs.detect.outputs.research == 'true'
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+ - run: python -m compileall -q docs
+ - name: Parse research JSON artifacts
+ run: |
+ python - <<'PY'
+ import json
+ from pathlib import Path
+ for path in Path("docs/topics").glob("**/*.json"):
+ json.loads(path.read_text(encoding="utf-8"))
+ print("Research JSON artifacts are parseable.")
+ PY
+
+ book-validation:
+ name: Book registry validation
+ needs: detect
+ if: needs.detect.outputs.books == 'true'
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@v4
+ - name: Validate book registry
+ run: |
+ python - <<'PY'
+ import json
+ from pathlib import Path
+ registry = Path("uet_history/3_publish/books/BOOK_REGISTRY.json")
+ if not registry.exists():
+ raise SystemExit("BOOK_REGISTRY.json is required for book changes.")
+ data = json.loads(registry.read_text(encoding="utf-8"))
+ if not isinstance(data, (dict, list)):
+ raise SystemExit("BOOK_REGISTRY.json must contain an object or list.")
+ print("Book registry JSON is valid.")
+ PY
+
+ service-validation:
+ name: Optional service boundary validation
+ needs: detect
+ if: needs.detect.outputs.services == 'true'
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@v4
+ - name: Check service source boundary
+ run: |
+ set -euo pipefail
+ if find services_and_experiments -type f -name '.env' | grep -q .; then
+ echo "Service .env files must remain local-only."
+ exit 1
+ fi
+ echo "Services remain optional; no research workflow depends on them."
+ - name: Install Rust
+ if: hashFiles('services_and_experiments/uet_core/Cargo.toml') != ''
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ toolchain: stable
+ components: rustfmt, clippy
+ - name: Rust format and compile
+ if: hashFiles('services_and_experiments/uet_core/Cargo.toml') != ''
+ run: |
+ cargo fmt --manifest-path services_and_experiments/uet_core/Cargo.toml --all -- --check
+ cargo check --manifest-path services_and_experiments/uet_core/Cargo.toml --all-targets
diff --git a/AGENTS.md b/AGENTS.md
index c7f11e7f1..b47566396 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -532,3 +532,40 @@ required to explain the same hardening wave.
Use the agent as a careful research assistant, auditor, and systems organizer, not as the
final authority that upgrades evidence by confidence alone.
+
+### Repository branch and automation policy
+
+main is the public canonical branch. Normal work enters main through a pull
+request and required CI checks; do not use a second permanent integration
+branch such as develop or staging.
+
+Use one short-lived branch per coherent unit of work:
+
+ codex/research/
+ codex/book/
+ codex/history/
+ codex/policy/
+ codex/services/
+ codex/repo/
+
+Before editing, confirm the current branch, worktree, and upstream. A local
+branch that tracks origin/main is still a feature branch and must not be
+treated as the canonical main worktree. Reconcile worktrees and stale
+remote-tracking refs before deleting branches; never delete a dirty worktree
+or a branch that contains unique unmerged commits.
+
+Every normal PR must identify its area, canonical workspace, public-safety
+boundary, checks actually run, and next blocker. GitHub Actions may validate,
+build reports, and deploy GitHub Pages, but must not silently commit or push
+source changes. Required checks must fail visibly; do not use
+continue-on-error to make a required gate appear green.
+
+The repository ruleset should prevent force-push and deletion of main, require
+the scoped PR checks, and auto-delete a merged head branch. Direct push is an
+emergency exception only: record the reason in WORK_LEDGER/ and verify that
+local main, remote main, and the GitHub page show the same SHA.
+
+The normal publish checkpoint is the same day as the completed section: commit
+the coherent unit, push the branch, and open or update a draft PR when it is
+not ready to merge. Use git fetch --prune origin during branch inventory so
+stale origin/codex/... references do not look like active remote branches.
diff --git a/CONTEXT-MAP.md b/CONTEXT-MAP.md
index 951d3db09..0c9d7d0a3 100644
--- a/CONTEXT-MAP.md
+++ b/CONTEXT-MAP.md
@@ -60,3 +60,25 @@ Every completed work section follows:
8. push the branch or open a draft PR the same day
At ten ledger entries for unpushed work, stop expanding scope and checkpoint.
+
+## Repository and branch routing
+
+main is the only public canonical branch. Normal work uses one short-lived
+branch per coherent unit with the area prefix codex/research/, codex/book/,
+codex/history/, codex/policy/, codex/services/, or codex/repo/, then enters
+main through a PR and required CI checks.
+
+CONTRIBUTING.md is the human contribution contract, AGENTS.md is the
+agent-facing operating summary, and .github/workflows/ is the executable
+validation layer. Do not create a second branch policy or semantic path to
+avoid an existing canonical workspace.
+
+Before cleanup, inspect worktrees, local branches, remote heads, PRs, and
+unique commits. git fetch --prune origin removes stale tracking refs, but a
+branch is deletable only after its work is merged or explicitly superseded and
+its unique commits are accounted for. A local branch tracking origin/main does
+not become main merely because its upstream points there.
+
+GitHub Actions may validate files and publish Pages, but it must not silently
+commit or push source changes. A completed section is visible only when its
+ledger entry, coherent commit, and pushed branch or PR are all present.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index cf2f38588..9471d716b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -297,3 +297,48 @@ reproducible, link that change to the pilot in the relevant update log.
The goal is not to make the repository look busy. It is to make each
meaningful change understandable, reproducible within its stated boundary, and
visible in the right history.
+
+## Branch lifecycle and CI enforcement
+
+main is the public canonical branch. The normal path is one focused,
+short-lived branch plus a PR:
+
+ codex/research/
+ codex/book/
+ codex/history/
+ codex/policy/
+ codex/services/
+ codex/repo/
+
+Do not create permanent develop, staging, or topic branches. A local branch
+that tracks origin/main is not the canonical main worktree. Before cleanup,
+inspect every worktree, remote head, PR, and unique commit. Run
+git fetch --prune origin to remove stale origin/codex/... tracking refs. Delete
+a branch only after confirming it is merged or explicitly superseded and has no
+unique work. Never delete a dirty worktree.
+
+The repository ruleset should require PR checks, prevent force-push and deletion
+of main, and auto-delete a merged PR head branch. No approval is required while
+this is a single-maintainer repository, but every required check must pass
+visibly. Direct push to main is an explicitly documented emergency only; record
+the reason in WORK_LEDGER/ and verify the local, remote, and GitHub main SHA.
+
+CI is split by responsibility:
+
+- pr-scope.yml checks branch naming, diff whitespace, raw/private/binary/cache
+ boundaries, JSON syntax, and path drift.
+- pr-validation.yml runs checks only for changed areas.
+- main-validation.yml runs after merge and stores validation reports.
+- nightly-research-audit.yml runs deeper audits on schedule or manual dispatch;
+ it never edits or pushes the repository.
+- GitHub Pages has its own workflow. Railway, API, and other platform deploys
+ remain paused until a real client, stable interface, provenance boundary,
+ tests, and regeneration path exist.
+
+Required checks must fail when their validation fails. Existing baseline
+failures must be repaired or explicitly moved to a visible, time-bounded
+non-required audit before a full suite becomes a required merge gate; they may
+not be hidden with continue-on-error.
+
+The PR template is the minimum submission contract. The work ledger, commit,
+PR/push, topic update log, and artifact/gate are separate records.
diff --git a/WORK_LEDGER/2026/2026-08-21.md b/WORK_LEDGER/2026/2026-08-21.md
index c252644c0..fe21a4ee5 100644
--- a/WORK_LEDGER/2026/2026-08-21.md
+++ b/WORK_LEDGER/2026/2026-08-21.md
@@ -6,4 +6,35 @@
- Verification: full gate regenerated as `BLOCKED_OPEN_T13_FULL_BRIDGE` / `PARTIAL`; machine-readable summary reports `169` closed lane results, `14` scoped no-go results, and `10` open full-result blockers; dependency audit remains blocked; focused regression `12 passed`.
- Public-safety status: `partial`.
- What remains uncommitted/private/unsafe: no physical source or calibration was promoted; Ding numeric `C_src` and independent dimensional calibration remain external blockers; Xie 2026 was not consumed; the worktree contains pre-existing unrelated changes and was not cleaned or committed.
-- Next action: pursue an authorized independent Phi/SI anchor and accepted Ding-compatible numeric source/reproduction, then rerun the full bridge gate without changing ontology, threshold, or holdout policy.
\ No newline at end of file
+- Next action: pursue an authorized independent Phi/SI anchor and accepted Ding-compatible numeric source/reproduction, then rerun the full bridge gate without changing ontology, threshold, or holdout policy.
+## Repository workflow standardization
+
+- Area: repo-ops (secondary: documentation-system)
+- Workspace: .github/, AGENTS.md, CONTRIBUTING.md, CONTEXT-MAP.md, WORK_LEDGER/
+- Changed: branch/worktree policy, PR contract, CI split, Pages boundary, and checkpoint rules
+- Validation: pending until workflow syntax and repository boundary checks complete
+- Public safety: safe
+- Remaining: staged review, CI validation, commit, push, and draft PR
+- Next action: run scoped checks and verify remote branch/PR state
+
+## Branch inventory checkpoint
+
+- Area: repo-ops
+- Remote cleanup: pruned stale tracking refs; removed superseded remote branches for the old archive, economics outline, and merged superconductivity workflow.
+- Local cleanup: removed merged/superseded local branches; removed clean duplicate worktrees; retained the superconductivity PR commits as local archive/superconductivity-allen-dynes-pr4.
+- Protected state: retained the separate main worktree because it has an untracked personal knowledge-base database; no dirty worktree was deleted.
+- Verification: remote branch inventory and worktree status inspected before cleanup.
+- Next action: commit this workflow change on codex/repo/github-workflow-system and publish a draft PR.
+
+## Public-boundary baseline checkpoint
+
+- Existing tracked boundary debt: 17 files match raw/database/zip/debug patterns on the current main baseline.
+- Policy change: PRs fail on newly changed forbidden paths; main validation reports existing baseline files as an artifact instead of hiding them or blocking every unrelated merge.
+- Follow-up: remove or relocate the existing raw/database/debug set in a separately scoped repo-ops cleanup after provenance review.
+
+## Research CI baseline
+
+- Full local command: docs/core suite collected a legacy import-path error and the main suite then reported 1,079 passed and 5 failed.
+- Known failures: one environment-version artifact mismatch, two generated-alignment payload/hash mismatches, and two matter-space artifact alignment mismatches.
+- Policy: full suite remains authoritative and visible; diagnostic legacy-excluded run is only for isolating the current baseline and is not an allowlist.
+- Next action: repair/regenerate the five artifact contracts and fix the legacy path before making full research validation a required merge check.