diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml new file mode 100644 index 0000000..bd1491c --- /dev/null +++ b/.github/workflows/auto-release.yml @@ -0,0 +1,268 @@ +name: Auto Release + +# Automatically creates a release when a PR is merged to main/master. +# Determines version bump from PR labels or commit messages: +# - Label "release:major" or commit with "BREAKING CHANGE" → major bump +# - Label "release:minor" or commit with "feat:" → minor bump +# - Label "release:patch" or commit with "fix:" → patch bump +# - Label "release:beta" → beta pre-release +# - Label "release:skip" → skip release entirely +# +# Can also be triggered manually via workflow_dispatch. + +on: + push: + branches: [main, master] + workflow_dispatch: + inputs: + bump_type: + description: "Version bump type" + required: true + type: choice + options: + - patch + - minor + - major + - beta + +permissions: + contents: write + pull-requests: read + +jobs: + determine-release: + name: Determine release type + runs-on: ubuntu-latest + # Skip release commits to avoid infinite loops + if: "!startsWith(github.event.head_commit.message, 'chore: release v')" + outputs: + should_release: ${{ steps.check.outputs.should_release }} + bump_type: ${{ steps.check.outputs.bump_type }} + is_prerelease: ${{ steps.check.outputs.is_prerelease }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check release type + id: check + env: + MANUAL_BUMP: ${{ github.event.inputs.bump_type }} + run: | + BUMP="" + IS_PRE="false" + + # Manual trigger takes priority + if [ -n "$MANUAL_BUMP" ]; then + if [ "$MANUAL_BUMP" = "beta" ]; then + BUMP="beta" + IS_PRE="true" + else + BUMP="$MANUAL_BUMP" + fi + echo "should_release=true" >> "$GITHUB_OUTPUT" + echo "bump_type=$BUMP" >> "$GITHUB_OUTPUT" + echo "is_prerelease=$IS_PRE" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Check merged PR labels + PR_NUMBER=$(gh pr list --state merged --base main --json number,mergedAt --jq 'sort_by(.mergedAt) | last | .number' 2>/dev/null || echo "") + if [ -n "$PR_NUMBER" ]; then + LABELS=$(gh pr view "$PR_NUMBER" --json labels --jq '.labels[].name' 2>/dev/null || echo "") + + if echo "$LABELS" | grep -q "release:skip"; then + echo "should_release=false" >> "$GITHUB_OUTPUT" + echo "bump_type=" >> "$GITHUB_OUTPUT" + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if echo "$LABELS" | grep -q "release:major"; then BUMP="major"; fi + if echo "$LABELS" | grep -q "release:minor"; then BUMP="minor"; fi + if echo "$LABELS" | grep -q "release:patch"; then BUMP="patch"; fi + if echo "$LABELS" | grep -q "release:beta"; then BUMP="beta"; IS_PRE="true"; fi + fi + + # Fallback: detect from commit messages + if [ -z "$BUMP" ]; then + LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + if [ -n "$LAST_TAG" ]; then + COMMITS=$(git log "$LAST_TAG..HEAD" --pretty=format:"%s" --no-merges) + else + COMMITS=$(git log --pretty=format:"%s" --no-merges -20) + fi + + if echo "$COMMITS" | grep -qiE "BREAKING CHANGE|^feat!:"; then + BUMP="major" + elif echo "$COMMITS" | grep -qiE "^feat(\(.+\))?:"; then + BUMP="minor" + elif echo "$COMMITS" | grep -qiE "^fix(\(.+\))?:"; then + BUMP="patch" + fi + fi + + # Default: no auto-release unless there's a clear signal + if [ -z "$BUMP" ]; then + echo "should_release=false" >> "$GITHUB_OUTPUT" + echo "bump_type=" >> "$GITHUB_OUTPUT" + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" + else + echo "should_release=true" >> "$GITHUB_OUTPUT" + echo "bump_type=$BUMP" >> "$GITHUB_OUTPUT" + echo "is_prerelease=$IS_PRE" >> "$GITHUB_OUTPUT" + fi + env: + GH_TOKEN: ${{ github.token }} + + auto-release: + name: Create release + runs-on: ubuntu-latest + needs: determine-release + if: needs.determine-release.outputs.should_release == 'true' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Bump version + id: bump + run: | + BUMP_TYPE="${{ needs.determine-release.outputs.bump_type }}" + IS_PRE="${{ needs.determine-release.outputs.is_prerelease }}" + CURRENT=$(cat VERSION | tr -d '[:space:]') + + if [ "$BUMP_TYPE" = "beta" ]; then + # Parse current version + BASE=$(echo "$CURRENT" | sed 's/-.*//') + IFS='.' read -r MAJOR MINOR PATCH <<< "$BASE" + NEXT_MINOR="$MAJOR.$((MINOR + 1)).0" + + # Check if already in beta + if echo "$CURRENT" | grep -q "beta"; then + BETA_NUM=$(echo "$CURRENT" | grep -oP 'beta\.\K[0-9]+') + NEW_VERSION="${NEXT_MINOR}-beta.$((BETA_NUM + 1))" + else + NEW_VERSION="${NEXT_MINOR}-beta.1" + fi + else + # Standard semantic version bump + BASE=$(echo "$CURRENT" | sed 's/-.*//') + IFS='.' read -r MAJOR MINOR PATCH <<< "$BASE" + case "$BUMP_TYPE" in + patch) NEW_VERSION="$MAJOR.$MINOR.$((PATCH + 1))" ;; + minor) NEW_VERSION="$MAJOR.$((MINOR + 1)).0" ;; + major) NEW_VERSION="$((MAJOR + 1)).0.0" ;; + esac + fi + + echo "current=$CURRENT" >> "$GITHUB_OUTPUT" + echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT" + echo "tag=v$NEW_VERSION" >> "$GITHUB_OUTPUT" + echo "Bumping $CURRENT → $NEW_VERSION" + + # Update all version files + echo "$NEW_VERSION" > VERSION + + cat > backend/_version.py << PYEOF + """Single source of truth for MIZAN version — auto-generated""" + + __version__ = "$NEW_VERSION" + PYEOF + + # Clean indentation from heredoc + sed -i 's/^ //' backend/_version.py + + sed -i "s/^version = \".*\"/version = \"$NEW_VERSION\"/" pyproject.toml + sed -i "s/\"version\": \".*\"/\"version\": \"$NEW_VERSION\"/" frontend/package.json + + # CLI version + IFS='.' read -r NM NMI NP <<< "$(echo "$NEW_VERSION" | sed 's/-.*//')" + SHORT_VER="$NM.$NMI" + sed -i "s/MIZAN v[0-9][0-9]*\.[0-9][0-9]*/MIZAN v$SHORT_VER/" backend/cli.py + sed -i "s/MIZAN v[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*/MIZAN v$NEW_VERSION/" backend/cli.py + + # API version + sed -i "s/\"version\": \"[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*[^\"]*\"/\"version\": \"$NEW_VERSION\"/g" backend/api/main.py + sed -i "s/version=\"[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*[^\"]*\"/version=\"$NEW_VERSION\"/" backend/api/main.py + + - name: Generate changelog + id: changelog + run: | + NEW_VERSION="${{ steps.bump.outputs.new_version }}" + DATE=$(date +%Y-%m-%d) + + LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + if [ -n "$LAST_TAG" ]; then + COMMITS=$(git log "$LAST_TAG..HEAD" --pretty=format:"- %s" --no-merges) + else + COMMITS=$(git log --pretty=format:"- %s" --no-merges -20) + fi + + # Categorize + FEATURES=$(echo "$COMMITS" | grep -iE "^- (feat|add|new)" || true) + FIXES=$(echo "$COMMITS" | grep -iE "^- (fix|bug|patch|hotfix)" || true) + CHANGES=$(echo "$COMMITS" | grep -iE "^- (refactor|update|improve|enhance|chore|docs|style|perf)" || true) + + # Build entry + ENTRY="## [v$NEW_VERSION] — $DATE" + if [ -n "$FEATURES" ]; then + ENTRY="$ENTRY + + ### Added + $FEATURES" + fi + if [ -n "$FIXES" ]; then + ENTRY="$ENTRY + + ### Fixed + $FIXES" + fi + if [ -n "$CHANGES" ]; then + ENTRY="$ENTRY + + ### Changed + $CHANGES" + fi + + # Save for release notes + echo "$ENTRY" > /tmp/release_notes.md + + # Prepend to CHANGELOG.md + if [ -f CHANGELOG.md ]; then + HEADER=$(head -3 CHANGELOG.md) + BODY=$(tail -n +4 CHANGELOG.md) + printf "%s\n\n%s\n\n%s" "$HEADER" "$ENTRY" "$BODY" > CHANGELOG.md + fi + + - name: Commit and tag + run: | + git add -A + git commit -m "chore: release v${{ steps.bump.outputs.new_version }} + + Bump version ${{ steps.bump.outputs.current }} → ${{ steps.bump.outputs.new_version }} and update CHANGELOG. + + [skip ci]" + git tag -a "v${{ steps.bump.outputs.new_version }}" -m "Release v${{ steps.bump.outputs.new_version }}" + git push origin HEAD + git push origin "v${{ steps.bump.outputs.new_version }}" + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: "v${{ steps.bump.outputs.new_version }}" + name: "MIZAN v${{ steps.bump.outputs.new_version }}" + body_path: /tmp/release_notes.md + draft: false + prerelease: ${{ needs.determine-release.outputs.is_prerelease == 'true' }} + generate_release_notes: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..681e469 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,49 @@ +name: Deploy Docs + +on: + push: + branches: [main, master] + paths: + - "docs/**" + - "CHANGELOG.md" + - "README.md" + - ".github/workflows/docs.yml" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + build: + name: Build docs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs + + deploy: + name: Deploy to GitHub Pages + runs-on: ubuntu-latest + needs: build + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy + id: deployment + uses: actions/deploy-pages@v4 diff --git a/Makefile b/Makefile index 3c62ee8..bc845cc 100644 --- a/Makefile +++ b/Makefile @@ -106,6 +106,12 @@ release-minor: ## Full release: bump minor, changelog, tag, push release-major: ## Full release: bump major, changelog, tag, push ./scripts/release.sh major +release-beta: ## Full release: create beta pre-release + ./scripts/release.sh beta + +release-rc: ## Full release: create release candidate + ./scripts/release.sh rc + release-dry: ## Dry run of a patch release (no changes) ./scripts/release.sh patch --dry-run diff --git a/docs/index.html b/docs/index.html index 7b2d744..0b2336c 100644 --- a/docs/index.html +++ b/docs/index.html @@ -189,6 +189,117 @@ } .card h4 { margin-top: 0; } + /* Search */ + .search-box { + padding: 12px 16px; + margin-bottom: 8px; + } + .search-input { + width: 100%; + padding: 8px 12px 8px 32px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text); + font-size: 13px; + outline: none; + transition: border-color 0.2s; + } + .search-input:focus { + border-color: var(--accent); + } + .search-box { position: relative; } + .search-icon { + position: absolute; + left: 26px; + top: 50%; + transform: translateY(-50%); + color: var(--text-dim); + font-size: 14px; + pointer-events: none; + } + + /* Theme toggle */ + .theme-toggle { + position: absolute; + top: 24px; + right: 16px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 8px; + padding: 6px 8px; + color: var(--text-dim); + cursor: pointer; + font-size: 16px; + line-height: 1; + transition: all 0.2s; + } + .theme-toggle:hover { color: var(--accent); border-color: var(--accent); } + + /* Section transitions */ + .section { + display: none; + animation: fadeIn 0.3s ease; + } + .section.active { display: block; } + @keyframes fadeIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } + } + + /* Version badge */ + .version-badge { + display: inline-block; + padding: 2px 10px; + border-radius: 12px; + font-size: 11px; + font-weight: 600; + background: rgba(245, 166, 35, 0.15); + color: var(--accent); + margin-left: 8px; + vertical-align: middle; + } + + /* Back to top */ + .back-to-top { + position: fixed; + bottom: 24px; + right: 24px; + width: 40px; + height: 40px; + border-radius: 12px; + background: var(--accent); + color: #000; + border: none; + cursor: pointer; + font-size: 18px; + display: none; + align-items: center; + justify-content: center; + box-shadow: 0 4px 12px rgba(245, 166, 35, 0.3); + transition: transform 0.2s; + z-index: 50; + } + .back-to-top:hover { transform: translateY(-2px); } + .back-to-top.visible { display: flex; } + + /* Light theme */ + [data-theme="light"] { + --bg: #fafafa; + --bg-card: #ffffff; + --bg-code: #f4f4f8; + --border: #e0e0e8; + --text: #1a1a2e; + --text-dim: #5a5a70; + --link: #2563eb; + } + + /* Highlight search results */ + .nav-item.search-match { + background: rgba(245, 166, 35, 0.1); + color: var(--accent) !important; + } + /* Mobile */ .menu-toggle { display: none; position: fixed; top: 16px; left: 16px; z-index: 200; background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; @@ -198,19 +309,27 @@ .sidebar.open { transform: translateX(0); } .main { margin-left: 0; padding: 24px 16px; padding-top: 64px; } .menu-toggle { display: block; } + .theme-toggle { top: 16px; right: 56px; position: fixed; z-index: 200; } }
+ +MIZAN's perception system implements Quranic sensory processing: Sam' (hearing) processes before Basar (sight), following the order established in Quran 16:78 and 17:36.
+ +Auditory-first processing: Audio input is always processed before visual input, mirroring the Quranic ordering of sam' before basar in every mention.
+The BasirahEngine analyzes images using LLM vision capabilities. It returns structured JSON with:
Vision analysis adapts based on emotional state:
+| Qalb State | Focus Adjustment |
|---|---|
| frustrated | Pay attention to errors, warnings, and problematic elements |
| confused | Focus on clarity, labels, and explanatory elements |
| anxious | Look for reassuring elements and positive signals |
| determined | Identify actionable items and next steps |
The NutqEngine handles speech synthesis and analysis:
Perception results flow into the QCA engine via process_input_multimodal():
Input → Sam' (audio) → Basar (vision) → Fu'ad (integration)
+ ↓ ↓ ↓ ↓
+Nutq Transcribe Analyze Merge results
+Engine intent & category & + ISM root analysis
+ language elements + Lawh storage
+
+ Navigate to Tools → Perception in the sidebar, or attach an image/audio file directly in the chat interface. The system automatically routes media to the appropriate perception engine.
+POST /api/perception/analyze
+
+{
+ "text": "What does this diagram show?",
+ "image_base64": "iVBORw0KGgo...",
+ "audio_base64": "SUQzBAA...",
+ "media_type": "image/png",
+ "agent_id": "agent_abc123",
+ "qalb_state": "neutral"
+}
+
+Response:
+{
+ "perception": {
+ "sam": { ... },
+ "basar": { ... },
+ "fuad": { ... },
+ "nutq": {
+ "text": "What does this diagram show?",
+ "confidence": 0.92,
+ "language": "english",
+ "intent": "question"
+ },
+ "basirah": {
+ "description": "A system architecture diagram showing ...",
+ "category": "diagram",
+ "confidence": 0.87,
+ "extracted_text": "QCA Engine → Memory → ...",
+ "key_elements": ["architecture", "pipeline", "layers"]
+ }
+ },
+ "roots_identified": { "ع-ل-م": { ... } },
+ "key_terms": ["architecture", "pipeline"],
+ "zahir": "User is asking about a technical diagram",
+ "batin": "User wants to understand system structure"
+}
+
+ // Send via WebSocket
+{
+ "type": "multimodal",
+ "text": "Describe this image",
+ "image_base64": "...",
+ "media_type": "image/png",
+ "session_id": "session_123",
+ "agent_id": "agent_abc"
+}
+
+// Receive perception_result
+{
+ "type": "perception_result",
+ "result": { ... } // Same as REST response
+}
+ MIZAN uses Semantic Versioning (MAJOR.MINOR.PATCH) with support for pre-release versions (beta, rc).
+ +| Format | Example | When |
|---|---|---|
MAJOR.MINOR.PATCH | 3.1.0 | Stable release |
X.Y.Z-beta.N | 3.2.0-beta.1 | Beta pre-release |
X.Y.Z-rc.N | 3.2.0-rc.1 | Release candidate |
# Stable releases
+make release-patch # 3.0.0 → 3.0.1 (bug fixes)
+make release-minor # 3.0.0 → 3.1.0 (new features)
+make release-major # 3.0.0 → 4.0.0 (breaking changes)
+
+# Pre-releases
+make release-beta # 3.0.0 → 3.1.0-beta.1
+make release-rc # 3.1.0-beta.2 → 3.1.0-rc.1
+
+# Preview without changes
+make release-dry # Shows what would happen
+
+ make release-patch
+ ↓
+scripts/release.sh:
+ 1. Pre-flight: checks branch, clean tree, up-to-date
+ 2. Bumps version in 6 files (VERSION, pyproject.toml, etc.)
+ 3. Auto-generates CHANGELOG from git commits
+ 4. Commits: "chore: release vX.Y.Z"
+ 5. Creates annotated git tag: vX.Y.Z
+ 6. Pushes branch + tag to origin
+ ↓
+GitHub Actions (release.yml) triggers on v* tag:
+ 1. Validates: lint + test
+ 2. Publishes to PyPI
+ 3. Pushes Docker images to ghcr.io
+ 4. Creates GitHub Release with changelog notes
+
+ When a PR is merged to main, the auto-release workflow determines the version bump automatically:
| Signal | Bump |
|---|---|
PR label release:major or commit BREAKING CHANGE | Major |
PR label release:minor or commit prefix feat: | Minor |
PR label release:patch or commit prefix fix: | Patch |
PR label release:beta | Beta pre-release |
PR label release:skip | No release |
Tip: Add a release:minor label to your PR before merging to trigger an automatic minor release with auto-generated changelog.
The version is stored in 6 locations, all kept in sync by scripts/bump-version.sh:
| File | Purpose |
|---|---|
VERSION | Single source of truth |
pyproject.toml | Python package metadata |
backend/_version.py | Runtime import (from backend._version import __version__) |
frontend/package.json | Frontend version |
backend/cli.py | CLI banner display |
backend/api/main.py | API version metadata |
CHANGELOG.md follows Keep a Changelog. During release, commits are auto-categorized:
+| Commit Prefix | Category |
|---|---|
feat: / add: / new: | Added |
fix: / bug: / hotfix: | Fixed |
refactor: / docs: / chore: / perf: | Changed |
The main branch is protected with these rules:
# Apply protection rules (requires gh CLI)
+make protect
+