AI guidelines for common enhancements to a Gramps feature

Yesterday, motivated by a discussion in a Discourse thread, I decided to try adding some common Gramps usability features to an Addon Tool

Surprisingly, Claude had a much harder time accomplishing the same enhancements than it did 45 days ago. But as before, it decided to build its own style of methods where it could have used Gramps API calls or re-used a debugged functionality imported from a reference built-in Gramps plug-in. I had to feed Claude examples from several different addons before it created a (mostly) error–message-free Addon.

I am wondering if we could evolve some AI “coding style reference” plug-ins for each plug-in type? So emerging Developers could reference that sample in our prompts for AIs. Where they are to follow that style and re-use that code as a plug-in needs to be enhanced from the “barest functionalities” stage of development to the “modern Gramps conveniences” stage.

Here are some conveniences that seem important to model: (green and underlined indicates future)

  1. hotlinking object text and table cells (without the underlines that add to visual clutter) to sublaunch the appropriate Object Editor with properly defined windowing parentage
  2. creating a contextual tooltip for hotlinked objects
  3. creating a dynamic context menu alternative to all click/double-click actions
  4. Context menu options for copying data to the Gramps clipboard, OS clipboard, or a styled Note (as a scratchpad)
  5. adding a Help button locked to the bottom of the dialog or Icon in the body or titlebar and linking it to the registered help_url or a specified relative/absolute URI.
  6. making the dialog content (table rows or free-form text) selectable and clipboard-ready
  7. making all table columns sortable
  8. when to add a progress bar to preempt the “Gramps is not responding : force quit or wait” at 5 busy seconds
  9. citing AI contribution

In this experiment, the idea was to evolve the Check Associations data utilities tool from what is a not-so-quick static Quick Report to a interactive navigation tool for the listed objects and Associations in the table.

I expect that the code is not at all optimized for Gramps. That requires a proficient Developer… which is far from my skill level.

Baseline: v1.1.12 Check Associations data addon Utilities tool in Gramps 5.2

v1.2.0 experiment

#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2000-2006  Donald N. Allingham
# Copyright (C) 2025  Jerome Rapinat
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#

"""Associations Statistics.

Inherited from gedcom model.

Generated-by: Claude Sonnet 4.6 (Anthropic, claude-sonnet-4-6, June 2025)
Prompts: Review associationstool.py Gramps plugin and add:
  1) Hotlinks to Person Editor (cols 1 & 4) and Association Editor (col 5)
  2) Context menu to copy row data or create a hotlinked Note
  3) Fix column sorting for cols 2, 4, 5; suppress sorting on col 3
Constraints: gramps-project.org/wiki/index.php/Howto:_Contribute_to_Gramps#AI_generated_code
             github.com/gramps-project/gramps/blob/master/AGENTS.md
"""

# ------------------------
# Python modules
# ------------------------
import logging

# ------------------------
# Gramps modules
# ------------------------
from gi.repository import Gdk, Gtk

from gramps.gen.const import GRAMPS_LOCALE as glocale
from gramps.gen.display.name import displayer as name_displayer
from gramps.gen.errors import HandleError, WindowActiveError
from gramps.gen.lib import Note, NoteType, StyledText, StyledTextTag, StyledTextTagType
from gramps.gen.relationship import get_relationship_calculator
from gramps.gui.editors import EditNote, EditPerson
from gramps.gui.display import display_url
from gramps.gui.listmodel import NOSORT, ListModel
from gramps.gui.managedwindow import ManagedWindow
from gramps.gui.plug import tool

# ------------------------
# Gramps specific
# ------------------------

try:
    _trans = glocale.get_addon_translator(__file__)
except ValueError:
    _trans = glocale.translation
_ = _trans.gettext

LOG = logging.getLogger(__name__)
LOG.setLevel(logging.WARNING)

