diff --git a/README.org b/README.org index afafb553..c4f8cabd 100644 --- a/README.org +++ b/README.org @@ -152,6 +152,7 @@ Enable installation of packages from MELPA by adding an entry to package-archive - *Context-Aware Code Actions*: The menu exposes dedicated entries for changing code (`c`), implementing TODOs (`i`), asking questions (`q`), explaining code (`x`), sending free-form commands (``), and refreshing AI context (`@`). Each command automatically captures the surrounding function, region, or clipboard contents (via `C-u`) to keep prompts precise. - *Agile Development Workflows*: Use the refactoring navigator (`r`), the guided TDD cycle (`t`), and the pull/review diff helper (`v`) to keep AI-assisted work aligned with agile best practices. Prompt authoring is first-class through quick access to the prompt file (`p`), build/test helper (`b`), and AI-assisted shell/file execution (`!`). In prompt files, send the current block with `C-c C-c`. - *Productivity & Debugging Utilities*: Initialize project navigation assets (`.`), investigate exceptions (`e`), auto-fix Flycheck issues in scope (`f`), copy or open file paths formatted for prompts (`k`, `o`), generate MCP inspector commands (`m`), capture session notes straight into Org (`n`), dictate prompts with speech-to-text (`:`), and toggle desktop notifications (`N`) to get alerted when AI responses are ready in background sessions. +- *Repo Guardrails Capture*: Derive a concise architecture guardrails file for the current repository with `C-c a A`. The command creates or updates `.ai.code.files/architecture/guardrails.org` so future AI coding sessions can reuse practical module boundaries, dependency rules, and validation expectations. - *Seamless Prompt Management*: Open the prompt file via `ai-code-open-prompt-file` (stored under `.ai.code.files/.ai.code.prompt.org` by default), send regions with `ai-code-prompt-send-block`, and reuse prompt snippets via `yasnippet` to keep conversations organized. - *Interactive Chat & Context Tools*: Dedicated buffers hold long-running chats, automatically enriched with file paths, diffs, and history from Magit or Git commands for richer AI responses. - *AI-Assisted Bash Commands*: From Dired, shell, eshell, or vterm, run `C-c a !` and type natural-language commands prefixed with `:` (e.g., `:count lines of python code recursively`); the tool generates the shell command for review and executes it in a compile buffer. diff --git a/ai-code-discussion.el b/ai-code-discussion.el index 566e8495..8b741fd1 100644 --- a/ai-code-discussion.el +++ b/ai-code-discussion.el @@ -679,6 +679,107 @@ This value is used by `ai-code-take-notes' when suggesting where to store notes. "Content of the most recent AI output" "Default request text for `ai-code-take-notes'.") +(defconst ai-code-discussion--architecture-guardrails-file-name + "guardrails.org" + "File name for derived architecture guardrails.") + +(defconst ai-code-discussion--architecture-guardrails-directory-name + "architecture" + "Directory name for derived architecture guardrails.") + +(defconst ai-code-discussion--architecture-guardrails-template + (mapconcat #'identity + '("#+TITLE: Architecture Guardrails" + "" + "* Purpose" + "" + "* Important Modules / Areas" + "" + "* Dependency Rules" + "" + "* State and Ownership Rules" + "" + "* AI Change Rules" + "" + "* Required Validation" + "" + "* Notes and Uncertainties" + "") + "\n") + "Initial Org template for architecture guardrails.") + +(defun ai-code--architecture-guardrails-relative-path () + "Return the repo-relative path for the architecture guardrails file." + (concat ai-code-files-dir-name "/" + ai-code-discussion--architecture-guardrails-directory-name "/" + ai-code-discussion--architecture-guardrails-file-name)) + +(defun ai-code--architecture-guardrails-file-path () + "Return the absolute path for the architecture guardrails file." + (expand-file-name ai-code-discussion--architecture-guardrails-file-name + (expand-file-name + ai-code-discussion--architecture-guardrails-directory-name + (ai-code--ensure-files-directory)))) + +(defun ai-code--ensure-architecture-guardrails-file () + "Create the architecture guardrails file with a starter template if missing." + (let ((target-file (ai-code--architecture-guardrails-file-path))) + (unless (file-directory-p (file-name-directory target-file)) + (make-directory (file-name-directory target-file) t)) + (unless (file-exists-p target-file) + (with-temp-file target-file + (insert ai-code-discussion--architecture-guardrails-template))) + target-file)) + +(defun ai-code--build-architecture-guardrails-prompt (git-root) + "Build the default prompt to derive architecture guardrails for GIT-ROOT." + (let ((relative-path (ai-code--architecture-guardrails-relative-path))) + (mapconcat + #'identity + (list "Derive a lightweight architecture guardrails document for this existing repository." + (format "Repository path: %s" git-root) + (format "Write or update @%s in Org-mode format." relative-path) + "" + "Infer practical module boundaries, dependency rules, state ownership rules, and validation expectations from the current code, tests, docs, and filenames." + "Do not invent an ideal architecture." + "Do not force DDD, Hexagonal Architecture, or Clean Architecture onto the repository." + "Prefer simple, practical rules over abstract architecture theory." + "Mark uncertain conclusions clearly." + "Focus on what helps future AI coding sessions avoid breaking boundaries or introducing messy dependencies." + "Do not suggest large refactors unless clearly separated as optional future ideas." + "Keep it concise, practical, and small enough to reuse in future AI prompts." + "" + "Use this Org structure:" + "#+TITLE: Architecture Guardrails" + "" + "* Purpose" + "* Important Modules / Areas" + "* Dependency Rules" + "* State and Ownership Rules" + "* AI Change Rules" + "* Required Validation" + "* Notes and Uncertainties" + "" + "If the file already exists, refine it instead of rewriting unrelated guidance.") + "\n"))) + +;;;###autoload +(defun ai-code-derive-architecture-guardrails () + "Ask the current AI backend to derive repository architecture guardrails." + (interactive) + (let ((git-root (ai-code--git-root))) + (unless git-root + (user-error "Not in a git repository")) + (ai-code--ensure-architecture-guardrails-file) + (if-let ((final-prompt + (ai-code-read-string + "Prompt: " + (ai-code--build-architecture-guardrails-prompt git-root)))) + (progn + (ai-code--insert-prompt final-prompt) + (message "Requested architecture guardrails for %s" git-root)) + (message "Architecture guardrails request cancelled")))) + (defun ai-code--get-note-candidates (default-note-file) "Get a list of candidate note files. DEFAULT-NOTE-FILE is included in the list. Visible org buffers are prioritized." diff --git a/ai-code.el b/ai-code.el index e9955735..973d56a1 100644 --- a/ai-code.el +++ b/ai-code.el @@ -461,6 +461,7 @@ Shows the current backend label to the right." ("P" "AI session checkpoint" ai-code-session-checkpoint) ("e" "Debug exception (C-u: clipboard)" ai-code-investigate-exception) ("f" "Fix Flycheck errors in scope" ai-code-flycheck-fix-errors-in-scope) + ("A" "Derive Architecture Guardrails" ai-code-derive-architecture-guardrails) ("k" "Copy Cur File Name (C-u: full)" ai-code-copy-buffer-file-name-to-clipboard) ;; ("o" "Open recent file (C-u: insert)" ai-code-git-repo-recent-modified-files) ("p" "Open prompt history file" ai-code-open-prompt-file) diff --git a/test/test_ai-code-discussion.el b/test/test_ai-code-discussion.el index 565e6aa2..72973ead 100644 --- a/test/test_ai-code-discussion.el +++ b/test/test_ai-code-discussion.el @@ -447,6 +447,117 @@ (ai-code-take-notes t) (should (equal captured-file "/tmp/project/.ai.code.files/test-notes.org"))))) +(ert-deftest ai-code-test-derive-architecture-guardrails-creates-template-and-prompt () + "Test `ai-code-derive-architecture-guardrails' initializes the Org file and prompt." + (let* ((tmp-root (make-temp-file "ai-code-guardrails" t)) + (target-file (expand-file-name ".ai.code.files/architecture/guardrails.org" tmp-root)) + captured-initial-prompt + captured-final-prompt) + (unwind-protect + (cl-letf (((symbol-function 'ai-code--git-root) + (lambda (&optional _dir) + tmp-root)) + ((symbol-function 'ai-code-read-string) + (lambda (prompt initial-input &optional _candidate-list) + (should (equal prompt "Prompt: ")) + (setq captured-initial-prompt initial-input) + initial-input)) + ((symbol-function 'ai-code--insert-prompt) + (lambda (prompt) + (setq captured-final-prompt prompt)))) + (ai-code-derive-architecture-guardrails) + (should (file-exists-p target-file)) + (with-temp-buffer + (insert-file-contents target-file) + (should (string-match-p (regexp-quote "#+TITLE: Architecture Guardrails") + (buffer-string))) + (should (string-match-p (regexp-quote "* Dependency Rules") + (buffer-string))) + (should (string-match-p (regexp-quote "* Required Validation") + (buffer-string)))) + (should (string-match-p (regexp-quote "Derive a lightweight architecture guardrails document") + captured-initial-prompt)) + (should (string-match-p (regexp-quote "current code, tests, docs, and filenames") + captured-initial-prompt)) + (should (string-match-p (regexp-quote "Do not invent an ideal architecture") + captured-initial-prompt)) + (should (string-match-p (regexp-quote "Keep it concise") + captured-initial-prompt)) + (should (string-match-p (regexp-quote "@.ai.code.files/architecture/guardrails.org") + captured-initial-prompt)) + (should (string-match-p (regexp-quote "Org-mode format") + captured-initial-prompt)) + (should (equal captured-final-prompt captured-initial-prompt))) + (ignore-errors (delete-directory tmp-root t))))) + +(ert-deftest ai-code-test-derive-architecture-guardrails-preserves-existing-file () + "Test `ai-code-derive-architecture-guardrails' does not overwrite an existing file." + (let* ((tmp-root (make-temp-file "ai-code-guardrails-existing" t)) + (files-dir (expand-file-name ".ai.code.files/architecture" tmp-root)) + (target-file (expand-file-name "guardrails.org" files-dir)) + (existing-content "#+TITLE: Existing guardrails\n")) + (unwind-protect + (progn + (make-directory files-dir t) + (with-temp-file target-file + (insert existing-content)) + (cl-letf (((symbol-function 'ai-code--git-root) + (lambda (&optional _dir) + tmp-root)) + ((symbol-function 'ai-code-read-string) + (lambda (_prompt initial-input &optional _candidate-list) + initial-input)) + ((symbol-function 'ai-code--insert-prompt) + (lambda (_prompt)))) + (ai-code-derive-architecture-guardrails)) + (with-temp-buffer + (insert-file-contents target-file) + (should (equal (buffer-string) existing-content)))) + (ignore-errors (delete-directory tmp-root t))))) + +(ert-deftest ai-code-test-derive-architecture-guardrails-errors-outside-git-repo () + "Test `ai-code-derive-architecture-guardrails' requires a git repository." + (cl-letf (((symbol-function 'ai-code--git-root) + (lambda (&optional _dir) + nil))) + (should-error (ai-code-derive-architecture-guardrails) + :type 'user-error))) + +(ert-deftest ai-code-test-derive-architecture-guardrails-reports-cancelled-request () + "Test `ai-code-derive-architecture-guardrails' reports cancellation." + (let* ((tmp-root (make-temp-file "ai-code-guardrails-cancel" t)) + captured-message + insert-called) + (unwind-protect + (cl-letf (((symbol-function 'ai-code--git-root) + (lambda (&optional _dir) + tmp-root)) + ((symbol-function 'ai-code-read-string) + (lambda (&rest _args) + nil)) + ((symbol-function 'ai-code--insert-prompt) + (lambda (&rest _args) + (setq insert-called t))) + ((symbol-function 'message) + (lambda (format-string &rest args) + (setq captured-message + (apply #'format format-string args))))) + (ai-code-derive-architecture-guardrails) + (should-not insert-called) + (should (equal captured-message + "Architecture guardrails request cancelled"))) + (ignore-errors (delete-directory tmp-root t))))) + +(ert-deftest ai-code-test-menu-source-includes-derive-architecture-guardrails-entry () + "Test the menu source exposes the architecture guardrails command." + (let ((repo-root + (file-name-directory (locate-library "ai-code-discussion")))) + (with-temp-buffer + (insert-file-contents (expand-file-name "ai-code.el" repo-root)) + (should (re-search-forward + "(\"A\" \"Derive Architecture Guardrails\" ai-code-derive-architecture-guardrails)" + nil t))))) + (provide 'test_ai-code-discussion) ;;; test_ai-code-discussion.el ends here