aboutsummaryrefslogtreecommitdiff
path: root/scripts/rename-ai-artifact.sh
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-31 12:19:34 -0500
committerCraig Jennings <c@cjennings.net>2026-05-31 12:19:34 -0500
commitddf48dc7ac780da1aacdff4e03f1d7da255b8f39 (patch)
tree99926b681a9ea6d4210d0dcd1bd8e8a6d47d7d9e /scripts/rename-ai-artifact.sh
parentb46619cd17ed4e36f2e59c1b600078521b2049ef (diff)
downloadrulesets-ddf48dc7ac780da1aacdff4e03f1d7da255b8f39.tar.gz
rulesets-ddf48dc7ac780da1aacdff4e03f1d7da255b8f39.zip
feat: add rename-ai-artifact tool and rename the drill-deck family to flashcard
Renaming an .ai artifact by hand is the kind of mechanical job that gets done incompletely: the canonical copy moves but the mirror doesn't, a reference in the INDEX is missed, a trigger phrase points at the old name. I'd also assumed a rename was costly because references scatter, when the index update is trivial and the drift check already guards it. So I built the discipline into a script instead of re-deriving it each time. scripts/rename-ai-artifact.sh takes old and new basenames, moves the file in both the canonical and mirror trees, and rewrites every reference repo-wide on a token boundary so renaming "foo" can't corrupt "foobar" or "foo-bar". It rewrites the underscore module-name variant too (a hyphenated script imported as foo_bar via importlib), leaves the archived session records under sessions/ alone because they're history, and runs workflow-integrity + sync-check at the end to prove no drift. rename-artifact.org documents it and indexes the triggers. Then I used the tool to do the rename that prompted it: the org-drill deck workflow and its helpers are now flashcard-named, since "flashcard" is the word you'd actually search for. The renamed set is flashcard-review.org plus flashcard-stats.py, flashcard-sync, flashcard-to-anki.py, and flashcard-diff-ids.py, with their tests, every reference, and the INDEX entry updated. The deck is still an org-drill deck under the hood, so the ":drill:" tag handling and the "drill deck" trigger phrases stay. I added "review/update the flashcards" alongside them. Tests: 9 bats for the rename tool (including the prefix-collision and history-preservation edges), and the renamed script suites all pass under make test.
Diffstat (limited to 'scripts/rename-ai-artifact.sh')
-rwxr-xr-xscripts/rename-ai-artifact.sh129
1 files changed, 129 insertions, 0 deletions
diff --git a/scripts/rename-ai-artifact.sh b/scripts/rename-ai-artifact.sh
new file mode 100755
index 0000000..9af6326
--- /dev/null
+++ b/scripts/rename-ai-artifact.sh
@@ -0,0 +1,129 @@
+#!/usr/bin/env bash
+# Rename an .ai artifact (a workflow or a script) across the canonical + mirror
+# trees, rewriting every reference to it and leaving archived session records
+# alone. Encodes the rename discipline so it isn't re-derived by hand each time:
+#
+# - canonical (claude-templates/.ai/) and mirror (.ai/) move in lockstep
+# - every reference to the artifact's stem is rewritten, repo-wide
+# - .ai/sessions/ (both trees) is history — never edited
+# - references match on a token boundary, so renaming `foo` can't corrupt
+# `foobar` or `foo-bar`
+# - the underscore module-name variant is rewritten too (a hyphenated script
+# imported as `foo_bar` via importlib), not just the hyphenated path
+# - workflow-integrity.py + sync-check.sh run at the end to prove no drift
+#
+# Usage: rename-ai-artifact.sh OLD-BASENAME NEW-BASENAME
+# e.g. rename-ai-artifact.sh old-workflow.org new-workflow.org
+# rename-ai-artifact.sh old-helper.py new-helper.py
+#
+# Renames ONE artifact per call; run it once per file in a family. Order within
+# a family doesn't matter — token-boundary matching keeps shared prefixes apart.
+#
+# Exit: 0 renamed (verify may still warn); 1 usage / not-found / target-exists.
+
+set -euo pipefail
+
+OLD="${1:-}"
+NEW="${2:-}"
+
+if [ -z "$OLD" ] || [ -z "$NEW" ]; then
+ echo "usage: rename-ai-artifact.sh OLD-BASENAME NEW-BASENAME" >&2
+ exit 1
+fi
+if [ "$OLD" = "$NEW" ]; then
+ echo "rename: OLD and NEW are identical ($OLD)" >&2
+ exit 1
+fi
+
+REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
+if [ -z "$REPO_ROOT" ]; then
+ echo "rename: not inside a git repository" >&2
+ exit 1
+fi
+CANON="$REPO_ROOT/claude-templates/.ai"
+MIRROR="$REPO_ROOT/.ai"
+
+# Locate OLD under the canonical tree (the source of truth). Exactly one match.
+mapfile -t hits < <(find "$CANON" -type f -name "$OLD" 2>/dev/null)
+if [ "${#hits[@]}" -eq 0 ]; then
+ echo "rename: artifact not found under canonical tree: $OLD" >&2
+ exit 1
+fi
+if [ "${#hits[@]}" -gt 1 ]; then
+ echo "rename: $OLD is ambiguous (${#hits[@]} matches under $CANON):" >&2
+ printf ' %s\n' "${hits[@]}" >&2
+ exit 1
+fi
+canon_old="${hits[0]}"
+relpath="${canon_old#"$CANON"/}" # e.g. workflows/foo.org
+reldir="$(dirname "$relpath")"
+canon_new="$CANON/$reldir/$NEW"
+mirror_old="$MIRROR/$relpath"
+mirror_new="$MIRROR/$reldir/$NEW"
+
+if [ ! -f "$mirror_old" ]; then
+ echo "rename: canonical has $relpath but the mirror copy is missing: $mirror_old" >&2
+ exit 1
+fi
+for n in "$canon_new" "$mirror_new"; do
+ if [ -e "$n" ]; then
+ echo "rename: target already exists: $n" >&2
+ exit 1
+ fi
+done
+
+# Stems drive reference rewriting: foo.org -> foo, foo-helper.py -> foo-helper,
+# a no-extension script keeps its whole name. The stem replacement also covers
+# the extensioned form (foo.org), since the "." after the stem is a boundary.
+old_stem="${OLD%.*}"
+new_stem="${NEW%.*}"
+# Python imports a hyphenated script under an underscored module name
+# (importlib.spec_from_file_location("foo_bar", "foo-bar.py")). Rewrite that
+# variant too so the module-name reference isn't left behind.
+old_us="${old_stem//-/_}"
+new_us="${new_stem//-/_}"
+
+echo "rename: $relpath"
+echo " $OLD -> $NEW (stem: $old_stem -> $new_stem)"
+
+git -C "$REPO_ROOT" mv "$canon_old" "$canon_new"
+git -C "$REPO_ROOT" mv "$mirror_old" "$mirror_new"
+echo " moved in canonical and mirror"
+
+# Rewrite references repo-wide across tracked files, except archived session
+# records (history) and the two files just renamed (already carry the new name
+# in their path; their bodies get the same stem rewrite as everything else).
+rewritten=0
+while IFS= read -r f; do
+ case "$f" in
+ */sessions/*) continue ;; # history — never edit
+ esac
+ # Token-boundary replace: not preceded/followed by an identifier char or '-'.
+ # Rewrite the hyphenated stem, then the underscored variant when it differs.
+ PERL_OLD="$old_stem" PERL_NEW="$new_stem" perl -i -pe \
+ 's/(?<![A-Za-z0-9_-])\Q$ENV{PERL_OLD}\E(?![A-Za-z0-9_-])/$ENV{PERL_NEW}/g' "$f" 2>/dev/null || true
+ if [ "$old_us" != "$old_stem" ]; then
+ PERL_OLD="$old_us" PERL_NEW="$new_us" perl -i -pe \
+ 's/(?<![A-Za-z0-9_-])\Q$ENV{PERL_OLD}\E(?![A-Za-z0-9_-])/$ENV{PERL_NEW}/g' "$f" 2>/dev/null || true
+ fi
+done < <(git -C "$REPO_ROOT" grep -lI --untracked -e "$old_stem" -e "$old_us" 2>/dev/null || true)
+
+# Count files that still changed (git sees them as modified).
+rewritten="$(git -C "$REPO_ROOT" status --porcelain | grep -c '^ *M' || true)"
+echo " rewrote references (${rewritten} file(s) modified, sessions/ left as history)"
+
+# Verify — best-effort; skips cleanly when the checkers aren't present (tests).
+status=0
+if [ -f "$REPO_ROOT/scripts/workflow-integrity.py" ]; then
+ echo " verify: workflow-integrity"
+ python3 "$REPO_ROOT/scripts/workflow-integrity.py" || status=$?
+fi
+if [ -f "$REPO_ROOT/scripts/sync-check.sh" ]; then
+ echo " verify: sync-check"
+ bash "$REPO_ROOT/scripts/sync-check.sh" || status=$?
+fi
+if [ "$status" -ne 0 ]; then
+ echo "rename: done, but a verify step reported drift — review before committing." >&2
+fi
+echo "rename: done."
+exit 0