# Visible column indices
COL_NAME1 = 0
COL_CALC = 1
COL_BULLET = 2
COL_NAME2 = 3
COL_LINK = 4

# Hidden model columns (tooltip text, person handles)
COL_TOOLTIP = 5
COL_HANDLE1 = 6
COL_HANDLE2 = 7


#------------------------------------------------------------
#
# AssociationsTool
#
#------------------------------------------------------------
class AssociationsTool(tool.Tool, ManagedWindow):
    """Tool to display all person associations in a sortable, hotlinked table."""

    def __init__(
        self,
        dbstate,
        user,
        options_class,
        name,
        callback=None,
    ):
        """
        Initialise and display the Associations tool window.

        :param dbstate: The Gramps database state object.
        :param user: The Gramps user object (carries uistate).
        :param options_class: Tool options class.
        :param name: Tool name string.
        :param callback: Optional callback (unused).
        """
        uistate = user.uistate
        self.label = _("Associations state tool")
        self.dbstate = dbstate
        tool.Tool.__init__(self, dbstate, options_class, name)
        if uistate:
            ManagedWindow.__init__(self, uistate, [], self.__class__)

        stats_list = self._build_stats_list(dbstate)

        if uistate:
            self._build_gui(stats_list)
        else:
            self._print_cli(stats_list)

    # ------------------------------------------------------------------
    # Data
    # ------------------------------------------------------------------

    def _build_stats_list(self, dbstate):
        """
        Build the list of association rows from the database.

        Each row tuple: (name1, rel, bullet, name2, value, tooltip,
        handle1, handle2)

        :param dbstate: The Gramps database state object.
        :returns: List of row tuples.
        """
        stats_list = []
        relationship = get_relationship_calculator()
        relcon = _(" to ")

        plist = dbstate.db.get_person_handles(sort_handles=True)
        for handle in plist:
            try:
                person = dbstate.db.get_person_from_handle(handle)
            except HandleError:
                LOG.warning(_("Invalid handle: %s") % handle)
                continue
            name1 = name_displayer.display(person)
            refs = person.get_person_ref_list()
            if refs:
                for ref in person.serialize()[-1]:
                    (_a, _b, _c, two, value) = ref
                    try:
                        person2 = dbstate.db.get_person_from_handle(two)
                    except HandleError:
                        LOG.warning(_("Invalid handle: %s") % two)
                        continue
                    name2 = name_displayer.display(person2)
                    rel = relationship.get_one_relationship(
                        dbstate.db, person2, person
                    )
                    tooltip = "%s %s%s%s [%s]" % (
                        _("Association:"),
                        name1,
                        relcon,
                        name2,
                        value,
                    )
                    stats_list.append(
                        (name1, rel, relcon, name2, value, tooltip, handle, two)
                    )
        return stats_list

    # ------------------------------------------------------------------
    # GUI
    # ------------------------------------------------------------------

    def _build_gui(self, stats_list):
        """
        Build and display the GTK window with the associations treeview.

        :param stats_list: List of association row data tuples.
        """
        # Hidden cols use NOSORT and width 1 so they are invisible.
        # NOSORT on the bullet column suppresses sorting clicks on it.
        titles = [
            (_("Starting Name"), COL_NAME1, 200),
            (_("Calculated"), COL_CALC, 200),
            (" • ", NOSORT, 35),
            (_("Associate"), COL_NAME2, 200),
            (_("Associate's Link type"), COL_LINK, 200),
            ("", NOSORT, 1),  # COL_TOOLTIP
            ("", NOSORT, 1),  # COL_HANDLE1
            ("", NOSORT, 1),  # COL_HANDLE2
        ]

        self._treeview = Gtk.TreeView()

        # Use a static model column for automatic per-row tooltips
        self._treeview.set_tooltip_column(COL_TOOLTIP)

        self._model = ListModel(
            self._treeview,
            titles,
            right_click=self._cb_right_click,
        )

        # row-activated gives us the column directly — cleaner than button-press
        self._treeview.connect("row-activated", self._cb_row_activated)

        for entry in stats_list:
            self._model.add(list(entry), entry[COL_NAME1])

        # Hide the internal columns
        columns = self._treeview.get_columns()
        for col_idx in (COL_TOOLTIP, COL_HANDLE1, COL_HANDLE2):
            if col_idx < len(columns):
                columns[col_idx].set_visible(False)

        # Sort state
        self._sort_col = COL_NAME1
        self._sort_ascending = True

        # Wire sortable column headers; suppress bullet + hidden cols
        for col_idx, col in enumerate(columns):
            if col_idx in (COL_BULLET, COL_TOOLTIP, COL_HANDLE1, COL_HANDLE2):
                col.set_clickable(False)
            else:
                col.set_clickable(True)
                col.connect("clicked", self._cb_column_clicked, col_idx)

        window = Gtk.Window()
        window.set_default_size(1000, 600)

        # Set transient parent to suppress Gdk "no parent" warning
        if self.uistate and self.uistate.window:
            window.set_transient_for(self.uistate.window)

        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)

        scroller = Gtk.ScrolledWindow()
        scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scroller.add(self._treeview)
        vbox.pack_start(scroller, True, True, 0)

        # Button bar at the bottom
        btn_bar = Gtk.ButtonBox(orientation=Gtk.Orientation.HORIZONTAL)
        btn_bar.set_layout(Gtk.ButtonBoxStyle.START)
        btn_bar.set_border_width(4)

        btn_help = Gtk.Button.new_with_mnemonic(_("_Help"))
        btn_help.connect("clicked", self._cb_help)
        btn_bar.pack_start(btn_help, False, False, 0)

        vbox.pack_start(btn_bar, False, False, 0)

        window.add(vbox)
        window.show_all()
        self.set_window(window, None, self.label)
        self.show()

    def _print_cli(self, stats_list):
        """
        Print associations to stdout when no UI is available.

        :param stats_list: List of association row data tuples.
        """
        print(
            "\t%s" * 5
            % (
                _("Starting Name"),
                _("Calculated"),
                " • ",
                _("Associate"),
                _("Associate's Link type"),
            )
        )
        print()
        for entry in stats_list:
            print("\t%s" * 5 % entry[:5])

    # ------------------------------------------------------------------
    # Sorting
    # ------------------------------------------------------------------

    def _cb_column_clicked(self, column, col_idx):
        """
        Handle column header click to sort by that column.

        :param column: The clicked TreeViewColumn.
        :param col_idx: Integer index of the column.
        """
        if self._sort_col == col_idx:
            self._sort_ascending = not self._sort_ascending
        else:
            self._sort_col = col_idx
            self._sort_ascending = True

        sort_order = (
            Gtk.SortType.ASCENDING
            if self._sort_ascending
            else Gtk.SortType.DESCENDING
        )

        for idx, col in enumerate(self._treeview.get_columns()):
            if idx == col_idx:
                col.set_sort_indicator(True)
                col.set_sort_order(sort_order)
            else:
                col.set_sort_indicator(False)

        self._treeview.get_model().set_sort_column_id(col_idx, sort_order)

    # ------------------------------------------------------------------
    # Selection helper
    # ------------------------------------------------------------------

    def _get_selected_row(self):
        """
        Return the full model row values for the selected row, or None.

        :returns: Tuple of 8 model values, or None.
        """
        model, tree_iter = self._treeview.get_selection().get_selected()
        if not tree_iter:
            return None
        return tuple(model.get_value(tree_iter, col) for col in range(8))

    # ------------------------------------------------------------------
    # Double-click via row-activated (GTK passes path + column directly)
    # ------------------------------------------------------------------

    def _cb_row_activated(self, treeview, path, column):
        """
        Route a double-click to the correct editor based on which column was clicked.

        GTK's row-activated signal fires on double-click and provides the exact
        column that was activated, so no coordinate math is needed.

        :param treeview: The TreeView widget.
        :param path: Gtk.TreePath of the activated row.
        :param column: Gtk.TreeViewColumn that was double-clicked.
        """
        columns = treeview.get_columns()
        col_idx = columns.index(column)
        tree_iter = self._model.model.get_iter(path)
        if tree_iter is None:
            return
        handle1 = self._model.model.get_value(tree_iter, COL_HANDLE1)
        handle2 = self._model.model.get_value(tree_iter, COL_HANDLE2)

        if col_idx == COL_NAME2:
            self._open_person_editor(handle2)
        elif col_idx == COL_LINK:
            self._open_association_editor(handle1, handle2)
        else:
            # COL_NAME1, COL_CALC, COL_BULLET — open the starting person
            self._open_person_editor(handle1)



    # ------------------------------------------------------------------
    # Right-click context menu (ListModel right_click)
    # ------------------------------------------------------------------

    def _cb_right_click(self, treeview, event):
        """
        Show a context menu on right-click.

        :param treeview: The TreeView widget.
        :param event: The button event.
        """
        row = self._get_selected_row()
        has_row = row is not None

        menu = Gtk.Menu()
        menu.set_reserve_toggle_size(False)

        entries = [
            (_("Edit Starting Person"), self._cb_edit_person1, has_row),
            (_("Edit Associate Person"), self._cb_edit_person2, has_row),
            (_("Edit Association"), self._cb_edit_association, has_row),
            (None, None, 0),
            (_("Copy row to clipboard"), self._cb_copy_row, has_row),
            (_("Create Note from row"), self._cb_create_note, has_row),
        ]

        for title, callback, sensitive in entries:
            if title is None:
                item = Gtk.SeparatorMenuItem()
            else:
                item = Gtk.MenuItem(label=title)
                if callback:
                    item.connect("activate", callback)
                item.set_sensitive(sensitive)
            item.show()
            menu.append(item)

        menu.popup_at_pointer(event)

    # ------------------------------------------------------------------
    # Context menu callbacks
    # ------------------------------------------------------------------

    def _cb_edit_person1(self, obj):
        """
        Open Person Editor for the Starting Name of the selected row.

        :param obj: Menu item (unused).
        """
        row = self._get_selected_row()
        if row:
            self._open_person_editor(row[COL_HANDLE1])

    def _cb_edit_person2(self, obj):
        """
        Open Person Editor for the Associate of the selected row.

        :param obj: Menu item (unused).
        """
        row = self._get_selected_row()
        if row:
            self._open_person_editor(row[COL_HANDLE2])

    def _cb_edit_association(self, obj):
        """
        Open the Association Editor for the selected row.

        :param obj: Menu item (unused).
        """
        row = self._get_selected_row()
        if row:
            self._open_association_editor(row[COL_HANDLE1], row[COL_HANDLE2])

    def _cb_copy_row(self, obj):
        """
        Copy the selected row display text to the system clipboard.

        :param obj: Menu item (unused).
        """
        row = self._get_selected_row()
        if not row:
            return
        text = "\t".join(str(row[col]) for col in range(5))
        clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
        clipboard.set_text(text, -1)

    def _cb_create_note(self, obj):
        """
        Create a Note pre-populated with row data and open the Note Editor.

        The note body lists each field on its own line so the user can read it
        easily. Person handles are rendered as gramps:// deep-link URLs on
        separate lines; the Note editor renders these as clickable hyperlinks.

        :param obj: Menu item (unused).
        """
        row = self._get_selected_row()
        if not row:
            return

        name1 = row[COL_NAME1]
        rel = row[COL_CALC]
        relcon = row[COL_BULLET]
        name2 = row[COL_NAME2]
        value = row[COL_LINK]
        handle1 = row[COL_HANDLE1]
        handle2 = row[COL_HANDLE2]

        url1 = "gramps://Person/handle/%s" % handle1
        url2 = "gramps://Person/handle/%s" % handle2

        # Build note text with styled hyperlinks on the person name spans.
        # Construct each segment explicitly so character offsets are exact.
        seg_header  = _("Association:") + "\n"
        seg_p1_pre  = _("Starting Person") + ": "
        # name1 will be the linked span
        seg_mid     = "\n" + _("Relationship") + ": "
        # name2 will be the linked span
        seg_suffix  = " (%s%s)\n" % (rel, relcon) + _("Link type") + ": " + value

        raw_text = seg_header + seg_p1_pre + name1 + seg_mid + name2 + seg_suffix

        off1_start = len(seg_header) + len(seg_p1_pre)
        off1_end   = off1_start + len(name1)
        off2_start = off1_end + len(seg_mid)
        off2_end   = off2_start + len(name2)

        tag1 = StyledTextTag(
            StyledTextTagType.LINK,
            url1,
            [(off1_start, off1_end)],
        )
        tag2 = StyledTextTag(
            StyledTextTagType.LINK,
            url2,
            [(off2_start, off2_end)],
        )
        styled = StyledText(raw_text, [tag1, tag2])

        note = Note()
        note.set_styledtext(styled)
        note.set_type(NoteType.GENERAL)

        EditNote(self.dbstate, self.uistate, [], note)

    # ------------------------------------------------------------------
    # Editor helpers
    # ------------------------------------------------------------------

    def _open_person_editor(self, handle):
        """
        Open the Person Editor for the given handle.

        :param handle: The Gramps person handle string.
        """
        try:
            person = self.dbstate.db.get_person_from_handle(handle)
            EditPerson(self.dbstate, self.uistate, [], person)
        except HandleError:
            LOG.warning(_("Invalid handle: %s") % handle)
        except WindowActiveError:
            pass

    def _open_association_editor(self, handle1, handle2):
        """
        Open the Association Editor for the association between two people.

        :param handle1: Handle of the primary person.
        :param handle2: Handle of the associated person.
        """
        try:
            person = self.dbstate.db.get_person_from_handle(handle1)
        except HandleError:
            LOG.warning(_("Invalid handle: %s") % handle1)
            return

        for ref in person.get_person_ref_list():
            if ref.ref == handle2:
                from gramps.gui.editors import EditPersonRef  # noqa: PLC0415

                EditPersonRef(
                    self.dbstate,
                    self.uistate,
                    [],
                    ref,
                    self._noop_callback,
                )
                return
        LOG.warning(
            _("No association found between %(h1)s and %(h2)s")
            % {"h1": handle1, "h2": handle2}
        )

    def _noop_callback(self, *args):
        """
        No-op callback placeholder for editor callbacks.

        :param args: Ignored arguments.
        """

    def _cb_help(self, obj):
        """
        Open the addon wiki page in the default web browser.

        :param obj: Button widget (unused).
        """
        display_url(
            "https://www.gramps-project.org/wiki/index.php/Addon:Check_Associations"
        )

    def build_menu_names(self, obj):
        """
        Return menu names for the managed window.

        :param obj: Ignored.
        :returns: Tuple of (label, None).
        """
        return (self.label, None)


