diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-31 12:19:34 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-31 12:19:34 -0500 |
| commit | ddf48dc7ac780da1aacdff4e03f1d7da255b8f39 (patch) | |
| tree | 99926b681a9ea6d4210d0dcd1bd8e8a6d47d7d9e /scripts/rename-ai-artifact.sh | |
| parent | b46619cd17ed4e36f2e59c1b600078521b2049ef (diff) | |
| download | rulesets-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-x | scripts/rename-ai-artifact.sh | 129 |
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 |
