Skip to content

Commit ccb2219

Browse files
Copilottninja
andauthored
Add architecture guardrails derivation command for existing repos (#361)
* Initial plan * feat: add architecture guardrails command Agent-Logs-Url: https://github.com/tninja/ai-code-interface.el/sessions/9370ff3b-9d42-4c80-b855-e61b0f2455a4 Co-authored-by: tninja <714625+tninja@users.noreply.github.com> * test: tighten guardrails command coverage Agent-Logs-Url: https://github.com/tninja/ai-code-interface.el/sessions/9370ff3b-9d42-4c80-b855-e61b0f2455a4 Co-authored-by: tninja <714625+tninja@users.noreply.github.com> * refactor: store architecture guardrails as org Agent-Logs-Url: https://github.com/tninja/ai-code-interface.el/sessions/9322736a-d00b-4e20-b59e-c90c9286ebe7 Co-authored-by: tninja <714625+tninja@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: tninja <714625+tninja@users.noreply.github.com>
1 parent 7ccd471 commit ccb2219

4 files changed

Lines changed: 214 additions & 0 deletions

File tree

README.org

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ Enable installation of packages from MELPA by adding an entry to package-archive
152152
- *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 (`<SPC>`), and refreshing AI context (`@`). Each command automatically captures the surrounding function, region, or clipboard contents (via `C-u`) to keep prompts precise.
153153
- *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`.
154154
- *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.
155+
- *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.
155156
- *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.
156157
- *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.
157158
- *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.

ai-code-discussion.el

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,107 @@ This value is used by `ai-code-take-notes' when suggesting where to store notes.
679679
"Content of the most recent AI output"
680680
"Default request text for `ai-code-take-notes'.")
681681

682+
(defconst ai-code-discussion--architecture-guardrails-file-name
683+
"guardrails.org"
684+
"File name for derived architecture guardrails.")
685+
686+
(defconst ai-code-discussion--architecture-guardrails-directory-name
687+
"architecture"
688+
"Directory name for derived architecture guardrails.")
689+
690+
(defconst ai-code-discussion--architecture-guardrails-template
691+
(mapconcat #'identity
692+
'("#+TITLE: Architecture Guardrails"
693+
""
694+
"* Purpose"
695+
""
696+
"* Important Modules / Areas"
697+
""
698+
"* Dependency Rules"
699+
""
700+
"* State and Ownership Rules"
701+
""
702+
"* AI Change Rules"
703+
""
704+
"* Required Validation"
705+
""
706+
"* Notes and Uncertainties"
707+
"")
708+
"\n")
709+
"Initial Org template for architecture guardrails.")
710+
711+
(defun ai-code--architecture-guardrails-relative-path ()
712+
"Return the repo-relative path for the architecture guardrails file."
713+
(concat ai-code-files-dir-name "/"
714+
ai-code-discussion--architecture-guardrails-directory-name "/"
715+
ai-code-discussion--architecture-guardrails-file-name))
716+
717+
(defun ai-code--architecture-guardrails-file-path ()
718+
"Return the absolute path for the architecture guardrails file."
719+
(expand-file-name ai-code-discussion--architecture-guardrails-file-name
720+
(expand-file-name
721+
ai-code-discussion--architecture-guardrails-directory-name
722+
(ai-code--ensure-files-directory))))
723+
724+
(defun ai-code--ensure-architecture-guardrails-file ()
725+
"Create the architecture guardrails file with a starter template if missing."
726+
(let ((target-file (ai-code--architecture-guardrails-file-path)))
727+
(unless (file-directory-p (file-name-directory target-file))
728+
(make-directory (file-name-directory target-file) t))
729+
(unless (file-exists-p target-file)
730+
(with-temp-file target-file
731+
(insert ai-code-discussion--architecture-guardrails-template)))
732+
target-file))
733+
734+
(defun ai-code--build-architecture-guardrails-prompt (git-root)
735+
"Build the default prompt to derive architecture guardrails for GIT-ROOT."
736+
(let ((relative-path (ai-code--architecture-guardrails-relative-path)))
737+
(mapconcat
738+
#'identity
739+
(list "Derive a lightweight architecture guardrails document for this existing repository."
740+
(format "Repository path: %s" git-root)
741+
(format "Write or update @%s in Org-mode format." relative-path)
742+
""
743+
"Infer practical module boundaries, dependency rules, state ownership rules, and validation expectations from the current code, tests, docs, and filenames."
744+
"Do not invent an ideal architecture."
745+
"Do not force DDD, Hexagonal Architecture, or Clean Architecture onto the repository."
746+
"Prefer simple, practical rules over abstract architecture theory."
747+
"Mark uncertain conclusions clearly."
748+
"Focus on what helps future AI coding sessions avoid breaking boundaries or introducing messy dependencies."
749+
"Do not suggest large refactors unless clearly separated as optional future ideas."
750+
"Keep it concise, practical, and small enough to reuse in future AI prompts."
751+
""
752+
"Use this Org structure:"
753+
"#+TITLE: Architecture Guardrails"
754+
""
755+
"* Purpose"
756+
"* Important Modules / Areas"
757+
"* Dependency Rules"
758+
"* State and Ownership Rules"
759+
"* AI Change Rules"
760+
"* Required Validation"
761+
"* Notes and Uncertainties"
762+
""
763+
"If the file already exists, refine it instead of rewriting unrelated guidance.")
764+
"\n")))
765+
766+
;;;###autoload
767+
(defun ai-code-derive-architecture-guardrails ()
768+
"Ask the current AI backend to derive repository architecture guardrails."
769+
(interactive)
770+
(let ((git-root (ai-code--git-root)))
771+
(unless git-root
772+
(user-error "Not in a git repository"))
773+
(ai-code--ensure-architecture-guardrails-file)
774+
(if-let ((final-prompt
775+
(ai-code-read-string
776+
"Prompt: "
777+
(ai-code--build-architecture-guardrails-prompt git-root))))
778+
(progn
779+
(ai-code--insert-prompt final-prompt)
780+
(message "Requested architecture guardrails for %s" git-root))
781+
(message "Architecture guardrails request cancelled"))))
782+
682783
(defun ai-code--get-note-candidates (default-note-file)
683784
"Get a list of candidate note files.
684785
DEFAULT-NOTE-FILE is included in the list. Visible org buffers are prioritized."

ai-code.el

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,7 @@ Shows the current backend label to the right."
462462
("P" "AI session checkpoint" ai-code-session-checkpoint)
463463
("e" "Debug exception (C-u: clipboard)" ai-code-investigate-exception)
464464
("f" "Fix Flycheck errors in scope" ai-code-flycheck-fix-errors-in-scope)
465+
("A" "Derive Architecture Guardrails" ai-code-derive-architecture-guardrails)
465466
("k" "Copy Cur File Name (C-u: full)" ai-code-copy-buffer-file-name-to-clipboard)
466467
;; ("o" "Open recent file (C-u: insert)" ai-code-git-repo-recent-modified-files)
467468
("p" "Open prompt history file" ai-code-open-prompt-file)

test/test_ai-code-discussion.el

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,117 @@
447447
(ai-code-take-notes t)
448448
(should (equal captured-file "/tmp/project/.ai.code.files/test-notes.org")))))
449449