#------------------------------------------------------------
#
# AssociationsToolOptions
#
#------------------------------------------------------------
class AssociationsToolOptions(tool.ToolOptions):
    """Defines options and provides handling interface."""

    def __init__(self, name, person_id=None):
        """
        Initialise tool options.

        :param name: Tool name.
        :param person_id: Optional active person Gramps ID.
        """
        tool.ToolOptions.__init__(self, name, person_id)

Sounds like AGENTS.md would be a good place for this in the addons-source repo. You already have a set of instructions you can start with.

@emyoulation how are you using Claude? Through a coding environment (like Claude Code) or through something else, like a chat interface?

Normally an AI environment will understand the conventions of a codebase by looking at other examples. But, as @codefarmer points out, the proper place for this (if needed) is in the AGENTS.md in the root folder of the project. (Not all AI agents will read the file, so you might have to tell it to, or symlink/copy it to a file known by the agent (eg CLAUDE.md)).

But every word you put in the AGENTS.md file increases the token count (and money and time). So we shouldn’t add it if it isn’t necessary.

BTW, that code would be pretty time-consuming on a large database. Rather than computing all relationships between all people and all of their associations, it probably would be better to only do the selected person.

[I doubt an AI would suggest the reduced scope, which is why we still need humans (for now).]

I’ve been working on AI guidelines, you can find a draft on the wiki - also available as markdown if you like. I think we could - and should - expand this with more items. In fact, I think this is where more and more of the developer work will migrate to as AI usage expands.

