diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 37a3c83..a529418 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -26,13 +26,19 @@ jobs: steps: - name: Checkout repository env: - REPOSITORY_URL: http://117.72.28.96:8765/Jacky/Hulumath-Web.git + CHECKOUT_TOKEN: ${{ github.token }} REVISION: ${{ gitea.sha }} run: | - git clone "$REPOSITORY_URL" . + set -euo pipefail + git init . + git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" + git -c http.extraHeader="Authorization: token ${CHECKOUT_TOKEN}" \ + fetch --depth=1 origin "$REVISION" git checkout --detach "$REVISION" - name: Show Python version run: python3 --version + - name: Validate repository Capsule + run: python3 scripts/check_context.py - name: Install MySQL build dependencies run: | sudo apt-get update diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..edf6b21 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,22 @@ +# Hulumath AI Working Agreement + +Read `context/boot.md` and `context/capsule.json` before changing the product. +Then select the smallest relevant product, architecture or deployment +document under `docs/`. + +## Source Boundaries + +- Hulumath owns its Django application, mathematical content, product rules + and release documentation. +- JLearn may link to Hulumath but does not own its users or runtime data. +- JOps owns shared edge, deployment observation and platform authentication + facts. +- Secrets, production databases, uploads and user data remain outside Git. + +## Change Protocol + +1. Preserve server-side authorization, scoring and anti-cheat checks. +2. Keep API and WebSocket routes backward compatible or version them. +3. Update product or deployment documentation with behavioral changes. +4. Run `make context-check`, `make check` and `make test` before committing. +5. Treat checked-in release notes as source history, not live runtime proof. diff --git a/Makefile b/Makefile index 7f43389..6773faf 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: install migrate seed local-admin run run-asgi test check +.PHONY: install migrate seed local-admin run run-asgi test check context-check install: python3 -m venv .venv @@ -23,7 +23,10 @@ run-asgi: test: .venv/bin/python -m pytest -q -check: +context-check: + python3 scripts/check_context.py + +check: context-check .venv/bin/ruff check backend scripts cd backend && ../.venv/bin/python manage.py check cd backend && ../.venv/bin/python manage.py makemigrations --check --dry-run diff --git a/context/boot.md b/context/boot.md new file mode 100644 index 0000000..e81f4a7 --- /dev/null +++ b/context/boot.md @@ -0,0 +1,31 @@ +# Hulumath Context Boot + +## Purpose + +Hulumath is a public mathematics product built as a Django modular monolith. +It combines mathematical identity, stories, competitions, courses, +progression and constrained mathematical tools. + +## Repository Map + +```text +backend/ Django applications, APIs, templates and static assets +docs/ product, release and deployment documentation +scripts/ deployment and maintenance helpers +deploy/ production configuration templates +context/ bounded repository identity for Universal Context +``` + +## Read Order + +```text +AGENTS.md +-> context/boot.md +-> context/capsule.json +-> README.md +-> relevant document under docs/ +-> relevant Django application under backend/ +``` + +Runtime credentials, databases, uploads and account data are never context +inputs. Current production claims require a fresh JOps observation. diff --git a/context/capsule.json b/context/capsule.json new file mode 100644 index 0000000..6ce1e54 --- /dev/null +++ b/context/capsule.json @@ -0,0 +1,95 @@ +{ + "schema": 1, + "kind": "jcm.repository-capsule", + "repository": { + "id": "hulumath", + "name": "Hulumath", + "remote": "Jacky/Hulumath-Web", + "role": "public-mathematics-product", + "summary": "Owns the public Hulumath Django application, mathematical content, competitions, learning tools and product release evidence." + }, + "domains": [ + "platform", + "learning" + ], + "context": { + "entrypoint": "AGENTS.md", + "read_order": [ + "AGENTS.md", + "context/boot.md", + "context/capsule.json", + "context/manifest.json", + "README.md", + "docs/RELEASE_NOTES_V1.2.1.md", + "docs/DEPLOYMENT.md" + ] + }, + "ownership": { + "owns": [ + "Hulumath product behavior and mathematical content", + "Django application, APIs and WebSocket contracts", + "Hulumath release and migration documentation" + ], + "excludes": [ + "JLearn accounts, sessions and learner data", + "Shared platform edge and runtime operations", + "Production secrets, databases and uploads" + ] + }, + "contracts": null, + "services": [ + { + "id": "hulumath-web", + "name": "Hulumath", + "kind": "public-mathematics-application", + "summary": "Responsive mathematics product with identity, stories, contests, courses and tools.", + "public_urls": [ + "https://math.jacky0987.xyz/" + ], + "domains": [ + "platform", + "learning" + ] + } + ], + "relationships": [ + { + "target": "jlearn", + "type": "integrates", + "summary": "Receives a public link from JLearn but keeps independent accounts and data." + }, + { + "target": "jops", + "type": "operates", + "summary": "Relies on JOps for shared edge and production runtime evidence." + } + ], + "runtime": { + "authority": "none" + }, + "current_state": { + "status": "released", + "summary": "The repository release baseline is v1.2.1; this source state does not claim current production health.", + "evidence": [ + "README.md", + "docs/RELEASE_NOTES_V1.2.1.md", + "docs/DEPLOYMENT.md" + ] + }, + "facts": [ + { + "id": "hulumath-platform-boundary", + "scope": "platform", + "summary": "Hulumath is an independently deployed product whose shared edge and runtime evidence belong to JOps.", + "authority": "canonical", + "evidence": "docs/DEPLOYMENT.md" + }, + { + "id": "hulumath-learning-scope", + "scope": "learning", + "summary": "Hulumath combines mathematical identity, stories, competitions, courses, progression and constrained tools.", + "authority": "canonical", + "evidence": "README.md" + } + ] +} diff --git a/context/manifest.json b/context/manifest.json new file mode 100644 index 0000000..5ef24bf --- /dev/null +++ b/context/manifest.json @@ -0,0 +1,35 @@ +{ + "schema_version": "2.0.0", + "bundle_type": "repo-local-ai", + "repository": { + "owner": "Jacky", + "name": "Hulumath-Web", + "role": "public-mathematics-product" + }, + "state": "RELEASED", + "runtime_state": "UNVERIFIED", + "entrypoint": "AGENTS.md", + "read_order": [ + "AGENTS.md", + "context/boot.md", + "context/capsule.json", + "context/manifest.json", + "README.md", + "docs/RELEASE_NOTES_V1.2.1.md", + "docs/DEPLOYMENT.md" + ], + "contracts": {}, + "dependencies": { + "strategy": "none" + }, + "runtime": { + "freshness": "requires_jops_observation" + }, + "artifacts": [ + "context/boot.md", + "context/capsule.json", + "README.md", + "docs/RELEASE_NOTES_V1.2.1.md", + "docs/DEPLOYMENT.md" + ] +} diff --git a/scripts/check_context.py b/scripts/check_context.py new file mode 100644 index 0000000..fcbfdde --- /dev/null +++ b/scripts/check_context.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 + +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +CAPSULE_FILE = ROOT / "context" / "capsule.json" +MANIFEST_FILE = ROOT / "context" / "manifest.json" + + +def fail(message): + raise SystemExit(f"context_check_error={message}") + + +try: + capsule = json.loads(CAPSULE_FILE.read_text(encoding="utf-8")) + manifest = json.loads(MANIFEST_FILE.read_text(encoding="utf-8")) +except (OSError, json.JSONDecodeError) as error: + fail(f"invalid_capsule:{error}") + +if ( + capsule.get("schema") != 1 + or capsule.get("kind") != "jcm.repository-capsule" + or capsule.get("repository", {}).get("id") != "hulumath" + or capsule.get("repository", {}).get("remote") != "Jacky/Hulumath-Web" +): + fail("invalid_identity") +if ( + manifest.get("schema_version") != "2.0.0" + or manifest.get("repository", {}).get("name") != "Hulumath-Web" + or "context/capsule.json" not in manifest.get("read_order", []) +): + fail("invalid_manifest") + +paths = [ + capsule.get("context", {}).get("entrypoint"), + *capsule.get("context", {}).get("read_order", []), + *capsule.get("current_state", {}).get("evidence", []), + *[fact.get("evidence") for fact in capsule.get("facts", [])], +] +for relative in paths: + if not relative or Path(relative).is_absolute() or ".." in Path(relative).parts: + fail(f"unsafe_path:{relative}") + if not (ROOT / relative).is_file(): + fail(f"missing_path:{relative}") + +print("context_check=ok")