chore(migration): preflight script + rename-aware plugin map

This commit is contained in:
Kjell Tore Guttormsen 2026-06-17 12:29:33 +02:00
commit f4c36fcda5
3 changed files with 329 additions and 0 deletions

View file

@ -0,0 +1,134 @@
#!/usr/bin/env bash
# Step 1 — Preflight, archive tag, and the plugin map.
# Asserts tooling + repo hygiene, builds the dedicated extraction mirror ($WORK/_mirror)
# via `git clone --no-local` (R4/M6 — extraction never reads the working checkout),
# enumerates >1MB blob-strip candidates in ms-ai-architect and emits blob_strip_safe
# into plugin-map.json (M4/F3), and creates the local archive tag (D2).
# NULL push (D8): never pushes, never mutates plugins/ shared/ marketplace.json.
#
# Usage: 00-preflight.sh [--dry-run]
# --dry-run : validate tooling + map + print the 11-target table; NO mirror, NO tag, NO map mutation.
#
# Bash 3.2 compatible (no associative arrays / mapfile). Override WORK= to relocate the workspace.
set -euo pipefail
DRY_RUN=0
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
*) echo "preflight: unknown arg: $arg" >&2; exit 2 ;;
esac
done
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MAP="$SCRIPT_DIR/plugin-map.json"
REPO_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel)"
WORK="${WORK:-/tmp/polyrepo-migration}"
MIRROR="$WORK/_mirror"
ARCHIVE_TAG="pre-polyrepo-archive"
BASELINE="f2d41c8"
fail() { printf 'PREFLIGHT FAIL: %s\n' "$*" >&2; exit 1; }
# --- tooling ---
git filter-repo --version >/dev/null 2>&1 || fail "git filter-repo not available — brew install git-filter-repo"
command -v python3 >/dev/null 2>&1 || fail "python3 not found"
python3 -c 'import sys; sys.exit(0 if sys.version_info[:2] >= (3, 6) else 1)' \
|| fail "python3 >= 3.6 required"
# --- map presence + parse + count ---
[ -f "$MAP" ] || fail "plugin-map.json missing at $MAP"
python3 -c "import json; json.load(open('$MAP'))" >/dev/null 2>&1 || fail "plugin-map.json does not parse"
TARGET_COUNT="$(python3 -c "import json; print(len(json.load(open('$MAP'))['targets']))")"
[ "$TARGET_COUNT" = "11" ] || fail "expected 11 targets in plugin-map.json, found $TARGET_COUNT"
# --- repo baseline (HEAD descends from the ratified brief commit, on main, not behind remote) ---
BRANCH="$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD)"
[ "$BRANCH" = "main" ] || fail "not on main (HEAD on '$BRANCH')"
git -C "$REPO_ROOT" merge-base --is-ancestor "$BASELINE" HEAD \
|| fail "HEAD does not descend from baseline $BASELINE"
if git -C "$REPO_ROOT" rev-parse --verify -q origin/main >/dev/null; then
BEHIND="$(git -C "$REPO_ROOT" rev-list --count HEAD..origin/main)"
[ "$BEHIND" = "0" ] || fail "behind origin/main by $BEHIND commit(s) — pull/rebase before migrating"
AHEAD="$(git -C "$REPO_ROOT" rev-list --count origin/main..HEAD)"
echo " baseline: descends $BASELINE on main; $AHEAD local commit(s) ahead of origin/main (unpushed — expected under D8)"
else
echo " baseline: descends $BASELINE on main (no origin/main ref present)"
fi
# --- hygiene (M12): tolerate untracked stray docs; abort only on a TRACKED uncommitted
# change to a migration-sensitive surface. The literal `git status --porcelain plugins ...`
# would also list untracked (??) files, which M12 explicitly tolerates, so we filter them. ---
DIRTY="$(git -C "$REPO_ROOT" status --porcelain plugins shared .claude-plugin/marketplace.json scripts \
| grep -v '^??' || true)"
[ -z "$DIRTY" ] || fail "uncommitted change to a migration-sensitive surface:
$DIRTY"
# --- print the 11-target table ---
echo "PREFLIGHT OK"
python3 - "$MAP" <<'PY'
import json, sys
m = json.load(open(sys.argv[1]))
t = m["targets"]
print(" %-26s %-9s %-5s %-7s %-5s %s" % ("target", "tag", "paths", "vendor", "blob", "repo_url"))
for k in sorted(t):
e = t[k]
print(" %-26s %-9s %-5d %-7s %-5s %s" % (
k, e["tag"], len(e["paths"]), str(e["has_vendor_ds"]), str(e.get("blob_strip", False)), e["repo_url"]))
print(" drop:", ", ".join(m.get("drop", [])) or "(none)")
print(" targets:", len(t))
PY
if [ "$DRY_RUN" = "1" ]; then
echo " (--dry-run: no mirror clone, no archive tag, no map mutation)"
exit 0
fi
# --- full run: build the dedicated extraction mirror once (R4/M6) ---
mkdir -p "$WORK"
if [ -e "$MIRROR/HEAD" ] || [ -d "$MIRROR/.git" ]; then
echo " mirror: present at $MIRROR (reused)"
else
rm -rf "$MIRROR"
git clone --no-local --quiet "$REPO_ROOT" "$MIRROR"
echo " mirror: built $MIRROR via clone --no-local (pinned at $(git -C "$MIRROR" rev-parse --short HEAD))"
fi
# --- enumerate >1MB blobs in ms-ai-architect history; safe iff all are screenshots (M4/F3) ---
BLOBLIST="$(git -C "$MIRROR" rev-list --objects --all \
| git -C "$MIRROR" cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
| awk '$1=="blob" && $3+0>1048576 { p=""; for (i=4;i<=NF;i++) p = p (i>4?" ":"") $i; print $3"\t"p }' \
| grep -E 'plugins/ms-ai-architect/' || true)"
SAFE=true
if [ -n "$BLOBLIST" ]; then
NONSCREEN="$(printf '%s\n' "$BLOBLIST" | grep -vE 'plugins/ms-ai-architect/playground/screenshots/' || true)"
[ -z "$NONSCREEN" ] || SAFE=false
fi
echo " blob-strip candidates (>1MB) in ms-ai-architect history:"
if [ -n "$BLOBLIST" ]; then printf '%s\n' "$BLOBLIST" | sed 's/^/ /'; else echo " (none)"; fi
echo " blob_strip_safe = $SAFE"
# --- idempotent emit into plugin-map.json (only rewrites if the value actually changed) ---
python3 - "$MAP" "$SAFE" <<'PY'
import json, sys
path, safe = sys.argv[1], (sys.argv[2] == "true")
m = json.load(open(path))
cur = m["targets"].get("ms-ai-architect", {}).get("blob_strip_safe")
if cur != safe:
m["targets"]["ms-ai-architect"]["blob_strip_safe"] = safe
with open(path, "w") as f:
f.write(json.dumps(m, indent=2) + "\n")
print(" plugin-map.json: blob_strip_safe set to", safe)
else:
print(" plugin-map.json: blob_strip_safe already", safe, "(no change)")
PY
# --- create the local annotated archive tag idempotently (D2; pushed later, in the window) ---
if git -C "$REPO_ROOT" rev-parse -q --verify "refs/tags/$ARCHIVE_TAG" >/dev/null; then
echo " tag: $ARCHIVE_TAG already exists (idempotent)"
else
git -C "$REPO_ROOT" tag -a "$ARCHIVE_TAG" -m "Archive of the monorepo before the polyrepo split (D2)"
echo " tag: created $ARCHIVE_TAG at HEAD (local only — pushed in the operator window)"
fi
echo "PREFLIGHT COMPLETE"