I agree. When your tree grows, it becomes a problem that most of the Tools and all the Dashboard gramplets are scoped to the WHOLE tree. And usually users have a scope with a much more narrow focus.

Since using the tool on my tree of 59k+ persons took 15 seconds to build an 85 Association record list, I asked Copilot to add a progress dialog. (It choose the busiest variant by default… which refreshes FAR too often and adds burden to the process.) And it took a while to get that dialog to preempt the “Gramps is not responsive” dialog but not choke when the tree is tiny and fast to process.

Also, an Association Gramplet with a scope of the Selected person (or a slightly broader focus of all the Persons of any family where the Person is a child or Spouse) would be sufficient to cover most needs.

I am probably using it in the most inefficient manner: using Claude.ai’s web chat interface as an impromptu remote development environment — essentially treating the conversation as a stateful editor and test loop.

Copilot’s 1.2.1 revision to reduce Claude’s complexity:
AssociationsTool.addon.tgz (12.1 KB)

v1.2.1 Release Summary

associationstool.gpr.py — Registration file

  • Version 1.2.1
  • Gramps 5.2+ compatible
  • Authors: Jerome Rapinat, Brian McCullough
  • Proper copyright attribution for 2026 per Agents.md guidelines

associationstool.py — Main tool implementation

  • Full feature set retained (context menu, styled notes, sorting)
  • Progress dialog for large databases (65,000+ persons)
  • Status bar with association count
  • Help button with dynamic wiki URL
  • All Gramps API calls (backend-agnostic)
  • Proper AGENTS.md compliance
  • Zero threading issues (main thread only)
  • Type hints, docstrings, import organization

README.md — User documentation

  • Updated with progress dialog info
  • Documents status bar features
  • Clear usage instructions
  • See also section with links

Key Features Locked Down

:white_check_mark: Double-click editing (person or association)
:white_check_mark: Right-click context menu (6 options including styled notes)
:white_check_mark: Column sorting (all columns except bullet)
:white_check_mark: Progress feedback for large trees (appears after 1 second)
:white_check_mark: Status bar with count (bottom-right)
:white_check_mark: Help button (bottom-left, uses wiki URL)
:white_check_mark: Tooltip on hover
:white_check_mark: Works with any Gramps DB backend
:white_check_mark: CLI and GUI modes

Testing Checklist for Distribution

  • Works with 5-person test tree (instant results)
  • No “unresponsive” dialogs on large databases
  • Window appears immediately on small trees
  • Progress dialog appears after 1 second on large trees
  • All editors open correctly
  • Context menu works
  • Styled note creation works with hyperlinks
  • Status count accurate
  • Help button opens wiki page

Yes, please! Adding addons that add 15 seconds to the render time should not be the default. I know there are a lot of them, and I hope to start working on those to either speed them up, or give a warning that “This addon is computationally expensive for your specific tree. Remove it from view?”

Currently, on my 100k tree, I just remove the bottom bar. But with all of the new enhancements waiting to be merged, 100k people tree is very snappy!

