From f4c36fcda556ac611906b408d1e8a2a8a86793e9 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Wed, 17 Jun 2026 12:29:33 +0200 Subject: [PATCH] chore(migration): preflight script + rename-aware plugin map --- .../migration/00-preflight.sh | 134 +++++++++++++++++ .../migration/00-preflight.test.mjs | 55 +++++++ .../migration/plugin-map.json | 140 ++++++++++++++++++ 3 files changed, 329 insertions(+) create mode 100644 docs/marketplace-polyrepo-migration/migration/00-preflight.sh create mode 100644 docs/marketplace-polyrepo-migration/migration/00-preflight.test.mjs create mode 100644 docs/marketplace-polyrepo-migration/migration/plugin-map.json diff --git a/docs/marketplace-polyrepo-migration/migration/00-preflight.sh b/docs/marketplace-polyrepo-migration/migration/00-preflight.sh new file mode 100644 index 0000000..77659a0 --- /dev/null +++ b/docs/marketplace-polyrepo-migration/migration/00-preflight.sh @@ -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" diff --git a/docs/marketplace-polyrepo-migration/migration/00-preflight.test.mjs b/docs/marketplace-polyrepo-migration/migration/00-preflight.test.mjs new file mode 100644 index 0000000..651e31d --- /dev/null +++ b/docs/marketplace-polyrepo-migration/migration/00-preflight.test.mjs @@ -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 (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`); + } +}); + +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')); +}); diff --git a/docs/marketplace-polyrepo-migration/migration/plugin-map.json b/docs/marketplace-polyrepo-migration/migration/plugin-map.json new file mode 100644 index 0000000..e1e631e --- /dev/null +++ b/docs/marketplace-polyrepo-migration/migration/plugin-map.json @@ -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)" + } + } +}