diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e1d8e5b..9eafc7e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,7 +6,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Lint shell scripts - run: find . -name '*.sh' -not -path '*/.*' | xargs shellcheck --severity=warning + - name: Checkout code + uses: actions/checkout@v4 + - name: Lint shell scripts + run: find . -name '*.sh' -not -path '*/.*' | xargs shellcheck --severity=warning diff --git a/README.md b/README.md new file mode 100644 index 0000000..3d6218b --- /dev/null +++ b/README.md @@ -0,0 +1,671 @@ +``` + █████╗ ██╗ ██╗ +██╔══██╗╚██╗██╔╝ +███████║ ╚███╔╝ +██╔══██║ ██╔██╗ +██║ ██║██╔╝ ██╗ +╚═╝ ╚═╝╚═╝ ╚═╝ +``` + +--- + +# ⚡ AX — The Cybernetic Workflow Architect + +> **"It sees your stack. It learns your structure. It writes what you'd spend hours crafting."** + +**AX** is a single-file, zero-dependency Bash utility that autonomously generates production-ready GitHub Actions CI/CD workflows. It scans your project's file tree, concurrently fingerprints every programming language present using its **Hybrid Detection Engine**, packages that intelligence into a structured prompt, and dispatches it to **OpenAI GPT-4o** — all while a live **Cybernetic Eye** animation renders in your terminal at 16 frames per second. + +The result: a valid `.github/workflows/main.yml` tailored exactly to your stack, written in seconds, without you touching a YAML key. + +**Developer:** Nikan Eidi — Computer Programming & Analysis Student + +--- + +## 📡 Table of Contents + +1. [System Architecture & Logic](#-system-architecture--logic) +2. [Hybrid Detection Engine](#-hybrid-detection-engine) +3. [Pseudocode — Core Execution Flows](#-pseudocode--core-execution-flows) +4. [Technical Features](#-technical-features) + - [Cybernetic Eye Animation](#-cybernetic-eye-animation) + - [Ghost-Dependency Isolation](#-ghost-dependency-isolation) + - [TUI Arrow-Key Navigation](#-tui-arrow-key-navigation) +5. [Public Documentation Directory](#-public-documentation-directory) +6. [Installation & Usage](#-installation--usage) +7. [Technical Stack](#-technical-stack) +8. [Author](#-author) + +--- + +## 🧠 System Architecture & Logic + +AX executes through a strict, **seven-state linear pipeline**. A global `STATE` variable transitions from `IDLE` through each named phase, providing a deterministic execution trace auditable at any point. Every transition is reflected in the live animation label running in parallel. + +``` +IDLE → SCANNING → BUILDING → REQUESTING → PARSING → WRITING → DONE + ↕ + ERROR ←──────────────── (any phase) +``` + +### End-to-End Execution Flowchart + +```mermaid +%%{init: {'theme': 'dark', 'themeVariables': { + 'primaryColor': '#00fff7', + 'primaryTextColor': '#ffffff', + 'primaryBorderColor': '#c724b1', + 'lineColor': '#c724b1', + 'secondaryColor': '#0d0d0d', + 'tertiaryColor': '#0d0d0d', + 'background': '#0d0d0d', + 'mainBkg': '#0d0d0d', + 'nodeBorder': '#00fff7' +}}}%% +flowchart TD + ENTRY([⚡ ax — invoked from project root]) + ENTRY --> A + + subgraph P1 ["🔍 PHASE 1 — TREE SCAN • STATE: SCANNING"] + A["get_project_tree()\nfind . -maxdepth 3 -not -path '*/.*'\ngrep -vE node_modules · .git · build · dist · bin · obj · target · venv\nsed pipeline → indented tree representation"] + end + + subgraph P2 ["⚙️ PHASE 2 — HYBRID DETECTION • STATE: BUILDING"] + B["get_context()\nConcurrent manifest + glob checks\nAccumulate all hits into tags[] array\nNode · Python · Go · Rust · Java · C++ · Ruby · Shell · Windows"] + end + + subgraph P3 ["📦 PHASE 3 — PAYLOAD PACKAGING • STATE: BUILDING"] + C["create_payload()\npython3 -c json.dumps()\nInject system prompt with 8 directives\nax.sh explicitly excluded · temperature: 0.1"] + end + + subgraph P4 ["🤖 PHASE 4 — AI CONSULTATION • STATE: REQUESTING"] + D["request_ai()\ncurl -s -X POST api.openai.com/v1/chat/completions\nModel: gpt-4o · --max-time 60 · --connect-timeout 10\nCapture body + HTTP status via -w flag\nFull curl-exit + HTTP error classification matrix"] + end + + subgraph P5 ["🧹 PHASE 5 — YAML SANITISATION • STATE: PARSING"] + E["parse_response()\npython3 json.load via stdin\nre.sub strip ``` opening fence with optional lang tag\nre.sub strip ``` closing fence\nRoute API_ERROR · PARSE_ERROR prefix strings"] + end + + subgraph P6 ["💾 PHASE 6 — FILE WRITE • STATE: WRITING"] + F["mkdir -p .github/workflows/\nValidate final_yaml non-empty\necho final_yaml > main.yml\nCheck write success"] + end + + subgraph P7 ["🔗 PHASE 7 — GIT-SYNC PROMPT • STATE: DONE"] + G["prompt_git_sync()\n8-line TUI panel · tput sc/rc scroll-safe\nArrow-key + y/n/Enter/ESC input loop\ncommand -v git-sync before dispatch"] + end + + ENTRY --> P1 --> P2 --> P3 --> P4 --> P5 --> P6 --> P7 + A --> B --> C --> D --> E --> F --> G + + D -- "NETWORK_ERROR\nAUTH_ERROR · RATE_ERROR\nSERVER_ERROR · HTTP_ERROR" --> ERR(["⚡ stop_loader\ntput cnorm\nprintf error msg\nexit 1"]) + E -- "API_ERROR\nPARSE_ERROR\nempty output" --> ERR + F -- "mkdir failure\nwrite failure" --> ERR + + G -- "YES + git-sync on PATH" --> SYNC["git-sync"] + G -- "YES + git-sync absent" --> MISS(["⚡ install hint\nexit 1"]) + G -- "NO · ESC · n" --> OK(["✓ Graceful exit 0"]) + SYNC --> OK + + style P1 fill:#0d0d0d,stroke:#00fff7,color:#00fff7 + style P2 fill:#0d0d0d,stroke:#c724b1,color:#c724b1 + style P3 fill:#0d0d0d,stroke:#00fff7,color:#00fff7 + style P4 fill:#0d0d0d,stroke:#c724b1,color:#c724b1 + style P5 fill:#0d0d0d,stroke:#00fff7,color:#00fff7 + style P6 fill:#0d0d0d,stroke:#c724b1,color:#c724b1 + style P7 fill:#0d0d0d,stroke:#00fff7,color:#00fff7 + style ERR fill:#3d0000,stroke:#ff0040,color:#ff4060 + style MISS fill:#3d0000,stroke:#ff0040,color:#ff4060 + style OK fill:#001a00,stroke:#00ff88,color:#00ff88 +``` + +--- + +## ⚙️ Hybrid Detection Engine + +`get_context()` is AX's language fingerprinter. It performs a **single-pass, non-exclusive concurrent check** — every test runs independently, so matching Node.js never skips the Python or Go checks. Every hit appends a label to a local `tags[]` Bash array. The final `${tags[*]}` expansion produces a space-separated context string that is passed verbatim into the GPT-4o user message. + +```bash +# Exact source — get_context() in ax.sh +local tags=() + +[ -f "package.json" ] && tags+=("Node.js") +([ -f "requirements.txt" ] || [ -f "pyproject.toml" ] || ls *.py &>/dev/null) && tags+=("Python") +([ -f "go.mod" ] || ls *.go &>/dev/null) && tags+=("Go") +([ -f "Cargo.toml" ] || ls *.rs &>/dev/null) && tags+=("Rust") +([ -f "pom.xml" ] || [ -f "build.gradle" ] || ls *.java &>/dev/null) && tags+=("Java") +(ls *.cpp &>/dev/null || ls *.c &>/dev/null || ls *.h &>/dev/null) && tags+=("C/C++") +([ -f "Gemfile" ] || ls *.rb &>/dev/null) && tags+=("Ruby") +(ls *.sh &>/dev/null) && tags+=("Shell") +(ls *.bat &>/dev/null || ls *.ps1 &>/dev/null) && tags+=("Windows-Script") + +if [ ${#tags[@]} -eq 0 ]; then + echo "General/Single-File" +else + echo "${tags[*]}" +fi +``` + +### Detection Signal Matrix + +| Language Tag | Manifest Signal | Glob Signal | Detection Logic | +|---|---|---|---| +| **Node.js** | `package.json` | — | Manifest only | +| **Python** | `requirements.txt` · `pyproject.toml` | `*.py` | Manifest **OR** glob | +| **Go** | `go.mod` | `*.go` | Manifest **OR** glob | +| **Rust** | `Cargo.toml` | `*.rs` | Manifest **OR** glob | +| **Java** | `pom.xml` · `build.gradle` | `*.java` | Manifest **OR** glob | +| **C/C++** | — | `*.cpp` · `*.c` · `*.h` | Glob only | +| **Ruby** | `Gemfile` | `*.rb` | Manifest **OR** glob | +| **Shell** | — | `*.sh` | Glob only | +| **Windows-Script** | — | `*.bat` · `*.ps1` | Glob only | + +**Fallback:** If `tags[]` remains empty after all checks, the function returns `"General/Single-File"` — the AI generates a safe minimal skeleton rather than failing. + +> **Example — polyglot monorepo with 5 stacks present:** +> ``` +> Node.js Python Go Rust Shell +> ``` +> GPT-4o receives this string and produces a five-language hybrid workflow in a single API call. + +--- + +## 📋 Pseudocode — Core Execution Flows + +### `main()` — Primary Orchestration + +``` +PROCEDURE main(): + + SET state ← SCANNING + START background loader WITH label "Scanning project structure..." + + SET progress ← 10% + CALL get_project_tree() → structure + ON FAILURE: stop loader · print "⚡ Failed to scan project tree." · EXIT 1 + + CALL get_context() → context + + SET progress ← 30%, label ← "Building AI payload [ {context} ]..." + SET state ← BUILDING + CALL create_payload(structure, context) → payload + IF payload IS EMPTY: stop loader · print "⚡ Payload is empty — aborting." · EXIT 1 + + SET progress ← 50%, label ← "Consulting OpenAI [ gpt-4o ]..." + SET state ← REQUESTING + CALL request_ai(payload) → raw_response, req_status + IF req_status != 0 OR raw_response MATCHES (*_ERROR:*): + stop loader · print "⚡ {raw_response}" · SET state ERROR · EXIT 1 + + SET progress ← 80%, label ← "Parsing response..." + SET state ← PARSING + CALL parse_response(raw_response) → final_yaml + IF final_yaml MATCHES "API_ERROR:*": stop loader · print OpenAI error · EXIT 1 + IF final_yaml MATCHES "PARSE_ERROR*": stop loader · print parse error · EXIT 1 + IF final_yaml IS EMPTY: stop loader · print empty error · EXIT 1 + + SET progress ← 95%, label ← "Writing workflow file..." + SET state ← WRITING + CREATE directory .github/workflows/ ON FAIL: stop loader · EXIT 1 + WRITE final_yaml → .github/workflows/main.yml + ON FAIL: stop loader · print "⚡ Failed to write main.yml" · EXIT 1 + + SET progress ← 100%, label ← "Complete!" + SET state ← DONE + SLEEP 0.4 seconds + STOP loader + + PRINT "[+] Success! .github/workflows/main.yml has been created." + CALL prompt_git_sync() + +END PROCEDURE +``` + +--- + +### `_loader_loop()` — Background Animation Engine + +``` +PROCEDURE _loader_loop(stop_file, prog_file, label_file, line_count=18): + + HIDE terminal cursor → tput civis + PRINT line_count blank lines → reserve vertical space + MOVE cursor UP line_count rows → \033[18A + SAVE cursor position → tput sc ← scroll-safe absolute anchor + + SET frame ← 0 + + WHILE stop_file DOES NOT EXIST: + + READ progress ← prog_file (default: "0") + READ label ← label_file (default: "Processing...") + + RESTORE cursor to saved anchor → tput rc + + COMPUTE blink_step ← frame MOD 16 + IF blink_step IN {0..9, 13..15} → eye_state = OPEN + ELIF blink_step IN {10, 12} → eye_state = HALF_CLOSED + ELIF blink_step = 11 → eye_state = FULLY_CLOSED + + SELECT iris_glyph ← ['◉','●','◎','◉','◈','●','◎','◉'] AT (frame MOD 8) + SELECT sparkle_glyph← ['·','∘','·','°','·','∘','·','°'] AT (frame MOD 8) + SELECT spinner ← braille 10-glyph set AT (frame MOD 10) + BUILD progress_bar ← (progress × 24 / 100) × '█' + remainder × '░' + + RENDER eye body → 13 lines (_eye_open / _eye_half / _eye_closed) + RENDER blank line → 1 line (\033[2K) + RENDER bar box top → 1 line (┌────────────────────────────────┐) + RENDER bar row → 1 line (│ {bar} {progress}% │) + RENDER bar box bot → 1 line (└────────────────────────────────┘) + RENDER label line → 1 line ({spinner} {label}) + // Total = 18 lines — invariant must never drift + + frame ← frame + 1 + SLEEP 0.06 seconds → ≈ 16 fps + + END WHILE + + RESTORE cursor to anchor → tput rc + ERASE 18 lines → \033[2K\n × 18 + RESTORE cursor to anchor → tput rc + SHOW terminal cursor → tput cnorm + +END PROCEDURE +``` + +--- + +## 🔬 Technical Features + +--- + +### ⚡ Cybernetic Eye Animation + +The animation is a **parallel background process** that communicates with the parent process entirely through three shared tmpfiles. It renders an 18-line deterministic block at approximately 16fps using raw ANSI escape sequences and `tput` for all cursor operations. + +#### Process & IPC Architecture + +``` +Parent Process (main) Background Subshell (_loader_loop &) +───────────────────── ────────────────────────────────────── +start_loader() + mktemp → /tmp/ci_stop.XXXXXX ←→ Polls: while [ ! -f "$stop_file" ] + mktemp → /tmp/ci_prog.XXXXXX → Reads: progress each frame + mktemp → /tmp/ci_label.XXXXXX → Reads: label each frame + rm -f $_STOP_FILE Loop runs while stop file is ABSENT + echo "0" > $_PROG_FILE + echo "$label" > $_LABEL_FILE + _loader_loop ... & + LOADER_PID=$! + +set_loader_progress 80 "Parsing..." + echo "80" > $_PROG_FILE → Next frame reads "80" for bar render + echo "Parsing" > $_LABEL_FILE → Next frame reads new label text + +stop_loader() + touch "$_STOP_FILE" → Loop detects file · exits cleanly + wait "$LOADER_PID" + rm -f all three tmpfiles + clear all global variables +``` + +No signals, no pipes, no subshell variable scope leakage — the entire IPC channel is three files in `/tmp/`. + +#### The 18-Line Frame Budget + +Every call to `_draw_frame()` emits **exactly** 18 lines. This invariant is what makes the `tput rc` cursor-rewind safe — if any frame emits 17 or 19 lines, all subsequent frames drift permanently. + +| Lines | Component | Emitted By | +|---|---|---| +| 13 | Eye body | `_eye_open()` · `_eye_half()` · `_eye_closed()` | +| 1 | Blank separator | `printf '%s\n' "$CLR"` | +| 1 | Bar box top | `┌────────────────────────────────┐` | +| 1 | Bar row | `│ ████████████░░░░░░░░░░░░ 50% │` | +| 1 | Bar box bottom | `└────────────────────────────────┘` | +| 1 | Animated label | `⠹ Consulting OpenAI [ gpt-4o ]...` | +| **18** | **Total — invariant** | | + +#### The 16-Step Blink Cycle + +```bash +# Exact source — _draw_frame() in ax.sh +local step=$(( f % 16 )) + +local eye_state="open" +[[ $step -eq 10 || $step -eq 12 ]] && eye_state="half" +[[ $step -eq 11 ]] && eye_state="closed" +``` + +``` +Frame: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +State: [──────────────── OPEN ────────────] [HALF] [SHUT] [HALF] [OPEN] +``` + +Three independent animation counters run simultaneously: + +| Glyph Set | Array | Cycle | Counter | +|---|---|---|---| +| Iris center | `◉ ● ◎ ◉ ◈ ● ◎ ◉` | 8 frames | `f % 8` | +| Pupil sparkle | `· ∘ · ° · ∘ · °` | 8 frames | `f % 8` | +| Status spinner | 10 Braille chars | 10 frames | `f % 10` | + +Each cycles independently of the 16-frame blink, producing organic asynchronous micro-animation across all eye states. + +#### Why `tput sc` / `tput rc` and Not `\033[nA` + +```bash +# FRAGILE — breaks on any terminal scroll event +printf "\033[18A" + +# CORRECT — used throughout ax.sh +tput sc # Saves absolute cursor address in terminal's own state memory +# ... render 18 lines ... +tput rc # Restores that absolute address — unaffected by scroll +``` + +`\033[nA` (cursor-up N rows) is relative to the current viewport top. A single scroll event during the API call shifts the viewport, making the count wrong — all subsequent frames render at the wrong position with no recovery. `tput sc` stores an absolute cursor coordinate in the terminal emulator's own memory. `tput rc` retrieves it unconditionally regardless of how much the viewport has scrolled. + +#### Eye Frame Sub-Renderers + +Each sub-renderer prints **exactly 13 lines** — the contract that upholds the 18-line budget: + +| Function | Eye State | Frames | Signature Visual | +|---|---|---|---| +| `_eye_open()` | Fully open | 0–9, 13–15 | Tiered lashes · sclera dots · live iris ring · animated pupil | +| `_eye_half()` | Half-closed | 10, 12 | Descending `▄▄▄` lid · iris arc peeking · drooping lashes | +| `_eye_closed()` | Fully sealed | 11 | `╔═══╗` border · neon `━━━` seam · `▄▄▄/▀▀▀` fill · flat lashes | + +`_eye_open()` receives the current frame number and applies an additional iris ring flicker: when `f % 4 == 0`, the ring color shifts to `${BOLD}${GLOW}`, creating a 4-frame brightness pulse layered on top of all other animation cycles. + +#### The Progress Bar + +```bash +# Exact source — _build_bar() in ax.sh +local width=24 +local filled=$(( prog * width / 100 )) +local empty=$(( width - filled )) +local bar="" spaces="" +for ((i=0; i YES < ║ NO ║ ← selected = 0 + ╠════════════════╩═══════════════════════════╣ + ║ ↑← YES ↓→ NO ↵ confirm y/n quick ║ + ╚════════════════════════════════════════════╝ +``` + +The box inner width is 44 characters — split into a **16-char left cell** (YES) and a **27-char right cell** (NO), joined by a `╦`/`╩` column divider. The active option is highlighted using `${BOLD}${REV}` (reverse video), redrawing on every navigation event for immediate visual feedback. + +#### Raw Input Event Loop + +```bash +# Exact source — prompt_git_sync() event loop in ax.sh +IFS= read -rsn1 key # Read exactly 1 raw byte + +if [[ "$key" == $'\033' ]]; then # ESC detected (0x1B) + IFS= read -rsn1 -t 0.05 seq # Read 2nd byte (50ms timeout) + if [[ "$seq" == '[' ]]; then + IFS= read -rsn1 -t 0.05 key # Read 3rd byte: direction char + case "$key" in + A|D) selected=0 ;; # ↑ Up or ← Left → YES + B|C) selected=1 ;; # ↓ Down or → Right → NO + esac + tput rc 2>/dev/null # Restore to panel top + _draw_prompt "$selected" # Immediate redraw + continue + fi + selected=1; break # Lone ESC → cancel = NO +fi + +case "$key" in + y|Y) selected=0; break ;; # Direct YES, no Enter needed + n|N) selected=1; break ;; # Direct NO, no Enter needed + $'\n'|$'r'|'') break ;; # Enter → confirm current selection +esac +``` + +#### Complete Key Mapping + +| Key Input | Raw Bytes | Action | +|---|---|---| +| `↑` Up arrow | `1B 5B 41` (ESC [ A) | Select **YES** | +| `←` Left arrow | `1B 5B 44` (ESC [ D) | Select **YES** | +| `↓` Down arrow | `1B 5B 42` (ESC [ B) | Select **NO** | +| `→` Right arrow | `1B 5B 43` (ESC [ C) | Select **NO** | +| `y` or `Y` | 1 byte | Confirm **YES** instantly (no Enter required) | +| `n` or `N` | 1 byte | Confirm **NO** instantly (no Enter required) | +| `Enter` / `↵` | `0x0A` | Confirm current selection | +| `ESC` (lone) | `0x1B` + 50ms silence | Cancel → **NO** | + +The lone-ESC detection relies on a 50ms timeout on the follow-up read. If no second byte arrives within 50ms of the ESC, it is classified as a standalone Escape keypress (cancel → NO), not the start of an arrow sequence. After any confirmation, the panel is wiped with 8× `\033[2K\n`, the cursor is restored to the panel origin, and AX either runs `git-sync` (after a `command -v` guard) or exits gracefully with code `0`. + +--- + +## 📁 Public Documentation Directory + +The `/public` directory ships four companion reference documents providing complete engineering transparency for every layer of AX. + +--- + +### [`/public/Traceability Matrix.md`](./public/Traceability%20Matrix.md) + +A structured mapping of every **user-facing feature to its implementing function(s)**, execution phase, and dependencies. Covers **96 traced features** across seven sections: Core Pipeline, State Machine, Animation System, Ghost-Dependency Prevention, TUI & Interaction, Error Handling, and Visual/ANSI. + +Use this document for code review, regression auditing, and contributor onboarding. + +--- + +### [`/public/Functions Logs & Descriptions.md`](./public/Functions%20Logs%20%26%20Descriptions.md) + +A complete **function-level API reference** for every Bash function and embedded Python3 helper in `ax.sh`. Each entry documents: purpose, full signature, typed parameter table, return values, side effects, step-by-step execution sequence, error behaviour, and design rationale notes. + +Includes a global variable reference table and the full ANSI 256-color variable reference. + +**All 21 functions documented:** `main` · `set_state` · `get_state` · `cleanup` · `on_error` · `start_loader` · `stop_loader` · `set_loader_progress` · `_loader_loop` · `_draw_frame` · `_build_bar` · `_eye_open` · `_eye_half` · `_eye_closed` · `get_project_tree` · `get_context` · `create_payload` · `request_ai` · `parse_response` · `prompt_git_sync` · `_draw_prompt` + +--- + +### [`/public/Test Cases.md`](./public/Test%20Cases.md) + +Formal test scenario specifications for **20 test cases** covering the full surface area of AX — pipeline correctness, AI output quality, error handling, animation resilience, and TUI edge cases. Every case includes exact environment setup scripts, expected outputs, pass criteria, and fail indicators. + +| ID | Name | What It Validates | +|---|---|---| +| **TC-001** | The Chonk-Meter | 5-language monorepo → hybrid workflow · zero ghost steps | +| **TC-002** | Hybrid Stress Test | Node + Python → exact two-language workflow only | +| **TC-003** | Ghost-Dep Isolation | Shell-only → exactly 2 steps · `ax.sh` never referenced | +| **TC-005** | Auth Error Handling | Missing key → `AUTH_ERROR:` · exit 1 · clean terminal | +| **TC-006** | SIGINT Resilience | Ctrl+C at any phase → exit 130 · cursor restored · no orphan PIDs | +| **TC-011** | Fence Stripping | 4 fence variants → identical clean YAML output | +| **TC-014** | Animation Scroll Safety | Viewport scroll during API call → animation stays aligned | + +--- + +### [`/public/Structure.md`](./public/Structure.md) + +A deep-dive technical architecture reference covering every structural and design decision in `ax.sh`. Topics include: the 4-stage `find | grep | sed` tree scanner pipeline and why `maxdepth 3`, the hybrid detection array accumulation pattern, the SINGLEQUOTE token substitution design, the tmpfile IPC architecture, the `tput sc/rc` vs `\033[nA` rationale, the 3-byte ESC sequence input parser, the 18-line frame budget invariant, the `temperature: 0.1` selection reasoning, the ERR trap `$LINENO` deferred expansion, and the complete external command dependency map. + +--- + +## 🚀 Installation & Usage + +### Prerequisites + +| Requirement | Minimum Version | Role in AX | +|---|---|---| +| **Bash** | 4.0+ | Runtime — indexed array syntax requires v4 | +| **Python 3** | 3.7+ | `create_payload()` and `parse_response()` — stdlib only (`json`, `re`, `sys`) | +| **curl** | 7.x+ | HTTP POST to OpenAI — `-w` status capture requires 7.x | +| **OpenAI API Key** | — | GPT-4o access | +| **git-sync** | any | *(Optional)* — only invoked if you confirm YES at the TUI prompt | + +--- + +### Step 1 — Clone the Repository + +```bash +git clone https://github.com//ax.git +cd ax +``` + +--- + +### Step 2 — Install Globally to `/usr/local/bin/ax` + +```bash +sudo cp ax.sh /usr/local/bin/ax +sudo chmod +x /usr/local/bin/ax +``` + +Verify the installation: + +```bash +which ax +# → /usr/local/bin/ax +``` + +--- + +### Step 3 — Configure Your OpenAI API Key + +**Zsh (recommended):** + +```bash +echo 'export OPENAI_API_KEY="sk-your-key-here"' >> ~/.zshrc +source ~/.zshrc +``` + +**Bash:** + +```bash +echo 'export OPENAI_API_KEY="sk-your-key-here"' >> ~/.bashrc +source ~/.bashrc +``` + +Confirm it is set: + +```bash +echo $OPENAI_API_KEY +# → sk-your-key-here +``` + +> ⚠️ **Security:** Never commit your API key to version control. For per-project key isolation, use [`direnv`](https://direnv.net/) with a `.envrc` file added to your `.gitignore`. + +--- + +### Step 4 — Run AX from Any Project Root + +```bash +cd /path/to/your/project +ax +``` + +AX will scan, detect, consult GPT-4o, sanitise the response, and write the workflow — all with the Cybernetic Eye animation tracking each phase. When complete, the TUI prompts for git-sync. + +**Generated output:** + +``` +.github/ +└── workflows/ + └── main.yml ← production-ready, stack-accurate CI workflow +``` + +--- + +### Updating AX + +```bash +cd /path/to/ax-repo +git pull origin main +sudo cp ax.sh /usr/local/bin/ax +``` + +--- + +## 🛠 Technical Stack + +| Layer | Technology | Role in AX | +|---|---|---| +| **Runtime** | Bash 4+ | All orchestration, state machine, IPC, signal handling, TUI event loop | +| **Data Serialisation** | Python 3 — `json` · `re` · `sys` | `create_payload()`: `json.dumps()` for safe JSON build · `parse_response()`: `re.sub()` for fence stripping and `json.load()` for response parsing | +| **AI Engine** | OpenAI GPT-4o (`gpt-4o`) | Single completions call per run · `temperature: 0.1` for determinism · system prompt with 8 behavioural directives | +| **HTTP Transport** | `curl` 7.x | `-s -w` dual-capture (body + HTTP code) · `--max-time 60` · `--connect-timeout 10` · full curl-exit-code + HTTP-status error matrix | +| **Terminal Colour** | ANSI 256-color escapes | `\033[38;5;{n}m` foreground palette — 10 named colors + `DIM` · `BOLD` · `REV` · `CLR` modifiers | +| **Cursor Control** | `tput` (ncurses) | `tput sc/rc` scroll-safe anchor · `tput civis/cnorm` cursor hide/show · `tput rev` reverse-video highlighting | +| **Parallel Process** | Bash subshell `&` | `_loader_loop` runs detached · `LOADER_PID` tracked · `wait` for clean join on stop | +| **IPC** | `mktemp` tmpfiles | Three files in `/tmp/` — progress integer · label string · stop sentinel (existence-based signal) | +| **Signal Handling** | `trap` | `INT TERM HUP` → `cleanup()` (exit 130) · `ERR` → `on_error $LINENO` (exit original code) | +| **Raw Input** | `read -rsn1` | 3-byte ESC sequence detection for arrow keys · 50ms timeout for lone-ESC disambiguation | + +--- + +## 👤 Author + +**Nikan Eidi** +Computer Programming & Analysis Student + +> *"AX started as a workflow generator. It became a study in how far you can push a terminal."* + +--- + +
+ +``` +⚡ See the structure. Generate the workflow. Own the pipeline. ⚡ +``` + +
\ No newline at end of file diff --git a/ax.sh b/ax.sh index 1fe1ce1..7e50480 100755 --- a/ax.sh +++ b/ax.sh @@ -559,6 +559,13 @@ prompt_git_sync() { tput sc 2>/dev/null _draw_prompt "$selected" + # Save terminal state — read -rs modifies stty settings; must restore on exit + local stty_save + stty_save=$(stty -g 2>/dev/null) + + # Drain any stray bytes left in stdin by the loader before reading input + while IFS= read -rsn1 -t 0 _discard 2>/dev/null; do :; done + # Event loop — reads escape sequences (arrows) and plain chars while true; do local key seq @@ -566,22 +573,21 @@ prompt_git_sync() { # Read first byte — detects start of any keypress IFS= read -rsn1 key - # Arrow keys send ESC [ A/B — consume remaining 2 bytes if ESC detected + # Arrow keys send ESC [ C/D — read both remaining bytes in one call if [[ "$key" == $'\033' ]]; then - IFS= read -rsn1 -t 0.05 seq - if [[ "$seq" == '[' ]]; then - IFS= read -rsn1 -t 0.05 key - case "$key" in - A|D) # Up / Left arrow → YES - selected=0 ;; - B|C) # Down / Right arrow → NO - selected=1 ;; - esac - # Restore saved position and redraw after every navigation event - tput rc 2>/dev/null - _draw_prompt "$selected" - continue - fi + IFS= read -rsn2 -t 0.15 seq || true + case "$seq" in + '[D') # Left arrow → YES + selected=0 + tput rc 2>/dev/null + _draw_prompt "$selected" + continue ;; + '[C') # Right arrow → NO + selected=1 + tput rc 2>/dev/null + _draw_prompt "$selected" + continue ;; + esac # Lone ESC treated as cancel → NO selected=1 break @@ -595,6 +601,9 @@ prompt_git_sync() { esac done + # Restore terminal state — undo any stty changes made by read -rs + stty "$stty_save" 2>/dev/null + # Wipe the prompt panel then restore cursor to panel origin tput rc 2>/dev/null for ((i=0; i **Document Type:** Function-Level API Reference +> **Project:** AX — The Cybernetic Workflow Architect +> **Developer:** Nikan Eidi +> **Version:** 1.0.0 +> **Scope:** Complete documentation of every Bash function and embedded Python3 helper in `ax.sh` — including purpose, signature, inputs, outputs, side effects, error behaviour, and internal logic notes. + +--- + +## Conventions + +| Symbol | Meaning | +|--------|---------| +| `→` | Returns / outputs | +| `⚠️` | Side effect or destructive operation | +| `💀` | Can cause script exit | +| `🔁` | Runs in background subshell | +| `📁` | Reads or writes filesystem | + +--- + +## Table of Contents + +1. [State Management](#1-state-management) +2. [Signal & Event Handlers](#2-signal--event-handlers) +3. [Animation — Loader Control](#3-animation--loader-control) +4. [Animation — Frame Renderers](#4-animation--frame-renderers) +5. [Project Scanning](#5-project-scanning) +6. [Language Detection](#6-language-detection) +7. [Payload Construction](#7-payload-construction) +8. [API Communication](#8-api-communication) +9. [Response Parsing](#9-response-parsing) +10. [TUI Interaction](#10-tui-interaction) +11. [Main Orchestration](#11-main-orchestration) + +--- + +## 1. State Management + +--- + +### `set_state()` + +**Type:** Bash function +**Visibility:** Internal + +**Purpose:** +Sets the global `STATE` variable to represent the current execution phase of AX. Provides a single, consistent write point for state transitions — making the execution flow auditable and traceable. + +**Signature:** +```bash +set_state "PHASE_NAME" +``` + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `$1` | String | Yes | New state value. Accepted values: `IDLE`, `SCANNING`, `BUILDING`, `REQUESTING`, `PARSING`, `WRITING`, `DONE`, `ERROR` | + +**Returns:** Nothing (void). Modifies global `STATE` variable in place. + +**Side Effects:** +⚠️ Modifies global `STATE`. No other side effects. + +**Example:** +```bash +set_state "SCANNING" +# STATE is now "SCANNING" +``` + +**Notes:** +No validation is performed on the state string — any value can be written. Convention is to use the uppercase identifiers defined in the state machine. + +--- + +### `get_state()` + +**Type:** Bash function +**Visibility:** Internal / Debug + +**Purpose:** +Reads and echoes the current value of the global `STATE` variable. Provides a consistent read interface for any function that needs to inspect the current execution phase. + +**Signature:** +```bash +current=$(get_state) +``` + +**Parameters:** None. + +**Returns:** → Current value of `$STATE` (stdout). + +**Side Effects:** None. + +**Example:** +```bash +get_state +# Output: SCANNING +``` + +--- + +## 2. Signal & Event Handlers + +--- + +### `cleanup()` + +**Type:** Bash trap handler +**Trigger:** `INT`, `TERM`, `HUP` signals + +**Purpose:** +Provides a guaranteed clean exit path when the user presses `Ctrl+C`, or when the process receives a termination or hangup signal. Ensures the terminal is left in a usable state regardless of where in the execution the signal was received. + +**Signature:** +```bash +trap cleanup INT TERM HUP +``` + +**Parameters:** None (called directly by trap). + +**Returns:** Does not return — calls `exit 130`. + +**Execution Sequence:** +1. Calls `stop_loader()` — signals the background animation process to stop and waits for it +2. Calls `tput cnorm` — restores terminal cursor visibility +3. Prints `⚡ Sequence terminated.` in bold red with a newline +4. Calls `exit 130` + +**Side Effects:** +⚠️ Terminates the entire script process. +⚠️ Exit code `130` follows the POSIX/Unix convention for `128 + SIGINT(2)`. +⚠️ Removes all animation tmpfiles via `stop_loader()`. +📁 IPC tmpfiles are cleaned up. + +💀 **Fatal** — always exits. + +**Example trigger:** +```bash +# User presses Ctrl+C during AI request +# → cleanup() fires +# → loader stops +# → cursor restored +# → "⚡ Sequence terminated." printed +# → exit 130 +``` + +--- + +### `on_error()` + +**Type:** Bash trap handler +**Trigger:** `ERR` pseudo-signal (any command returning non-zero without explicit handling) + +**Purpose:** +Catches unexpected runtime errors that were not anticipated by the explicit error checks in `main()`. Provides a last-resort safety net that reports the exact line number and exit code of the failing command before performing cleanup. + +**Signature:** +```bash +trap 'on_error $LINENO' ERR +``` + +**Parameters:** + +| Parameter | Source | Description | +|-----------|--------|-------------| +| `$1` | `$LINENO` (trap expansion) | Line number of the failing command | + +**Internal Variables:** +- `exit_code` — captured from `$?` at the moment of invocation (before any commands change it) +- `line_num` — the passed-in `$1` + +**Returns:** Does not return — calls `exit "$exit_code"`. + +**Execution Sequence:** +1. Captures `$?` into `exit_code` +2. Calls `stop_loader()` +3. Calls `tput cnorm` +4. Prints `⚡ Unexpected error on line {N} (exit {code})` in bold red +5. Calls `exit "$exit_code"` + +**Side Effects:** +⚠️ Terminates the script with the original failing exit code. +⚠️ Cleans up animation state. + +💀 **Fatal** — always exits. + +**Notes:** +The `ERR` trap is intentionally distinct from `cleanup()` because it preserves and re-exits with the original non-zero exit code, enabling shell wrappers or CI systems to detect what kind of failure occurred. + +--- + +## 3. Animation — Loader Control + +--- + +### `start_loader()` + +**Type:** Bash function +**Visibility:** Public (called from `main()`) + +**Purpose:** +Initialises the animation subsystem: creates three IPC tmpfiles for inter-process communication, sets the initial progress to 0, and spawns `_loader_loop()` as a detached background process. Stores the background PID in `LOADER_PID` for lifecycle management. + +**Signature:** +```bash +start_loader "Initial label text" +``` + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `$1` | String | No | `"Processing..."` | Initial status label shown below the animation | + +**Returns:** Nothing (void). + +**Side Effects:** +📁 Creates three tmpfiles: `_STOP_FILE`, `_PROG_FILE`, `_LABEL_FILE` (all in `/tmp/`) +⚠️ Spawns a background process (PID stored in `$LOADER_PID`) +⚠️ `_STOP_FILE` is created by `mktemp` then **immediately deleted** — the loop checks for its *existence* as the stop signal, so it must start absent + +**Global Variables Written:** +- `$_STOP_FILE` — path to sentinel file +- `$_PROG_FILE` — path to progress percentage file +- `$_LABEL_FILE` — path to label text file +- `$LOADER_PID` — PID of background animation process + +**Example:** +```bash +start_loader "Scanning project structure..." +# → tmpfiles created +# → _loader_loop launched in background +# → animation begins +``` + +**Notes:** +`rm -f "$_STOP_FILE"` is called after `mktemp` to ensure the stop file does not exist when the loop begins. This is the "armed" state. + +--- + +### `set_loader_progress()` + +**Type:** Bash function +**Visibility:** Public (called from `main()`) + +**Purpose:** +Updates the progress percentage and optionally the label text displayed in the running animation. Communicates with the background loader process entirely through filesystem writes to the shared tmpfiles — no signals, no subshell variables. + +**Signature:** +```bash +set_loader_progress 50 "Consulting OpenAI..." +``` + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `$1` | Integer (0–100) | No | `0` | Progress percentage to display | +| `$2` | String | No | *(empty = no update)* | New label text. If empty, label is not updated | + +**Returns:** Nothing (void). + +**Side Effects:** +📁 Overwrites `$_PROG_FILE` with the progress integer +📁 Overwrites `$_LABEL_FILE` with the new label (only if `$2` is non-empty and file exists) + +**Example:** +```bash +set_loader_progress 80 "Parsing response..." +# _PROG_FILE now contains "80" +# _LABEL_FILE now contains "Parsing response..." +``` + +**Notes:** +Both writes are guarded with `[ -f "$file" ]` existence checks — if the loader was never started or has already been stopped and cleaned up, the writes are silently skipped rather than failing. + +--- + +### `stop_loader()` + +**Type:** Bash function +**Visibility:** Public (called from `main()`, `cleanup()`, `on_error()`) + +**Purpose:** +Signals the background animation process to stop, waits for it to clean up the terminal and exit, then removes all IPC tmpfiles and clears all loader-related global variables. Idempotent — safe to call even if no loader is running. + +**Signature:** +```bash +stop_loader +``` + +**Parameters:** None. + +**Returns:** Nothing (void). + +**Execution Sequence:** +1. Checks `$LOADER_PID` is non-empty and the process is still running (`kill -0`) +2. If running: creates `$_STOP_FILE` via `touch` (arms the stop signal) +3. Waits for background process to exit (`wait "$LOADER_PID"`) +4. Clears `LOADER_PID` +5. Removes `_STOP_FILE`, `_PROG_FILE`, `_LABEL_FILE` with `rm -f` +6. Clears all four global variables to empty string + +**Side Effects:** +⚠️ Sends stop signal to background animation process +📁 Removes three tmpfiles +⚠️ Clears `LOADER_PID`, `_STOP_FILE`, `_PROG_FILE`, `_LABEL_FILE` + +**Idempotency:** +If `LOADER_PID` is empty or the process is no longer running, the function skips directly to cleanup without error. + +--- + +### `_loader_loop()` 🔁 + +**Type:** Bash function (runs in background subshell) +**Visibility:** Private + +**Purpose:** +The core animation engine. Runs in a background process, reading progress and label from shared tmpfiles on each iteration, rendering a complete 18-line animation frame, then sleeping 60ms (≈16fps). Exits cleanly when the stop sentinel file appears. + +**Signature:** +```bash +_loader_loop "$_STOP_FILE" "$_PROG_FILE" "$_LABEL_FILE" "$_LOADER_LINES" & +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$1` | File path | Stop sentinel file — loop exits when this file exists | +| `$2` | File path | Progress file — contains integer 0-100 | +| `$3` | File path | Label file — contains current status string | +| `$4` | Integer | Number of lines the animation block occupies (18) | + +**Returns:** Nothing (exits when stop file appears). + +**Execution Sequence:** +1. Hides terminal cursor (`tput civis`) +2. Prints `$4` blank lines to reserve vertical space +3. Moves cursor up `$4` lines (`\033[{n}A`) +4. Saves cursor position (`tput sc`) +5. **Loop** (while stop file absent): + a. Reads current `prog` from `$_PROG_FILE` (default 0) + b. Reads current `label` from `$_LABEL_FILE` (default "Processing...") + c. Restores cursor to saved position (`tput rc`) + d. Calls `_draw_frame "$frame" "$label" "$prog"` + e. Increments `frame` + f. Sleeps 0.06 seconds +6. **On stop:** Restores cursor, prints 18× `\033[2K\n` (clean wipe), restores cursor +7. Shows terminal cursor (`tput cnorm`) + +**Side Effects:** +⚠️ Hides and restores terminal cursor +⚠️ Writes directly to terminal stdout +📁 Reads `$_PROG_FILE` and `$_LABEL_FILE` every frame +📁 Polls `$_STOP_FILE` every frame + +**Why `tput sc/rc` and not `\033[{n}A`?** +`\033[{n}A` (cursor-up N rows) cannot move the cursor above the current **viewport top**. If the terminal scrolls even one line during a long API call, the cursor-up count would be wrong and subsequent frames would render at the wrong position. `tput sc` stores an absolute cursor address in the terminal's own memory, which survives any scroll events. + +--- + +## 4. Animation — Frame Renderers + +--- + +### `_draw_frame()` + +**Type:** Bash function +**Visibility:** Private (called from `_loader_loop`) + +**Purpose:** +Master frame compositor. Determines the eye state based on the current frame number, selects animated glyph variants, and dispatches to the appropriate eye sub-renderer. Then appends the separator, progress bar box, and animated status label to complete the 18-line frame. + +**Signature:** +```bash +_draw_frame "$frame" "$label" "$progress" +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$1` | Integer | Current frame number (monotonically increasing) | +| `$2` | String | Status label text | +| `$3` | Integer (0–100) | Current progress percentage | + +**Returns:** Writes exactly 18 lines to stdout. + +**Blink Cycle Logic:** +``` +step = frame % 16 + +step 0-9: eye_state = "open" +step 10, 12: eye_state = "half" +step 11: eye_state = "closed" +step 13-15: eye_state = "open" +``` + +**Glyph Cycles:** + +| Variable | Array | Cycle Length | Expression | +|----------|-------|-------------|------------| +| `ir` (iris) | `◉ ● ◎ ◉ ◈ ● ◎ ◉` | 8 | `frame % 8` | +| `gl` (sparkle) | `· ∘ · ° · ∘ · °` | 8 | `frame % 8` | +| `s` (spinner) | Braille 10-glyph set | 10 | `frame % 10` | + +**Output structure (18 lines):** +``` +Lines 1-13: Eye body (dispatched to _eye_open/half/closed) +Line 14: Blank separator +Lines 15-17: Progress bar box (top border, bar+%, bottom border) +Line 18: Animated spinner + label +``` + +--- + +### `_build_bar()` + +**Type:** Bash function +**Visibility:** Private (called from `_draw_frame`) + +**Purpose:** +Constructs a 24-character wide progress bar string using Unicode block characters. The filled portion uses `█` (U+2588 FULL BLOCK) in cyan, and the unfilled portion uses `░` (U+2591 LIGHT SHADE) in dim magenta. + +**Signature:** +```bash +bar=$(_build_bar "$progress") +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$1` | Integer (0–100) | Progress percentage | + +**Returns:** → Styled progress bar string (stdout, captured via `$()`) + +**Calculation:** +``` +width = 24 +filled = progress × 24 / 100 (integer division) +empty = 24 − filled +``` + +**Output example at 50%:** +``` +████████████░░░░░░░░░░░░ (12 filled, 12 empty) +``` + +--- + +### `_eye_open()` + +**Type:** Bash function +**Visibility:** Private (called from `_draw_frame`) + +**Purpose:** +Renders a fully-open cybernetic eye as 13 precisely formatted terminal lines. Includes animated lash tiers (upper and lower), flanking `<-` and `->` directional indicators, a sclera region, an iris ring, and an animated pupil with sparkle glyphs. + +**Signature:** +```bash +_eye_open "$iris_glyph" "$sparkle_glyph" "$frame" +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$1` (`ir`) | String | Iris center glyph (e.g. `◉`, `◎`) | +| `$2` (`gl`) | String | Pupil sparkle glyph (e.g. `·`, `°`) | +| `$3` (`f`) | Integer | Current frame number (used for iris ring flicker: `f % 4 == 0` → bold) | + +**Returns:** Writes exactly 13 lines to stdout. + +**Line Layout:** + +| Line | Content | +|------|---------| +| 1 | Upper lash tips — vertical pipes in bold magenta | +| 2 | Upper lash tier-2 — angled shafts converging to lid | +| 3 | Upper lash tier-3 — roots at lid top in dim cyan | +| 4 | Upper lid — `╔╩╩...╩╩╩╗` with lash-root connectors | +| 5 | Sclera top — dim white dots | +| 6 | Iris outer ring top arc — `/ ─────────── \` | +| 7 | Iris center — sparkle + iris glyph + sparkle | +| 8 | Iris outer ring bottom arc — `\ ─────────── /` | +| 9 | Sclera bottom — dim white dots | +| 10 | Lower lid — `╚╦╦...╦╦╦╝` with lash-root connectors | +| 11 | Lower lash tier-1 — roots at lid bottom | +| 12 | Lower lash tier-2 — fanning shafts | +| 13 | Lower lash tips — vertical pipes in bold magenta | + +**Iris ring flicker:** +When `f % 4 == 0`, the iris ring color is set to `${BOLD}${GLOW}` (brighter). On all other frames it is `${GLOW}` (normal). This creates a subtle 4-frame luminosity pulse on top of the standard 16-frame blink cycle. + +--- + +### `_eye_half()` + +**Type:** Bash function +**Visibility:** Private (called from `_draw_frame`) + +**Purpose:** +Renders the transitional half-closed eye frame — 13 lines showing the upper lid descending over the iris, lashes drooping and compressing, with only the lower portion of the iris and pupil remaining visible. + +**Signature:** +```bash +_eye_half "$iris_glyph" +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$1` (`ir`) | String | Iris center glyph (still partially visible) | + +**Returns:** Writes exactly 13 lines to stdout. + +**Key visual differences from `_eye_open`:** + +| Element | Open | Half | +|---------|------|------| +| Upper lashes | Upright, tiered | Swept sideways, drooping (`-\--\--`) | +| Upper lid | Open border (`╔╩╩...╗`) | Filled block (`╔▄▄...╗`) with shadow gradient | +| Sclera top | Dots visible | Replaced by second lid row (`▄▄▄`) | +| Iris ring top | Full arc visible | Barely peeking (`- - /────\- -`) in dim | +| Iris center | Full visibility | Half-obscured, bottom half only | + +--- + +### `_eye_closed()` + +**Type:** Bash function +**Visibility:** Private (called from `_draw_frame`) + +**Purpose:** +Renders the fully sealed eye — 13 lines showing both lids pressed together, lashes flat against the lid surfaces, and a neon seam line where the lids meet. A faint iris afterglow line appears above the upper lid as a residual light effect. + +**Signature:** +```bash +_eye_closed +``` + +**Parameters:** None. + +**Returns:** Writes exactly 13 lines to stdout. + +**Key visual elements:** + +| Element | Description | +|---------|-------------| +| Afterglow line | Dim glow dots above upper lid (` . . . .`) | +| Upper lashes | Flat, pressed outward (`--\--\--\`) | +| Upper lid | `╔═══...═══╗` sealed top edge | +| Lid body | `▄▄▄▄▄▄▄▄▄` deep blue-black fill | +| Seam line | `━━━━━━━━━━` in bold magenta | +| Seam texture | `-.-.-.-.-.-` dot-dash pattern | +| Lower lid body | `▀▀▀▀▀▀▀▀▀` fill | +| Lower lid | `╚═══...═══╝` sealed bottom edge | +| Lower lashes | Flat, pressed outward (`/--/--/`) | + +--- + +## 5. Project Scanning + +--- + +### `get_project_tree()` + +**Type:** Bash function +**Visibility:** Internal (called from `main()`) + +**Purpose:** +Generates a human-readable, indented representation of the project's directory structure. The output is passed directly into the OpenAI payload as structural context for workflow generation. + +**Signature:** +```bash +structure=$(get_project_tree) +``` + +**Parameters:** None (operates on current working directory `.`). + +**Returns:** → Multi-line indented tree string (stdout). + +**Full pipeline:** +```bash +find . -maxdepth 3 -not -path '*/.*' \ + | grep -vE "(node_modules|.git|build|dist|bin|obj|target|venv)" \ + | sed -e 's/[^-][^\/]*\// |/g' -e "s/|/ /g" +``` + +**Pipeline breakdown:** + +| Stage | Command | Purpose | +|-------|---------|---------| +| 1 | `find . -maxdepth 3` | Enumerate all files and directories to depth 3 | +| 2 | `-not -path '*/.*'` | Exclude hidden entries at find level | +| 3 | `grep -vE "(node_modules\|.git\|build\|dist\|bin\|obj\|target\|venv)"` | Exclude generated/build directories | +| 4 | `sed -e 's/[^-][^\/]*\// \|/g'` | Convert path separators to indentation pipes | +| 5 | `sed -e "s/\|/ /g"` | Replace pipes with two-space indentation | + +**Why `maxdepth 3`?** +Deeper scans increase the GPT-4o prompt token count without meaningful accuracy gains. Language detection (`get_context()`) uses independent glob patterns, so the tree serves as structural narrative only — not as the primary detection mechanism. + +**Side Effects:** None (read-only filesystem traversal). + +--- + +## 6. Language Detection + +--- + +### `get_context()` + +**Type:** Bash function +**Visibility:** Internal (called from `main()`) + +**Purpose:** +Performs concurrent, non-exclusive multi-language fingerprinting of the current directory. Checks for both manifest files (`package.json`, `go.mod`, etc.) and source file globs (`*.py`, `*.go`, etc.), accumulating every match into a `tags[]` array. Returns a space-separated string of all detected language contexts. + +**Signature:** +```bash +context=$(get_context) +``` + +**Parameters:** None. + +**Returns:** → Space-separated context string (stdout). Examples: +- `"Node.js"` — only `package.json` found +- `"Node.js Python Shell"` — three languages detected +- `"General/Single-File"` — nothing matched + +**Detection Matrix:** + +| Tag | Condition A (manifest) | Condition B (glob) | Operator | +|-----|------------------------|-------------------|----------| +| `Node.js` | `package.json` | — | A | +| `Python` | `requirements.txt` OR `pyproject.toml` | `*.py` | A OR B | +| `Go` | `go.mod` | `*.go` | A OR B | +| `Rust` | `Cargo.toml` | `*.rs` | A OR B | +| `Java` | `pom.xml` OR `build.gradle` | `*.java` | A OR B | +| `C/C++` | — | `*.cpp` OR `*.c` OR `*.h` | B | +| `Ruby` | `Gemfile` | `*.rb` | A OR B | +| `Shell` | — | `*.sh` | B | +| `Windows-Script` | — | `*.bat` OR `*.ps1` | B | + +**Fallback logic:** +```bash +if [ ${#tags[@]} -eq 0 ]; then + echo "General/Single-File" +else + echo "${tags[*]}" +fi +``` + +**Side Effects:** +⚠️ Glob checks (`ls *.py &>/dev/null`) may produce false negatives in directories with unusual permission settings. In standard project environments this is not a concern. + +**Glob suppression:** +All glob checks use `&>/dev/null` to suppress "no matches found" errors — these are intentional non-detections, not errors. + +--- + +## 7. Payload Construction + +--- + +### `create_payload()` + +**Type:** Bash function (delegates to embedded Python3) +**Visibility:** Internal (called from `main()`) + +**Purpose:** +Constructs the complete JSON request body for the OpenAI API call. Uses an embedded Python3 heredoc invocation to ensure correct JSON serialisation — avoiding manual quoting/escaping issues that would make Bash-native JSON construction fragile. Injects a rich system prompt containing all behavioural constraints for the AI. + +**Signature:** +```bash +payload=$(create_payload "$structure" "$context") +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$1` | String | Project tree output from `get_project_tree()` | +| `$2` | String | Context string from `get_context()` | + +**Returns:** → JSON string (stdout), ready for direct use as curl `-d` body. + +**Python3 invocation:** +```bash +python3 -c '...' "$tree" "$context" "$MODEL_NAME" +``` +Arguments are passed as `sys.argv[1]`, `sys.argv[2]`, `sys.argv[3]` to avoid shell injection in the JSON string values. + +**JSON structure produced:** +```json +{ + "model": "gpt-4o", + "messages": [ + { "role": "system", "content": "" }, + { "role": "user", "content": "Detected context: .\n\nProject tree:\n" } + ], + "temperature": 0.1 +} +``` + +**System Prompt Directives (summary):** + +| Directive | Purpose | +|-----------|---------| +| `IDENTIFICATION` | Detect actual project types; build hybrid workflows for polyglot repos | +| `EXCLUSION` | Completely ignore `ax.sh` | +| `MINIMALISM` | Shell-only projects → two-step workflow (checkout + shellcheck) | +| `SINGLEQUOTE` | Token substitution for shell-safe `find` command embedding | +| `NO GHOST DEPENDENCIES` | Never invent setup steps without evidentiary source files | +| `SINGLE FILE` | One or two files → minimalist CI | +| `CLEAN OUTPUT` | Return raw YAML only — no markdown, no commentary | +| `STANDARDS` | Always use `ubuntu-latest` and `actions/checkout@v4` | + +**Temperature = 0.1:** +Deliberately low to maximise determinism. YAML structure and CI conventions do not benefit from creative variation — consistency and correctness are the primary objectives. + +**Side Effects:** None. + +--- + +## 8. API Communication + +--- + +### `request_ai()` + +**Type:** Bash function (uses `curl`) +**Visibility:** Internal (called from `main()`) + +**Purpose:** +Sends the constructed JSON payload to the OpenAI Chat Completions API endpoint. Captures both the response body and HTTP status code in a single curl invocation, then classifies and surfaces any network-level or HTTP-level errors with descriptive prefixed messages. + +**Signature:** +```bash +raw_response=$(request_ai "$payload") +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$1` | String | JSON payload from `create_payload()` | + +**Returns:** +- **On success (HTTP 200):** → Raw response body JSON (stdout) +- **On any error:** → Error string with prefix (stdout), return code 1 + +**Pre-flight check:** +```bash +if [ -z "$OPENAI_API_KEY" ]; then + echo "AUTH_ERROR: OPENAI_API_KEY is not set." + return 1 +fi +``` + +**curl flags:** + +| Flag | Value | Purpose | +|------|-------|---------| +| `-s` | — | Silent mode (no progress bar) | +| `-w` | `"\n__STATUS__%{http_code}"` | Append HTTP status to body | +| `-X POST` | — | HTTP method | +| `Content-Type` | `application/json` | Request header | +| `Authorization` | `Bearer $OPENAI_API_KEY` | Auth header | +| `--max-time` | `60` | Total request timeout (seconds) | +| `--connect-timeout` | `10` | TCP connection timeout (seconds) | +| `-d` | `$json_payload` | Request body | + +**Status parsing:** +```bash +http_code=$(printf '%s' "$raw_out" | grep -o '__STATUS__[0-9]*' | grep -o '[0-9]*') +body=$(printf '%s' "$raw_out" | sed 's/__STATUS__[0-9]*$//') +``` + +**Network error classification (curl exit codes):** + +| Exit Code | Error Prefix | Meaning | +|-----------|-------------|---------| +| `6` | `NETWORK_ERROR` | Could not resolve host | +| `7` | `NETWORK_ERROR` | Connection refused | +| `28` | `NETWORK_ERROR` | Timed out after 60s | +| `35` | `NETWORK_ERROR` | SSL handshake failed | +| `*` | `NETWORK_ERROR` | Generic curl failure | + +**HTTP error classification:** + +| HTTP Status | Error Prefix | Meaning | +|-------------|-------------|---------| +| `200` | *(success — return body)* | OK | +| `400` | `REQUEST_ERROR` | Bad request | +| `401` | `AUTH_ERROR` | Invalid API key | +| `403` | `AUTH_ERROR` | Forbidden | +| `429` | `RATE_ERROR` | Rate limit exceeded | +| `500` | `SERVER_ERROR` | OpenAI internal error | +| `502` | `SERVER_ERROR` | Bad gateway | +| `503` | `SERVER_ERROR` | Service unavailable | +| `*` | `HTTP_ERROR` | Unexpected status | + +**Side Effects:** +⚠️ Makes an outbound HTTPS request to `https://api.openai.com/v1/chat/completions` +⚠️ Consumes OpenAI API credits + +--- + +## 9. Response Parsing + +--- + +### `parse_response()` + +**Type:** Bash function (delegates to embedded Python3) +**Visibility:** Internal (called from `main()`) + +**Purpose:** +Extracts the YAML string from the raw OpenAI API response JSON. Handles the common case where the model wraps its output in markdown fences (` ```yaml ... ``` `) despite instructions to return raw YAML. Routes all error conditions as prefixed string outputs rather than non-zero exit codes. + +**Signature:** +```bash +final_yaml=$(parse_response "$raw_response") +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$1` | String | Raw JSON response body from `request_ai()` | + +**Returns:** +- **On success:** → Clean YAML string, stripped of fences and surrounding whitespace (stdout) +- **On API error:** → `API_ERROR: {json}` string +- **On parse failure:** → `PARSE_ERROR: {description}` string + +**Python3 logic:** +```python +data = json.load(sys.stdin) +if 'choices' in data: + raw = data['choices'][0]['message']['content'] + # Strip opening fence: optional language tag, any whitespace + clean = re.sub(r'^\s*```[a-zA-Z]*\s*', '', raw) + # Strip closing fence: trailing whitespace + backticks + clean = re.sub(r'\s*```\s*$', '', clean) + print(clean.strip()) +else: + print('API_ERROR: ' + json.dumps(data)) +``` + +**Exception routing:** + +| Exception | Output Prefix | Scenario | +|-----------|-------------|---------| +| `json.JSONDecodeError` | `PARSE_ERROR: invalid JSON —` | Response was not valid JSON | +| `KeyError` | `PARSE_ERROR: missing field —` | `choices` or `message` absent | +| `IndexError` | `PARSE_ERROR: choices array is empty` | `choices` exists but empty | +| `Exception` | `PARSE_ERROR:` | Any other unexpected error | + +**Fence stripping regex detail:** + +| Pattern | Targets | +|---------|---------| +| `r'^\s*```[a-zA-Z]*\s*'` | Opening fence: ` ```yaml `, ` ```yml `, ` ``` `, all with optional whitespace | +| `r'\s*```\s*$'` | Closing fence: trailing ` ``` ` with optional surrounding whitespace | + +**Side Effects:** None. + +--- + +## 10. TUI Interaction + +--- + +### `prompt_git_sync()` + +**Type:** Bash function +**Visibility:** Internal (called from `main()` after successful write) + +**Purpose:** +Renders an 8-line interactive confirmation panel asking the user whether to run `git-sync` after the workflow file has been written. Implements a complete keyboard event loop supporting arrow keys, direct character input, and Enter confirmation. + +**Signature:** +```bash +prompt_git_sync +``` + +**Parameters:** None. + +**Returns:** Nothing (void). May call `git-sync` or exit with code 1 if git-sync is missing. + +**Internal State:** +- `selected` — integer: `0` = YES, `1` = NO (initial: `0`) +- `PROMPT_LINES` — constant `8` (panel height) + +**Keyboard Event Loop:** + +The loop uses `IFS= read -rsn1 key` (read one raw byte, no echo, no special processing) as the innermost read: + +``` +read 1 byte +├── Is ESC (\033)? +│ ├── read 1 more byte (50ms timeout) +│ │ ├── Is '['? +│ │ │ └── read 1 more byte (direction) +│ │ │ ├── A or D → selected=0 (YES), redraw, continue +│ │ │ └── B or C → selected=1 (NO), redraw, continue +│ │ └── Not '[' → treat as lone ESC → selected=1, break +│ └── Timeout → selected=1, break +├── 'y' or 'Y' → selected=0, break +├── 'n' or 'N' → selected=1, break +└── Enter (\n, \r, '') → break (confirm current selection) +``` + +**Arrow key sequences:** + +| Key | Bytes | Action | +|-----|-------|--------| +| `↑` Up arrow | `1B 5B 41` (ESC [ A) | YES | +| `←` Left arrow | `1B 5B 44` (ESC [ D) | YES | +| `↓` Down arrow | `1B 5B 42` (ESC [ B) | NO | +| `→` Right arrow | `1B 5B 43` (ESC [ C) | NO | + +**Post-selection dispatch:** + +``` +selected == 0 (YES): + Print "[>] Confirmed — checking for git-sync..." + command -v git-sync + ├── Found: print "[+] Launching git-sync", call git-sync + └── Not found: print error + install URL, exit 1 + +selected == 1 (NO): + Print "[-] Skipped — exiting gracefully." + (return normally) +``` + +**Side Effects:** +⚠️ Hides and restores terminal cursor +⚠️ Modifies terminal raw input mode during read +⚠️ May invoke `git-sync` binary +💀 Exits with code 1 if YES selected but `git-sync` not on PATH + +--- + +### `_draw_prompt()` + +**Type:** Bash function (nested inside `prompt_git_sync`) +**Visibility:** Private + +**Purpose:** +Renders the 8-line YES/NO selection panel using box-drawing characters. Applies `tput rev` (reverse video) highlighting to the currently selected option. Called on every navigation event to provide immediate visual feedback. + +**Signature:** +```bash +_draw_prompt "$selected" +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$1` | Integer | `0` = YES highlighted, `1` = NO highlighted | + +**Returns:** Writes exactly 8 lines to stdout. + +**Panel layout (44-character inner width):** +``` +╔════════════════════════════════════════════╗ ← Line 1: top border +║ [!] EXECUTE GIT-SYNC? ║ ← Line 2: title +║ Would you like to run git-sync now? ║ ← Line 3: subtitle +╠════════════════╦═══════════════════════════╣ ← Line 4: divider (16+1+27) +║ > YES < ║ NO ║ ← Line 5: options (selected highlighted) +╠════════════════╩═══════════════════════════╣ ← Line 6: divider close +║ ↑← YES ↓→ NO ↵ confirm y/n quick ║ ← Line 7: key hints +╚════════════════════════════════════════════╝ ← Line 8: bottom border +``` + +**Column geometry:** +- Box inner width: 44 characters +- Left cell (YES): 16 characters +- Divider: 1 character (`╦` / `╩`) +- Right cell (NO): 27 characters + +**Highlighting styles:** + +| Option | Selected | Unselected | +|--------|----------|-----------| +| YES | `${BOLD}${REV}${CYAN}` (reverse video cyan) | `${DIM}${WHT}` | +| NO | `${BOLD}${REV}${MAG}` (reverse video magenta) | `${DIM}${WHT}` | + +**Side Effects:** +⚠️ Uses `tput rc` to restore cursor to saved position before rendering (caller must have saved with `tput sc`). + +--- + +## 11. Main Orchestration + +--- + +### `main()` + +**Type:** Bash function +**Visibility:** Entry point (called at script bottom) + +**Purpose:** +The top-level orchestrator of the entire AX execution pipeline. Owns the STATE machine transitions, calls each phase function in sequence, performs post-call error validation, and manages the loader lifecycle across all phases. + +**Signature:** +```bash +main +``` + +**Parameters:** None. + +**Returns:** Exits with code 0 on success, code 1 on any fatal error. + +**Complete execution sequence:** + +``` +1. set_state "SCANNING" +2. start_loader "Scanning project structure..." +3. set_loader_progress 10 +4. structure=$(get_project_tree) — abort if fails +5. context=$(get_context) +6. set_loader_progress 30 "Building AI payload [ $context ]..." +7. set_state "BUILDING" +8. payload=$(create_payload "$structure" "$context") — abort if empty +9. set_loader_progress 50 "Consulting OpenAI [ $MODEL_NAME ]..." +10. set_state "REQUESTING" +11. raw_response=$(request_ai "$payload") — abort on *_ERROR: prefix +12. set_loader_progress 80 "Parsing response..." +13. set_state "PARSING" +14. final_yaml=$(parse_response "$raw_response") — abort on error prefix or empty +15. set_loader_progress 95 "Writing workflow file..." +16. set_state "WRITING" +17. mkdir -p .github/workflows/ — abort if fails +18. echo "$final_yaml" > .github/workflows/main.yml — abort if fails +19. set_loader_progress 100 "Complete!" +20. set_state "DONE" +21. sleep 0.4 +22. stop_loader +23. printf success message +24. prompt_git_sync +``` + +**Error handling pattern:** +Every potentially-failing call is followed by one of: +- `|| { stop_loader; printf error; exit 1; }` — for commands with non-zero exits +- `[[ "$var" == *_ERROR:* ]]` — for functions that return error strings on stdout +- `[ -z "$var" ]` — for empty output validation + +**Side Effects:** +⚠️ Spawns and manages background animation process +📁 Creates `.github/workflows/main.yml` +⚠️ Makes outbound HTTPS call to OpenAI +⚠️ Optionally invokes `git-sync` + +**Global variables used:** +- `STATE` — updated throughout +- `LOADER_PID`, `_STOP_FILE`, `_PROG_FILE`, `_LABEL_FILE` — managed via loader functions +- `MODEL_NAME`, `API_URL` — configuration constants at script top + +--- + +## Global Variables Reference + +| Variable | Type | Initial Value | Set By | Read By | +|----------|------|--------------|--------|---------| +| `STATE` | String | `"IDLE"` | `set_state()` | `get_state()` | +| `LOADER_PID` | String (PID) | `""` | `start_loader()` | `stop_loader()` | +| `_STOP_FILE` | String (path) | `""` | `start_loader()` | `stop_loader()`, `_loader_loop()` | +| `_PROG_FILE` | String (path) | `""` | `start_loader()` | `set_loader_progress()`, `_loader_loop()` | +| `_LABEL_FILE` | String (path) | `""` | `start_loader()` | `set_loader_progress()`, `_loader_loop()` | +| `_LOADER_LINES` | Integer | `18` | Global init | `start_loader()` | +| `API_URL` | String | OpenAI endpoint | Global config | `request_ai()` | +| `MODEL_NAME` | String | `"gpt-4o"` | Global config | `create_payload()`, loader label | + +--- + +## ANSI Color Variable Reference + +| Variable | Escape Code | Color/Effect | +|----------|-------------|--------------| +| `R` | `\033[0m` | Reset all attributes | +| `CYAN` | `\033[38;5;51m` | Bright cyan | +| `MAG` | `\033[38;5;201m` | Bright magenta/neon pink | +| `YEL` | `\033[38;5;226m` | Bright yellow | +| `GRN` | `\033[38;5;118m` | Bright green | +| `RED` | `\033[38;5;196m` | Bright red | +| `BLU` | `\033[38;5;39m` | Bright blue | +| `WHT` | `\033[38;5;255m` | Near-white | +| `GLOW` | `\033[38;5;87m` | Pale cyan glow | +| `GOLD` | `\033[38;5;220m` | Gold/amber | +| `DIM` | `\033[2m` | Dim/faint modifier | +| `BOLD` | `\033[1m` | Bold/bright modifier | +| `CLR` | `\033[2K` | Erase entire current line | +| `REV` | `\033[7m` | Reverse video (invert fg/bg) | + +--- + +*Last updated: AX v1.0.0 — Nikan Eidi* diff --git a/public/Structure.md b/public/Structure.md new file mode 100644 index 0000000..f3a054e --- /dev/null +++ b/public/Structure.md @@ -0,0 +1,745 @@ +# ⚡ AX — Structure + +> **Document Type:** Deep-Dive Technical Architecture Reference +> **Project:** AX — The Cybernetic Workflow Architect +> **Developer:** Nikan Eidi +> **Version:** 1.0.0 +> **Scope:** Complete structural documentation covering the Tree-Logic Scanner, the Hybrid Detection Engine internals, the State Machine architecture, the Animation subsystem IPC design, the payload construction pipeline, and all configuration constants. + +--- + +## Table of Contents + +1. [Script-Level Architecture](#1-script-level-architecture) +2. [Configuration Constants](#2-configuration-constants) +3. [State Machine Design](#3-state-machine-design) +4. [Tree-Logic Scanner — Deep Dive](#4-tree-logic-scanner--deep-dive) +5. [Hybrid Detection Engine — Deep Dive](#5-hybrid-detection-engine--deep-dive) +6. [Payload Construction Pipeline](#6-payload-construction-pipeline) +7. [Animation Subsystem — IPC Architecture](#7-animation-subsystem--ipc-architecture) +8. [ANSI Rendering Architecture](#8-ansi-rendering-architecture) +9. [Signal & Trap Architecture](#9-signal--trap-architecture) +10. [TUI Input Architecture](#10-tui-input-architecture) +11. [Error String Protocol](#11-error-string-protocol) +12. [File & Directory Outputs](#12-file--directory-outputs) +13. [Dependency Map](#13-dependency-map) + +--- + +## 1. Script-Level Architecture + +`ax.sh` is a **single-file self-contained Bash script**. It has no external Bash dependencies beyond the standard utilities available on any modern Linux system. It does embed Python3 logic at two points — but calls Python as a subprocess rather than requiring a separate file. + +### Top-Level Source Sections + +``` +ax.sh +├── ── Configuration ──────────────────────────── API_URL, MODEL_NAME +├── ── ANSI Color Palette ─────────────────────── 14 color/style variables +├── ── State Management ───────────────────────── STATE, set_state(), get_state() +├── ── Signal / Event Handlers ────────────────── cleanup(), on_error(), trap registrations +├── ── Loader — Progress Bar Builder ──────────── _build_bar() +├── ── Loader — Eye Frame: OPEN ───────────────── _eye_open() +├── ── Loader — Eye Frame: HALF-CLOSED ────────── _eye_half() +├── ── Loader — Eye Frame: FULLY CLOSED ───────── _eye_closed() +├── ── Loader — Master Frame Renderer ─────────── _draw_frame() +├── ── Loader — Background Animation Loop ─────── _loader_loop() +├── ── Loader — Public Interface ──────────────── start_loader(), set_loader_progress(), stop_loader() +├── ── Project Scanning ───────────────────────── get_project_tree() +├── ── Language Detection ─────────────────────── get_context() +├── ── Payload Construction ───────────────────── create_payload() [embeds Python3] +├── ── API Communication ──────────────────────── request_ai() +├── ── Response Parsing ───────────────────────── parse_response() [embeds Python3] +├── ── Interactive git-sync prompt ────────────── prompt_git_sync() [contains _draw_prompt()] +└── ── Main Logic ─────────────────────────────── main() + main ← entry call +``` + +### Execution Entry Point + +The very last line of `ax.sh` is: + +```bash +main +``` + +There is no argument parsing, no `--help` flag, and no mode switching in v1.0.0. AX is invoked from the project root with no arguments. + +--- + +## 2. Configuration Constants + +All tunable configuration is declared at the very top of `ax.sh`, making it trivial to adapt AX for different API providers or models. + +```bash +API_URL="https://api.openai.com/v1/chat/completions" +MODEL_NAME="gpt-4o" +``` + +| Constant | Type | Default | Effect of Changing | +|----------|------|---------|-------------------| +| `API_URL` | String | OpenAI v1 completions | Change to point at any OpenAI-compatible API (Azure OpenAI, local llama.cpp server, etc.) | +| `MODEL_NAME` | String | `gpt-4o` | Change to `gpt-4-turbo`, `gpt-3.5-turbo`, or any model your API key has access to | + +**`_LOADER_LINES`** is a second configuration constant defined in the State Management section: + +```bash +_LOADER_LINES=18 +``` + +This integer must always equal the exact number of terminal lines the animation block occupies. Changing any eye sub-renderer to emit a different line count **requires updating this value** — otherwise the cursor rewind will misalign the animation. + +--- + +## 3. State Machine Design + +### States + +AX tracks a single global `STATE` string variable that progresses through a linear sequence. Error conditions branch to `ERROR` from any phase. + +``` +IDLE → SCANNING → BUILDING → REQUESTING → PARSING → WRITING → DONE + ↓ + ERROR ← ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ +``` + +### Transition Table + +| From State | Event | To State | Triggered By | +|------------|-------|----------|-------------| +| `IDLE` | `main()` called | `SCANNING` | `set_state "SCANNING"` | +| `SCANNING` | Tree scan complete | `BUILDING` | `set_state "BUILDING"` | +| `BUILDING` | Payload built | `REQUESTING` | `set_state "REQUESTING"` | +| `REQUESTING` | Response received | `PARSING` | `set_state "PARSING"` | +| `PARSING` | YAML extracted | `WRITING` | `set_state "WRITING"` | +| `WRITING` | File written | `DONE` | `set_state "DONE"` | +| Any | Fatal error | `ERROR` | `set_state "ERROR"` then `exit 1` | +| Any | SIGINT/TERM/HUP | *(cleanup)* | `cleanup()` trap | + +### Design Rationale + +The STATE variable is currently used for tracking and potential future conditional logic. In v1.0.0 it is not branched on by any function — it serves as an audit trail readable by process monitors or future additions to AX. + +--- + +## 4. Tree-Logic Scanner — Deep Dive + +### Full Pipeline + +```bash +get_project_tree() { + local ignore="(node_modules|.git|build|dist|bin|obj|target|venv)" + find . -maxdepth 3 -not -path '*/.*' \ + | grep -vE "$ignore" \ + | sed -e 's/[^-][^\/]*\// |/g' -e "s/|/ /g" +} +``` + +### Stage 1: `find . -maxdepth 3` + +**What it does:** Recursively enumerates all filesystem entries (files and directories) under the current working directory, stopping at depth 3. + +**Why depth 3?** + +| Depth | Typical content at that level | +|-------|------------------------------| +| 1 | `./src`, `./tests`, `./docs`, `./package.json` | +| 2 | `./src/utils`, `./src/components`, `./tests/unit` | +| 3 | `./src/utils/helpers`, `./src/components/Button` | +| 4+ | Individual implementation files, deeply nested modules | + +Depths 1–3 capture the structural intent of the project — what packages, modules, and directories exist — without enumerating every implementation file. This keeps the GPT-4o prompt token count manageable and focused on architecture rather than exhaustive inventory. + +**Output format at this stage:** +``` +. +./package.json +./src +./src/index.js +./src/utils +./src/utils/helpers.js +./node_modules ← still present at this point +./.git ← still present at this point +``` + +--- + +### Stage 2: `-not -path '*/.*'` + +**What it does:** Instructs `find` itself to skip any path component that begins with `.`. This is evaluated at the `find` level — the filesystem traversal never enters hidden directories. + +**Affected directories:** +- `.git/` — version control internals +- `.github/` — will exclude the generated workflow directory from future scans (by design — AX should not analyze its own output) +- `.env` — environment files +- `.DS_Store` — macOS metadata +- Any dotfile or hidden directory + +**Why at `find` level and not grep?** +Excluding at `find` level is more efficient than post-filtering with `grep` because `find` never descends into the excluded directories. For a large repo with a deep `.git` history, this avoids enumerating potentially thousands of object files. + +--- + +### Stage 3: `grep -vE "$ignore"` + +The ignore pattern is: +``` +(node_modules|.git|build|dist|bin|obj|target|venv) +``` + +**What it does:** A second pass that removes lines containing any of these directory names. This catches directories that may have slipped through stage 2 (e.g., `./build` which is not a hidden directory) and any that match the pattern anywhere in their path. + +**Why include `.git` here when stage 2 already excluded it?** +Belt-and-suspenders design. If `find` ever encounters a `.git` that wasn't hidden (edge case: bare repos configured differently), the grep pass catches it. + +**Directory-to-exclusion rationale:** + +| Directory | Reason for Exclusion | +|-----------|---------------------| +| `node_modules` | Third-party packages — not project code, massive file count | +| `.git` | Version control — not project structure | +| `build` / `dist` | Generated output — not source structure | +| `bin` | Compiled binaries — not source | +| `obj` | C/C++ object files — not source | +| `target` | Rust/Maven build output — not source | +| `venv` | Python virtual environment — not project code | + +--- + +### Stage 4: `sed -e 's/[^-][^\/]*\// |/g' -e "s/|/ /g"` + +**What it does:** Transforms raw `find` path strings into a visually indented tree representation. + +**First `sed` expression:** `'s/[^-][^\/]*\// |/g'` + +This regex matches any sequence of characters that: +- Does not start with `-` (to avoid matching `./` as a "path segment start") +- Followed by any characters that are not `/` +- Followed by `/` + +Each such match is replaced with ` |` — a space and a pipe character. + +**Second `sed` expression:** `"s/|/ /g"` + +Replaces every `|` with two spaces, creating the indentation effect. + +**Example transformation:** + +``` +Input: ./src/utils/helpers.js +After 1: . |utils |helpers.js +After 2: utils helpers.js +``` + +This is a simple approximation of a tree view — not as precise as `tree(1)` but sufficient for the structural narrative passed to GPT-4o. + +**Why not use `tree(1)` directly?** +`tree` is not guaranteed to be available on all systems. The `find` + `sed` combination uses only POSIX-standard utilities that are present on every Unix system where Bash 4+ runs. AX has zero optional dependencies in its scanner. + +--- + +### What the Scanner Does NOT Do + +| Limitation | Impact | Mitigation | +|------------|--------|-----------| +| Does not read file contents | Cannot detect languages from `#!` shebangs | `get_context()` uses glob patterns independently | +| Does not follow symlinks | May miss symlinked source trees | Acceptable for standard project structures | +| Truncates at depth 3 | May miss deeply nested language signals | `get_context()` scans at root level with globs | +| Does not count files | Cannot distinguish "5 Python files" from "1" | Context string treated as categorical, not quantitative | + +--- + +## 5. Hybrid Detection Engine — Deep Dive + +### Architecture + +`get_context()` uses a **sequential non-exclusive test array accumulation** pattern. Every test is independent — a match does not skip subsequent tests. This is what enables hybrid detection. + +```bash +get_context() { + local tags=() + + [ -f "package.json" ] && tags+=("Node.js") + ([ -f "requirements.txt" ] || ...) && tags+=("Python") + ... + + if [ ${#tags[@]} -eq 0 ]; then + echo "General/Single-File" + else + echo "${tags[*]}" + fi +} +``` + +### Detection Mechanism: Manifest-First, Glob-Second + +Each language uses a two-tier detection strategy: + +**Tier 1 — Manifest file check:** +```bash +[ -f "package.json" ] && tags+=("Node.js") +``` +Manifest files are definitive — if `package.json` exists, this is a Node.js project. No ambiguity. + +**Tier 2 — Source glob check:** +```bash +(ls *.py &>/dev/null) && tags+=("Python") +``` +Used when no manifest is present but source files exist (e.g., a single-file Python script with no `requirements.txt`). The `ls *.py &>/dev/null` pattern: +- Uses `ls` for glob expansion (not `find`, avoiding recursion) +- Redirects both stdout and stderr to `/dev/null` — the return code is what matters, not the output +- Returns exit code 0 if any `*.py` file exists in the current directory, 1 otherwise + +**Combined logic for Python:** +```bash +([ -f "requirements.txt" ] || [ -f "pyproject.toml" ] || ls *.py &>/dev/null) && tags+=("Python") +``` +The subshell `( ... )` groups the OR conditions so that the `&&` applies to the entire group as a unit. Python is detected if ANY of the three signals are present. + +### Glob Scope + +All glob checks (`ls *.ext`) scan only the **current working directory** — they are not recursive. This is intentional: the presence of any source file at root level is sufficient signal for the language to be considered "in scope" for CI. + +For deeply nested single-language projects (e.g., a Go project where all `.go` files are under `./cmd/server/`), the `go.mod` manifest at root provides the detection signal. + +### Why `ls` and not `find`? + +```bash +# Used in ax.sh +ls *.py &>/dev/null + +# Not used +find . -name "*.py" -maxdepth 1 &>/dev/null +``` + +`ls *.go` is faster for the shallow check — it delegates glob expansion to the shell itself. `find` with `-maxdepth 1` would work but is more verbose. In the context of `get_context()` running dozens of checks, the simpler `ls` form is preferred. + +### The `tags[]` Array + +```bash +local tags=() +``` + +`tags` is a local indexed Bash array. Each matching language appends its label string: +```bash +tags+=("Node.js") # tags=("Node.js") +tags+=("Python") # tags=("Node.js" "Python") +tags+=("Shell") # tags=("Node.js" "Python" "Shell") +``` + +The final echo uses `${tags[*]}` — the `*` expansion joins all elements with the first character of `IFS` (a space by default): +```bash +echo "${tags[*]}" +# Output: Node.js Python Shell +``` + +This space-separated string is passed verbatim into the AI user message: +``` +Detected context: Node.js Python Shell. +``` + +The AI is trained to interpret this as "generate a hybrid workflow for all three contexts." + +--- + +## 6. Payload Construction Pipeline + +### Why Python3, Not Pure Bash? + +Bash has no native JSON serialisation. Building JSON with string concatenation is: +1. Fragile — any quote, backslash, or newline in the project tree string would break the JSON +2. Unmaintainable — escaping rules compound with nesting depth +3. Unnecessary — Python3 is available wherever AX can be installed + +The `create_payload()` function passes data as **command-line arguments** to an inline Python3 script: + +```bash +python3 -c '...' "$tree" "$context" "$MODEL_NAME" +``` + +Inside Python, these are `sys.argv[1]`, `sys.argv[2]`, `sys.argv[3]`. The `json.dumps()` call handles all necessary escaping automatically. + +### System Prompt Architecture + +The system prompt is a single string assembled from multiple directive sentences. Each directive targets a specific failure mode observed in AI-generated CI workflows: + +| Directive Name | Problem it Prevents | Implementation | +|----------------|--------------------|-| +| `IDENTIFICATION` | Generic boilerplate workflow regardless of stack | Instructs AI to base output on actual detected types | +| `EXCLUSION: ax.sh` | AX's own script treated as a project dependency | Named exclusion of the utility file | +| `MINIMALISM` | Over-engineered workflows for simple shell scripts | Constrains shell-only output to exactly 2 steps | +| `SINGLEQUOTE` | Shell single-quotes breaking Python string embedding | Token substitution pattern for `find ... '*.sh'` | +| `NO GHOST DEPENDENCIES` | Setup steps for languages not in the project | Explicit prohibition | +| `SINGLE FILE` | Full multi-step workflows for trivial single files | Scale-down instruction | +| `CLEAN OUTPUT` | Markdown fences and prose commentary in output | Raw YAML instruction (belt + `parse_response()` suspenders) | +| `STANDARDS` | Non-standard runner OS or outdated action versions | Explicit version pinning instruction | + +### The SINGLEQUOTE Token Pattern + +The shell-only minimalism directive needs to embed a `find` command with single quotes into the JSON string: + +``` +find . -name '*.sh' -not -path '*/.*' | xargs shellcheck --severity=warning +``` + +Single quotes inside a Python string defined via `sys.argv` (which passes through shell quoting) create escaping conflicts. The solution is a **token substitution** pattern: + +```python +system_prompt = ( + "...find . -name SINGLEQUOTE*.shSINGLEQUOTE " + "-not -path SINGLEQUOTE*/.*SINGLEQUOTE | xargs shellcheck... " + "In the final YAML output replace every token SINGLEQUOTE with an actual single-quote character." +) +``` + +The AI is instructed to replace every occurrence of the literal string `SINGLEQUOTE` with `'` in its YAML output. This avoids shell quoting conflicts entirely while producing correct YAML. + +### Temperature Selection: 0.1 + +```json +"temperature": 0.1 +``` + +Temperature controls the randomness of the AI's token sampling: + +| Temperature | Effect | Use Case | +|-------------|--------|----------| +| `0.0` | Fully deterministic (greedy decoding) | Exact reproducibility required | +| `0.1` | Near-deterministic with minimal variation | **AX's choice** — YAML structure is correctness-sensitive | +| `0.7` | Moderate creativity | General-purpose chat | +| `1.0+` | High creativity/randomness | Creative writing, brainstorming | + +For CI workflow generation, creativity is an anti-feature. The same project scanned twice should produce functionally equivalent workflows. `0.1` provides the near-determinism needed while avoiding the occasional issues with `0.0` (some models become overly terse at exactly 0). + +--- + +## 7. Animation Subsystem — IPC Architecture + +### Process Model + +``` +Parent Process (main()) +│ +├── Writes to: $_PROG_FILE (progress %) +├── Writes to: $_LABEL_FILE (label text) +├── Signals via: touch $_STOP_FILE +│ +└── Background Subshell (_loader_loop &) + ├── Reads from: $_PROG_FILE every frame + ├── Reads from: $_LABEL_FILE every frame + ├── Polls: $_STOP_FILE every frame + └── Renders: 18-line frame to stdout +``` + +### IPC via Tmpfiles + +The three tmpfiles serve as a **unidirectional message channel** from parent to child: + +| File | Direction | Content | Update Frequency | +|------|-----------|---------|-----------------| +| `_PROG_FILE` | Parent → Child | Integer 0–100 | Per phase (6 times) | +| `_LABEL_FILE` | Parent → Child | Label string | Per phase (6 times) | +| `_STOP_FILE` | Parent → Child | Existence = stop signal | Once, at end | + +**Why filesystem IPC?** +- Bash subshells cannot share variables with their parent — any variable set in a subshell is invisible to the parent and vice versa +- Pipes would require the parent to have an open file descriptor to the child, which complicates cleanup and signal handling +- tmpfiles are simple, debuggable (you can `cat` them while AX is running), and survive race conditions gracefully (the child always reads the last-written value) + +### The Stop Signal: File Existence + +```bash +# Parent signals stop: +touch "$_STOP_FILE" + +# Child checks on every frame: +while [ ! -f "$stop_file" ]; do + ... +done +``` + +**Why existence-based rather than content-based?** +Writing a value to the file (e.g., `echo "1" > $_STOP_FILE`) requires the parent to write AND the child to read and compare. Using file existence as the signal requires only: +- Parent: one `touch` syscall +- Child: one `stat` syscall (via `[ -f ... ]`) + +This minimises the window for race conditions at shutdown. + +### The `rm -f` then Re-Check Pattern + +```bash +_STOP_FILE=$(mktemp /tmp/ci_stop.XXXXXX) +# ... +rm -f "$_STOP_FILE" # ← Delete it immediately after creation +``` + +`mktemp` creates the file as part of its atomic creation guarantee. But the loop checks for the file's **absence** to continue running. If the file existed when the loop started, it would immediately exit. + +The solution: create with `mktemp` (to get a unique name with guaranteed uniqueness across processes), then immediately delete it, leaving only the path string in `$_STOP_FILE`. + +### Cursor Management: `tput sc/rc` Architecture + +The animation block relies on being able to return the cursor to the exact same position at the start of each frame. The naive approach: + +```bash +# WRONG — breaks on terminal scroll +printf "\033[18A" # move cursor up 18 lines +``` + +This fails because `\033[18A` cannot move the cursor above the current **viewport top**. If the user's terminal scrolls (even one line) during a long API call, the "18 lines up" would no longer point to the animation block's start — it would point to wherever the current viewport top is. + +The correct approach: + +```bash +# CORRECT — scroll-safe +tput sc # save absolute cursor address in terminal's memory +# ... render 18 lines ... +tput rc # restore cursor to saved absolute address +``` + +`tput sc` (save cursor) stores the current cursor position in the terminal emulator's own state memory. `tput rc` (restore cursor) retrieves that stored position unconditionally — it is not relative, it is not affected by scroll, and it is not affected by how many lines have been printed since the save. + +### Frame Budget: Always Exactly 18 Lines + +Every call to `_draw_frame()` must output exactly `_LOADER_LINES` (18) lines. This invariant is what makes the `tput rc` + print-18-lines pattern work: if any frame outputs 17 or 19 lines, subsequent frames will drift. + +The 18 is composed as: + +``` +_eye_open() → 13 lines (always exactly 13, regardless of eye state) +_eye_half() → 13 lines (same) +_eye_closed() → 13 lines (same) +separator → 1 line (blank printf) +bar top → 1 line (┌────┐) +bar middle → 1 line (│ ██░ │) +bar bottom → 1 line (└────┘) +label → 1 line (⠋ Processing...) + ────────── +Total = 18 lines +``` + +--- + +## 8. ANSI Rendering Architecture + +### Color Variables + +All color codes use the **256-color escape format**: `\033[38;5;{n}m` for foreground. This provides more consistent colour rendering across terminal emulators than the 8/16-color alternatives. + +```bash +CYAN=$'\033[38;5;51m' # 256-color index 51 — bright aqua +MAG=$'\033[38;5;201m' # 256-color index 201 — neon pink/magenta +GLOW=$'\033[38;5;87m' # 256-color index 87 — pale cyan glow +GOLD=$'\033[38;5;220m' # 256-color index 220 — amber gold +``` + +### The `$CLR` Prefix Pattern + +Every line in the animation is prefixed with `$CLR` (`\033[2K` — erase entire line): + +```bash +printf '%s %s| || | || | | || | || |%s\n' "$CLR" "${BOLD}${MAG}" "$R" +``` + +**Why?** +When `tput rc` restores the cursor, it places the cursor at the saved position — but does not clear the lines below. Without `\033[2K`, if a previous frame was longer or had wider characters, artifacts from the previous frame would remain visible at the right edge of lines in the current frame. + +Prefixing every line with `\033[2K` ensures the entire line is cleared before the new content is printed, making each frame a clean render. + +### The `$R` Suffix Pattern + +Every styled segment ends with `$R` (`\033[0m` — reset all attributes): + +```bash +printf '%s%s%s' "${BOLD}${CYAN}" "content" "$R" +``` + +This prevents color bleeding — if a reset is missed, subsequent output would inherit the style of the last active attribute, potentially corrupting the terminal's display until the user runs `reset`. + +--- + +## 9. Signal & Trap Architecture + +### Registered Traps + +```bash +trap cleanup INT TERM HUP +trap 'on_error $LINENO' ERR +``` + +| Signal | Handler | Trigger | +|--------|---------|---------| +| `SIGINT` (2) | `cleanup()` | Ctrl+C | +| `SIGTERM` (15) | `cleanup()` | External kill / system shutdown | +| `SIGHUP` (1) | `cleanup()` | Terminal disconnection | +| `ERR` (pseudo) | `on_error $LINENO` | Any command returns non-zero | + +### Why `trap 'on_error $LINENO' ERR` and not `trap on_error ERR`? + +`$LINENO` must be expanded **at the trap registration point** or **at the point of trap invocation**. By quoting the entire string with single quotes, the expansion of `$LINENO` is deferred to when the trap fires — meaning it captures the line number of the failing command, not the line of the `trap` registration itself. + +### ERR Trap Scope + +The `ERR` trap fires on any command that returns a non-zero exit code **unless** the command is: +- Part of an `if` condition (e.g., `if some_command; then`) +- Part of a `while` or `until` condition +- On the right side of `&&` or `||` +- Preceded by `!` + +This means all the intentional non-zero exit code uses in AX (the `ls *.py &>/dev/null` checks, the `kill -0 "$LOADER_PID" 2>/dev/null` check) do not trigger `on_error` because they are structured in ways that exempt them from the ERR trap. + +--- + +## 10. TUI Input Architecture + +### Raw Mode Reads + +```bash +IFS= read -rsn1 key +``` + +| Flag | Effect | +|------|--------| +| `-r` | Raw mode — backslash is not treated as escape character | +| `-s` | Silent — input is not echoed to terminal | +| `-n1` | Read exactly 1 character | +| `IFS=` | Empty IFS — preserves whitespace in `$key` (including spaces and Enter) | + +### The 3-Byte Arrow Key Protocol + +Terminal emulators send arrow keys as **ANSI escape sequences** — a sequence of 3 bytes: + +``` +ESC [ A (Up arrow) +1B 5B 41 + +ESC [ B (Down arrow) +1B 5B 42 + +ESC [ C (Right arrow) +1B 5B 43 + +ESC [ D (Left arrow) +1B 5B 44 +``` + +AX reads these in three separate `read` calls: + +```bash +IFS= read -rsn1 key # Reads first byte: 0x1B (ESC) + +if [[ "$key" == $'\033' ]]; then + IFS= read -rsn1 -t 0.05 seq # Reads second byte: '[' (with 50ms timeout) + if [[ "$seq" == '[' ]]; then + IFS= read -rsn1 -t 0.05 key # Reads third byte: A/B/C/D +``` + +The 50ms timeout (`-t 0.05`) on the second and third reads is critical: it distinguishes a **genuine ESC key press** (which produces only one byte, `0x1B`, with no following bytes) from an **arrow key sequence** (which produces three bytes in rapid succession). If no second byte arrives within 50ms, the ESC is treated as a lone ESC key. + +### Why Map Both LEFT and UP to YES? + +```bash +A|D) # Up / Left arrow → YES +B|C) # Down / Right arrow → NO +``` + +The YES/NO options are displayed side by side (left=YES, right=NO). Mapping spatial navigation intuitively: left and up move toward YES, right and down move toward NO. This matches the mental model of "I'm on YES (left), I press right to go to NO." + +--- + +## 11. Error String Protocol + +AX uses a **prefixed string return protocol** for functions that cannot use non-zero exit codes to signal errors (because non-zero would trigger the ERR trap prematurely). Instead, error conditions are returned as strings on stdout with structured prefixes: + +### Prefix Registry + +| Prefix | Source Function | Meaning | +|--------|----------------|---------| +| `AUTH_ERROR:` | `request_ai()` | API key missing or rejected | +| `NETWORK_ERROR:` | `request_ai()` | curl network failure | +| `RATE_ERROR:` | `request_ai()` | HTTP 429 rate limit | +| `REQUEST_ERROR:` | `request_ai()` | HTTP 400 bad request | +| `SERVER_ERROR:` | `request_ai()` | HTTP 5xx OpenAI server error | +| `HTTP_ERROR:` | `request_ai()` | Unexpected HTTP status | +| `API_ERROR:` | `parse_response()` | OpenAI returned an error object | +| `PARSE_ERROR:` | `parse_response()` | Response could not be parsed | + +### Detection Pattern in `main()` + +```bash +if [ $req_status -ne 0 ] || \ + [[ "$raw_response" == NETWORK_ERROR:* ]] || \ + [[ "$raw_response" == AUTH_ERROR:* ]] || \ + [[ "$raw_response" == RATE_ERROR:* ]] || \ + [[ "$raw_response" == REQUEST_ERROR:* ]] || \ + [[ "$raw_response" == SERVER_ERROR:* ]] || \ + [[ "$raw_response" == HTTP_ERROR:* ]]; then +``` + +The `[[ ... == PATTERN:* ]]` syntax uses Bash's glob pattern matching. The `*` matches any characters after the colon — making this a prefix-match test. + +--- + +## 12. File & Directory Outputs + +### Generated Files + +| Path | Created By | Contents | Condition | +|------|-----------|----------|-----------| +| `.github/workflows/main.yml` | `main()` → `echo "$final_yaml"` | AI-generated GitHub Actions YAML | Always, on success | +| `.github/workflows/` | `main()` → `mkdir -p` | Directory | Always, on success | + +### Temporary Files (Cleaned Up) + +| Pattern | Created By | Cleaned By | Purpose | +|---------|-----------|-----------|---------| +| `/tmp/ci_stop.XXXXXX` | `start_loader()` → `mktemp` | `stop_loader()` → `rm -f` | Animation stop sentinel | +| `/tmp/ci_prog.XXXXXX` | `start_loader()` → `mktemp` | `stop_loader()` → `rm -f` | Progress percentage IPC | +| `/tmp/ci_label.XXXXXX` | `start_loader()` → `mktemp` | `stop_loader()` → `rm -f` | Label text IPC | + +All three tmpfiles are cleaned on: +- Normal completion (`stop_loader()` in `main()`) +- User interrupt (`cleanup()` → `stop_loader()`) +- Unexpected error (`on_error()` → `stop_loader()`) + +--- + +## 13. Dependency Map + +### External Command Dependencies + +| Command | Required | Used By | Notes | +|---------|----------|---------|-------| +| `bash` | ✅ v4+ | Runtime | Array support requires v4+ | +| `python3` | ✅ v3.7+ | `create_payload()`, `parse_response()` | `json`, `re`, `sys` modules (stdlib) | +| `curl` | ✅ v7+ | `request_ai()` | `-w` status capture requires 7.x | +| `find` | ✅ | `get_project_tree()` | POSIX standard | +| `grep` | ✅ | `get_project_tree()`, `request_ai()` | POSIX standard | +| `sed` | ✅ | `get_project_tree()` | POSIX standard | +| `ls` | ✅ | `get_context()` | POSIX standard | +| `mktemp` | ✅ | `start_loader()` | GNU/BSD standard | +| `tput` | ✅ | Animation, TUI | Part of `ncurses` | +| `git-sync` | ❌ Optional | `prompt_git_sync()` | Only if user confirms YES | +| `shellcheck` | ❌ Runtime | Generated `main.yml` step | Required in CI environment, not locally | + +### Python3 Module Dependencies + +All Python3 modules used are part of the **standard library** — no `pip install` required: + +| Module | Used In | Purpose | +|--------|---------|---------| +| `json` | `create_payload()`, `parse_response()` | Serialise/deserialise JSON | +| `re` | `parse_response()` | Regex fence stripping | +| `sys` | `create_payload()`, `parse_response()` | `sys.argv`, `sys.stdin` | + +### Environment Variable Dependencies + +| Variable | Required | Fallback | +|----------|----------|---------| +| `OPENAI_API_KEY` | ✅ | None — `AUTH_ERROR` if absent | +| `TERM` | ❌ | `tput` commands silently fail gracefully if absent | +| `HOME` | ❌ | Not used directly | + +--- + +*Last updated: AX v1.0.0 — Nikan Eidi* diff --git a/public/Test_Cases.md b/public/Test_Cases.md new file mode 100644 index 0000000..0651064 --- /dev/null +++ b/public/Test_Cases.md @@ -0,0 +1,890 @@ +# ⚡ AX — Test Cases + +> **Document Type:** Test Case Specifications & Validation Scenarios +> **Project:** AX — The Cybernetic Workflow Architect +> **Developer:** Nikan Eidi +> **Version:** 1.0.0 +> **Scope:** Full documentation of all test scenarios covering pipeline correctness, error handling, animation resilience, TUI behaviour, edge cases, and boundary conditions. + +--- + +## Testing Philosophy + +AX is a system with two distinct concerns that must be tested independently and together: + +1. **Functional Correctness** — Does the generated workflow accurately reflect the project's technology stack? +2. **Operational Resilience** — Does AX handle failures, signals, and edge cases without leaving the terminal in a broken state? + +All tests assume the environment has `bash 4+`, `python3 3.7+`, `curl`, and the `OPENAI_API_KEY` variable set — unless the test is specifically validating the absence of these. + +--- + +## Test Case Index + +| ID | Name | Category | Priority | +|----|------|----------|----------| +| [TC-001](#tc-001--the-chonk-meter-monorepo-stress-test) | The Chonk-Meter | Integration / AI Output | P0 | +| [TC-002](#tc-002--hybrid-stress-test-node--python) | Hybrid Stress Test | Integration / AI Output | P0 | +| [TC-003](#tc-003--ghost-dependency-isolation-shell-only) | Ghost-Dependency Isolation | Integration / Correctness | P0 | +| [TC-004](#tc-004--single-file-minimalism) | Single-File Minimalism | Integration / AI Output | P1 | +| [TC-005](#tc-005--auth-error-handling) | Auth Error Handling | Unit / Error Path | P0 | +| [TC-006](#tc-006--sigint-resilience-ctrlc) | SIGINT Resilience | Unit / Signal Handling | P0 | +| [TC-007](#tc-007--network-timeout-handling) | Network Timeout | Unit / Error Path | P1 | +| [TC-008](#tc-008--rate-limit-error-handling) | Rate Limit Error | Unit / Error Path | P1 | +| [TC-009](#tc-009--malformed-api-response) | Malformed API Response | Unit / Parse Error | P1 | +| [TC-010](#tc-010--empty-yaml-response) | Empty YAML Response | Unit / Parse Error | P1 | +| [TC-011](#tc-011--markdown-fence-stripping) | Markdown Fence Stripping | Unit / Parsing | P1 | +| [TC-012](#tc-012--disk-write-failure) | Disk Write Failure | Unit / Error Path | P1 | +| [TC-013](#tc-013--general--single-file-fallback) | General Fallback | Integration / Fallback | P2 | +| [TC-014](#tc-014--animation-scroll-safety) | Animation Scroll Safety | Visual / Resilience | P1 | +| [TC-015](#tc-015--tui-arrow-key-navigation) | TUI Arrow-Key Navigation | UI / Interaction | P1 | +| [TC-016](#tc-016--tui-direct-character-dispatch) | TUI Direct Character | UI / Interaction | P1 | +| [TC-017](#tc-017--tui-lone-esc-cancel) | TUI ESC Cancel | UI / Edge Case | P2 | +| [TC-018](#tc-018--git-sync-not-on-path) | git-sync Missing | Integration / Error Path | P2 | +| [TC-019](#tc-019--progress-bar-boundary-values) | Progress Bar Boundaries | Unit / Visual | P2 | +| [TC-020](#tc-020--windows-script-detection) | Windows Script Detection | Unit / Detection | P2 | + +--- + +## TC-001 — The Chonk-Meter (Monorepo Stress Test) + +**Category:** Integration / AI Output +**Priority:** P0 +**Nickname:** *"The Chonk-Meter"* — named for the maximum payload size pushed through the detection and generation pipeline. + +### Objective +Verify that AX correctly generates a **five-language hybrid workflow** when all major supported stacks are present simultaneously in a single project root. + +### Environment Setup + +```bash +mkdir /tmp/ax-test-chonk && cd /tmp/ax-test-chonk + +# Node.js signal +echo '{"name":"test","scripts":{"test":"jest"}}' > package.json + +# Python signals +echo "requests==2.28.0" > requirements.txt +echo "print('hello')" > main.py + +# Go signal +echo "module test" > go.mod +echo "package main" > main.go + +# Rust signal +echo '[package]\nname = "test"' > Cargo.toml +echo "fn main() {}" > main.rs + +# Shell scripts (including ax.sh which should be ignored) +echo "#!/bin/bash\necho hi" > helper.sh +cp /usr/local/bin/ax ax.sh # simulate ax being present + +# No Java, Ruby, or C++ files intentionally +``` + +### Expected Behaviour + +**`get_context()` output:** +``` +Node.js Python Go Rust Shell +``` + +**`get_project_tree()` output:** Flat tree with all files visible to depth 3. + +**Generated `.github/workflows/main.yml` must contain:** + +| Required Step | Assertion | +|---------------|-----------| +| `actions/checkout@v4` | Present as first step | +| `runs-on: ubuntu-latest` | Present on job | +| Node.js setup | `actions/setup-node` or equivalent | +| Python setup | `actions/setup-python` or equivalent | +| Go setup | `actions/setup-go` or equivalent | +| Rust setup | `actions/cache` + `cargo` commands or `dtolnay/rust-toolchain` | +| Shell lint | `shellcheck` command present | + +**Generated `.github/workflows/main.yml` must NOT contain:** + +| Prohibited Content | Reason | +|--------------------|--------| +| Any reference to `ax.sh` | Ghost-dependency exclusion | +| Java setup steps (`setup-java`) | No Java source files | +| Ruby setup steps (`setup-ruby`) | No Ruby source files | +| C++ setup steps | No C++ source files | + +### Pass Criteria +- `main.yml` file exists at `.github/workflows/main.yml` +- All five required language steps present +- Zero prohibited ghost-dependency steps +- Valid YAML (parseable by `python3 -c "import yaml; yaml.safe_load(open('.github/workflows/main.yml'))"`) +- AX exits with code `0` + +### Fail Indicators +- Missing any of the five language setup steps +- `ax.sh` referenced in the workflow +- Any language step for Java, Ruby, or C++ +- Invalid YAML output +- Non-zero exit code + +--- + +## TC-002 — Hybrid Stress Test (Node + Python) + +**Category:** Integration / AI Output +**Priority:** P0 + +### Objective +Verify that AX generates a clean **two-language hybrid workflow** for a Node.js + Python project without injecting setup steps for any other language. + +### Environment Setup + +```bash +mkdir /tmp/ax-test-hybrid && cd /tmp/ax-test-hybrid + +# Node.js signal +echo '{"name":"app","dependencies":{"express":"^4.18.0"},"scripts":{"test":"jest"}}' > package.json + +# Python signals +echo "flask==2.3.0\nrequests==2.28.0" > requirements.txt +echo "from flask import Flask" > app.py +echo "import requests" > utils.py + +# No Go, Rust, Java, Shell, or C++ files +``` + +### Expected Behaviour + +**`get_context()` output:** +``` +Node.js Python +``` + +**Generated workflow must contain:** + +| Required | Assertion | +|----------|-----------| +| `actions/checkout@v4` | First step | +| `runs-on: ubuntu-latest` | On job | +| `actions/setup-node` | Node.js setup | +| `actions/setup-python` | Python setup | +| npm install or test step | CI action step | +| pip install step | `pip install -r requirements.txt` or equivalent | + +**Generated workflow must NOT contain:** + +| Prohibited | Reason | +|------------|--------| +| `shellcheck` | No `.sh` files present | +| `setup-go` | No Go files | +| `setup-rust` / cargo | No Rust files | +| `setup-java` | No Java files | + +### Pass Criteria +- Two and only two language setup blocks present +- Valid YAML, exit code 0 +- No ghost dependency steps + +--- + +## TC-003 — Ghost-Dependency Isolation (Shell-Only) + +**Category:** Integration / Correctness +**Priority:** P0 +**This is the most critical correctness test for AX's ghost-dependency prevention system.** + +### Objective +Verify that a project containing **only shell scripts** (including `ax.sh` itself) produces exactly a **two-step minimalist workflow** — `checkout` + `shellcheck` — with no setup steps for any programming language. + +### Environment Setup + +```bash +mkdir /tmp/ax-test-shell && cd /tmp/ax-test-shell + +# Only shell scripts — no manifests, no language source files +cp /usr/local/bin/ax ax.sh +echo '#!/bin/bash\necho "deploy"' > deploy.sh +echo '#!/bin/bash\necho "build"' > build.sh + +# Explicitly NO: package.json, requirements.txt, go.mod, Cargo.toml, *.py, *.go, *.rs, *.java +``` + +### Expected Behaviour + +**`get_context()` output:** +``` +Shell +``` + +**Generated workflow must contain EXACTLY:** + +```yaml +name: CI + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Lint shell scripts + run: find . -name '*.sh' -not -path '*/.*' | xargs shellcheck --severity=warning +``` +*(Exact YAML may vary in formatting — the semantic content is what matters.)* + +**Step count assertion:** The `steps` array must contain **exactly 2 entries**. + +**Generated workflow must NOT contain:** + +| Prohibited | Reason | +|------------|--------| +| `setup-node` / npm | No `package.json` | +| `setup-python` / pip | No Python files | +| `setup-go` | No Go files | +| `setup-rust` | No Rust files | +| Any reference to `ax.sh` | EXCLUSION directive | +| More than 2 steps | MINIMALISM directive | + +### Pass Criteria +- Exactly 2 steps in the workflow +- Step 1: `actions/checkout@v4` +- Step 2: shellcheck command with `--severity=warning` flag +- No language setup steps +- No reference to `ax.sh` +- Valid YAML, exit code 0 + +### Fail Indicators +- More than 2 steps +- `ax.sh` in the workflow +- Any `setup-*` step present +- shellcheck step missing or malformed + +--- + +## TC-004 — Single-File Minimalism + +**Category:** Integration / AI Output +**Priority:** P1 + +### Objective +Verify that a project with a single Python source file and no manifests receives a minimal Python workflow — without over-engineering. + +### Environment Setup + +```bash +mkdir /tmp/ax-test-single && cd /tmp/ax-test-single + +# Single file only +echo "def hello():\n print('world')\n\nhello()" > script.py + +# No manifests, no other source files +``` + +### Expected Behaviour + +**`get_context()` output:** +``` +Python +``` + +**Generated workflow must:** +- Contain `actions/checkout@v4` +- Contain `actions/setup-python` +- Have fewer than 6 total steps +- Not reference any tool not evidenced by the project + +### Pass Criteria +- Valid Python-only workflow +- Step count ≤ 5 +- Exit code 0 + +--- + +## TC-005 — Auth Error Handling + +**Category:** Unit / Error Path +**Priority:** P0 + +### Objective +Verify that AX exits cleanly and informatively when `OPENAI_API_KEY` is not set, without leaving the terminal in a broken state. + +### Environment Setup + +```bash +mkdir /tmp/ax-test-auth && cd /tmp/ax-test-auth +echo '{"name":"test"}' > package.json + +# Unset the API key +unset OPENAI_API_KEY +``` + +### Expected Behaviour + +**Execution flow:** +1. AX starts, loader animates through SCANNING and BUILDING phases +2. Upon reaching `request_ai()`, pre-flight check fires +3. Loader stops cleanly +4. Error message printed +5. Script exits + +**Expected terminal output (excerpt):** +``` +⚡ AUTH_ERROR: OPENAI_API_KEY is not set. +``` + +**Expected exit code:** `1` + +**Terminal state assertions:** +- Cursor is visible (`tput cnorm` was called) +- No animation artifacts remain on screen +- All three tmpfiles removed from `/tmp/` + +**Filesystem assertions:** +- `.github/workflows/main.yml` does **not** exist (write never reached) + +### Pass Criteria +- Exit code `1` +- `AUTH_ERROR:` prefix in output +- Cursor visible post-exit +- No partial file written +- No orphaned `/tmp/ci_stop.*`, `/tmp/ci_prog.*`, `/tmp/ci_label.*` files + +### Fail Indicators +- Exit code 0 +- Cursor hidden after script exits +- Partial or empty `main.yml` created +- Orphaned tmpfiles + +--- + +## TC-006 — SIGINT Resilience (Ctrl+C) + +**Category:** Unit / Signal Handling +**Priority:** P0 + +### Objective +Verify that pressing `Ctrl+C` at any point during execution causes a clean, graceful shutdown — with loader stopped, cursor restored, and no terminal corruption — and exits with code `130`. + +### Environment Setup + +```bash +mkdir /tmp/ax-test-sigint && cd /tmp/ax-test-sigint +echo '{"name":"test"}' > package.json +export OPENAI_API_KEY="sk-real-or-test-key" +``` + +### Test Variants + +| Variant | When to Send SIGINT | Expected Behaviour | +|---------|--------------------|--------------------| +| A | During SCANNING phase (< 1 second) | Immediate clean exit | +| B | During AI REQUESTING phase (long wait) | Loader stops, cursor restores, exit 130 | +| C | During TUI `prompt_git_sync` display | Panel wiped, cursor restored, exit 130 | + +### Procedure (Variant B — most critical) + +```bash +# In terminal 1: start AX +ax + +# When animation shows "Consulting OpenAI..." (REQUESTING phase): +# Press Ctrl+C +``` + +### Expected Terminal Output + +``` +⚡ Sequence terminated. +``` + +**Expected exit code:** `130` + +### Terminal State Assertions +- Cursor is visible +- Animation block completely erased +- Prompt is responsive (no raw mode stuck) +- No zombie background processes: `ps aux | grep _loader_loop` returns empty + +### Filesystem Assertions +- No `/tmp/ci_stop.*`, `/tmp/ci_prog.*`, `/tmp/ci_label.*` files +- No partial `.github/workflows/main.yml` (if SIGINT before WRITING phase) + +### Pass Criteria +- Exit code exactly `130` +- `"⚡ Sequence terminated."` in output +- Clean terminal state +- No orphaned processes or tmpfiles + +--- + +## TC-007 — Network Timeout Handling + +**Category:** Unit / Error Path +**Priority:** P1 + +### Objective +Verify that a network timeout (curl exit code 28) is classified correctly and surfaced as a `NETWORK_ERROR` message. + +### Method +Mock the API URL or use a host that will not respond within 10 seconds. The simplest approach is to temporarily point `API_URL` at a non-routable address. + +### Environment Setup + +```bash +# Patch API_URL to a non-routable address for testing +# (requires editing ax.sh or using a test harness that overrides API_URL) +API_URL="https://10.255.255.1/v1/chat/completions" # blackhole IP +``` + +### Expected Output + +``` +⚡ NETWORK_ERROR: Request timed out after 60s +``` + +*(Note: with `--connect-timeout 10`, the actual wait is 10 seconds, not 60.)* + +**Expected exit code:** `1` + +### Pass Criteria +- `NETWORK_ERROR:` prefix in output +- Exit code `1` +- Clean terminal + +--- + +## TC-008 — Rate Limit Error Handling + +**Category:** Unit / Error Path +**Priority:** P1 + +### Objective +Verify that an HTTP 429 response is classified as `RATE_ERROR` with an actionable message. + +### Method +Use a mock server or intercept curl response to return HTTP 429. + +### Expected Output + +``` +⚡ RATE_ERROR: Rate limit exceeded — retry after a moment +``` + +**Expected exit code:** `1` + +--- + +## TC-009 — Malformed API Response + +**Category:** Unit / Parse Error +**Priority:** P1 + +### Objective +Verify that a non-JSON response body (e.g., an HTML error page from a proxy) is caught by `parse_response()` and surfaced as a `PARSE_ERROR` without crashing. + +### Method +Mock `request_ai()` to return a non-JSON string like `"502 Bad Gateway"`. + +### Expected Behaviour + +**`parse_response()` should output:** +``` +PARSE_ERROR: invalid JSON — ... +``` + +**`main()` should then print:** +``` +⚡ Critical: Failed to parse API response — PARSE_ERROR: invalid JSON — ... +``` + +**Expected exit code:** `1` + +### Pass Criteria +- `PARSE_ERROR:` prefix in output +- No uncaught Python exception stack trace visible to user +- Exit code `1` + +--- + +## TC-010 — Empty YAML Response + +**Category:** Unit / Parse Error +**Priority:** P1 + +### Objective +Verify that if the AI returns a `choices[]` entry with an empty `content` field, AX detects the empty output and exits with an informative error rather than writing an empty `main.yml`. + +### Method +Mock `parse_response()` to return an empty string. + +### Expected Output + +``` +⚡ Critical: AI returned empty output. +``` + +**Expected exit code:** `1` + +**Filesystem assertion:** `.github/workflows/main.yml` must not exist or must not have been created in this run. + +--- + +## TC-011 — Markdown Fence Stripping + +**Category:** Unit / Parsing +**Priority:** P1 + +### Objective +Verify that `parse_response()` correctly strips all variations of markdown code fences that GPT-4o may emit despite the "raw YAML only" instruction. + +### Test Inputs (all should produce identical clean YAML output) + +**Input A — with yaml language tag:** +```` +```yaml +name: CI +on: [push] +jobs: + build: + runs-on: ubuntu-latest +``` +```` + +**Input B — with generic backticks:** +```` +``` +name: CI +on: [push] +``` +```` + +**Input C — no fences (ideal case):** +``` +name: CI +on: [push] +``` + +**Input D — fences with surrounding whitespace:** +```` + +```yaml + +name: CI +on: [push] + +``` + +```` + +### Expected Output (all variants) + +```yaml +name: CI +on: [push] +jobs: + build: + runs-on: ubuntu-latest +``` + +*(Inputs B and C produce the subset without the jobs block — that's expected.)* + +### Pass Criteria +- All four inputs produce clean YAML starting with `name: CI` (no fence characters) +- No leading/trailing whitespace +- No `yaml` language tag string in output + +--- + +## TC-012 — Disk Write Failure + +**Category:** Unit / Error Path +**Priority:** P1 + +### Objective +Verify that a write failure (e.g., read-only filesystem, full disk) is caught and surfaced cleanly rather than silently producing a corrupt or empty workflow file. + +### Method +Create a read-only `.github/workflows/` directory before running AX. + +```bash +mkdir -p /tmp/ax-test-write/.github/workflows +chmod 444 /tmp/ax-test-write/.github/workflows +cd /tmp/ax-test-write +echo '{"name":"test"}' > package.json +``` + +### Expected Output + +``` +⚡ Failed to write main.yml — check disk space or permissions. +``` + +**Expected exit code:** `1` + +### Pass Criteria +- Error message contains "permissions" or equivalent +- Exit code `1` +- Clean terminal + +--- + +## TC-013 — General / Single-File Fallback + +**Category:** Integration / Fallback +**Priority:** P2 + +### Objective +Verify that a directory with no recognisable language signals produces a valid minimal workflow under the `"General/Single-File"` context. + +### Environment Setup + +```bash +mkdir /tmp/ax-test-general && cd /tmp/ax-test-general + +# Only a text file — no manifests, no code files +echo "Project notes" > README.txt +``` + +### Expected Behaviour + +**`get_context()` output:** +``` +General/Single-File +``` + +**Generated workflow:** Should be a minimal CI skeleton — checkout only, or with a placeholder step. Should not contain any language-specific setup. + +### Pass Criteria +- `get_context()` returns `"General/Single-File"` +- Generated YAML is valid +- No language setup steps +- Exit code 0 + +--- + +## TC-014 — Animation Scroll Safety + +**Category:** Visual / Resilience +**Priority:** P1 + +### Objective +Verify that the animation block remains correctly positioned even when the terminal viewport scrolls during a long operation (e.g., the AI request phase). + +### Procedure + +```bash +# Use a terminal with a small scroll buffer (e.g., 10 lines height) +# Run AX in a project that takes 10+ seconds to get an AI response +# During the REQUESTING phase, scroll the terminal upward +# Observe: animation should continue rendering at correct position after scroll +``` + +### Expected Behaviour +- Animation does not "jump" to wrong position after scroll +- Animation continues rendering in a fixed block +- No duplicate or ghost frames outside the animation block + +### Rationale +This test validates the `tput sc/rc` (save/restore cursor) design choice over `\033[{n}A` up-counting. The `\033[nA` approach would fail this test because it cannot move the cursor above the current viewport top after a scroll. + +### Pass Criteria +- Animation remains stable and correctly positioned throughout +- No visual artifacts outside the 18-line animation block + +--- + +## TC-015 — TUI Arrow-Key Navigation + +**Category:** UI / Interaction +**Priority:** P1 + +### Objective +Verify that all four arrow keys correctly toggle the YES/NO selection in the `prompt_git_sync()` TUI panel. + +### Procedure + +Run AX to completion in a test project, wait for the TUI panel to appear, then test each key: + +| Step | Key Input | Expected `selected` | Expected Visual | +|------|-----------|--------------------|-----------------| +| 1 | *(panel appears)* | `0` (YES) | YES highlighted in reverse-video cyan | +| 2 | Press `↓` | `1` (NO) | NO highlighted in reverse-video magenta | +| 3 | Press `↑` | `0` (YES) | YES highlighted again | +| 4 | Press `→` | `1` (NO) | NO highlighted | +| 5 | Press `←` | `0` (YES) | YES highlighted | +| 6 | Press `Enter` | — | Panel wipes, YES confirmed | + +### Pass Criteria +- Each arrow key changes selection correctly +- Visual highlight updates immediately after each key +- Enter confirms current selection +- Panel wipes cleanly after Enter + +--- + +## TC-016 — TUI Direct Character Dispatch + +**Category:** UI / Interaction +**Priority:** P1 + +### Objective +Verify that `y`, `Y`, `n`, `N` key presses immediately confirm the corresponding selection without requiring Enter. + +| Input | Expected Selection | Expected Action | +|-------|--------------------|----------------| +| `y` | YES | Confirm YES immediately | +| `Y` | YES | Confirm YES immediately | +| `n` | NO | Confirm NO immediately | +| `N` | NO | Confirm NO immediately | + +### Pass Criteria +- Single character dispatches without Enter required +- Panel wipes immediately +- Correct branch executed (git-sync check for `y/Y`, graceful exit for `n/N`) + +--- + +## TC-017 — TUI Lone ESC Cancel + +**Category:** UI / Edge Case +**Priority:** P2 + +### Objective +Verify that pressing `Escape` without a following arrow key sequence (i.e., a lone ESC) is treated as a cancellation and selects NO. + +### Procedure +Press `Escape` key once during the TUI prompt. Wait 50ms+ for the timeout to expire. + +### Expected Behaviour +- `selected` becomes `1` (NO) +- Panel wipes cleanly +- Output: `[-] Skipped — exiting gracefully.` + +### Pass Criteria +- Exit code 0 +- NO branch executed +- No terminal corruption + +--- + +## TC-018 — git-sync Not on PATH + +**Category:** Integration / Error Path +**Priority:** P2 + +### Objective +Verify that selecting YES when `git-sync` is not installed produces a clear, actionable error message with an install URL — rather than a cryptic command-not-found error. + +### Procedure + +```bash +# Ensure git-sync is not installed +which git-sync # should return nothing + +# Run AX, answer YES to git-sync prompt +``` + +### Expected Output + +``` +[>] Confirmed — checking for git-sync... +[x] git-sync not found on PATH. + Install it first — see: https://github.com/kubernetes/git-sync +``` + +**Expected exit code:** `1` + +### Pass Criteria +- Install URL displayed +- Exit code `1` +- No `command not found` shell error message (which would be confusing) + +--- + +## TC-019 — Progress Bar Boundary Values + +**Category:** Unit / Visual +**Priority:** P2 + +### Objective +Verify that `_build_bar()` produces correct output at boundary values: 0%, 50%, and 100%. + +### Test Cases + +| Input | Expected `filled` | Expected `empty` | Expected bar visual | +|-------|-------------------|-----------------|---------------------| +| `0` | 0 | 24 | `░░░░░░░░░░░░░░░░░░░░░░░░` | +| `50` | 12 | 12 | `████████████░░░░░░░░░░░░` | +| `100` | 24 | 0 | `████████████████████████` | +| `101` (overflow) | 24 (capped) | 0 | Should not render more than 24 blocks | + +### Pass Criteria +- Correct character counts at 0, 50, 100 +- Total bar width always 24 characters (ignoring ANSI codes) +- No overflow at 100 or above + +--- + +## TC-020 — Windows Script Detection + +**Category:** Unit / Detection +**Priority:** P2 + +### Objective +Verify that `.bat` and `.ps1` files are detected and included in the context string. + +### Environment Setup + +```bash +mkdir /tmp/ax-test-win && cd /tmp/ax-test-win + +echo "@echo off\necho build" > build.bat +echo "Write-Host 'deploy'" > deploy.ps1 +``` + +### Expected Behaviour + +**`get_context()` output:** +``` +Windows-Script +``` + +**Generated workflow:** Should include a Windows or cross-platform CI consideration. Must contain `actions/checkout@v4` and `runs-on: ubuntu-latest` (or the AI may choose `windows-latest` — both are acceptable). + +### Pass Criteria +- `get_context()` returns string containing `"Windows-Script"` +- Generated YAML is valid +- Exit code 0 + +--- + +## Test Execution Summary Template + +Use this table to record results when running the full test suite: + +| Test ID | Name | Result | Exit Code | Notes | +|---------|------|--------|-----------|-------| +| TC-001 | The Chonk-Meter | ⬜ PASS / ⬜ FAIL | | | +| TC-002 | Hybrid Stress Test | ⬜ PASS / ⬜ FAIL | | | +| TC-003 | Ghost-Dep Isolation | ⬜ PASS / ⬜ FAIL | | | +| TC-004 | Single-File Minimalism | ⬜ PASS / ⬜ FAIL | | | +| TC-005 | Auth Error | ⬜ PASS / ⬜ FAIL | | | +| TC-006 | SIGINT Resilience | ⬜ PASS / ⬜ FAIL | | | +| TC-007 | Network Timeout | ⬜ PASS / ⬜ FAIL | | | +| TC-008 | Rate Limit | ⬜ PASS / ⬜ FAIL | | | +| TC-009 | Malformed Response | ⬜ PASS / ⬜ FAIL | | | +| TC-010 | Empty YAML | ⬜ PASS / ⬜ FAIL | | | +| TC-011 | Fence Stripping | ⬜ PASS / ⬜ FAIL | | | +| TC-012 | Disk Write Failure | ⬜ PASS / ⬜ FAIL | | | +| TC-013 | General Fallback | ⬜ PASS / ⬜ FAIL | | | +| TC-014 | Scroll Safety | ⬜ PASS / ⬜ FAIL | | | +| TC-015 | Arrow-Key Nav | ⬜ PASS / ⬜ FAIL | | | +| TC-016 | Direct Char Dispatch | ⬜ PASS / ⬜ FAIL | | | +| TC-017 | ESC Cancel | ⬜ PASS / ⬜ FAIL | | | +| TC-018 | git-sync Missing | ⬜ PASS / ⬜ FAIL | | | +| TC-019 | Progress Bar Bounds | ⬜ PASS / ⬜ FAIL | | | +| TC-020 | Windows Script | ⬜ PASS / ⬜ FAIL | | | + +--- + +*Last updated: AX v1.0.0 — Nikan Eidi* diff --git a/public/Traceability_Matrix.md b/public/Traceability_Matrix.md new file mode 100644 index 0000000..bec4ea0 --- /dev/null +++ b/public/Traceability_Matrix.md @@ -0,0 +1,197 @@ +# ⚡ AX — Traceability Matrix + +> **Document Type:** Feature-to-Implementation Traceability +> **Project:** AX — The Cybernetic Workflow Architect +> **Developer:** Nikan Eidi +> **Version:** 1.0.0 +> **Scope:** Full coverage of every user-facing feature, internal mechanism, and error path mapped to its implementing function(s), code location, and execution phase. + +--- + +## Purpose + +This matrix provides complete **bidirectional traceability** between: + +- **Features** — observable behaviours and capabilities of AX +- **Functions** — the exact Bash and Python3 code units that implement them +- **Phases** — the STATE machine step during which execution occurs +- **Lines** — approximate source line references for rapid navigation + +Use this document for code review, regression auditing, onboarding new contributors, and verifying that no feature exists without a corresponding implementation — and no function exists without a corresponding feature. + +--- + +## How to Read This Matrix + +| Column | Description | +|--------|-------------| +| **ID** | Unique traceability ID in format `AX-F-###` | +| **Feature** | The observable capability or behaviour | +| **Implementing Function(s)** | Bash/Python function(s) responsible | +| **Phase / State** | STATE variable value when this executes | +| **Dependencies** | Other features/functions this relies on | +| **Test Case Ref** | Corresponding test case in `Test Cases.md` | + +--- + +## Section 1 — Core Pipeline Features + +| ID | Feature | Implementing Function(s) | Phase / State | Dependencies | Test Case Ref | +|----|---------|--------------------------|---------------|--------------|---------------| +| AX-F-001 | Scan project directory tree up to 3 levels deep | `get_project_tree()` | `SCANNING` | `find`, `grep`, `sed` | TC-001, TC-004 | +| AX-F-002 | Filter hidden files and directories from scan | `get_project_tree()` → `-not -path '*/.*'` | `SCANNING` | `find` flags | TC-003 | +| AX-F-003 | Filter generated/build directories from scan | `get_project_tree()` → `grep -vE` pattern | `SCANNING` | `grep` | TC-001 | +| AX-F-004 | Format raw paths into indented tree representation | `get_project_tree()` → `sed` pipeline | `SCANNING` | `sed` | TC-001 | +| AX-F-005 | Detect Node.js projects | `get_context()` → `package.json` check | `BUILDING` | File presence | TC-002 | +| AX-F-006 | Detect Python projects | `get_context()` → `requirements.txt` / `pyproject.toml` / `*.py` | `BUILDING` | File presence + glob | TC-002 | +| AX-F-007 | Detect Go projects | `get_context()` → `go.mod` / `*.go` | `BUILDING` | File presence + glob | TC-001 | +| AX-F-008 | Detect Rust projects | `get_context()` → `Cargo.toml` / `*.rs` | `BUILDING` | File presence + glob | TC-001 | +| AX-F-009 | Detect Java projects | `get_context()` → `pom.xml` / `build.gradle` / `*.java` | `BUILDING` | File presence + glob | TC-001 | +| AX-F-010 | Detect C/C++ projects | `get_context()` → `*.cpp` / `*.c` / `*.h` globs | `BUILDING` | Glob expansion | TC-001 | +| AX-F-011 | Detect Ruby projects | `get_context()` → `Gemfile` / `*.rb` | `BUILDING` | File presence + glob | TC-001 | +| AX-F-012 | Detect Shell script projects | `get_context()` → `*.sh` glob | `BUILDING` | Glob expansion | TC-003 | +| AX-F-013 | Detect Windows script projects | `get_context()` → `*.bat` / `*.ps1` globs | `BUILDING` | Glob expansion | — | +| AX-F-014 | Concurrent multi-language hybrid detection | `get_context()` → `tags[]` accumulation | `BUILDING` | All AX-F-005 to AX-F-013 | TC-001 | +| AX-F-015 | Fallback to General/Single-File if no language detected | `get_context()` → empty-tags default | `BUILDING` | AX-F-014 | TC-004 | +| AX-F-016 | Build structured JSON payload for OpenAI | `create_payload()` → `python3 json.dumps()` | `BUILDING` | `get_project_tree()`, `get_context()` | TC-001 to TC-004 | +| AX-F-017 | Inject system prompt with behavioural constraints | `create_payload()` → `system_prompt` string | `BUILDING` | Python3 | TC-003 | +| AX-F-018 | Send HTTP POST request to OpenAI API | `request_ai()` → `curl -X POST` | `REQUESTING` | `OPENAI_API_KEY`, curl | TC-005 | +| AX-F-019 | Set 60s request timeout + 10s connection timeout | `request_ai()` → `--max-time 60 --connect-timeout 10` | `REQUESTING` | curl flags | — | +| AX-F-020 | Capture HTTP status code alongside response body | `request_ai()` → `-w "\n__STATUS__%{http_code}"` | `REQUESTING` | curl `-w` flag | TC-005 | +| AX-F-021 | Classify and surface network-level curl errors | `request_ai()` → `curl_exit` case statement | `REQUESTING` | curl exit codes 6/7/28/35 | TC-006 | +| AX-F-022 | Classify and surface HTTP-level API errors | `request_ai()` → `http_code` case statement | `REQUESTING` | HTTP 400/401/403/429/500/502/503 | TC-005 | +| AX-F-023 | Strip markdown code fences from AI response | `parse_response()` → `re.sub` (open + close fence) | `PARSING` | Python3 `re` module | TC-001 | +| AX-F-024 | Extract YAML content from OpenAI `choices[]` field | `parse_response()` → `data['choices'][0]['message']['content']` | `PARSING` | Python3 `json` module | TC-001 | +| AX-F-025 | Route API-level errors from response body | `parse_response()` → `API_ERROR:` prefix | `PARSING` | Python3 | TC-005 | +| AX-F-026 | Route JSON parse errors gracefully | `parse_response()` → `PARSE_ERROR:` prefix | `PARSING` | Python3 `try/except` | — | +| AX-F-027 | Create `.github/workflows/` directory if absent | `main()` → `mkdir -p` | `WRITING` | Bash built-in | TC-001 | +| AX-F-028 | Write final YAML to `.github/workflows/main.yml` | `main()` → `echo "$final_yaml"` redirect | `WRITING` | AX-F-023, AX-F-024 | TC-001 | +| AX-F-029 | Validate final YAML is non-empty before writing | `main()` → empty string check on `$final_yaml` | `WRITING` | AX-F-024 | — | + +--- + +## Section 2 — State Machine + +| ID | Feature | Implementing Function(s) | Phase / State | Dependencies | Test Case Ref | +|----|---------|--------------------------|---------------|--------------|---------------| +| AX-F-030 | Centralised STATE variable tracking execution phase | `set_state()` / `get_state()` | All phases | Bash globals | — | +| AX-F-031 | STATE transitions: IDLE → SCANNING → BUILDING → REQUESTING → PARSING → WRITING → DONE | `main()` → sequential `set_state()` calls | All phases | AX-F-030 | TC-001 | +| AX-F-032 | STATE set to ERROR on any fatal failure | `main()` → `set_state "ERROR"` before `exit 1` | Error paths | AX-F-030 | TC-005, TC-006 | + +--- + +## Section 3 — Animation System + +| ID | Feature | Implementing Function(s) | Phase / State | Dependencies | Test Case Ref | +|----|---------|--------------------------|---------------|--------------|---------------| +| AX-F-033 | Spawn background loader animation process | `start_loader()` → `_loader_loop ... &` | All active phases | Bash subshell, tmpfiles | — | +| AX-F-034 | Create shared IPC tmpfiles for animation control | `start_loader()` → `mktemp` × 3 | Pre-animation | `mktemp` | — | +| AX-F-035 | Live progress percentage update from parent process | `set_loader_progress()` → write to `_PROG_FILE` | All active phases | AX-F-034 | — | +| AX-F-036 | Live label text update from parent process | `set_loader_progress()` → write to `_LABEL_FILE` | All active phases | AX-F-034 | — | +| AX-F-037 | Stop animation via sentinel file signal | `stop_loader()` → `touch "$_STOP_FILE"` | Post-phase | AX-F-034 | TC-006 | +| AX-F-038 | Wait for loader process to exit cleanly | `stop_loader()` → `wait "$LOADER_PID"` | Post-phase | AX-F-033 | — | +| AX-F-039 | Clean up all IPC tmpfiles after loader stops | `stop_loader()` → `rm -f` × 3 | Post-phase | AX-F-034 | — | +| AX-F-040 | 16fps animation loop reading shared state files | `_loader_loop()` → 0.06s sleep cycle | Background | AX-F-034 | — | +| AX-F-041 | Scroll-safe cursor anchor using `tput sc/rc` | `_loader_loop()` → `tput sc` / `tput rc` | Background | `tput` | — | +| AX-F-042 | Reserve exact vertical space for animation block | `_loader_loop()` → 18× `printf "\n"` then `\033[18A` | Background | AX-F-041 | — | +| AX-F-043 | Clean wipe of animation block on stop | `_loader_loop()` → 18× `\033[2K\n` after stop file detected | Background | AX-F-041 | — | +| AX-F-044 | Hide terminal cursor during animation | `_loader_loop()` → `tput civis` | Background | `tput` | — | +| AX-F-045 | Restore terminal cursor after animation | `_loader_loop()` → `tput cnorm` | Background | `tput` | TC-006 | +| AX-F-046 | 16-step blink cycle selection (open/half/closed) | `_draw_frame()` → `step=$(( f % 16 ))` | Background | AX-F-040 | — | +| AX-F-047 | Iris glyph rotation across 8-frame cycle | `_draw_frame()` → `iris_g` array + `f % 8` | Background | AX-F-046 | — | +| AX-F-048 | Pupil sparkle glyph rotation | `_draw_frame()` → `glow_g` array + `f % 8` | Background | AX-F-046 | — | +| AX-F-049 | Braille spinner for status label row | `_draw_frame()` → `sp` array + `f % 10` | Background | AX-F-040 | — | +| AX-F-050 | Render fully-open eye (13-line frame) | `_eye_open()` | Background | AX-F-046 | — | +| AX-F-051 | Render half-closed eye (13-line frame) | `_eye_half()` | Background | AX-F-046 | — | +| AX-F-052 | Render fully-closed eye (13-line frame) | `_eye_closed()` | Background | AX-F-046 | — | +| AX-F-053 | Per-frame iris ring color flicker on frame mod 4 | `_eye_open()` → `ring_color` conditional | Background | AX-F-047 | — | +| AX-F-054 | Progress bar block rendering (24-char width) | `_build_bar()` → `█` fill + `░` empty | Background | AX-F-035 | — | +| AX-F-055 | Percentage display alongside progress bar | `_draw_frame()` → `%3d%%` format in bar box | Background | AX-F-035 | — | +| AX-F-056 | Constant 18-line render budget per frame | `_draw_frame()` → 13 (eye) + 1 + 3 + 1 | Background | AX-F-050 to AX-F-052 | — | + +--- + +## Section 4 — Ghost-Dependency Prevention + +| ID | Feature | Implementing Function(s) | Phase / State | Dependencies | Test Case Ref | +|----|---------|--------------------------|---------------|--------------|---------------| +| AX-F-057 | Explicit exclusion of `ax.sh` from AI analysis | `create_payload()` → `"EXCLUSION: Completely IGNORE the file ax.sh"` in system prompt | `BUILDING` | GPT-4o system prompt | TC-003 | +| AX-F-058 | Minimalist two-step workflow for shell-only projects | `create_payload()` → `"MINIMALISM: If the project has only shell scripts..."` | `BUILDING` | AX-F-057 | TC-003 | +| AX-F-059 | Prohibition on ghost language setup steps | `create_payload()` → `"NO GHOST DEPENDENCIES: Never add setup steps..."` | `BUILDING` | GPT-4o system prompt | TC-001, TC-002 | +| AX-F-060 | SINGLEQUOTE token substitution for shell-safe prompts | `create_payload()` → Python `sys.argv` passing; token replacement instruction | `BUILDING` | Python3 string handling | TC-003 | +| AX-F-061 | `actions/checkout@v4` and `ubuntu-latest` standardisation | `create_payload()` → `"STANDARDS: Always use runs-on: ubuntu-latest and actions/checkout@v4"` | `BUILDING` | GPT-4o system prompt | TC-001 to TC-004 | +| AX-F-062 | Low temperature (0.1) for deterministic AI output | `create_payload()` → `"temperature": 0.1` | `BUILDING` | OpenAI API parameter | TC-001 | +| AX-F-063 | Raw YAML-only output instruction (no commentary) | `create_payload()` → `"CLEAN OUTPUT: Output ONLY raw YAML"` | `BUILDING` | AX-F-023 | TC-001 | + +--- + +## Section 5 — TUI & User Interaction + +| ID | Feature | Implementing Function(s) | Phase / State | Dependencies | Test Case Ref | +|----|---------|--------------------------|---------------|--------------|---------------| +| AX-F-064 | Post-write git-sync confirmation prompt | `prompt_git_sync()` | Post `DONE` | `command -v git-sync` | — | +| AX-F-065 | 8-line interactive TUI panel rendering | `_draw_prompt()` (nested in `prompt_git_sync`) | Post `DONE` | `tput sc/rc` | — | +| AX-F-066 | YES/NO option highlighting with `tput rev` | `_draw_prompt()` → `REV` variable + conditional styling | Post `DONE` | `tput` | — | +| AX-F-067 | Arrow key navigation (UP/LEFT = YES, DOWN/RIGHT = NO) | `prompt_git_sync()` → `read -rsn1` 3-byte ESC sequence parser | Post `DONE` | Bash `read` | — | +| AX-F-068 | Direct `y`/`Y` and `n`/`N` character dispatch (no Enter) | `prompt_git_sync()` → case on single byte | Post `DONE` | Bash `read` | — | +| AX-F-069 | Enter key confirms current selection | `prompt_git_sync()` → `$'\n'` case | Post `DONE` | Bash `read` | — | +| AX-F-070 | Lone ESC treated as cancel (NO) | `prompt_git_sync()` → ESC with 50ms follow-up timeout | Post `DONE` | `read -t 0.05` | — | +| AX-F-071 | Wipe TUI panel cleanly after confirmation | `prompt_git_sync()` → 8× `\033[2K\n` | Post `DONE` | AX-F-041 | — | +| AX-F-072 | `git-sync` PATH check before dispatch | `prompt_git_sync()` → `command -v git-sync` | Post `DONE` | Bash built-in | — | +| AX-F-073 | Graceful exit with message when git-sync not found | `prompt_git_sync()` → install hint + `exit 1` | Post `DONE` | AX-F-072 | — | +| AX-F-074 | Panel redraw on every navigation event | `prompt_git_sync()` → `tput rc` + `_draw_prompt` after each arrow key | Post `DONE` | AX-F-065 | — | + +--- + +## Section 6 — Error Handling & Resilience + +| ID | Feature | Implementing Function(s) | Phase / State | Dependencies | Test Case Ref | +|----|---------|--------------------------|---------------|--------------|---------------| +| AX-F-075 | Trap `INT`, `TERM`, `HUP` signals for clean shutdown | `cleanup()` + `trap cleanup INT TERM HUP` | Any | Bash `trap` | TC-006 | +| AX-F-076 | Trap `ERR` pseudo-signal with line number reporting | `on_error()` + `trap 'on_error $LINENO' ERR` | Any | Bash `trap` | — | +| AX-F-077 | Stop loader and restore cursor on any exit path | `cleanup()` / `on_error()` → `stop_loader()` + `tput cnorm` | Any | AX-F-037, AX-F-045 | TC-006 | +| AX-F-078 | Exit code 130 on user interrupt (SIGINT convention) | `cleanup()` → `exit 130` | Any | POSIX convention | TC-006 | +| AX-F-079 | Classify curl exit code 6 (DNS failure) | `request_ai()` → case `6` | `REQUESTING` | curl | — | +| AX-F-080 | Classify curl exit code 7 (connection refused) | `request_ai()` → case `7` | `REQUESTING` | curl | — | +| AX-F-081 | Classify curl exit code 28 (timeout) | `request_ai()` → case `28` | `REQUESTING` | curl | — | +| AX-F-082 | Classify curl exit code 35 (SSL failure) | `request_ai()` → case `35` | `REQUESTING` | curl | — | +| AX-F-083 | Detect missing `OPENAI_API_KEY` before curl call | `request_ai()` → `[ -z "$OPENAI_API_KEY" ]` | `REQUESTING` | Bash variable check | TC-005 | +| AX-F-084 | Propagate all error prefixes through main() checks | `main()` → `[[ "$raw_response" == *_ERROR:* ]]` pattern | `REQUESTING` | AX-F-021, AX-F-022 | TC-005 | +| AX-F-085 | Check for empty payload before API call | `main()` → `[ -z "$payload" ]` | `BUILDING` | — | — | +| AX-F-086 | Check for directory creation failure | `main()` → `mkdir -p ... \|\| { ... exit 1 }` | `WRITING` | — | — | +| AX-F-087 | Check for file write failure (disk/permissions) | `main()` → `if ! echo ... > main.yml` | `WRITING` | — | — | +| AX-F-088 | `json.JSONDecodeError` caught and surfaced as `PARSE_ERROR` | `parse_response()` → Python `except json.JSONDecodeError` | `PARSING` | Python3 | — | +| AX-F-089 | `KeyError` caught for missing `choices` field | `parse_response()` → Python `except KeyError` | `PARSING` | Python3 | — | +| AX-F-090 | `IndexError` caught for empty `choices` array | `parse_response()` → Python `except IndexError` | `PARSING` | Python3 | — | + +--- + +## Section 7 — Visual & ANSI Styling + +| ID | Feature | Implementing Function(s) | Phase / State | Dependencies | Test Case Ref | +|----|---------|--------------------------|---------------|--------------|---------------| +| AX-F-091 | Cyberpunk ANSI color palette (10 colors + modifiers) | Global color variables: `CYAN`, `MAG`, `YEL`, `GRN`, `RED`, `BLU`, `WHT`, `GLOW`, `GOLD`, `DIM`, `BOLD`, `REV` | Global | ANSI escape codes | — | +| AX-F-092 | `$CLR` (`\033[2K`) line clear prepended to each animation row | All `_eye_*` + `_draw_frame()` → `"$CLR"` prefix | Background | ANSI | — | +| AX-F-093 | `$R` reset appended to every styled segment | All styled `printf` calls | Global | ANSI `\033[0m` | — | +| AX-F-094 | Box-drawing characters for eye frame borders (╔╗╚╝╦╩) | `_eye_open()`, `_eye_half()`, `_eye_closed()` | Background | Unicode box-drawing block | — | +| AX-F-095 | Block element characters for closed eye fill (▄▀) | `_eye_closed()`, `_eye_half()` | Background | Unicode block elements | — | +| AX-F-096 | Lash tip characters using safe narrow Unicode (╷╵) | `_eye_open()`, `_eye_half()`, `_eye_closed()` | Background | Unicode box-drawing | — | + +--- + +## Coverage Summary + +| Section | Features Defined | Functions Covered | +|---------|-----------------|-------------------| +| Core Pipeline | 29 | `get_project_tree`, `get_context`, `create_payload`, `request_ai`, `parse_response`, `main` | +| State Machine | 3 | `set_state`, `get_state`, `main` | +| Animation System | 24 | `start_loader`, `stop_loader`, `set_loader_progress`, `_loader_loop`, `_draw_frame`, `_eye_open`, `_eye_half`, `_eye_closed`, `_build_bar` | +| Ghost-Dep Prevention | 7 | `create_payload` (system prompt directives) | +| TUI & Interaction | 11 | `prompt_git_sync`, `_draw_prompt` | +| Error Handling | 16 | `cleanup`, `on_error`, `request_ai`, `parse_response`, `main` | +| Visual & ANSI | 6 | Global vars, all rendering functions | +| **Total** | **96** | **All functions** | + +--- + +*Last updated: AX v1.0.0 — Nikan Eidi*