From 19135970144ca2b0d51bba54e9a43db94db32d64 Mon Sep 17 00:00:00 2001 From: Justin Lee Date: Tue, 7 Jul 2026 12:53:07 +0800 Subject: [PATCH 01/10] fix(refactor): regenerate cross-language cache when older than last commit Stale .sourceatlas/cross-language.yaml from a previous run silently fed months-old counts into pilot reports (caught by harness fact-check on WordPress-iOS after repo update: 3/5 claims failed). Co-Authored-By: Claude Fable 5 --- plugin/commands/refactor/scripts/pilot-run.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/plugin/commands/refactor/scripts/pilot-run.sh b/plugin/commands/refactor/scripts/pilot-run.sh index d6ee2c3..fdca91e 100755 --- a/plugin/commands/refactor/scripts/pilot-run.sh +++ b/plugin/commands/refactor/scripts/pilot-run.sh @@ -72,9 +72,15 @@ bash "$SCRIPT_DIR/detect-zones.sh" "$TARGET" 2>&1 >> "$OUTPUT" || true echo '```' >> "$OUTPUT" echo "" >> "$OUTPUT" -# Cross-language context (global — regenerate only if missing) +# Cross-language context (global — regenerate if missing or older than last commit) xlang_file="$PROJECT_ROOT/.sourceatlas/cross-language.yaml" -if [[ ! -f "$xlang_file" ]]; then +xlang_stale=0 +if [[ -f "$xlang_file" ]]; then + last_commit=$(git -C "$PROJECT_ROOT" log -1 --format=%ct 2>/dev/null || echo 0) + cache_mtime=$(stat -f %m "$xlang_file" 2>/dev/null || stat -c %Y "$xlang_file" 2>/dev/null || echo 0) + [[ "$cache_mtime" -lt "$last_commit" ]] && xlang_stale=1 +fi +if [[ ! -f "$xlang_file" || "$xlang_stale" = 1 ]]; then bash "$SCRIPT_DIR/cross-language-visibility.sh" "$PROJECT_ROOT" >/dev/null 2>&1 || true fi echo "## Cross-Language Visibility (project-level)" >> "$OUTPUT" From fcedd313afe3506293c0f44d7fbcb2667428cdc9 Mon Sep 17 00:00:00 2001 From: Justin Lee Date: Tue, 7 Jul 2026 12:53:08 +0800 Subject: [PATCH 02/10] feat(refactor): extend state machine to Steps 8-13 with gate-postswap - state.sh: step keys 8_new_impl..13_delete_legacy (shadow 9a/9b/9c share 9_swap); advance refuses past final step 13 - init-state.sh + templates/state.yaml: schema 2.1 adds six step entries (old 2.0 state files unchanged through Step 7) - mode-dispatch.yaml: S8-S13 apply in every mode - gate-postswap.sh: Done-When grep signals for Steps 8/12/13 as exit codes; SKILL.md tables now point at it - test-postswap.sh: 9-assertion self-check (state walk 7-13 + gate pass/fail both directions) - SKILL.md: Gotchas section seeded with the stale-cache lesson Co-Authored-By: Claude Fable 5 --- plugin/commands/refactor/SKILL.md | 19 ++- .../refactor/references/mode-dispatch.yaml | 40 +++++ .../refactor/scripts/gate-postswap.sh | 137 ++++++++++++++++++ .../commands/refactor/scripts/init-state.sh | 8 +- plugin/commands/refactor/scripts/state.sh | 12 +- .../refactor/scripts/test-postswap.sh | 71 +++++++++ plugin/commands/refactor/templates/state.yaml | 10 +- 7 files changed, 288 insertions(+), 9 deletions(-) create mode 100755 plugin/commands/refactor/scripts/gate-postswap.sh create mode 100755 plugin/commands/refactor/scripts/test-postswap.sh diff --git a/plugin/commands/refactor/SKILL.md b/plugin/commands/refactor/SKILL.md index 3d4ae5c..9b97316 100644 --- a/plugin/commands/refactor/SKILL.md +++ b/plugin/commands/refactor/SKILL.md @@ -170,7 +170,7 @@ The Playbook Navigator adds: ## Steps 8-13: Post-Tool Guidance -Steps 8-13 are **user-driven** without tool assistance. After Step 7 passes, output the table below — each row names the starting artifact, the concrete action, and the verifiable Done signal so the user can self-check. +Steps 8-13 are **user-driven**, but state-tracked: `state.sh advance` continues past Step 7, and `bash "${CLAUDE_PLUGIN_ROOT}/commands/refactor/scripts/gate-postswap.sh" --module --step <8|12|13>` machine-verifies the grep-checkable Done signals below (Steps 9-11 are verified by test suites — re-run `gate-step7.sh` / the full suite). After Step 7 passes, output the table below — each row names the starting artifact, the concrete action, and the verifiable Done signal so the user can self-check. > **Mode variants**: The table below shows `seam-injection` (default). For `platform-migration`, `strangler-fig`, or `platform-strangler`, see **[references/steps-8-13-by-mode.md](references/steps-8-13-by-mode.md)**. > Check `state.yaml → migration_mode.mode_name` to determine which path to follow. @@ -179,12 +179,12 @@ Steps 8-13 are **user-driven** without tool assistance. After Step 7 passes, out | Step | Start From | Do (concrete actions) | Done When (verifiable signal) | |------|------------|-----------------------|-------------------------------| -| 8 — Write New Implementation | `5_interface.{ext}` + `4_tests.{ext}` | Create new file implementing the Seam Interface; no imports of legacy file; inject collaborators via constructor; write unit tests alongside | New file compiles, unit tests green, characterization tests still green, `grep -l "" ` returns no hits | +| 8 — Write New Implementation | `5_interface.{ext}` + `4_tests.{ext}` | Create new file implementing the Seam Interface; no imports of legacy file; inject collaborators via constructor; write unit tests alongside | New file compiles, unit tests green, characterization tests still green; `gate-postswap.sh --step 8 --impl-file ` passes | | 9 — Swap Implementation | New impl + `3_seams.yaml.recommended.enabling_point` | Replace `LegacyAdapter` with new impl at the ONE injection-site line; no other files touched in this commit | Single-file, single-line wiring change committed; characterization tests still pass | | 10 — Run Verification | Swapped code + `7_gate_results.yaml` (baseline) | Re-run `gate-step7.sh`; diff each section (Layer A / Layer B / contract CI) against the baseline | New gate output matches baseline 1:1 — same passes, same counts, no new failures | | 11 — Integration Testing | Verified swap from Step 10 | Run full app test suite; manual smoke every user-facing flow touching this module; check perf on hot paths | Full suite green; manual flows pass; no perf regression flagged | -| 12 — Clean Up | Integrated swap from Step 11 | Delete `6_adapter.{ext}`; rename Seam Interface → final Target Interface name; delete temporary mocks/shims; update imports / re-exports | `grep -r ""` and `grep -r ""` both return zero hits; full suite green | -| 13 — Delete Legacy | Cleaned codebase from Step 12 | `grep -r ""` to confirm zero refs; delete legacy file(s); final full-suite run; one dedicated commit | Legacy file no longer exists; full suite green; deletion is its own commit (not bundled with refactor work) | +| 12 — Clean Up | Integrated swap from Step 11 | Delete `6_adapter.{ext}`; rename Seam Interface → final Target Interface name; delete temporary mocks/shims; update imports / re-exports | `gate-postswap.sh --step 12` passes (adapter + temporary seam name zero refs); full suite green | +| 13 — Delete Legacy | Cleaned codebase from Step 12 | Delete legacy file(s); final full-suite run; one dedicated commit | `gate-postswap.sh --step 13` passes (legacy file gone, zero class refs); full suite green; deletion is its own commit (not bundled with refactor work) | ### Mode: `seam-injection` — swap_strategy: `shadow` @@ -198,8 +198,8 @@ Steps 8-13 are **user-driven** without tool assistance. After Step 7 passes, out | 9c — Hard Swap | Threshold met + `3_seams.yaml.recommended.enabling_point` | Replace `ShadowAdapter` with new impl directly at the ONE injection site. Single-file, single-line commit. | Characterization tests pass; shadow logger no longer called; `grep -l "ShadowAdapter" ` = 0 hits | | 10 — Run Verification | Swapped code + `7_gate_results.yaml` (baseline) | Re-run `gate-step7.sh`; diff against baseline | Baseline matched 1:1 | | 11 — Integration Testing | Verified swap from Step 10 | Full suite + manual smoke | Full suite green; no perf regression | -| 12 — Clean Up | Integrated swap from Step 11 | Delete `6_adapter.{ext}` (ShadowAdapter) + `6_logger_protocol.{ext}`; rename Seam Interface; delete logger implementation | `grep -r "ShadowAdapter\|ShadowLogger"` = 0 hits; full suite green | -| 13 — Delete Legacy | Cleaned codebase from Step 12 | Confirm zero refs to legacy class; delete; own commit | Legacy file deleted; full suite green | +| 12 — Clean Up | Integrated swap from Step 11 | Delete `6_adapter.{ext}` (ShadowAdapter) + `6_logger_protocol.{ext}`; rename Seam Interface; delete logger implementation | `gate-postswap.sh --step 12` passes (also checks the shadow logger protocol); full suite green | +| 13 — Delete Legacy | Cleaned codebase from Step 12 | Delete legacy file; own commit | `gate-postswap.sh --step 13` passes; full suite green | > See [references/playbook-overview.md](references/playbook-overview.md) for the complete 13-step overview with detailed checklists per step. @@ -255,3 +255,10 @@ $STEP_OUTPUT - **[../seam/references/seam-types.md](../seam/references/seam-types.md)** — Seam type taxonomy - **scripts/gate-contracts.sh** — Gate 2: deterministic contract verification dry-run - **scripts/gate-seams.sh** — Gate 3: deterministic enabling point existence check +- **scripts/gate-postswap.sh** — Steps 8/12/13: grep-checkable Done-When signals as exit codes (self-test: `scripts/test-postswap.sh`) + +--- + +## Gotchas + +- **`.sourceatlas/` caches lie after the repo moves on.** A cached `cross-language.yaml` from months ago silently fed stale counts into pilot reports (caught by fact-checking against a freshly updated repo, 2026-07). `pilot-run.sh` now regenerates any cache older than the last commit — apply the same freshness check before trusting any other `.sourceatlas/` artifact. diff --git a/plugin/commands/refactor/references/mode-dispatch.yaml b/plugin/commands/refactor/references/mode-dispatch.yaml index 8260c10..8d15bf4 100644 --- a/plugin/commands/refactor/references/mode-dispatch.yaml +++ b/plugin/commands/refactor/references/mode-dispatch.yaml @@ -97,6 +97,46 @@ steps: replacement_script: gate-platform-migration.sh note: "Runs both platform checks and strangler plan verification." + # Steps 8-13 apply in every mode; mode-specific actions live in + # references/steps-8-13-by-mode.md (strangler modes loop 8-12 per zone). + # gate-postswap.sh verifies the grep-checkable Done-When signals for 8/12/13. + + S8_new_impl: + seam-injection: applies + platform-migration: applies + strangler-fig: applies + platform-strangler: applies + + S9_swap: + seam-injection: applies + platform-migration: applies + strangler-fig: applies + platform-strangler: applies + + S10_verification: + seam-injection: applies + platform-migration: applies + strangler-fig: applies + platform-strangler: applies + + S11_integration: + seam-injection: applies + platform-migration: applies + strangler-fig: applies + platform-strangler: applies + + S12_cleanup: + seam-injection: applies + platform-migration: applies + strangler-fig: applies + platform-strangler: applies + + S13_delete_legacy: + seam-injection: applies + platform-migration: applies + strangler-fig: applies + platform-strangler: applies + # Skip reasons written to state.yaml when dispatch == skip: # state.steps.SN.status = "skipped" # state.steps.SN.skip_reason = diff --git a/plugin/commands/refactor/scripts/gate-postswap.sh b/plugin/commands/refactor/scripts/gate-postswap.sh new file mode 100755 index 0000000..ca6f4f8 --- /dev/null +++ b/plugin/commands/refactor/scripts/gate-postswap.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +# gate-postswap.sh — deterministic Done-When checks for Playbook Steps 8, 12, 13. +# +# Turns the grep-checkable signals from SKILL.md's Steps 8-13 tables into exit +# codes, so "cleanup complete" is a gate, not a claim. Steps 9-11 are verified +# by test suites (re-run gate-step7.sh / your full suite) — not this script. +# +# Usage: +# gate-postswap.sh --module --step 8 --impl-file +# gate-postswap.sh --module --step 12 [--adapter-name ] [--seam-name ] [--skip-seam-check] +# gate-postswap.sh --module --step 13 +# +# Name resolution (when flags omitted): +# legacy class = basename of 1_target.yaml `file:` without extension +# adapter name = first class/struct/@interface identifier in 6_adapter.* artifact +# seam name = 5_interface.yaml protocol.name +# shadow logger = 5_interface.yaml shadow_config.logger_protocol (shadow strategy only) +# +# Exit codes: 0 pass · 1 usage · 2 state/artifact missing · 3 gate failed +set -uo pipefail + +PROJECT_ROOT="${PROJECT_ROOT:-$(pwd)}" + +MODULE="" STEP="" IMPL_FILE="" ADAPTER_NAME="" SEAM_NAME="" SKIP_SEAM=0 + +usage() { + grep '^# ' "$0" | sed 's/^# //' >&2 + exit 1 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --module) MODULE="$2"; shift 2 ;; + --step) STEP="$2"; shift 2 ;; + --impl-file) IMPL_FILE="$2"; shift 2 ;; + --adapter-name) ADAPTER_NAME="$2"; shift 2 ;; + --seam-name) SEAM_NAME="$2"; shift 2 ;; + --skip-seam-check) SKIP_SEAM=1; shift ;; + *) echo "error: unknown flag: $1" >&2; usage ;; + esac +done + +[[ -n "$MODULE" && -n "$STEP" ]] || usage + +state_dir="$PROJECT_ROOT/.sourceatlas/refactor/$MODULE" +target_yaml="$state_dir/1_target.yaml" +interface_yaml="$state_dir/5_interface.yaml" +[[ -d "$state_dir" ]] || { echo "error: no state dir at $state_dir" >&2; exit 2; } + +# Search excludes: vendored/build dirs plus the playbook's own artifacts, +# which legitimately mention every name we grep for. +GREP_EXCLUDES=(--exclude-dir=.git --exclude-dir=.sourceatlas --exclude-dir=Pods + --exclude-dir=node_modules --exclude-dir=build --exclude-dir=.build + --exclude-dir=DerivedData --exclude-dir=Carthage --exclude-dir=vendor) + +ref_count() { # ref_count → number of referencing lines in the project + grep -rn "${GREP_EXCLUDES[@]}" -- "$1" "$PROJECT_ROOT" 2>/dev/null | wc -l | tr -d ' ' +} + +yaml_value() { # yaml_value → first "key: value", quotes stripped + awk -v key="$2" '$0 ~ "^[[:space:]]*" key ":" { + sub("^[[:space:]]*" key ":[[:space:]]*", ""); gsub(/["'"'"']/, ""); print; exit + }' "$1" +} + +legacy_rel=$(yaml_value "$target_yaml" "file") +[[ -n "$legacy_rel" ]] || { echo "error: cannot read file: from $target_yaml" >&2; exit 2; } +legacy_file="$PROJECT_ROOT/$legacy_rel" +legacy_class=$(basename "$legacy_rel"); legacy_class="${legacy_class%.*}" + +pass=0; fail=0 +check() { # check