Maybe we can add some guidelines about performance as well (measure, consider, test, use raw data, etc). Or maybe another setting in gpr.py files that state their complexity like O(n), O(n **2), O(log n), etc? We need to raise the awareness for developers on efficiency and complexity.

My last contribution to this addon, was seven years ago… Anyway, I just re-used one common way for displaying statistic lists and the Gtk ListModel. So, more you generate columns and more the time process will increase (a lot). Some limitations, but it sounded valid for a simple data retrieve process, seven years ago.

It is true that the same method has been used on a second addon (re-use of code), which has been reviewed on 2025. I guess, your AI made a mixup and was able to match both romjerome experimentations (or match the style!), then added a date and copyright?

About this thread and AI guidelines, I can share my workflow when I tried to update the second addon with the same (old and un-optimized) method (ListModel).

  1. I asked AI (was a Mistral model) to help me for refactoring the old code and to improve it with recent built-in python modules. Like before for any update on old code or method.
  2. Performances issues were more and less identified in the past. So, maybe either set some filter rules or to improve data handling. Filtering does not make sense by only looking at all associations fields into the db, but it was useful on the second addon one. Just asked AI, how to improve performances. Improvements were the use of lru_cache and some static methods. This was not too complicated and easy to understand.
  3. Then I had to “fight” with the AI because I was into a “no end process” as the “assistant” always proposed new features or improvements… So, my AI guidelines sounded more like a set of hijacked prompts, like counting any character into the code or so on, then re- focusing on the real assistance!
  4. I found that some technical parts of the proposed improvements were only a copy of an existing code used by some filter rules (2011, by Robert Cheramy). So, AI only looked at gramps core source code and it did not really provide code with copyrights or licence issues.