View file

@ -0,0 +1,55 @@
// Step 1 test — validates plugin-map.json shape (rename-awareness, HTTPS URLs, blob/vendor flags).
// Pattern: plugins/voyage/tests/validators/*.test.mjs (node:test style, zero deps).
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const here = dirname(fileURLToPath(import.meta.url));
const map = JSON.parse(readFileSync(join(here, 'plugin-map.json'), 'utf8'));
const targets = map.targets;
const keys = Object.keys(targets);
const RENAMED = ['voyage', 'llm-security', 'linkedin-studio'];
test('plugin-map.json parses and has exactly 11 targets (10 plugins + DS)', () => {
assert.equal(keys.length, 11, `expected 11 targets, found ${keys.length}: ${keys.join(', ')}`);
});
test('the 3 renamed plugins each carry >=2 --path entries (F1)', () => {
for (const k of RENAMED) {
assert.ok(targets[k], `missing renamed target ${k}`);
assert.ok(
Array.isArray(targets[k].paths) && targets[k].paths.length >= 2,
`${k} must carry >=2 paths (both names), has ${targets[k].paths?.length}`
);
}
});
test('every target repo_url is https:// (not ssh://) — F2 / #9740', () => {
for (const k of keys) {
assert.match(targets[k].repo_url, /^https:\/\/git\.fromaitochitta\.com\/open\//, `${k} repo_url not the expected HTTPS form`);
assert.doesNotMatch(targets[k].repo_url, /^ssh:\/\//, `${k} repo_url must not be ssh://`);
}
});
test('ms-ai-architect has blob_strip: true (F3)', () => {
assert.equal(targets['ms-ai-architect'].blob_strip, true);
});
test('only llm-security + ms-ai-architect have has_vendor_ds: true', () => {
const vendor = keys.filter((k) => targets[k].has_vendor_ds === true).sort();
assert.deepEqual(vendor, ['llm-security', 'ms-ai-architect']);
});
test('every target tag is v<semver> (F5 single clean re-tag per repo)', () => {
for (const k of keys) {
assert.match(targets[k].tag, /^v\d+\.\d+\.\d+$/, `${k} tag '${targets[k].tag}' is not v<semver>`);
}
});
test('the removed plugin is dropped, not extracted', () => {
assert.ok(Array.isArray(map.drop) && map.drop.includes('plugins/ultra-cc-architect/'));
assert.ok(!keys.includes('ultra-cc-architect'));
});

View file

@ -0,0 +1,140 @@
{
"_comment": "Rename-aware extraction map for the polyrepo migration. Tags come from each plugin's .claude-plugin/plugin.json version (verified 2026-06-17), NOT from README/CLAUDE.md (which the migration corrects, Step 9). 11 targets = 10 plugins + the design-system. URLs are HTTPS (F2, #9740). Renamed plugins carry >=2 paths (F1). blob_strip_safe is (re)computed by 00-preflight.sh against the mirror history.",
"drop": ["plugins/ultra-cc-architect/"],
"targets": {
"ai-psychosis": {
"paths": ["plugins/ai-psychosis/"],
"path_renames": { "plugins/ai-psychosis/": "" },
"tag": "v1.2.0",
"repo_url": "https://git.fromaitochitta.com/open/ai-psychosis.git",
"has_vendor_ds": false,
"blob_strip": false,
"blob_strip_safe": null,
"test_cmd": "node --test 'tests/**/*.test.mjs'",
"standalone_caveat": "runs from repo root (relative-fixture cwd)"
},
"claude-design": {
"paths": ["plugins/claude-design/"],
"path_renames": { "plugins/claude-design/": "" },
"tag": "v0.1.0",
"repo_url": "https://git.fromaitochitta.com/open/claude-design.git",
"has_vendor_ds": false,
"blob_strip": false,
"blob_strip_safe": null,
"test_cmd": "bash tests/validate-plugin.sh",
"standalone_caveat": "structure check via validate-plugin.sh (no unit suite)"
},
"config-audit": {
"paths": ["plugins/config-audit/"],
"path_renames": { "plugins/config-audit/": "" },
"tag": "v5.1.0",
"repo_url": "https://git.fromaitochitta.com/open/config-audit.git",
"has_vendor_ds": false,
"blob_strip": false,
"blob_strip_safe": null,
"test_cmd": "node --test 'tests/**/*.test.mjs'",
"standalone_caveat": "SC2 gate = full tests/**/*.test.mjs MINUS json-backcompat + raw-backcompat (frozen back-compat snapshots embed the original machine's absolute path + sibling marketplace + deleted plugins); re-seed snapshot-default-output via UPDATE_SNAPSHOT=1 in-clone (Step 7)"
},
"graceful-handoff": {
"paths": ["plugins/graceful-handoff/"],
"path_renames": { "plugins/graceful-handoff/": "" },
"tag": "v2.1.0",
"repo_url": "https://git.fromaitochitta.com/open/graceful-handoff.git",
"has_vendor_ds": false,
"blob_strip": false,
"blob_strip_safe": null,
"test_cmd": "node --test 'tests/**/*.test.mjs'",
"standalone_caveat": "pilot candidate (low-churn, has tests, no DS, no blob bloat)"
},
"human-friendly-style": {
"paths": ["plugins/human-friendly-style/"],
"path_renames": { "plugins/human-friendly-style/": "" },
"tag": "v1.1.0",
"repo_url": "https://git.fromaitochitta.com/open/human-friendly-style.git",
"has_vendor_ds": false,
"blob_strip": false,
"blob_strip_safe": null,
"test_cmd": "bash validate-plugin.generic.sh human-friendly-style",
"standalone_caveat": "no unit suite — ported generic structure validator (Step 7)"
},
"linkedin-studio": {
"paths": ["plugins/linkedin-studio/", "plugins/linkedin-thought-leadership/"],
"path_renames": {
"plugins/linkedin-studio/": "",
"plugins/linkedin-thought-leadership/": ""
},
"tag": "v0.4.0",
"repo_url": "https://git.fromaitochitta.com/open/linkedin-studio.git",
"has_vendor_ds": false,
"blob_strip": false,
"blob_strip_safe": null,
"test_cmd": "node --test hooks/scripts/__tests__/*.test.mjs render/__tests__/*.test.mjs agents/__tests__/*.test.mjs",
"test_cmd_advisory": "cd scripts/analytics && npm install && npm test",
"standalone_caveat": "renamed from linkedin-thought-leadership (F1). SC2 two-tier (M11): .mjs core is the HARD gate (zero deps); TS analytics suite is ADVISORY (npm/network required, NOT a clean-room gate)"
},
"llm-security": {
"paths": ["plugins/llm-security/", "plugins/llm-security-copilot/"],
"path_renames": {
"plugins/llm-security/": "",
"plugins/llm-security-copilot/": ""
},
"tag": "v7.7.2",
"repo_url": "https://git.fromaitochitta.com/open/llm-security.git",
"has_vendor_ds": true,
"blob_strip": false,
"blob_strip_safe": null,
"test_cmd": "node --test 'tests/**/*.test.mjs'",
"standalone_caveat": "renamed from llm-security-copilot (F1); vendors the design-system (playground)"
},
"ms-ai-architect": {
"paths": ["plugins/ms-ai-architect/"],
"path_renames": { "plugins/ms-ai-architect/": "" },
"tag": "v1.15.0",
"repo_url": "https://git.fromaitochitta.com/open/ms-ai-architect.git",
"has_vendor_ds": true,
"blob_strip": true,
"blob_strip_safe": true,
"test_cmd": "bash tests/validate-plugin.sh",
"standalone_caveat": "148 MB screenshot blob-bomb (F3): single filter-repo pass strips >1MB blobs when blob_strip_safe (all under playground/screenshots/), else surgical --path-glob; vendors the design-system"
},
"okr": {
"paths": ["plugins/okr/"],
"path_renames": { "plugins/okr/": "" },
"tag": "v1.3.0",
"repo_url": "https://git.fromaitochitta.com/open/okr.git",
"has_vendor_ds": false,
"blob_strip": false,
"blob_strip_safe": null,
"test_cmd": "bash validate-plugin.generic.sh okr",
"standalone_caveat": "no unit suite — ported generic structure validator (Step 7). okr/templates/okr.local.md.template is intentionally tracked (SC7 allow-exception)"
},
"voyage": {
"paths": ["plugins/voyage/", "plugins/ultraplan-local/"],
"path_renames": {
"plugins/voyage/": "",
"plugins/ultraplan-local/": ""
},
"tag": "v5.1.1",
"repo_url": "https://git.fromaitochitta.com/open/voyage.git",
"has_vendor_ds": false,
"blob_strip": false,
"blob_strip_safe": null,
"test_cmd": "node --test 'tests/**/*.test.mjs'",
"standalone_caveat": "renamed from ultraplan-local (F1, 136 pre-rename commits). Carries .forgejo/ISSUE_TEMPLATE/ under --path — assert it survives, no monorepo-relative refs (M17)"
},
"playground-design-system": {
"paths": ["shared/playground-design-system/", "shared/playground-examples/"],
"path_renames": {
"shared/playground-design-system/": "",
"shared/playground-examples/": "playground-examples/"
},
"tag": "v0.6.0",
"repo_url": "https://git.fromaitochitta.com/open/playground-design-system.git",
"has_vendor_ds": false,
"blob_strip": false,
"blob_strip_safe": null,
"test_cmd": "bash -c 'test -f tokens.css && test -f base.css && test -d schemas'",
"standalone_caveat": "DS source repo (the 2 consumers are llm-security + ms-ai-architect). Receives sync-design-system.mjs + test + PLAYGROUND-MAINTENANCE.md at standup (Step 4)"
}
}
}