diff --git a/.github/scripts/generate-architecture.sh b/.github/scripts/generate-architecture.sh index 3fce369..d8e6a4d 100755 --- a/.github/scripts/generate-architecture.sh +++ b/.github/scripts/generate-architecture.sh @@ -1,14 +1,18 @@ #!/usr/bin/env bash -# generate-architecture.sh — Query GitNexus knowledge graph and generate ARCHITECTURE.md +# generate-architecture.sh — Update data-driven sections in ARCHITECTURE.md +# Uses marker-based partial updates to preserve hand-curated content. # Usage: bash .github/scripts/generate-architecture.sh -# Requires: npx (with gitnexus), jq, git -# Output: Writes ARCHITECTURE.md to the current directory +# Requires: npx (with gitnexus), jq, awk +# Output: Updates content between markers in ARCHITECTURE.md set -euo pipefail +ARCH_FILE="ARCHITECTURE.md" +REPO_NAME="${GITNEXUS_REPO:-}" + # ── Dependency validation ───────────────────────────────────────────── check_deps() { local missing=() - for cmd in jq npx; do + for cmd in jq npx awk; do if ! command -v "$cmd" &>/dev/null; then missing+=("$cmd") fi @@ -21,110 +25,164 @@ check_deps() { check_deps -# ── Query GitNexus for architectural data ───────────────────────────── -query_gitnexus() { - local query="$1" - local result - if ! result=$(npx gitnexus query "$query" --limit 10 2>&1); then - echo "::warning::GitNexus query failed for '$query': $result" >&2 - echo '{"processes":[]}' +# ── Resolve repo name ──────────────────────────────────────────────── +resolve_repo() { + if [[ -n "$REPO_NAME" ]]; then + echo "$REPO_NAME" return fi - # Validate we got valid JSON - if ! echo "$result" | jq -e '.processes' >/dev/null 2>&1; then - echo "::warning::GitNexus returned invalid JSON for '$query'" >&2 - echo '{"processes":[]}' - return + # Check how many repos are indexed; parse "Indexed Repositories (N)" header + local repo_list + repo_list=$(npx gitnexus list 2>&1 || true) + local count + count=$(echo "$repo_list" | grep -oP 'Indexed Repositories \(\K[0-9]+' 2>/dev/null || echo "1") + if [[ "$count" -le 1 ]]; then + # Single repo — no --repo flag needed + echo "" + else + # Multiple repos — use current directory name as repo identifier + basename "$(pwd)" fi - echo "$result" } -echo "Querying GitNexus knowledge graph..." >&2 +REPO=$(resolve_repo) +REPO_FLAG="" +if [[ -n "$REPO" ]]; then + REPO_FLAG="--repo $REPO" +fi -ENTRY_POINTS=$(query_gitnexus "entry points main CLI") -CORE_MODULES=$(query_gitnexus "core modules services") -DATA_FLOW=$(query_gitnexus "data flow pipeline") -AGENTS=$(query_gitnexus "agent orchestration") +# ── Validate ARCHITECTURE.md exists with markers ───────────────────── +if [[ ! -f "$ARCH_FILE" ]]; then + echo "::error::$ARCH_FILE not found. Cannot perform marker-based update." >&2 + exit 1 +fi -# ── Extract process summaries ───────────────────────────────────────── -extract_summaries() { - echo "$1" | jq -r '[.processes[]? | .summary] | join("\n")' 2>/dev/null || echo "none" -} +if ! grep -q '' "$ARCH_FILE"; then + echo "::error::$ARCH_FILE missing marker. Add markers before running." >&2 + exit 1 +fi -ENTRY_SUMMARY=$(extract_summaries "$ENTRY_POINTS") -CORE_SUMMARY=$(extract_summaries "$CORE_MODULES") -DATA_SUMMARY=$(extract_summaries "$DATA_FLOW") -AGENT_SUMMARY=$(extract_summaries "$AGENTS") - -# ── Count files by extension ────────────────────────────────────────── -PY_COUNT=$(find . -name '*.py' -not -path './.git/*' -not -path './.gitnexus/*' | wc -l) -RS_COUNT=$(find . -name '*.rs' -not -path './.git/*' -not -path './.gitnexus/*' | wc -l) -YML_COUNT=$(find . -name '*.yml' -path './.github/*' | wc -l) -MD_COUNT=$(find . -name '*.md' -not -path './.git/*' -not -path './.gitnexus/*' | wc -l) - -# ── Helper: render section ──────────────────────────────────────────── -render_section() { - local title="$1" - local summary="$2" - local fallback="$3" - - echo "## $title" - echo "" - if [[ -n "$summary" && "$summary" != "none" ]]; then - echo "$summary" | while IFS= read -r line; do - [[ -n "$line" ]] && echo "- $line" - done - else - echo "- $fallback" +if ! grep -q '' "$ARCH_FILE"; then + echo "::error::$ARCH_FILE missing closing marker." >&2 + exit 1 +fi + +# ── Query GitNexus for graph stats ─────────────────────────────────── +cypher_query() { + local query="$1" + local result + # shellcheck disable=SC2086 + if ! result=$(npx gitnexus cypher $REPO_FLAG "$query" 2>&1); then + echo "::warning::Cypher query failed: $result" >&2 + echo "" + return fi - echo "" + echo "$result" } -# ── Generate ARCHITECTURE.md ────────────────────────────────────────── -GENERATED_DATE=$(date -u '+%Y-%m-%d %H:%M UTC') - -cat > ARCHITECTURE.md <&2 -> Auto-generated by GitNexus knowledge graph analysis. -> Last updated: ${GENERATED_DATE} +SYMBOL_COUNT=$(cypher_query 'MATCH (n) RETURN count(n) as c' | jq -r '.markdown' | tail -1 | tr -d '| ' || echo "0") +EDGE_COUNT=$(cypher_query 'MATCH ()-[r]->() RETURN count(r) as c' | jq -r '.markdown' | tail -1 | tr -d '| ' || echo "0") +PROCESS_COUNT=$(cypher_query 'MATCH (p:Process) RETURN count(p) as c' | jq -r '.markdown' | tail -1 | tr -d '| ' || echo "0") -## Codebase Overview +# Format numbers with commas +format_number() { + echo "$1" | sed ':a;s/\B[0-9]\{3\}\>$/,&/;ta' +} -| Metric | Count | +SYMBOL_FMT=$(format_number "$SYMBOL_COUNT") +EDGE_FMT=$(format_number "$EDGE_COUNT") +PROCESS_FMT=$(format_number "$PROCESS_COUNT") + +echo " Symbols: $SYMBOL_FMT, Relationships: $EDGE_FMT, Flows: $PROCESS_FMT" >&2 + +# ── Count files by extension (with correct exclusions) ─────────────── +PY_COUNT=$(find . -name '*.py' \ + -not -path './.git/*' \ + -not -path './.gitnexus/*' \ + -not -path './.venv/*' \ + -not -path './venv/*' \ + -not -path './node_modules/*' \ + | wc -l | tr -d ' ') + +RS_COUNT=$(find . -name '*.rs' \ + -not -path './.git/*' \ + -not -path './.gitnexus/*' \ + -not -path './target/*' \ + | wc -l | tr -d ' ') + +YML_COUNT=$(find .github/workflows -maxdepth 1 -name '*.yml' 2>/dev/null | wc -l | tr -d ' ') + +MD_COUNT=$(find . \( -name '*.md' -o -name '*.rst' \) \ + -not -path './.git/*' \ + -not -path './.gitnexus/*' \ + -not -path './.venv/*' \ + -not -path './venv/*' \ + -not -path './node_modules/*' \ + -not -path './target/*' \ + | wc -l | tr -d ' ') + +echo " Files: py=$PY_COUNT rs=$RS_COUNT yml=$YML_COUNT md=$MD_COUNT" >&2 + +# ── Build replacement content for overview section ─────────────────── +OVERVIEW_CONTENT="| Metric | Count | |--------|-------| +| Total symbols | ${SYMBOL_FMT} | +| Relationships | ${EDGE_FMT} | +| Execution flows | ${PROCESS_FMT} | | Python files | ${PY_COUNT} | | Rust files | ${RS_COUNT} | | GitHub workflows | ${YML_COUNT} | -| Documentation files | ${MD_COUNT} | - -EOF - -{ - render_section "Entry Points" "$ENTRY_SUMMARY" "No entry points detected" - render_section "Core Modules" "$CORE_SUMMARY" "No core modules detected" - render_section "Data Flow" "$DATA_SUMMARY" "No data flows detected" - render_section "Agent Orchestration" "$AGENT_SUMMARY" "No agent orchestration detected" - echo "---" - echo "*Generated by [GitNexus](https://github.com/nicholasgriffintn/gitnexus) code intelligence*" -} >> ARCHITECTURE.md - -# ── Validate output ─────────────────────────────────────────────────── -if [[ ! -s ARCHITECTURE.md ]]; then - echo "::error::Generated ARCHITECTURE.md is empty" >&2 +| Documentation files | ${MD_COUNT} |" + +# ── Replace content between markers ────────────────────────────────── +replace_marker_content() { + local file="$1" + local marker="$2" + local content="$3" + local start_marker="" + local end_marker="" + + awk -v start="$start_marker" -v end="$end_marker" -v replacement="$content" ' + $0 == start { + print + print replacement + skip = 1 + next + } + $0 == end { + print + skip = 0 + next + } + !skip { print } + ' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file" +} + +replace_marker_content "$ARCH_FILE" "overview" "$OVERVIEW_CONTENT" + +# ── Update timestamp ───────────────────────────────────────────────── +GENERATED_DATE=$(date -u '+%Y-%m-%d %H:%M UTC') +sed -i "s|^> Last updated:.*|> Last updated: ${GENERATED_DATE}|" "$ARCH_FILE" + +# ── Validate output ────────────────────────────────────────────────── +LINE_COUNT=$(wc -l < "$ARCH_FILE") +if [[ "$LINE_COUNT" -lt 100 ]]; then + echo "::error::Generated $ARCH_FILE has only $LINE_COUNT lines (expected >100). Marker replacement may have failed." >&2 exit 1 fi -if ! head -1 ARCHITECTURE.md | grep -q '^# '; then - echo "::error::Generated ARCHITECTURE.md does not start with a heading" >&2 +if ! head -1 "$ARCH_FILE" | grep -q '^# '; then + echo "::error::$ARCH_FILE does not start with a heading" >&2 exit 1 fi -for section in "Codebase Overview" "Entry Points" "Core Modules"; do - if ! grep -q "## $section" ARCHITECTURE.md; then - echo "::error::Generated ARCHITECTURE.md missing expected section: $section" >&2 +for section in "Codebase Overview" "Architecture Diagram" "Functional Areas" "Key Execution Flows" "Testing Architecture"; do + if ! grep -q "## $section" "$ARCH_FILE"; then + echo "::error::$ARCH_FILE missing expected section: $section" >&2 exit 1 fi done -echo "ARCHITECTURE.md generated successfully ($(wc -l < ARCHITECTURE.md) lines)" >&2 +echo "ARCHITECTURE.md updated successfully ($LINE_COUNT lines)" >&2 diff --git a/.github/workflows/update-architecture.yml b/.github/workflows/update-architecture.yml index 60243c2..9a4ae02 100644 --- a/.github/workflows/update-architecture.yml +++ b/.github/workflows/update-architecture.yml @@ -5,6 +5,7 @@ on: branches: [main] paths-ignore: - 'ARCHITECTURE.md' + - 'AGENTS.md' - '.github/workflows/update-architecture.yml' workflow_dispatch: @@ -53,12 +54,12 @@ jobs: - name: Check for changes id: changes run: | - if git diff --quiet ARCHITECTURE.md 2>/dev/null; then + if git diff --quiet ARCHITECTURE.md AGENTS.md 2>/dev/null; then echo "changed=false" >> "$GITHUB_OUTPUT" - echo "No changes to ARCHITECTURE.md" + echo "No changes detected" else echo "changed=true" >> "$GITHUB_OUTPUT" - echo "ARCHITECTURE.md has been updated" + echo "Architecture docs have been updated" fi - name: Create pull request diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 052c313..8c62fe4 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,10 +1,11 @@ # SuperClaude Architecture > Auto-generated by GitNexus knowledge graph analysis. -> Last updated: 2026-04-01 UTC +> Last updated: 2026-04-03 23:33 UTC ## Codebase Overview + | Metric | Count | |--------|-------| | Total symbols | 8,994 | @@ -12,8 +13,9 @@ | Execution flows | 300 | | Python files | 217 | | Rust files | 88 | -| GitHub workflows | 23 | +| GitHub workflows | 20 | | Documentation files | 270 | + SuperClaude is a framework for augmenting Claude Code with structured commands, agent personas, quality orchestration, and MCP integrations. It ships as an installable CLI toolkit with both Python and Rust components. diff --git a/tests/github_scripts/test_generate_architecture_sh.py b/tests/github_scripts/test_generate_architecture_sh.py index ae06251..0e0b7a6 100644 --- a/tests/github_scripts/test_generate_architecture_sh.py +++ b/tests/github_scripts/test_generate_architecture_sh.py @@ -72,19 +72,20 @@ def test_no_bare_fallback_without_warning(self): "Expected ::warning:: annotations for GitNexus query failures" ) - def test_query_errors_produce_valid_fallback(self): - """Fallback JSON should be valid (empty processes array).""" + def test_cypher_errors_produce_empty_fallback(self): + """Cypher query failures should return empty string fallback.""" content = SCRIPT_PATH.read_text() - assert '{"processes":[]}' in content + assert 'echo ""' in content, "Expected empty string fallback for failed cypher queries" class TestOutputValidation: """Validate that generated ARCHITECTURE.md is validated before output.""" - def test_validates_non_empty(self): - """Script checks that output is non-empty.""" + def test_validates_minimum_line_count(self): + """Script checks that output has a minimum line count.""" content = SCRIPT_PATH.read_text() - assert "-s ARCHITECTURE.md" in content or "! -s" in content + assert "LINE_COUNT" in content + assert "-lt 100" in content def test_validates_heading(self): """Script checks that output starts with a heading.""" @@ -95,12 +96,22 @@ def test_validates_expected_sections(self): """Script checks for expected sections like 'Codebase Overview'.""" content = SCRIPT_PATH.read_text() assert "Codebase Overview" in content + assert "Architecture Diagram" in content + assert "Functional Areas" in content + assert "Key Execution Flows" in content + assert "Testing Architecture" in content class TestStructure: """Validate expected functions and structure.""" - EXPECTED_FUNCTIONS = ["check_deps", "query_gitnexus", "extract_summaries", "render_section"] + EXPECTED_FUNCTIONS = [ + "check_deps", + "resolve_repo", + "cypher_query", + "format_number", + "replace_marker_content", + ] def test_all_functions_defined(self): """Script defines all expected helper functions.""" @@ -112,23 +123,161 @@ def test_all_functions_defined(self): # ── Tier 2: Execution Tests ────────────────────────────────────────── +SEED_ARCHITECTURE = textwrap.dedent("""\ +# SuperClaude Architecture + +> Auto-generated by GitNexus knowledge graph analysis. +> Last updated: 2026-01-01 00:00 UTC + +## Codebase Overview + + +| Metric | Count | +|--------|-------| +| Total symbols | 0 | +| Relationships | 0 | +| Execution flows | 0 | +| Python files | 0 | +| Rust files | 0 | +| GitHub workflows | 0 | +| Documentation files | 0 | + + +SuperClaude is a test project. + +## Architecture Diagram + +```mermaid +graph TB + subgraph CLI["CLI Layer"] + Install["install"] + Update["update"] + Uninstall["uninstall"] + end + + subgraph Core["Core Engine"] + LoopOrch["Loop Orchestrator"] + QualityAssess["Quality Assessment"] + end + + subgraph AgentSystem["Agent System"] + AgentReg["Agent Registry"] + AgentSel["Agent Selector"] + end + + subgraph MCPLayer["MCP Integration"] + PAL["PAL MCP"] + Rube["Rube MCP"] + end + + subgraph SetupCore["Setup & Components"] + Registry["Component Registry"] + Installer["Component Installer"] + Validator["System Validator"] + end + + Install --> Registry + Install --> Installer + Update --> Registry + LoopOrch --> QualityAssess + AgentSel --> AgentReg + + style CLI fill:#4a9eff,color:#fff + style Core fill:#ff6b6b,color:#fff + style AgentSystem fill:#ffd93d,color:#333 + style MCPLayer fill:#6bcb77,color:#fff + style SetupCore fill:#8b5cf6,color:#fff +``` + +## Functional Areas + +### Core Engine (`core/`) +The central orchestration layer. Test area. + +| Module | Purpose | +|--------|---------| +| `loop_orchestrator.py` | Agentic loop runner | +| `quality_assessment.py` | Multi-dimensional scoring | +| `metrics.py` | Telemetry collection | + +### Agent System (`agents/`) +Agent discovery and selection. + +| Component | Purpose | +|-----------|---------| +| `registry.py` | Agent indexing | +| `selector.py` | Intent matching | + +### Setup & Installer (`setup/`) +CLI toolkit for components. + +| Subsystem | Purpose | +|-----------|---------| +| CLI Commands | install, update, uninstall | +| Component Registry | Discovery and instantiation | +| System Validator | Requirement checks | + +## Key Execution Flows + +### 1. System Validation (7 steps) +Test flow description. + +``` +get_system_info -> check_node -> validate -> ValidationError +``` + +### 2. Component Installation (6 steps) +Installs a component. + +``` +install_component -> install -> validate_prerequisites +``` + +### 3. Component Update (6 steps) +Updates components. + +``` +perform_update -> create_component_instances -> get_metadata +``` + +## Testing Architecture + +Tests mirror the source structure: + +| Directory | Coverage | +|-----------|----------| +| `tests/core/` | Loop orchestrator, quality | +| `tests/agents/` | Registry, selector | +| `tests/setup/` | Uninstall, environment | +| `tests/integration/` | Workflow configs | +| `tests/e2e/` | Full pipeline | + +--- +*Generated by [GitNexus](https://github.com/nicholasgriffintn/gitnexus) code intelligence* +""") + + @pytest.fixture def mock_bin_dir(tmp_path): """Create a temp directory with mock binaries for npx and jq.""" bin_dir = tmp_path / "bin" bin_dir.mkdir() - # Mock npx: returns canned JSON for gitnexus query + # Mock npx: handles gitnexus list and cypher commands npx_script = bin_dir / "npx" npx_script.write_text( textwrap.dedent("""\ #!/usr/bin/env bash - if [[ "$1" == "gitnexus" && "$2" == "query" ]]; then - echo '{"processes":[{"id":"proc_1","summary":"Test Process","priority":0.9,"symbol_count":3,"process_type":"core","step_count":5}],"process_symbols":[],"definitions":[]}' + if [[ "$1" == "gitnexus" && "$2" == "list" ]]; then + echo " Indexed Repositories (1)" + echo "" + echo " TestRepo" + echo " Path: /tmp/test" + elif [[ "$1" == "gitnexus" && "$2" == "cypher" ]]; then + # Return mock count for any cypher query + echo '{"markdown":"| c |\\n| --- |\\n| 42 |","row_count":1}' elif [[ "$1" == "gitnexus" && "$2" == "analyze" ]]; then echo "Indexed." - elif [[ "$1" == "gitnexus" && "$2" == "status" ]]; then - echo "Indexed: 100 files, 500 symbols" else echo "mock npx: $*" >&2 fi @@ -156,10 +305,18 @@ def mock_bin_dir(tmp_path): def run_generate(mock_bin_dir, tmp_path): """Helper to run generate-architecture.sh with mock environment.""" - def _run(extra_env=None): + def _run(extra_env=None, seed_content=SEED_ARCHITECTURE): work_dir = tmp_path / "repo" work_dir.mkdir(exist_ok=True) + # Seed ARCHITECTURE.md with markers (required for marker-based updates) + arch_file = work_dir / "ARCHITECTURE.md" + arch_file.write_text(seed_content) + + # Create .github/workflows dir for find to count + wf_dir = work_dir / ".github" / "workflows" + wf_dir.mkdir(parents=True, exist_ok=True) + env = os.environ.copy() env["PATH"] = f"{mock_bin_dir}:{env['PATH']}" env["HOME"] = str(tmp_path) @@ -175,7 +332,6 @@ def _run(extra_env=None): cwd=str(work_dir), timeout=30, ) - arch_file = work_dir / "ARCHITECTURE.md" arch_content = arch_file.read_text() if arch_file.exists() else "" return result, arch_content @@ -185,35 +341,91 @@ def _run(extra_env=None): class TestExecution: """Functional tests via subprocess with mock binaries.""" - def test_generates_architecture_md(self, run_generate): - """Script creates ARCHITECTURE.md with expected structure.""" + def test_updates_architecture_md(self, run_generate): + """Script updates ARCHITECTURE.md preserving curated content.""" result, content = run_generate() assert result.returncode == 0, f"Script failed: {result.stderr}" assert content.startswith("# SuperClaude Architecture") assert "## Codebase Overview" in content - assert "## Entry Points" in content - assert "## Core Modules" in content + assert "## Architecture Diagram" in content + assert "## Functional Areas" in content + assert "## Key Execution Flows" in content + assert "## Testing Architecture" in content - def test_includes_file_counts(self, run_generate): - """Generated file includes file count table.""" + def test_updates_file_counts(self, run_generate): + """Updated file includes file count table with data from cypher queries.""" result, content = run_generate() assert result.returncode == 0 assert "Python files" in content assert "Rust files" in content + assert "Total symbols" in content + assert "Relationships" in content - def test_includes_process_summaries(self, run_generate): - """Generated file includes process summaries from GitNexus.""" + def test_preserves_curated_content(self, run_generate): + """Script preserves hand-curated content outside markers.""" result, content = run_generate() assert result.returncode == 0 - assert "Test Process" in content + # These sections are from the seed and should be preserved + assert "SuperClaude is a test project" in content + assert "graph TB" in content # Mermaid diagram + assert "Test area" in content # Functional area description + assert "Test flow description" in content # Execution flow def test_includes_footer(self, run_generate): - """Generated file includes GitNexus attribution footer.""" + """Updated file preserves GitNexus attribution footer.""" result, content = run_generate() assert result.returncode == 0 assert "GitNexus" in content assert "code intelligence" in content + def test_updates_timestamp(self, run_generate): + """Script updates the last-updated timestamp.""" + result, content = run_generate() + assert result.returncode == 0 + assert "2026-01-01 00:00 UTC" not in content # Old timestamp replaced + assert "Last updated:" in content + + def test_fails_without_architecture_md(self, mock_bin_dir, tmp_path): + """Script fails with error if ARCHITECTURE.md is missing.""" + work_dir = tmp_path / "empty_repo" + work_dir.mkdir() + + env = os.environ.copy() + env["PATH"] = f"{mock_bin_dir}:{env['PATH']}" + env["HOME"] = str(tmp_path) + + result = subprocess.run( + ["bash", str(SCRIPT_PATH)], + capture_output=True, + text=True, + env=env, + cwd=str(work_dir), + timeout=30, + ) + assert result.returncode != 0 + assert "not found" in result.stderr + + def test_fails_without_markers(self, mock_bin_dir, tmp_path): + """Script fails with error if ARCHITECTURE.md has no markers.""" + work_dir = tmp_path / "no_markers" + work_dir.mkdir() + (work_dir / "ARCHITECTURE.md").write_text("# Architecture\n\nNo markers here.\n") + + env = os.environ.copy() + env["PATH"] = f"{mock_bin_dir}:{env['PATH']}" + env["HOME"] = str(tmp_path) + + result = subprocess.run( + ["bash", str(SCRIPT_PATH)], + capture_output=True, + text=True, + env=env, + cwd=str(work_dir), + timeout=30, + ) + assert result.returncode != 0 + assert "marker" in result.stderr.lower() + def test_missing_dependency_exits_with_error(self, tmp_path): """Missing jq causes exit 1 with error message.""" # Create a restricted PATH with symlinks to real coreutils but NOT jq. @@ -241,6 +453,8 @@ def test_missing_dependency_exits_with_error(self, tmp_path): "echo", "test", "command", + "awk", + "mv", ]: real = shutil.which(cmd) if real: