[Markdown] Tools for working with agents.
fix Keep bare and brace-grouped tags in the vocabulary
rail-tools--tag-axes only understood [ ] groups. Tags declared outside a group, or inside a { } group, were dropped, so a file that used them silently accepted any tag. Handle both group markers, and collect ungrouped tags under a misc axis. Add a test for the mixed case. Also declare the two MCP framework functions for the byte compiler, require rail-tools from the test file, silence Org state-change messages in the test fixture, and update the copyright year.
Changed files
skills/rail/rail-tests.el
@@ -15,6 +15,7 @@
15
15
16
16
(require 'ert)
17
17
(require 'json)
18
Added:
(require 'rail-tools)
18
19
19
20
;;; Fixtures
20
21
@@ -53,13 +54,15 @@
53
54
54
55
(defmacro rail-tests--with-stream (root &rest body)
55
56
"Create a temporary project directory, bind ROOT to it, then run BODY.
56
Removed:
Delete the directory and its buffers after BODY."
57
Added:
Delete the directory and its buffers after BODY. Silence the messages
58
Added:
that Org prints on each state change, so the ERT output stays readable."
57
59
(declare (indent 1))
58
60
`(let* ((,root (file-name-as-directory (make-temp-file "rail-test" t)))
59
61
(file (expand-file-name rail-stream-file-name ,root))
60
62
(enable-local-variables :all)
61
63
(org-id-track-globally nil)
62
Removed:
(create-lockfiles nil))
64
Added:
(create-lockfiles nil)
65
Added:
(inhibit-message t))
63
66
(unwind-protect
64
67
(progn
65
68
(with-temp-file file (insert rail-tests--preamble))
@@ -423,6 +426,37 @@
423
426
(should (equal (mapcar #'car axes) '(kind scope impact)))
424
427
(should (equal (append (alist-get 'impact axes) nil) '("minor" "major")))
425
428
(should (equal (append (alist-get 'scope axes) nil) '("core" "app" "web" "ui"))))))
429
Added:
430
Added:
(ert-deftest rail-test-inspect-collects-bare-tags-under-misc ()
431
Added:
"Tags outside a named group land under the `misc' axis, and still count.
432
Added:
Org parses `{ }' groups and `[ ]' groups into different markers. A
433
Added:
group with no group tag has no name. The tool must keep every declared
434
Added:
tag in the vocabulary, so an undeclared tag is still refused."
435
Added:
(let ((rail-tests--preamble
436
Added:
(concat "#+TITLE: Mixed vocabulary\n"
437
Added:
"#+TODO: TODO | DONE\n"
438
Added:
"#+TAGS: [ Kind : feat fix ]\n"
439
Added:
"#+TAGS: { Scope : core app }\n"
440
Added:
"#+TAGS: { alpha beta }\n"
441
Added:
"#+TAGS: loose other\n")))
442
Added:
(rail-tests--with-stream root
443
Added:
(let* ((result (rail-tests--call #'rail-tools--inspect-handler
444
Added:
(list (cons 'root root))))
445
Added:
(axes (alist-get 'tags result)))
446
Added:
(should (equal (mapcar #'car axes) '(kind scope misc)))
447
Added:
(should (equal (append (alist-get 'scope axes) nil) '("core" "app")))
448
Added:
(should (equal (append (alist-get 'misc axes) nil)
449
Added:
'("alpha" "beta" "loose" "other"))))
450
Added:
;; A bare tag is part of the vocabulary.
451
Added:
(should (stringp (rail-tests--capture root "Bare tag" ["loose"])))
452
Added:
;; An undeclared tag is still refused, because the file does declare
453
Added:
;; a vocabulary.
454
Added:
(let ((result (rail-tests--decode
455
Added:
(rail-tools--capture-handler
456
Added:
(list (cons 'root root) (cons 'title "Bad tag")
457
Added:
(cons 'tags ["undeclared"])
458
Added:
(cons 'body "Body."))))))
459
Added:
(should (stringp (alist-get 'error result)))))))
426
460
427
461
(ert-deftest rail-test-a-file-without-tags-accepts-any-tag ()
428
462
"A file that declares no #+TAGS: vocabulary accepts any tag.
skills/rail/rail-tools.el
@@ -1,6 +1,6 @@
1
1
;;; rail-tools.el --- RAIL MCP tools for RAIL.org -*- lexical-binding: t; -*-
2
2
3
Removed:
;; Copyright (C) 2025
3
Added:
;; Copyright (C) 2026
4
4
5
5
;; This file is NOT part of GNU Emacs.
6
6
@@ -67,6 +67,12 @@
67
67
(defconst rail-mcp-available (require 'mcp-server-tools nil t)
68
68
"Non-nil when the Emacs MCP tool framework is available.")
69
69
70
Added:
;; Tell the byte compiler about the two framework entry points. The
71
Added:
;; stubs below define them at run time when the framework is absent, but
72
Added:
;; the compiler does not see inside the `unless' form.
73
Added:
(declare-function make-mcp-server-tool "mcp-server-tools")
74
Added:
(declare-function mcp-server-register-tool "mcp-server-tools")
75
Added:
70
76
(unless rail-mcp-available
71
77
;; Define plain functions, never a struct. A stub struct would clobber the
72
78
;; real slot layout if the framework loads later in the same session.
@@ -286,27 +292,42 @@
286
292
287
293
(defun rail-tools--tag-axes ()
288
294
"Return the tag vocabulary of the current buffer, grouped by axis.
289
Removed:
Read the group tags that Org parsed from the `#+TAGS:' lines into
295
Added:
Read the tags that Org parsed from the `#+TAGS:' lines into
290
296
`org-current-tag-alist'. Return an alist that maps each axis symbol to
291
Removed:
its list of tag strings. Return nil when the file declares no axis, and
292
Removed:
then the file accepts any tag."
293
Removed:
(let ((axes '()) (current nil))
297
Added:
its list of tag strings.
298
Added:
299
Added:
A named group, such as `[ Kind : feat fix ]' or `{ Kind : feat fix }',
300
Added:
gives one axis named after its group tag, in lower case. Tags outside
301
Added:
any group, and tags in a group with no group tag, land under the axis
302
Added:
`misc'. Return nil when the file declares no tag at all, and then the
303
Added:
file accepts any tag."
304
Added:
(let ((axes '()) (misc '())
305
Added:
(in-group nil) (named nil) (current nil))
294
306
(dolist (entry org-current-tag-alist)
295
307
(pcase entry
296
Removed:
(`(:startgrouptag) (setq current nil))
297
Removed:
(`(:endgrouptag)
298
Removed:
(when current
308
Added:
((or `(:startgroup) `(:startgrouptag))
309
Added:
(setq in-group t named nil current nil))
310
Added:
(`(:grouptags)
311
Added:
(setq named t))
312
Added:
((or `(:endgroup) `(:endgrouptag))
313
Added:
(cond
314
Added:
((and named current)
315
Added:
;; The first tag in a named group is the axis name.
299
316
(push (cons (intern (downcase (car current)))
300
317
(nreverse (cdr current)))
301
318
axes))
302
Removed:
(setq current nil))
303
Removed:
(`(:grouptags))
319
Added:
(current
320
Added:
(setq misc (append (reverse current) misc))))
321
Added:
(setq in-group nil named nil current nil))
304
322
(`(,(and tag (pred stringp)) . ,_)
305
Removed:
(if current
306
Removed:
(setcdr current (cons tag (cdr current)))
307
Removed:
;; The first tag in a group is the axis name.
308
Removed:
(setq current (cons tag '()))))))
309
Removed:
(nreverse axes)))
323
Added:
(cond
324
Added:
((not in-group) (push tag misc))
325
Added:
((null current) (setq current (list tag)))
326
Added:
(t (push tag (cdr current)))))))
327
Added:
(setq axes (nreverse axes))
328
Added:
(if misc
329
Added:
(append axes (list (cons 'misc (nreverse misc))))
330
Added:
axes)))
310
331
311
332
(defun rail-tools--all-tags ()
312
333
"Return every tag the current buffer declares, as one flat list.