chore(migration): catalog-thinning script (staged for window)

70-thin-catalog.sh: renders the thin-catalog end-state into --workspace (never the live tree). git-archive HEAD -> remove plugins/ + shared/ + scripts/sync-design-system.mjs(+test) -> extract CLAUDE.md conventions into CONVENTIONS.md (D6) -> thin the catalog CLAUDE.md -> rewrite README.md with external Forgejo repo links and versions re-stated from plugin-map.json (M15 coupled to the D6 rewrite).

Verify: workspace has no plugins/ or shared/, CONVENTIONS.md present; rewritten README has 0 local links and corrects the stale playground-design-system v0.1 -> v0.6.0. Test 4/4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-06-17 13:38:42 +02:00
commit 3a5f558a48
2 changed files with 213 additions and 0 deletions

View file

@ -0,0 +1,135 @@
#!/usr/bin/env bash
# Step 9 — catalog-thinning script (staged for the operator window).
#
# Renders the THIN-CATALOG end-state into a --workspace. NEVER mutates the live tree
# (forbidden_paths: plugins, shared, CLAUDE.md, README.md) — every write lands inside --workspace.
# After the operator window the catalog repo hosts only the marketplace manifest + the catalog-level
# docs; the 10 plugins + the design-system live in their own Forgejo repos under
# https://git.fromaitochitta.com/open/. This script produces that state for inspection/staging:
#
# 1. archives the tracked catalog (git archive HEAD) into --workspace (clean: no .git, no untracked)
# 2. removes plugins/, shared/, scripts/sync-design-system.mjs (+ its test)
# 3. extracts the marketplace conventions from CLAUDE.md into CONVENTIONS.md (D6)
# 4. thins the catalog CLAUDE.md to catalog-maintenance only
# 5. rewrites the landing README.md to point at the external plugin repos, re-stating every version
# from plugin-map.json (the verified source). M15 is COUPLED to this D6 rewrite, not a ride-along:
# a roster that re-states versions/counts must state the verified ones or it ships a false document.
#
# NULL push (D8): writes only inside --workspace.
#
# Usage: 70-thin-catalog.sh --workspace <dir>
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
MAP="$SCRIPT_DIR/plugin-map.json"
WS=""
while [ $# -gt 0 ]; do
case "$1" in
--workspace) WS="${2:-}"; shift 2 ;;
*) echo "unknown arg: $1" >&2; exit 2 ;;
esac
done
[ -n "$WS" ] || { echo "usage: 70-thin-catalog.sh --workspace <dir>" >&2; exit 2; }
[ -f "$MAP" ] || { echo "plugin-map.json missing at $MAP" >&2; exit 1; }
# --- 1. Archive the tracked catalog into the workspace (no .git, no untracked) ---
rm -rf "$WS"
mkdir -p "$WS"
git -C "$REPO_ROOT" archive HEAD | tar -x -C "$WS"
# --- 2. Remove the components that move out to their own repos ---
rm -rf "$WS/plugins" "$WS/shared" \
"$WS/scripts/sync-design-system.mjs" "$WS/scripts/sync-design-system.test.mjs"
rmdir "$WS/scripts" 2>/dev/null || true
# --- 3. Extract conventions -> CONVENTIONS.md (D6) ---
{
echo "# Conventions — ktg-plugin-marketplace catalog"
echo
echo "> Extracted from the marketplace CLAUDE.md (D6). These are the marketplace-wide conventions"
echo "> every plugin repo inherits under fork-and-own. See GOVERNANCE.md for the governance model."
echo
awk '
/^## Konvensjoner/ { f=1; print; next }
f && /^## / { exit }
f { print }
' "$WS/CLAUDE.md"
} > "$WS/CONVENTIONS.md"
# --- 4. Thin the catalog CLAUDE.md to catalog-maintenance only ---
cat > "$WS/CLAUDE.md" <<'MD'
# ktg-plugin-marketplace (catalog)
Catalog repository for the ktg-plugin-marketplace. After the polyrepo migration this repo hosts only
the marketplace manifest and the catalog-level docs; every plugin and the shared design-system live in
their own Forgejo repositories under `https://git.fromaitochitta.com/open/`.
## What lives here
- `.claude-plugin/marketplace.json` — the marketplace manifest (plugin entries point at external repos)
- `README.md` — the landing/catalog page
- `CONVENTIONS.md` — marketplace-wide conventions inherited by every plugin repo
- `GOVERNANCE.md` — governance + fork-and-own model
- `.mailmap`, `.gitleaks.toml`, `.gitleaksignore` — shared git-hygiene baselines
## Catalog maintenance
- Marketplace conventions: see CONVENTIONS.md.
- Adding/updating a plugin entry: edit `.claude-plugin/marketplace.json` (external `source: "url"` with
a pinned `ref`) and re-state the plugin in README.md with its verified version.
- Plugin source, issues, and releases live in each plugin's own repository — not here.
MD
# --- 5. Rewrite the landing README.md: external repo links + versions re-stated from plugin-map.json ---
python3 - "$MAP" "$WS/README.md" <<'PY'
import json, re, sys
map_path, readme_path = sys.argv[1], sys.argv[2]
targets = json.load(open(map_path))["targets"]
def browse(key):
u = targets[key]["repo_url"]
return u[:-4] if u.endswith(".git") else u
tag = {k: targets[k].get("tag", "") for k in targets}
# shared/playground-examples/ is bundled into the playground-design-system repo (plugin-map path_renames)
EXAMPLES_OWNER = "playground-design-system"
src = open(readme_path).read()
def link_repl(m):
container, key, sub = m.group(1), m.group(2), (m.group(3) or "")
sub = sub.lstrip("/")
if container == "shared" and key == "playground-examples":
owner = EXAMPLES_OWNER
subpath = ("playground-examples/" + sub) if sub else ""
elif key in targets:
owner = key
subpath = sub
else:
return m.group(0) # unknown — leave as-is (surfaced by the no-local-links assertion)
base = browse(owner)
if not subpath or subpath == "README.md":
return "](%s)" % base
return "](%s/src/branch/main/%s)" % (base, subpath)
# swap every local ](plugins/<key>/...) / ](shared/<key>/...) link to its external repo
src = re.sub(r"\]\((plugins|shared)/([a-z0-9-]+)(/[^)]*)?\)", link_repl, src)
# re-state the version backtick on heading lines that now link to a known external repo
def fix_version(line):
m = re.search(r"\]\(https://git\.fromaitochitta\.com/open/([a-z0-9-]+)\)", line)
if m and m.group(1) in tag and tag[m.group(1)]:
line = re.sub(r"`v[0-9][^`]*`", "`%s`" % tag[m.group(1)], line, count=1)
return line
out = "\n".join(fix_version(l) for l in src.split("\n"))
open(readme_path, "w").write(out)
PY
echo "thin-catalog ready at $WS"
echo " removed: plugins/ shared/ scripts/sync-design-system.mjs"
echo " added: CONVENTIONS.md (extracted from CLAUDE.md conventions)"
echo " README.md rewritten: external repo links + versions re-stated from plugin-map.json"