There was no guidelines for an automatic review of code by AI. Once I found the same logic and code into an other gramps module, I just added, by myself, copyrights line with the reference from 2011.

By only pointing to gramps project github repositories, I do not need advanced configurations for AI, like RAG, LLM, premium account or so.

The optimized version of the addon - the review in 2025 - uses batch and generators, something like:

     # Traitement des résultats avec le générateur
     count = 0
     batch_size = 50  # Taille du lot pour les mises Ă  jour par batch
     batch_entries = []

     # Traitement des résultats avec le générateur
     for result_entry, name in generate_results():
         # Ajoute le résultat à la liste et au modèle
         count += 1
         self.stats_list.append(result_entry)

         if uistate:
             batch_entries.append((result_entry, int(result_entry[0])))
             # Mise à jour par lots pour améliorer les performances
             if len(batch_entries) >= batch_size:
                 GLib.idle_add(self._add_batch_to_model, batch_entries)
                 batch_entries = []

             # Mise Ă  jour de la progression
             if count % 100 == 0:
                 self.progress.set_header("%d/%d" % (count, len(self.filtered_list)))

     # Ajouter les entrées restantes (si le batch n'est pas plein)
     if uistate and batch_entries:
         GLib.idle_add(self._add_batch_to_model, batch_entries)
         self.show()

I guess this might be also used on the old CheckAssociations addon tool and large database?

Any guidelines for AI would not increase the quality of this code (during my review), because my prompts were only for assistance, so the AI only helps me to format some parts as the use was limited to a review and not for generating a new code from scratch. Sure, I kept the control to avoid a large refactoring with any modern python libs. So, I can maintain the code and the primary ideas are still behind (ways, logic & co).

One of my policy rule during this review was to check if AI does not try to add or remove some old comments into the addon! They are my own internal guideline.

Note, after update (new version on new gramps branch), I also set the addon to a “limited” audience and scary/freaky status as well as moved to the listing files.

include_in_listing=True,
status=EXPERIMENTAL,
audience = DEVELOPER,

So, no more a beta code, could be used, close to a proof of concept, with maintenance but no power features, optimized but with experimental methods (filters support on tools) or ways.