From f83f8f755be05a28c5e39caa561089a9ed26921c Mon Sep 17 00:00:00 2001 From: bastien Date: Mon, 28 Sep 2026 11:48:44 +0200 Subject: [PATCH] feat(profiles): prune the gstack catalog, add max, honor a removed denylist Nine gstack skills leave every profile (ship is trunk-based on Gitea, land-and-deploy auto-merges and deploys, setup-deploy, autoplan reads paths that do not exist here, context-save has no restore, learn is an unused parallel store, careful and guard hooks never fired, design-shotgun needs an absent OpenAI key). lib/gstack-removed.sh is the single denylist; profile.sh gstack on and toggle-external.sh enable gstack skip it. full now carries everything every other profile carries (user rule), minus the parked make-pdf, diagram and 21st-ai/ui-explore/ui-review, which live in the new max profile together with pr-review-toolkit. The 21st trio also leaves web, web-full and design (redundant with impeccable + ui-ux-pro-max). lib/tests/profile-census.test.sh asserts the invariants live and on a baseline fixture plus one mutant per invariant; gstack-removed.test.sh covers both restore paths. --- README.md | 8 +- USAGE.md | 2 +- lib/gstack-removed.sh | 35 ++++ lib/profile.sh | 36 +++- lib/profiles/backend.profile | 8 +- lib/profiles/design.profile | 6 +- lib/profiles/dev.profile | 5 - lib/profiles/full.profile | 25 +-- lib/profiles/max.profile | 102 ++++++++++ lib/profiles/web-full.profile | 6 - lib/profiles/web.profile | 6 - lib/tests/gstack-removed.test.sh | 66 ++++++ lib/tests/profile-census.test.sh | 192 ++++++++++++++++++ lib/tests/profile-default.test.sh | 3 +- lib/tests/profile-set-managed.test.sh | 3 +- .../toggle-external-repo-resolution.test.sh | 1 + lib/toggle-external.sh | 17 +- skills/profile/SKILL.md | 3 +- 18 files changed, 457 insertions(+), 67 deletions(-) create mode 100644 lib/gstack-removed.sh create mode 100644 lib/profiles/max.profile create mode 100755 lib/tests/gstack-removed.test.sh create mode 100755 lib/tests/profile-census.test.sh diff --git a/README.md b/README.md index d7e0c55..2716368 100644 --- a/README.md +++ b/README.md @@ -172,12 +172,12 @@ a different package, ships its own conflicting `graphify` bin) — see | `/web-validate` | W3C HTML/CSS validity + WCAG 2.1 accessibility audit | | `/geo` | GEO-only audit — AI-search visibility (ChatGPT, Perplexity, Claude, Gemini…) | | `/client-handover` | Final project delivery — audits + branded deliverable (Markdown / HTML / PDF) | -| `/profile` | Activate a skill profile (web / seo / web-full / full / backend / design / dev / qa / audit / minimal) (default: full) | +| `/profile` | Activate a skill profile (web / seo / web-full / full / max / backend / design / dev / qa / audit / minimal) (default: full) | | `/tour` | Grouped all-axes sweep — cleanup + security + reconcile + doc, fix and loop until clean | > This table lists personal skills. Gstack skills (investigate, review, retro, -> office-hours, context-save, context-restore, cso…) and marketplace plugins add -> many more — run `/skills-perso` to list your hand-written skills, or browse `skills/`. +> office-hours, cso…) and marketplace plugins add many more — run +> `/skills-perso` to list your hand-written skills, or browse `skills/`. --- @@ -354,7 +354,7 @@ make update # update Claude Code, config, submodules, plugins, a make test # run deterministic tests (lib/tests/*.test.sh + lib/seo-data/*.test.sh + lib/gitflow-test.sh + lib/tests/run-*.sh) make onboard # onboard an existing project (run from its dir) make seo-connect # connect a Google account for /seo FULL (OAuth consent) -make profile cmd="set X" # activate a skill profile (web/seo/web-full/full/backend/design/dev/qa/audit/minimal) +make profile cmd="set X" # activate a skill profile (web/seo/web-full/full/max/backend/design/dev/qa/audit/minimal) make profile-list # list skill profiles make profile-current # show the active profile (full when none selected) make profile-reset # go to the default profile (full) diff --git a/USAGE.md b/USAGE.md index 0393523..7b36a20 100644 --- a/USAGE.md +++ b/USAGE.md @@ -163,7 +163,7 @@ Tu veux... | `/pdf-translate` | Traduire un PDF vers une autre langue | Sortie HTML fidèle (images, layout, style préservés) | | `/impeccable` | Audit/polish design + détecteur anti-slop déterministe | 23 verbes ; `npx impeccable detect` (exit 0/2) | | `/tour` | Sweep groupé sur un ou plusieurs projets | Sécu + nettoyage + reconcile + doc, boucle jusqu'à un pass propre | -| `/profile` | Changer le profil de skills | web / seo / web-full / full / backend / design / dev / qa / audit / minimal | +| `/profile` | Changer le profil de skills | web / seo / web-full / full / max / backend / design / dev / qa / audit / minimal | > Cette table couvre les skills personnels principaux. Les plugins (gstack, > pr-review-toolkit…) et marketplaces externes en ajoutent beaucoup d'autres — diff --git a/lib/gstack-removed.sh b/lib/gstack-removed.sh new file mode 100644 index 0000000..dfd8643 --- /dev/null +++ b/lib/gstack-removed.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# ============================================================ +# lib/gstack-removed.sh — the gstack skills this config never exposes +# +# Single source for the denylist. Sourced by lib/profile.sh +# (enable_all_gstack, enable_skill) and lib/toggle-external.sh +# (`enable gstack`), read by lib/tests/profile-census.test.sh. A name +# listed here is in no profile, `max` included, and every "bring gstack +# back" path skips it. Decided 2026-09-28 (skill-catalog prune, user go): +# +# ship base = origin/HEAD = main on Gitea, skips develop +# land-and-deploy `gh pr merge --squash --delete-branch` then deploys +# setup-deploy companion of land-and-deploy (Claude never deploys) +# autoplan reads ~/.claude/skills/gstack/plan-*/ paths that do +# not exist in this install +# context-save its pair context-restore is not linked; never used +# learn parallel JSONL store outside .claude/memory, unused +# careful, guard hooks exit 127 (missing bin path) — vacuous; the +# house permissions.deny is stricter (BDR-095) +# design-shotgun mockups need OPENAI_API_KEY, absent +# +# No `set -euo pipefail` here (sourced lib, mirrors lib/detect-plugins.sh). +# ============================================================ + +GSTACK_REMOVED=(ship land-and-deploy setup-deploy autoplan context-save + learn careful guard design-shotgun) + +# gstack_is_removed — exit 0 when is on the denylist. +gstack_is_removed() { + local name="$1" entry + for entry in "${GSTACK_REMOVED[@]}"; do + [ "$entry" = "$name" ] && return 0 + done + return 1 +} diff --git a/lib/profile.sh b/lib/profile.sh index 5cfa3d1..f4d0618 100755 --- a/lib/profile.sh +++ b/lib/profile.sh @@ -55,6 +55,11 @@ PROFILES_DIR="$REPO/lib/profiles" ACTIVE_CACHE="$REPO/.active-profile" # statusline reads this — keep fast (single-line file, profile name only) DEFAULT_PROFILE="full" # profile in force when none is selected (cache absent, empty, or legacy "none") +# GSTACK_REMOVED + gstack_is_removed() — single source, honored by every +# "bring gstack back" path below (enable_all_gstack, enable_skill). +# shellcheck source=lib/gstack-removed.sh disable=SC1091 +source "$(dirname "${BASH_SOURCE[0]}")/gstack-removed.sh" + # Plugins that are toggle-managed by `set`. Anything NOT in this list is # never auto-disabled — protects always-on plugins (security-guidance, # superpowers) and unrelated user plugins. Add a plugin here only when its @@ -313,7 +318,11 @@ enable_skill() { local skill="$1" type="$2" case "$type" in gstack) - if [ -e "$DISABLED_DIR/gstack__$skill" ]; then + if gstack_is_removed "$skill"; then + warn "refusing to enable removed skill: $skill (lib/gstack-removed.sh \ +— a profile census failure, not a crash)" + return 0 + elif [ -e "$DISABLED_DIR/gstack__$skill" ]; then rm -rf "${SKILLS_DIR:?}/${skill:?}" mv "$DISABLED_DIR/gstack__$skill" "$SKILLS_DIR/$skill" ok "enabled: $skill" @@ -454,18 +463,27 @@ disable_skill() { # ── Shared gstack operations ────────────────────────────── # Re-enable every gstack skill parked in skills-disabled/ (move gstack__* -# back into skills/). Shared by cmd_reset and `gstack on`. Side effects -# only; prints one confirmation per restored skill. +# back into skills/), skipping a name lib/gstack-removed.sh denies (left +# parked — the policy line goes to stderr). Shared by cmd_reset and +# `gstack on`. Echoes the REAL restored count (skipped names excluded) on +# stdout so the caller can report it accurately; per-skill confirmations +# go to stderr so that count is the only thing captured with `$(...)`. enable_all_gstack() { - local entry name - [ -d "$DISABLED_DIR" ] || return 0 + local entry name restored=0 + [ -d "$DISABLED_DIR" ] || { echo 0; return 0; } for entry in "$DISABLED_DIR"/gstack__*; do [ -e "$entry" ] || continue name="$(basename "$entry" | sed 's/^gstack__//')" + if gstack_is_removed "$name"; then + info "skipped (removed by policy, lib/gstack-removed.sh): $name" >&2 + continue + fi rm -rf "${SKILLS_DIR:?}/${name:?}" mv "$entry" "$SKILLS_DIR/$name" - ok "re-enabled: $name" + ok "re-enabled: $name" >&2 + restored=$((restored + 1)) done + echo "$restored" } # Disable gstack-origin skills not listed in the given profile. Shared by @@ -648,13 +666,13 @@ cmd_gstack() { on) # Restore whatever is parked, but DON'T touch active-profile — the # user is adding gstack on top of their current profile, not clearing it. - local parked + local parked restored parked="$(parked_gstack_count)" if [ "$parked" -eq 0 ]; then info "nothing parked — gstack skills are linked per profile (set/apply/reset)" else - enable_all_gstack - ok "$parked parked gstack skills restored" + restored="$(enable_all_gstack)" + ok "$restored parked gstack skills restored" fi ;; off) diff --git a/lib/profiles/backend.profile b/lib/profiles/backend.profile index c200109..f6de8ca 100644 --- a/lib/profiles/backend.profile +++ b/lib/profiles/backend.profile @@ -18,11 +18,8 @@ observability-and-instrumentation external deprecation-and-migration external ci-cd-and-automation external -# Ship + review + land -ship +# Review review -context-save -land-and-deploy # Second opinion for hard problems codex @@ -32,11 +29,8 @@ cso health # Session hygiene -careful freeze unfreeze -guard -learn retro # pr-review-toolkit removed (audit 2026-07-02 #12 — ~2.2k tokens, PR-only): diff --git a/lib/profiles/design.profile b/lib/profiles/design.profile index 4354a3f..6f6aac1 100644 --- a/lib/profiles/design.profile +++ b/lib/profiles/design.profile @@ -11,13 +11,12 @@ # GATE-BLOCK: 21st 21st-ui-build # Core design skills (gstack) -design-shotgun design-review design-consultation design-html plan-design-review -# Browser tooling — design-review and design-shotgun rely on it +# Browser tooling — design-review relies on it browse open-gstack-browser setup-browser-cookies @@ -44,10 +43,7 @@ site-motion personal # External: 21st.dev pack — CLI-driven (no MCP, no API key). 21st-registry # and 21st-design-sync are publishing flows; installed but left parked. 21st-ui-build external -21st-ui-explore external -21st-ui-review external 21st-cli-use external -21st-ai external # Plugin (auto-toggle) ui-ux-pro-max plugin@ui-ux-pro-max-skill diff --git a/lib/profiles/dev.profile b/lib/profiles/dev.profile index 3415189..4c347c5 100644 --- a/lib/profiles/dev.profile +++ b/lib/profiles/dev.profile @@ -6,7 +6,6 @@ # Implementation feat personal ship-feature personal -ship # Bug fixing hotfix personal @@ -23,7 +22,3 @@ commit-change personal observability-and-instrumentation external deprecation-and-migration external ci-cd-and-automation external - -# Session hygiene -context-save -land-and-deploy diff --git a/lib/profiles/full.profile b/lib/profiles/full.profile index 62959d6..fb05c55 100644 --- a/lib/profiles/full.profile +++ b/lib/profiles/full.profile @@ -1,7 +1,8 @@ -# DESC: Maximum mode — web-full + plan + dev for end-to-end MVP via /init-project -# Activate when: scaffolding new project with /init-project and need -# brainstorm → design → architecture review → scaffold → implement → ship → audit -# pipeline available in one session. Superset of web-full + dev. +# DESC: Default profile — carries everything every other profile carries +# (user rule 2026-09-28: full does what each profile does), minus the +# broken or doctrine-breaking gstack skills (lib/gstack-removed.sh) and +# the parked tools (make-pdf, diagram, 21st-ai/ui-explore/ui-review) that +# live in `max`. One named exception: pr-review-toolkit (see below). # === Brainstorm + plan-mode reviews ================================== office-hours @@ -9,11 +10,9 @@ plan-ceo-review plan-eng-review plan-design-review plan-devex-review -autoplan spec # === Design pipeline ================================================= -design-shotgun design-review design-consultation design-html @@ -35,12 +34,8 @@ refactor personal code-clean personal commit-change personal -# === Ship + review + land ============================================ -ship +# === Review =========================================================== review -context-save -land-and-deploy -setup-deploy # === Second opinion ================================================== codex @@ -63,20 +58,15 @@ qa-only # === Docs + translation ============================================== doc personal document-release -diagram -make-pdf pdf-translate personal # === Session hygiene + memory ======================================== close personal prune-memory personal status personal -learn retro -careful freeze unfreeze -guard # === External + plugin + MCP ========================================= emil-design-eng external @@ -99,10 +89,7 @@ ui-ux-pro-max plugin@ui-ux-pro-max-skill # or profile-based: bash lib/profile.sh apply audit (audit.profile keeps it; # a later `set full` re-disables it — MANAGED_PLUGINS lifecycle). 21st-ui-build external -21st-ui-explore external -21st-ui-review external 21st-cli-use external -21st-ai external # Personal: motion implementation companion (skills/site-motion) site-motion personal diff --git a/lib/profiles/max.profile b/lib/profiles/max.profile new file mode 100644 index 0000000..426b572 --- /dev/null +++ b/lib/profiles/max.profile @@ -0,0 +1,102 @@ +# DESC: Everything — full + the parked generation/review tools + PR review +# SUPERSET-OF: full +# Activate when: you need make-pdf, diagram, the 21st-ai/ui-explore/ +# ui-review generation trio, or pr-review-toolkit, on top of everything +# full carries. Never a broken or doctrine-breaking gstack skill +# (lib/gstack-removed.sh) — max is full's superset, not the raw catalog. + +# === Brainstorm + plan-mode reviews ================================== +office-hours +plan-ceo-review +plan-eng-review +plan-design-review +plan-devex-review +spec + +# === Design pipeline ================================================= +design-review +design-consultation +design-html + +# === Browser + dogfooding ============================================ +browse +open-gstack-browser +setup-browser-cookies +scrape +skillify + +# === Code work — implementation ====================================== +feat personal +ship-feature personal +hotfix personal +bugfix personal +investigate +refactor personal +code-clean personal +commit-change personal + +# === Review =========================================================== +review + +# === Second opinion ================================================== +codex + +# === SEO / GEO / standards / security ================================ +seo personal +geo personal +web-validate personal +harden personal +analyze personal +cso + +# === Perf + canary + QA ============================================== +health +benchmark +canary +qa +qa-only + +# === Docs + translation ============================================== +doc personal +document-release +diagram +make-pdf +pdf-translate personal + +# === Session hygiene + memory ======================================== +close personal +prune-memory personal +status personal +retro +freeze +unfreeze + +# === External + plugin + MCP ========================================= +emil-design-eng external +frontend-design external +design-motion-principles external +impeccable external +observability-and-instrumentation external +deprecation-and-migration external +ci-cd-and-automation external +scroll-world-storytelling external +build-threejs-scroll-worlds external +scroll-scrubbed-visual-sequence external +scroll-scrubbed-word-reveal external +scroll-progress-timeline external +ui-ux-pro-max plugin@ui-ux-pro-max-skill +pr-review-toolkit plugin@claude-code-plugins +21st-ui-build external +21st-ui-explore external +21st-ui-review external +21st-cli-use external +21st-ai external + +# Personal: motion implementation companion (skills/site-motion) +site-motion personal + +# === CLIs (advisory) ================================================= +21st cli +ctx7 cli +graphify cli +gsd cli diff --git a/lib/profiles/web-full.profile b/lib/profiles/web-full.profile index 18da3ab..ed80e59 100644 --- a/lib/profiles/web-full.profile +++ b/lib/profiles/web-full.profile @@ -4,7 +4,6 @@ # polish, audit, and verify. # === Design =========================================================== -design-shotgun design-review design-consultation design-html @@ -25,9 +24,7 @@ feat personal ship-feature personal hotfix personal bugfix personal -ship review -context-save commit-change personal refactor personal @@ -55,10 +52,7 @@ scroll-scrubbed-visual-sequence external scroll-scrubbed-word-reveal external scroll-progress-timeline external 21st-ui-build external -21st-ui-explore external -21st-ui-review external 21st-cli-use external -21st-ai external ui-ux-pro-max plugin@ui-ux-pro-max-skill # Personal: motion implementation companion (skills/site-motion) diff --git a/lib/profiles/web.profile b/lib/profiles/web.profile index 0eb19a0..ee53a99 100644 --- a/lib/profiles/web.profile +++ b/lib/profiles/web.profile @@ -4,7 +4,6 @@ # For SEO/GEO audit on top, use web-full or apply seo afterwards. # Design skills (gstack) — full design pipeline -design-shotgun design-review design-consultation design-html @@ -23,9 +22,7 @@ plan-eng-review feat personal ship-feature personal hotfix personal -ship # gstack review # gstack -context-save # gstack commit-change personal refactor personal @@ -51,10 +48,7 @@ site-motion personal # External: 21st.dev pack (publishing flows 21st-registry / -design-sync # stay parked) 21st-ui-build external -21st-ui-explore external -21st-ui-review external 21st-cli-use external -21st-ai external # Plugin: UI/UX intelligence (auto-toggle) ui-ux-pro-max plugin@ui-ux-pro-max-skill diff --git a/lib/tests/gstack-removed.test.sh b/lib/tests/gstack-removed.test.sh new file mode 100755 index 0000000..91ced07 --- /dev/null +++ b/lib/tests/gstack-removed.test.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# lib/tests/gstack-removed.test.sh — GSTACK_REMOVED denylist honored by +# every "bring gstack back" path: profile.sh `gstack on` and +# toggle-external.sh `enable gstack` both skip a policy-removed name and +# leave it parked, restoring only what policy allows; gstack_is_removed() +# itself, positive + negative. Covers contract criterion 17. Hermetic: +# fixture repo via PROFILE_REPO_OVERRIDE / TOGGLE_EXTERNAL_REPO_OVERRIDE — +# same harness as lib/tests/profile-default.test.sh and +# lib/tests/toggle-external-repo-resolution.test.sh. +set -u +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +pass=0; fail=0 +check() { if [ "$2" = "$3" ]; then pass=$((pass+1)); else fail=$((fail+1)); + printf 'FAIL %s: got[%s] want[%s]\n' "$1" "$2" "$3"; fi; } +check_has() { case "$2" in *"$3"*) pass=$((pass+1));; *) fail=$((fail+1)); + printf 'FAIL %s: [%s] does not contain [%s]\n' "$1" "$2" "$3";; esac; } +check_not() { case "$2" in *"$3"*) fail=$((fail+1)); + printf 'FAIL %s: [%s] unexpectedly contains [%s]\n' "$1" "$2" "$3";; *) pass=$((pass+1));; esac; } + +# mk_fixture — minimal repo: profile.sh + toggle-external.sh + +# gstack-removed.sh under lib/, one removed name (ship) and one kept name +# (browse) parked in skills-disabled/. +mk_fixture() { + local fx="$1" + mkdir -p "$fx/skills" "$fx/skills-disabled/gstack__ship" \ + "$fx/skills-disabled/gstack__browse" "$fx/lib" + cp "$ROOT/lib/profile.sh" "$ROOT/lib/toggle-external.sh" \ + "$ROOT/lib/gstack-removed.sh" "$fx/lib/" +} + +# --- T1-T5: profile.sh gstack on restores browse, skips + parks ship --- +FX1="$(mktemp -d)"; mk_fixture "$FX1" +out="$(PROFILE_REPO_OVERRIDE="$FX1" bash "$FX1/lib/profile.sh" gstack on 2>&1)" +check T1-browse-restored "$([ -e "$FX1/skills/browse" ] && echo on || echo off)" on +check T2-ship-parked "$([ -e "$FX1/skills-disabled/gstack__ship" ] && echo p || echo n)" p +check T3-ship-not-live "$([ -e "$FX1/skills/ship" ] && echo on || echo off)" off +check_has T4-skip-names-ship "$out" "ship" +check_has T5-real-count "$out" "1 parked gstack skills restored" +rm -rf "$FX1" + +# --- T6-T9: toggle-external.sh enable gstack — same skip + restore --- +FX2="$(mktemp -d)"; mk_fixture "$FX2" +out="$(TOGGLE_EXTERNAL_REPO_OVERRIDE="$FX2" bash "$FX2/lib/toggle-external.sh" enable gstack 2>&1)" +check T6-browse-restored "$([ -e "$FX2/skills/browse" ] && echo on || echo off)" on +check T7-ship-parked "$([ -e "$FX2/skills-disabled/gstack__ship" ] && echo p || echo n)" p +check T8-ship-not-live "$([ -e "$FX2/skills/ship" ] && echo on || echo off)" off +check_has T9-skip-names-ship "$out" "ship" +rm -rf "$FX2" + +# --- T10-T11: toggle-external.sh, only removed names parked — 0 restored, +# the policy message fires instead of the "re-run gstack setup" hint --- +FX3="$(mktemp -d)" +mkdir -p "$FX3/skills" "$FX3/skills-disabled/gstack__ship" "$FX3/lib" +cp "$ROOT/lib/toggle-external.sh" "$ROOT/lib/gstack-removed.sh" "$FX3/lib/" +out="$(TOGGLE_EXTERNAL_REPO_OVERRIDE="$FX3" bash "$FX3/lib/toggle-external.sh" enable gstack 2>&1)" +check_has T10-policy-msg "$out" "policy-removed skills remain parked" +check_not T11-no-setup-hint "$out" "re-run gstack setup" +rm -rf "$FX3" + +# --- T12-T13: gstack_is_removed() itself, positive + negative --- +# shellcheck source=lib/gstack-removed.sh disable=SC1091 +source "$ROOT/lib/gstack-removed.sh" +gstack_is_removed ship; check T12-removed-positive "$?" 0 +gstack_is_removed browse; check T13-removed-negative "$?" 1 + +printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ] diff --git a/lib/tests/profile-census.test.sh b/lib/tests/profile-census.test.sh new file mode 100755 index 0000000..3aadcc1 --- /dev/null +++ b/lib/tests/profile-census.test.sh @@ -0,0 +1,192 @@ +#!/usr/bin/env bash +# lib/tests/profile-census.test.sh — profile catalog invariants (skill- +# catalog prune, 2026-09-28): no GSTACK_REMOVED name listed in any +# profile, exactly one profile carries the `# SUPERSET-OF: full` marker +# and it is a superset of full + the parked tools + every name any +# profile carries, and `full` carries every name any other (non-max) +# profile carries save one named exception (pr-review-toolkit). +# +# Hermetic: a passing baseline fixture, then one single-change mutant per +# invariant (each mutant is a positive control — it MUST be detected). +# Live part runs against this repo's real lib/profiles/ (a violation +# there fails the suite for real, same style as +# lib/tests/skill-routing-census.test.sh). +set -u +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +# shellcheck source=lib/gstack-removed.sh disable=SC1091 +source "$ROOT/lib/gstack-removed.sh" + +# Parked = kept installed, out of `full`, listed only in the superset +# profile (`max`). Names come from the single denylist above for REMOVED; +# PARKED has no such shared source (it is a positive allowlist, not a +# denylist), so it is spelled out here. +PARKED=(make-pdf diagram 21st-ai 21st-ui-explore 21st-ui-review) + +# full carries every name any other (non-max) profile carries, with one +# named exception: pr-review-toolkit is deliberately out of full (audit +# 2026-07-02 #12, heaviest single plugin, ~2.2k tokens/session, PR-only +# use) — measured 2026-09-28: it is the only name any specialized +# profile carries that full lacks (audit.profile). +FULL_EXCEPTIONS=(pr-review-toolkit) + +pass=0; fail=0 +check() { if [ "$2" = "$3" ]; then pass=$((pass+1)); else fail=$((fail+1)); + printf 'FAIL %s: got[%s] want[%s]\n' "$1" "$2" "$3"; fi; } + +# census_check [parked_csv] [exceptions_csv] — one +# violation code per line on stdout, rc 0 iff none. REMOVED always comes +# from the sourced denylist (single source, never overridden); +# PARKED/FULL_EXCEPTIONS default to the real production sets and can be +# overridden per call (fixture runs use a small standalone catalog). +census_check() { + local dir="$1" + # Unset (arg omitted, live call) falls back to the real production + # set; an explicitly EMPTY string (fixture calls) means "none" and + # must stay empty — hence "-" defaults, never ":-" (which would treat + # empty the same as unset and silently pull the real set back in). + local parked_csv="${2-$(IFS=,; echo "${PARKED[*]}")}" + local exc_csv="${3-$(IFS=,; echo "${FULL_EXCEPTIONS[*]}")}" + local removed_csv out + removed_csv="$(IFS=,; echo "${GSTACK_REMOVED[*]}")" + out=$(python3 - "$dir" "$removed_csv" "$parked_csv" "$exc_csv" <<'PY' +import glob, os, re, sys + +def entries(path): + """Entry = first whitespace token of a non-blank, non-comment line.""" + names = set() + for line in open(path, encoding="utf-8"): + s = line.strip() + if s and not s.startswith("#"): + names.add(s.split()[0]) + return names + +def has_marker(path): + text = open(path, encoding="utf-8").read() + return re.search(r'^# SUPERSET-OF: full\s*$', text, re.M) is not None + +def check_removed(by_name, removed): + return [f"REMOVED_LISTED:{n}:{e}" for n, ents in by_name.items() + for e in sorted(ents & removed)] + +def find_superset(files): + hits = [p for p in files if has_marker(p)] + if not hits: + return None, ["NO_SUPERSET"] + if len(hits) > 1: + return None, ["MANY_SUPERSETS"] + return os.path.basename(hits[0])[:-len(".profile")], [] + +def check_superset_gap(by_name, sname, parked, exceptions): + full = by_name.get("full", set()) + target = full | parked | exceptions + return [f"SUPERSET_GAP:{n}" for n in sorted(target - by_name[sname])] + +def check_max_gap(by_name, sname, removed): + union = set().union(*by_name.values()) + gap = (union - removed) - by_name[sname] + return [f"MAX_GAP:{n}" for n in sorted(gap)] + +def check_full_gap(by_name, sname, removed, exceptions): + trio = {"21st-ai", "21st-ui-explore", "21st-ui-review"} + src = set().union(*(e for n, e in by_name.items() + if n not in ("full", sname))) + full = by_name.get("full", set()) + gap = src - full - removed - trio - exceptions + return [f"FULL_GAP:{n}" for n in sorted(gap)] + +def main(): + d, removed_csv, parked_csv, exc_csv = sys.argv[1:5] + removed = {x for x in removed_csv.split(",") if x} + parked = {x for x in parked_csv.split(",") if x} + exceptions = {x for x in exc_csv.split(",") if x} + + files = sorted(glob.glob(os.path.join(d, "*.profile"))) + by_name = {os.path.basename(p)[:-len(".profile")]: entries(p) + for p in files} + + violations = check_removed(by_name, removed) + sname, sup_violations = find_superset(files) + violations += sup_violations + if sname: + violations += check_superset_gap(by_name, sname, parked, exceptions) + violations += check_max_gap(by_name, sname, removed) + violations += check_full_gap(by_name, sname, removed, exceptions) + + # Reason codes only, one per line; no exit here — the bash caller + # derives pass/fail from whether this captured output is empty. + print("\n".join(violations)) + +main() +PY +) + if [ -n "$out" ]; then + printf '%s\n' "$out" + return 1 + fi + return 0 +} + +# run_mutant