View file

@ -0,0 +1,78 @@
// Step 9 test — the thin-catalog state (against a workspace copy):
// - no plugins/, no shared/, no scripts/sync-design-system.mjs (SC4)
// - CONVENTIONS.md exists + carries the Forgejo / conventional-commits / three-doc rules (D6)
// - marketplace.json + README.md + GOVERNANCE.md remain
// - the rewritten README has zero local plugins//shared/ links and uses external Forgejo repos
// - the stale playground-design-system version is corrected from plugin-map (v0.6.0)
// Pattern: plugins/config-audit/tests/*.test.mjs.
import test from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, rmSync, existsSync, readFileSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const here = path.dirname(fileURLToPath(import.meta.url));
const SCRIPT = path.join(here, '70-thin-catalog.sh');
function thin() {
const ws = mkdtempSync(path.join(os.tmpdir(), 'thin-catalog-'));
const r = spawnSync('bash', [SCRIPT, '--workspace', ws], { encoding: 'utf8' });
return { ws, r };
}
test('removes plugins/, shared/, and scripts/sync-design-system.mjs (SC4)', () => {
const { ws, r } = thin();
try {
assert.equal(r.status, 0, `expected exit 0:\n${r.stdout}\n${r.stderr}`);
assert.ok(!existsSync(path.join(ws, 'plugins')), 'plugins/ must be gone');
assert.ok(!existsSync(path.join(ws, 'shared')), 'shared/ must be gone');
assert.ok(!existsSync(path.join(ws, 'scripts', 'sync-design-system.mjs')), 'sync-design-system.mjs must be gone');
} finally {
rmSync(ws, { recursive: true, force: true });
}
});
test('CONVENTIONS.md exists and carries Forgejo / conventional-commits / three-doc rules', () => {
const { ws, r } = thin();
try {
assert.equal(r.status, 0, r.stderr);
const conv = path.join(ws, 'CONVENTIONS.md');
assert.ok(existsSync(conv), 'CONVENTIONS.md must exist');
const text = readFileSync(conv, 'utf8');
assert.match(text, /Forgejo/, 'must carry the Forgejo rule');
assert.match(text, /Conventional Commits/, 'must carry the conventional-commits rule');
assert.match(text, /tre doc-nivåer/, 'must carry the three-doc rule');
} finally {
rmSync(ws, { recursive: true, force: true });
}
});
test('marketplace.json, README.md, GOVERNANCE.md remain', () => {
const { ws, r } = thin();
try {
assert.equal(r.status, 0, r.stderr);
assert.ok(existsSync(path.join(ws, '.claude-plugin', 'marketplace.json')), 'marketplace.json must remain');
assert.ok(existsSync(path.join(ws, 'README.md')), 'README.md must remain');
assert.ok(existsSync(path.join(ws, 'GOVERNANCE.md')), 'GOVERNANCE.md must remain');
} finally {
rmSync(ws, { recursive: true, force: true });
}
});
test('rewritten README has no local plugins//shared/ links and corrects the DS version (M15)', () => {
const { ws, r } = thin();
try {
assert.equal(r.status, 0, r.stderr);
const readme = readFileSync(path.join(ws, 'README.md'), 'utf8');
assert.ok(!/\]\(plugins\//.test(readme), 'no local plugins/ links may remain');
assert.ok(!/\]\(shared\//.test(readme), 'no local shared/ links may remain');
assert.match(readme, /https:\/\/git\.fromaitochitta\.com\/open\//, 'must reference external Forgejo repos');
// playground-design-system was stale at v0.1 in the live README; plugin-map pins v0.6.0
assert.ok(!/`v0\.1`/.test(readme), 'stale playground-design-system v0.1 must be corrected');
assert.match(readme, /`v0\.6\.0`/, 'playground-design-system must read v0.6.0 from plugin-map');
} finally {
rmSync(ws, { recursive: true, force: true });
}
});