450+
(ert-deftest ai-code-test-derive-architecture-guardrails-creates-template-and-prompt ()
451+
"Test `ai-code-derive-architecture-guardrails' initializes the Org file and prompt."
452+
(let* ((tmp-root (make-temp-file "ai-code-guardrails" t))
453+
(target-file (expand-file-name ".ai.code.files/architecture/guardrails.org" tmp-root))
454+
captured-initial-prompt
455+
captured-final-prompt)
456+
(unwind-protect
457+
(cl-letf (((symbol-function 'ai-code--git-root)
458+
(lambda (&optional _dir)
459+
tmp-root))
460+
((symbol-function 'ai-code-read-string)
461+
(lambda (prompt initial-input &optional _candidate-list)
462+
(should (equal prompt "Prompt: "))
463+
(setq captured-initial-prompt initial-input)
464+
initial-input))
465+
((symbol-function 'ai-code--insert-prompt)
466+
(lambda (prompt)
467+
(setq captured-final-prompt prompt))))
468+
(ai-code-derive-architecture-guardrails)
469+
(should (file-exists-p target-file))
470+
(with-temp-buffer
471+
(insert-file-contents target-file)
472+
(should (string-match-p (regexp-quote "#+TITLE: Architecture Guardrails")
473+
(buffer-string)))
474+
(should (string-match-p (regexp-quote "* Dependency Rules")
475+
(buffer-string)))
476+
(should (string-match-p (regexp-quote "* Required Validation")
477+
(buffer-string))))
478+
(should (string-match-p (regexp-quote "Derive a lightweight architecture guardrails document")
479+
captured-initial-prompt))
480+
(should (string-match-p (regexp-quote "current code, tests, docs, and filenames")
481+
captured-initial-prompt))
482+
(should (string-match-p (regexp-quote "Do not invent an ideal architecture")
483+
captured-initial-prompt))
484+
(should (string-match-p (regexp-quote "Keep it concise")
485+
captured-initial-prompt))
486+
(should (string-match-p (regexp-quote "@.ai.code.files/architecture/guardrails.org")
487+
captured-initial-prompt))
488+
(should (string-match-p (regexp-quote "Org-mode format")
489+
captured-initial-prompt))
490+
(should (equal captured-final-prompt captured-initial-prompt)))
491+
(ignore-errors (delete-directory tmp-root t)))))
492+
493+
(ert-deftest ai-code-test-derive-architecture-guardrails-preserves-existing-file ()
494+
"Test `ai-code-derive-architecture-guardrails' does not overwrite an existing file."
495+
(let* ((tmp-root (make-temp-file "ai-code-guardrails-existing" t))
496+
(files-dir (expand-file-name ".ai.code.files/architecture" tmp-root))
497+
(target-file (expand-file-name "guardrails.org" files-dir))
498+
(existing-content "#+TITLE: Existing guardrails\n"))
499+
(unwind-protect
500+
(progn
501+
(make-directory files-dir t)
502+
(with-temp-file target-file
503+
(insert existing-content))
504+
(cl-letf (((symbol-function 'ai-code--git-root)
505+
(lambda (&optional _dir)
506+
tmp-root))
507+
((symbol-function 'ai-code-read-string)
508+
(lambda (_prompt initial-input &optional _candidate-list)
509+
initial-input))
510+
((symbol-function 'ai-code--insert-prompt)
511+
(lambda (_prompt))))
512+
(ai-code-derive-architecture-guardrails))
513+
(with-temp-buffer
514+
(insert-file-contents target-file)
515+
(should (equal (buffer-string) existing-content))))
516+
(ignore-errors (delete-directory tmp-root t)))))
517+
518+
(ert-deftest ai-code-test-derive-architecture-guardrails-errors-outside-git-repo ()
519+
"Test `ai-code-derive-architecture-guardrails' requires a git repository."
520+
(cl-letf (((symbol-function 'ai-code--git-root)
521+
(lambda (&optional _dir)
522+
nil)))
523+
(should-error (ai-code-derive-architecture-guardrails)
524+
:type 'user-error)))
525+
526+
(ert-deftest ai-code-test-derive-architecture-guardrails-reports-cancelled-request ()
527+
"Test `ai-code-derive-architecture-guardrails' reports cancellation."
528+
(let* ((tmp-root (make-temp-file "ai-code-guardrails-cancel" t))
529+
captured-message
530+
insert-called)
531+
(unwind-protect
532+
(cl-letf (((symbol-function 'ai-code--git-root)
533+
(lambda (&optional _dir)
534+
tmp-root))
535+
((symbol-function 'ai-code-read-string)
536+
(lambda (&rest _args)
537+
nil))
538+
((symbol-function 'ai-code--insert-prompt)
539+
(lambda (&rest _args)
540+
(setq insert-called t)))
541+
((symbol-function 'message)
542+
(lambda (format-string &rest args)
543+
(setq captured-message
544+
(apply #'format format-string args)))))
545+
(ai-code-derive-architecture-guardrails)
546+
(should-not insert-called)
547+
(should (equal captured-message
548+
"Architecture guardrails request cancelled")))
549+
(ignore-errors (delete-directory tmp-root t)))))
550+
551+
(ert-deftest ai-code-test-menu-source-includes-derive-architecture-guardrails-entry ()
552+
"Test the menu source exposes the architecture guardrails command."
553+
(let ((repo-root
554+
(file-name-directory (locate-library "ai-code-discussion"))))
555+
(with-temp-buffer
556+
(insert-file-contents (expand-file-name "ai-code.el" repo-root))
557+
(should (re-search-forward
558+
"(\"A\" \"Derive Architecture Guardrails\" ai-code-derive-architecture-guardrails)"
559+
nil t)))))
560+
450561
(provide 'test_ai-code-discussion)
451562

452563
;;; test_ai-code-discussion.el ends here

0 commit comments

Comments
 (0)